commit c5170a88d247aed6694e590b20363683d0cb6204 Author: Hamza-Ayed Date: Thu Jun 11 13:47:40 2026 +0300 Update: 2026-06-11 13:47:39 diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index c264a61..41dbe32 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -846,6 +846,7 @@ class RideLifecycleController extends GetxController { rideIsBeginPassengerTimer(); runWhenRideIsBegin(); + _updatePassengerWalkLine(); update(); } @@ -1730,7 +1731,7 @@ class RideLifecycleController extends GetxController { String generateTrackingLink(String rideId, String driverId) { String cleanRideId = rideId.toString().trim(); String cleanDriverId = driverId.toString().trim(); - const String secretSalt = "Intaleq_Secure_Track_2025"; + const String secretSalt = "Siro_Secure_Track_2025"; String rawString = "$cleanRideId$cleanDriverId$secretSalt"; var bytes = utf8.encode(rawString); @@ -2260,6 +2261,7 @@ class RideLifecycleController extends GetxController { } mapEngine.fitCameraToPoints(driverPos, passengerPos); + _updatePassengerWalkLine(); update(); } } catch (e) { @@ -2386,6 +2388,7 @@ class RideLifecycleController extends GetxController { ), }; } + _updatePassengerWalkLine(); update(); } } @@ -3508,9 +3511,29 @@ class RideLifecycleController extends GetxController { LatLngBounds boundsObj = LatLngBounds(northeast: northeastBound, southwest: southwestBound); - var cameraUpdate = CameraUpdate.newLatLngBounds(boundsObj, - left: 180, top: 180, right: 180, bottom: 180); - mapController!.animateCamera(cameraUpdate); + + final latDiff = (northeastBound.latitude - southwestBound.latitude).abs(); + final lngDiff = (northeastBound.longitude - southwestBound.longitude).abs(); + + if (latDiff < 0.0001 || lngDiff < 0.0001) { + final center = LatLng( + (northeastBound.latitude + southwestBound.latitude) / 2, + (northeastBound.longitude + southwestBound.longitude) / 2, + ); + mapController!.animateCamera(CameraUpdate.newLatLngZoom(center, 17)); + } else { + try { + var cameraUpdate = CameraUpdate.newLatLngBounds(boundsObj, + left: 180, top: 180, right: 180, bottom: 180); + mapController!.animateCamera(cameraUpdate); + } catch (e) { + final center = LatLng( + (northeastBound.latitude + southwestBound.latitude) / 2, + (northeastBound.longitude + southwestBound.longitude) / 2, + ); + mapController!.animateCamera(CameraUpdate.newLatLngZoom(center, 17)); + } + } update(); } @@ -4562,4 +4585,82 @@ class RideLifecycleController extends GetxController { sinLng; return 2 * R * atan2(pow(h, 0.5).toDouble(), pow(1 - h, 0.5).toDouble()); } + + // دالة لبناء الخط المنقط + List _buildDashedLine(LatLng start, LatLng end, + {required Color color, required String prefixId}) { + List segments = []; + double dist = Geolocator.distanceBetween( + start.latitude, start.longitude, end.latitude, end.longitude); + + const double dashLengthMeters = 8.0; + const double gapLengthMeters = 6.0; + + double latDiff = end.latitude - start.latitude; + double lngDiff = end.longitude - start.longitude; + + double totalLength = 0; + int segmentCount = 0; + + while (totalLength < dist) { + double startFraction = totalLength / dist; + double endFraction = (totalLength + dashLengthMeters) / dist; + + if (endFraction > 1.0) { + endFraction = 1.0; + } + + double startLat = start.latitude + latDiff * startFraction; + double startLng = start.longitude + lngDiff * startFraction; + double endLat = start.latitude + latDiff * endFraction; + double endLng = start.longitude + lngDiff * endFraction; + + segments.add( + Polyline( + polylineId: PolylineId('${prefixId}_dash_$segmentCount'), + points: [LatLng(startLat, startLng), LatLng(endLat, endLng)], + color: color, + width: 4, + ), + ); + segmentCount++; + totalLength += dashLengthMeters + gapLengthMeters; + } + return segments; + } + + // تحديث الخط المنقط ومكان أيقونة المشي للراكب + void _updatePassengerWalkLine() { + polyLines.removeWhere( + (p) => p.polylineId.value.startsWith('passenger_walk_line')); + markers.removeWhere((m) => m.markerId.value == 'walk_end_marker'); + + bool shouldShowWalkPath = + (statusRide == 'Apply' || statusRide == 'Arrived') && + _currentDriverRoutePoints.isNotEmpty && + passengerLocation.latitude != 0; + + if (shouldShowWalkPath) { + final LatLng lastRoadPt = _currentDriverRoutePoints.last; + + final walkDashes = _buildDashedLine( + lastRoadPt, + passengerLocation, + color: Colors.blueGrey, + prefixId: 'passenger_walk_line', + ); + polyLines.addAll(walkDashes); + + markers.add( + Marker( + markerId: const MarkerId('walk_end_marker'), + position: lastRoadPt, + icon: InlqBitmap.fromStyleImage('walk_icon'), + anchor: const Offset(0.5, 0.5), + ), + ); + } + mapEngine.update(); + update(); + } } commit 977adfe99daffca96839c0cab3fef5546ebfb31a Author: Hamza-Ayed Date: Wed Jun 10 18:11:50 2026 +0300 Update: 2026-06-10 18:11:50 diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index c229ad2..c264a61 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -112,6 +112,7 @@ class RideLifecycleController extends GetxController { late String driverId = ''; late String make = ''; late String model = ''; + late String gender = ''; late String carColor = ''; late String licensePlate = ''; late String driverName = ''; @@ -120,6 +121,9 @@ class RideLifecycleController extends GetxController { late String colorHex = ''; late String carYear = ''; late String driverRate = '5.0'; + late String driverRatingCount = '0'; + late String driverCompletedRides = '0'; + late String driverTier = 'Verified driver'; late String driverToken = ''; double kazan = 8; @@ -1481,7 +1485,8 @@ class RideLifecycleController extends GetxController { // إيقاف جلب السيارات المجاورة ومسحها، باستثناء السائق الذي قبل الطلب mapEngine.reloadStartApp = false; - mapEngine.markers.removeWhere((marker) => marker.markerId.value != driverId.toString()); + mapEngine.markers + .removeWhere((marker) => marker.markerId.value != driverId.toString()); mapEngine.update(); await getDriverCarsLocationToPassengerAfterApplied(); @@ -1490,8 +1495,7 @@ class RideLifecycleController extends GetxController { LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; Log.print( '[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation'); - await getInitialDriverDistanceAndDuration(driverPos, passengerLocation); - await drawDriverPathOnly(driverPos, passengerLocation); + await calculateDriverToPassengerRoute(driverPos, passengerLocation); mapEngine.fitCameraToPoints(driverPos, passengerLocation); } @@ -1656,6 +1660,9 @@ class RideLifecycleController extends GetxController { driverToken = data['token']?.toString() ?? ''; carYear = data['year']?.toString() ?? ''; driverRate = data['ratingDriver']?.toString() ?? '5.0'; + driverRatingCount = data['ratingCount']?.toString() ?? '0'; + driverCompletedRides = data['completedRides']?.toString() ?? '0'; + driverTier = data['driverTier']?.toString() ?? 'Verified driver'; update(); } @@ -2221,6 +2228,15 @@ class RideLifecycleController extends GetxController { polyLines = polyLines .where((p) => !p.polylineId.value.startsWith('driver_route')) .toSet(); + polyLines = { + ...polyLines, + Polyline( + polylineId: const PolylineId('main_route'), + points: decodedPoints, + color: const Color(0xFF2196F3), + width: 6, + ) + }; } else { // مسح السلمات القديمة أولاً polyLines = polyLines @@ -2290,7 +2306,9 @@ class RideLifecycleController extends GetxController { _routeHeadingMismatchCount = 0; _isRecalculatingRoute = true; if (statusRide == 'Begin' || - currentRideState.value == RideState.inProgress) { + statusRide == 'Arrived' || + currentRideState.value == RideState.inProgress || + currentRideState.value == RideState.driverArrived) { await calculateDriverToPassengerRoute(driverPos, myDestination, isBeginPhase: true); } else { @@ -2504,6 +2522,8 @@ class RideLifecycleController extends GetxController { String icon; if (model.contains('دراجة') || make.contains('دراجة')) { icon = mapEngine.motoIcon; + } else if (gender == 'Female') { + icon = mapEngine.ladyIcon; } else { icon = mapEngine.carIcon; } @@ -3026,6 +3046,17 @@ class RideLifecycleController extends GetxController { mapEngine.playRouteAnimation( mapEngine.polylineCoordinates, mapEngine.lastComputedBounds); } + + if (driverCarsLocationToPassengerAfterApplied.isNotEmpty && + myDestination.latitude != 0 && + myDestination.longitude != 0) { + await calculateDriverToPassengerRoute( + driverCarsLocationToPassengerAfterApplied.last, + myDestination, + isBeginPhase: true, + ); + } + update(); } @@ -3903,12 +3934,37 @@ class RideLifecycleController extends GetxController { make = data['make']?.toString() ?? ''; model = data['model']?.toString() ?? ''; + gender = data['gender']?.toString() ?? ''; carColor = data['color']?.toString() ?? ''; colorHex = data['color_hex']?.toString() ?? ''; licensePlate = data['car_plate']?.toString() ?? ''; carYear = data['year']?.toString() ?? ''; + // المحاولة الفورية لرسم السائق إذا توفرت الإحداثيات في البيانات + double lat = double.tryParse( + data['latitude']?.toString() ?? data['lat']?.toString() ?? '0') ?? + 0; + double lng = double.tryParse(data['longitude']?.toString() ?? + data['lng']?.toString() ?? + '0') ?? + 0; + double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0; + + if (lat != 0 && lng != 0) { + LatLng initialPos = LatLng(lat, lng); + if (driverCarsLocationToPassengerAfterApplied.isEmpty) { + driverCarsLocationToPassengerAfterApplied.add(initialPos); + } else { + driverCarsLocationToPassengerAfterApplied[0] = initialPos; + } + // تحديث الماركر فوراً لضمان ظهوره بشكل موثوق + updateDriverMarker(initialPos, heading); + } + driverRate = data['ratingDriver']?.toString() ?? '5.0'; + driverRatingCount = data['ratingCount']?.toString() ?? '0'; + driverCompletedRides = data['completedRides']?.toString() ?? '0'; + driverTier = data['driverTier']?.toString() ?? 'Verified driver'; driverToken = data['token']?.toString() ?? ''; update(); @@ -4185,55 +4241,6 @@ class RideLifecycleController extends GetxController { ); } - 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'); commit d8901e1a879f696e512e13d389d666baae33dc84 Author: Hamza-Ayed Date: Tue Jun 9 08:40:31 2026 +0300 first commit diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart new file mode 100644 index 0000000..c229ad2 --- /dev/null +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -0,0 +1,4558 @@ +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()); + } +}