4571 lines
156 KiB
Diff
4571 lines
156 KiB
Diff
commit d8901e1a879f696e512e13d389d666baae33dc84
|
||
Author: Hamza-Ayed <hamzaayedflutter@gmail.com>
|
||
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<String, dynamic> rideData = {};
|
||
+ Map<String, dynamic> dInfo = {};
|
||
+ List<LatLng> datadriverCarsLocationToPassengerAfterApplied = [];
|
||
+ double distanceOfTrip = 0.0;
|
||
+ double apiDistanceMeters = 0.0;
|
||
+ double tax = 0.0;
|
||
+ int selectedPassengerCount = 1;
|
||
+ final GlobalKey<FormState> increaseFeeFormKey = GlobalKey<FormState>();
|
||
+ final GlobalKey<FormState> messagesFormKey = GlobalKey<FormState>();
|
||
+ final GlobalKey<FormState> promoFormKey = GlobalKey<FormState>();
|
||
+ String walletStr = '0';
|
||
+ double walletVal = 0.0;
|
||
+ bool rideConfirm = false;
|
||
+ LatLng driverLocationToPassenger = const LatLng(32, 35);
|
||
+ final TextEditingController messageToDriver = TextEditingController();
|
||
+ int carsOrder = 0;
|
||
+
|
||
+ Rx<RideState> 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<RideState, int> _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<LatLng> _currentDriverRoutePoints = [];
|
||
+ double _currentDriverRouteDistanceMeters = 0.0;
|
||
+ int _currentDriverRouteDurationSeconds = 0;
|
||
+
|
||
+ int _currentSearchPhase = 0;
|
||
+ bool _isFetchingDriverLocation = false;
|
||
+ Timer? _watchdogTimer;
|
||
+
|
||
+ final List<int> _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<String> _rideStatusStreamController =
|
||
+ StreamController<String>.broadcast();
|
||
+ Stream<String> get rideStatusStream => _rideStatusStreamController.stream;
|
||
+
|
||
+ final StreamController<String> _beginRideStreamController =
|
||
+ StreamController<String>.broadcast();
|
||
+ Stream<String> get beginRideStream => _beginRideStreamController.stream;
|
||
+
|
||
+ final StreamController<int> _timerStreamController = StreamController<int>();
|
||
+ Stream<int> 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<LocationSearchController>();
|
||
+ MapEngineController get mapEngine => Get.find<MapEngineController>();
|
||
+ NearbyDriversController get nearbyDrivers =>
|
||
+ Get.find<NearbyDriversController>();
|
||
+ MapSocketController get mapSocket => Get.find<MapSocketController>();
|
||
+ UiInteractionsController get uiInteractions =>
|
||
+ Get.find<UiInteractionsController>();
|
||
+
|
||
+ // 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<String> get placesCoordinate => locSearch.placesCoordinate;
|
||
+ set placesCoordinate(List<String> val) => locSearch.placesCoordinate = val;
|
||
+
|
||
+ int get activeMenuWaypointCount => locSearch.activeMenuWaypointCount;
|
||
+ set activeMenuWaypointCount(int val) =>
|
||
+ locSearch.activeMenuWaypointCount = val;
|
||
+
|
||
+ List<LatLng?> get menuWaypoints => locSearch.menuWaypoints;
|
||
+ set menuWaypoints(List<LatLng?> val) => locSearch.menuWaypoints = val;
|
||
+
|
||
+ List<String> get menuWaypointNames => locSearch.menuWaypointNames;
|
||
+ set menuWaypointNames(List<String> 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<Marker> get markers => mapEngine.markers;
|
||
+ set markers(Set<Marker> val) {
|
||
+ mapEngine.markers = val;
|
||
+ mapEngine.update();
|
||
+ }
|
||
+
|
||
+ Set<Polyline> get polyLines => mapEngine.polyLines;
|
||
+ set polyLines(Set<Polyline> 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<LatLng> get driverCarsLocationToPassengerAfterApplied =>
|
||
+ nearbyDrivers.driverCarsLocationToPassengerAfterApplied;
|
||
+ set driverCarsLocationToPassengerAfterApplied(List<LatLng> 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<void> _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<void> _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<void> _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<void> 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<void> 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<int>();
|
||
+ 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<void> 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<void> begiVIPTripFromPassenger() async {
|
||
+ timeToPassengerFromDriverAfterApplied = 0;
|
||
+ remainingTime = 0;
|
||
+ isBottomSheetShown = false;
|
||
+ remainingTimeToPassengerFromDriverAfterApplied = 0;
|
||
+ remainingTimeDriverWaitPassenger5Minute = 0;
|
||
+ rideTimerBegin = true;
|
||
+ statusRideVip = 'Begin';
|
||
+ isDriverInPassengerWay = false;
|
||
+ isDriverArrivePassenger = false;
|
||
+ update();
|
||
+ rideIsBeginPassengerTimerVIP();
|
||
+ runWhenRideIsBegin();
|
||
+ }
|
||
+
|
||
+ Future<void> 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<String, dynamic>? tripData;
|
||
+ try {
|
||
+ var rawTrip = box.read(BoxName.tripData);
|
||
+ if (rawTrip is Map) {
|
||
+ tripData = Map<String, dynamic>.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<LatLng> 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<bool> postRideDetailsToServer() async {
|
||
+ if (mapEngine.polylineCoordinates.isEmpty) return false;
|
||
+
|
||
+ LatLng startLoc = mapEngine.polylineCoordinates.first;
|
||
+ LatLng endLoc = mapEngine.polylineCoordinates.last;
|
||
+
|
||
+ Map<String, dynamic> 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<PaymentController>().isWalletChecked.toString(),
|
||
+ "has_steps": Get.find<WayPointController>().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<void> 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<void> 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<void> 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<LatLng> 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<void> 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<LatLng> 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<void> calculateDriverToPassengerRoute(
|
||
+ LatLng driverPos, LatLng passengerPos,
|
||
+ {bool isBeginPhase = false}) async {
|
||
+ final Map<String, String> 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<LatLng> 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<void> 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<LatLng> 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<void> 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<Marker?>().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<Marker?>().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<void> _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<PaymentController>().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<void> 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<void> 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<void> 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<void> 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<void> detectAndCacheDeviceTier() async {
|
||
+ bool isHighEnd = await DevicePerformanceManager.isHighEndDevice();
|
||
+ Log.print("Device Analysis - Is Flagship/HighEnd? $isHighEnd");
|
||
+ box.write(BoxName.lowEndMode, !isHighEnd);
|
||
+ }
|
||
+
|
||
+ Future<void> 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<void> 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<DateTime> 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<void> saveTripData(
|
||
+ Map<String, dynamic> driver, DateTime tripDateTime) async {
|
||
+ try {
|
||
+ LatLng startLoc = mapEngine.polylineCoordinates.first;
|
||
+ Map<String, dynamic> 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<NotificationController>().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<void> 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<String> notifiedDrivers = {};
|
||
+
|
||
+ Future<void> 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<void> processRideFinished(List<dynamic> 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<AudioRecorderController>()) {
|
||
+ Get.find<AudioRecorderController>().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<void> 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<void>
|
||
+ 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<PaymentController>()
|
||
+ .payToDriverForCancelAfterAppliedAndHeNearYou(rideId);
|
||
+ },
|
||
+ );
|
||
+ },
|
||
+ ),
|
||
+ ],
|
||
+ ),
|
||
+ );
|
||
+ }
|
||
+ }
|
||
+
|
||
+ Future<void> 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<void> _checkAndRefreshMapStyle() async {
|
||
+ try {
|
||
+ final String styleJson = await rootBundle.loadString('assets/style.json');
|
||
+ final Map<String, dynamic> 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<WayPointController>()) {
|
||
+ Get.find<WayPointController>().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<String> 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<String> 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<void> 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<LatLng> 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<void> 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<LatLng> routePoints) {
|
||
+ nearbyDrivers.analyzeBehavior(currentPosition, routePoints);
|
||
+ }
|
||
+
|
||
+ void detectStops(Position currentPosition) {
|
||
+ nearbyDrivers.detectStops(currentPosition);
|
||
+ }
|
||
+
|
||
+ Future<void> getDirectionMap(String origin, String destination,
|
||
+ [List<String> 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<String, String> 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<String, dynamic> 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<LatLng> 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<dynamic> 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<void> _retryProcess(String origin, String dest, List<String> 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<void> _restorePolyline(String polylineString) async {
|
||
+ try {
|
||
+ List<LatLng> 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<void> processRideAcceptance(
|
||
+ {Map<String, dynamic>? 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<String, dynamic> 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<void> 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<String?> 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<String> 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<void> 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<PaymentController>().isWalletChecked.toString(),
|
||
+ "has_steps": Get.find<WayPointController>().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<void> 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<void> 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<String, dynamic> 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<void> _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<void> _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<Polyline> _buildDashedPolylines({
|
||
+ required List<LatLng> points,
|
||
+ required double dashLengthMeters,
|
||
+ required double gapLengthMeters,
|
||
+ required Color color,
|
||
+ required int width,
|
||
+ required String idPrefix,
|
||
+ }) {
|
||
+ final Set<Polyline> result = {};
|
||
+ if (points.length < 2) return result;
|
||
+
|
||
+ int segmentIndex = 0;
|
||
+ bool isDash = true;
|
||
+ double remaining = dashLengthMeters;
|
||
+ List<LatLng> 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<LatLng> 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());
|
||
+ }
|
||
+}
|