import 'dart:async'; import 'dart:math'; import 'dart:math' as 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 '../../../print.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'<[^>]*>'), ' '); double _haversineKm(double lat1, double lon1, double lat2, double lon2) { const R = 6371.0; // km final dLat = (lat2 - lat1) * math.pi / 180.0; final dLon = (lon2 - lon1) * math.pi / 180.0; final a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(lat1 * math.pi / 180.0) * math.cos(lat2 * math.pi / 180.0) * math.sin(dLon / 2) * math.sin(dLon / 2); final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); return R * c; } /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض double _kmToLatDelta(double km) => km / 111.0; /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض) double _kmToLngDelta(double km, double atLat) => km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9); /// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة) double _relevanceScore(String name, String query) { final n = name.toLowerCase(); final parts = query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2); double s = 0.0; for (final p in parts) { if (n.startsWith(p)) { s += 2.0; } else if (n.contains(p)) { s += 1.0; } } return s; } Future getPlaces() async { final q = placeDestinationController.text.trim(); if (q.isEmpty) { placesDestination = []; update(); return; } final lat = myLocation!.latitude; final lng = myLocation!.longitude; // نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك) const radiusKm = 200.0; // حساب الباوند الصحيح (درجات، وليس 2.2 درجة ثابتة) final latDelta = _kmToLatDelta(radiusKm); final lngDelta = _kmToLngDelta(radiusKm, lat); final latMin = lat - latDelta; final latMax = lat + latDelta; final lngMin = lng - lngDelta; final lngMax = lng + lngDelta; try { final response = await CRUD().post( link: AppLink.getPlacesSyria, payload: { 'query': q, 'lat_min': latMin.toString(), 'lat_max': latMax.toString(), 'lng_min': lngMin.toString(), 'lng_max': lngMax.toString(), }, ); // يدعم شكلي استجابة: إما {"...","message":[...]} أو قائمة مباشرة [...] List list; if (response is Map && response['message'] is List) { list = List.from(response['message'] as List); } else if (response is List) { list = List.from(response); } else { print('Unexpected response shape'); return; } // جهّز الحقول المحتملة للأسماء String _bestName(Map p) { return (p['name'] ?? p['name_ar'] ?? p['name_en'] ?? '').toString(); } // احسب المسافة ودرجة التطابق والنقاط for (final p in list) { final plat = double.tryParse(p['latitude']?.toString() ?? '') ?? 0.0; final plng = double.tryParse(p['longitude']?.toString() ?? '') ?? 0.0; final d = _haversineKm(lat, lng, plat, plng); final rel = _relevanceScore(_bestName(p), q); // معادلة ترتيب ذكية: مسافة أقل + تطابق أعلى = نقاط أعلى // تضيف +1 لضمان عدم وصول الوزن للصفر عند عدم وجود تطابق final score = (1.0 / (1.0 + d)) * (1.0 + rel); p['distanceKm'] = d; p['relevance'] = rel; p['score'] = score; } // رتّب حسب score تنازليًا، ثم المسافة تصاعديًا كحسم list.sort((a, b) { final sa = (a['score'] ?? 0.0) as double; final sb = (b['score'] ?? 0.0) as double; final cmp = sb.compareTo(sa); if (cmp != 0) return cmp; final da = (a['distanceKm'] ?? 1e9) as double; final db = (b['distanceKm'] ?? 1e9) as double; return da.compareTo(db); }); // خذ أول 10–15 للعرض (اختياري)، أو اعرض الكل placesDestination = list.take(15).toList(); Log.print('placesDestination: $placesDestination'); update(); } catch (e) { print('Exception in getPlaces: $e'); } } void onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 700), () => getPlaces()); } }