2026-04-05-add navigation page and correct whatsapp link

This commit is contained in:
Hamza-Ayed
2026-04-05 16:21:31 +03:00
parent 4d5800ff9b
commit 531c6d07ef
4 changed files with 1212 additions and 517 deletions

View File

@@ -2995,7 +2995,6 @@ class MapPassengerController extends GetxController {
Future<Map<String, double>?> extractCoordinatesFromLinkAsync( Future<Map<String, double>?> extractCoordinatesFromLinkAsync(
String link) async { String link) async {
try { try {
// 1. استخراج الرابط فقط من النص (في حال كان هناك نص مع الرابط في الواتساب)
int urlStartIndex = link.indexOf(RegExp(r'https?://')); int urlStartIndex = link.indexOf(RegExp(r'https?://'));
if (urlStartIndex == -1) return null; if (urlStartIndex == -1) return null;
String cleanLink = link.substring(urlStartIndex).trim(); String cleanLink = link.substring(urlStartIndex).trim();
@@ -3003,67 +3002,44 @@ class MapPassengerController extends GetxController {
Uri uri = Uri.parse(cleanLink); Uri uri = Uri.parse(cleanLink);
String finalUrl = cleanLink; String finalUrl = cleanLink;
// 2. فك الروابط المختصرة (Unshorten URLs) // فك التوجيه للروابط المختصرة
if (cleanLink.contains('goo.gl') || if (cleanLink.contains('goo.gl') ||
cleanLink.contains('maps.app.goo.gl')) { cleanLink.contains('maps.google.com')) {
try { try {
// نقوم بطلب HTTP عادي، وhttp يتبع التوجيه التلقائي (Redirects) var response =
var response = await http.get(uri); await http.get(uri).timeout(const Duration(seconds: 5));
// نأخذ الرابط النهائي بعد التوجيه
finalUrl = response.request?.url.toString() ?? cleanLink; finalUrl = response.request?.url.toString() ?? cleanLink;
} catch (e) { } catch (e) {
Log.print('Failed to follow redirect: $e'); Log.print('Redirect logic failed, using original: $e');
} }
} }
Log.print('Final Unshortened URL: $finalUrl'); // الأنماط المشتركة لخرائط جوجل (تكون دائماً Lat ثم Lng)
RegExp regex = RegExp(r'(-?\d+\.\d+)[,/~](-?\d+\.\d+)');
var match = regex.firstMatch(finalUrl);
// 3. استخراج الإحداثيات باستخدام تعبيرات نمطية (Regex) قوية تغطي خرائط جوجل وغيرها if (match != null) {
double lat = double.parse(match.group(1)!);
double lng = double.parse(match.group(2)!);
// 🔥 منطق التصحيح الذاتي (Smart Swap) للمنطقة (سوريا/الأردن/مصر)
// إذا كان الرقم الأول أكبر من الرقم الثاني بشكل واضح، فهذا يعني أن الرابط مقلوب أو أننا نحتاج للتأكد
// في منطقتنا Latitude حوالي 30-35 و Longitude حوالي 36-44
if (lat > 40 && lat > lng) {
Log.print("⚠️ Detected Swapped Coordinates in Link. Correcting...");
double temp = lat;
lat = lng;
lng = temp;
}
// النمط الأول: @lat,lng (الأكثر شيوعاً في خرائط جوجل)
RegExp regexAt = RegExp(r'@(-?\d+\.\d+),(-?\d+\.\d+)');
var matchAt = regexAt.firstMatch(finalUrl);
if (matchAt != null) {
return { return {
'latitude': double.parse(matchAt.group(1)!), 'latitude': lat,
'longitude': double.parse(matchAt.group(2)!), 'longitude': lng,
};
}
// النمط الثاني: q=lat,lng أو ll=lat,lng أو query=lat,lng
RegExp regexQuery =
RegExp(r'(?:q|ll|query)=(-?\d+\.\d+)[,~](-?\d+\.\d+)');
var matchQuery = regexQuery.firstMatch(finalUrl);
if (matchQuery != null) {
return {
'latitude': double.parse(matchQuery.group(1)!),
'longitude': double.parse(matchQuery.group(2)!),
};
}
// النمط الثالث: search/lat,lng (موجود في بعض أشكال خرائط جوجل)
RegExp regexSearch = RegExp(r'search/(-?\d+\.\d+),(-?\d+\.\d+)');
var matchSearch = regexSearch.firstMatch(finalUrl);
if (matchSearch != null) {
return {
'latitude': double.parse(matchSearch.group(1)!),
'longitude': double.parse(matchSearch.group(2)!),
};
}
// النمط الرابع: place/lat,lng (غالباً متواجد في الروابط المشتركة من خرائط جوجل)
RegExp regexPlace = RegExp(r'place/(-?\d+\.\d+),(-?\d+\.\d+)');
var matchPlace = regexPlace.firstMatch(finalUrl);
if (matchPlace != null) {
return {
'latitude': double.parse(matchPlace.group(1)!),
'longitude': double.parse(matchPlace.group(2)!),
}; };
} }
} catch (e) { } catch (e) {
Log.print('Error parsing location link: $e'); Log.print('Error parsing location link: $e');
} }
return null; return null;
} }
@@ -3087,16 +3063,36 @@ class MapPassengerController extends GetxController {
void goToWhatappLocation() async { void goToWhatappLocation() async {
if (sosFormKey.currentState!.validate()) { if (sosFormKey.currentState!.validate()) {
changeIsWhatsAppOrder(true); // 1. استخراج الإحداثيات أولاً بشكل محلي لضمان عدم حدوث سباق بيانات (Race Condition)
Get.back(); Map<String, double>? coordinates =
handleWhatsAppLink(whatsAppLocationText.text); await extractCoordinatesFromLinkAsync(whatsAppLocationText.text);
myDestination = LatLng(latitudeWhatsApp, longitudeWhatsApp);
await mapController?.animateCamera(CameraUpdate.newLatLng( if (coordinates != null) {
LatLng(passengerLocation.latitude, passengerLocation.longitude))); latitudeWhatsApp = coordinates['latitude']!;
changeMainBottomMenuMap(); longitudeWhatsApp = coordinates['longitude']!;
passengerStartLocationFromMap = true;
isPickerShown = true; Log.print(
update(); '📍 Final Coordinates for OSM: Lat: $latitudeWhatsApp, Lng: $longitudeWhatsApp');
changeIsWhatsAppOrder(true);
Get.back();
// إعداد الوجهة
myDestination = LatLng(latitudeWhatsApp, longitudeWhatsApp);
// تحريك الكاميرا لموقع الراكب (البداية) وليس الوجهة فوراً لضمان تحميل الخريطة
if (passengerLocation != null) {
await mapController?.animateCamera(CameraUpdate.newLatLng(
LatLng(passengerLocation.latitude, passengerLocation.longitude)));
}
changeMainBottomMenuMap();
passengerStartLocationFromMap = true;
isPickerShown = true;
update();
} else {
mySnackbarWarning('لم نتمكن من استخراج الموقع من الرابط');
}
} }
} }

View File

@@ -44,7 +44,7 @@ GetBuilder<MapPassengerController> leftMainMenuIcons() {
children: [ children: [
// --- تم استخدام دالة مساعدة جديدة للزر --- // --- تم استخدام دالة مساعدة جديدة للزر ---
_buildMapActionButton( _buildMapActionButton(
icon: Icons.satellite_alt_outlined, icon: Icons.near_me_outlined,
tooltip: 'Toggle Map Type', tooltip: 'Toggle Map Type',
onPressed: () => Get.to(() => NavigationView()), onPressed: () => Get.to(() => NavigationView()),
), ),

View File

@@ -7,20 +7,37 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; // Replaced Google Maps import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../../constant/box_name.dart'; import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../constant/country_polygons.dart';
import '../../../constant/links.dart'; import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart'; import '../../../controller/functions/crud.dart';
import '../../../controller/functions/tts.dart'; import '../../../controller/functions/tts.dart';
import '../../../controller/home/decode_polyline_isolate.dart'; import '../../../controller/home/decode_polyline_isolate.dart';
import '../../../env/env.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../../print.dart'; import '../../../print.dart';
class NavigationController extends GetxController { class NavigationController extends GetxController {
// ==========================================================================
// ── Tunables ──────────────────────────────────────────────────────────────
// ==========================================================================
/// How often we snapshot the current position into the local buffer.
static const Duration _recordInterval = Duration(seconds: 3);
/// How often we flush the buffer and POST it to the server.
static const Duration _uploadInterval = Duration(minutes: 2);
/// Minimum metres the device must move before we bother recording a point.
static const double _minMoveToRecord = 10.0;
/// Minimum metres the device must move between general location ticks.
static const double _minMoveToProcess = 2.0;
// ==========================================================================
// ── Map state ─────────────────────────────────────────────────────────────
// ==========================================================================
bool isLoading = false; bool isLoading = false;
MaplibreMapController? mapController; MaplibreMapController? mapController;
bool isStyleLoaded = false; bool isStyleLoaded = false;
@@ -29,36 +46,88 @@ class NavigationController extends GetxController {
LatLng? myLocation; LatLng? myLocation;
double heading = 0.0; double heading = 0.0;
double currentSpeed = 0.0; // km/h
double totalDistance = 0.0; // metres accumulated this session
// MapLibre Object Tracking // MapLibre objects
Symbol? carSymbol; Symbol? carSymbol;
Symbol? destinationSymbol; Symbol? destinationSymbol;
Line? remainingRouteLine; Line? remainingRouteLine;
Line? traveledRouteLine; Line? traveledRouteLine;
// General location polling
Timer? _locationUpdateTimer; Timer? _locationUpdateTimer;
final Duration _currentUpdateInterval = const Duration(seconds: 1); LatLng? _lastProcessedLocation;
LatLng? _lastRecordedLocation;
// Search
List<dynamic> placesDestination = []; List<dynamic> placesDestination = [];
Timer? _debounce; Timer? _debounce;
// Route
LatLng? _finalDestination; LatLng? _finalDestination;
List<Map<String, dynamic>> routeSteps = []; List<Map<String, dynamic>> routeSteps = [];
List<LatLng> _fullRouteCoordinates = []; List<LatLng> _fullRouteCoordinates = [];
int _lastTraveledIndexInFullRoute = 0; int _lastTraveledIndexInFullRoute = 0;
// Navigation guidance
bool _nextInstructionSpoken = false; bool _nextInstructionSpoken = false;
String currentInstruction = ""; String currentInstruction = "";
String nextInstruction = ""; String nextInstruction = "";
int currentStepIndex = 0; int currentStepIndex = 0;
double currentSpeed = 0.0;
String distanceToNextStep = ""; String distanceToNextStep = "";
String totalDistanceRemaining = "";
String estimatedTimeRemaining = "";
// Stored route totals (for ETA re-calculation)
double _routeTotalDistanceM = 0;
double _routeTotalDurationS = 0;
// Camera
bool isNavigating = false;
bool _cameraLockedToUser = true;
// ==========================================================================
// ── Batch location tracking ───────────────────────────────────────────────
// ==========================================================================
/// In-memory ring buffer — points accumulate here every 3 s.
final List<Map<String, dynamic>> _trackBuffer = [];
Timer? _recordTimer;
Timer? _uploadBatchTimer;
/// Last position that was written to the buffer (for distance gate).
LatLng? _lastBufferedLocation;
DateTime? _lastBufferedTime;
/// Last position used to accumulate `totalDistance`.
LatLng? _lastDistanceLocation;
// ==========================================================================
// ── Speed-adaptive camera ─────────────────────────────────────────────────
// ==========================================================================
double get _targetZoom {
if (currentSpeed < 15) return 19.0;
if (currentSpeed < 40) return 18.0;
if (currentSpeed < 70) return 17.0;
if (currentSpeed < 100) return 16.0;
return 15.0;
}
double get _targetTilt {
if (currentSpeed < 10) return 0.0;
if (currentSpeed < 40) return 40.0;
return 55.0;
}
static final String _routeApiBaseUrl = static final String _routeApiBaseUrl =
"${AppLink.routesOsm}/route/v1/driving"; "${AppLink.routesOsm}/route/v1/driving";
// ==========================================================================
// ── Lifecycle ─────────────────────────────────────────────────────────────
// ==========================================================================
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@@ -75,15 +144,20 @@ class NavigationController extends GetxController {
@override @override
void onClose() { void onClose() {
_locationUpdateTimer?.cancel(); _locationUpdateTimer?.cancel();
mapController?.dispose(); _recordTimer?.cancel();
_uploadBatchTimer?.cancel();
_debounce?.cancel(); _debounce?.cancel();
mapController?.dispose();
placeDestinationController.dispose(); placeDestinationController.dispose();
// Final flush before closing so no points are lost
_flushBufferToServer();
super.onClose(); super.onClose();
} }
// ======================================================================= // ==========================================================================
// Map Initialization & Callbacks // ── Map callbacks ─────────────────────────────────────────────────────────
// ======================================================================= // ==========================================================================
void onMapCreated(MaplibreMapController controller) { void onMapCreated(MaplibreMapController controller) {
mapController = controller; mapController = controller;
@@ -104,18 +178,26 @@ class NavigationController extends GetxController {
} }
Future<void> onMapLongPressed(Point<double> point, LatLng tappedPoint) async { Future<void> onMapLongPressed(Point<double> point, LatLng tappedPoint) async {
HapticFeedback.mediumImpact();
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
title: const Text('بدء الملاحة؟'), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
content: const Text('هل تريد الذهاب إلى هذا الموقع المحدد؟'), title: const Text('بدء الملاحة؟',
actionsAlignment: MainAxisAlignment.spaceBetween, style: TextStyle(fontWeight: FontWeight.bold)),
content: const Text('هل تريد الذهاب إلى هذا الموقع؟'),
actions: [ actions: [
TextButton( TextButton(
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)), child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
onPressed: () => Get.back(), onPressed: () => Get.back(),
), ),
TextButton( ElevatedButton(
child: const Text('اذهب الآن'), style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0D47A1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child:
const Text('اذهب الآن', style: TextStyle(color: Colors.white)),
onPressed: () { onPressed: () {
Get.back(); Get.back();
startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد'); startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد');
@@ -126,18 +208,19 @@ class NavigationController extends GetxController {
); );
} }
// ======================================================================= // ==========================================================================
// Location Management // ── Location polling (every second) ──────────────────────────────────────
// ======================================================================= // ==========================================================================
Future<void> _getCurrentLocationAndStartUpdates() async { Future<void> _getCurrentLocationAndStartUpdates() async {
try { try {
Position position = await Geolocator.getCurrentPosition( final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high); desiredAccuracy: LocationAccuracy.high);
myLocation = LatLng(position.latitude, position.longitude); myLocation = LatLng(position.latitude, position.longitude);
update(); update();
if (isStyleLoaded) animateCameraToPosition(myLocation!); if (isStyleLoaded) animateCameraToPosition(myLocation!);
_startLocationTimer(); _startLocationTimer();
_startBatchTimers(); // ← start tracking as soon as we have a fix
} catch (e) { } catch (e) {
Log.print("Error getting initial location: $e"); Log.print("Error getting initial location: $e");
} }
@@ -145,45 +228,159 @@ class NavigationController extends GetxController {
void _startLocationTimer() { void _startLocationTimer() {
_locationUpdateTimer?.cancel(); _locationUpdateTimer?.cancel();
_locationUpdateTimer = Timer.periodic(_currentUpdateInterval, (timer) { _locationUpdateTimer =
_updateLocationAndProcess(); Timer.periodic(const Duration(seconds: 1), (_) => _tick());
});
} }
Future<void> _updateLocationAndProcess() async { Future<void> _tick() async {
try { try {
final position = await Geolocator.getCurrentPosition( final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high); desiredAccuracy: LocationAccuracy.high);
final newLoc = LatLng(position.latitude, position.longitude); final newLoc = LatLng(position.latitude, position.longitude);
if (_lastRecordedLocation != null) { // Gate: ignore micro-jitter
double dist = Geolocator.distanceBetween( if (_lastProcessedLocation != null) {
newLoc.latitude, final d = Geolocator.distanceBetween(
newLoc.longitude, newLoc.latitude,
_lastRecordedLocation!.latitude, newLoc.longitude,
_lastRecordedLocation!.longitude); _lastProcessedLocation!.latitude,
if (dist < 2.0) return; _lastProcessedLocation!.longitude,
);
if (d < _minMoveToProcess) return;
} }
// Accumulate session distance
if (_lastDistanceLocation != null) {
final d = Geolocator.distanceBetween(
_lastDistanceLocation!.latitude,
_lastDistanceLocation!.longitude,
newLoc.latitude,
newLoc.longitude,
);
if (d > 5.0) totalDistance += d;
}
_lastDistanceLocation = newLoc;
myLocation = newLoc; myLocation = newLoc;
_lastRecordedLocation = newLoc; _lastProcessedLocation = newLoc;
heading = position.heading; heading = position.heading;
currentSpeed = position.speed * 3.6; currentSpeed = position.speed * 3.6;
if (isStyleLoaded) _updateCarMarker(); if (isStyleLoaded) _updateCarMarker();
if (_fullRouteCoordinates.isNotEmpty) { if (_fullRouteCoordinates.isNotEmpty) {
animateCameraToPosition(myLocation!, bearing: heading, zoom: 18.0); if (_cameraLockedToUser) {
animateCameraToPosition(myLocation!,
bearing: heading, zoom: _targetZoom, tilt: _targetTilt);
}
_updateTraveledPolylineSmart(myLocation!); _updateTraveledPolylineSmart(myLocation!);
_checkNavigationStep(myLocation!); _checkNavigationStep(myLocation!);
_recomputeETA();
} }
update(); update();
} catch (_) {}
}
// ==========================================================================
// ── Batch tracking: record every 3 s, upload every 2 min ─────────────────
// ==========================================================================
void _startBatchTimers() {
_recordTimer?.cancel();
_uploadBatchTimer?.cancel();
_recordTimer = Timer.periodic(_recordInterval, (_) => _recordToBuffer());
_uploadBatchTimer =
Timer.periodic(_uploadInterval, (_) => _flushBufferToServer());
Log.print('📍 Batch tracking started '
'(record: ${_recordInterval.inSeconds}s, '
'upload: ${_uploadInterval.inMinutes}min)');
}
void _stopBatchTimers() {
_recordTimer?.cancel();
_uploadBatchTimer?.cancel();
_recordTimer = null;
_uploadBatchTimer = null;
}
/// Called every 3 seconds. Adds one point to the buffer if the device
/// has moved enough OR if 60 s have elapsed since the last record.
void _recordToBuffer() {
if (myLocation == null) return;
if (myLocation!.latitude == 0 && myLocation!.longitude == 0) return;
final now = DateTime.now();
// Distance gate
final distFromLast = _lastBufferedLocation == null
? 999.0
: Geolocator.distanceBetween(
_lastBufferedLocation!.latitude,
_lastBufferedLocation!.longitude,
myLocation!.latitude,
myLocation!.longitude,
);
final bool moved = distFromLast > _minMoveToRecord && currentSpeed > 0.5;
final bool timeForced = _lastBufferedTime == null ||
now.difference(_lastBufferedTime!).inSeconds >= 60;
if (!moved && !timeForced) return;
_lastBufferedLocation = myLocation;
_lastBufferedTime = now;
final point = {
'lat': double.parse(myLocation!.latitude.toStringAsFixed(6)),
'lng': double.parse(myLocation!.longitude.toStringAsFixed(6)),
'spd': double.parse(currentSpeed.toStringAsFixed(1)),
'head': heading.toStringAsFixed(0),
'dist': double.parse(totalDistance.toStringAsFixed(1)),
'ts': now.toIso8601String(),
};
_trackBuffer.add(point);
Log.print('📌 Buffered point #${_trackBuffer.length} '
'(${point['lat']}, ${point['lng']}) ${point['spd']} km/h');
}
/// Drains the buffer and POSTs the JSON batch to the server.
/// Called every 2 minutes (and once more on close).
Future<void> _flushBufferToServer() async {
if (_trackBuffer.isEmpty) return;
final batch = List<Map<String, dynamic>>.from(_trackBuffer);
_trackBuffer.clear();
final String passengerId = (box.read(BoxName.passengerID) ?? '').toString();
Log.print('📤 Uploading ${batch.length} tracking points '
'for passenger $passengerId...');
try {
await CRUD().post(
link: '${AppLink.locationServerSide}/add_batch.php',
payload: {
'driver_id': passengerId,
'batch_data': jsonEncode(batch),
'session_dist': totalDistance.toStringAsFixed(1),
},
);
Log.print('✅ Batch uploaded successfully.');
} catch (e) { } catch (e) {
// Log.print("Loc update error: $e"); // Put the points back so they are retried on the next cycle
_trackBuffer.insertAll(0, batch);
Log.print('❌ Batch upload failed points kept for retry: $e');
} }
} }
// ==========================================================================
// ── Car marker ────────────────────────────────────────────────────────────
// ==========================================================================
Future<void> _updateCarMarker() async { Future<void> _updateCarMarker() async {
if (myLocation == null || mapController == null || !isStyleLoaded) return; if (myLocation == null || mapController == null || !isStyleLoaded) return;
@@ -196,60 +393,84 @@ class NavigationController extends GetxController {
)); ));
} else { } else {
mapController!.updateSymbol( mapController!.updateSymbol(
carSymbol!, carSymbol!,
SymbolOptions( SymbolOptions(geometry: myLocation, iconRotate: heading),
geometry: myLocation, );
iconRotate: heading,
));
} }
} }
// ==========================================================================
// ── Camera ────────────────────────────────────────────────────────────────
// ==========================================================================
void animateCameraToPosition(LatLng position, void animateCameraToPosition(LatLng position,
{double zoom = 17.0, double bearing = 0.0}) { {double? zoom, double bearing = 0.0, double tilt = 0.0}) {
mapController?.animateCamera( mapController?.animateCamera(
CameraUpdate.newCameraPosition( CameraUpdate.newCameraPosition(
CameraPosition( CameraPosition(
target: position, zoom: zoom, bearing: bearing, tilt: 45.0), target: position,
zoom: zoom ?? (isNavigating ? _targetZoom : 16.0),
bearing: bearing,
tilt: tilt,
),
), ),
); );
} }
// ======================================================================= void onUserPanned() {
// Route Management _cameraLockedToUser = false;
// ======================================================================= update();
}
void relockCameraToUser() {
_cameraLockedToUser = true;
if (myLocation != null) {
animateCameraToPosition(myLocation!,
bearing: heading, zoom: _targetZoom, tilt: _targetTilt);
}
update();
}
bool get isCameraLocked => _cameraLockedToUser;
// ==========================================================================
// ── Route polylines ───────────────────────────────────────────────────────
// ==========================================================================
void _updateTraveledPolylineSmart(LatLng currentPos) { void _updateTraveledPolylineSmart(LatLng currentPos) {
if (_fullRouteCoordinates.isEmpty) return; if (_fullRouteCoordinates.isEmpty) return;
int searchWindow = 60; const int searchWindow = 60;
int startIndex = _lastTraveledIndexInFullRoute; final int startIndex = _lastTraveledIndexInFullRoute;
int endIndex = min(startIndex + searchWindow, _fullRouteCoordinates.length); final int endIndex =
min(startIndex + searchWindow, _fullRouteCoordinates.length);
double minDistance = double.infinity; double minDist = double.infinity;
int closestIndex = startIndex; int closestIdx = startIndex;
bool foundCloser = false; bool foundCloser = false;
for (int i = startIndex; i < endIndex; i++) { for (int i = startIndex; i < endIndex; i++) {
final point = _fullRouteCoordinates[i]; final d = Geolocator.distanceBetween(
final dist = Geolocator.distanceBetween(currentPos.latitude, currentPos.latitude,
currentPos.longitude, point.latitude, point.longitude); currentPos.longitude,
_fullRouteCoordinates[i].latitude,
if (dist < minDistance) { _fullRouteCoordinates[i].longitude,
minDistance = dist; );
closestIndex = i; if (d < minDist) {
minDist = d;
closestIdx = i;
foundCloser = true; foundCloser = true;
} }
} }
if (foundCloser && if (foundCloser &&
minDistance < 50 && minDist < 50 &&
closestIndex > _lastTraveledIndexInFullRoute) { closestIdx > _lastTraveledIndexInFullRoute) {
_lastTraveledIndexInFullRoute = closestIndex; _lastTraveledIndexInFullRoute = closestIdx;
final remaining = _updatePolylinesSets(
_fullRouteCoordinates.sublist(_lastTraveledIndexInFullRoute); _fullRouteCoordinates.sublist(0, closestIdx + 1),
final traveled = _fullRouteCoordinates.sublist(closestIdx),
_fullRouteCoordinates.sublist(0, _lastTraveledIndexInFullRoute + 1); );
_updatePolylinesSets(traveled, remaining);
} }
} }
@@ -265,8 +486,8 @@ class NavigationController extends GetxController {
if (remaining.isNotEmpty) { if (remaining.isNotEmpty) {
remainingRouteLine = await mapController!.addLine(LineOptions( remainingRouteLine = await mapController!.addLine(LineOptions(
geometry: remaining, geometry: remaining,
lineColor: '#0D47A1', lineColor: '#1A73E8',
lineWidth: 6.0, lineWidth: 7.0,
lineJoin: 'round', lineJoin: 'round',
)); ));
} }
@@ -275,20 +496,21 @@ class NavigationController extends GetxController {
traveledRouteLine = await mapController!.addLine(LineOptions( traveledRouteLine = await mapController!.addLine(LineOptions(
geometry: traveled, geometry: traveled,
lineColor: '#BDBDBD', lineColor: '#BDBDBD',
lineWidth: 6.0, lineWidth: 5.0,
lineJoin: 'round', lineJoin: 'round',
lineOpacity: 0.6,
)); ));
} }
} }
// ======================================================================= // ==========================================================================
// Routing API & Navigation // ── Routing API ───────────────────────────────────────────────────────────
// ======================================================================= // ==========================================================================
Future<void> getRoute(LatLng origin, LatLng destination) async { Future<void> getRoute(LatLng origin, LatLng destination) async {
String coords = final coords = "${origin.longitude},${origin.latitude};"
"${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}"; "${destination.longitude},${destination.latitude}";
String url = final url =
"$_routeApiBaseUrl/$coords?steps=true&overview=full&geometries=polyline"; "$_routeApiBaseUrl/$coords?steps=true&overview=full&geometries=polyline";
try { try {
@@ -298,36 +520,47 @@ class NavigationController extends GetxController {
return; return;
} }
final responseData = jsonDecode(response.body); final data = jsonDecode(response.body);
if (responseData['code'] != 'Ok' || if (data['code'] != 'Ok' || (data['routes'] as List).isEmpty) {
(responseData['routes'] as List).isEmpty) {
mySnackbarWarning('لم يتم العثور على مسار.'); mySnackbarWarning('لم يتم العثور على مسار.');
return; return;
} }
var route = responseData['routes'][0]; final route = data['routes'][0];
final pointsString = route['geometry'];
// فك تشفير Polyline بطريقة آمنة نوعياً (Type-Safe)
_fullRouteCoordinates = await compute<String, List<LatLng>>( _fullRouteCoordinates = await compute<String, List<LatLng>>(
decodePolylineIsolate, pointsString.toString()); decodePolylineIsolate, route['geometry'].toString());
_lastTraveledIndexInFullRoute = 0; _lastTraveledIndexInFullRoute = 0;
if (isStyleLoaded) _updatePolylinesSets([], _fullRouteCoordinates); if (isStyleLoaded) _updatePolylinesSets([], _fullRouteCoordinates);
var legs = route['legs'] as List; final legs = route['legs'] as List;
if (legs.isNotEmpty) { if (legs.isNotEmpty) {
var steps = legs[0]['steps'] as List; routeSteps = List<Map<String, dynamic>>.from(legs[0]['steps'] as List);
routeSteps = List<Map<String, dynamic>>.from(steps);
_routeTotalDistanceM = (legs[0]['distance'] as num).toDouble();
_routeTotalDurationS = (legs[0]['duration'] as num).toDouble();
totalDistanceRemaining = _routeTotalDistanceM > 1000
? "${(_routeTotalDistanceM / 1000).toStringAsFixed(1)} كم"
: "${_routeTotalDistanceM.toStringAsFixed(0)} م";
final minutes = (_routeTotalDurationS / 60).round();
estimatedTimeRemaining = minutes > 60
? "${(minutes / 60).floor()} س ${minutes % 60} د"
: "$minutes د";
} else { } else {
routeSteps = []; routeSteps = [];
} }
for (var step in routeSteps) { for (final step in routeSteps) {
step['instruction_text'] = _createInstructionFromManeuver(step); step['instruction_text'] = _createInstructionFromManeuver(step);
} }
currentStepIndex = 0; currentStepIndex = 0;
_nextInstructionSpoken = false; _nextInstructionSpoken = false;
isNavigating = true;
_cameraLockedToUser = true;
if (routeSteps.isNotEmpty) { if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['instruction_text']; currentInstruction = routeSteps[0]['instruction_text'];
nextInstruction = routeSteps.length > 1 nextInstruction = routeSteps.length > 1
@@ -339,17 +572,35 @@ class NavigationController extends GetxController {
if (_fullRouteCoordinates.isNotEmpty) { if (_fullRouteCoordinates.isNotEmpty) {
final bounds = _boundsFromLatLngList(_fullRouteCoordinates); final bounds = _boundsFromLatLngList(_fullRouteCoordinates);
mapController?.animateCamera(CameraUpdate.newLatLngBounds(bounds, mapController?.animateCamera(CameraUpdate.newLatLngBounds(bounds,
bottom: 200, top: 150, left: 50, right: 50)); bottom: 220, top: 150, left: 50, right: 50));
} }
update(); update();
} catch (e) { } catch (e) {
Log.print("GetRoute Error: $e"); Log.print("GetRoute Error: $e");
Get.snackbar('خطأ', 'حدث خطأ غير متوقع.');
} }
} }
// --- Map Object Handlers --- // Crude but fast ETA re-estimate based on fraction of route remaining.
void _recomputeETA() {
if (_routeTotalDistanceM == 0 || _fullRouteCoordinates.isEmpty) return;
final fraction =
(_fullRouteCoordinates.length - _lastTraveledIndexInFullRoute) /
_fullRouteCoordinates.length;
final remainingM = _routeTotalDistanceM * fraction;
final remainingS = _routeTotalDurationS * fraction;
totalDistanceRemaining = remainingM > 1000
? "${(remainingM / 1000).toStringAsFixed(1)} كم"
: "${remainingM.toStringAsFixed(0)} م";
final minutes = (remainingS / 60).round();
estimatedTimeRemaining = minutes > 60
? "${(minutes / 60).floor()} س ${minutes % 60} د"
: "$minutes د";
}
Future<void> startNavigationTo(LatLng destination, Future<void> startNavigationTo(LatLng destination,
{String infoWindowTitle = ''}) async { {String infoWindowTitle = ''}) async {
@@ -380,8 +631,7 @@ class NavigationController extends GetxController {
if (myLocation == null || _finalDestination == null || isLoading) return; if (myLocation == null || _finalDestination == null || isLoading) return;
isLoading = true; isLoading = true;
update(); update();
Get.snackbar('إعادة التوجيه', 'جاري حساب مسار جديد...', mySnackbarInfo('جاري حساب مسار جديد...');
backgroundColor: AppColor.goldenBronze);
await getRoute(myLocation!, _finalDestination!); await getRoute(myLocation!, _finalDestination!);
isLoading = false; isLoading = false;
update(); update();
@@ -402,6 +652,10 @@ class NavigationController extends GetxController {
traveledRouteLine = null; traveledRouteLine = null;
} }
_finalDestination = null; _finalDestination = null;
isNavigating = false;
// Flush whatever is in the buffer when navigation ends
await _flushBufferToServer();
} }
routeSteps.clear(); routeSteps.clear();
_fullRouteCoordinates.clear(); _fullRouteCoordinates.clear();
@@ -409,39 +663,42 @@ class NavigationController extends GetxController {
currentInstruction = ""; currentInstruction = "";
nextInstruction = ""; nextInstruction = "";
distanceToNextStep = ""; distanceToNextStep = "";
totalDistanceRemaining = "";
estimatedTimeRemaining = "";
_routeTotalDistanceM = 0;
_routeTotalDurationS = 0;
update(); update();
} }
Future<void> _loadCustomIcons() async { Future<void> _loadCustomIcons() async {
if (mapController == null) return; if (mapController == null) return;
final carBytes = await rootBundle.load('assets/images/car.png');
final ByteData carBytes = await rootBundle.load('assets/images/car.png'); final destBytes = await rootBundle.load('assets/images/b.png');
final Uint8List carList = carBytes.buffer.asUint8List(); await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
await mapController!.addImage('car_icon', carList); await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
final ByteData destBytes = await rootBundle.load('assets/images/b.png');
final Uint8List destList = destBytes.buffer.asUint8List();
await mapController!.addImage('dest_icon', destList);
} }
// --- Step Tracking & Instructions (Omitted unchanged logic to save space, retain your existing string matchers) --- // ==========================================================================
void _checkNavigationStep(LatLng currentPosition) { // ── Step tracking & TTS ───────────────────────────────────────────────────
// ==========================================================================
void _checkNavigationStep(LatLng pos) {
if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length) return; if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length) return;
final step = routeSteps[currentStepIndex];
final maneuver = step['maneuver']; final maneuver = routeSteps[currentStepIndex]['maneuver'];
final List<dynamic> location = maneuver['location']; final loc = maneuver['location'] as List;
final endLatLng = LatLng(location[1], location[0]); final endLatLng = LatLng(loc[1] as double, loc[0] as double);
final distance = Geolocator.distanceBetween( final distance = Geolocator.distanceBetween(
currentPosition.latitude, pos.latitude,
currentPosition.longitude, pos.longitude,
endLatLng.latitude, endLatLng.latitude,
endLatLng.longitude, endLatLng.longitude,
); );
distanceToNextStep = distance > 1000 distanceToNextStep = distance > 1000
? "${(distance / 1000).toStringAsFixed(1)} كم" ? "${(distance / 1000).toStringAsFixed(1)} كم"
: "${distance.toStringAsFixed(0)} متر"; : "${distance.toStringAsFixed(0)} م";
if (distance < 50 && if (distance < 50 &&
!_nextInstructionSpoken && !_nextInstructionSpoken &&
@@ -471,44 +728,26 @@ class NavigationController extends GetxController {
currentInstruction = "لقد وصلت إلى وجهتك"; currentInstruction = "لقد وصلت إلى وجهتك";
nextInstruction = ""; nextInstruction = "";
distanceToNextStep = ""; distanceToNextStep = "";
isNavigating = false;
Get.find<TextToSpeechController>().speakText(currentInstruction); Get.find<TextToSpeechController>().speakText(currentInstruction);
// Final flush on arrival
_flushBufferToServer();
update(); update();
} }
String _createInstructionFromManeuver(Map<String, dynamic> step) { String _createInstructionFromManeuver(Map<String, dynamic> step) {
if (step['maneuver'] == null) return "تابع المسير"; if (step['maneuver'] == null) return "تابع المسير";
final maneuver = step['maneuver']; final type = step['maneuver']['type'] ?? 'continue';
final type = maneuver['type'] ?? 'continue'; final modifier = step['maneuver']['modifier'] ?? 'straight';
final modifier = maneuver['modifier'] ?? 'straight';
final name = step['name'] ?? ''; final name = step['name'] ?? '';
String instruction = "";
switch (type) { String instruction = type == 'depart'
case 'depart': ? "انطلق"
instruction = "انطلق"; : type == 'arrive'
break; ? "لقد وصلت إلى وجهتك، $name"
case 'arrive': : _getTurnInstruction(modifier);
return "لقد وصلت إلى وجهتك، $name";
case 'turn':
case 'fork':
case 'roundabout':
case 'merge':
case 'on ramp':
case 'off ramp':
case 'end of road':
instruction = _getTurnInstruction(modifier);
break;
case 'new name':
instruction = "تابع المسير";
break;
default:
instruction = "تابع المسير";
}
if (name.isNotEmpty) if (name.isNotEmpty && type != 'arrive') instruction += " نحو $name";
instruction += (type == 'new name' || type == 'continue')
? " على $name"
: " نحو $name";
return instruction; return instruction;
} }
@@ -535,7 +774,10 @@ class NavigationController extends GetxController {
} }
} }
// --- Search & Utils (Retained entirely, no map logic here) --- // ==========================================================================
// ── Search ────────────────────────────────────────────────────────────────
// ==========================================================================
Future<void> getPlaces() async { Future<void> getPlaces() async {
final q = placeDestinationController.text.trim(); final q = placeDestinationController.text.trim();
if (q.length < 3) { if (q.length < 3) {
@@ -548,6 +790,7 @@ class NavigationController extends GetxController {
final lat = myLocation!.latitude; final lat = myLocation!.latitude;
final lng = myLocation!.longitude; final lng = myLocation!.longitude;
const radiusKm = 200.0; const radiusKm = 200.0;
final payload = { final payload = {
'query': q, 'query': q,
'lat_min': (lat - _kmToLatDelta(radiusKm)).toString(), 'lat_min': (lat - _kmToLatDelta(radiusKm)).toString(),
@@ -568,9 +811,8 @@ class NavigationController extends GetxController {
return; return;
for (final p in list) { for (final p in list) {
final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0; final plat = double.tryParse(p['latitude']?.toString() ?? '0') ?? 0.0;
final plng = final plng = double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0;
double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0;
p['distanceKm'] = _haversineKm(lat, lng, plat, plng); p['distanceKm'] = _haversineKm(lat, lng, plat, plng);
} }
@@ -579,15 +821,15 @@ class NavigationController extends GetxController {
placesDestination = list; placesDestination = list;
update(); update();
} catch (e) { } catch (e) {
print('Exception in getPlaces: $e'); Log.print('getPlaces error: $e');
} }
} }
Future<void> selectDestination(dynamic place) async { Future<void> selectDestination(dynamic place) async {
placeDestinationController.clear(); placeDestinationController.clear();
placesDestination = []; placesDestination = [];
final double lat = double.parse(place['latitude'].toString()); final lat = double.parse(place['latitude'].toString());
final double lng = double.parse(place['longitude'].toString()); final lng = double.parse(place['longitude'].toString());
await startNavigationTo(LatLng(lat, lng), await startNavigationTo(LatLng(lat, lng),
infoWindowTitle: place['name'] ?? 'وجهة'); infoWindowTitle: place['name'] ?? 'وجهة');
} }
@@ -597,6 +839,10 @@ class NavigationController extends GetxController {
_debounce = Timer(const Duration(milliseconds: 700), () => getPlaces()); _debounce = Timer(const Duration(milliseconds: 700), () => getPlaces());
} }
// ==========================================================================
// ── Geo utils ─────────────────────────────────────────────────────────────
// ==========================================================================
double _haversineKm(double lat1, double lon1, double lat2, double lon2) { double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
const R = 6371.0; const R = 6371.0;
final dLat = (lat2 - lat1) * (pi / 180.0); final dLat = (lat2 - lat1) * (pi / 180.0);
@@ -615,15 +861,15 @@ class NavigationController extends GetxController {
LatLngBounds _boundsFromLatLngList(List<LatLng> list) { LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
double? x0, x1, y0, y1; double? x0, x1, y0, y1;
for (LatLng latLng in list) { for (final ll in list) {
if (x0 == null) { if (x0 == null) {
x0 = x1 = latLng.latitude; x0 = x1 = ll.latitude;
y0 = y1 = latLng.longitude; y0 = y1 = ll.longitude;
} else { } else {
if (latLng.latitude > x1!) x1 = latLng.latitude; if (ll.latitude > x1!) x1 = ll.latitude;
if (latLng.latitude < x0) x0 = latLng.latitude; if (ll.latitude < x0) x0 = ll.latitude;
if (latLng.longitude > y1!) y1 = latLng.longitude; if (ll.longitude > y1!) y1 = ll.longitude;
if (latLng.longitude < y0!) y0 = latLng.longitude; if (ll.longitude < y0!) y0 = ll.longitude;
} }
} }
return LatLngBounds( return LatLngBounds(

File diff suppressed because it is too large Load Diff