import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'dart:convert'; // <<<--- إضافة جديدة 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/crud.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: 2); // --- متغيرات البحث عن الأماكن --- List placesDestination = []; Timer? _debounce; // --- متغيرات الملاحة (Navigation) --- LatLng? _finalDestination; List> routeSteps = []; List _fullRouteCoordinates = []; List> _stepPolylines = []; bool _nextInstructionSpoken = false; String currentInstruction = ""; String nextInstruction = ""; int currentStepIndex = 0; // <<<--- [تعديل] ---: متغير جديد لتتبع المسار المقطوع بدقة int _lastTraveledIndexInFullRoute = 0; double currentSpeed = 0.0; String distanceToNextStep = ""; final List _stepBounds = []; // --- ثوابت الـ API الجديد --- static const String _routeApiBaseUrl = 'https://routec.intaleq.xyz/'; static final String _routeApiKey = Env.mapKeyOsm; @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(); } // ======================================================================= // ١. النظام الذكي لتحديد الموقع والتحديث // ======================================================================= // Helper function to check if a ray from the point intersects with a polygon segment 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; // Check if the point is outside the vertical bounds of the segment if ((py < v1y && py < v2y) || (py > v1y && py > v2y)) { return false; } // Calculate the intersection of the ray and the segment double intersectX = v1x + (py - v1y) * (v2x - v1x) / (v2y - v1y); // Check if the intersection is to the right of the point return intersectX > px; } // Function to check if the point is inside the polygon 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]; // Loop back to the start if (_rayIntersectsSegment(point, vertex1, vertex2)) { intersections++; } } // If the number of intersections is odd, the point is inside return intersections % 2 != 0; } String getLocationArea(double latitude, double longitude) { LatLng passengerPoint = LatLng(latitude, longitude); // 1. فحص الأردن if (isPointInPolygon(passengerPoint, CountryPolygons.jordanBoundary)) { box.write(BoxName.countryCode, 'Jordan'); // يمكنك تعيين AppLink.endPoint هنا إذا كان منطقك الداخلي لا يزال يعتمد عليه // box.write(BoxName.serverChosen, // AppLink.IntaleqSyriaServer); // مثال: اختر سيرفر سوريا للبيانات return 'Jordan'; } // 2. فحص سوريا if (isPointInPolygon(passengerPoint, CountryPolygons.syriaBoundary)) { box.write(BoxName.countryCode, 'Syria'); // box.write(BoxName.serverChosen, AppLink.IntaleqSyriaServer); return 'Syria'; } // 3. فحص مصر if (isPointInPolygon(passengerPoint, CountryPolygons.egyptBoundary)) { box.write(BoxName.countryCode, 'Egypt'); // box.write(BoxName.serverChosen, AppLink.IntaleqAlexandriaServer); return 'Egypt'; } // 4. الافتراضي (إذا كان خارج المناطق المخدومة) box.write(BoxName.countryCode, 'Jordan'); // box.write(BoxName.serverChosen, AppLink.IntaleqSyriaServer); return 'Unknown Location (Defaulting to Jordan)'; } 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) { print("Error getting location: $e"); } } 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']; if (currentStepDistance > 1500) { _currentUpdateInterval = const Duration(seconds: 4); } 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 = routeSteps[currentStepIndex]['instruction_text']; nextInstruction = ((currentStepIndex + 1) < routeSteps.length) ? routeSteps[currentStepIndex + 1]['instruction_text'] : "الوجهة النهائية"; _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) { // 1. التأكد من أن المسار الكامل محمل if (_fullRouteCoordinates.isEmpty) return; double minDistance = double.infinity; // 2. ابدأ البحث دائماً من النقطة الأخيرة التي تم الوصول إليها int newClosestIndex = _lastTraveledIndexInFullRoute; // 3. ابحث للأمام فقط (من آخر نقطة مسجلة إلى نهاية المسار) for (int i = _lastTraveledIndexInFullRoute; i < _fullRouteCoordinates.length; i++) { final point = _fullRouteCoordinates[i]; final distance = Geolocator.distanceBetween( currentPosition.latitude, currentPosition.longitude, point.latitude, point.longitude, ); if (distance < minDistance) { minDistance = distance; newClosestIndex = i; } else if (distance > minDistance + 50) { // 4. تحسين: إذا بدأت المسافة بالزيادة، فتوقف عن البحث // هذا يعني أننا تجاوزنا أقرب نقطة (50 متر هامش أمان) break; } } // 5. قم بتحديث آخر نقطة مسجلة _lastTraveledIndexInFullRoute = newClosestIndex; // 6. ارسم المسار المقطوع (من البداية إلى أقرب نقطة) List traveledPoints = _fullRouteCoordinates.sublist(0, newClosestIndex + 1); traveledPoints.add(currentPosition); // أضف الموقع الحالي لنعومة الخط // 7. ارسم المسار المتبقي (من أقرب نقطة إلى النهاية) List remainingPoints = _fullRouteCoordinates.sublist(newClosestIndex); remainingPoints.insert(0, currentPosition); // ابدأ من الموقع الحالي // 8. تحديث الخطوط على الخريطة 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; // } // ======================================================================= // ٤. دوال مساعدة وتجهيز البيانات // ======================================================================= Future _prepareStepData() async { _stepBounds.clear(); _stepPolylines.clear(); if (routeSteps.isEmpty) return; for (final step in routeSteps) { final pointsString = step['geometry']; final List polylineCoordinates = await compute( decodePolylineIsolate as ComputeCallback>, pointsString); _stepPolylines.add(polylineCoordinates); _stepBounds.add(_boundsFromLatLngList(polylineCoordinates)); } } 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(); } } // --- (تعديل) ---: تم تعديل الدالة لاستخدام http.get Future getRoute(LatLng origin, LatLng destination) async { final url = '$_routeApiBaseUrl/route?origin=${origin.latitude},${origin.longitude}&destination=${destination.latitude},${destination.longitude}&steps=true&overview=full'; try { final response = await http.get( Uri.parse(url), headers: {'X-API-KEY': _routeApiKey}, ); if (response.statusCode != 200) { print("Error from route API: ${response.statusCode}"); Get.snackbar('خطأ', 'لم يتم العثور على مسار.'); return; } final responseData = jsonDecode(response.body); if (responseData == null || responseData['status'] != 'ok') { Get.snackbar('خطأ', 'لم يتم العثور على مسار.'); return; } polylines.clear(); final pointsString = responseData['polyline']; _fullRouteCoordinates = await compute( decodePolylineIsolate as ComputeCallback>, pointsString); // <<<--- [تعديل] ---: تصفير الـ index عند بدء مسار جديد _lastTraveledIndexInFullRoute = 0; 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(responseData['steps']); await _prepareStepData(); for (int i = 0; i < routeSteps.length; i++) { var step = routeSteps[i]; if (i < _stepPolylines.length && _stepPolylines[i].isNotEmpty) { LatLng endLocation = _stepPolylines[i].last; step['end_location'] = { 'lat': endLocation.latitude, 'lng': endLocation.longitude }; } else { var loc = step['maneuver']['location']; // [lng, lat] step['end_location'] = {'lat': loc[1], 'lng': loc[0]}; } step['instruction_text'] = _createInstructionFromManeuver(step); } currentStepIndex = 0; _nextInstructionSpoken = false; if (routeSteps.isNotEmpty) { currentInstruction = routeSteps[0]['instruction_text']; nextInstruction = (routeSteps.length > 1) ? routeSteps[1]['instruction_text'] : "الوجهة النهائية"; Get.find().speakText(currentInstruction); } _adjustUpdateInterval(); if (_fullRouteCoordinates.isNotEmpty) { final bounds = _boundsFromLatLngList(_fullRouteCoordinates); mapController ?.animateCamera(CameraUpdate.newLatLngBounds(bounds, 100.0)); } } catch (e) { print("Exception in getRoute: $e"); Get.snackbar('خطأ', 'حدث خطأ في الشبكة.'); } } 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(); // <<<--- [تعديل] ---: تصفير الـ index عند إلغاء المسار _lastTraveledIndexInFullRoute = 0; 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 _createInstructionFromManeuver(Map step) { 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': instruction = "لقد وصلت إلى وجهتك"; if (name.isNotEmpty) instruction += "، $name"; return instruction; case 'turn': case 'fork': case 'off ramp': case 'on ramp': case 'roundabout': instruction = _getTurnInstruction(modifier); break; case 'continue': instruction = "استمر"; break; default: instruction = "اتجه"; } if (name.isNotEmpty) { if (instruction == "استمر") { instruction += " على $name"; } else { instruction += " إلى $name"; } } else if (type == 'continue' && modifier == 'straight') { instruction = "استمر بشكل مستقيم"; } 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 "اتجه"; } } // ======================================================================= // ٥. دالة البحث عن الأماكن المحدثة والدوال المساعدة لها // ======================================================================= // --- (تعديل) ---: تم تعديل الدالة لاستخدام http.post Future getPlaces() async { final q = placeDestinationController.text.trim(); if (q.isEmpty || q.length < 3) { placesDestination = []; update(); return; } if (myLocation == null) { print('myLocation is null, cannot search for places.'); return; } final lat = myLocation!.latitude; final lng = myLocation!.longitude; const radiusKm = 200.0; 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; // تجهيز البيانات لإرسالها كـ JSON final payload = { 'query': q, 'lat_min': latMin.toString(), 'lat_max': latMax.toString(), 'lng_min': lngMin.toString(), 'lng_max': lngMax.toString(), }; try { final response = await CRUD().post(link: AppLink.getPlacesSyria, payload: payload); // إرسال البيانات كـ JSON final responseData = (response); List list; if (responseData is Map) { if (responseData['status'] == 'success' && responseData['message'] is List) { list = List.from(responseData['message'] as List); } else if (responseData['status'] == 'failure') { print('Server Error: ${responseData['message']}'); return; } else { print('Unexpected Map shape from server'); return; } } else if (responseData is List) { list = List.from(responseData); } else { print('Unexpected response shape from server'); return; } String _bestName(Map p) { return (p['name_ar'] ?? p['name'] ?? p['name_en'] ?? '').toString(); } 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); final relevance = _relevanceScore(_bestName(p), q); final score = (1.0 / (1.0 + distance)) * (1.0 + relevance); p['distanceKm'] = distance; p['relevance'] = relevance; p['score'] = score; } list.sort((a, b) { final sa = (a['score'] ?? 0.0) as double; final sb = (b['score'] ?? 0.0) as double; return sb.compareTo(sa); }); placesDestination = list; Log.print('Updated places: $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()); } // ----------------------------------------------------------------- // --== دوال مساعدة (محدثة) ==-- // ----------------------------------------------------------------- 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 rLat1 = lat1 * (pi / 180.0); final rLat2 = lat2 * (pi / 180.0); final a = sin(dLat / 2) * sin(dLat / 2) + cos(rLat1) * cos(rLat2) * sin(dLon / 2) * sin(dLon / 2); final c = 2 * atan2(sqrt(a), sqrt(1 - a)); return R * c; } double _relevanceScore(String placeName, String query) { if (placeName.isEmpty || query.isEmpty) return 0.0; final pLower = placeName.toLowerCase(); final qLower = query.toLowerCase(); if (pLower.startsWith(qLower)) return 1.0; if (pLower.contains(qLower)) return 0.5; return 0.0; } double _kmToLatDelta(double km) { const kmInDegree = 111.32; return km / kmInDegree; } double _kmToLngDelta(double km, double latitude) { const kmInDegree = 111.32; return km / (kmInDegree * cos(latitude * (pi / 180.0))); } }