import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:http/http.dart' as http; import 'package:sefer_driver/constant/colors.dart'; import 'package:sefer_driver/controller/functions/crud.dart'; import '../../../constant/box_name.dart'; import '../../../constant/country_polygons.dart'; import '../../../constant/links.dart'; import '../../../env/env.dart'; import '../../../main.dart'; import '../../../print.dart'; import '../../functions/tts.dart'; import 'decode_polyline_isolate.dart'; class NavigationController extends GetxController { // --- متغيرات الحالة العامة --- bool isLoading = false; GoogleMapController? mapController; final TextEditingController placeDestinationController = TextEditingController(); // --- متغيرات الخريطة والموقع --- LatLng? myLocation; double heading = 0.0; final Set markers = {}; final Set polylines = {}; BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker; BitmapDescriptor destinationIcon = BitmapDescriptor.defaultMarker; // --- متغيرات النظام الذكي للتحديث --- Timer? _locationUpdateTimer; // تحديث كل ثانية لضمان سلاسة الحركة Duration _currentUpdateInterval = const Duration(seconds: 1); LatLng? _lastRecordedLocation; // --- متغيرات البحث --- List placesDestination = []; Timer? _debounce; // --- متغيرات الملاحة (Navigation) --- LatLng? _finalDestination; List> routeSteps = []; List _fullRouteCoordinates = []; int _lastTraveledIndexInFullRoute = 0; bool _nextInstructionSpoken = false; String currentInstruction = ""; String nextInstruction = ""; int currentStepIndex = 0; double currentSpeed = 0.0; String distanceToNextStep = ""; // الرابط الجديد static const String _routeApiBaseUrl = "https://routesjo.intaleq.xyz/route/v1/driving"; @override void onInit() { super.onInit(); _initialize(); } Future _initialize() async { await _loadCustomIcons(); await _getCurrentLocationAndStartUpdates(); if (!Get.isRegistered()) { Get.put(TextToSpeechController()); } } @override void onClose() { _locationUpdateTimer?.cancel(); mapController?.dispose(); _debounce?.cancel(); placeDestinationController.dispose(); super.onClose(); } // ======================================================================= // ✅ دوال الخريطة الأساسية // ======================================================================= void onMapCreated(GoogleMapController controller) { mapController = controller; if (myLocation != null) { animateCameraToPosition(myLocation!); } } Future onMapLongPressed(LatLng tappedPoint) async { Get.dialog( AlertDialog( title: const Text('بدء الملاحة؟'), content: const Text('هل تريد الذهاب إلى هذا الموقع المحدد؟'), actionsAlignment: MainAxisAlignment.spaceBetween, actions: [ TextButton( child: const Text('إلغاء', style: TextStyle(color: Colors.grey)), onPressed: () => Get.back(), ), TextButton( child: const Text('اذهب الآن'), onPressed: () { Get.back(); startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد'); }, ), ], ), ); } // ======================================================================= // ١. النظام الذكي للموقع (Optimized) // ======================================================================= Future _getCurrentLocationAndStartUpdates() async { try { Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high); myLocation = LatLng(position.latitude, position.longitude); getLocationArea(myLocation!.latitude, myLocation!.longitude); update(); animateCameraToPosition(myLocation!); _startLocationTimer(); } catch (e) { Log.print("Error getting initial location: $e"); } } void _startLocationTimer() { _locationUpdateTimer?.cancel(); _locationUpdateTimer = Timer.periodic(_currentUpdateInterval, (timer) { _updateLocationAndProcess(); }); } Future _updateLocationAndProcess() async { try { final position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high); final newLoc = LatLng(position.latitude, position.longitude); // فلتر الاهتزاز (Jitter Filter) if (_lastRecordedLocation != null) { double dist = Geolocator.distanceBetween( newLoc.latitude, newLoc.longitude, _lastRecordedLocation!.latitude, _lastRecordedLocation!.longitude); if (dist < 2.0) return; // تجاهل الحركة الأقل من 2 متر } myLocation = newLoc; _lastRecordedLocation = newLoc; heading = position.heading; currentSpeed = position.speed * 3.6; _updateCarMarker(); if (polylines.isNotEmpty && _fullRouteCoordinates.isNotEmpty) { animateCameraToPosition( myLocation!, bearing: heading, zoom: 18.0, ); _updateTraveledPolylineSmart(myLocation!); _checkNavigationStep(myLocation!); } update(); } catch (e) { // Log.print("Loc update error: $e"); } } void _updateCarMarker() { if (myLocation == null) return; markers.removeWhere((m) => m.markerId.value == 'myLocation'); markers.add( Marker( markerId: const MarkerId('myLocation'), position: myLocation!, icon: carIcon, rotation: heading, anchor: const Offset(0.5, 0.5), flat: true, zIndex: 2, ), ); } void animateCameraToPosition(LatLng position, {double zoom = 17.0, double bearing = 0.0}) { mapController?.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( target: position, zoom: zoom, bearing: bearing, tilt: 45.0), ), ); } // ======================================================================= // ٢. خوارزمية رسم المسار الذكية (Sliding Window) - مخففة للجهاز // ======================================================================= void _updateTraveledPolylineSmart(LatLng currentPos) { if (_fullRouteCoordinates.isEmpty) return; // نبحث فقط في الـ 60 نقطة القادمة لتخفيف الحمل على المعالج int searchWindow = 60; int startIndex = _lastTraveledIndexInFullRoute; int endIndex = min(startIndex + searchWindow, _fullRouteCoordinates.length); double minDistance = double.infinity; int closestIndex = startIndex; bool foundCloser = false; for (int i = startIndex; i < endIndex; i++) { final point = _fullRouteCoordinates[i]; final dist = Geolocator.distanceBetween(currentPos.latitude, currentPos.longitude, point.latitude, point.longitude); if (dist < minDistance) { minDistance = dist; closestIndex = i; foundCloser = true; } } // شرط التحديث: وجدنا نقطة أقرب، وهي أمامنا (وليست خلفنا)، والمسافة منطقية if (foundCloser && minDistance < 50 && closestIndex > _lastTraveledIndexInFullRoute) { _lastTraveledIndexInFullRoute = closestIndex; // استخدام sublist وهو سريع جداً في Dart final remaining = _fullRouteCoordinates.sublist(_lastTraveledIndexInFullRoute); final traveled = _fullRouteCoordinates.sublist(0, _lastTraveledIndexInFullRoute + 1); _updatePolylinesSets(traveled, remaining); } } void _updatePolylinesSets(List traveled, List remaining) { // إزالة الخطوط القديمة وإضافة الجديدة بدلاً من تحديث الكل polylines.removeWhere((p) => p.polylineId.value == 'remaining_route'); polylines.removeWhere((p) => p.polylineId.value == 'traveled_route'); // المسار المتبقي (أزرق واضح) polylines.add(Polyline( polylineId: const PolylineId('remaining_route'), points: remaining, color: const Color(0xFF0D47A1), width: 6, startCap: Cap.roundCap, endCap: Cap.roundCap, jointType: JointType.round, )); // المسار المقطوع (رمادي) polylines.add(Polyline( polylineId: const PolylineId('traveled_route'), points: traveled, color: Colors.grey.shade400, width: 6, jointType: JointType.round, )); } // ======================================================================= // ٣. التحكم في التوجيهات // ======================================================================= void _checkNavigationStep(LatLng currentPosition) { if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length) return; final step = routeSteps[currentStepIndex]; // في OSRM، يجب التأكد من وجود maneuver location final maneuver = step['maneuver']; final List location = maneuver['location']; // [lng, lat] final endLatLng = LatLng(location[1], location[0]); final distance = Geolocator.distanceBetween( currentPosition.latitude, currentPosition.longitude, endLatLng.latitude, endLatLng.longitude, ); distanceToNextStep = distance > 1000 ? "${(distance / 1000).toStringAsFixed(1)} كم" : "${distance.toStringAsFixed(0)} متر"; // نطق التعليمات قبل 50 متر if (distance < 50 && !_nextInstructionSpoken && nextInstruction.isNotEmpty) { Get.find().speakText(nextInstruction); _nextInstructionSpoken = true; } // الانتقال للخطوة التالية عند الاقتراب (20 متر) if (distance < 20) { _advanceStep(); } } void _advanceStep() { currentStepIndex++; if (currentStepIndex < routeSteps.length) { currentInstruction = routeSteps[currentStepIndex]['instruction_text']; // تجهيز التعليمات القادمة if ((currentStepIndex + 1) < routeSteps.length) { nextInstruction = "ثم ${routeSteps[currentStepIndex + 1]['instruction_text']}"; } else { nextInstruction = "ستصل إلى وجهتك"; } _nextInstructionSpoken = false; update(); } else { _finishNavigation(); } } void _finishNavigation() { currentInstruction = "لقد وصلت إلى وجهتك"; nextInstruction = ""; distanceToNextStep = ""; Get.find().speakText(currentInstruction); update(); } // ======================================================================= // ٤. جلب المسار (🔥 تم التحديث للرابط الجديد 🔥) // ======================================================================= Future getRoute(LatLng origin, LatLng destination) async { // 🔥 بناء الرابط حسب التنسيق: /driving/lng,lat;lng,lat String coords = "${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}"; // 🔥 تفعيل steps=true لجلب الخطوات String url = "$_routeApiBaseUrl/$coords?steps=true&overview=full&geometries=polyline"; try { // الرابط الجديد قد لا يحتاج API Key في الهيدر، ولكن نتركه إذا كان السيرفر يطلبه // إذا كان الرابط عام، يمكن إزالة الهيدر final response = await http.get(Uri.parse(url)); if (response.statusCode != 200) { Get.snackbar('تنبيه', 'تعذر الاتصال بخدمة التوجيه.'); return; } final responseData = jsonDecode(response.body); // التحقق حسب هيكلية OSRM if (responseData['code'] != 'Ok' || (responseData['routes'] as List).isEmpty) { Get.snackbar('عذراً', 'لم يتم العثور على مسار.'); return; } var route = responseData['routes'][0]; // 1. فك تشفير Polyline final pointsString = route['geometry']; _fullRouteCoordinates = await compute( decodePolylineIsolate as ComputeCallback>, pointsString); // تهيئة الرسم _lastTraveledIndexInFullRoute = 0; _updatePolylinesSets([], _fullRouteCoordinates); // 2. استخراج الخطوات (داخل Legs في OSRM) var legs = route['legs'] as List; if (legs.isNotEmpty) { var steps = legs[0]['steps'] as List; routeSteps = List>.from(steps); } else { routeSteps = []; } // 3. معالجة النصوص العربية للخطوات for (var step in routeSteps) { step['instruction_text'] = _createInstructionFromManeuver(step); } // 4. بدء الملاحة currentStepIndex = 0; _nextInstructionSpoken = false; if (routeSteps.isNotEmpty) { currentInstruction = routeSteps[0]['instruction_text']; if (routeSteps.length > 1) { nextInstruction = "ثم ${routeSteps[1]['instruction_text']}"; } else { nextInstruction = "الوجهة النهائية"; } Get.find().speakText(currentInstruction); } // 5. ضبط الكاميرا if (_fullRouteCoordinates.isNotEmpty) { final bounds = _boundsFromLatLngList(_fullRouteCoordinates); mapController ?.animateCamera(CameraUpdate.newLatLngBounds(bounds, 80.0)); } update(); } catch (e) { Log.print("GetRoute Error: $e"); Get.snackbar('خطأ', 'حدث خطأ غير متوقع.'); } } // 🔥 تحويل تعليمات OSRM إلى العربية String _createInstructionFromManeuver(Map step) { if (step['maneuver'] == null) return "تابع المسير"; final maneuver = step['maneuver']; final type = maneuver['type'] ?? 'continue'; final modifier = maneuver['modifier'] ?? 'straight'; final name = step['name'] ?? ''; // اسم الشارع String instruction = ""; switch (type) { case 'depart': instruction = "انطلق"; break; case 'arrive': 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 (type == 'new name' || type == 'continue') { instruction += " على $name"; } else { instruction += " نحو $name"; } } return instruction; } String _getTurnInstruction(String modifier) { switch (modifier) { case 'uturn': return "قم بالاستدارة والعودة"; case 'sharp right': return "انعطف يميناً بحدة"; case 'right': return "انعطف يميناً"; case 'slight right': return "انعطف يميناً قليلاً"; case 'straight': return "استمر للأمام"; case 'slight left': return "انعطف يساراً قليلاً"; case 'left': return "انعطف يساراً"; case 'sharp left': return "انعطف يساراً بحدة"; default: return "اتجه"; } } // ======================================================================= // ٥. أدوات مساعدة // ======================================================================= Future startNavigationTo(LatLng destination, {String infoWindowTitle = ''}) async { isLoading = true; update(); try { _finalDestination = destination; clearRoute(isNewRoute: true); markers.add(Marker( markerId: const MarkerId('destination'), position: destination, icon: destinationIcon, infoWindow: InfoWindow(title: infoWindowTitle), )); await getRoute(myLocation!, destination); } finally { isLoading = false; update(); } } Future recalculateRoute() async { if (myLocation == null || _finalDestination == null || isLoading) return; isLoading = true; update(); Get.snackbar('إعادة التوجيه', 'جاري حساب مسار جديد...', backgroundColor: AppColor.goldenBronze); await getRoute(myLocation!, _finalDestination!); isLoading = false; update(); } void clearRoute({bool isNewRoute = false}) { if (!isNewRoute) { markers.removeWhere((m) => m.markerId.value == 'destination'); _finalDestination = null; polylines.clear(); } routeSteps.clear(); _fullRouteCoordinates.clear(); _lastTraveledIndexInFullRoute = 0; currentInstruction = ""; nextInstruction = ""; distanceToNextStep = ""; update(); } Future _loadCustomIcons() async { carIcon = await BitmapDescriptor.fromAssetImage( const ImageConfiguration(size: Size(40, 40)), 'assets/images/car.png'); destinationIcon = await BitmapDescriptor.fromAssetImage( const ImageConfiguration(size: Size(30, 30)), 'assets/images/b.png'); } Future getPlaces() async { final q = placeDestinationController.text.trim(); if (q.length < 3) { placesDestination = []; update(); return; } if (myLocation == null) return; final lat = myLocation!.latitude; final lng = myLocation!.longitude; const radiusKm = 200.0; final latDelta = _kmToLatDelta(radiusKm); final lngDelta = _kmToLngDelta(radiusKm, lat); final payload = { 'query': q, 'lat_min': (lat - latDelta).toString(), 'lat_max': (lat + latDelta).toString(), 'lng_min': (lng - lngDelta).toString(), 'lng_max': (lng + lngDelta).toString(), }; try { final response = await CRUD().post(link: AppLink.getPlacesSyria, payload: payload); final responseData = (response); List list; if (responseData is Map && responseData['status'] == 'success') { list = List.from(responseData['message'] as List); } else if (responseData is List) { list = List.from(responseData); } else { return; } for (final p in list) { final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0; final plng = double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0; final distance = _haversineKm(lat, lng, plat, plng); p['distanceKm'] = distance; } list.sort((a, b) => (a['distanceKm'] as double).compareTo(b['distanceKm'] as double)); placesDestination = list; update(); } catch (e) { print('Exception in getPlaces: $e'); } } Future selectDestination(dynamic place) async { placeDestinationController.clear(); placesDestination = []; final double lat = double.parse(place['latitude'].toString()); final double lng = double.parse(place['longitude'].toString()); await startNavigationTo(LatLng(lat, lng), infoWindowTitle: place['name'] ?? 'وجهة'); } void onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 700), () => getPlaces()); } // --- دوال مساعدة للحسابات --- double _haversineKm(double lat1, double lon1, double lat2, double lon2) { const R = 6371.0; final dLat = (lat2 - lat1) * (pi / 180.0); final dLon = (lon2 - lon1) * (pi / 180.0); final a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * pi / 180) * cos(lat2 * pi / 180) * sin(dLon / 2) * sin(dLon / 2); return R * 2 * atan2(sqrt(a), sqrt(1 - a)); } double _kmToLatDelta(double km) => km / 111.32; double _kmToLngDelta(double km, double lat) => km / (111.32 * cos(lat * pi / 180)); LatLngBounds _boundsFromLatLngList(List list) { double? x0, x1, y0, y1; for (LatLng latLng in list) { if (x0 == null) { x0 = x1 = latLng.latitude; y0 = y1 = latLng.longitude; } else { if (latLng.latitude > x1!) x1 = latLng.latitude; if (latLng.latitude < x0) x0 = latLng.latitude; if (latLng.longitude > y1!) y1 = latLng.longitude; if (latLng.longitude < y0!) y0 = latLng.longitude; } } return LatLngBounds( northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!)); } // --- دوال التحقق من الدولة --- String getLocationArea(double latitude, double longitude) { LatLng p = LatLng(latitude, longitude); if (isPointInPolygon(p, CountryPolygons.jordanBoundary)) { box.write(BoxName.countryCode, 'Jordan'); return 'Jordan'; } if (isPointInPolygon(p, CountryPolygons.syriaBoundary)) { box.write(BoxName.countryCode, 'Syria'); return 'Syria'; } if (isPointInPolygon(p, CountryPolygons.egyptBoundary)) { box.write(BoxName.countryCode, 'Egypt'); return 'Egypt'; } return 'Unknown'; } bool isPointInPolygon(LatLng point, List polygon) { int intersections = 0; for (int i = 0; i < polygon.length; i++) { LatLng vertex1 = polygon[i]; LatLng vertex2 = polygon[(i + 1) % polygon.length]; if (_rayIntersectsSegment(point, vertex1, vertex2)) intersections++; } return intersections % 2 != 0; } bool _rayIntersectsSegment(LatLng point, LatLng vertex1, LatLng vertex2) { double px = point.longitude; double py = point.latitude; double v1x = vertex1.longitude; double v1y = vertex1.latitude; double v2x = vertex2.longitude; double v2y = vertex2.latitude; if ((py < v1y && py < v2y) || (py > v1y && py > v2y)) return false; double intersectX = v1x + (py - v1y) * (v2x - v1x) / (v2y - v1y); return intersectX > px; } }