import 'dart:async'; import 'dart:math'; 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:google_polyline_algorithm/google_polyline_algorithm.dart'; import 'package:sefer_driver/constant/colors.dart'; // استخدام نفس مسارات الاستيراد التي قدمتها import '../../../constant/api_key.dart'; import '../../../constant/links.dart'; import '../../functions/crud.dart'; import '../../functions/tts.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: 2); // القيمة الافتراضية // --- متغيرات البحث عن الأماكن --- List placesDestination = []; Timer? _debounce; // --- متغيرات الملاحة (Navigation) --- LatLng? _finalDestination; List> routeSteps = []; List _fullRouteCoordinates = []; List> _stepPolylines = []; // لتخزين نقاط كل خطوة على حدة bool _nextInstructionSpoken = false; String currentInstruction = ""; String nextInstruction = ""; int currentStepIndex = 0; double currentSpeed = 0.0; String distanceToNextStep = ""; final List _stepBounds = []; @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(); } // ======================================================================= // ١. النظام الذكي لتحديد الموقع والتحديث // ======================================================================= Future _getCurrentLocationAndStartUpdates() async { try { Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high); myLocation = LatLng(position.latitude, position.longitude); update(); animateCameraToPosition(myLocation!); // بدء التحديثات باستخدام المؤقت بدلاً من الـ Stream _startLocationTimer(); } catch (e) { print("Error getting location: $e"); } } // --- تم استبدال الـ Stream بمؤقت للتحكم الكامل --- void _startLocationTimer() { _locationUpdateTimer?.cancel(); // إلغاء أي مؤقت قديم _locationUpdateTimer = Timer.periodic(_currentUpdateInterval, (timer) { _updateLocationAndProcess(); }); } // --- هذه الدالة هي التي تعمل الآن بشكل دوري --- Future _updateLocationAndProcess() async { try { // طلب موقع واحد فقط عند كل مرة يعمل فيها المؤقت final position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high); myLocation = LatLng(position.latitude, position.longitude); heading = position.heading; currentSpeed = position.speed * 3.6; _updateCarMarker(); if (polylines.isNotEmpty && myLocation != null) { animateCameraToPosition( myLocation!, bearing: heading, zoom: 18.5, ); _checkNavigationStep(myLocation!); } update(); } catch (e) { print("Error in _updateLocationAndProcess: $e"); } } // --- الدالة المسؤولة عن تغيير سرعة التحديث ديناميكياً --- void _adjustUpdateInterval() { if (currentStepIndex >= routeSteps.length) return; final currentStepDistance = routeSteps[currentStepIndex]['distance']['value']; // إذا كانت الخطوة الحالية طويلة (شارع سريع > 1.5 كم) if (currentStepDistance > 1500) { _currentUpdateInterval = const Duration(seconds: 4); } // إذا كانت الخطوة قصيرة (منعطفات داخل المدينة < 1.5 كم) else { _currentUpdateInterval = const Duration(seconds: 2); } // إعادة تشغيل المؤقت بالسرعة الجديدة _startLocationTimer(); } // ... باقي دوال إعداد الخريطة ... void onMapCreated(GoogleMapController controller) { mapController = controller; if (myLocation != null) { animateCameraToPosition(myLocation!); } } 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, ), ); } void animateCameraToPosition(LatLng position, {double zoom = 16.0, double bearing = 0.0}) { mapController?.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( target: position, zoom: zoom, bearing: bearing, tilt: 45.0), ), ); } // ======================================================================= // ٢. الملاحة والتحقق من الخطوات // ======================================================================= void _checkNavigationStep(LatLng currentPosition) { if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length || _finalDestination == null) return; _updateTraveledPolyline(currentPosition); final step = routeSteps[currentStepIndex]; final endLatLng = LatLng(step['end_location']['lat'], step['end_location']['lng']); final distance = Geolocator.distanceBetween( currentPosition.latitude, currentPosition.longitude, endLatLng.latitude, endLatLng.longitude, ); distanceToNextStep = (distance > 1000) ? "${(distance / 1000).toStringAsFixed(1)} كم" : "${distance.toStringAsFixed(0)} متر"; if (distance < 30 && !_nextInstructionSpoken && nextInstruction.isNotEmpty) { Get.find().speakText(nextInstruction); _nextInstructionSpoken = true; } if (distance < 20) { _advanceStep(); } } void _advanceStep() { currentStepIndex++; if (currentStepIndex < routeSteps.length) { currentInstruction = _parseInstruction(routeSteps[currentStepIndex]['html_instructions']); nextInstruction = ((currentStepIndex + 1) < routeSteps.length) ? _parseInstruction( routeSteps[currentStepIndex + 1]['html_instructions']) : "الوجهة النهائية"; _nextInstructionSpoken = false; // **هنا يتم تعديل سرعة التحديث عند الانتقال لخطوة جديدة** _adjustUpdateInterval(); if (currentStepIndex < _stepBounds.length) { mapController?.animateCamera( CameraUpdate.newLatLngBounds(_stepBounds[currentStepIndex], 70.0)); } update(); } else { currentInstruction = "لقد وصلت إلى وجهتك."; nextInstruction = ""; distanceToNextStep = ""; _locationUpdateTimer?.cancel(); // إيقاف التحديثات عند الوصول Get.find().speakText(currentInstruction); update(); } } // ======================================================================= // ٣. تحسين خوارزمية البحث ورسم المسار المقطوع // ======================================================================= void _updateTraveledPolyline(LatLng currentPosition) { // **التحسين:** البحث فقط في الخطوة الحالية والخطوة التالية int searchEndIndex = (currentStepIndex + 1 < _stepPolylines.length) ? currentStepIndex + 1 : currentStepIndex; int overallClosestIndex = -1; double minDistance = double.infinity; // البحث في نقاط الخطوة الحالية والتالية فقط for (int i = currentStepIndex; i <= searchEndIndex; i++) { for (int j = 0; j < _stepPolylines[i].length; j++) { final distance = Geolocator.distanceBetween( currentPosition.latitude, currentPosition.longitude, _stepPolylines[i][j].latitude, _stepPolylines[i][j].longitude); if (distance < minDistance) { minDistance = distance; // نحتاج إلى حساب الفهرس العام في القائمة الكاملة overallClosestIndex = _getOverallIndex(i, j); } } } if (overallClosestIndex == -1) return; List traveledPoints = _fullRouteCoordinates.sublist(0, overallClosestIndex + 1); traveledPoints.add(currentPosition); List remainingPoints = _fullRouteCoordinates.sublist(overallClosestIndex); remainingPoints.insert(0, currentPosition); polylines.removeWhere((p) => p.polylineId.value == 'traveled_route'); polylines.add(Polyline( polylineId: const PolylineId('traveled_route'), points: traveledPoints, color: Colors.grey.shade600, width: 7, )); polylines.removeWhere((p) => p.polylineId.value == 'remaining_route'); polylines.add(Polyline( polylineId: const PolylineId('remaining_route'), points: remainingPoints, color: const Color(0xFF4A80F0), width: 7, )); } // دالة مساعدة لحساب الفهرس العام int _getOverallIndex(int stepIndex, int pointInStepIndex) { int overallIndex = 0; for (int i = 0; i < stepIndex; i++) { overallIndex += _stepPolylines[i].length; } return overallIndex + pointInStepIndex; } // ======================================================================= // ٤. دوال مساعدة وتجهيز البيانات // ======================================================================= void _prepareStepData() { _stepBounds.clear(); _stepPolylines.clear(); if (routeSteps.isEmpty) return; for (final step in routeSteps) { final pointsString = step['polyline']['points']; final List> points = decodePolyline(pointsString).cast>(); final polylineCoordinates = points .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) .toList(); _stepPolylines.add(polylineCoordinates); // تخزين نقاط الخطوة _stepBounds.add(_boundsFromLatLngList(polylineCoordinates)); } } // ... باقي دوال الكنترولر بدون تغيير ... // (selectDestination, onMapLongPressed, startNavigationTo, getRoute, etc.) Future selectDestination(dynamic place) async { placeDestinationController.clear(); placesDestination = []; final double lat = double.parse(place['latitude'].toString()); final double lng = double.parse(place['longitude'].toString()); final LatLng destination = LatLng(lat, lng); await startNavigationTo(destination, infoWindowTitle: place['name'] ?? 'وجهة محددة'); } 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: 'الموقع المحدد'); }, ), ], ), ); } 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); } catch (e) { Get.snackbar('خطأ', 'حدث خطأ أثناء تحديد الوجهة.'); print("Error starting navigation: $e"); } finally { isLoading = false; update(); } } Future getRoute(LatLng origin, LatLng destination) async { final url = '${AppLink.googleMapsLink}directions/json?language=ar&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}'; var response = await CRUD().getGoogleApi(link: url, payload: {}); if (response == null || response['routes'].isEmpty) { Get.snackbar('خطأ', 'لم يتم العثور على مسار.'); return; } polylines.clear(); final pointsString = response['routes'][0]['overview_polyline']['points']; final List> points = decodePolyline(pointsString).cast>(); _fullRouteCoordinates = points .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) .toList(); polylines.add( Polyline( polylineId: const PolylineId('remaining_route'), points: _fullRouteCoordinates, color: const Color(0xFF4A80F0), width: 7, startCap: Cap.roundCap, endCap: Cap.roundCap, ), ); polylines.add( const Polyline( polylineId: PolylineId('traveled_route'), points: [], color: Colors.grey, width: 7, ), ); routeSteps = List>.from( response['routes'][0]['legs'][0]['steps']); _prepareStepData(); currentStepIndex = 0; _nextInstructionSpoken = false; if (routeSteps.isNotEmpty) { currentInstruction = _parseInstruction(routeSteps[0]['html_instructions']); nextInstruction = (routeSteps.length > 1) ? _parseInstruction(routeSteps[1]['html_instructions']) : "الوجهة النهائية"; Get.find().speakText(currentInstruction); } _adjustUpdateInterval(); // تحديد سرعة التحديث لأول مرة final boundsData = response['routes'][0]['bounds']; mapController?.animateCamera(CameraUpdate.newLatLngBounds( LatLngBounds( northeast: LatLng( boundsData['northeast']['lat'], boundsData['northeast']['lng']), southwest: LatLng( boundsData['southwest']['lat'], boundsData['southwest']['lng']), ), 100.0, )); } 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}) { polylines.clear(); if (!isNewRoute) { markers.removeWhere((m) => m.markerId.value == 'destination'); _finalDestination = null; } routeSteps.clear(); currentInstruction = ""; nextInstruction = ""; distanceToNextStep = ""; currentSpeed = 0.0; _stepBounds.clear(); _fullRouteCoordinates.clear(); _stepPolylines.clear(); _nextInstructionSpoken = false; _locationUpdateTimer?.cancel(); // إيقاف التحديثات عند إلغاء المسار update(); } LatLngBounds _boundsFromLatLngList(List list) { assert(list.isNotEmpty); 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!)); } 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(25, 25)), 'assets/images/b.png'); } String _parseInstruction(String html) => html.replaceAll(RegExp(r'<[^>]*>'), ' '); Future getPlaces() async { if (placeDestinationController.text.trim().isEmpty) { placesDestination = []; update(); return; } if (myLocation == null) { Get.snackbar('انتظر', 'جاري تحديد موقعك الحالي...'); return; } final query = placeDestinationController.text.trim(); final lat = myLocation!.latitude; final lng = myLocation!.longitude; const double range = 2.2; final lat_min = lat - range, lat_max = lat + range, lng_min = lng - range, lng_max = lng + range; try { final response = await CRUD().post( link: AppLink.getPlacesSyria, payload: { 'query': query, 'lat_min': lat_min.toString(), 'lat_max': lat_max.toString(), 'lng_min': lng_min.toString(), 'lng_max': lng_max.toString(), }, ); if (response != 'failure') { placesDestination = response['message'] ?? []; } else { placesDestination = []; } } catch (e) { print('Exception in getPlaces: $e'); } finally { update(); } } void onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 700), () => getPlaces()); } }