Files
Siro/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart
2026-06-15 19:39:21 +03:00

4365 lines
146 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 gender = '';
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 driverRatingCount = '0';
late String driverCompletedRides = '0';
late String driverTier = 'Verified driver';
late String driverToken = '';
String totalPassenger = '0';
double totalDriver = 0;
double costDistance = 0;
double costDuration = 0;
double averageDuration = 0;
String totalCostPassenger = '0';
String totalPassengerSpeed = '0';
String totalPassengerBalash = '0';
String totalPassengerComfort = '0';
String totalPassengerElectric = '0';
String totalPassengerLady = '0';
String totalPassengerScooter = '0';
String totalPassengerVan = '0';
String totalPassengerRayehGai = '0';
String totalPassengerRayehGaiComfort = '0';
String totalPassengerRayehGaiBalash = '0';
String priceToken = '';
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 _isReviewProcessed = false; // 🛡️ Gatekeeper لمنع فتح التقييم مرتين
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 {
// 🛡️ Gatekeeper: منع فتح التقييم مرتين (Race Condition)
if (_isReviewProcessed) {
Log.print("✋ _checkLastRideForReview: Already processed. Skipping.");
return;
}
_isReviewProcessed = true;
Log.print('⭐ FORCE OPEN RATING PAGE (Get.to mode)');
await getRideStatusFromStartApp();
if (rideStatusFromStartApp['data'] == null) {
_isReviewProcessed = false;
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 = double.parse(totalPassenger) * 1.10;
increasePriceAndRestartSearch(newPrice);
},
),
],
),
barrierDismissible: false,
);
}
Future<void> increasePriceAndRestartSearch(double newPrice) async {
totalPassenger = newPrice.toStringAsFixed(2);
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';
box.write(BoxName.passengerWalletTotal, '0');
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();
_updatePassengerWalkLine();
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 {
// 🛑 Race Condition Guard: إذا تمت معالجتها مسبقاً، تخطى فوراً
if (_isFinishProcessed) {
Log.print("✋ tripFinishedFromDriver: Already processed. Skipping.");
return;
}
_isFinishProcessed = true;
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": double.parse(totalPassenger.toString()).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] : "",
"price_token": priceToken,
};
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 calculateDriverToPassengerRoute(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';
driverRatingCount = data['ratingCount']?.toString() ?? '0';
driverCompletedRides = data['completedRides']?.toString() ?? '0';
driverTier = data['driverTier']?.toString() ?? 'Verified driver';
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) {}
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 = "Siro_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;
try {
final res = await CRUD().post(link: AppLink.getPrices, payload: {
'distance': distance.toString(),
'durationToRide': durationToRide.toString(),
'startNameAddress': startNameAddress,
'endNameAddress': endNameAddress,
'destLat': myDestination.latitude.toString(),
'destLng': myDestination.longitude.toString(),
'passengerLat': newMyLocation.latitude.toString(),
'passengerLng': newMyLocation.longitude.toString(),
'walletVal': box.read(BoxName.passengerWalletTotal)?.toString() ?? '0',
'activeMenuWaypointCount': activeMenuWaypointCount.toString(),
'promo_code': promo.text,
'passenger_id': box.read(BoxName.passengerID),
'country': box.read(BoxName.countryCode) ?? '',
});
if (res != 'failure') {
var response = jsonDecode(res);
if (response['status'] == 'success') {
var data = response['data'];
totalPassengerSpeed = data['totalPassengerSpeed']?.toString() ?? '0';
totalPassengerBalash =
data['totalPassengerBalash']?.toString() ?? '0';
totalPassengerComfort =
data['totalPassengerComfort']?.toString() ?? '0';
totalPassengerElectric =
data['totalPassengerElectric']?.toString() ?? '0';
totalPassengerLady = data['totalPassengerLady']?.toString() ?? '0';
totalPassengerScooter =
data['totalPassengerScooter']?.toString() ?? '0';
totalPassengerVan = data['totalPassengerVan']?.toString() ?? '0';
totalPassengerRayehGai =
data['totalPassengerRayehGai']?.toString() ?? '0';
totalPassengerRayehGaiComfort =
data['totalPassengerRayehGaiComfort']?.toString() ?? '0';
totalPassengerRayehGaiBalash =
data['totalPassengerRayehGaiBalash']?.toString() ?? '0';
promoTaken = true;
update();
Confetti.launch(
context,
options:
const ConfettiOptions(particleCount: 100, spread: 70, y: 0.6),
);
} else {
MyDialog().getDialog(
'Promo Error'.tr,
response['message']?.toString() ?? 'Invalid Promo'.tr,
() => Get.back());
return;
}
}
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 {
durationToAdd = Duration(seconds: durationToRide);
hours = durationToAdd.inHours;
minutes = (durationToAdd.inMinutes % 60).round();
final DateTime currentTime = DateTime.now();
newTime = currentTime.add(durationToAdd);
try {
final res = await CRUD().post(link: AppLink.getPrices, payload: {
'distance': distance.toString(),
'durationToRide': durationToRide.toString(),
'startNameAddress': startNameAddress,
'endNameAddress': endNameAddress,
'destLat': myDestination.latitude.toString(),
'destLng': myDestination.longitude.toString(),
'passengerLat': newMyLocation.latitude.toString(),
'passengerLng': newMyLocation.longitude.toString(),
'walletVal': box.read(BoxName.passengerWalletTotal)?.toString() ?? '0',
'activeMenuWaypointCount': activeMenuWaypointCount.toString(),
'passenger_id': box.read(BoxName.passengerID) ?? '',
'country': box.read(BoxName.countryCode) ?? '',
});
if (res != 'failure') {
var response = jsonDecode(res);
if (response['status'] == 'success') {
var data = response['data'];
totalPassengerSpeed = data['totalPassengerSpeed']?.toString() ?? '0';
totalPassengerBalash =
data['totalPassengerBalash']?.toString() ?? '0';
totalPassengerComfort =
data['totalPassengerComfort']?.toString() ?? '0';
totalPassengerElectric =
data['totalPassengerElectric']?.toString() ?? '0';
totalPassengerLady = data['totalPassengerLady']?.toString() ?? '0';
totalPassengerScooter =
data['totalPassengerScooter']?.toString() ?? '0';
totalPassengerVan = data['totalPassengerVan']?.toString() ?? '0';
totalPassengerRayehGai =
data['totalPassengerRayehGai']?.toString() ?? '0';
totalPassengerRayehGaiComfort =
data['totalPassengerRayehGaiComfort']?.toString() ?? '0';
totalPassengerRayehGaiBalash =
data['totalPassengerRayehGaiBalash']?.toString() ?? '0';
// Save price_token from server response
priceToken = response['price_token']?.toString() ?? '';
totalPassenger = totalPassengerSpeed;
totalCostPassenger = totalPassenger;
}
}
} catch (e) {
Log.print("Error fetching prices: $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();
polyLines = {
...polyLines,
Polyline(
polylineId: const PolylineId('main_route'),
points: decodedPoints,
color: const Color(0xFF2196F3),
width: 6,
)
};
} 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);
_updatePassengerWalkLine();
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' ||
statusRide == 'Arrived' ||
currentRideState.value == RideState.inProgress ||
currentRideState.value == RideState.driverArrived) {
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,
),
};
}
_updatePassengerWalkLine();
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 if (gender == 'Female') {
icon = mapEngine.ladyIcon;
} 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": double.parse(totalPassenger.toString()).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 selectDriverAndCarForMishwariTrip() async {
try {
// Logic for mishwari trip driver selection
} 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);
}
if (driverCarsLocationToPassengerAfterApplied.isNotEmpty &&
myDestination.latitude != 0 &&
myDestination.longitude != 0) {
await calculateDriverToPassengerRoute(
driverCarsLocationToPassengerAfterApplied.last,
myDestination,
isBeginPhase: true,
);
}
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);
final latDiff = (northeastBound.latitude - southwestBound.latitude).abs();
final lngDiff = (northeastBound.longitude - southwestBound.longitude).abs();
if (latDiff < 0.0001 || lngDiff < 0.0001) {
final center = LatLng(
(northeastBound.latitude + southwestBound.latitude) / 2,
(northeastBound.longitude + southwestBound.longitude) / 2,
);
mapController!.animateCamera(CameraUpdate.newLatLngZoom(center, 17));
} else {
try {
var cameraUpdate = CameraUpdate.newLatLngBounds(boundsObj,
left: 180, top: 180, right: 180, bottom: 180);
mapController!.animateCamera(cameraUpdate);
} catch (e) {
final center = LatLng(
(northeastBound.latitude + southwestBound.latitude) / 2,
(northeastBound.longitude + southwestBound.longitude) / 2,
);
mapController!.animateCamera(CameraUpdate.newLatLngZoom(center, 17));
}
}
update();
}
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() ?? '';
gender = data['gender']?.toString() ?? '';
carColor = data['color']?.toString() ?? '';
colorHex = data['color_hex']?.toString() ?? '';
licensePlate = data['car_plate']?.toString() ?? '';
carYear = data['year']?.toString() ?? '';
// المحاولة الفورية لرسم السائق إذا توفرت الإحداثيات في البيانات
double lat = double.tryParse(
data['latitude']?.toString() ?? data['lat']?.toString() ?? '0') ??
0;
double lng = double.tryParse(data['longitude']?.toString() ??
data['lng']?.toString() ??
'0') ??
0;
double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0;
if (lat != 0 && lng != 0) {
LatLng initialPos = LatLng(lat, lng);
if (driverCarsLocationToPassengerAfterApplied.isEmpty) {
driverCarsLocationToPassengerAfterApplied.add(initialPos);
} else {
driverCarsLocationToPassengerAfterApplied[0] = initialPos;
}
// تحديث الماركر فوراً لضمان ظهوره بشكل موثوق
updateDriverMarker(initialPos, heading);
}
driverRate = data['ratingDriver']?.toString() ?? '5.0';
driverRatingCount = data['ratingCount']?.toString() ?? '0';
driverCompletedRides = data['completedRides']?.toString() ?? '0';
driverTier = data['driverTier']?.toString() ?? 'Verified driver';
driverToken = data['token']?.toString() ?? '';
update();
} 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> _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 {} 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());
}
// دالة لبناء الخط المنقط
List<Polyline> _buildDashedLine(LatLng start, LatLng end,
{required Color color, required String prefixId}) {
List<Polyline> segments = [];
double dist = Geolocator.distanceBetween(
start.latitude, start.longitude, end.latitude, end.longitude);
const double dashLengthMeters = 8.0;
const double gapLengthMeters = 6.0;
double latDiff = end.latitude - start.latitude;
double lngDiff = end.longitude - start.longitude;
double totalLength = 0;
int segmentCount = 0;
while (totalLength < dist) {
double startFraction = totalLength / dist;
double endFraction = (totalLength + dashLengthMeters) / dist;
if (endFraction > 1.0) {
endFraction = 1.0;
}
double startLat = start.latitude + latDiff * startFraction;
double startLng = start.longitude + lngDiff * startFraction;
double endLat = start.latitude + latDiff * endFraction;
double endLng = start.longitude + lngDiff * endFraction;
segments.add(
Polyline(
polylineId: PolylineId('${prefixId}_dash_$segmentCount'),
points: [LatLng(startLat, startLng), LatLng(endLat, endLng)],
color: color,
width: 4,
),
);
segmentCount++;
totalLength += dashLengthMeters + gapLengthMeters;
}
return segments;
}
// تحديث الخط المنقط ومكان أيقونة المشي للراكب
void _updatePassengerWalkLine() {
polyLines.removeWhere(
(p) => p.polylineId.value.startsWith('passenger_walk_line'));
markers.removeWhere((m) => m.markerId.value == 'walk_end_marker');
bool shouldShowWalkPath =
(statusRide == 'Apply' || statusRide == 'Arrived') &&
_currentDriverRoutePoints.isNotEmpty &&
passengerLocation.latitude != 0;
if (shouldShowWalkPath) {
final LatLng lastRoadPt = _currentDriverRoutePoints.last;
final walkDashes = _buildDashedLine(
lastRoadPt,
passengerLocation,
color: Colors.blueGrey,
prefixId: 'passenger_walk_line',
);
polyLines.addAll(walkDashes);
markers.add(
Marker(
markerId: const MarkerId('walk_end_marker'),
position: lastRoadPt,
icon: InlqBitmap.fromStyleImage('walk_icon'),
anchor: const Offset(0.5, 0.5),
),
);
}
mapEngine.update();
update();
}
initilizeGetStorage() async {
if (box.read(BoxName.addWork) == null) {
box.write(BoxName.addWork, 'addWork');
}
if (box.read(BoxName.addHome) == null) {
box.write(BoxName.addHome, 'addHome');
}
}
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; // Default rating
} 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;
}
}
firstTimeRunToGetCoupon() async {
if (box.read(BoxName.isFirstTime).toString() == '0' &&
box.read(BoxName.isInstall).toString() == '1' &&
box.read(BoxName.isGiftToken).toString() == '0') {
var promo, discount, validity;
var resPromo = await CRUD().get(link: AppLink.getPromoFirst, payload: {
"passengerID": box.read(BoxName.passengerID).toString(),
});
if (resPromo != 'failure') {
var d1 = jsonDecode(resPromo);
promo = 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: promo ?? '',
discountPercentage: discount ?? '',
validity: validity ?? '',
),
),
),
);
}
}
}