import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:sefer_driver/controller/firebase/local_notification.dart'; import 'package:sefer_driver/controller/home/captin/behavior_controller.dart'; import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart'; import 'package:sefer_driver/controller/home/navigation/decode_polyline_isolate.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:sefer_driver/views/widgets/mydialoug.dart'; import 'package:bubble_head/bubble.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:google_polyline_algorithm/google_polyline_algorithm.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../constant/api_key.dart'; import '../../../constant/box_name.dart'; import '../../../constant/colors.dart'; import '../../../constant/country_polygons.dart'; import '../../../constant/links.dart'; import '../../../constant/table_names.dart'; import '../../../env/env.dart'; import '../../../main.dart'; import '../../../print.dart'; import '../../../views/Rate/rate_passenger.dart'; import '../../../views/home/Captin/home_captain/home_captin.dart'; import '../../firebase/firbase_messge.dart'; import '../../firebase/notification_service.dart'; import '../../functions/crud.dart'; import '../../functions/location_controller.dart'; import '../../functions/tts.dart'; class MapDriverController extends GetxController { bool isLoading = true; final formKey1 = GlobalKey(); final formKey2 = GlobalKey(); final formKeyCancel = GlobalKey(); final messageToPassenger = TextEditingController(); final sosEmergincyNumberCotroller = TextEditingController(); final cancelTripCotroller = TextEditingController(); List data = []; List dataDestination = []; LatLngBounds? boundsData; BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker; BitmapDescriptor passengerIcon = BitmapDescriptor.defaultMarker; BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker; BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker; final List polylineCoordinates = []; final List polylineCoordinatesDestination = []; List polyLines = []; List polyLinesDestination = []; Set markers = {}; late String passengerLocation; late String passengerDestination; late String step0; late String step1; late String step2; late String step3; late String step4; late String passengerWalletBurc; late String timeOfOrder; late String duration; late String totalCost; String distance = '0'; String? passengerName; late String passengerEmail; late String totalPricePassenger; late String passengerPhone; late String rideId; late String isHaveSteps; String paymentAmount = '0'; late String paymentMethod; late String passengerId; late String driverId; late String tokenPassenger; String durationToPassenger = '100'; late String walletChecked; late String direction; late String durationOfRideValue; late String status; int timeWaitingPassenger = 5; //5 miniute bool isPassengerInfoWindow = false; bool isBtnRideBegin = false; bool isArrivedSend = true; bool isdriverWaitTimeEnd = false; bool isRideFinished = false; bool isRideStarted = false; bool isPriceWindow = false; double passengerInfoWindowHeight = Get.height * .38; double driverEndPage = 100; double progress = 0; double progressToPassenger = 0; double progressInPassengerLocationFromDriver = 0; bool isRideBegin = false; int progressTimerToShowPassengerInfoWindowFromDriver = 25; int remainingTimeToShowPassengerInfoWindowFromDriver = 25; int remainingTimeToPassenger = 60; int remainingTimeInPassengerLocatioWait = 60; bool isDriverNearPassengerStart = false; GoogleMapController? mapController; late LatLng myLocation; int remainingTimeTimerRideBegin = 60; String stringRemainingTimeRideBegin = ''; String stringRemainingTimeRideBegin1 = ''; double progressTimerRideBegin = 0; Timer? timer; String? mapAPIKEY; final zones = []; String canelString = 'yet'; LatLng latLngPassengerLocation = LatLng(0, 0); late LatLng latLngPassengerDestination = LatLng(0, 0); List> routeSteps = []; String currentInstruction = ""; int currentStepIndex = 0; bool isTtsEnabled = false; // في MapDriverController void toggleTts() { isTtsEnabled = !isTtsEnabled; if (!isTtsEnabled && Get.isRegistered()) { Get.find().stop(); } update(); } void playVoiceInstruction(String text) { if (!isTtsEnabled) return; if (Get.isRegistered()) { Get.find().speakText(text); } } void disposeEverything() { onClose(); } @override void onClose() { print("--- KILLING ALL DRIVER TIMERS ---"); _rideTimer?.cancel(); _rideTimer = null; _waitingTimer?.cancel(); timer?.cancel(); timer = null; _navigationTimer?.cancel(); _navigationTimer = null; _posSub?.cancel(); _posSub = null; mapController?.dispose(); super.onClose(); } void onMapCreated(GoogleMapController controller) { mapController = controller; if (Get.isRegistered()) { myLocation = Get.find().myLocation; controller.animateCamera(CameraUpdate.newLatLngZoom(myLocation, 16)); } // بدء الاستماع للموقع للملاحة وتحديث الماركر startListeningStepNavigation(); } bool isCameraLocked = true; // للتحكم في تتبع الكاميرا Future startListeningStepNavigation() async { _posSub?.cancel(); _posSub = Geolocator.getPositionStream( locationSettings: LocationSettings( accuracy: LocationAccuracy.bestForNavigation, // دقة عالية للملاحة distanceFilter: 5, // تحديث كل 5 أمتار ), ).listen((position) { LatLng 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; // تجاهل الحركة الطفيفة } _lastRecordedLocation = newLoc; myLocation = newLoc; double heading = position.heading; double speedKmh = position.speed * 3.6; if (!isClosed) { // 1. تحديث الماركر updateMarker(); // 2. تحديث المسار (Smart Snapping) if (upcomingPathPoints.isNotEmpty) { _updateTraveledPolylineSmart(myLocation); } // 3. تحريك الكاميرا (Navigation Mode) // فقط إذا كان "القفل" مفعلاً والسرعة > 5 كم (لتجنب الدوران العشوائي عند الوقوف) if (isCameraLocked && mapController != null) { double bearing = (speedKmh > 5) ? heading : 0.0; // ملاحظة: يمكنك تخزين آخر bearing معروف واستخدامه عند التوقف لتحسين التجربة _animateCameraToNavigationMode(newLoc, heading); } // 4. فحص التعليمات الصوتية checkForNextStep(myLocation); // 5. فحص الوصول للوجهة (للإشعار) checkDestinationProximity(); update(); } }); } void changeStatusDriver() { status = 'On'; update(); } void changeDriverEndPage() { remainingTimeTimerRideBegin < 60 ? driverEndPage = 160 : 100; update(); } takeSnapMap() { mapController!.takeSnapshot(); } @override void dispose() { print("--- KILLING ALL DRIVER TIMERS ---"); _stopAllServices(); super.dispose(); } void _stopAllServices() { _rideTimer?.cancel(); _passengerTimer?.cancel(); _waitingTimer?.cancel(); _posSub?.cancel(); mapController?.dispose(); } Future openGoogleMapFromDriverToPassenger() async { var endLat = latLngPassengerLocation.latitude; var endLng = latLngPassengerLocation.longitude; var startLat = Get.find().myLocation.latitude; var startLng = Get.find().myLocation.longitude; String url = 'https://www.google.com/maps/dir/$startLat,$startLng/$endLat,$endLng/&directionsmode=driving'; if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url)); } else { throw 'Could not launch google maps'; } } void clearPolyline() { polyLines = []; polyLinesDestination = []; polylineCoordinates.clear(); polylineCoordinatesDestination.clear(); update(); } void changeRideToBeginToPassenger() { isRideBegin = true; passengerInfoWindowHeight = Get.height * .22; update(); } // متغير لمنع التكرار bool _isCancelReceived = false; /// **معالجة إلغاء الراكب الموحدة (Gatekeeper)** void processRideCancelledByPassenger(String reason, {String source = "Unknown"}) { if (_isCancelReceived) return; // تم المعالجة مسبقاً _isCancelReceived = true; Log.print("🚫 Ride Cancelled by Passenger via $source. Reason: $reason"); // 1. إيقاف التوجيه والتايمرات // stopNavigation(); // stopAllTimers(); // 2. تنظيف الحالة box.write(BoxName.rideStatus, 'Canceled'); // أو الحالة الافتراضية box.remove(BoxName.rideArguments); // 3. عرض رسالة للسائق if (Get.isDialogOpen == true) Get.back(); // إغلاق أي ديالوج مفتوح Get.defaultDialog( title: "تم إلغاء الرحلة".tr, titleStyle: TextStyle(color: Colors.red), barrierDismissible: false, content: Column( children: [ Icon(Icons.person_off, size: 50, color: Colors.orange), SizedBox(height: 10), Text( "${"Passenger cancelled the ride.".tr}\n${"Reason".tr}: $reason", textAlign: TextAlign.center, ), ], ), confirm: ElevatedButton( onPressed: () { Get.back(); // إغلاق الديالوج Get.offAll(() => HomeCaptain()); // العودة للرئيسية }, child: Text("OK".tr), ), ); // تشغيل صوت تنبيه للإلغاء // AudioService.playCancelSound(); } Future cancelTripFromDriverAfterApplied() async { if (formKeyCancel.currentState!.validate()) { Get.dialog(const Center(child: CircularProgressIndicator()), barrierDismissible: false); try { // 1. استدعاء السيرفر var response = await CRUD().post( link: "${AppLink.ride}/rides/cancel_ride_by_driver.php", payload: { "ride_id": (rideId).toString(), "driver_id": box.read(BoxName.driverID).toString(), "reason": (cancelTripCotroller.text) ?? '', "passenger_token": tokenPassenger.toString(), }); if (response['status'] == 'success') { // 🔥🔥 معالجة الحظر (The Penalty Logic) 🔥🔥 bool isBlocked = response['is_blocked'] ?? false; if (isBlocked) { String blockExpiryStr = response['block_until']; // "2024-10-10 14:30:00" // حفظ تاريخ فك الحظر في الذاكرة المحلية box.write(BoxName.blockUntilDate, blockExpiryStr); // تحويل الحالة لأوفلاين إجبارياً box.write(BoxName.statusDriverLocation, 'blocked'); // عرض رسالة العقوبة Get.snackbar("تم تقييد حسابك مؤقتاً ⛔", "بسبب كثرة الإلغاءات (3 مرات)، تم إيقاف استقبال الطلبات لمدة 4 ساعات.", duration: Duration(seconds: 8), backgroundColor: Colors.red, colorText: Colors.white, snackPosition: SnackPosition.BOTTOM); } else { // تحذير فقط int count = response['cancel_count'] ?? 0; Get.snackbar("تنبيه", "لقد ألغيت $count رحلات اليوم. الوصول لـ 3 سيعرضك للإيقاف المؤقت.", backgroundColor: Colors.orange); } // تنظيف البيانات box.remove(BoxName.rideArgumentsFromBackground); box.remove(BoxName.rideArguments); box.write(BoxName.rideStatus, 'Cancel'); // تسجيل محلي (اختياري) try { await sql.insertData({ 'order_id': rideId, 'created_at': DateTime.now().toString(), 'driver_id': box.read(BoxName.driverID), }, TableName.driverOrdersRefuse); } catch (_) {} Get.find().getRefusedOrderByCaptain(); if (Get.isDialogOpen == true) Get.back(); Get.offAll( () => HomeCaptain()); // العودة للرئيسية ليتم تطبيق الحظر هناك } else { if (Get.isDialogOpen == true) Get.back(); Get.snackbar("Error", "Failed to cancel ride"); } } catch (e) { if (Get.isDialogOpen == true) Get.back(); Log.print("Error: $e"); } } } Timer? _passengerTimer; // مرجع للتايمر لإيقافه لاحقاً /// **بدء مؤقت الوصول للراكب (Passenger Arrival Timer)** /// /// تقوم هذه الدالة بإدارة العد التنازلي للوقت المتوقع لوصول السائق إلى موقع الراكب. /// /// **آلية العمل:** /// 1. تتحقق من حالة الرحلة؛ إذا كانت قد بدأت بالفعل ('Begin')، تخفي نافذة المعلومات وتوقف العمل. /// 2. تحسب المدة الإجمالية من [durationToPassenger]. /// 3. تستخدم [Timer.periodic] لتحديث الواجهة كل ثانية بدقة عالية. /// 4. تقوم بحساب النسبة المئوية [progressToPassenger] وتنسيق الوقت المتبقي [stringRemainingTimeToPassenger]. /// 5. عند انتهاء الوقت، تُفعل زر بدء الرحلة [isBtnRideBegin]. void startTimerToShowPassengerInfoWindowFromDriver() { // 1. تنظيف أي تايمر سابق لضمان عدم تداخل العدادات _passengerTimer?.cancel(); // 2. التحقق من حالة الرحلة String currentStatus = box.read(BoxName.rideStatus) ?? ''; if (currentStatus == 'Begin') { Log.print('rideStatus from map: $currentStatus - Hiding Info Window'); isPassengerInfoWindow = false; update(); return; } // 3. تهيئة المتغيرات isPassengerInfoWindow = true; final int totalDuration = int.tryParse(durationToPassenger.toString()) ?? 60; // استخدام DateTime لضمان دقة الوقت وعدم التأثر ببطء الجهاز final DateTime endTime = DateTime.now().add(Duration(seconds: totalDuration)); // 4. بدء التايمر الدوري _passengerTimer = Timer.periodic(const Duration(seconds: 1), (timer) { // أمان: إيقاف التايمر إذا تم إغلاق الكنترولر if (isClosed) { timer.cancel(); return; } final DateTime now = DateTime.now(); final int remainingSeconds = endTime.difference(now).inSeconds; // أ) تحديث القيم if (remainingSeconds <= 0) { // انتهى الوقت remainingTimeToPassenger = 0; progressToPassenger = 1.0; stringRemainingTimeToPassenger = "00:00"; isBtnRideBegin = true; timer.cancel(); // إيقاف التايمر } else { // ما زال العد مستمراً remainingTimeToPassenger = remainingSeconds; // حساب النسبة المئوية (مع منع القسمة على صفر) progressToPassenger = totalDuration > 0 ? 1 - (remainingSeconds / totalDuration) : 0.0; // تنسيق الوقت (دقائق:ثواني) final int minutes = (remainingSeconds / 60).floor(); final int seconds = remainingSeconds % 60; stringRemainingTimeToPassenger = '$minutes:${seconds.toString().padLeft(2, '0')}'; } // ب) تحديث الواجهة update(); }); } String stringRemainingTimeToPassenger = ''; String stringRemainingTimeWaitingPassenger = ''; Timer? _waitingTimer; // متغير للتحكم في تايمر الانتظار /// **بدء مؤقت انتظار الراكب (Waiting For Passenger Timer)** /// /// تحسب الوقت الذي ينتظره السائق عند نقطة الانطلاق. /// /// **التحسينات:** /// 1. استبدال الحلقة التكرارية بـ [Timer.periodic] لأداء أفضل. /// 2. استخدام [DateTime] لحساب الوقت المتبقي بدقة حتى لو كان التطبيق في الخلفية. /// 3. إدارة حالات الخروج (بدء الرحلة أو انتهاء وقت الانتظار) بشكل أنظف. void startTimerToShowDriverWaitPassengerDuration() { // 1. تنظيف أي تايمر سابق _waitingTimer?.cancel(); // 2. حساب المدة الكلية بالثواني final int totalDurationSeconds = timeWaitingPassenger * 60; // 3. تحديد وقت الانتهاء المتوقع (للدقة) final DateTime endTime = DateTime.now().add(Duration(seconds: totalDurationSeconds)); Log.print("⏳ Driver Waiting Timer Started: $totalDurationSeconds seconds"); // 4. بدء التايمر الدوري _waitingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { // أ) التحقق من إغلاق الكنترولر if (isClosed) { timer.cancel(); return; } // ب) شرط الإيقاف الفوري: إذا بدأت الرحلة فعلياً if (isRideBegin) { remainingTimeInPassengerLocatioWait = 0; stringRemainingTimeWaitingPassenger = "00:00"; timer.cancel(); update(); return; } // ج) حساب الوقت المتبقي بناءً على الساعة الحالية final DateTime now = DateTime.now(); final int remainingSeconds = endTime.difference(now).inSeconds; // د) تحديث المتغيرات if (remainingSeconds <= 0) { // انتهى وقت الانتظار remainingTimeInPassengerLocatioWait = 0; progressInPassengerLocationFromDriver = 1.0; stringRemainingTimeWaitingPassenger = "00:00"; isdriverWaitTimeEnd = true; // تفعيل زر الإلغاء المدفوع timer.cancel(); } else { // ما زال الانتظار جارياً remainingTimeInPassengerLocatioWait = remainingSeconds; // حساب التقدم (من 0.0 إلى 1.0) // Elapsed = Total - Remaining final int elapsed = totalDurationSeconds - remainingSeconds; progressInPassengerLocationFromDriver = totalDurationSeconds > 0 ? (elapsed / totalDurationSeconds) : 1.0; // تنسيق النص final int minutes = (remainingSeconds / 60).floor(); final int seconds = remainingSeconds % 60; stringRemainingTimeWaitingPassenger = '$minutes:${seconds.toString().padLeft(2, '0')}'; } update(); }); } bool isSocialPressed = false; // نغير نوع الإرجاع إلى Future Future driverCallPassenger() async { try { // نضع الكود داخل try لضمان عدم توقف التطبيق عند انقطاع النت String scam = await getDriverScam(); int scamCount = int.tryParse(scam) ?? 0; // 1. منطق الحظر if (scamCount > 3) { box.write(BoxName.statusDriverLocation, 'on'); Get.find().stopLocationUpdates(); // إرسال تنبيه (لا ننتظره await لأنه لا يؤثر على المنع) CRUD().post( link: AppLink.addNotificationCaptain, payload: { 'driverID': box.read(BoxName.driverID), 'title': 'scams operations'.tr, 'body': 'you have connect to passengers and let them cancel the order' .tr, }, ); // نرجع false لمنع الاتصال return false; } // 2. تسجيل العملية (تسجيل آمن) if (isSocialPressed == true && passengerId != null && rideId != null) { box.write(BoxName.statusDriverLocation, 'off'); // لا نستخدم await هنا لكي لا نؤخر فتح الهاتف CRUD().post( link: AppLink.addDriverScam, payload: { 'driverID': box.read(BoxName.driverID), 'passengerID': passengerId, 'rideID': rideId, 'isDriverCallPassenger': 'true', }, ); } // نسمح بالاتصال return true; } catch (e) { // في حال حدث خطأ في النت أو السيرفر، هل تريد السماح له بالاتصال؟ // الأفضل نعم، حتى لا تتعطل الخدمة بسبب خطأ تقني، ونسجل الخطأ في اللوج print("Error in scam check: $e"); return true; } } // دالة مساعدة لحماية التطبيق من كراش الخرائط Future safeAnimateCamera(CameraUpdate cameraUpdate) async { if (isClosed || mapController == null) return; try { await mapController!.animateCamera(cameraUpdate); } catch (e) { Log.print("Camera error ignored"); } } Future getDriverScam() async { var res = await CRUD().post(link: AppLink.getDriverScam, payload: { 'driverID': box.read(BoxName.driverID), }); if (res == 'failure') { box.write(BoxName.statusDriverLocation, 'off'); return '0'; } var d = (res); Log.print('d: ${d}'); // 1. Check if the response status is 'failure' (API level check) if (d['status'] == 'failure') { // If the API status is failure, the message is a String (e.g., 'No ride scam record found') // and there's no 'count' array to read. return '0'; } // 2. Safely access the List/Map structure for 'count' // This assumes a successful response looks like: // {'status': 'success', 'message': [{'count': '12'}]} var messageData = d['message']; // Check if messageData is actually a List before accessing index [0] if (messageData is List && messageData.isNotEmpty && messageData[0] is Map) { return messageData[0]['count']; } // Fallback if the successful data structure is unexpected return '0'; // --- FIX END --- } void startRideFromStartApp() { // if (box.read(BoxName.rideStatus) == 'Begin') { changeRideToBeginToPassenger(); isPassengerInfoWindow = false; isRideStarted = true; isRideFinished = false; remainingTimeInPassengerLocatioWait = 0; timeWaitingPassenger = 0; box.write(BoxName.statusDriverLocation, 'on'); update(); // } rideIsBeginPassengerTimer(); } Position? currentPosition; /// **بدء الرحلة من طرف السائق (Start Ride)** /// /// تقوم هذه الدالة بالتحقق من المسافة بين السائق والراكب، فإذا كانت قريبة (< 400م)، /// ترسل طلباً للسيرفر لبدء الرحلة رسمياً. /// /// * السيرفر سيقوم بإرسال الإشعارات (Socket + FCM) للراكب تلقائياً. /// **بدء الرحلة (Start Ride)** /// /// التحسينات: /// 1. إظهار Loading لمنع تكرار الضغط. /// 2. استخدام التحديث الموجه (Targeted Update) لمنع وميض الشاشة. /// 3. معالجة فشل السيرفر (Revert Logic) لضمان عدم ضياع حالة الرحلة. Future startRideFromDriver() async { // 1. إظهار مؤشر تحميل فوري (Blocking) Get.dialog(const Center(child: CircularProgressIndicator()), barrierDismissible: false); try { // 2. التحقق من المسافة (Async) double distanceToPassenger = await calculateDistanceBetweenDriverAndPassengerLocation(); // إغلاق مؤشر التحميل لأننا حصلنا على النتيجة if (Get.isDialogOpen == true) Get.back(); if (distanceToPassenger < 100) { // زدت المسافة قليلاً لمرونة أكبر (150م) // --- أ) تحديث الحالة المحلية (Optimistic Update) --- changeRideToBeginToPassenger(); // تغيير المتغيرات الداخلية isPassengerInfoWindow = false; isRideStarted = true; isRideFinished = false; remainingTimeInPassengerLocatioWait = 0; timeWaitingPassenger = 0; // الحفظ في التخزين المحلي box.write(BoxName.statusDriverLocation, 'on'); box.write(BoxName.rideStatus, 'Begin'); // بدء الخدمات (الملاحة والعداد) await startListeningStepNavigation(); rideIsBeginPassengerTimer(); // --- ب) تحديث الواجهة (Targeted Updates Only) --- // نحدث فقط الأجزاء التي تتغير، بدلاً من إعادة رسم الخريطة كاملة // update(['PassengerInfo']); // لإخفاء نافذة معلومات الراكب // update(['DriverEndBar']); // لإظهار شريط إنهاء الرحلة // update(['SosConnect']); // لتفعيل زر الطوارئ (تأكد من وضع ID للودجت) update(); // --- ج) إرسال الطلب للسيرفر (Background) --- // لا ننتظر النتيجة لتعطيل الواجهة، لكن نعالج الخطأ إن حدث CRUD().post( link: "${AppLink.server}/ride/rides/start_ride.php", payload: { 'id': rideId.toString(), 'driver_id': box.read(BoxName.driverID).toString(), 'status': 'Begin', "passengerToken": tokenPassenger.toString() }).then((response) { // هنا يمكن التحقق مما إذا كان السيرفر قد رفض الطلب (اختياري) if (response['status'] == 'failure') { // Revert logic if needed (نادر الحدوث) Log.print("Server failed to start ride!"); } }); } else { // --- حالة الرفض (بعيد جداً) --- MyDialog().getDialog( 'You are far from passenger location'.tr, 'Please go closer to the passenger location (less than 150m)'.tr, () => Get.back() // إغلاق الديالوج فقط ); } } catch (e) { // تنظيف اللودينج في حال حدوث خطأ غير متوقع if (Get.isDialogOpen == true) Get.back(); Log.print("Error starting ride: $e"); Get.snackbar("Error", "Could not start ride. Please check internet."); } } calculateDistanceInMeter(LatLng prev, LatLng current) async { double distance2 = Geolocator.distanceBetween( prev.latitude, prev.longitude, current.latitude, current.longitude, ); return distance2; } double speedoMeter = 0; void updateLocation() async { try { for (var i = 0; i < remainingTimeTimerRideBegin; i++) { await Future.delayed(const Duration(seconds: 3)); await safeAnimateCamera( CameraUpdate.newCameraPosition( CameraPosition( bearing: Get.find().heading, target: myLocation, zoom: 17, ), ), ); // }); update(); } // Stop listening after ride finishes if (!isRideBegin) {} } catch (error) { debugPrint('Error listening to GPS: $error'); // Handle GPS errors gracefully } // Periodically call updateLocation again await Future.delayed(const Duration(seconds: 1)); updateLocation(); } calculateDistanceBetweenDriverAndPassengerLocation() async { Get.put(LocationController()); var res = await CRUD().get( link: AppLink.getLatestLocationPassenger, payload: {'rideId': (rideId)}); if (res != 'failure') { var passengerLatestLocationString = jsonDecode(res)['message']; double distance2 = Geolocator.distanceBetween( double.parse(passengerLatestLocationString[0]['lat'].toString()), double.parse(passengerLatestLocationString[0]['lng'].toString()), Get.find().myLocation.latitude, Get.find().myLocation.longitude, ); return distance2; } else { double distance2 = Geolocator.distanceBetween( latLngPassengerLocation.latitude, latLngPassengerLocation.longitude, Get.find().myLocation.latitude, Get.find().myLocation.longitude, ); return distance2; } } /// دالة مساعدة لحساب التكلفة (Logic Helper) double _calculateWaitingCost() { bool isEgypt = box.read(BoxName.countryCode) == 'Egypt'; double waitingMinutes = 5.0; if (isEgypt) { // معادلة مصر: (المسافة * 0.08) + (5 دقائق * 1) return (distanceBetweenDriverAndPassengerWhenConfirm * 0.08) + (waitingMinutes * 1.0); } else { // معادلة الأردن/أخرى: (المسافة * 11) + (5 دقائق * 0.06) // تأكد من منطق الوحدات هنا (هل المسافة بالكيلومتر أم بالمتر؟) return (distanceBetweenDriverAndPassengerWhenConfirm * 11) + (waitingMinutes * 0.06); } } Future addWaitingTimeCostFromPassengerToDriverWallet() async { // ... (فحص المسافة واللودينج كما هو) ... Get.dialog(const Center(child: CircularProgressIndicator()), barrierDismissible: false); try { // 1. حساب التكلفة double costOfWaiting = _calculateWaitingCost(); double costForPassenger = costOfWaiting * -1; // بالسالب // 2. تحديث البيانات التشغيلية (Main Server) // هذا الطلب لا يحتاج توكنات مالية، فقط تحديث حالة await CRUD().post( link: "${AppLink.ride}/rides/update_ride_cancel_wait.php", payload: { 'ride_id': rideId.toString(), 'driver_id': box.read(BoxName.driverID).toString(), }); // 3. توليد التوكنات (Server-Side Logic Security) // نحتاج توكن للسائق وتوكن للراكب final tokens = await Future.wait([ generateTokenDriver(costOfWaiting.toString()), generateTokenPassenger(costForPassenger.toString()) ]); // 4. تنفيذ العملية المالية الموحدة (Payment Server) var paymentResponse = await CRUD().postWallet( link: "${AppLink.paymentServer}/ride/passengerWallet/process_wait_compensation.php", // الرابط الجديد payload: { 'ride_id': rideId.toString(), 'driver_id': box.read(BoxName.driverID).toString(), 'passenger_id': passengerId.toString(), 'amount': costOfWaiting.toString(), // المبلغ الموجب 'amount_passenger': costForPassenger.toString(), // المبلغ السالب 'token_driver': tokens[0], 'token_passenger': tokens[1], }); if (paymentResponse['status'] == 'success') { // النجاح if (Get.isDialogOpen == true) Get.back(); Get.snackbar( 'Compensation Received'.tr, '${'You gained'.tr} ${costOfWaiting.toStringAsFixed(2)} ${'in your wallet'.tr}', backgroundColor: AppColor.deepPurpleAccent, ); box.write(BoxName.statusDriverLocation, 'off'); Get.offAll(() => HomeCaptain()); } else { throw Exception("Payment Transaction Failed"); } } catch (e) { if (Get.isDialogOpen == true) Get.back(); Log.print("Error: $e"); Get.snackbar("Error", "Transaction failed, please try again."); } } /// **التحقق من إنهاء الرحلة (Validate & Finish Ride)** /// /// تقوم هذه الدالة بحماية الرحلة من الإنهاء المبكر الخاطئ. /// /// **آلية العمل:** /// 1. تحسب المسافة الخطية (Displacement) التي قطعها السائق بعيداً عن نقطة الانطلاق. /// 2. تقارن هذه المسافة مع "عتبة دنيا" (Minimum Threshold) وهي (1/5) من إجمالي مسافة الرحلة. /// 3. **إذا تحقق الشرط:** تظهر نافذة تأكيد الإنهاء وتستدعي [finishRideFromDriver1]. /// 4. **إذا فشل الشرط:** تمنع الإنهاء وتصدر تنبيهاً صوتياً ونصياً بأن المسافة المقطوعة غير كافية. /// **التحقق من إنهاء الرحلة (Validate & Finish Ride)** /// /// [isFromSlider]: إذا كانت القيمة true، فهذا يعني أن السائق سحب الشريط /// وبالتالي هو موافق ضمنياً، فلا داعي لعرض ديالوج "هل أنت متأكد؟" Future finishRideFromDriver({bool isFromSlider = false}) async { // 1. تحويل مسافة الرحلة الكلية // 1. نقوم أولاً بتنظيف النص من أي حروف (مثل 'km' أو 'كم') ونبقي الأرقام والنقطة فقط String cleanDistance = distance.toString().replaceAll(RegExp(r'[^0-9.]'), ''); // 2. حماية إضافية: إذا كان النص فارغاً بعد التنظيف نعتبره صفر if (cleanDistance.isEmpty) cleanDistance = "0.0"; // 3. الآن التحويل سيتم بنجاح بدون أخطاء final double totalTripDistanceMeters = double.parse(cleanDistance) * 1000; // 2. حساب المسافة المقطوعة final double displacementMeters = Geolocator.distanceBetween( latLngPassengerLocation.latitude, latLngPassengerLocation.longitude, Get.find().myLocation.latitude, Get.find().myLocation.longitude, ); // 3. تحديد الحد الأدنى (الخمس) final double minimumDistanceThreshold = totalTripDistanceMeters / 5; Log.print('📏 Total Distance (m): $totalTripDistanceMeters'); Log.print('🚗 Moved Displacement (m): $displacementMeters'); // 4. اتخاذ القرار if (displacementMeters > minimumDistanceThreshold) { // ✅ الحالة سليمة if (isFromSlider) { // إذا جاء من السلايدر، نفذ فوراً finishRideFromDriver1(); } else { // إذا جاء من زر عادي (إن وجد)، اطلب التأكيد MyDialog().getDialog( 'Are you sure to exit ride?'.tr, '', () { Get.back(); finishRideFromDriver1(); }, ); } } else { // ❌ الحالة مرفوضة: المسافة غير كافية final textToSpeechController = Get.put(TextToSpeechController()); // إظهار رسالة خطأ حتى لو سحب السلايدر if (Get.isDialogOpen == true) Get.back(); // إغلاق أي ديالوج سابق MyDialog().getDialog( "You haven't moved sufficiently!".tr, "Please complete more distance before ending.".tr, () => Get.back(), ); await textToSpeechController .speakText("You haven't moved sufficiently!".tr); } } String paymentToken = ''; Future generateTokenDriver(String amount) async { var res = await CRUD().postWallet(link: AppLink.addPaymentTokenDriver, payload: { 'driverID': box.read(BoxName.driverID).toString(), 'amount': amount.toString(), }); var d = (res); return d['message']; } String paymentTokenPassenger = ''; Future generateTokenPassenger(String amount) async { var res = await CRUD() .postWallet(link: AppLink.addPaymentTokenPassenger, payload: { 'passengerId': passengerId, 'amount': amount.toString(), }); var d = (res); return d['message']; } // ... other controller code ... /// **إنهاء الرحلة ومعالجة المدفوعات (Finish Ride & Process Payments)** /// /// تقوم هذه الدالة بإدارة عملية إنهاء الرحلة كاملة من جانب السائق. /// /// **آلية العمل:** /// 1. **تحديث الواجهة:** تخفي نافذة السعر وتعرض مؤشر تحميل لمنع التكرار. /// 2. **حساب التكلفة:** تحسب السعر النهائي بناءً على نوع السيارة والمسافة، مع مراعاة الحد الأدنى للأجور. /// 3. **تجهيز البيانات:** تحضر البيانات اللازمة لتحديث حالة الرحلة (`rides`) وتسجيل الدفع (`payments`). /// 4. **التنفيذ المتوازي (Parallel Execution):** تستخدم `Future.wait` لإرسال طلب التحديث وطلب الدفع في آن واحد للسيرفر لتقليل وقت الانتظار. /// 5. **التحقق (Validation):** تتأكد من نجاح كلا العمليتين (التحديث والدفع) قبل الانتقال. /// 6. **الختام:** في حال النجاح، تحذف البيانات المؤقتة وتوجه السائق لصفحة التقييم. في حال الفشل، تعيد حالة الواجهة ليتمكن السائق من المحاولة مجدداً. /// **إنهاء الرحلة (Finish Ride)** /// /// التحسينات: /// 1. التحقق من المسافة المقطوعة (Anti-Fraud). /// 2. استخدام Future.wait لتنفيذ الطلبات بالتوازي (سرعة مضاعفة). /// 3. استخدام `finally` لضمان إغلاق اللودينج دائماً. Future finishRideFromDriver1({bool isFromSlider = false}) async { // 1. التحقق الأمني: هل تحرك السائق فعلاً؟ // (هذا الكود يجب أن يكون سريعاً ولا يعطل الواجهة) if (!await _validateTripDistance(isFromSlider)) return; // 2. إظهار لودينج (Blocking) لمنع التكرار Get.dialog(const Center(child: CircularProgressIndicator()), barrierDismissible: false); try { // 3. تحديث الحالة المحلية لمنع أي تفاعلات أخرى isRideFinished = true; isRideStarted = false; isPriceWindow = false; box.write(BoxName.rideStatus, 'Finished'); box.write(BoxName.statusDriverLocation, 'off'); // 4. حساب التكلفة النهائية (Logic) _calculateFinalTotalCost(); // 5. تجهيز البيانات (Payloads) final rideUpdatePayload = { 'rideId': rideId.toString(), 'driver_id': box.read(BoxName.driverID).toString(), 'status': 'Finished', 'price': totalCost, 'passengerId': passengerId.toString(), 'driver_token': box.read(BoxName.tokenDriver).toString(), 'passengerToken': tokenPassenger.toString(), }; // توليد توكن الدفع أولاً final String paymentAuthToken = await generateTokenDriver(paymentAmount.toString()); final paymentProcessingPayload = { 'rideId': rideId.toString(), 'driverId': box.read(BoxName.driverID).toString(), 'passengerId': passengerId.toString(), 'paymentAmount': paymentAmount, 'paymentMethod': paymentMethod, 'walletChecked': walletChecked.toString(), 'passengerWalletBurc': passengerWalletBurc.toString(), 'authToken': paymentAuthToken, }; // 6. التنفيذ المتوازي (Parallel Execution) - الأسرع final results = await Future.wait([ CRUD().post( link: "${AppLink.ride}/rides/finish_ride_updates.php", payload: rideUpdatePayload), CRUD().postWallet( link: "${AppLink.paymentServer}/ride/payment/process_ride_payments.php", payload: paymentProcessingPayload), ]); final rideRes = results[0]; final payRes = results[1]; // 7. التحقق من النجاح if (rideRes['status'] == 'success' && payRes['status'] == 'success') { // تنظيف البيانات box.remove(BoxName.rideArguments); box.remove(BoxName.rideArgumentsFromBackground); // إغلاق اللودينج if (Get.isDialogOpen == true) Get.back(); // إرسال تقرير السلوك (Fire and forget) Get.put(DriverBehaviorController()) .sendSummaryToServer(driverId, rideId); // الانتقال لصفحة التقييم Get.off(() => RatePassenger(), arguments: { 'passengerId': passengerId, 'rideId': rideId, 'price': paymentAmount.toString(), 'walletChecked': walletChecked.toString() ?? 'false' }); } else { throw Exception( "Server Error: Ride=${rideRes['status']}, Payment=${payRes['status']}"); } } catch (e) { // 8. معالجة الأخطاء (Revert State) if (Get.isDialogOpen == true) Get.back(); // إغلاق اللودينج Log.print("Error finishing ride: $e"); Get.snackbar( "Error".tr, "Failed to finish ride. Please check internet.".tr, backgroundColor: Colors.red, colorText: Colors.white); // إعادة الحالة للسماح للمستخدم بالمحاولة مرة أخرى isRideFinished = false; isRideStarted = true; box.write(BoxName.rideStatus, 'Begin'); update(); // تحديث الشريط ليعود للوضع النشط } } // --- دوال مساعدة (Helpers) لتنظيف الكود --- Future _validateTripDistance(bool isFromSlider) async { // منطق التحقق من المسافة المقطوعة (كما هو موجود لديك) String cleanDistance = distance.toString().replaceAll(RegExp(r'[^0-9.]'), ''); if (cleanDistance.isEmpty) cleanDistance = "0.0"; final double totalTripDistanceMeters = double.parse(cleanDistance) * 1000; final double displacementMeters = Geolocator.distanceBetween( latLngPassengerLocation.latitude, latLngPassengerLocation.longitude, Get.find().myLocation.latitude, Get.find().myLocation.longitude, ); final double minimumThreshold = totalTripDistanceMeters / 5; if (displacementMeters > minimumThreshold || isFromSlider) { if (isFromSlider) return true; // إذا لم يكن من السلايدر، نعرض تأكيد bool confirmed = false; MyDialog().getDialog('Exit Ride?'.tr, '', () { confirmed = true; Get.back(); }); return confirmed; } else { // المسافة غير كافية Get.find() .speakText("You haven't moved sufficiently!".tr); MyDialog().getDialog( "Warning".tr, "You haven't moved sufficiently!".tr, () => Get.back()); return false; } } void _calculateFinalTotalCost() { // منطق حساب السعر (كما هو موجود لديك) if (price < 172) { totalCost = (carType == 'Comfort' || carType == 'Mishwar Vip' || carType == 'Lady') ? '200' : '172'; } else if (price < double.parse(totalPricePassenger)) { totalCost = totalPricePassenger; } else { totalCost = (carType == 'Comfort' || carType == 'Mishwar Vip' || carType == 'Lady') ? price.toStringAsFixed(2) : totalPricePassenger; } paymentAmount = totalCost; } void cancelCheckRideFromPassenger() async { var res = await CRUD().get( link: "${AppLink.endPoint}/ride/driver_order/getOrderCancelStatus.php", payload: { 'order_id': (rideId), }); //.then((value) { var response = jsonDecode(res); canelString = response['data']['status']; update(); if (canelString == 'Cancel') { remainingTimeTimerRideBegin = 0; remainingTimeToShowPassengerInfoWindowFromDriver = 0; remainingTimeToPassenger = 0; isRideStarted = false; isRideFinished = false; isPassengerInfoWindow = false; clearPolyline(); update(); MyDialog().getDialog( 'Order Cancelled'.tr, 'Order Cancelled by Passenger'.tr, () { Get.offAll(HomeCaptain()); }, ); } } int rideTimerFromBegin = 0; double price = 0; DateTime currentTime = DateTime.now(); /// أثناء الرحلة: نعرض السعر لحظياً بدون مضاعفة العمولة في كل ثانية. /// - نستخدم سعر الدقيقة حسب الوقت، مع قواعد الرحلات البعيدة: /// >25كم أو >35كم => دقيقة = 600، سقف 60 دقيقة، ومع >35كم عفو 10 دقائق. /// - سرعة طويلة: لو المسافة المخططة > 40كم نستخدم 2600 ل.س/كم للـ Speed، //// ونطبق نفس نسبة التخفيض على Comfort/Electric/Van. /// - نضيف فقط "الزيادة" فوق التسعيرة المقتبسة (وقت زائد + كم زائد). /// - نعكس العمولة kazán مرة واحدة على الزيادة (وليس كل ثانية). // ========================================================= // الدالة الرئيسية المعدلة (العداد) // ========================================================= // متغيرات العداد الجديد Timer? _rideTimer; DateTime? _rideStartTime; DateTime? get rideStartTime => _rideStartTime; // إضافة هذا السطر double currentRideDistanceKm = 0.0; // لحساب المسافة // متغيرات المراقبة // =========================================================================== // 4. منطق العداد والتسعير (The Engine) // =========================================================================== void rideIsBeginPassengerTimer() { _rideTimer?.cancel(); _rideStartTime = DateTime.now(); currentRideDistanceKm = 0.0; // جلب الاعتماديات final loc = Get.find(); final hc = Get.find(); // إعداد متغيرات التسعير final double perKmSpeedBase = hc.speedPrice; final double perKmComfortRaw = hc.comfortPrice; final double perKmDelivery = hc.deliveryPrice; final double perKmVanRaw = hc.familyPrice; const double electricUpliftKm = 400; final double perKmElectricRaw = perKmComfortRaw + electricUpliftKm; final double perMinNature = hc.naturePrice; final double perMinLate = hc.latePrice; final double perMinHeavy = hc.heavyPrice; // القيم الأولية final double basePassengerQuote = safeParseDouble(totalCost); final int quotedMinutes = safeParseInt(duration) != 0 ? safeParseInt(duration) : 20; final double kazanPct = safeParseDouble(kazan) / 100.0; double plannedKm = safeParseDouble(distance); final double startKm = loc.totalDistance / 1000; if (plannedKm <= 0) plannedKm = (startKm > 0) ? startKm : 0.0; bool isAirport(String s) => s.toLowerCase().contains('airport') || s.contains('مطار'); final bool isAirportContext = isAirport(startNameLocation ?? '') || isAirport(endNameLocation ?? ''); double lastKmForNoise = loc.totalDistance / 1000; const double jitterMeters = 0.01; // 10 متر _rideTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (box.read(BoxName.rideStatus) != 'Begin') { timer.cancel(); return; } try { final now = DateTime.now(); final int elapsedSeconds = now.difference(_rideStartTime!).inSeconds; // أ) حساب المسافة المقطوعة (Logic) double currentTotalKm = loc.totalDistance / 1000; double delta = currentTotalKm - lastKmForNoise; if (delta.abs() > jitterMeters) { currentRideDistanceKm += delta; lastKmForNoise = currentTotalKm; } // ب) حساب السعر (Brain) price = _calculateCurrentPrice( now: now, elapsedSeconds: elapsedSeconds, liveKm: currentRideDistanceKm, plannedKm: plannedKm, startKm: startKm, quotedMinutes: quotedMinutes, basePassengerQuote: basePassengerQuote, kazanPct: kazanPct, isAirportContext: isAirportContext, perKmSpeedBase: perKmSpeedBase, perKmComfortRaw: perKmComfortRaw, perKmDelivery: perKmDelivery, perKmVanRaw: perKmVanRaw, perKmElectricRaw: perKmElectricRaw, perMinNature: perMinNature, perMinLate: perMinLate, perMinHeavy: perMinHeavy, ); // ج) تحديث واجهة المستخدم stringRemainingTimeRideBegin = "${currentRideDistanceKm.toStringAsFixed(2)} km"; speed = loc.speed * 3.6; update(); // تحديث الشريط فقط } catch (e) { print("Timer Error: $e"); } }); } double _calculateCurrentPrice({ required DateTime now, required int elapsedSeconds, required double liveKm, required double plannedKm, required double startKm, required int quotedMinutes, required double basePassengerQuote, required double kazanPct, required bool isAirportContext, required double perKmSpeedBase, required double perKmComfortRaw, required double perKmDelivery, required double perKmVanRaw, required double perKmElectricRaw, required double perMinNature, required double perMinLate, required double perMinHeavy, }) { // 🛑 1. قاعدة السعر الثابت: تجميد السعر if (carType == 'Speed' || carType == 'Fixed Price' || carType == 'Awfar Car') { return basePassengerQuote; } // 🟢 2. المنطق المتغير const double longTripPerMin = 600.0; const double mediumDistThresholdKm = 25.0; const double longDistThresholdKm = 35.0; // نسبة التخفيض double reductionPct = 0.0; if (liveKm > 40.0 && perKmComfortRaw > 0) { double r40 = (1.0 - (2600.0 / (perKmComfortRaw * 1.2))).clamp(0.0, 0.35); reductionPct = r40; if (liveKm > 100.0) { reductionPct = (r40 + 0.07).clamp(0.0, 0.35); } } // سعر الكيلومتر double finalPerKmRate; switch (carType) { case 'Comfort': case 'Mishwar Vip': case 'Lady': finalPerKmRate = perKmComfortRaw * (1.0 - reductionPct); break; case 'Electric': finalPerKmRate = perKmElectricRaw * (1.0 - reductionPct); break; case 'Van': finalPerKmRate = perKmVanRaw * (1.0 - reductionPct); break; case 'Delivery': finalPerKmRate = perKmDelivery; break; default: finalPerKmRate = perKmComfortRaw * (1.0 - reductionPct); } // سعر الدقيقة double perMinRate; if (liveKm > longDistThresholdKm) { perMinRate = longTripPerMin; } else { final h = now.hour; if (isAirportContext || h >= 21 || h < 1) { perMinRate = perMinLate; } else if (h >= 14 && h <= 17) { perMinRate = perMinHeavy; } else { perMinRate = perMinNature; } } // الفروقات final int elapsedMinutes = (elapsedSeconds ~/ 60); int extraMinutes = elapsedMinutes - quotedMinutes; if (extraMinutes < 0) extraMinutes = 0; double liveCostCalculation = (liveKm * finalPerKmRate) + (elapsedMinutes * perMinRate); double totalLivePrice = liveCostCalculation * (1.0 + kazanPct); // القرار النهائي (الأعلى بين المتفق عليه والمحسوب) return (totalLivePrice > basePassengerQuote) ? totalLivePrice : basePassengerQuote; } double recentDistanceToDash = 0; double recentAngelToMarker = 0; double speed = 0; void updateMarker() { // استخدم الماركر الذي يتحرك مع الخريطة markers.removeWhere((m) => m.markerId.value == 'MyLocation'); final locCtrl = Get.find(); myLocation = locCtrl.myLocation; markers.add( Marker( markerId: MarkerId('MyLocation'), position: myLocation, icon: carIcon, rotation: locCtrl.heading, anchor: const Offset(0.5, 0.5), flat: true, zIndex: 2, ), ); update(); } void addCustomCarIcon() { ImageConfiguration config = ImageConfiguration( size: const Size(30, 35), devicePixelRatio: Get.pixelRatio); BitmapDescriptor.asset( config, 'assets/images/car.png', // mipmaps: false, ).then((value) { carIcon = value; update(); }); } void addCustomStartIcon() async { // Create the marker with the resized image ImageConfiguration config = ImageConfiguration( size: const Size(30, 30), devicePixelRatio: Get.pixelRatio); BitmapDescriptor.asset( config, 'assets/images/A.png', ).then((value) { startIcon = value; update(); }); } void addCustomEndIcon() { ImageConfiguration config = ImageConfiguration( size: const Size(25, 25), devicePixelRatio: Get.pixelRatio); BitmapDescriptor.asset( config, 'assets/images/b.png', ).then((value) { endIcon = value; update(); }); } void addCustomPassengerIcon() { ImageConfiguration config = ImageConfiguration( size: const Size(30, 30), devicePixelRatio: Get.pixelRatio // scale: 1.0, ); BitmapDescriptor.asset( config, 'assets/images/picker.png', ).then((value) { passengerIcon = value; update(); }); } var activeRouteSteps = >[]; var traveledPathPoints = []; // المسار المقطوع (رمادي) var upcomingPathPoints = []; // المسار المتبقي (أزرق/أحمر) // --- متغيرات الأيقونات والمواقع --- var heading = 0.0; // ... يمكنك إضافة أيقونات البداية والنهاية هنا // --- متغيرات قياس الأداء الذكي --- final List _performanceReadings = []; final int _readingsToCollect = 10; // اجمع 10 قراءات bool _hasMadeDecision = false; var updateInterval = 5.obs; // القيمة الافتراضية // --- متغيرات داخلية للملاحة --- var _stepBounds = []; var _stepEndPoints = []; var _allPointsForActiveRoute = []; 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)'; } /// **جلب ورسم المسار (OSRM - New Standard System)** /// /// تستخدم السيرفر الجديد: https://routesjo.intaleq.xyz/route/v1/driving Future getRoute({ required LatLng origin, required LatLng destination, required Color routeColor, }) async { // 1. استخدام الرابط الجديد والإعدادات الصحيحة String coordinates = '${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}'; // استخدام الرابط من الكلاس المرجعي لأنه أحدث var url = "https://routesjo.intaleq.xyz/route/v1/driving/$coordinates?steps=true&overview=full"; try { var response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { var decoded = jsonDecode(response.body); if (decoded['code'] != 'Ok' || (decoded['routes'] as List).isEmpty) { mySnackeBarError("لم يتم العثور على مسار"); return; } var route = decoded['routes'][0]; // أ) تشغيل الـ Isolate لفك التشفير (ممتاز، ابقِ عليه) final String pointsString = route["geometry"]; List fullRoute = await compute(decodePolylineIsolate, pointsString); // ب) تهيئة المتغيرات upcomingPathPoints.assignAll(fullRoute); traveledPathPoints.clear(); _lastTraveledIndex = 0; // ج) رسم المسار الأولي polyLines.clear(); polyLines.add(Polyline( polylineId: const PolylineId("upcoming_route"), points: fullRoute, width: 8, color: routeColor, startCap: Cap.roundCap, endCap: Cap.roundCap, )); // د) معالجة الخطوات (Legs & Steps) List legs = route['legs']; if (legs.isNotEmpty) { final stepsList = List>.from(legs[0]['steps']); // 🔥 استخدام دالة الترجمة المحسنة من الكلاس المرجعي for (var step in stepsList) { step['html_instructions'] = _createInstructionFromManeuverSmart(step); // تصحيح مواقع المناورات if (step['maneuver'] != null && step['maneuver']['location'] != null) { var loc = step['maneuver']['location']; step['end_location'] = {'lat': loc[1], 'lng': loc[0]}; } } routeSteps = stepsList; currentStepIndex = 0; // نطق أول تعليمة if (routeSteps.isNotEmpty) { currentInstruction = routeSteps[0]['html_instructions']; Get.find().speakText(currentInstruction); } } // هـ) تحريك الكاميرا لتشمل المسار if (fullRoute.isNotEmpty) { final bounds = _boundsFromLatLngList(fullRoute); safeAnimateCamera(CameraUpdate.newLatLngBounds(bounds, 80)); } update(); } } catch (e) { Log.print("Route Error: $e"); } } // 🔥 دالة الترجمة المحسنة (من NavigationController) String _createInstructionFromManeuverSmart(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; default: instruction = "تابع المسير"; } if (name.isNotEmpty) { if (type == 'continue') { instruction += " على $name"; } else { instruction += " نحو $name"; } } return instruction; } 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 "اتجه"; } } /** * دالة لحساب حدود الخريطة (Bounds) من قائمة نقاط */ 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!)); } // الدالة التي يتم استدعاؤها من خدمة الموقع كل 5 ثوان (أو حسب الفترة المحددة) void onLocationUpdated(Position newPosition) { myLocation = LatLng(newPosition.latitude, newPosition.longitude); heading = newPosition.heading; // -->> منطق قياس الأداء يبدأ هنا <<-- final stopwatch = Stopwatch()..start(); // -->> منطق الملاحة وتحديث المسار <<-- _onLocationTick(myLocation); stopwatch.stop(); // -->> تحليل الأداء واتخاذ القرار <<-- if (!_hasMadeDecision) { _performanceReadings.add(stopwatch.elapsedMilliseconds); if (_performanceReadings.length >= _readingsToCollect) { _analyzePerformance(); _hasMadeDecision = true; } } } // ================================================================= // 3. منطق الملاحة الداخلي (Internal Navigation Logic) // ================================================================= void _onLocationTick(LatLng pos) { if (activeRouteSteps.isEmpty || currentStepIndex >= _stepBounds.length) { return; } final double dToEnd = _distanceMeters(pos, _stepEndPoints[currentStepIndex]); if (dToEnd <= 35) { // 35 متر عتبة للوصول لنهاية الخطوة _advanceStep(); } } void _advanceStep() { if (currentStepIndex >= _stepBounds.length - 1) { // وصل للنهاية currentInstruction = "لقد وصلت إلى وجهتك"; return; } currentStepIndex++; currentInstruction = _parseInstruction( activeRouteSteps[currentStepIndex]['html_instructions']); Get.isRegistered() ? Get.find().speakText(currentInstruction) : Get.put(TextToSpeechController()).speakText(currentInstruction); // -->> هنا يتم تحديث لون المسار <<-- _updateTraveledPath(); _fitToBounds(_stepBounds[currentStepIndex], padding: 80); // تقريب الكاميرا على الخطوة التالية update(); } // داخل MapDriverController Future markDriverAsArrived() async { // 1. إظهار لودينج فوراً لمنع التكرار وإشعار السائق Get.dialog( const Center( child: CircularProgressIndicator( color: AppColor.gold, )), barrierDismissible: false); try { double distance = await calculateDistanceBetweenDriverAndPassengerLocation(); if (distance < 100) { // 2. طلب الـ API await CRUD() .post(link: "${AppLink.ride}/rides/arrive_ride.php", payload: { "ride_id": rideId, "driver_id": box.read(BoxName.driverID), "passengerToken": tokenPassenger }); // 3. إغلاق اللودينج وتحديث الواجهة if (Get.isDialogOpen == true) Get.back(); // منطق بدء العداد startTimerToShowDriverWaitPassengerDuration(); isArrivedSend = false; // تحديث فقط الجزء الخاص بمعلومات الراكب update(); // رسم المسار للوجهة getRoute( origin: latLngPassengerLocation, destination: latLngPassengerDestination, routeColor: Colors.blue); } else { if (Get.isDialogOpen == true) Get.back(); mySnackeBarError("يجب أن تكون أقرب من 100 متر للوصول"); } } catch (e) { if (Get.isDialogOpen == true) Get.back(); mySnackeBarError("حدث خطأ في الاتصال"); } } void _updateTraveledPath() { // استخراج كل النقاط للخطوات التي تم اجتيازها List pointsForTraveledSteps = []; for (int i = 0; i < currentStepIndex; i++) { final stepPolyline = activeRouteSteps[i]['polyline']['points']; pointsForTraveledSteps.addAll(decodePolylineToLatLng(stepPolyline)); } traveledPathPoints.assignAll(pointsForTraveledSteps); } void _prepareStepData(List> steps) { _stepBounds.clear(); _stepEndPoints.clear(); for (final s in steps) { // 1. استخراج نقطة النهاية (الكود الحالي سليم) final end = s['end_location']; _stepEndPoints.add(LatLng( (end['lat'] as num).toDouble(), (end['lng'] as num).toDouble(), )); // 2. فك تشفير البوليلاين الخاص بالخطوة وتحويله إلى LatLng // -->> هنا تم التصحيح <<-- List pts = decodePolyline(s['polyline']['points']) .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) .toList(); // أضف نقاط البداية والنهاية إذا لم تكن موجودة في البوليلاين لضمان دقة الحدود if (pts.isNotEmpty) { final start = s['start_location']; final startLatLng = LatLng( (start['lat'] as num).toDouble(), (start['lng'] as num).toDouble()); if (pts.first != startLatLng) { pts.insert(0, startLatLng); } } _stepBounds.add(_boundsFromPoints(pts)); } } // A helper function to decode and convert the polyline string List decodePolylineToLatLng(String polylineString) { // 1. Decode the string into a list of number lists (e.g., [[lat, lng], ...]) List> decodedPoints = decodePolyline(polylineString); // 2. Map each [lat, lng] pair to a LatLng object, ensuring conversion to double List latLngPoints = decodedPoints .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) .toList(); return latLngPoints; } // ================================================================= // 4. منطق الأداء الذكي (Smart Performance Logic) // ================================================================= void _analyzePerformance() { final int sum = _performanceReadings.reduce((a, b) => a + b); final double averageTime = sum / _performanceReadings.length; if (averageTime > 1000) { // إذا كانت العملية تستغرق أكثر من ثانية _suggestOptimization(); } } void _suggestOptimization() { Get.snackbar( "تحسين أداء التطبيق", "لضمان أفضل تجربة، نقترح تعديل الإعدادات لتناسب جهازك. هل تود المتابعة؟", duration: const Duration(seconds: 15), mainButton: TextButton( child: const Text("نعم، قم بالتحسين"), onPressed: () { updateInterval.value = 8; // غير الفترة إلى 8 ثوانٍ // save setting to shared_preferences box.write(BoxName.updateInterval, 8); Get.back(); }, ), ); } // ================================================================= // 5. دوال مساعدة (Helper Functions) // ================================================================= void _resetRouteState() { activeRouteSteps.clear(); traveledPathPoints.clear(); upcomingPathPoints.clear(); _allPointsForActiveRoute.clear(); currentStepIndex = 0; } String _parseInstruction(String html) => html.replaceAll(RegExp(r'<[^>]*>'), ''); Future _fitToBounds(LatLngBounds b, {double padding = 60}) async { // نستخدم الدالة الآمنة التي أنشأناها await safeAnimateCamera(CameraUpdate.newLatLngBounds(b, padding)); } double distanceBetweenDriverAndPassengerWhenConfirm = 0; // فحص التعليمات الصوتية void checkForNextStep(LatLng currentPosition) { if (currentStepIndex >= routeSteps.length) return; final step = routeSteps[currentStepIndex]; final endLocation = step['end_location']; final endLatLng = LatLng(endLocation['lat'], endLocation['lng']); final distance = Geolocator.distanceBetween( currentPosition.latitude, currentPosition.longitude, endLatLng.latitude, endLatLng.longitude, ); if (distance < 50) { // 50 متر currentStepIndex++; if (currentStepIndex < routeSteps.length) { currentInstruction = routeSteps[currentStepIndex]['html_instructions']; playVoiceInstruction(currentInstruction); update(); } } } /// Calculates the distance in meters between two latitude/longitude points. double calculateDistance(double lat1, double lon1, double lat2, double lon2) { const double earthRadius = 6371000; // meters double dLat = _degreesToRadians(lat2 - lat1); double dLon = _degreesToRadians(lon2 - lon1); double a = (sin(dLat / 2) * sin(dLat / 2)) + cos(_degreesToRadians(lat1)) * cos(_degreesToRadians(lat2)) * (sin(dLon / 2) * sin(dLon / 2)); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distance = earthRadius * c; return distance; } double _degreesToRadians(double degrees) { return degrees * (3.1415926535897932 / 180.0); } bool isNearDestinationNotified = false; // فحص الوصول للوجهة للإشعارات void checkDestinationProximity() { if (isNearDestinationNotified) return; double dist = Geolocator.distanceBetween( myLocation.latitude, myLocation.longitude, latLngPassengerDestination.latitude, latLngPassengerDestination.longitude); if (dist < 300) { isNearDestinationNotified = true; NotificationService.sendNotification( target: tokenPassenger.toString(), title: "You are near the destination".tr, body: "The driver is approaching.".tr, isTopic: false, tone: 'ding', driverList: [], category: "Destination Proximity", ); } } List stepBounds = []; List stepEndPoints = []; List stepInstructions = []; StreamSubscription? _posSub; DateTime _lastCameraUpdateTs = DateTime.fromMillisecondsSinceEpoch(0); LatLngBounds _boundsFromPoints(List pts) { double? minLat, maxLat, minLng, maxLng; for (final p in pts) { minLat = (minLat == null) ? p.latitude : math.min(minLat, p.latitude); maxLat = (maxLat == null) ? p.latitude : math.max(maxLat, p.latitude); minLng = (minLng == null) ? p.longitude : math.min(minLng, p.longitude); maxLng = (maxLng == null) ? p.longitude : math.max(maxLng, p.longitude); } return LatLngBounds( southwest: LatLng(minLat ?? 0, minLng ?? 0), northeast: LatLng(maxLat ?? 0, maxLng ?? 0), ); } bool _contains(LatLngBounds b, LatLng p) { final south = math.min(b.southwest.latitude, b.northeast.latitude); final north = math.max(b.southwest.latitude, b.northeast.latitude); final west = math.min(b.southwest.longitude, b.northeast.longitude); final east = math.max(b.southwest.longitude, b.northeast.longitude); return (p.latitude >= south && p.latitude <= north && p.longitude >= west && p.longitude <= east); } double _distanceMeters(LatLng a, LatLng b) { // هافرساين مبسطة const R = 6371000.0; // m final dLat = _deg2rad(b.latitude - a.latitude); final dLng = _deg2rad(b.longitude - a.longitude); final s1 = math.sin(dLat / 2); final s2 = math.sin(dLng / 2); final aa = s1 * s1 + math.cos(_deg2rad(a.latitude)) * math.cos(_deg2rad(b.latitude)) * s2 * s2; final c = 2 * math.atan2(math.sqrt(aa), math.sqrt(1 - aa)); return R * c; } double _deg2rad(double d) => d * math.pi / 180.0; void updateCameraFromBoundsAfterGetMap(dynamic response) { final bounds = response["routes"][0]["bounds"]; LatLng northeast = LatLng(bounds['northeast']['lat'], bounds['northeast']['lng']); LatLng southwest = LatLng(bounds['southwest']['lat'], bounds['southwest']['lng']); // Create the LatLngBounds object LatLngBounds boundsData = LatLngBounds(northeast: northeast, southwest: southwest); // Fit the camera to the bounds var cameraUpdate = CameraUpdate.newLatLngBounds(boundsData, 140); safeAnimateCamera(cameraUpdate); } void changePassengerInfoWindow() { isPassengerInfoWindow = !isPassengerInfoWindow; passengerInfoWindowHeight = isPassengerInfoWindow == true ? 200 : 0; update(); } double mpg = 0; calculateConsumptionFuel() { mpg = Get.find().fuelPrice / 12; //todo in register car add mpg in box update(); } argumentLoading() async { try { passengerLocation = Get.arguments['passengerLocation']; passengerDestination = Get.arguments['passengerDestination']; duration = Get.arguments['Duration']; totalCost = Get.arguments['totalCost']; passengerId = Get.arguments['passengerId']; driverId = Get.arguments['driverId']; distance = Get.arguments['Distance']; passengerName = Get.arguments['name']; passengerEmail = Get.arguments['email']; totalPricePassenger = Get.arguments['totalPassenger']; passengerPhone = Get.arguments['phone']; walletChecked = Get.arguments['WalletChecked']; tokenPassenger = Get.arguments['tokenPassenger']; direction = Get.arguments['direction']; durationToPassenger = Get.arguments['DurationToPassenger']; rideId = Get.arguments['rideId']; durationOfRideValue = Get.arguments['durationOfRideValue']; paymentAmount = Get.arguments['paymentAmount']; paymentMethod = Get.arguments['paymentMethod']; isHaveSteps = Get.arguments['isHaveSteps']; step0 = Get.arguments['step0']; step1 = Get.arguments['step1']; step2 = Get.arguments['step2']; step3 = Get.arguments['step3']; step4 = Get.arguments['step4']; passengerWalletBurc = Get.arguments['passengerWalletBurc']; timeOfOrder = Get.arguments['timeOfOrder']; carType = Get.arguments['carType']; kazan = Get.arguments['kazan']; startNameLocation = Get.arguments['startNameLocation']; endNameLocation = Get.arguments['endNameLocation']; // Parse to double latlng(passengerLocation, passengerDestination); String lat = Get.find().myLocation.latitude.toString(); String lng = Get.find().myLocation.longitude.toString(); String origin = '$lat,$lng'; // Set the origin and destination coordinates for the Google Maps directions request. Future.delayed(const Duration(seconds: 1)); getRoute( origin: Get.find().myLocation, destination: latLngPassengerLocation, routeColor: Colors.yellow // أو أي لون ); update(); } catch (e) { Log.print("Error parsing arguments: $e"); } } latlng(String passengerLocation, passengerDestination) { double latPassengerLocation = double.parse(passengerLocation.toString().split(',')[0]); double lngPassengerLocation = double.parse(passengerLocation.toString().split(',')[1]); double latPassengerDestination = double.parse(passengerDestination.toString().split(',')[0]); double lngPassengerDestination = double.parse(passengerDestination.toString().split(',')[1]); latLngPassengerLocation = LatLng(latPassengerLocation, lngPassengerLocation); latLngPassengerDestination = LatLng(latPassengerDestination, lngPassengerDestination); } late Duration durationToAdd; int hours = 0; int minutes = 0; String carType = ''; late String kazan; late String startNameLocation; late String endNameLocation; Future runGoogleMapDirectly() async { if (box.read(BoxName.googlaMapApp) == true) { if (Platform.isAndroid) { Bubble().startBubbleHead(sendAppToBackground: true); } await openGoogleMapFromDriverToPassenger(); } } @override void onInit() async { mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY); // Get the passenger location from the arguments. await argumentLoading(); Get.put(FirebaseMessagesController()); runGoogleMapDirectly(); addCustomCarIcon(); addCustomPassengerIcon(); addCustomStartIcon(); addCustomEndIcon(); if (!Get.isRegistered()) { Get.put(TextToSpeechController(), permanent: true); // permanent: true تمنع حذفه عند تغيير الصفحات } // updateMarker(); // updateLocation(); startTimerToShowPassengerInfoWindowFromDriver(); // durationToAdd = Duration(seconds: int.parse(duration)); durationToAdd = Duration(seconds: parseDurationToInt(duration)); hours = durationToAdd.inHours; minutes = (durationToAdd.inMinutes % 60).round(); calculateConsumptionFuel(); // updateLocation();// for now to test it // cancelCheckRidefromPassenger(); // checkIsDriverNearPassenger(); super.onInit(); } int parseDurationToInt(dynamic value) { if (value == null) return 0; String text = value.toString(); // حذف كل شيء ما عدا الأرقام (الإنجليزية) String digits = text.replaceAll(RegExp(r'[^0-9]'), ''); if (digits.isEmpty) return 0; return int.tryParse(digits) ?? 0; } Timer? _navigationTimer; // أضف هذا المتغير في الكلاس LatLng? _lastRecordedLocation; // Future startListeningStepNavigation() async { // _posSub?.cancel(); // _navigationTimer?.cancel(); // _posSub = Geolocator.getPositionStream( // locationSettings: const LocationSettings( // accuracy: LocationAccuracy.high, // distanceFilter: 5, // قللناها لـ 5 أمتار لدقة أعلى // ), // ).listen((position) { // LatLng newLoc = LatLng(position.latitude, position.longitude); // // 🔥 Jitter Filter: تجاهل التحركات الطفيفة جداً (أقل من 2 متر) // if (_lastRecordedLocation != null) { // double dist = Geolocator.distanceBetween( // newLoc.latitude, // newLoc.longitude, // _lastRecordedLocation!.latitude, // _lastRecordedLocation!.longitude); // if (dist < 2.0) return; // } // _lastRecordedLocation = newLoc; // myLocation = newLoc; // heading = position.heading; // if (!isClosed) { // // نحدث الماركر فوراً // updateMarker(); // // 🔥 تحديث المسار بذكاء (Smart Route Snapping) // if (upcomingPathPoints.isNotEmpty) { // _updateTraveledPolylineSmart(myLocation); // } // // 🔥🔥 الجزء الأهم: تحريك الكاميرا لتتبع السيارة 🔥🔥 // if (mapController != null) { // _animateCameraToNavigationMode(position); // } // // التحقق من الخطوة التالية // checkForNextStep(myLocation); // update(); // } // }); // } void _animateCameraToNavigationMode(LatLng target, double bearing) { mapController?.animateCamera( CameraUpdate.newCameraPosition( CameraPosition( target: target, bearing: bearing, tilt: 45.0, // منظور ثلاثي الأبعاد (3D Perspective) zoom: 18.0, // تقريب للملاحة ), ), ); } // متغير لتتبع آخر نقطة وصلنا لها int _lastTraveledIndex = 0; // تحديث المسار الذكي void _updateTraveledPolylineSmart(LatLng currentPos) { if (upcomingPathPoints.isEmpty) return; // Sliding Window: نبحث فقط في الـ 60 نقطة القادمة int searchWindow = 60; int startIndex = _lastTraveledIndex; int endIndex = min(startIndex + searchWindow, upcomingPathPoints.length); double minDistance = double.infinity; int closestIndex = startIndex; bool foundCloser = false; for (int i = startIndex; i < endIndex; i++) { final point = upcomingPathPoints[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 > _lastTraveledIndex) { _lastTraveledIndex = closestIndex; final remaining = upcomingPathPoints.sublist(_lastTraveledIndex); final traveled = upcomingPathPoints.sublist(0, _lastTraveledIndex + 1); polyLines.removeWhere((p) => p.polylineId.value == 'upcoming_route'); polyLines.removeWhere((p) => p.polylineId.value == 'traveled_route'); // المسار المتبقي (أزرق) polyLines.add(Polyline( polylineId: const PolylineId('upcoming_route'), points: remaining, color: Colors.blue, width: 8, zIndex: 2, startCap: Cap.roundCap, endCap: Cap.roundCap, )); // المسار المقطوع (رمادي) polyLines.add(Polyline( polylineId: const PolylineId('traveled_route'), points: traveled, color: Colors.grey.withOpacity(0.8), width: 7, zIndex: 1, )); update(); } } void stopListeningStepNavigation() { _posSub?.cancel(); _posSub = null; } } double safeParseDouble(dynamic value, {double defaultValue = 0.0}) { if (value == null) return defaultValue; if (value is double) return value; if (value is int) return value.toDouble(); return double.tryParse(value.toString()) ?? defaultValue; } int safeParseInt(dynamic value, {int defaultValue = 0}) { if (value == null) return defaultValue; if (value is int) return value; return int.tryParse(value.toString()) ?? defaultValue; }