4365 lines
146 KiB
Dart
4365 lines
146 KiB
Dart
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 ?? '',
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|