import 'dart:async'; import 'dart:convert'; import 'dart:ui'; import 'dart:math' show cos, max, min, pi, pow, sin, atan2; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import '../../../constant/api_key.dart'; import '../../../services/offline_map_service.dart'; import '../../../models/model/painter_copoun.dart'; import '../../../views/widgets/mycircular.dart'; import '../deep_link_controller.dart'; import '../../../constant/box_name.dart'; import '../../../constant/links.dart'; import '../../../constant/colors.dart'; import '../../../constant/style.dart'; import '../../../constant/country_polygons.dart'; import '../../../env/env.dart'; import '../../../main.dart'; // contains global 'box', 'sql' import '../../../print.dart'; import '../../../services/pip_service.dart'; import '../../../services/ride_live_notification.dart'; import '../../../views/home/map_page_passenger.dart'; import '../../../views/Rate/rate_captain.dart'; import '../../../views/Rate/rating_driver_bottom.dart'; import '../../../views/widgets/mydialoug.dart'; import '../../../views/widgets/elevated_btn.dart'; import '../../../views/home/map_widget.dart/car_details_widget_to_go.dart'; import '../../../views/home/map_widget.dart/select_driver_mishwari.dart'; import '../../functions/crud.dart'; import '../../functions/launch.dart'; import '../../payment/payment_controller.dart'; import '../points_for_rider_controller.dart'; import 'map_engine_controller.dart'; import 'location_search_controller.dart'; import 'nearby_drivers_controller.dart'; import 'ui_interactions_controller.dart'; import 'map_socket_controller.dart'; import '../decode_polyline_isolate.dart'; import '../ios_live_activity_service.dart'; import '../../firebase/local_notification.dart'; import '../../firebase/notification_service.dart'; import '../../functions/audio_record1.dart'; import '../../functions/package_info.dart'; import '../../functions/secure_storage.dart'; import '../vip_waitting_page.dart'; import '../device_performance.dart'; import 'ride_state.dart'; import '../../../views/widgets/error_snakbar.dart'; import 'package:flutter_confetti/flutter_confetti.dart' hide Circle; import 'package:crypto/crypto.dart'; class RideLifecycleController extends GetxController { // --- Missing variables from monolithic controller --- String currentRideId = ''; bool isDrawingRoute = false; bool isAnotherOreder = false; bool isWhatsAppOrder = false; LatLng startLocation = const LatLng(32, 35); LatLng endLocation = const LatLng(32, 35); String dynamicApiUrl = 'https://routec.intaleq.xyz/route'; String? cardNumber; bool isBeginRideFromDriverRunning = false; bool isDriversTokensSend = false; Map rideData = {}; Map dInfo = {}; List datadriverCarsLocationToPassengerAfterApplied = []; double distanceOfTrip = 0.0; double apiDistanceMeters = 0.0; double tax = 0.0; int selectedPassengerCount = 1; final GlobalKey increaseFeeFormKey = GlobalKey(); final GlobalKey messagesFormKey = GlobalKey(); final GlobalKey promoFormKey = GlobalKey(); String walletStr = '0'; double walletVal = 0.0; bool rideConfirm = false; LatLng driverLocationToPassenger = const LatLng(32, 35); final TextEditingController messageToDriver = TextEditingController(); int carsOrder = 0; Rx currentRideState = RideState.noRide.obs; String statusRide = 'wait'; String statusRideVip = 'wait'; bool statusRideFromStart = false; double distance = 0; double duration = 0; int durationToRide = 0; int remainingTime = 25; int remainingTimeToPassengerFromDriverAfterApplied = 60; int remainingTimeDriverWaitPassenger5Minute = 60; int timeToPassengerFromDriverAfterApplied = 0; Timer? timerToPassengerFromDriverAfterApplied; DateTime? _driverEtaUpdatedAt; int _driverEtaSecondsAtUpdate = 0; int _driverEtaCountdownTicks = 0; bool rideTimerBegin = false; double progressTimerRideBegin = 0; int remainingTimeTimerRideBegin = 60; String stringRemainingTimeRideBegin = ''; late String rideId = 'yet'; late String driverId = ''; late String make = ''; late String model = ''; late String carColor = ''; late String licensePlate = ''; late String driverName = ''; late String passengerName = ''; late String driverPhone = ''; late String colorHex = ''; late String carYear = ''; late String driverRate = '5.0'; late String driverToken = ''; double kazan = 8; double totalPassenger = 0; double totalDriver = 0; double costDistance = 0; double costDuration = 0; double averageDuration = 0; double totalCostPassenger = 0; double totalPassengerSpeed = 0; double totalPassengerBalash = 0; double totalPassengerComfort = 0; double totalPassengerElectric = 0; double totalPassengerLady = 0; double totalPassengerScooter = 0; double totalPassengerVan = 0; double totalPassengerRayehGai = 0; double totalPassengerRayehGaiComfort = 0; double totalPassengerRayehGaiBalash = 0; double latePrice = 0; double fuelPrice = 0; double heavyPrice = 0; double naturePrice = 0; bool isRideFinished = false; String stringRemainingTimeToPassenger = ''; String stringRemainingTimeDriverWaitPassenger5Minute = ''; bool isDriverInPassengerWay = false; bool isDriverArrivePassenger = false; bool isSearchingWindow = false; bool shouldFetch = true; double progressTimerToPassengerFromDriverAfterApplied = 0; double progressTimerDriverWaitPassenger5Minute = 0; bool isCashSelectedBeforeConfirmRide = false; bool isPassengerChosen = false; Timer? _masterTimer; Timer? _searchTimer; Timer? _timer; Timer? _uiCountdownTimer; bool _isArrivalProcessed = false; bool _isFinishProcessed = false; bool _isCancelProcessed = false; bool _isAcceptanceProcessed = false; bool _isRatingScreenOpen = false; bool _isRecalculatingRoute = false; String _rideAcceptedViaSource = "Unknown"; final double kDurationScalar = 1.5348; // مسافة الانحراف المسموح بها بالمتر قبل إعادة حساب المسار تلقائيًا للرحلة. // إذا انحرف السائق عن المسار بأكثر من هذه المسافة، يُعاد حساب المسار. final double _deviationThresholdMeters = 30.0; int _routeHeadingMismatchCount = 0; final Map _pollingIntervals = { RideState.noRide: 6, RideState.searching: 8, RideState.driverApplied: 10, RideState.driverArrived: 15, RideState.inProgress: 15, RideState.cancelled: 3600, RideState.finished: 3600, RideState.preCheckReview: 3600, }; Timer? _locationPollingTimer; List _currentDriverRoutePoints = []; double _currentDriverRouteDistanceMeters = 0.0; int _currentDriverRouteDurationSeconds = 0; int _currentSearchPhase = 0; bool _isFetchingDriverLocation = false; Timer? _watchdogTimer; final List _searchRadii = [2400, 3000, 3100]; final int _searchPhaseDurationSeconds = 30; final int _totalSearchTimeoutSeconds = 90; int _noRideSearchCount = 0; final int _noRideMaxTries = 3; final int _noRideIntervalSec = 5; DateTime? _noRideNextAllowed; bool _noRideSearchCapped = false; int _masterIntervalSeconds = -1; final StreamController _rideStatusStreamController = StreamController.broadcast(); Stream get rideStatusStream => _rideStatusStreamController.stream; final StreamController _beginRideStreamController = StreamController.broadcast(); Stream get beginRideStream => _beginRideStreamController.stream; final StreamController _timerStreamController = StreamController(); Stream get timerStream => _timerStreamController.stream; bool isTimerFromDriverToPassengerAfterAppliedRunning = true; bool isTimerRunning = false; int beginRideInterval = 10; Timer? _rideProgressTimer; bool _hasShownSpeedWarning = false; bool rideInProgress = true; double elapsedTimeInSeconds = 0; String stringElapsedTimeRideBeginVip = '0:00'; Map rideStatusFromStartApp = {}; bool isStartAppHasRide = false; late Duration durationToAdd; late DateTime newTime = DateTime.now(); String durationByPassenger = ''; int hours = 0; int minutes = 0; int selectedReason = -1; String? cancelNote; double latitudeWhatsApp = 0; double longitudeWhatsApp = 0; // Getters for linked controllers LocationSearchController get locSearch => Get.find(); MapEngineController get mapEngine => Get.find(); NearbyDriversController get nearbyDrivers => Get.find(); MapSocketController get mapSocket => Get.find(); UiInteractionsController get uiInteractions => Get.find(); // LocationSearchController pass-throughs LatLng get passengerLocation => locSearch.passengerLocation; set passengerLocation(LatLng val) => locSearch.passengerLocation = val; LatLng get newMyLocation => locSearch.newMyLocation; set newMyLocation(LatLng val) => locSearch.newMyLocation = val; LatLng get newStartPointLocation => locSearch.newStartPointLocation; set newStartPointLocation(LatLng val) => locSearch.newStartPointLocation = val; LatLng get myDestination => locSearch.myDestination; set myDestination(LatLng val) => locSearch.myDestination = val; String get startNameAddress => locSearch.startNameAddress; set startNameAddress(String val) => locSearch.startNameAddress = val; String get endNameAddress => locSearch.endNameAddress; set endNameAddress(String val) => locSearch.endNameAddress = val; List get placesCoordinate => locSearch.placesCoordinate; set placesCoordinate(List val) => locSearch.placesCoordinate = val; int get activeMenuWaypointCount => locSearch.activeMenuWaypointCount; set activeMenuWaypointCount(int val) => locSearch.activeMenuWaypointCount = val; List get menuWaypoints => locSearch.menuWaypoints; set menuWaypoints(List val) => locSearch.menuWaypoints = val; List get menuWaypointNames => locSearch.menuWaypointNames; set menuWaypointNames(List val) => locSearch.menuWaypointNames = val; bool get passengerStartLocationFromMap => locSearch.passengerStartLocationFromMap; set passengerStartLocationFromMap(bool val) => locSearch.passengerStartLocationFromMap = val; List get coordinatesWithoutEmpty => locSearch.coordinatesWithoutEmpty; // MapEngineController pass-throughs Set get markers => mapEngine.markers; set markers(Set val) { mapEngine.markers = val; mapEngine.update(); } Set get polyLines => mapEngine.polyLines; set polyLines(Set val) { mapEngine.polyLines = val; mapEngine.update(); } IntaleqMapController? get mapController => mapEngine.mapController; bool get isStyleLoaded => mapEngine.isStyleLoaded; set isStyleLoaded(bool val) => mapEngine.isStyleLoaded = val; bool get isBottomSheetShown => mapEngine.isBottomSheetShown; set isBottomSheetShown(bool val) => mapEngine.isBottomSheetShown = val; double get heightBottomSheetShown => mapEngine.heightBottomSheetShown; set heightBottomSheetShown(double val) => mapEngine.heightBottomSheetShown = val; bool get isPickerShown => mapEngine.isPickerShown; set isPickerShown(bool val) => mapEngine.isPickerShown = val; bool get isMarkersShown => mapEngine.isMarkersShown; set isMarkersShown(bool val) => mapEngine.isMarkersShown = val; bool get isMainBottomMenuMap => mapEngine.isMainBottomMenuMap; set isMainBottomMenuMap(bool val) => mapEngine.isMainBottomMenuMap = val; double get mainBottomMenuMapHeight => mapEngine.mainBottomMenuMapHeight; set mainBottomMenuMapHeight(double val) => mapEngine.mainBottomMenuMapHeight = val; bool get isWayPointSheet => mapEngine.isWayPointSheet; set isWayPointSheet(bool val) => mapEngine.isWayPointSheet = val; bool get isWayPointStopsSheet => mapEngine.isWayPointStopsSheet; set isWayPointStopsSheet(bool val) => mapEngine.isWayPointStopsSheet = val; bool get isWayPointStopsSheetUtilGetMap => mapEngine.isWayPointStopsSheetUtilGetMap; set isWayPointStopsSheetUtilGetMap(bool val) => mapEngine.isWayPointStopsSheetUtilGetMap = val; double get wayPointSheetHeight => mapEngine.wayPointSheetHeight; set wayPointSheetHeight(double val) => mapEngine.wayPointSheetHeight = val; double get cashConfirmPageShown => mapEngine.cashConfirmPageShown; set cashConfirmPageShown(double val) => mapEngine.cashConfirmPageShown = val; bool get isCashConfirmPageShown => mapEngine.isCashConfirmPageShown; set isCashConfirmPageShown(bool val) => mapEngine.isCashConfirmPageShown = val; bool get isCancelRidePageShown => mapEngine.isCancelRidePageShown; set isCancelRidePageShown(bool val) => mapEngine.isCancelRidePageShown = val; void changeCashConfirmPageShown() => mapEngine.changeCashConfirmPageShown(); void resetNoRideSearch() { _noRideSearchCount = 0; _noRideSearchCapped = false; _noRideNextAllowed = null; } double get paymentPageShown => mapEngine.paymentPageShown; set paymentPageShown(double val) => mapEngine.paymentPageShown = val; void changeCancelRidePageShow() => mapEngine.changeCancelRidePageShow(); // NearbyDriversController pass-throughs List get carsLocationByPassenger => nearbyDrivers.carsLocationByPassenger; set carsLocationByPassenger(List val) => nearbyDrivers.carsLocationByPassenger = val; List get driverCarsLocationToPassengerAfterApplied => nearbyDrivers.driverCarsLocationToPassengerAfterApplied; set driverCarsLocationToPassengerAfterApplied(List val) => nearbyDrivers.driverCarsLocationToPassengerAfterApplied = val; bool get noCarString => nearbyDrivers.noCarString; set noCarString(bool val) => nearbyDrivers.noCarString = val; double get speed => locSearch.speed; set speed(double val) => locSearch.speed = val; Timer? get locationPollingTimer => _locationPollingTimer; bool isActiveRideState() { return currentRideState.value == RideState.searching || currentRideState.value == RideState.driverApplied || currentRideState.value == RideState.driverArrived || currentRideState.value == RideState.inProgress; } void startMasterTimer() { _masterTimer?.cancel(); _masterTimer = Timer.periodic(const Duration(seconds: 13), (_) { _handleRideState(currentRideState.value); }); } void cancelMasterTimer() { _masterTimer?.cancel(); _masterTimer = null; } void startMasterTimerWithInterval(int seconds) { if (_masterTimer != null && _masterIntervalSeconds == seconds) return; _masterIntervalSeconds = seconds; _masterTimer?.cancel(); _masterTimer = Timer.periodic(Duration(seconds: seconds), (_) { _handleRideState(currentRideState.value); }); } void stopAllTimers() { Log.print('🛑 FORCE STOP: Stopping ALL Timers and Streams 🛑'); _masterTimer?.cancel(); _masterTimer = null; timerToPassengerFromDriverAfterApplied?.cancel(); timerToPassengerFromDriverAfterApplied = null; _timer?.cancel(); _timer = null; _uiCountdownTimer?.cancel(); _uiCountdownTimer = null; _locationPollingTimer?.cancel(); _locationPollingTimer = null; _watchdogTimer?.cancel(); _watchdogTimer = null; _searchTimer?.cancel(); _searchTimer = null; _rideProgressTimer?.cancel(); _rideProgressTimer = null; isTimerRunning = false; isBeginRideFromDriverRunning = false; _isFetchingDriverLocation = false; update(); } Future _handleRideState(RideState state) async { if (_isRatingScreenOpen) { Log.print('⛔ Rating Screen is Open. Skipping Logic.'); stopAllTimers(); return; } Log.print('Handling state: $state'); int effectivePollingInterval = _pollingIntervals[state] ?? 13; switch (state) { case RideState.noRide: final now = DateTime.now(); if (_noRideSearchCount >= _noRideMaxTries) { if (!_noRideSearchCapped) { _noRideSearchCapped = true; Log.print('[noRide] search capped at $_noRideMaxTries attempts'); } break; } if (_noRideNextAllowed != null && now.isBefore(_noRideNextAllowed!)) { break; } _noRideSearchCount++; Log.print('_noRideSearchCount: $_noRideSearchCount'); _noRideNextAllowed = now.add(Duration(seconds: _noRideIntervalSec)); nearbyDrivers.getCarsLocationByPassengerAndReloadMarker(); nearbyDrivers.getNearestDriverByPassengerLocation(); break; case RideState.cancelled: stopAllTimers(); break; case RideState.preCheckReview: stopAllTimers(); _checkLastRideForReview(); break; case RideState.searching: if (rideId == 'yet' || rideId.isEmpty) break; try { String statusFromServer = await getRideStatus(rideId); if (statusFromServer == 'Apply' || statusFromServer == 'Applied') { await processRideAcceptance(source: "Polling"); break; } } catch (e) { Log.print('Error polling getRideStatus: $e'); } final now = DateTime.now(); final int elapsedSeconds = now.difference(_searchStartTime!).inSeconds; if (elapsedSeconds > _totalSearchTimeoutSeconds) { stopAllTimers(); currentRideState.value = RideState.noRide; isSearchingWindow = false; update(); _showIncreaseFeeDialog(); break; } int targetPhase = (elapsedSeconds / _searchPhaseDurationSeconds).floor(); if (targetPhase >= _searchRadii.length) { targetPhase = _searchRadii.length - 1; } bool isNewPhase = targetPhase > _currentSearchPhase; bool timeToScanForNewDrivers = (elapsedSeconds % 15 == 0); if (isNewPhase || timeToScanForNewDrivers || elapsedSeconds < 5) { _currentSearchPhase = targetPhase; int currentRadius = _searchRadii[_currentSearchPhase]; Log.print( '[Search Logic] Scanning for drivers. Phase: $_currentSearchPhase, Radius: $currentRadius'); } if (elapsedSeconds < 5) { driversStatusForSearchWindow = 'Your order is being prepared'.tr; } else if (elapsedSeconds < 15) { driversStatusForSearchWindow = 'Your order sent to drivers'.tr; } else { driversStatusForSearchWindow = 'The drivers are reviewing your request'.tr; } update(); break; case RideState.driverApplied: if (!_isDriverAppliedLogicExecuted && !_isAcceptanceProcessed) { Log.print('[handleRideState] Execution driverApplied logic.'); rideAppliedFromDriver(true); _isDriverAppliedLogicExecuted = true; } if (!mapSocket.isSocketConnected) { try { String statusFromServer = await getRideStatus(rideId); if (statusFromServer == 'Arrived') { currentRideState.value = RideState.driverArrived; break; } else if (statusFromServer == 'Begin' || statusFromServer == 'inProgress') { processRideBegin(); break; } } catch (e) { Log.print('Error polling for Arrived/Begin status: $e'); } } if (!_isSocketHealthy()) { getDriverCarsLocationToPassengerAfterApplied(); } break; case RideState.driverArrived: if (!_isDriverArrivedLogicExecuted) { _isDriverArrivedLogicExecuted = true; startTimerDriverWaitPassenger5Minute(); uiInteractions.driverArrivePassengerDialoge(); } break; case RideState.inProgress: if (!mapSocket.isSocketConnected) { try { String statusFromServer = await getRideStatus(rideId); if (statusFromServer == 'Finished' || statusFromServer == 'finished') { Log.print( '🏁 DETECTED FINISHED: Killing processes and forcing Review.'); stopAllTimers(); currentRideState.value = RideState.preCheckReview; tripFinishedFromDriver(); _checkLastRideForReview(); return; } } catch (e) { Log.print('Error polling status: $e'); } } if (!_isRideBeginLogicExecuted) { _isRideBeginLogicExecuted = true; _executeBeginRideLogic(); } if (!_isSocketHealthy()) { getDriverCarsLocationToPassengerAfterApplied(); } break; case RideState.finished: tripFinishedFromDriver(); stopAllTimers(); effectivePollingInterval = 3600; break; } startMasterTimerWithInterval(effectivePollingInterval); } bool _isSocketHealthy() { return mapSocket.isSocketHealthy(); } Future _checkInitialRideStatus() async { await getRideStatusFromStartApp(); if (rideStatusFromStartApp['data'] == null) { currentRideState.value = RideState.noRide; _handleRideState(currentRideState.value); return; } String _status = rideStatusFromStartApp['data']['status'] ?? ''; String _lowerStatus = _status.toLowerCase(); if (_lowerStatus == 'waiting' || _lowerStatus == 'apply' || _lowerStatus == 'applied' || _lowerStatus == 'accepted' || _lowerStatus == 'arrived' || _lowerStatus == 'begin') { rideId = rideStatusFromStartApp['data']['rideId'].toString(); currentRideState.value = _lowerStatus == 'waiting' ? RideState.searching : (_lowerStatus == 'apply' || _lowerStatus == 'applied' || _lowerStatus == 'accepted') ? RideState.driverApplied : _lowerStatus == 'arrived' ? RideState.driverArrived : _lowerStatus == 'begin' ? RideState.inProgress : _lowerStatus == 'cancel' ? RideState.cancelled : RideState.noRide; } else if (_lowerStatus == 'finished') { if (rideStatusFromStartApp['data']['needsReview'] == 1) { currentRideState.value = RideState.preCheckReview; } else { currentRideState.value = RideState.noRide; } } else { currentRideState.value = RideState.noRide; } _handleRideState(currentRideState.value); } Future _checkLastRideForReview() async { Log.print('⭐ FORCE OPEN RATING PAGE (Get.to mode)'); await getRideStatusFromStartApp(); if (rideStatusFromStartApp['data'] == null) { currentRideState.value = RideState.noRide; startMasterTimer(); return; } String needsReview = rideStatusFromStartApp['data']['needsReview'].toString(); if (needsReview == '1') { _isRatingScreenOpen = true; var args = { 'driverId': rideStatusFromStartApp['data']['driver_id'].toString(), 'rideId': rideStatusFromStartApp['data']['rideId'].toString(), 'driverName': rideStatusFromStartApp['data']['driverName'], 'price': rideStatusFromStartApp['data']['price'], }; await Get.to( () => RatingDriverBottomSheet(), arguments: args, preventDuplicates: true, popGesture: false, ); Log.print('✅ Rating Page Closed. Resetting App.'); _isRatingScreenOpen = false; restCounter(); currentRideState.value = RideState.noRide; startMasterTimer(); } else { currentRideState.value = RideState.noRide; startMasterTimer(); } } DateTime? _searchStartTime; bool _isDriverAppliedLogicExecuted = false; bool _isDriverArrivedLogicExecuted = false; bool _isRideBeginLogicExecuted = false; String driversStatusForSearchWindow = ''; void startSearchingForDriver() async { if (currentRideState.value == RideState.searching) return; isSearchingWindow = true; currentRideState.value = RideState.searching; driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr; _searchStartTime = DateTime.now(); _currentSearchPhase = 0; update(); bool rideCreated = await postRideDetailsToServer(); if (!rideCreated) { isSearchingWindow = false; currentRideState.value = RideState.noRide; mySnackbarWarning("Could not create ride. Please try again.".tr); update(); return; } _addRideToWaitingTable(); mapSocket.initConnectionWithSocket(); } void _showIncreaseFeeDialog() { Get.dialog( CupertinoAlertDialog( title: Text("No drivers accepted your request yet".tr), content: Text( "Increasing the fare might attract more drivers. Would you like to increase the price?" .tr), actions: [ CupertinoDialogAction( child: Text("Cancel Ride".tr, style: TextStyle(color: AppColor.redColor)), onPressed: () { Get.back(); mapEngine.changeCancelRidePageShow(); }, ), CupertinoDialogAction( child: Text("Increase Fare".tr, style: TextStyle(color: AppColor.greenColor)), onPressed: () { Get.back(); double newPrice = totalPassenger * 1.10; increasePriceAndRestartSearch(newPrice); }, ), ], ), barrierDismissible: false, ); } Future increasePriceAndRestartSearch(double newPrice) async { totalPassenger = newPrice; update(); await CRUD() .post(link: "${AppLink.server}/ride/rides/update.php", payload: { "id": rideId, "price": newPrice.toStringAsFixed(2), }); Log.print( '[increasePrice] Price changed. Clearing notified list to resend.'); notifiedDrivers.clear(); _searchStartTime = DateTime.now(); _currentSearchPhase = 0; isSearchingWindow = true; update(); startMasterTimer(); } void _stopWaitPassengerTimer({bool resetUI = false}) { _waitPassengerTimer?.cancel(); _waitPassengerTimer = null; if (resetUI) { progressTimerDriverWaitPassenger5Minute = 0.0; remainingTimeDriverWaitPassenger5Minute = 0; stringRemainingTimeDriverWaitPassenger5Minute = '00:00'; update(); } } void _executeBeginRideLogic() { Log.print('[executeBeginRideLogic] execution of ride start logic...'); _stopWaitPassengerTimer(resetUI: true); timeToPassengerFromDriverAfterApplied = 0; remainingTime = 0; remainingTimeToPassengerFromDriverAfterApplied = 0; remainingTimeDriverWaitPassenger5Minute = 0; rideTimerBegin = true; statusRide = 'Begin'; isDriverInPassengerWay = false; isDriverArrivePassenger = false; box.write(BoxName.passengerWalletTotal, '0'); update(); rideIsBeginPassengerTimer(); runWhenRideIsBegin(); NotificationController().showNotification( 'Trip is Begin'.tr, 'The trip has started! Feel free to contact emergency numbers, share your trip, or activate voice recording for the journey' .tr, 'start'); } Future processRideBegin({String source = "Unknown"}) async { if (currentRideState.value == RideState.inProgress || _isRideStartedProcessed) { return; } _isRideStartedProcessed = true; currentRideState.value = RideState.inProgress; statusRide = 'Begin'; remainingTimeDriverWaitPassenger5Minute = 0; _stopWaitPassengerTimer(); // مسح الخطوط القديمة (pickup/direct) قبل رسم خط المرحلة الجديدة polyLines = polyLines .where((p) => p.polylineId.value != 'main_route' && p.polylineId.value != 'route_direct' && !p.polylineId.value.startsWith('driver_route')) .toSet(); // موقع السائق الحالي من آخر تحديث LatLng driverPos = passengerLocation; if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { driverPos = driverCarsLocationToPassengerAfterApplied.last; } // رسم المسار من موقع السائق إلى الهدف بخط أزرق مستمر await calculateDriverToPassengerRoute(driverPos, myDestination, isBeginPhase: true); rideIsBeginPassengerTimer(); runWhenRideIsBegin(); update(); } bool _isRideStartedProcessed = false; void updateDriverRouteMetrics({int? etaSeconds, double? distanceMeters}) { if (distanceMeters != null && distanceMeters > 0) { distanceByPassenger = distanceMeters.toStringAsFixed(0); } if (etaSeconds == null) return; final int clampedEta = max(0, etaSeconds); timeToPassengerFromDriverAfterApplied = clampedEta; remainingTimeToPassengerFromDriverAfterApplied = clampedEta; durationToPassenger = clampedEta; _driverEtaSecondsAtUpdate = clampedEta; _driverEtaUpdatedAt = DateTime.now(); final int minutes = (clampedEta / 60).floor(); final int seconds = clampedEta % 60; stringRemainingTimeToPassenger = '$minutes:${seconds.toString().padLeft(2, '0')}'; } void startTimerFromDriverToPassengerAfterApplied() { stopTimerFromDriverToPassengerAfterApplied(); if (isTimerRunning) return; isTimerRunning = true; isTimerFromDriverToPassengerAfterAppliedRunning = true; _driverEtaUpdatedAt ??= DateTime.now(); _driverEtaSecondsAtUpdate = timeToPassengerFromDriverAfterApplied; _driverEtaCountdownTicks = 0; timerToPassengerFromDriverAfterApplied = Timer.periodic(const Duration(seconds: 1), (timer) { bool isRideActive = (statusRide == 'Apply' || statusRide == 'Arrived' || statusRide == 'Begin' || currentRideState.value == RideState.driverApplied || currentRideState.value == RideState.driverArrived || currentRideState.value == RideState.inProgress); if (!isRideActive || !isTimerFromDriverToPassengerAfterAppliedRunning) { timer.cancel(); timerToPassengerFromDriverAfterApplied = null; isTimerRunning = false; return; } _driverEtaCountdownTicks++; if (!_timerStreamController.isClosed) { _timerStreamController.add(_driverEtaCountdownTicks); } final int secondsElapsedSinceEta = _driverEtaUpdatedAt == null ? 0 : DateTime.now().difference(_driverEtaUpdatedAt!).inSeconds; remainingTimeToPassengerFromDriverAfterApplied = _driverEtaSecondsAtUpdate - secondsElapsedSinceEta; if (remainingTimeToPassengerFromDriverAfterApplied < 0) { remainingTimeToPassengerFromDriverAfterApplied = 0; } int minutes = (remainingTimeToPassengerFromDriverAfterApplied / 60).floor(); int seconds = remainingTimeToPassengerFromDriverAfterApplied % 60; stringRemainingTimeToPassenger = '$minutes:${seconds.toString().padLeft(2, '0')}'; if (_driverEtaCountdownTicks % 5 == 0) { double currentProgress = 1 - (remainingTimeToPassengerFromDriverAfterApplied / (_driverEtaSecondsAtUpdate == 0 ? 1 : _driverEtaSecondsAtUpdate)); IosLiveActivityService.updateRideActivity( status: 'waiting', driverName: driverName, carDetails: '$make • $model • $carColor', etaText: stringRemainingTimeToPassenger, progress: currentProgress.clamp(0.0, 1.0), ); } if (_driverEtaCountdownTicks % beginRideInterval == 0) { uploadPassengerLocation(); } else { update(); } }); } void stopTimerFromDriverToPassengerAfterApplied() { isTimerFromDriverToPassengerAfterAppliedRunning = false; timerToPassengerFromDriverAfterApplied?.cancel(); timerToPassengerFromDriverAfterApplied = null; isTimerRunning = false; update(); } Timer? _waitPassengerTimer; static const int _waitPassengerTotalSeconds = 300; int _waitPassengerElapsedSeconds = 0; void startTimerDriverWaitPassenger5Minute() { if (currentRideState.value != RideState.driverArrived) return; stopTimerFromDriverToPassengerAfterApplied(); isTimerRunning = false; _stopWaitPassengerTimer(); isDriverArrivePassenger = true; isDriverInPassengerWay = false; timeToPassengerFromDriverAfterApplied = 0; _waitPassengerElapsedSeconds = 0; remainingTimeDriverWaitPassenger5Minute = _waitPassengerTotalSeconds; progressTimerDriverWaitPassenger5Minute = 0; int m = (remainingTimeDriverWaitPassenger5Minute / 60).floor(); int s = remainingTimeDriverWaitPassenger5Minute % 60; stringRemainingTimeDriverWaitPassenger5Minute = '$m:${s.toString().padLeft(2, '0')}'; update(); _waitPassengerTimer = Timer.periodic(const Duration(seconds: 1), (t) { if (currentRideState.value != RideState.driverArrived) { _stopWaitPassengerTimer(resetUI: true); if (currentRideState.value == RideState.inProgress) { isDriverArrivePassenger = false; } update(); return; } _waitPassengerElapsedSeconds++; int remaining = _waitPassengerTotalSeconds - _waitPassengerElapsedSeconds; if (remaining < 0) remaining = 0; remainingTimeDriverWaitPassenger5Minute = remaining; progressTimerDriverWaitPassenger5Minute = _waitPassengerElapsedSeconds / _waitPassengerTotalSeconds; int minutes = (remaining / 60).floor(); int seconds = remaining % 60; stringRemainingTimeDriverWaitPassenger5Minute = '$minutes:${seconds.toString().padLeft(2, '0')}'; update(); if (remaining == 0) { _stopWaitPassengerTimer(); } }); } void beginRideTimer() { Timer.periodic(const Duration(seconds: 1), (timer) { if (!timerController.isClosed) { timerController.add(timer.tick); } update(); }); } final timerController = StreamController(); void stopRideTimer() { timerController.close(); update(); } void rideIsBeginPassengerTimer() { _rideProgressTimer?.cancel(); _hasShownSpeedWarning = false; DateTime now = DateTime.now(); DateTime expectedArrivalTime = now.add(Duration(seconds: durationToRide)); var arrivalTime = DateFormat('hh:mm a').format(expectedArrivalTime); box.write(BoxName.arrivalTime, arrivalTime); Log.print("⏳ Ride Timer Started. Duration: $durationToRide sec"); _rideProgressTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { if (currentRideState.value != RideState.inProgress) { timer.cancel(); return; } DateTime currentNow = DateTime.now(); int remainingSeconds = expectedArrivalTime.difference(currentNow).inSeconds; if (remainingSeconds < 0) remainingSeconds = 0; remainingTimeTimerRideBegin = remainingSeconds; progressTimerRideBegin = durationToRide > 0 ? 1 - (remainingSeconds / durationToRide) : 1.0; int minutes = (remainingSeconds / 60).floor(); int seconds = remainingSeconds % 60; stringRemainingTimeRideBegin = '$minutes:${seconds.toString().padLeft(2, '0')}'; final percent = (progressTimerRideBegin * 100).clamp(0, 100).toInt(); if (remainingSeconds % 5 == 0 || remainingSeconds == 0) { IosLiveActivityService.updateRideActivity( status: 'ongoing', driverName: driverName, carDetails: '$make • $model • $carColor', etaText: stringRemainingTimeRideBegin, progress: progressTimerRideBegin.clamp(0.0, 1.0), ); } if (remainingSeconds % 60 == 0 || remainingSeconds == 0) { await RideLiveNotification.showTripInProgress( percentage: percent, etaText: stringRemainingTimeRideBegin, ); } if (speed > 100 && !_hasShownSpeedWarning) { _hasShownSpeedWarning = true; _triggerSpeedWarning(); } if (speed < 80 && _hasShownSpeedWarning) { _hasShownSpeedWarning = false; } if (remainingSeconds <= 0) { timer.cancel(); } update(); }); } void _triggerSpeedWarning() { NotificationController().showNotification("Warning: Speeding detected!".tr, 'You can call or record audio of this trip'.tr, 'tone1'); Get.defaultDialog( barrierDismissible: false, title: "Warning: Speeding detected!".tr, titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), content: Column( children: [ Icon(Icons.speed, size: 50, color: AppColor.redColor), const SizedBox(height: 10), Text( "We noticed the speed is exceeding 100 km/h. Please slow down for your safety..." .tr, textAlign: TextAlign.center, style: AppStyle.title, ), ], ), confirm: MyElevatedButton( title: "Share Trip Details".tr, kolor: AppColor.redColor, onPressed: () { Get.back(); uiInteractions.sosPassenger(); }, ), cancel: MyElevatedButton( title: "I'm Safe".tr, kolor: AppColor.greenColor, onPressed: () { Get.back(); }, ), ); } void rideIsBeginPassengerTimerVIP() async { rideInProgress = true; bool sendSOS = false; while (rideInProgress) { await Future.delayed(const Duration(seconds: 1)); elapsedTimeInSeconds++; int minutes = (elapsedTimeInSeconds / 60).floor(); int seconds = (elapsedTimeInSeconds % 60).toInt(); stringElapsedTimeRideBeginVip = '$minutes:${seconds.toString().padLeft(2, '0')}'; if (speed > 100 && !sendSOS) { Get.defaultDialog( barrierDismissible: false, title: "Warning: Speeding detected!".tr, titleStyle: AppStyle.title, content: Text( "We noticed the speed is exceeding 100 km/h. Please slow down for your safety. If you feel unsafe, you can share your trip details with a contact or call the police using the red SOS button." .tr, style: AppStyle.title, ), confirm: MyElevatedButton( title: "Share Trip Details".tr, onPressed: () { Get.back(); String message = "**Emergency SOS from Passenger:**\n"; message += "* ${'Origin'.tr}: $passengerLocation\n"; message += "* ${'Destination'.tr}: $myDestination\n"; message += "* ${'Driver Name'.tr}: $passengerName\n"; message += "* ${'Driver Car Plate'.tr}: $licensePlate\n\n"; message += "* ${'Driver Phone'.tr}: $driverPhone\n\n"; message += "${'Current Location'.tr}:https://www.google.com/maps/place/${passengerLocation.latitude},${passengerLocation.longitude} \n"; message += "Please help! Contact me as soon as possible.".tr; launchCommunication( 'whatsapp', box.read(BoxName.sosPhonePassenger), message); sendSOS = true; }, kolor: AppColor.redColor, ), cancel: MyElevatedButton( title: "Cancel".tr, onPressed: () { Get.back(); }, kolor: AppColor.greenColor, ), ); } update(); } } Future tripFinishedFromDriver() async { Log.print('🧹 Cleaning UI for Finish'); if (Get.isDialogOpen == true) Get.back(); if (Get.isBottomSheetOpen == true) Get.back(); statusRide = 'Finished'; currentRideState.value = RideState.finished; isSearchingWindow = false; rideTimerBegin = false; shouldFetch = false; stopAllTimers(); resetAllMapStates(); mapEngine.clearPolyline(); markers = {}; update(); } void listenToBeginRideStream() { beginRideStream.listen((status) { Log.print("Ride status: $status"); }, onError: (error) { Log.print("Error in Begin Ride Stream: $error"); }); } Future begiVIPTripFromPassenger() async { timeToPassengerFromDriverAfterApplied = 0; remainingTime = 0; isBottomSheetShown = false; remainingTimeToPassengerFromDriverAfterApplied = 0; remainingTimeDriverWaitPassenger5Minute = 0; rideTimerBegin = true; statusRideVip = 'Begin'; isDriverInPassengerWay = false; isDriverArrivePassenger = false; update(); rideIsBeginPassengerTimerVIP(); runWhenRideIsBegin(); } Future getRideStatusFromStartApp() async { try { var res = await CRUD().get( link: AppLink.getRideStatusFromStartApp, payload: {'passenger_id': box.read(BoxName.passengerID)}); Log.print('rideStatusFromStartApp: $res'); if (res == 'failure') { rideStatusFromStartApp = { 'data': {'status': 'NoRide', 'needsReview': false} }; isStartAppHasRide = false; } else { var decoded = jsonDecode(res); if (decoded['status'] == 'failure') { rideStatusFromStartApp = { 'data': {'status': 'NoRide', 'needsReview': false} }; isStartAppHasRide = false; } else { rideStatusFromStartApp = decoded; } } String status = (rideStatusFromStartApp['data']['status'] ?? '').toString(); String lowerStatus = status.toLowerCase(); if (lowerStatus == 'begin' || lowerStatus == 'apply' || lowerStatus == 'applied' || lowerStatus == 'accepted' || lowerStatus == 'arrived') { statusRide = status; isStartAppHasRide = true; final bool isBeginStatus = lowerStatus == 'begin'; final bool isPickupStatus = lowerStatus == 'apply' || lowerStatus == 'applied' || lowerStatus == 'accepted' || lowerStatus == 'arrived'; currentRideState.value = lowerStatus == 'begin' ? RideState.inProgress : lowerStatus == 'arrived' ? RideState.driverArrived : RideState.driverApplied; driverId = rideStatusFromStartApp['data']['driver_id']?.toString() ?? ''; driverName = rideStatusFromStartApp['data']['driverName']?.toString() ?? ''; driverRate = rideStatusFromStartApp['data']['rateDriver']?.toString() ?? '5.0'; final LatLng? pickupPoint = _parseLatLng( rideStatusFromStartApp['data']['start_location']?.toString()); final LatLng? destinationPoint = _parseLatLng( rideStatusFromStartApp['data']['end_location']?.toString()); if (pickupPoint != null) { passengerLocation = pickupPoint; } if (destinationPoint != null) { myDestination = destinationPoint; } statusRideFromStart = true; update(); // Safe recovery of trip data Map? tripData; try { var rawTrip = box.read(BoxName.tripData); if (rawTrip is Map) { tripData = Map.from(rawTrip); } } catch (e) { Log.print("Error reading BoxName.tripData: $e"); } String? pointsString = tripData?['polyline']; if (pointsString == null || pointsString.isEmpty) { // No local polyline saved: Re-fetch the route from API final String startLoc = rideStatusFromStartApp['data']['start_location'] ?? ''; final String endLoc = rideStatusFromStartApp['data']['end_location'] ?? ''; if (startLoc.isNotEmpty && endLoc.isNotEmpty) { Log.print("🔄 Re-fetching route from API: $startLoc -> $endLoc"); // Call getDirectionMap to fetch the route asynchronously getDirectionMap(startLoc, endLoc, []); } } else { List decodedPoints = await compute(decodePolylineIsolate, pointsString); mapEngine.clearPolyline(); mapEngine.polylineCoordinates.clear(); mapEngine.polylineCoordinates.addAll(decodedPoints); if (decodedPoints.isNotEmpty) { passengerLocation = pickupPoint ?? decodedPoints.first; myDestination = destinationPoint ?? decodedPoints.last; } var polyline = Polyline( polylineId: const PolylineId('main_route'), points: mapEngine.polylineCoordinates, width: 6, color: const Color(0xFF2196F3), ); polyLines = {...polyLines, polyline}; } timeToPassengerFromDriverAfterApplied = 0; remainingTime = 0; remainingTimeToPassengerFromDriverAfterApplied = 0; remainingTimeDriverWaitPassenger5Minute = 0; rideTimerBegin = isBeginStatus; isDriverInPassengerWay = false; isDriverArrivePassenger = false; // Safe durationToAdd parsing if (tripData != null && tripData['distance_m'] != null) { var distVal = tripData['distance_m']; if (distVal is Duration) { durationToAdd = distVal; } else if (distVal is num) { durationToAdd = Duration(seconds: distVal.toInt()); } } else { durationToAdd = Duration.zero; } mapSocket.initConnectionWithSocket(); if (isBeginStatus) { _isRideBeginLogicExecuted = true; _isRideStartedProcessed = true; await getDriverCarsLocationToPassengerAfterApplied(); if (driverCarsLocationToPassengerAfterApplied.isNotEmpty && myDestination.latitude != 0 && myDestination.longitude != 0) { await calculateDriverToPassengerRoute( driverCarsLocationToPassengerAfterApplied.last, myDestination, isBeginPhase: true, ); } rideIsBeginPassengerTimer(); runWhenRideIsBegin(); } else if (isPickupStatus) { _isAcceptanceProcessed = true; _isDriverAppliedLogicExecuted = true; await getDriverCarsLocationToPassengerAfterApplied(); if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { await calculateDriverToPassengerRoute( driverCarsLocationToPassengerAfterApplied.last, passengerLocation, ); startTimerFromDriverToPassengerAfterApplied(); } _startSocketWatchdog(); } update(); } } catch (e) { Log.print("Error getRideStatusFromStartApp: $e"); } } void driverArrivePassenger() { timeToPassengerFromDriverAfterApplied = 0; remainingTime = 0; update(); rideIsBeginPassengerTimer(); } void cancelTimerToPassengerFromDriverAfterApplied() { timerToPassengerFromDriverAfterApplied?.cancel(); } Future postRideDetailsToServer() async { if (mapEngine.polylineCoordinates.isEmpty) return false; LatLng startLoc = mapEngine.polylineCoordinates.first; LatLng endLoc = mapEngine.polylineCoordinates.last; Map payload = { "start_location": '${startLoc.latitude},${startLoc.longitude}', "end_location": '${endLoc.latitude},${endLoc.longitude}', "date": DateTime.now().toString(), "time": DateTime.now().toString(), "endtime": "00:00:00", "price": totalPassenger.toStringAsFixed(2), "passenger_id": box.read(BoxName.passengerID).toString(), "driver_id": "0", "status": "waiting", "carType": box.read(BoxName.carType), "price_for_driver": totalPassenger.toString(), "price_for_passenger": totalME.toString(), "distance": distance.toString(), "passenger_name": box.read(BoxName.name).toString(), "passenger_phone": box.read(BoxName.phone).toString(), "passenger_token": box.read(BoxName.tokenFCM).toString(), "passenger_email": box.read(BoxName.email).toString(), "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), "passenger_rating": (passengerRate ?? 5.0).toString(), "start_name": startNameAddress, "end_name": endNameAddress, "duration_text": "${(durationToRide / 60).floor()}", "distance_text": "$distance", "is_wallet": Get.find().isWalletChecked.toString(), "has_steps": Get.find().wayPoints.length > 1 ? 'true' : 'false', "step0": placesCoordinate.length > 0 ? placesCoordinate[0] : "", "step1": placesCoordinate.length > 1 ? placesCoordinate[1] : "", "step2": placesCoordinate.length > 2 ? placesCoordinate[2] : "", "step3": placesCoordinate.length > 3 ? placesCoordinate[3] : "", "step4": placesCoordinate.length > 4 ? placesCoordinate[4] : "", }; Log.print(' 📦 Payload: $payload'); try { var response = await CRUD().post( link: "${AppLink.server}/ride/rides/add_ride.php", payload: payload); var jsonResponse = (response is String) ? jsonDecode(response) : response; if (jsonResponse['status'] == 'success') { rideId = jsonResponse['message'].toString(); Log.print("✅ Ride Created ID: $rideId"); return true; } else { Log.print("❌ Ride Creation Failed: $response"); return false; } } catch (e) { Log.print("❌ Exception in postRide: $e"); return false; } } Future rideAppliedFromDriver(bool isApplied) async { Log.print('[rideAppliedFromDriver] 🚀 Starting logic...'); await getUpdatedRideForDriverApply(rideId); if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) { NotificationController().showNotification('Fixed Price'.tr, 'The captain is responsible for the route.'.tr, 'ding'); } else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) { NotificationController().showNotification('Attention'.tr, 'The price may increase if the route changes.'.tr, 'ding'); } isApplied = true; statusRide = 'Apply'; rideConfirm = false; isSearchingWindow = false; _isDriverAppliedLogicExecuted = true; update(); // إيقاف جلب السيارات المجاورة ومسحها، باستثناء السائق الذي قبل الطلب mapEngine.reloadStartApp = false; mapEngine.markers.removeWhere((marker) => marker.markerId.value != driverId.toString()); mapEngine.update(); await getDriverCarsLocationToPassengerAfterApplied(); if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; Log.print( '[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation'); await getInitialDriverDistanceAndDuration(driverPos, passengerLocation); await drawDriverPathOnly(driverPos, passengerLocation); mapEngine.fitCameraToPoints(driverPos, passengerLocation); } startTimerFromDriverToPassengerAfterApplied(); } Future getInitialDriverDistanceAndDuration( LatLng driverPos, LatLng passengerPos) async { final String apiUrl = 'https://routec.intaleq.xyz/route'; final String apiKey = Env.mapKeyOsm; final String origin = '${driverPos.latitude},${driverPos.longitude}'; final String dest = '${passengerPos.latitude},${passengerPos.longitude}'; final Uri uri = Uri.parse( '$apiUrl?origin=$origin&destination=$dest&steps=false&overview=false'); try { final response = await http.get(uri, headers: {'X-API-KEY': apiKey}); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['status'] == 'ok') { double durationSecondsRaw = (data['duration_s'] as num).toDouble(); int finalDurationSeconds = (durationSecondsRaw * kDurationScalar).toInt(); double distanceMeters = (data['distance_m'] as num).toDouble(); updateDriverRouteMetrics( etaSeconds: finalDurationSeconds, distanceMeters: distanceMeters, ); update(); } } } catch (e) { Log.print('Error getInitialDriverDistanceAndDuration: $e'); } } int durationToPassenger = 0; String distanceByPassenger = ''; Future drawDriverPathOnly(LatLng driverPos, LatLng passengerPos) async { final String apiUrl = 'https://routec.intaleq.xyz/route'; final String apiKey = Env.mapKeyOsm; final String origin = '${driverPos.latitude},${driverPos.longitude}'; final String dest = '${passengerPos.latitude},${passengerPos.longitude}'; final Uri uri = Uri.parse( '$apiUrl?origin=$origin&destination=$dest&steps=false&overview=full'); try { final response = await http.get(uri, headers: {'X-API-KEY': apiKey}); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['status'] == 'ok' && data['polyline'] != null) { final String pointsString = data['polyline']; List decodedPoints = await compute(decodePolylineIsolate, pointsString); _currentDriverRoutePoints = decodedPoints; final double decodedDistance = _pathDistanceMeters(decodedPoints); if (decodedDistance > 0) { _currentDriverRouteDistanceMeters = decodedDistance; } polyLines = polyLines .where((p) => !p.polylineId.value.startsWith('driver_route') && p.polylineId.value != 'main_route' && p.polylineId.value != 'route_primary' && p.polylineId.value != 'route_direct') .toSet(); // رسم خط صلب (Solid) من السائق للراكب final Polyline driverSolidPolyline = Polyline( polylineId: const PolylineId('driver_route_solid'), points: decodedPoints, color: Colors.amber, // مسار القدوم باللون الأصفر width: 5, ); polyLines.add(driverSolidPolyline); update(); } } } catch (e) { Log.print('Error drawing driver path: $e'); } } void listenToRideStatusStream() { rideStatusStream.listen((rideStatus) { Log.print("Ride Status: $rideStatus"); }, onError: (error) { Log.print("Error in Ride Status Stream: $error"); }); } void start15SecondTimer(String rideId) {} void startUiCountdown() { _uiCountdownTimer?.cancel(); progress = 0; remainingTime = durationTimer; _uiCountdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { int i = timer.tick; progress = i / durationTimer; remainingTime = durationTimer - i; if (remainingTime <= 0) { timer.cancel(); rideConfirm = false; timeToPassengerFromDriverAfterApplied += durationToPassenger; timerEnded(); } update(); }); } double progress = 0; int durationTimer = 9; void timerEnded() async { runEvery30SecondsUntilConditionMet(); isCancelRidePageShown = false; update(); } late String driverCarModel = '', driverCarMake = '', driverLicensePlate = ''; Future getUpdatedRideForDriverApply(String rideId) async { if (rideId == 'yet' || rideId.isEmpty) return; try { final res = await CRUD().get( link: "${AppLink.server}/ride/rides/getRideOrderID.php", payload: {'passengerID': box.read(BoxName.passengerID).toString()}); if (res != 'failure') { var response = jsonDecode(res); Log.print('getUpdatedRideForDriverApply Response: $response'); if (response['status'] == 'success' && response['data'] != null && response['data'] is Map) { var data = response['data']; driverId = data['driver_id']?.toString() ?? ''; driverPhone = data['phone']?.toString() ?? ''; driverCarMake = data['make']?.toString() ?? ''; model = data['model']?.toString() ?? ''; colorHex = data['color_hex']?.toString() ?? ''; carColor = data['color']?.toString() ?? ''; make = data['make']?.toString() ?? ''; licensePlate = data['car_plate']?.toString() ?? ''; String firstName = data['passengerName']?.toString() ?? ''; String lastName = data['last_name']?.toString() ?? ''; passengerName = lastName.isNotEmpty ? "$firstName $lastName" : firstName; driverName = data['driverName']?.toString() ?? ''; driverToken = data['token']?.toString() ?? ''; carYear = data['year']?.toString() ?? ''; driverRate = data['ratingDriver']?.toString() ?? '5.0'; update(); } } } catch (e) { Log.print("Error in getUpdatedRideForDriverApply: $e"); } } String getLocationArea(double latitude, double longitude) { LatLng passengerPoint = LatLng(latitude, longitude); String previousCountry = box.read(BoxName.countryCode)?.toString() ?? ''; String newCountry = 'Jordan'; if (isPointInPolygon(passengerPoint, CountryPolygons.jordanBoundary)) { newCountry = 'Jordan'; } else if (isPointInPolygon( passengerPoint, CountryPolygons.syriaBoundary)) { newCountry = 'Syria'; } else if (isPointInPolygon( passengerPoint, CountryPolygons.egyptBoundary)) { newCountry = 'Egypt'; } else { newCountry = 'Jordan'; } box.write(BoxName.countryCode, newCountry); box.write(BoxName.serverChosen, AppLink.server); if (newCountry != previousCountry) { unawaited(getKazanPercent()); } return newCountry; } bool isPointInPolygon(LatLng point, List polygon) { int intersections = 0; for (int i = 0; i < polygon.length; i++) { LatLng vertex1 = polygon[i]; LatLng vertex2 = polygon[(i + 1) % polygon.length]; if (_rayIntersectsSegment(point, vertex1, vertex2)) { intersections++; } } return intersections % 2 != 0; } bool _rayIntersectsSegment(LatLng point, LatLng vertex1, LatLng vertex2) { double px = point.longitude; double py = point.latitude; double v1x = vertex1.longitude; double v1y = vertex1.latitude; double v2x = vertex2.longitude; double v2y = vertex2.latitude; if ((py < v1y && py < v2y) || (py > v1y && py > v2y)) { return false; } double intersectX = v1x + (py - v1y) * (v2x - v1x) / (v2y - v1y); return intersectX > px; } String generateTrackingLink(String rideId, String driverId) { String cleanRideId = rideId.toString().trim(); String cleanDriverId = driverId.toString().trim(); const String secretSalt = "Intaleq_Secure_Track_2025"; String rawString = "$cleanRideId$cleanDriverId$secretSalt"; var bytes = utf8.encode(rawString); var digest = md5.convert(bytes); String token = digest.toString(); return "https://intaleqapp.com/track/index.php?id=$cleanRideId&token=$token"; } calcualateDistsanceInMetet(LatLng prev, current) async { double distance2 = Geolocator.distanceBetween( prev.latitude, prev.longitude, current.latitude, current.longitude, ); return distance2; } uploadPassengerLocation() async { await CRUD().post(link: AppLink.addpassengerLocation, payload: { "passengerId": box.read(BoxName.passengerID), "lat": passengerLocation.latitude.toString(), "lng": passengerLocation.longitude.toString(), "rideId": rideId.toString() }); } void _showRideStartNotifications() { if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) { NotificationController().showNotification('Fixed Price'.tr, 'The captain is responsible for the route.'.tr, 'ding'); } else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) { NotificationController().showNotification('Attention'.tr, 'The price may increase if the route changes.'.tr, 'ding'); } } bool promoTaken = false; final promo = TextEditingController(); void applyPromoCodeToPassenger(BuildContext context) async { if (promoTaken == true) { MyDialog().getDialog( 'Promo Already Used'.tr, 'You have already used this promo code.'.tr, () => Get.back(), ); return; } if (!promoFormKey.currentState!.validate()) return; const double minPromoLowSYP = 172; const double minPromoHighSYP = 200; try { final value = await CRUD().get( link: AppLink.getPassengersPromo, payload: {'promo_code': promo.text}, ); if (value == 'failure') { MyDialog().getDialog( 'Promo Ended'.tr, 'The promotion period has ended.'.tr, () => Get.back(), ); return; } final bool eligibleNow = (totalPassengerSpeed >= minPromoLowSYP) || (totalPassengerBalash >= minPromoLowSYP) || (totalPassengerComfort >= minPromoHighSYP) || (totalPassengerElectric >= minPromoHighSYP) || (totalPassengerLady >= minPromoHighSYP); if (!eligibleNow) { Get.snackbar( 'Lowest Price Achieved'.tr, 'Cannot apply further discounts.'.tr, backgroundColor: AppColor.yellowColor, ); return; } final decode = jsonDecode(value); if (decode["status"] != "success") { MyDialog().getDialog( 'Promo Ended'.tr, 'The promotion period has ended.'.tr, () => Get.back(), ); return; } Get.snackbar('Promo Code Accepted'.tr, '', backgroundColor: AppColor.greenColor); final firstElement = decode["message"][0]; final int discountPercentage = int.tryParse(firstElement['amount'].toString()) ?? 0; final double walletVal = double.tryParse( box.read(BoxName.passengerWalletTotal)?.toString() ?? '0') ?? 0.0; final bool isWalletNegative = walletVal < 0; double _applyDiscountPerTier({ required double fare, required double minThreshold, required bool isWalletNegative, }) { if (fare < minThreshold) return fare; final double discount = fare * (discountPercentage / 100.0); double result; if (isWalletNegative) { double neg = (-1) * walletVal; result = fare + neg - discount; } else { result = fare - discount; } if (result < minThreshold) { result = minThreshold; } return result.clamp(0.0, double.infinity); } totalPassengerComfort = _applyDiscountPerTier( fare: totalPassengerComfort, minThreshold: minPromoHighSYP, isWalletNegative: isWalletNegative, ); totalPassengerElectric = _applyDiscountPerTier( fare: totalPassengerElectric, minThreshold: minPromoHighSYP, isWalletNegative: isWalletNegative, ); totalPassengerLady = _applyDiscountPerTier( fare: totalPassengerLady, minThreshold: minPromoHighSYP, isWalletNegative: isWalletNegative, ); totalPassengerSpeed = _applyDiscountPerTier( fare: totalPassengerSpeed, minThreshold: minPromoLowSYP, isWalletNegative: isWalletNegative, ); totalPassengerBalash = _applyDiscountPerTier( fare: totalPassengerBalash, minThreshold: minPromoLowSYP, isWalletNegative: isWalletNegative, ); totalDriver = totalDriver - (totalDriver * discountPercentage / 100.0); promoTaken = true; update(); Confetti.launch( context, options: const ConfettiOptions(particleCount: 100, spread: 70, y: 0.6), ); Get.back(); await Future.delayed(const Duration(milliseconds: 120)); } catch (e) { Get.snackbar('Error'.tr, e.toString(), backgroundColor: AppColor.redColor); } } double getDistanceFromText(String distanceText) { String distanceValue = distanceText.replaceAll(RegExp(r'[^0-9.]+'), ''); double distance = double.parse(distanceValue); return distance; } double costForDriver = 0; Future bottomSheet() async { const double minFareSYP = 160; const double minBillableKm = 0.3; const double ladyFlatAddon = 20; const double airportAddonSYP = 200; const double damascusAirportBoundAddon = 1400; const double electricPerKmUplift = 4; const double electricFlatAddon = 10; const double longSpeedThresholdKm = 40.0; const double longSpeedPerKm = 26.0; const double mediumDistThresholdKm = 25.0; const double longDistThresholdKm = 35.0; const double longTripPerMin = 6.0; const int minuteCapMedium = 60; const int minuteCapLong = 80; const int freeMinutesLong = 10; const double extraReduction100 = 0.07; const double maxReductionCap = 0.35; durationToAdd = Duration(seconds: durationToRide); hours = durationToAdd.inHours; minutes = (durationToAdd.inMinutes % 60).round(); final DateTime currentTime = DateTime.now(); newTime = currentTime.add(durationToAdd); averageDuration = (durationToRide / 60) / distance; final int waypointSurchargeMinutes = activeMenuWaypointCount * 5; final int totalMinutes = (durationToRide / 60).floor() + waypointSurchargeMinutes; bool _isAirport(String s) { final t = s.toLowerCase(); return t.contains('airport') || s.contains('مطار') || s.contains('المطار'); } bool _isClub(String s) { final t = s.toLowerCase(); return t.contains('club') || t.contains('nightclub') || t.contains('night club') || s.contains('ديسكو') || s.contains('ملهى ليلي'); } bool _isInsideDamascusAirportBounds(double lat, double lng) { final double northLat = 33.415313; final double southLat = 33.400265; final double eastLng = 36.531505; final double westLng = 36.499687; bool isLatInside = (lat <= northLat) && (lat >= southLat); bool isLngInside = (lng <= eastLng) && (lng >= westLng); return isLatInside && isLngInside; } final double naturePerMin = naturePrice; final double latePerMin = latePrice; final double heavyPerMin = heavyPrice; double _perMinuteByTime(DateTime now, bool clubCtx) { final h = now.hour; if (h >= 21 || h < 1) return latePerMin; if (h >= 1 && h < 5) return clubCtx ? (latePerMin * 2) : latePerMin; if (h >= 14 && h <= 17) return heavyPerMin; return naturePerMin; } double _applyMinFare(double fare) => (fare < minFareSYP) ? minFareSYP : fare; double _withCommission(double base) => (base * (1 + kazan / 100)).ceilToDouble(); final bool airportCtx = _isAirport(startNameAddress) || _isAirport(endNameAddress); final bool clubCtx = _isClub(startNameAddress) || _isClub(endNameAddress); double destLat = 0.0; double destLng = 0.0; try { destLat = myDestination.latitude; destLng = myDestination.longitude; } catch (_) { if (locSearch.coordinatesWithoutEmpty.isNotEmpty) { destLat = double.tryParse( locSearch.coordinatesWithoutEmpty.last.split(',')[0]) ?? 0.0; destLng = double.tryParse( locSearch.coordinatesWithoutEmpty.last.split(',')[1]) ?? 0.0; } } final bool damascusAirportBoundCtx = _isInsideDamascusAirportBounds(destLat, destLng); final bool isInDamascusAirportBoundCtx = _isInsideDamascusAirportBounds( newMyLocation.latitude.toDouble(), newMyLocation.longitude.toDouble(), ); final double billableDistance = (distance < minBillableKm) ? minBillableKm : distance; final bool isLongSpeed = billableDistance > longSpeedThresholdKm; final double perKmSpeedBaseFromServer = speedPrice; final double perKmSpeed = isLongSpeed ? longSpeedPerKm : perKmSpeedBaseFromServer; double reductionPct40 = 0.0; if (perKmSpeedBaseFromServer > 0) { reductionPct40 = (1.0 - (longSpeedPerKm / perKmSpeedBaseFromServer)) .clamp(0.0, maxReductionCap); } final double reductionPct100 = (reductionPct40 + extraReduction100).clamp(0.0, maxReductionCap); double distanceReduction = 0.0; if (billableDistance > 100.0) { distanceReduction = reductionPct100; } else if (billableDistance > 40.0) { distanceReduction = reductionPct40; } double effectivePerMin = _perMinuteByTime(currentTime, clubCtx); int billableMinutes = totalMinutes; if (billableDistance > longDistThresholdKm) { effectivePerMin = longTripPerMin; final int capped = (billableMinutes > minuteCapLong) ? minuteCapLong : billableMinutes; billableMinutes = capped - freeMinutesLong; if (billableMinutes < 0) billableMinutes = 0; } else if (billableDistance > mediumDistThresholdKm) { effectivePerMin = longTripPerMin; billableMinutes = (billableMinutes > minuteCapMedium) ? minuteCapMedium : billableMinutes; } final double perKmComfortRaw = comfortPrice; final double perKmDelivery = deliveryPrice; final double perKmVanRaw = (familyPrice > 0 ? familyPrice : (speedPrice + 13)); final double perKmElectricRaw = perKmComfortRaw + electricPerKmUplift; double perKmComfort = perKmComfortRaw * (1.0 - distanceReduction); double perKmElectric = perKmElectricRaw * (1.0 - distanceReduction); double perKmVan = perKmVanRaw * (1.0 - distanceReduction); perKmComfort = perKmComfort.clamp(0, double.infinity); perKmElectric = perKmElectric.clamp(0, double.infinity); perKmVan = perKmVan.clamp(0, double.infinity); final double perKmBalash = (perKmSpeed - 5).clamp(0, double.infinity); double _oneWayFare({ required double perKm, required bool isLady, double flatAddon = 0, }) { double fare = billableDistance * perKm; fare += billableMinutes * effectivePerMin; fare += flatAddon; if (isLady) fare += ladyFlatAddon; if (airportCtx) fare += airportAddonSYP; if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) { fare += damascusAirportBoundAddon; } return _applyMinFare(fare); } double _roundTripFare({required double perKm}) { double distPart = (billableDistance * 2 * perKm) - ((billableDistance * perKm) * 0.4); double timePart = (billableMinutes * 2) * effectivePerMin; double fare = distPart + timePart; if (airportCtx) fare += airportAddonSYP; if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) { fare += damascusAirportBoundAddon; } return _applyMinFare(fare); } final double costSpeed = _oneWayFare(perKm: perKmSpeed, isLady: false); final double costBalash = _oneWayFare(perKm: perKmBalash, isLady: false); final double costComfort = _oneWayFare(perKm: perKmComfort, isLady: false); final double costElectric = _oneWayFare( perKm: perKmElectric, isLady: false, flatAddon: electricFlatAddon); final double costDelivery = _oneWayFare(perKm: perKmDelivery, isLady: false); final double costLady = _oneWayFare(perKm: perKmComfort, isLady: true); final double costVan = _oneWayFare(perKm: perKmVan, isLady: false); final double costRayehGai = _roundTripFare(perKm: perKmSpeed); final double costRayehGaiComfort = _roundTripFare(perKm: perKmComfort); final double costRayehGaiBalash = _roundTripFare(perKm: perKmBalash); totalPassengerSpeed = _withCommission(costSpeed); totalPassengerBalash = _withCommission(costBalash); totalPassengerComfort = _withCommission(costComfort); totalPassengerElectric = _withCommission(costElectric); totalPassengerLady = _withCommission(costLady); totalPassengerScooter = _withCommission(costDelivery); totalPassengerVan = _withCommission(costVan); totalPassengerRayehGai = _withCommission(costRayehGai); totalPassengerRayehGaiComfort = _withCommission(costRayehGaiComfort); totalPassengerRayehGaiBalash = _withCommission(costRayehGaiBalash); totalPassenger = totalPassengerSpeed; totalCostPassenger = totalPassenger; try { final walletStr = box.read(BoxName.passengerWalletTotal).toString(); final walletVal = double.tryParse(walletStr) ?? 0.0; if (walletVal < 0) { final neg = (-1) * walletVal; totalPassenger += neg; totalPassengerComfort += neg; totalPassengerElectric += neg; totalPassengerLady += neg; totalPassengerBalash += neg; totalPassengerScooter += neg; totalPassengerRayehGai += neg; totalPassengerRayehGaiComfort += neg; totalPassengerRayehGaiBalash += neg; totalPassengerVan += neg; } } catch (e) { Log.print("Error: $e"); } update(); mapEngine.changeBottomSheetShown(forceValue: true); } // حساب المسار بين السائق والراكب وعرضه على الخريطة. // ترسل هذه الدالة طلبًا للخادم للحصول على إحداثيات المسار وتفك تشفيره. // ثم تقوم بتحديث المسافة والوقت وعرض الخطوط المناسبة على الخريطة. Future calculateDriverToPassengerRoute( LatLng driverPos, LatLng passengerPos, {bool isBeginPhase = false}) async { final Map queryParams = { 'fromLat': driverPos.latitude.toString(), 'fromLng': driverPos.longitude.toString(), 'toLat': passengerPos.latitude.toString(), 'toLng': passengerPos.longitude.toString(), }; final uri = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); Log.print('📍 Calculating Driver Route: $uri'); try { final response = await http.get(uri, headers: { 'x-api-key': Env.mapSaasKey, }).timeout(const Duration(seconds: 20)); if (response.statusCode == 200) { final responseData = json.decode(response.body); var routeData = responseData['routes'] != null ? responseData['routes'][0] : responseData; double durationSecondsRaw = (routeData['duration'] as num).toDouble(); int finalDurationSeconds = (durationSecondsRaw * kDurationScalar).toInt(); double distanceMeters = (routeData['distance'] as num).toDouble(); updateDriverRouteMetrics( etaSeconds: finalDurationSeconds, distanceMeters: distanceMeters, ); _currentDriverRouteDistanceMeters = distanceMeters; _currentDriverRouteDurationSeconds = finalDurationSeconds; int minutes = (finalDurationSeconds / 60).floor(); int seconds = finalDurationSeconds % 60; stringRemainingTimeToPassenger = '$minutes:${seconds.toString().padLeft(2, '0')}'; Log.print( '✅ Driver Route Info: $minutes min, ${distanceMeters.toInt()} m'); String pointsString = routeData['points'] ?? routeData['geometry'] ?? ""; if (pointsString.isNotEmpty) { List decodedPoints = await compute(decodePolylineIsolate, pointsString); _currentDriverRoutePoints = decodedPoints; final double decodedDistance = _pathDistanceMeters(decodedPoints); if (decodedDistance > 0) { _currentDriverRouteDistanceMeters = decodedDistance; } // مسح كل السلمات السابقة (الخط المستمر والمتقطع على حد سواء) polyLines = polyLines .where((p) => !p.polylineId.value.startsWith('driver_route') && p.polylineId.value != 'main_route' && p.polylineId.value != 'route_primary' && p.polylineId.value != 'route_direct') .toSet(); if (isBeginPhase) { // حالة Begin: لا نرسم مسار السائق القديم إطلاقاً لأنه وصل والآن الرحلة ستبدأ polyLines = polyLines .where((p) => !p.polylineId.value.startsWith('driver_route')) .toSet(); } else { // مسح السلمات القديمة أولاً polyLines = polyLines .where((p) => !p.polylineId.value.startsWith('driver_route') && p.polylineId.value != 'main_route' && p.polylineId.value != 'route_primary' && p.polylineId.value != 'route_direct') .toSet(); // حالة Apply/Arrived: خط متصل صلب بدل المتقطع polyLines = { ...polyLines, Polyline( polylineId: const PolylineId('driver_route_solid'), points: decodedPoints, color: Colors.amber, // مسار القدوم باللون الأصفر width: 5, ) }; } } mapEngine.fitCameraToPoints(driverPos, passengerPos); update(); } } catch (e) { Log.print('❌ Error calculating driver route: $e'); } } Future checkAndRecalculateIfDeviated( LatLng driverPos, { double? heading, double? speed, }) async { if (_isRecalculatingRoute || _currentDriverRoutePoints.isEmpty) return; double minDistance = 100000.0; int closestIdx = 0; for (int idx = 0; idx < _currentDriverRoutePoints.length; idx++) { final point = _currentDriverRoutePoints[idx]; double dist = Geolocator.distanceBetween(driverPos.latitude, driverPos.longitude, point.latitude, point.longitude); if (dist < minDistance) { minDistance = dist; closestIdx = idx; } } final bool distanceDeviation = minDistance > _deviationThresholdMeters; final bool headingDeviation = _isHeadingAwayFromRoute( heading: heading, speed: speed, closestRouteIndex: closestIdx, distanceFromRouteMeters: minDistance, ); if (!headingDeviation) { _routeHeadingMismatchCount = 0; } else { _routeHeadingMismatchCount++; } if (distanceDeviation || _routeHeadingMismatchCount >= 2) { Log.print( "⚠️ Driver deviated (${minDistance.toStringAsFixed(1)} m, heading mismatch: $_routeHeadingMismatchCount). Recalculating route..."); _routeHeadingMismatchCount = 0; _isRecalculatingRoute = true; if (statusRide == 'Begin' || currentRideState.value == RideState.inProgress) { await calculateDriverToPassengerRoute(driverPos, myDestination, isBeginPhase: true); } else { await calculateDriverToPassengerRoute(driverPos, passengerLocation); } _isRecalculatingRoute = false; } } // تحديث الجزء المتبقي من المسار والمسافة والوقت بشكل تفاعلي. // تحدد الدالة أقرب نقطة للسائق على المسار الحالي وتقوم بقص النقاط السابقة. // ثم تعيد حساب المسافة والوقت المتبقيين محلياً وتحديث الخطوط على الخريطة. void updateRemainingRoute(LatLng driverPos, {bool updateEta = true}) { if (_currentDriverRoutePoints.isEmpty) return; int closestIdx = 0; double minDistance = double.infinity; for (int i = 0; i < _currentDriverRoutePoints.length; i++) { double dist = Geolocator.distanceBetween( driverPos.latitude, driverPos.longitude, _currentDriverRoutePoints[i].latitude, _currentDriverRoutePoints[i].longitude); if (dist < minDistance) { minDistance = dist; closestIdx = i; } } if (minDistance < 150.0) { List remainingPoints = _currentDriverRoutePoints.sublist(closestIdx); if (updateEta) { final double remainingDistance = _pathDistanceMeters(remainingPoints); int remainingDuration = _currentDriverRouteDurationSeconds; if (remainingDistance > 0 && _currentDriverRouteDistanceMeters > 0 && _currentDriverRouteDurationSeconds > 0) { remainingDuration = ((_currentDriverRouteDurationSeconds * remainingDistance) / _currentDriverRouteDistanceMeters) .round(); } remainingDuration = max(0, remainingDuration); updateDriverRouteMetrics( etaSeconds: remainingDuration, distanceMeters: remainingDistance, ); } polyLines = polyLines .where((p) => !p.polylineId.value.startsWith('driver_route') && p.polylineId.value != 'main_route' && p.polylineId.value != 'route_primary' && p.polylineId.value != 'route_direct') .toSet(); if (statusRide == 'Begin' || currentRideState.value == RideState.inProgress) { // لا نرسم أي شيء في حالة البدء لأنه وصل polyLines = polyLines .where((p) => !p.polylineId.value.startsWith('driver_route')) .toSet(); } else { polyLines = { ...polyLines, Polyline( polylineId: const PolylineId('driver_route_solid'), points: remainingPoints, color: Colors.amber, width: 5, ), }; } update(); } } Future getDriverCarsLocationToPassengerAfterApplied() async { bool isRideActive = (statusRide == 'Apply' || statusRide == 'Arrived' || statusRide == 'Begin' || currentRideState.value == RideState.driverApplied || currentRideState.value == RideState.driverArrived || currentRideState.value == RideState.inProgress); if (!isRideActive || statusRide == 'Finished' || statusRide == 'Cancel' || currentRideState.value == RideState.finished || currentRideState.value == RideState.noRide || currentRideState.value == RideState.preCheckReview) { return; } if (_isFetchingDriverLocation) return; _isFetchingDriverLocation = true; try { var res = await CRUD().get( link: AppLink.getDriverCarsLocationToPassengerAfterApplied, payload: {'driver_id': driverId}); if (res != 'failure') { var datadriverLocation = jsonDecode(res); if (datadriverLocation['message'] != null && datadriverLocation['message'].isNotEmpty) { var _data = datadriverLocation['message'][0]; LatLng newDriverPos = LatLng( double.parse(_data['latitude'].toString()), double.parse(_data['longitude'].toString())); double newHeading = double.tryParse(_data['heading']?.toString() ?? '0') ?? 0.0; double speed = double.tryParse(_data['speed']?.toString() ?? '0') ?? 0; if (driverCarsLocationToPassengerAfterApplied.length > 10) { driverCarsLocationToPassengerAfterApplied.removeAt(0); } driverCarsLocationToPassengerAfterApplied.add(newDriverPos); checkAndRecalculateIfDeviated( newDriverPos, heading: newHeading, speed: speed, ); updateRemainingRoute(newDriverPos); if (statusRide == 'Begin' || currentRideState.value == RideState.inProgress) { double zoom = 16.5; if (speed > 0) { zoom = 17.0 - ((speed - 10) / 70) * 2.5; zoom = zoom.clamp(14.5, 17.0); } if (mapEngine.mapController != null) { mapEngine.mapController!.animateCamera( CameraUpdate.newLatLngZoom(newDriverPos, zoom)); } } mapEngine.clearMarkersExceptStartEndAndDriver(); reloadMarkerDriverCarsLocationToPassengerAfterApplied( datadriverLocation); } } update(); } catch (e) { Log.print('Error fetching driver location: $e'); } finally { _isFetchingDriverLocation = false; } } void reloadMarkerDriverCarsLocationToPassengerAfterApplied( dynamic datadriverLocation) { if (datadriverLocation == null || datadriverLocation['message'] == null || datadriverLocation['message'].isEmpty) { return; } var driverData = datadriverLocation['message'][0]; LatLng newPosition = LatLng(double.parse(driverData['latitude'].toString()), double.parse(driverData['longitude'].toString())); double newHeading = double.tryParse(driverData['heading'].toString()) ?? 0.0; String icon; if (driverData['model'].toString().contains('دراجة') || driverData['make'].toString().contains('دراجة')) { icon = mapEngine.motoIcon; } else if (driverData['gender'] == 'Female') { icon = mapEngine.ladyIcon; } else { icon = mapEngine.carIcon; } final String markerId = 'assigned_driver_marker'; final mId = MarkerId(markerId); final existingMarker = markers.cast().firstWhere( (m) => m?.markerId == mId, orElse: () => null, ); if (existingMarker != null) { mapEngine.smoothlyUpdateMarker( existingMarker, newPosition, newHeading, icon); } else { markers = { ...markers, Marker( markerId: mId, position: newPosition, rotation: newHeading, icon: InlqBitmap.fromStyleImage(icon), anchor: const Offset(0.5, 0.5), ), }; update(); } } void updateDriverMarker(LatLng position, double heading) { const String markerId = 'assigned_driver_marker'; const mId = MarkerId(markerId); // Choose icon based on vehicle type String icon; if (model.contains('دراجة') || make.contains('دراجة')) { icon = mapEngine.motoIcon; } else { icon = mapEngine.carIcon; } final existingMarker = markers.cast().firstWhere( (m) => m?.markerId == mId, orElse: () => null, ); if (existingMarker != null) { mapEngine.smoothlyUpdateMarker(existingMarker, position, heading, icon); } else { markers = { ...markers, Marker( markerId: mId, position: position, icon: InlqBitmap.fromStyleImage(icon), rotation: heading, anchor: const Offset(0.5, 0.5), ), }; update(); } } Future runEvery30SecondsUntilConditionMet() async { double tripDurationInMinutes = durationToPassenger / 5; int loopCount = tripDurationInMinutes.ceil(); for (var i = 0; i < loopCount; i++) { await Future.delayed(const Duration(seconds: 5)); if (rideTimerBegin == true || statusRide == 'Apply') { await getDriverCarsLocationToPassengerAfterApplied(); } } } Future runWhenRideIsBegin() async { double tripDurationInMinutes = durationToRide / 6; int loopCount = tripDurationInMinutes.ceil(); mapEngine.clearMarkersExceptStartEndAndDriver(); for (var i = 0; i < loopCount; i++) { await Future.delayed(const Duration(seconds: 4)); await getDriverCarsLocationToPassengerAfterApplied(); } } // بدء مراقب اتصال المقبس (Socket Watchdog). // يتحقق دورياً كل 5 ثوانٍ من آخر تحديث تم استلامه عبر المقبس. // في حال وجود خمول لأكثر من 15 ثانية، يجلب الموقع عبر واجهة التطبيق كطلب مفرد. // وإذا زاد الخمول عن 30 ثانية، يبدأ آلية الاقتراع الدوري كخيار احتياطي. void _startSocketWatchdog() { _watchdogTimer?.cancel(); Log.print("👀 Starting Socket Watchdog (Hybrid Mode)..."); _watchdogTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { if (currentRideState.value != RideState.driverApplied && currentRideState.value != RideState.driverArrived && currentRideState.value != RideState.inProgress) { timer.cancel(); return; } final lastTime = mapSocket.lastDriverLocationTime ?? DateTime.now().subtract(const Duration(minutes: 1)); final difference = DateTime.now().difference(lastTime).inSeconds; if (difference < 15 && mapSocket.isSocketConnected) { if (_locationPollingTimer != null && _rideAcceptedViaSource == "Socket") { Log.print("✅ Socket recovered. Stopping polling fallback."); stopDriverLocationPolling(); } } else if (difference >= 15 && difference < 30) { Log.print("⚠️ Socket silent for ${difference}s. Single API Poll..."); await getDriverCarsLocationToPassengerAfterApplied(); try { String statusFromServer = await getRideStatus(rideId); _handleServerStatusTransition(statusFromServer); } catch (e) { Log.print("Error polling ride status in watchdog: $e"); } } else if (difference >= 30) { if (_locationPollingTimer == null) { Log.print( "🔴 Socket dead for ${difference}s. Activating polling fallback!"); _startDriverLocationPollingWithTimer(); } else { await getDriverCarsLocationToPassengerAfterApplied(); try { String statusFromServer = await getRideStatus(rideId); _handleServerStatusTransition(statusFromServer); } catch (e) { Log.print("Error polling ride status in watchdog: $e"); } } } }); } // بدء الاقتراع الدوري لموقع السائق كخيار احتياطي عند توقف أو فشل المقبس. // يقوم هذا التوقيت دورياً كل 6 ثوانٍ بجلب موقع السائق وتحديث حالة الرحلة. void _startDriverLocationPollingWithTimer() { Log.print("📍 Starting Driver Location Polling (6s interval)"); _locationPollingTimer?.cancel(); _locationPollingTimer = Timer.periodic(Duration(seconds: 6), (timer) async { if (currentRideState.value == RideState.finished || currentRideState.value == RideState.cancelled || currentRideState.value == RideState.noRide) { timer.cancel(); return; } await getDriverCarsLocationToPassengerAfterApplied(); try { String statusFromServer = await getRideStatus(rideId); _handleServerStatusTransition(statusFromServer); } catch (e) { Log.print("Error polling ride status in fallback timer: $e"); } }); } void _handleServerStatusTransition(String status) { String lowerStatus = status.toLowerCase(); Log.print( "🔄 _handleServerStatusTransition status: $lowerStatus | Current state: ${currentRideState.value}"); if (lowerStatus == 'arrived' && currentRideState.value != RideState.driverArrived) { processDriverArrival("Polling"); } else if ((lowerStatus == 'begin' || lowerStatus == 'started' || lowerStatus == 'inprogress') && currentRideState.value != RideState.inProgress) { processRideBegin(source: "Polling"); } else if ((lowerStatus == 'finished' || lowerStatus == 'ended') && currentRideState.value != RideState.finished && currentRideState.value != RideState.preCheckReview) { Log.print( "🏁 Polling detected Finished. Releasing ride and moving to rating."); stopAllTimers(); currentRideState.value = RideState.preCheckReview; tripFinishedFromDriver(); _checkLastRideForReview(); } else if (lowerStatus == 'cancelled' || lowerStatus == 'cancel') { processRideCancelledByDriver({'reason': 'Cancelled by driver'}, source: "Polling"); } } void stopDriverLocationPolling() { Log.print("🛑 Stopping Location Polling"); _locationPollingTimer?.cancel(); _locationPollingTimer = null; } Future _addRideToWaitingTable() async { try { LatLng startLoc = mapEngine.polylineCoordinates.first; LatLng endLoc = mapEngine.polylineCoordinates.last; await CRUD().post(link: AppLink.addWaitingRide, payload: { 'id': rideId.toString(), "start_location": '${startLoc.latitude},${startLoc.longitude}', "end_location": '${endLoc.latitude},${endLoc.longitude}', "date": DateTime.now().toString(), "time": DateTime.now().toString(), "price": totalPassenger.toStringAsFixed(2), 'passenger_id': box.read(BoxName.passengerID).toString(), 'status': 'waiting', 'carType': box.read(BoxName.carType), 'passengerRate': passengerRate.toStringAsFixed(2), 'price_for_passenger': totalME.toStringAsFixed(2), 'distance': distance.toStringAsFixed(1), 'duration': duration.toStringAsFixed(1), 'payment_method': Get.find().isWalletChecked ? 'wallet' : 'cash', "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), }); Log.print('[WaitingTable] Ride $rideId added to waiting_ride table.'); } catch (e) { Log.print('Error adding ride to waiting_ride table: $e'); } } double totalME = 0; double passengerRate = 5; double comfortPrice = 45; double speedPrice = 40; double mashwariPrice = 40; double familyPrice = 55; double deliveryPrice = 1.2; Future getKazanPercent() async { var res = await CRUD().get( link: AppLink.getKazanPercent, payload: {'country': box.read(BoxName.countryCode).toString()}, ); if (res != 'failure') { var json = jsonDecode(res); var dataList = json['data'] ?? json['message']; if (dataList != null && dataList is List && dataList.isNotEmpty) { var firstRow = dataList[0]; kazan = double.parse(firstRow['kazan'].toString()); naturePrice = double.parse(firstRow['naturePrice'].toString()); heavyPrice = double.parse(firstRow['heavyPrice'].toString()); latePrice = double.parse(firstRow['latePrice'].toString()); comfortPrice = double.parse(firstRow['comfortPrice'].toString()); speedPrice = double.parse(firstRow['speedPrice'].toString()); deliveryPrice = double.parse(firstRow['deliveryPrice'].toString()); mashwariPrice = double.parse(firstRow['freePrice'].toString()); familyPrice = double.parse(firstRow['familyPrice'].toString()); fuelPrice = double.parse(firstRow['fuelPrice'].toString()); } } } Future getPassengerRate() async { var res = await CRUD().get( link: AppLink.getPassengerRate, payload: {'passenger_id': box.read(BoxName.passengerID)}); if (res != 'failure') { var json = jsonDecode(res); var message = json['data'] ?? json['message']; if (message['rating'] == null) { passengerRate = 5.0; } else { var rating = message['rating']; if (rating is String) { passengerRate = double.tryParse(rating) ?? 5.0; } else if (rating is num) { passengerRate = rating.toDouble(); } else { passengerRate = 5.0; } } } else { passengerRate = 5.0; } } Future addFingerPrint() async { String fingerPrint = await DeviceHelper.getDeviceFingerprint(); await CRUD().postWallet(link: AppLink.addFingerPrint, payload: { 'token': (box.read(BoxName.tokenFCM.toString())), 'passengerID': box.read(BoxName.passengerID).toString(), "fingerPrint": fingerPrint }); } Future firstTimeRunToGetCoupon() async { if (box.read(BoxName.isFirstTime).toString() == '0' && box.read(BoxName.isInstall).toString() == '1' && box.read(BoxName.isGiftToken).toString() == '0') { var promoCode, discount, validity; var resPromo = await CRUD().get(link: AppLink.getPromoFirst, payload: { "passengerID": box.read(BoxName.passengerID).toString(), }); if (resPromo != 'failure') { var d1 = jsonDecode(resPromo); promoCode = d1['message']['promo_code']; discount = d1['message']['amount']; validity = d1['message']['validity_end_date']; } box.write(BoxName.isFirstTime, '1'); Get.dialog( AlertDialog( contentPadding: EdgeInsets.zero, content: SizedBox( width: 300, child: PromoBanner( promoCode: promoCode, discountPercentage: discount, validity: validity, ), ), ), ); } } Future detectAndCacheDeviceTier() async { bool isHighEnd = await DevicePerformanceManager.isHighEndDevice(); Log.print("Device Analysis - Is Flagship/HighEnd? $isHighEnd"); box.write(BoxName.lowEndMode, !isHighEnd); } Future initilizeGetStorage() async { if (box.read(BoxName.addWork) == null) { box.write(BoxName.addWork, 'addWork'); } if (box.read(BoxName.addHome) == null) { box.write(BoxName.addHome, 'addHome'); } if (box.read(BoxName.lowEndMode) == null) { detectAndCacheDeviceTier(); } } Future selectDriverAndCarForMishwariTrip() async { double latitudeOffset = 0.1; double longitudeOffset = 0.12; double southwestLat = passengerLocation.latitude - latitudeOffset; double northeastLat = passengerLocation.latitude + latitudeOffset; double southwestLon = passengerLocation.longitude - longitudeOffset; double northeastLon = passengerLocation.longitude + longitudeOffset; var payload = { 'southwestLat': southwestLat.toString(), 'northeastLat': northeastLat.toString(), 'southwestLon': southwestLon.toString(), 'northeastLon': northeastLon.toString(), }; try { var res = await CRUD().get( link: AppLink.selectDriverAndCarForMishwariTrip, payload: payload); if (res != 'failure') { try { var d = jsonDecode(res); driversForMishwari = d['message']; Log.print('driversForMishwari: $driversForMishwari'); update(); } catch (e) { Log.print("Error decoding JSON: $e"); } } } catch (e) { Log.print("Error Mishwari select: $e"); } } List driversForMishwari = []; final Rx selectedDateTime = DateTime.now().obs; void updateDateTime(DateTime newDateTime) { selectedDateTime.value = newDateTime; } Future mishwariOption() async { isLoading = true; update(); await selectDriverAndCarForMishwariTrip(); Future.delayed(Duration.zero); isLoading = false; update(); Get.to(() => CupertinoDriverListWidget()); } bool isLoading = false; var driverIdVip = ''; Future saveTripData( Map driver, DateTime tripDateTime) async { try { LatLng startLoc = mapEngine.polylineCoordinates.first; Map tripData = { 'id': driver['driver_id'].toString(), 'phone': driver['phone'], 'gender': driver['gender'], 'name': driver['NAME'], 'name_english': driver['name_english'], 'address': driver['address'], 'religion': driver['religion'] ?? 'UnKnown', 'age': driver['age'].toString(), 'education': driver['education'] ?? 'UnKnown', 'license_type': driver['license_type'] ?? 'UnKnown', 'national_number': driver['national_number'] ?? 'UnKnown', 'car_plate': driver['car_plate'], 'make': driver['make'], 'model': driver['model'], 'year': driver['year'].toString(), 'color': driver['color'], 'color_hex': driver['color_hex'], 'displacement': driver['displacement'], 'fuel': driver['fuel'], 'token': driver['token'], 'rating': driver['rating'].toString(), 'countRide': driver['ride_count'].toString(), 'passengerId': box.read(BoxName.passengerID), 'timeSelected': tripDateTime.toIso8601String(), 'status': 'pending', 'startNameAddress': startNameAddress.toString(), 'locationCoordinate': '${startLoc.latitude},${startLoc.longitude}', }; Log.print('tripData: $tripData'); var response = await CRUD().post(link: AppLink.addMishwari, payload: tripData); if (response != 'failure') { var id = response['message']['id'].toString(); await CRUD() .post(link: '${AppLink.server}/ride/rides/add.php', payload: { "start_location": '${startLoc.latitude},${startLoc.longitude}', "end_location": '${startLoc.latitude},${startLoc.longitude}', "date": DateTime.now().toString(), "time": DateTime.now().toString(), "endtime": DateTime.now().add(const Duration(hours: 2)).toString(), "price": '50', "passenger_id": box.read(BoxName.passengerID).toString(), "driver_id": driver['driver_id'].toString(), "status": "waiting", 'carType': 'vip', "price_for_driver": '50', "price_for_passenger": '50', "distance": '20', "paymentMethod": 'cash', }).then((value) { if (value is String) { final parsedValue = jsonDecode(value); rideId = parsedValue['message']; } else if (value is Map) { rideId = value['message']; } }); driverIdVip = driver['driver_id'].toString(); driverId = driver['driver_id'].toString(); DateTime timeSelected = DateTime.parse(tripDateTime.toIso8601String()); Get.find().scheduleNotificationsForTimeSelected( "Your trip is scheduled".tr, "Don't forget your ride!".tr, "tone1", timeSelected); await NotificationService.sendNotification( category: 'OrderVIP', target: driver['token'].toString(), title: 'OrderVIP'.tr, body: '$rideId - VIP Trip', isTopic: false, tone: 'tone1', driverList: [ id, rideId, driver['id'], passengerLocation.latitude.toString(), startNameAddress.toString(), passengerLocation.longitude.toString(), (box.read(BoxName.name).toString().split(' ')[0]).toString(), box.read(BoxName.passengerID).toString(), box.read(BoxName.phone).toString(), box.read(BoxName.email).toString(), box.read(BoxName.passengerPhotoUrl).toString(), box.read(BoxName.tokenFCM).toString(), (driver['token'].toString()), ], ); if (response['message'] == "Trip updated successfully") { mySnackbarSuccess("Trip updated successfully".tr); await NotificationService.sendNotification( category: 'Order VIP Canceld', target: response['previous_driver_token'].toString(), title: 'Order VIP Canceld'.tr, body: 'Passenger cancel order'.tr, isTopic: false, tone: 'cancel', driverList: [], ); } isBottomSheetShown = false; update(); Get.to(() => VipWaittingPage()); } else { throw Exception('Failed to save trip'); } } catch (e) { Get.snackbar('Error'.tr, 'Failed to book trip: $e'.tr, backgroundColor: AppColor.redColor); } } Future cancelVip(String token, tripId) async { var res = await CRUD() .post(link: AppLink.cancelMishwari, payload: {'id': tripId}); if (res != 'failure') { Get.back(); mySnackbarSuccess('You canceled VIP trip'.tr); } } void sendToDriverAgain(String token) { NotificationService.sendNotification( category: 'Order VIP Canceld', target: token.toString(), title: 'Order VIP Canceld'.tr, body: 'Passenger cancel order'.tr, isTopic: false, tone: 'cancel', driverList: [], ); } Set notifiedDrivers = {}; Future processDriverArrival(String source) async { if (currentRideState.value == RideState.driverArrived || _isArrivalProcessed) { Log.print("✋ Ignored Arrival from $source. Already processed."); return; } _isArrivalProcessed = true; Log.print("🚖 Driver Arrived via $source! Processing..."); currentRideState.value = RideState.driverArrived; statusRide = 'Arrived'; await RideLiveNotification.showDriverArrived(driverName); uiInteractions.driverArrivePassengerDialoge(); startTimerDriverWaitPassenger5Minute(); if (mapEngine.polylineCoordinates.isNotEmpty) { mapEngine.playRouteAnimation( mapEngine.polylineCoordinates, mapEngine.lastComputedBounds); } update(); } Future processRideFinished(List driverList, {String source = "Unknown"}) async { if (currentRideState.value == RideState.finished || _isFinishProcessed) { Log.print("✋ Ignored Finish Request from $source. Already Finished."); return; } _isFinishProcessed = true; Log.print("🏁 Ride Finished via $source."); currentRideState.value = RideState.finished; mapSocket.disposeRideSocket(); stopDriverLocationPolling(); if (Get.isRegistered()) { Get.find().stopRecording(); } if (Get.isDialogOpen == true) Get.back(); NotificationController().showNotification( 'Alert'.tr, "Please make sure not to leave any personal belongings in the car.".tr, 'tone1', ); IosLiveActivityService.endRideActivity(); PipService.disablePip(); await RideLiveNotification.cancel(); if (driverList.length >= 4) { String price = driverList[3].toString(); Get.offAll(() => RateDriverFromPassenger(), arguments: { 'driverId': driverList[0].toString(), 'rideId': driverList[1].toString(), 'price': price }); } } Future processRideCancelledByDriver(dynamic data, {String source = "Unknown"}) async { if (_isCancelProcessed) return; _isCancelProcessed = true; stopAllTimers(); if (Get.isDialogOpen == true) Get.back(); await RideLiveNotification.cancel(); IosLiveActivityService.endRideActivity(); PipService.disablePip(); Get.defaultDialog( title: "Sorry 😔".tr, titleStyle: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold), barrierDismissible: false, content: Column( children: [ const Icon(Icons.cancel_presentation, size: 50, color: Colors.redAccent), const SizedBox(height: 10), Text( "The driver cancelled the trip for an emergency reason.\nDo you want to search for another driver immediately?" .tr, textAlign: TextAlign.center, ), ], ), actions: [ TextButton( onPressed: () { Get.back(); handleNoDriverFound(); }, child: Text("Cancel Trip".tr, style: const TextStyle(color: Colors.grey)), ), ElevatedButton.icon( style: ElevatedButton.styleFrom(backgroundColor: AppColor.primaryColor), icon: const Icon(Icons.refresh, color: Colors.white), label: Text("Search for another driver".tr, style: const TextStyle(color: Colors.white)), onPressed: () { Get.back(); retrySearchForDrivers(); }, ), ], ); } void showNoDriverDialog() { Get.defaultDialog( title: "No Drivers Found".tr, middleText: "Sorry, there are no cars available of this type right now.".tr, textConfirm: "Refresh Map".tr, textCancel: "Cancel".tr, confirmTextColor: Colors.white, onConfirm: () { Get.back(); restCounter(); stopAllTimers(); clearControllersAndGoHome(); }, ); } Future calculateDistanceBetweenPassengerAndDriverBeforeCancelRide() async { await getDriverCarsLocationToPassengerAfterApplied(); double dist = Geolocator.distanceBetween( passengerLocation.latitude, passengerLocation.longitude, driverCarsLocationToPassengerAfterApplied.last.latitude, driverCarsLocationToPassengerAfterApplied.last.longitude, ); if (dist > 500) { isCancelRidePageShown = true; update(); } else { Get.defaultDialog( barrierDismissible: false, title: 'The Driver Will be in your location soon .'.tr, middleText: 'The distance less than 500 meter.'.tr, confirm: Column( children: [ MyElevatedButton( kolor: AppColor.greenColor, title: 'Ok'.tr, onPressed: () { Get.back(); }, ), MyElevatedButton( kolor: AppColor.redColor, title: 'No, I want to cancel this trip'.tr, onPressed: () { Get.back(); MyDialog().getDialog( 'Attention'.tr, 'You will be charged for the cost of the driver coming to your location.' .tr, () async { Get.back(); Get.find() .payToDriverForCancelAfterAppliedAndHeNearYou(rideId); }, ); }, ), ], ), ); } } Future cancelRideAfterRejectFromAll() async { locSearch.clearPlacesDestination(); mapEngine.clearPolyline(); data = []; await CRUD().post( link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php", payload: { "ride_id": rideId.toString(), "reason": 'notApplyFromAnyDriver' }); rideConfirm = false; statusRide = 'Cancel'; isSearchingWindow = false; shouldFetch = false; isPassengerChosen = false; isCashConfirmPageShown = false; isCashSelectedBeforeConfirmRide = false; timeToPassengerFromDriverAfterApplied = 0; mapEngine.changeCancelRidePageShow(); remainingTime = 0; update(); } void selectReason0(int index, String note) { selectedReason = index; cancelNote = note; update(); } int selectedReasonIndex = -1; String selectedReasonText = ""; TextEditingController otherReasonController = TextEditingController(); void selectReason(int index, String reason) { selectedReasonIndex = index; selectedReasonText = reason; update(); } List data = []; void restCounter() { locSearch.clearPlacesDestination(); mapEngine.clearPolyline(); data = []; rideConfirm = false; shouldFetch = false; timeToPassengerFromDriverAfterApplied = 0; update(); } Future _checkAndRefreshMapStyle() async { try { final String styleJson = await rootBundle.loadString('assets/style.json'); final Map decoded = json.decode(styleJson); final String? currentVersion = decoded['metadata'] != null ? decoded['metadata']['version'] : null; if (currentVersion == null) return; final String lastVersion = box.read(BoxName.styleVersion) ?? "0.0.0"; if (currentVersion != lastVersion) { Log.print( "♻️ Map Style Version mismatch ($lastVersion -> $currentVersion). Purging offline cache..."); await OfflineMapService.instance.clearCache(); await Future.delayed(const Duration(milliseconds: 500)); box.write(BoxName.styleVersion, currentVersion); Log.print("✅ Style Version updated to $currentVersion"); } } catch (e) { Log.print("⚠️ Style version check failed: $e"); } } void reinit() { if (currentRideState.value != RideState.noRide && currentRideState.value != RideState.cancelled) { Log.print('ℹ️ reinit() skipped: ride is active'); return; } Log.print('🔄 reinit() calling resetAllMapStates and restarting timers...'); resetAllMapStates(); stopAllTimers(); currentRideState.value = RideState.noRide; // Restart location search locSearch.getLocation(); // Restart lifecycle timers & stages getLocationArea(passengerLocation.latitude, passengerLocation.longitude); unawaited(_stagePricingAndState()); unawaited(_stageNiceToHave()); startMasterTimer(); } void resetAllMapStates() { Log.print('🧹 Resetting all map states to prevent sticky location bug'); locSearch.clearPlacesDestination(); locSearch.clearPlacesStart(); locSearch.waypoints.clear(); locSearch.clearAllMenuWaypoints(); if (Get.isRegistered()) { Get.find().reset(); } // Call reset on mapEngine which handles clearing markers, polylines, animation timers and UI states mapEngine.reset(); data = []; locSearch.passengerStartLocationFromMap = false; locSearch.startLocationFromMap = false; isPickerShown = false; locSearch.workLocationFromMap = false; locSearch.homeLocationFromMap = false; isAnotherOreder = false; isWhatsAppOrder = false; myDestination = passengerLocation; locSearch.hintTextDestinationPoint = 'Select your destination'.tr; locSearch.placeDestinationController.clear(); locSearch.placeStartController.clear(); rideConfirm = false; shouldFetch = true; // reset to true by default for next ride search polling isDrawingRoute = false; isLoading = false; // Reset RideLifecycleController specific search and lifecycle states isSearchingWindow = false; currentRideState.value = RideState.noRide; statusRide = 'wait'; statusRideVip = 'wait'; statusRideFromStart = false; isDriverInPassengerWay = false; isDriverArrivePassenger = false; _isArrivalProcessed = false; _isFinishProcessed = false; _isCancelProcessed = false; _isAcceptanceProcessed = false; _isRatingScreenOpen = false; _isRecalculatingRoute = false; _isRideStartedProcessed = false; _isDriverAppliedLogicExecuted = false; _isDriverArrivedLogicExecuted = false; _isRideBeginLogicExecuted = false; _currentDriverRoutePoints = []; _currentDriverRouteDistanceMeters = 0.0; _currentDriverRouteDurationSeconds = 0; _driverEtaUpdatedAt = null; _driverEtaSecondsAtUpdate = 0; _driverEtaCountdownTicks = 0; _routeHeadingMismatchCount = 0; distanceByPassenger = ''; durationToPassenger = 0; stringRemainingTimeToPassenger = ''; update(); } void _handleFatalError(String title, String message) { if (Get.isBottomSheetOpen == true || Get.isDialogOpen == true) { Get.back(); } if (Get.isSnackbarOpen) Get.closeCurrentSnackbar(); isDrawingRoute = false; isLoading = false; update(); Get.defaultDialog( title: title, titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), middleText: message, middleTextStyle: AppStyle.subtitle, barrierDismissible: false, confirm: MyElevatedButton( title: "Close".tr, kolor: AppColor.redColor, onPressed: () { Get.back(); clearControllersAndGoHome(); }, ), ); } String shortenAddress(String fullAddress) { List parts = fullAddress.split('،'); parts = parts.map((part) => part.trim()).toList(); parts = parts.where((part) => part.isNotEmpty).toList(); String shortAddress = ''; if (parts.isNotEmpty) { shortAddress += parts[0]; } if (parts.length > 2) { shortAddress += '، ${parts[2]}'; } else if (parts.length > 1) { shortAddress += '، ${parts[1]}'; } if (parts.length > 1) { shortAddress += '، ${parts.last}'; } shortAddress = shortAddress .split('،') .where((part) => !RegExp(r'^[0-9 ]+$').hasMatch(part.trim())) .join('er'); bool isEnglish = RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(shortAddress.replaceAll('،', '')); if (isEnglish) { List englishParts = shortAddress.split('،'); if (englishParts.length > 2) { shortAddress = '${englishParts[0]}، ${englishParts[1]}، ${englishParts.last}'; } else if (englishParts.length > 1) { shortAddress = '${englishParts[0]}، ${englishParts.last}'; } } return shortAddress; } double distanceOfDestination = 0; bool haveSteps = false; Future getMapPoints( String originSteps, String destinationSteps, int index) async { isWayPointStopsSheetUtilGetMap = false; await nearbyDrivers.getCarsLocationByPassengerAndReloadMarker(); update(); var url = ('${AppLink.googleMapsLink}directions/json?&language=${box.read(BoxName.lang)}&avoid=tolls|ferries&destination=$destinationSteps&origin=$originSteps&key=${AK.mapAPIKEY}'); var response = await CRUD().getGoogleApi(link: url, payload: {}); data = response['routes'][0]['legs']; int durationToRide0 = data[0]['duration']['value']; durationToRide = durationToRide + durationToRide0; distance = distanceOfDestination + (data[0]['distance']['value']) / 1000; update(); final String pointsString = response['routes'][0]["overview_polyline"]["points"]; List decodedPoints = await compute(decodePolylineIsolate, pointsString); for (int i = 0; i < decodedPoints.length; i++) { mapEngine.polylineCoordinates.add(decodedPoints[i]); } if (polyLines.isEmpty) { var polyline = Polyline( polylineId: PolylineId('route_$index'), points: locSearch.polylineCoordinatesPointsAll[index], width: 6, color: const Color(0xFF2196F3), ); polyLines = {...polyLines, polyline}; rideConfirm = false; update(); } } void updateCameraForDistanceAfterGetMap() { LatLng coord1 = LatLng( double.parse(locSearch.coordinatesWithoutEmpty.first.split(',')[0]), double.parse(locSearch.coordinatesWithoutEmpty.first.split(',')[1])); LatLng coord2 = LatLng( double.parse(locSearch.coordinatesWithoutEmpty.last.split(',')[0]), double.parse(locSearch.coordinatesWithoutEmpty.last.split(',')[1])); LatLng northeastBound; LatLng southwestBound; if (coord1.latitude > coord2.latitude) { northeastBound = coord1; southwestBound = coord2; } else { northeastBound = coord2; southwestBound = coord1; } LatLngBounds boundsObj = LatLngBounds(northeast: northeastBound, southwest: southwestBound); var cameraUpdate = CameraUpdate.newLatLngBounds(boundsObj, left: 180, top: 180, right: 180, bottom: 180); mapController!.animateCamera(cameraUpdate); update(); } int selectedIndex = -1; void selectCarFromList(int index) { selectedIndex = index; carTypes.forEach((element) => element.isSelected = false); carTypes[index].isSelected = true; update(); } Future showBottomSheet1() async { await bottomSheet(); isBottomSheetShown = true; heightBottomSheetShown = 250; update(); } double calculateAngleBetweenLocations(LatLng start, LatLng end) { double startLat = start.latitude * pi / 180; double startLon = start.longitude * pi / 180; double endLat = end.latitude * pi / 180; double endLon = end.longitude * pi / 180; double dLon = endLon - startLon; double y = sin(dLon) * cos(endLat); double x = cos(startLat) * sin(endLat) - sin(startLat) * cos(endLat) * cos(dLon); double angle = atan2(y, x); double angleDegrees = angle * 180 / pi; return angleDegrees; } get dataCarsLocationByPassenger { return nearbyDrivers.carsLocationByPassenger; } set dataCarsLocationByPassenger(var val) { nearbyDrivers.carsLocationByPassenger = val; } double calculateBearing(double lat1, double lon1, double lat2, double lon2) { return nearbyDrivers.calculateBearing(lat1, lon1, lat2, lon2); } void analyzeBehavior(Position currentPosition, List routePoints) { nearbyDrivers.analyzeBehavior(currentPosition, routePoints); } void detectStops(Position currentPosition) { nearbyDrivers.detectStops(currentPosition); } Future getDirectionMap(String origin, String destination, [List waypoints = const [], int attemptCount = 0]) async { if (attemptCount == 0) { isDrawingRoute = true; update(); if (isDrawingRoute) showDrawingBottomSheet(); await nearbyDrivers.getCarsLocationByPassengerAndReloadMarker(); } if (origin.isEmpty) { origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; } var coordDestination = destination.split(','); double latDest = double.parse(coordDestination[0]); double lngDest = double.parse(coordDestination[1]); myDestination = LatLng(latDest, lngDest); Uri uri; var originCoords = origin.split(','); final Map queryParams = { 'fromLat': originCoords[0].trim(), 'fromLng': originCoords[1].trim(), 'toLat': latDest.toString(), 'toLng': lngDest.toString(), }; for (int i = 0; i < activeMenuWaypointCount; i++) { final wp = menuWaypoints[i]; if (wp != null) { queryParams['stop${i + 1}Lat'] = wp.latitude.toString(); queryParams['stop${i + 1}Lng'] = wp.longitude.toString(); } } uri = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); Log.print( 'Requesting Route URI (SaaS, Attempt: ${attemptCount + 1}): $uri'); http.Response response; Map responseData; try { response = await http.get(uri, headers: { 'x-api-key': Env.mapSaasKey, }).timeout(const Duration(seconds: 20)); responseData = json.decode(response.body); bool isRequestValid = response.statusCode == 200; if (!isRequestValid) { if (attemptCount < 2) { await _retryProcess(origin, destination, waypoints, attemptCount); return; } _handleFatalError( "Server Error".tr, "Connection failed. Please try again.".tr); return; } double apiDistanceMeters; String pointsString; dynamic routeData; apiDistanceMeters = (responseData['distance'] as num).toDouble(); pointsString = responseData['points'] ?? ""; routeData = responseData; var origCoords = origin.split(','); double startLat = double.parse(origCoords[0]); double startLng = double.parse(origCoords[1]); double aerialDistance = Geolocator.distanceBetween(startLat, startLng, latDest, lngDest); if (apiDistanceMeters < 50.0 && aerialDistance > 200.0) { Log.print( "⚠️ Suspicious Route detected! Server: $apiDistanceMeters m | Aerial: $aerialDistance m"); if (attemptCount < 2) { Log.print("🔄 Retrying request (Attempt ${attemptCount + 2})..."); await Future.delayed(const Duration(seconds: 1)); await getDirectionMap( origin, destination, waypoints, attemptCount + 1); return; } else { Log.print("❌ All retries failed. Calculating Route is impossible."); _handleFatalError( "Route Not Found".tr, "We couldn't find a valid route to this destination. Please try selecting a different point." .tr); return; } } box.remove(BoxName.tripData); box.write(BoxName.tripData, routeData); durationToRide = ((routeData['duration'] as num) * kDurationScalar).toInt(); double distanceOfTrip = apiDistanceMeters / 1000.0; distance = distanceOfTrip; data = routeData['legs'] != null && routeData['legs'].isNotEmpty ? (routeData['legs'][0]['steps'] ?? []) : []; List decodedPoints = []; if (pointsString.isNotEmpty) { decodedPoints = await compute(decodePolylineIsolate, pointsString); } if (decodedPoints.isEmpty) { _handleFatalError("Map Error".tr, "Received empty route data.".tr); return; } mapEngine.polylineCoordinates.clear(); mapEngine.polylineCoordinates.addAll(decodedPoints); final LatLng startLoc = mapEngine.polylineCoordinates.first; final LatLng endLoc = mapEngine.polylineCoordinates.last; startNameAddress = responseData['startName'] ?? 'Start Point'.tr; endNameAddress = responseData['endName'] ?? 'Destination'.tr; Log.print('📍 ROUTE START: $startNameAddress'); Log.print('📍 ROUTE END: $endNameAddress'); if (responseData['bbox'] != null) { List bbox = responseData['bbox']; if (bbox.length == 4) { mapEngine.lastComputedBounds = LatLngBounds( southwest: LatLng(bbox[1], bbox[0]), northeast: LatLng(bbox[3], bbox[2]), ); } } else { double? minLat, maxLat, minLng, maxLng; for (LatLng point in mapEngine.polylineCoordinates) { minLat = minLat == null ? point.latitude : min(minLat, point.latitude); maxLat = maxLat == null ? point.latitude : max(maxLat, point.latitude); minLng = minLng == null ? point.longitude : min(minLng, point.longitude); maxLng = maxLng == null ? point.longitude : max(maxLng, point.longitude); } if (minLat != null) { mapEngine.lastComputedBounds = LatLngBounds( northeast: LatLng(maxLat!, maxLng!), southwest: LatLng(minLat!, minLng!)); } } if (isDrawingRoute) { Log.print('🔔 Finalizing route drawing state'); isDrawingRoute = false; isLoading = false; update(); } durationToAdd = Duration(seconds: durationToRide); hours = durationToAdd.inHours; minutes = (durationToAdd.inMinutes % 60).round(); markers = { Marker( markerId: const MarkerId('start'), position: startLoc, icon: InlqBitmap.fromStyleImage('orange_marker'), infoWindow: const InfoWindow(title: 'A'), anchor: const Offset(0.5, 1.0), ), Marker( markerId: const MarkerId('end'), position: endLoc, icon: InlqBitmap.fromStyleImage('violet_marker'), infoWindow: const InfoWindow(title: 'B'), anchor: const Offset(0.5, 1.0), ), }; for (int i = 0; i < activeMenuWaypointCount; i++) { final wp = menuWaypoints[i]; if (wp != null) { final bool isFirstWaypoint = i == 0; markers.add(Marker( markerId: MarkerId('waypoint_$i'), position: wp, icon: InlqBitmap.fromStyleImage( isFirstWaypoint ? 'orange_marker' : 'violet_marker'), infoWindow: InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'), anchor: const Offset(0.5, 1.0), )); } } if (polyLines.isNotEmpty) mapEngine.clearPolyline(); rideConfirm = false; isMarkersShown = true; update(); await bottomSheet(); await mapEngine.playRouteAnimation( mapEngine.polylineCoordinates, mapEngine.lastComputedBounds); } catch (e, stackTrace) { if (isDrawingRoute) { isDrawingRoute = false; isLoading = false; update(); } Log.print('🚨 CRITICAL ERROR IN getDirectionMap: $e'); Log.print('🚨 STACKTRACE: $stackTrace'); if (attemptCount < 2) { await _retryProcess(origin, destination, waypoints, attemptCount); } else { _handleFatalError("Connection Error".tr, "Please check your internet and try again.".tr); } } } Future _retryProcess(String origin, String dest, List waypoints, int currentAttempt) async { Log.print( "🔄 Exception or Error caught. Retrying in 1s... (Attempt ${currentAttempt + 1})"); await Future.delayed(const Duration(seconds: 1)); getDirectionMap(origin, dest, waypoints, currentAttempt + 1); } bool _isUsingFallback = false; void _startPollingFallback() { if (_isUsingFallback) return; Log.print('🔄 Starting Polling Fallback Mode'); _isUsingFallback = true; startMasterTimer(); } Future _restorePolyline(String polylineString) async { try { List points = await compute(decodePolylineIsolate, polylineString); mapEngine.polylineCoordinates.clear(); mapEngine.polylineCoordinates.addAll(points); mapEngine.clearPolyline(); mapEngine.polyLines = { ...mapEngine.polyLines, Polyline( polylineId: const PolylineId('route_direct'), points: mapEngine.polylineCoordinates, color: const Color(0xFF2196F3), width: 6, ) }; update(); } catch (e) { Log.print('Error restoring polyline: $e'); } } Future processRideAcceptance( {Map? driverData, required String source}) async { if (_isAcceptanceProcessed || currentRideState.value == RideState.driverApplied || currentRideState.value == RideState.driverArrived || currentRideState.value == RideState.inProgress) { Log.print("✋ Ignored Acceptance from $source. Already processed."); return; } _rideAcceptedViaSource = source; _isAcceptanceProcessed = true; _isDriverAppliedLogicExecuted = true; Log.print("🚀 Winner: $source triggered acceptance! Processing..."); _masterTimer?.cancel(); currentRideState.value = RideState.driverApplied; statusRide = 'Apply'; isSearchingWindow = false; if (driverData != null && driverData.isNotEmpty) { Log.print("📥 Populating Data from $source payload..."); _fillDriverDataLocally(driverData); } else { Log.print("⚠️ No Data in Payload. Fallback to API."); await getUpdatedRideForDriverApply(rideId); } await IosLiveActivityService.startRideActivity( rideId: rideId, driverName: driverName, carDetails: '$make • $carColor', etaText: stringRemainingTimeToPassenger, progress: 0.0, ); _showRideStartNotifications(); final etaText = stringRemainingTimeToPassenger; final carInfo = '$make • $model • $licensePlate'; await RideLiveNotification.showDriverOnWay( driverName: driverName, etaText: etaText, carInfo: carInfo, ); update(); await getDriverCarsLocationToPassengerAfterApplied(); _startSocketWatchdog(); if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; await calculateDriverToPassengerRoute(driverPos, passengerLocation); startTimerFromDriverToPassengerAfterApplied(); } PipService.enablePip(); if (source == "Socket" && mapSocket.isSocketConnected) { Log.print( "🧠 Smart Mode: Socket accepted ride. Skipping polling, relying on WebSocket."); } else { Log.print("🔄 Fallback Mode: $source accepted ride. Starting polling."); _startDriverLocationPollingWithTimer(); } } void _fillDriverDataLocally(Map data) { try { driverId = data['driver_id']?.toString() ?? ''; driverPhone = data['phone']?.toString() ?? ''; String fName = (data['first_name'] ?? data['driver_first_name'] ?? '') .toString() .trim(); String lName = (data['last_name'] ?? data['driver_last_name'] ?? '') .toString() .trim(); final socketDriverName = (data['driverName'] ?? data['driver_name'] ?? '').toString().trim(); driverName = socketDriverName.isNotEmpty ? socketDriverName : [fName, lName].where((part) => part.isNotEmpty).join(' '); make = data['make']?.toString() ?? ''; model = data['model']?.toString() ?? ''; carColor = data['color']?.toString() ?? ''; colorHex = data['color_hex']?.toString() ?? ''; licensePlate = data['car_plate']?.toString() ?? ''; carYear = data['year']?.toString() ?? ''; driverRate = data['ratingDriver']?.toString() ?? '5.0'; driverToken = data['token']?.toString() ?? ''; update(); } catch (e) { Log.print("Error parsing socket driver data: $e"); } } Future cancelRide() async { if (selectedReasonIndex == -1) { Get.snackbar( 'Attention'.tr, 'Please select a reason first'.tr, snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.orange, colorText: Colors.white, ); return; } String finalReason = selectedReasonText; if (finalReason == "Other".tr) { if (otherReasonController.text.trim().isEmpty) { Get.snackbar("Attention".tr, "Please write the reason...".tr, backgroundColor: Colors.red, colorText: Colors.white); return; } finalReason = otherReasonController.text.trim(); } Get.back(); if (isCancelRidePageShown) { mapEngine.changeCancelRidePageShow(); } resetAllMapStates(); stopAllTimers(); currentRideState.value = RideState.cancelled; await RideLiveNotification.cancel(); IosLiveActivityService.endRideActivity(); PipService.disablePip(); if (rideId != 'yet' && rideId != null) { Log.print( '📡 Sending Cancel Request to Server with Reason: $finalReason'); try { await CRUD().post( link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php", payload: { "ride_id": rideId.toString(), "reason": finalReason, "driver_token": driverToken, }, ); } catch (e) { Log.print("Error cancelling on server: $e"); } } clearControllersAndGoHome(); } Future getAIKey(String key) async { var res = await CRUD().get(link: AppLink.getapiKey, payload: {"keyName": key}); if (res != 'failure') { var d = jsonDecode(res)['message']; return d[key].toString(); } return null; } Future getRideStatus(String rideId) async { final response = await CRUD().get( link: "${AppLink.rideServerSide}/ride/rides/getRideStatus.php", payload: {'id': rideId}); Log.print(response); Log.print('2176'); return jsonDecode(response)['data']; } void handleActiveRideOnStartup(dynamic data) { try { if (data == null || data['has_active_ride'] != true) { Log.print('[Startup] No active ride'); currentRideState.value = RideState.noRide; startMasterTimer(); return; } Log.print('[Startup] ✅ Active ride found!'); var rideData = data['ride']; rideId = rideData['ride_id'].toString(); currentRideId = rideId; driverId = rideData['driver_id']?.toString() ?? ''; String status = rideData['status']?.toString().toLowerCase() ?? ''; if (status == 'waiting' || status == 'searching') { currentRideState.value = RideState.searching; isSearchingWindow = true; } else if (status == 'apply' || status == 'applied') { currentRideState.value = RideState.driverApplied; statusRide = 'Apply'; mapSocket.socket.emit('subscribe_driver_location', { 'ride_id': rideId, 'driver_id': driverId, }); if (rideData['driver_info'] != null) { var dInfo = rideData['driver_info']; passengerName = dInfo['first_name']?.toString() ?? ''; driverPhone = dInfo['phone']?.toString() ?? ''; model = dInfo['model']?.toString() ?? ''; licensePlate = dInfo['license_plate']?.toString() ?? ''; } } else if (status == 'arrived') { currentRideState.value = RideState.driverArrived; statusRide = 'Arrived'; isDriverArrivePassenger = true; } else if (status == 'begin' || status == 'started') { currentRideState.value = RideState.inProgress; statusRide = 'Begin'; rideTimerBegin = true; if (rideData['polyline'] != null) { _restorePolyline(rideData['polyline']); } rideIsBeginPassengerTimer(); } update(); startMasterTimer(); } catch (e) { Log.print('[Startup] Error: $e'); currentRideState.value = RideState.noRide; startMasterTimer(); } } Future handleNoDriverFound() async { stopAllTimers(); await RideLiveNotification.cancel(); IosLiveActivityService.endRideActivity(); PipService.disablePip(); _isCancelProcessed = false; currentRideState.value = RideState.noRide; resetAllMapStates(); clearControllersAndGoHome(); Get.defaultDialog( title: "We apologize 😔".tr, middleText: "No drivers found at the moment.\nPlease try again later.".tr, confirm: ElevatedButton( onPressed: () => Navigator.pop(Get.context!), child: Text("Ok".tr), ), ); } bool isDriversDataValid() { return dataCarsLocationByPassenger != 'failure' && dataCarsLocationByPassenger != null && (dataCarsLocationByPassenger is Map) && dataCarsLocationByPassenger.containsKey('message') && dataCarsLocationByPassenger['message'] != null; } void retrySearchForDrivers() async { _isCancelProcessed = false; isSearchingWindow = true; currentRideState.value = RideState.searching; driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr; update(); try { Log.print("🔄 Retrying search for ride ID: $rideId"); var payload = { "ride_id": rideId.toString(), "passenger_id": box.read(BoxName.passengerID).toString(), "passenger_name": box.read(BoxName.name).toString(), "passenger_phone": box.read(BoxName.phone).toString(), "passenger_email": box.read(BoxName.email).toString(), "passenger_token": box.read(BoxName.tokenFCM).toString(), "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), "passenger_rating": "5.0", "start_lat": startLocation.latitude.toString(), "start_lng": startLocation.longitude.toString(), "end_lat": endLocation.latitude.toString(), "end_lng": endLocation.longitude.toString(), "start_name": startNameAddress, "end_name": endNameAddress, "distance": distance.toString(), "distance_text": distanceByPassenger, "duration_text": durationToPassenger.toString(), "price": totalPassenger.toString(), "price_for_driver": costForDriver.toString(), "car_type": box.read(BoxName.carType).toString(), "is_wallet": Get.find().isWalletChecked.toString(), "has_steps": Get.find().wayPoints.length > 1 ? "true" : "false", }; var response = await CRUD().post( link: "${AppLink.rideServerSide}/rides/retry_search_drivers.php", payload: payload, ); if (response['status'] == 'success') { Log.print("✅ Search reset successfully."); startSearchingTimer(); } else { Log.print("❌ Failed to reset search: $response"); handleNoDriverFound(); } } catch (e) { Log.print("❌ Exception in retrySearchForDrivers: $e"); handleNoDriverFound(); } } Future startSearchingTimer() async { _searchTimer?.cancel(); int seconds = 0; Log.print("⏳ Search Timer Started (90s)..."); await RideLiveNotification.showSearching(driversStatusForSearchWindow); _searchTimer = Timer.periodic(const Duration(seconds: 1), (timer) { seconds++; if (currentRideState.value != RideState.searching) { timer.cancel(); return; } if (seconds >= 90) { timer.cancel(); handleNoDriverFound(); } }); } void showNoDriversDialog() { Get.dialog( BackdropFilter( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: CupertinoAlertDialog( title: Text("No Car or Driver Found in your area.".tr, style: AppStyle.title .copyWith(fontSize: 20, fontWeight: FontWeight.bold)), content: Text("No Car or Driver Found in your area.".tr, style: AppStyle.title.copyWith(fontSize: 16)), actions: [ CupertinoDialogAction( onPressed: () { Get.back(); clearControllersAndGoHome(); }, child: Text('OK'.tr, style: TextStyle(color: AppColor.greenColor)), ), ], ), ), barrierDismissible: false, ); } Future getDistanceFromDriverAfterAcceptedRide( String origin, String destination) async { String apiKey = Env.mapKeyOsm; if (origin.isEmpty) { origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; } var uri = Uri.parse( '$dynamicApiUrl?origin=$origin&destination=$destination&steps=false&overview=false'); Log.print('uri: $uri'); http.Response response; Map responseData; try { response = await http.get( uri, headers: { 'X-API-KEY': apiKey, }, ).timeout(const Duration(seconds: 20)); if (response.statusCode != 200) { Log.print('Error from API: ${response.statusCode}'); isLoading = false; update(); return; } if (Get.isBottomSheetOpen ?? false) { Get.back(); } isDrawingRoute = false; responseData = json.decode(response.body); Log.print('responseData: $responseData'); if (responseData['status'] != 'ok') { Log.print('API returned an error: ${responseData['message']}'); isLoading = false; update(); return; } } catch (e) { Log.print('Failed to get directions: $e'); isLoading = false; update(); return; } } Future _stageNiceToHave() async { Log.print('🚀 MapPassengerController: Starting _stageNiceToHave'); await Future.wait([ Future(() async { try { Log.print('🔍 Loading Favorites...'); await locSearch.getFavioratePlaces(); } catch (e) { Log.print("Error: $e"); } }), Future(() async { try { Log.print('🔍 Loading Waypoints...'); locSearch.readyWayPoints(); } catch (e) { Log.print("Error: $e"); } }), Future(() async { try { Log.print('🔍 Loading Rate...'); await getPassengerRate(); } catch (e) { Log.print("Error: $e"); } }), Future(() async { try { Log.print('🔍 Loading Coupons...'); await firstTimeRunToGetCoupon(); } catch (e) { Log.print("Error: $e"); } }), ]); Log.print('✅ MapPassengerController: _stageNiceToHave complete'); try { cardNumber = await SecureStorage().readData(BoxName.cardNumber); } catch (e) { Log.print("Error: $e"); } } Future _stagePricingAndState() async { try { await getKazanPercent(); } catch (e) { Log.print("Error: $e"); } try { await _checkInitialRideStatus(); } catch (e) { Log.print("Error: $e"); } _applyLowEndModeIfNeeded(); } void _applyLowEndModeIfNeeded() { // Placeholder comment from original } void showDrawingBottomSheet() { Log.print( '🔔 showDrawingBottomSheet called. isDrawingRoute: $isDrawingRoute'); final context = Get.context; if (context == null) return; WidgetsBinding.instance.addPostFrameCallback((_) { // Close any existing open dialogs first if (Get.isDialogOpen == true) { Get.back(); } Get.dialog( Dialog( backgroundColor: Colors.transparent, elevation: 0, child: Container( padding: const EdgeInsets.all(24), width: 180, decoration: BoxDecoration( color: Colors.white.withOpacity(0.95), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 20, spreadRadius: 5, ) ], ), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ // App Logo Image.asset( 'assets/images/logo.gif', height: 64, errorBuilder: (context, error, stackTrace) => const Icon( Icons.map, size: 64, color: AppColor.primaryColor, ), ), const SizedBox(height: 16), const SizedBox( width: 24, height: 24, child: MyCircularProgressIndicator(), ), const SizedBox(height: 16), Text( 'Drawing route on map...'.tr, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: AppColor.primaryColor, ), textAlign: TextAlign.center, ), ], ), ), ), barrierDismissible: false, ); // Auto-dismiss after exactly 2 seconds Future.delayed(const Duration(seconds: 2), () { if (Get.isDialogOpen == true) { Get.back(); } }); }); } @override void onInit() async { super.onInit(); await _checkAndRefreshMapStyle(); Get.put(DeepLinkController(), permanent: true); await initilizeGetStorage(); getLocationArea(passengerLocation.latitude, passengerLocation.longitude); unawaited(_stagePricingAndState()); unawaited(_stageNiceToHave()); startMasterTimer(); } @override void onClose() { stopAllTimers(); if (!_timerStreamController.isClosed) { _timerStreamController.close(); } if (!_beginRideStreamController.isClosed) { _beginRideStreamController.close(); } if (!_rideStatusStreamController.isClosed) { _rideStatusStreamController.close(); } if (!timerController.isClosed) { timerController.close(); } super.onClose(); } void clearControllersAndGoHome() { Get.offAll(() => const MapPagePassenger()); } /// Builds a Set of short [Polyline] segments that simulate a dashed line. /// intaleq_maps (MapLibre) doesn't support `patterns`, so we manually /// split the route into dash/gap alternating segments. Set _buildDashedPolylines({ required List points, required double dashLengthMeters, required double gapLengthMeters, required Color color, required int width, required String idPrefix, }) { final Set result = {}; if (points.length < 2) return result; int segmentIndex = 0; bool isDash = true; double remaining = dashLengthMeters; List currentSegment = [points[0]]; for (int i = 0; i < points.length - 1; i++) { final LatLng a = points[i]; final LatLng b = points[i + 1]; double segLen = _haversineDistance(a, b); double covered = 0.0; while (covered < segLen) { double leftInSeg = segLen - covered; if (remaining <= leftInSeg) { // interpolate the endpoint of this dash/gap double fraction = (covered + remaining) / segLen; LatLng interp = LatLng( a.latitude + fraction * (b.latitude - a.latitude), a.longitude + fraction * (b.longitude - a.longitude), ); currentSegment.add(interp); if (isDash && currentSegment.length >= 2) { result.add(Polyline( polylineId: PolylineId('${idPrefix}_seg_$segmentIndex'), points: List.from(currentSegment), color: color, width: width, )); } final double consumed = remaining; segmentIndex++; isDash = !isDash; remaining = isDash ? dashLengthMeters : gapLengthMeters; currentSegment = [interp]; covered += consumed; } else { currentSegment.add(b); covered = segLen; remaining -= leftInSeg; } } } // Flush last dash segment if (isDash && currentSegment.length >= 2) { result.add(Polyline( polylineId: PolylineId('${idPrefix}_seg_$segmentIndex'), points: List.from(currentSegment), color: color, width: width, )); } return result; } /// Haversine distance in meters between two LatLng points. LatLng? _parseLatLng(String? raw) { if (raw == null || raw.trim().isEmpty) return null; final parts = raw.split(','); if (parts.length < 2) return null; final lat = double.tryParse(parts[0].trim()); final lng = double.tryParse(parts[1].trim()); if (lat == null || lng == null) return null; if (lat == 0 && lng == 0) return null; return LatLng(lat, lng); } bool _isHeadingAwayFromRoute({ required double? heading, required double? speed, required int closestRouteIndex, required double distanceFromRouteMeters, }) { if (heading == null || speed == null || speed < 2.5) return false; if (_currentDriverRoutePoints.length < 2) return false; if (distanceFromRouteMeters < 10) return false; int fromIndex = closestRouteIndex; int toIndex = min(closestRouteIndex + 1, _currentDriverRoutePoints.length - 1); if (fromIndex == toIndex && fromIndex > 0) { fromIndex--; } if (fromIndex == toIndex) return false; final double routeBearing = _bearingBetween( _currentDriverRoutePoints[fromIndex], _currentDriverRoutePoints[toIndex], ); final double angleDiff = _angleDifference(heading, routeBearing); return angleDiff > 110; } double _bearingBetween(LatLng a, LatLng b) { final double lat1 = a.latitude * pi / 180; final double lat2 = b.latitude * pi / 180; final double dLng = (b.longitude - a.longitude) * pi / 180; final double y = sin(dLng) * cos(lat2); final double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLng); return (atan2(y, x) * 180 / pi + 360) % 360; } double _angleDifference(double a, double b) { final double diff = ((a - b + 540) % 360) - 180; return diff.abs(); } double _pathDistanceMeters(List points) { if (points.length < 2) return 0.0; double total = 0.0; for (int i = 0; i < points.length - 1; i++) { total += _haversineDistance(points[i], points[i + 1]); } return total; } double _haversineDistance(LatLng a, LatLng b) { const R = 6371000.0; final dLat = (b.latitude - a.latitude) * pi / 180; final dLng = (b.longitude - a.longitude) * pi / 180; final sinLat = sin(dLat / 2); final sinLng = sin(dLng / 2); final h = sinLat * sinLat + cos(a.latitude * pi / 180) * cos(b.latitude * pi / 180) * sinLng * sinLng; return 2 * R * atan2(pow(h, 0.5).toDouble(), pow(1 - h, 0.5).toDouble()); } }