Files
Siro/siro_driver/lib/controller/home/captin/map_driver_controller.dart
2026-06-11 13:47:40 +03:00

2889 lines
103 KiB
Dart
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:ui';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
import 'package:siro_driver/views/widgets/error_snakbar.dart';
import 'package:siro_driver/views/widgets/mydialoug.dart';
import 'package:bubble_head/bubble.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart' as geo;
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../constant/country_polygons.dart';
import '../../../constant/links.dart';
import '../../../constant/table_names.dart';
import '../../../env/env.dart';
import '../../../views/home/Captin/orderCaptin/marker_generator.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/Rate/rate_passenger.dart';
import '../../../views/home/Captin/home_captain/home_captin.dart';
import '../../firebase/firbase_messge.dart';
import '../../firebase/notification_service.dart';
import '../../functions/crud.dart';
import '../../functions/location_controller.dart';
import '../../functions/tts.dart';
import '../../functions/audio_recorder_controller.dart';
import 'behavior_controller.dart';
class MapDriverController extends GetxController
with GetSingleTickerProviderStateMixin {
bool isLoading = true;
final formKey1 = GlobalKey<FormState>();
final formKey2 = GlobalKey<FormState>();
final formKeyCancel = GlobalKey<FormState>();
final messageToPassenger = TextEditingController();
final sosEmergincyNumberCotroller = TextEditingController();
final cancelTripCotroller = TextEditingController();
List data = [];
List dataDestination = [];
LatLngBounds? boundsData;
InlqBitmap carIcon = InlqBitmap.defaultMarker;
InlqBitmap passengerIcon = InlqBitmap.defaultMarker;
InlqBitmap startIcon = InlqBitmap.defaultMarker;
InlqBitmap endIcon = InlqBitmap.defaultMarker;
InlqBitmap? walkIcon;
final List<LatLng> polylineCoordinates = [];
final List<LatLng> polylineCoordinatesDestination = [];
List<Polyline> polyLines = [];
List<Polyline> polyLinesDestination = [];
Set<Marker> markers = {};
String passengerLocation = '';
String passengerDestination = '';
// [Fix N-4] استبدال 5 متغيرات منفصلة بقائمة واحدة
List<String> steps = ['', '', '', '', ''];
String passengerWalletBurc = '';
String timeOfOrder = '';
String duration = '';
String totalCost = '';
String distance = '0';
String? passengerName;
String passengerEmail = '';
String totalPricePassenger = '';
String passengerPhone = '';
String rideId = '';
String isHaveSteps = '';
String paymentAmount = '0';
String paymentMethod = '';
String passengerId = '';
String driverId = '';
String tokenPassenger = '';
String durationToPassenger = '100';
String walletChecked = '';
String direction = '';
String durationOfRideValue = '';
String status = '';
int timeWaitingPassenger = 5; //5 miniute
bool isPassengerInfoWindow = false;
bool isBtnRideBegin = false;
bool isArrivedSend = true;
bool isdriverWaitTimeEnd = false;
bool isRideFinished = false;
bool isRideStarted = false;
bool isPriceWindow = false;
double passengerInfoWindowHeight = Get.height * .38;
double driverEndPage = 100;
double progress = 0;
double progressToPassenger = 0;
double progressInPassengerLocationFromDriver = 0;
bool isRideBegin = false;
int progressTimerToShowPassengerInfoWindowFromDriver = 25;
int remainingTimeToShowPassengerInfoWindowFromDriver = 25;
int remainingTimeToPassenger = 60;
int remainingTimeInPassengerLocatioWait = 60;
// [Fix P-1] Gatekeeper لمنع الاستدعاءات المتكررة لـ getRoute()
bool _isRouteRequested = false;
// 🔥 [Fix Double-Init] Debounce لمنع التنفيذ المتزامن/المتسارع لـ argumentLoading()
// StatelessWidget يُسجّل addPostFrameCallback في كل build() — هذا يمنع التنفيذ المتكرر
DateTime? _lastArgumentLoadingTime;
bool _argumentLoadingInProgress = false;
Completer<void>? _mapReadyCompleter;
// [Fix P-2] Throttle لتقليل تحديثات UI من مستمع GPS
DateTime _lastUIUpdate = DateTime.fromMillisecondsSinceEpoch(0);
static const _uiThrottleMs = 400;
// ─── Navigation & Smoothing ──────────────────────────────────────────
AnimationController? _animController;
LatLng? smoothedLocation;
double smoothedHeading = 0.0;
LatLng? _oldLoc;
LatLng? _targetLoc;
double _oldHeading = 0.0;
double _targetHeading = 0.0;
List<Map<String, dynamic>> routeSteps = [];
int currentStepIndex = 0;
String currentInstruction = "";
String nextInstruction = "";
String distanceToNextStep = "";
int currentManeuverModifier = 0;
bool _nextInstructionSpoken = false;
bool isTtsEnabled = true;
StreamSubscription<geo.Position>? _locationSubscription;
// ─────────────────────────────────────────────────────────────────────
bool isDriverNearPassengerStart = false;
IntaleqMapController? mapController;
LatLng myLocation = LatLng(0, 0);
int remainingTimeTimerRideBegin = 60;
String stringRemainingTimeRideBegin = '';
String stringRemainingTimeRideBegin1 = '';
double progressTimerRideBegin = 0;
Timer? timer;
String? mapAPIKEY;
final zones = <Zone>[];
String canelString = 'yet';
LatLng latLngPassengerLocation = LatLng(0, 0);
LatLng latLngPassengerDestination = LatLng(0, 0);
// في MapDriverController
void toggleTts() {
isTtsEnabled = !isTtsEnabled;
if (!isTtsEnabled && Get.isRegistered<TextToSpeechController>()) {
Get.find<TextToSpeechController>().stop();
}
update();
}
void playVoiceInstruction(String text) {
if (!isTtsEnabled) return;
if (Get.isRegistered<TextToSpeechController>()) {
Get.find<TextToSpeechController>().speakText(text);
}
}
/// [Fix M-5] disposeEverything يجب أن يتوقف عن استدعاء onClose مباشرة
/// لأن GetX يتولى ذلك تلقائياً. نستخدم _stopAllServices فقط.
void disposeEverything() {
print("--- KILLING ALL DRIVER TIMERS (via disposeEverything) ---");
_stopAllServices();
}
@override
void onClose() {
print("--- KILLING ALL DRIVER TIMERS ---");
_rideTimer?.cancel();
_rideTimer = null;
_passengerTimer?.cancel();
_passengerTimer = null;
_waitingTimer?.cancel();
_waitingTimer = null;
timer?.cancel();
timer = null;
_navigationTimer?.cancel();
_navigationTimer = null;
_posSub?.cancel();
_posSub = null;
_animController?.dispose();
_locationSubscription?.cancel();
super.onClose();
}
void onMapCreated(IntaleqMapController controller) {
mapController = controller;
if (Get.isRegistered<LocationController>()) {
myLocation = Get.find<LocationController>().myLocation;
controller.animateCamera(CameraUpdate.newLatLngZoom(myLocation, 16));
}
// [Fix P-1] إكمال الـ Completer لتحرير argumentLoading من الانتظار
_mapReadyCompleter?.complete();
_mapReadyCompleter = null;
if (!_isRouteRequested) {
// أول مرة — argumentLoading لم تكتمل بعد، ارسم المسار الأولي
// startListeningStepNavigation() محذوفة من هنا عمداً:
// تُستدعى من argumentLoading() بعد اكتمال getRoute() وتحميل البيانات
if (isRideStarted) {
getRoute(
origin: myLocation,
destination: latLngPassengerDestination,
routeColor: Colors.blue,
);
} else {
getRoute(
origin: myLocation,
destination: latLngPassengerLocation,
routeColor: Colors.yellow,
);
}
} else {
// 🔥 [Fix Rebuild] إعادة بناء الخريطة (مثلاً تغيير theme أو update())
// أعد رسم البوليلاين الموجود وأعد تشغيل الملاحة إذا لزم
_redrawExistingPolylines();
if (_navigationTimer == null || !_navigationTimer!.isActive) {
startListeningStepNavigation();
}
}
}
/// إعادة رسم البوليلاين الموجود بعد إعادة بناء الخريطة (GetBuilder rebuild)
/// يُستدعى فقط من onMapCreated() عند _isRouteRequested == true
void _redrawExistingPolylines() {
if (upcomingPathPoints.isEmpty) {
// لا يوجد مسار محمّل — لا شيء نرسمه
return;
}
final remaining = upcomingPathPoints.sublist(_lastTraveledIndex);
polyLines.clear();
polyLines.add(
Polyline(
polylineId: const PolylineId("upcoming_route"),
points: remaining,
width: 8,
color: isRideStarted ? Colors.blue : Colors.yellow,
),
);
// إعادة رسم المسار المقطوع (رمادي) إذا وجد
if (_lastTraveledIndex > 0) {
final traveled = upcomingPathPoints.sublist(0, _lastTraveledIndex + 1);
polyLines.add(
Polyline(
polylineId: const PolylineId('traveled_route'),
points: traveled,
color: Colors.grey.withValues(alpha: 0.8),
width: 7,
zIndex: 1,
),
);
}
_updatePassengerWalkLine();
Log.print(
"🔄 [onMapCreated] Redrawn ${polyLines.length} polylines after map rebuild.");
update();
}
bool isCameraLocked = true; // للتحكم في تتبع الكاميرا
Timer? _cameraLockTimer;
void onUserMapInteraction() {
if (isCameraLocked) {
isCameraLocked = false;
update();
}
_cameraLockTimer?.cancel();
_cameraLockTimer = Timer(const Duration(seconds: 5), () {
if (!isClosed) {
isCameraLocked = true;
if (myLocation != null && mapController != null) {
if (Get.isRegistered<LocationController>()) {
final locCtrl = Get.find<LocationController>();
final double speedKmh = locCtrl.speed * 3.6;
final double bearing = speedKmh > 5 ? locCtrl.heading : 0.0;
_animateCameraToNavigationMode(myLocation!, bearing);
} else {
_animateCameraToNavigationMode(myLocation!, 0.0);
}
}
update();
}
});
}
int _walkLineUpdateCounter = 0;
Future<void> startListeningStepNavigation() async {
// Cancel any previous listener
_navigationTimer?.cancel();
// Reuse LocationController's GPS stream instead of opening a
// second GPS channel — eliminates duplicate battery/CPU drain.
// Lightweight Timer-based polling at 500ms (was a full GPS stream).
_navigationTimer = Timer.periodic(
const Duration(milliseconds: 500),
(_) {
if (isClosed || mapController == null) return;
final locCtrl = Get.isRegistered<LocationController>()
? Get.find<LocationController>()
: null;
if (locCtrl == null || locCtrl.myLocation.latitude == 0) return;
final LatLng newLoc = locCtrl.myLocation;
final double heading = locCtrl.heading;
final double speedKmh = locCtrl.speed * 3.6;
// Jitter filter
if (_lastRecordedLocation != null) {
final double dist = Geolocator.distanceBetween(
newLoc.latitude,
newLoc.longitude,
_lastRecordedLocation!.latitude,
_lastRecordedLocation!.longitude,
);
if (dist < 3.0) return;
}
_lastRecordedLocation = newLoc;
myLocation = newLoc;
_oldLoc = smoothedLocation ?? newLoc;
_targetLoc = newLoc;
_oldHeading = smoothedHeading;
_targetHeading = speedKmh > 0.5 ? heading : _oldHeading;
_animController?.forward(from: 0.0);
if (!isClosed) {
updateMarker();
if (upcomingPathPoints.isNotEmpty) {
_updateTraveledPolylineSmart(myLocation);
}
// تحديث خط المشي المنقط كل 3 ثوانٍ أثناء التنقل (كل 6 دورات × 500ms)
_walkLineUpdateCounter++;
if (_walkLineUpdateCounter % 6 == 0) {
_updatePassengerWalkLine();
}
if (isCameraLocked) {
final double bearing = speedKmh > 5 ? heading : 0.0;
_animateCameraToNavigationMode(newLoc, bearing);
}
checkForNextStep(myLocation);
checkDestinationProximity();
final now = DateTime.now();
if (now.difference(_lastUIUpdate).inMilliseconds > _uiThrottleMs) {
_lastUIUpdate = now;
update();
}
}
},
);
}
void changeStatusDriver() {
status = 'On';
update();
}
AudioRecorderController _getAudioController() {
if (!Get.isRegistered<AudioRecorderController>()) {
Get.put(AudioRecorderController(), permanent: true);
}
return Get.find<AudioRecorderController>();
}
void changeDriverEndPage() {
driverEndPage = remainingTimeTimerRideBegin < 60 ? 160 : 100;
update();
}
takeSnapMap() {
// mapController!.takeSnapshot();
}
// [Fix P-3] إزالة dispose() المكررة — GetX يستدعي onClose() تلقائياً
void _stopAllServices() {
_rideTimer?.cancel();
_passengerTimer?.cancel();
_waitingTimer?.cancel();
_posSub?.cancel();
}
Future openGoogleMapFromDriverToPassenger() async {
var endLat = latLngPassengerLocation.latitude;
var endLng = latLngPassengerLocation.longitude;
var startLat = Get.find<LocationController>().myLocation.latitude;
var startLng = Get.find<LocationController>().myLocation.longitude;
/// [Fix N-1] تصحيح رابط Google Maps: & → ?
String url =
'https://www.google.com/maps/dir/$startLat,$startLng/$endLat,$endLng/?directionsmode=driving';
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url));
} else {
throw 'Could not launch google maps';
}
}
void clearPolyline() {
polyLines = [];
polyLinesDestination = [];
polylineCoordinates.clear();
polylineCoordinatesDestination.clear();
update();
}
void changeRideToBeginToPassenger() {
isRideBegin = true;
passengerInfoWindowHeight = Get.height * .22;
update();
}
// متغير لمنع التكرار
bool _isCancelReceived = false;
/// **معالجة إلغاء الراكب الموحدة (Gatekeeper)**
void processRideCancelledByPassenger(
String reason, {
String source = "Unknown",
}) {
if (_isCancelReceived) return;
_isCancelReceived = true;
// [Fix 2] إيقاف جميع التايمرات النشطة فوراً عند إلغاء الراكب.
// كانت هذه التايمرات تستمر حتى onClose() مما يسبب تسرب في الموارد.
_rideTimer?.cancel();
_rideTimer = null;
_passengerTimer?.cancel();
_passengerTimer = null;
_waitingTimer?.cancel();
_waitingTimer = null;
Log.print("🛑 All ride timers cancelled due to passenger cancellation.");
try {
final AudioRecorderController audioRecorderController =
_getAudioController();
if (audioRecorderController.isRecording) {
audioRecorderController.stopRecording();
}
} catch (e) {
Log.print("Error stopping audio recording: $e");
}
Log.print("🚫 Ride Cancelled by Passenger via $source. Reason: $reason");
// 1. إيقاف التوجيه والتايمرات
// stopNavigation();
// stopAllTimers();
// 2. تنظيف الحالة
box.write(BoxName.rideStatus, 'Canceled'); // أو الحالة الافتراضية
box.remove(BoxName.rideArguments);
box.remove(BoxName.passengerID);
box.remove(BoxName.rideId);
// 3. عرض رسالة للسائق
if (Get.isDialogOpen == true) {
Get.back();
}
Get.defaultDialog(
title: "تم إلغاء الرحلة".tr,
titleStyle: TextStyle(color: Colors.red),
barrierDismissible: false,
content: Column(
children: [
Icon(Icons.person_off, size: 50, color: Colors.orange),
SizedBox(height: 10),
Text(
"${"Passenger cancelled the ride.".tr}\n${"Reason".tr}: $reason",
textAlign: TextAlign.center,
),
],
),
confirm: ElevatedButton(
onPressed: () {
Get.back(); // إغلاق الديالوج
Get.delete<
HomeCaptainController>(); // clear old controller to fix map generation
Get.offAll(() => HomeCaptain()); // العودة للرئيسية
},
child: Text("OK".tr),
),
);
// تشغيل صوت تنبيه للإلغاء
// AudioService.playCancelSound();
}
Future<void> cancelTripFromDriverAfterApplied() async {
if (formKeyCancel.currentState!.validate()) {
Get.dialog(
const Center(child: CircularProgressIndicator()),
barrierDismissible: false,
);
try {
// 1. استدعاء السيرفر
var response = await CRUD().post(
link: "${AppLink.ride}/rides/cancel_ride_by_driver.php",
payload: {
"ride_id": (rideId).toString(),
"driver_id": box.read(BoxName.driverID).toString(),
"reason": (cancelTripCotroller.text) ?? '',
"passenger_token": tokenPassenger.toString(),
},
);
if (response['status'] == 'success') {
// 🔥🔥 معالجة الحظر (The Penalty Logic) 🔥🔥
bool isBlocked = response['is_blocked'] ?? false;
if (isBlocked) {
String blockExpiryStr =
response['block_until']; // "2024-10-10 14:30:00"
// حفظ تاريخ فك الحظر في الذاكرة المحلية
box.write(BoxName.blockUntilDate, blockExpiryStr);
// تحويل الحالة لأوفلاين إجبارياً
box.write(BoxName.statusDriverLocation, 'blocked');
// عرض رسالة العقوبة
mySnackeBarError(
"Due to excessive cancellations (3 times), receiving orders has been suspended for 4 hours."
.tr,
);
} else {
// تحذير فقط
int count = response['cancel_count'] ?? 0;
mySnackbarWarning(
"لقد ألغيت $count رحلات اليوم. الوصول لـ 3 سيعرضك للإيقاف المؤقت."
.tr,
);
}
// تنظيف البيانات
try {
final AudioRecorderController audioRecorderController =
_getAudioController();
if (audioRecorderController.isRecording) {
await audioRecorderController.stopRecording();
}
} catch (e) {
Log.print("Error stopping audio recording: $e");
}
box.remove('cached_trip_route');
box.remove(BoxName.rideArgumentsFromBackground);
box.remove(BoxName.rideArguments);
box.remove(BoxName.passengerID);
box.remove(BoxName.rideId);
box.write(BoxName.rideStatus, 'Cancel');
// تسجيل محلي (اختياري)
try {
await sql.insertData({
'order_id': rideId,
'created_at': DateTime.now().toString(),
'driver_id': box.read(BoxName.driverID),
}, TableName.driverOrdersRefuse);
} catch (_) {}
if (Get.isRegistered<HomeCaptainController>()) {
Get.find<HomeCaptainController>().getRefusedOrderByCaptain();
} else {
// في حال لم يكن مسجل (جاي من background)
Get.put(HomeCaptainController(), permanent: true)
.getRefusedOrderByCaptain();
}
if (Get.isDialogOpen == true) {
Get.back();
}
Get.delete<
HomeCaptainController>(); // clear old controller to fix map generation
Get.offAll(
() => HomeCaptain(),
); // العودة للرئيسية ليتم تطبيق الحظر هناك
} else {
if (Get.isDialogOpen == true) {
Get.back();
}
mySnackeBarError("Failed to cancel ride".tr);
}
} catch (e) {
if (Get.isDialogOpen == true) {
Get.back();
}
Log.print("Error: $e");
}
}
}
Timer? _passengerTimer; // مرجع للتايمر لإيقافه لاحقاً
/// **بدء مؤقت الوصول للراكب (Passenger Arrival Timer)**
///
/// تقوم هذه الدالة بإدارة العد التنازلي للوقت المتوقع لوصول السائق إلى موقع الراكب.
///
/// **آلية العمل:**
/// 1. تتحقق من حالة الرحلة؛ إذا كانت قد بدأت بالفعل ('Begin')، تخفي نافذة المعلومات وتوقف العمل.
/// 2. تحسب المدة الإجمالية من [durationToPassenger].
/// 3. تستخدم [Timer.periodic] لتحديث الواجهة كل ثانية بدقة عالية.
/// 4. تقوم بحساب النسبة المئوية [progressToPassenger] وتنسيق الوقت المتبقي [stringRemainingTimeToPassenger].
/// 5. عند انتهاء الوقت، تُفعل زر بدء الرحلة [isBtnRideBegin].
void startTimerToShowPassengerInfoWindowFromDriver() {
// 1. تنظيف أي تايمر سابق لضمان عدم تداخل العدادات
_passengerTimer?.cancel();
// 2. التحقق من حالة الرحلة
String currentStatus = box.read(BoxName.rideStatus) ?? '';
if (currentStatus == 'Begin') {
Log.print('rideStatus from map: $currentStatus - Hiding Info Window');
isPassengerInfoWindow = false;
update();
return;
}
// 3. تهيئة المتغيرات
isPassengerInfoWindow = true;
final int totalDuration =
int.tryParse(durationToPassenger.toString()) ?? 60;
// استخدام DateTime لضمان دقة الوقت وعدم التأثر ببطء الجهاز
final DateTime endTime = DateTime.now().add(
Duration(seconds: totalDuration),
);
// 4. بدء التايمر الدوري
_passengerTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
// أمان: إيقاف التايمر إذا تم إغلاق الكنترولر
if (isClosed) {
timer.cancel();
return;
}
final DateTime now = DateTime.now();
final int remainingSeconds = endTime.difference(now).inSeconds;
// أ) تحديث القيم
if (remainingSeconds <= 0) {
// انتهى الوقت
remainingTimeToPassenger = 0;
progressToPassenger = 1.0;
stringRemainingTimeToPassenger = "00:00";
isBtnRideBegin = true;
timer.cancel(); // إيقاف التايمر
} else {
// ما زال العد مستمراً
remainingTimeToPassenger = remainingSeconds;
// حساب النسبة المئوية (مع منع القسمة على صفر)
progressToPassenger =
totalDuration > 0 ? 1 - (remainingSeconds / totalDuration) : 0.0;
// تنسيق الوقت (دقائق:ثواني)
final int minutes = (remainingSeconds / 60).floor();
final int seconds = remainingSeconds % 60;
stringRemainingTimeToPassenger =
'$minutes:${seconds.toString().padLeft(2, '0')}';
}
// ب) تحديث الواجهة
update();
});
}
String stringRemainingTimeToPassenger = '';
String stringRemainingTimeWaitingPassenger = '';
Timer? _waitingTimer; // متغير للتحكم في تايمر الانتظار
/// **بدء مؤقت انتظار الراكب (Waiting For Passenger Timer)**
///
/// تحسب الوقت الذي ينتظره السائق عند نقطة الانطلاق.
///
/// **التحسينات:**
/// 1. استبدال الحلقة التكرارية بـ [Timer.periodic] لأداء أفضل.
/// 2. استخدام [DateTime] لحساب الوقت المتبقي بدقة حتى لو كان التطبيق في الخلفية.
/// 3. إدارة حالات الخروج (بدء الرحلة أو انتهاء وقت الانتظار) بشكل أنظف.
void startTimerToShowDriverWaitPassengerDuration() {
// 1. تنظيف أي تايمر سابق
_waitingTimer?.cancel();
// 2. حساب المدة الكلية بالثواني
final int totalDurationSeconds = timeWaitingPassenger * 60;
// 3. تحديد وقت الانتهاء المتوقع (للدقة)
final DateTime endTime = DateTime.now().add(
Duration(seconds: totalDurationSeconds),
);
Log.print("⏳ Driver Waiting Timer Started: $totalDurationSeconds seconds");
// 4. بدء التايمر الدوري
_waitingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
// أ) التحقق من إغلاق الكنترولر
if (isClosed) {
timer.cancel();
return;
}
// ب) شرط الإيقاف الفوري: إذا بدأت الرحلة فعلياً
if (isRideBegin) {
remainingTimeInPassengerLocatioWait = 0;
stringRemainingTimeWaitingPassenger = "00:00";
timer.cancel();
update();
return;
}
// ج) حساب الوقت المتبقي بناءً على الساعة الحالية
final DateTime now = DateTime.now();
final int remainingSeconds = endTime.difference(now).inSeconds;
// د) تحديث المتغيرات
if (remainingSeconds <= 0) {
// انتهى وقت الانتظار
remainingTimeInPassengerLocatioWait = 0;
progressInPassengerLocationFromDriver = 1.0;
stringRemainingTimeWaitingPassenger = "00:00";
isdriverWaitTimeEnd = true; // تفعيل زر الإلغاء المدفوع
timer.cancel();
} else {
// ما زال الانتظار جارياً
remainingTimeInPassengerLocatioWait = remainingSeconds;
// حساب التقدم (من 0.0 إلى 1.0)
// Elapsed = Total - Remaining
final int elapsed = totalDurationSeconds - remainingSeconds;
progressInPassengerLocationFromDriver =
totalDurationSeconds > 0 ? (elapsed / totalDurationSeconds) : 1.0;
// تنسيق النص
final int minutes = (remainingSeconds / 60).floor();
final int seconds = remainingSeconds % 60;
stringRemainingTimeWaitingPassenger =
'$minutes:${seconds.toString().padLeft(2, '0')}';
}
update();
});
}
bool isSocialPressed = false;
// نغير نوع الإرجاع إلى Future<bool>
Future<bool> driverCallPassenger() async {
try {
// نضع الكود داخل try لضمان عدم توقف التطبيق عند انقطاع النت
String scam = await getDriverScam();
int scamCount = int.tryParse(scam) ?? 0;
// 1. منطق الحظر
if (scamCount > 3) {
box.write(BoxName.statusDriverLocation, 'on');
Get.find<LocationController>().stopLocationUpdates();
// إرسال تنبيه (لا ننتظره await لأنه لا يؤثر على المنع)
CRUD().post(
link: AppLink.addNotificationCaptain,
payload: {
'driverID': box.read(BoxName.driverID),
'title': 'scams operations'.tr,
'body':
'you have connect to passengers and let them cancel the order'
.tr,
},
);
// نرجع false لمنع الاتصال
return false;
}
// [Fix M-7] استخدام isNotEmpty بدلاً من != null لأن passengerId و rideId من نوع String غير قابل للـ null
// لو كانا فارغين '' يمر الشرط ويُرسل بيانات فارغة للسيرفر
if (isSocialPressed == true &&
passengerId.isNotEmpty &&
rideId.isNotEmpty) {
box.write(BoxName.statusDriverLocation, 'off');
// لا نستخدم await هنا لكي لا نؤخر فتح الهاتف
CRUD().post(
link: AppLink.addDriverScam,
payload: {
'driverID': box.read(BoxName.driverID),
'passengerID': passengerId,
'rideID': rideId,
'isDriverCallPassenger': 'true',
},
);
}
// نسمح بالاتصال
return true;
} catch (e) {
// في حال حدث خطأ في النت أو السيرفر، هل تريد السماح له بالاتصال؟
// الأفضل نعم، حتى لا تتعطل الخدمة بسبب خطأ تقني، ونسجل الخطأ في اللوج
print("Error in scam check: $e");
return true;
}
}
// دالة مساعدة لحماية التطبيق من كراش الخرائط
Future<void> safeAnimateCamera(CameraUpdate cameraUpdate) async {
if (isClosed || mapController == null) return;
try {
await mapController!.animateCamera(cameraUpdate);
} catch (e) {
Log.print("Camera error ignored");
}
}
Future<String> getDriverScam() async {
var res = await CRUD().post(
link: AppLink.getDriverScam,
payload: {'driverID': box.read(BoxName.driverID)},
);
if (res == 'failure') {
box.write(BoxName.statusDriverLocation, 'off');
return '0';
}
var d = (res);
Log.print('d: ${d}');
// 1. Check if the response status is 'failure' (API level check)
if (d['status'] == 'failure') {
// If the API status is failure, the message is a String (e.g., 'No ride scam record found')
// and there's no 'count' array to read.
return '0';
}
// 2. Safely access the List/Map structure for 'count'
// This assumes a successful response looks like:
// {'status': 'success', 'message': [{'count': '12'}]}
var messageData = d['message'];
// Check if messageData is actually a List before accessing index [0]
if (messageData is List &&
messageData.isNotEmpty &&
messageData[0] is Map) {
return messageData[0]['count'];
}
// Fallback if the successful data structure is unexpected
return '0';
// --- FIX END ---
}
void startRideFromStartApp() async {
// if (box.read(BoxName.rideStatus) == 'Begin') {
changeRideToBeginToPassenger();
isPassengerInfoWindow = false;
isRideStarted = true; // يجب أن يكون true قبل getRoute لتفعيل وضع الملاحة
isRideFinished = false;
remainingTimeInPassengerLocatioWait = 0;
timeWaitingPassenger = 0;
box.write(BoxName.statusDriverLocation, 'on');
update();
// }
// Immediately fetch high-accuracy location for navigation origin
try {
Position currentPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.bestForNavigation);
myLocation = LatLng(currentPos.latitude, currentPos.longitude);
smoothedLocation = myLocation;
smoothedHeading = currentPos.heading;
} catch (e) {
Log.print(
"Error getting current position on start ride from start app: $e");
}
// التحقق من صحة إحداثيات الهدف قبل الرسم
if (latLngPassengerDestination.latitude == 0 ||
latLngPassengerDestination.longitude == 0) {
Log.print(
"⚠️ startRideFromStartApp: destination is (0,0) — skipping getRoute");
return;
}
// ── إعادة رسم المسار إلى وجهة الراكب النهائية ────────────────
await getRoute(
origin: myLocation.latitude == 0
? Get.find<LocationController>().myLocation
: myLocation,
destination: latLngPassengerDestination,
routeColor: Colors.blue,
);
updateMarker();
// بدء الخدمات (الملاحة والعداد)
await startListeningStepNavigation();
rideIsBeginPassengerTimer();
try {
final AudioRecorderController audioRecorderController =
_getAudioController();
audioRecorderController.startRecording(rideId: rideId.toString());
} catch (e) {
Log.print("Error starting audio recording: $e");
}
}
Position? currentPosition;
/// **بدء الرحلة من طرف السائق (Start Ride)**
///
/// تقوم هذه الدالة بالتحقق من المسافة بين السائق والراكب، فإذا كانت قريبة (< 400م)،
/// ترسل طلباً للسيرفر لبدء الرحلة رسمياً.
///
/// * السيرفر سيقوم بإرسال الإشعارات (Socket + FCM) للراكب تلقائياً.
/// **بدء الرحلة (Start Ride)**
///
/// التحسينات:
/// 1. إظهار Loading لمنع تكرار الضغط.
/// 2. استخدام التحديث الموجه (Targeted Update) لمنع وميض الشاشة.
/// 3. معالجة فشل السيرفر (Revert Logic) لضمان عدم ضياع حالة الرحلة.
Future<void> startRideFromDriver() async {
// 1. إظهار مؤشر تحميل فوري (Blocking)
Get.dialog(
const Center(child: CircularProgressIndicator()),
barrierDismissible: false,
);
try {
// 2. التحقق من المسافة (Async)
double distanceToPassenger =
await calculateDistanceBetweenDriverAndPassengerLocation();
// إغلاق مؤشر التحميل لأننا حصلنا على النتيجة
if (Get.isDialogOpen == true) {
Get.back();
}
if (distanceToPassenger < 150) {
// زدت المسافة قليلاً لمرونة أكبر (150م)
// --- أ) تحديث الحالة المحلية (Optimistic Update) ---
changeRideToBeginToPassenger(); // تغيير المتغيرات الداخلية
isPassengerInfoWindow = false;
isRideStarted = true;
isRideFinished = false;
remainingTimeInPassengerLocatioWait = 0;
timeWaitingPassenger = 0;
// الحفظ في التخزين المحلي
box.write(BoxName.statusDriverLocation, 'on');
box.write(BoxName.rideStatus, 'Begin');
// Immediately fetch high-accuracy location for navigation origin
try {
Position currentPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.bestForNavigation);
myLocation = LatLng(currentPos.latitude, currentPos.longitude);
smoothedLocation = myLocation;
smoothedHeading = currentPos.heading;
} catch (e) {
Log.print("Error getting current position on start ride: $e");
}
// 🔥 [Fix Polyline] مسح المسار الأصفر (للراكب) فوراً قبل رسم الأزرق (للوجهة)
// بدون هذا قد يبقى الخط الأصفر ظاهراً إذا تأخر getRoute() أو أُعيد بناء الخريطة
clearPolyline();
// ── إعادة رسم المسار إلى وجهة الراكب النهائية ────────────────
String? cachedRoute = box.read('cached_trip_route');
await getRoute(
origin: myLocation.latitude == 0
? Get.find<LocationController>().myLocation
: myLocation,
destination: latLngPassengerDestination,
routeColor: Colors
.black, // Color for the actual trip (black as user prefers solid)
cachedResponse: cachedRoute,
);
updateMarker();
// بدء الخدمات (الملاحة والعداد)
await startListeningStepNavigation();
rideIsBeginPassengerTimer();
try {
final AudioRecorderController audioRecorderController =
_getAudioController();
audioRecorderController.startRecording(rideId: rideId.toString());
} catch (e) {
Log.print("Error starting audio recording: $e");
}
// --- ب) تحديث الواجهة (Targeted Updates Only) ---
// نحدث فقط الأجزاء التي تتغير، بدلاً من إعادة رسم الخريطة كاملة
// update(['PassengerInfo']); // لإخفاء نافذة معلومات الراكب
// update(['DriverEndBar']); // لإظهار شريط إنهاء الرحلة
// update(['SosConnect']); // لتفعيل زر الطوارئ (تأكد من وضع ID للودجت)
update();
// --- ج) إرسال الطلب للسيرفر (Background) ---
// لا ننتظر النتيجة لتعطيل الواجهة، لكن نعالج الخطأ إن حدث
CRUD().post(
link: "${AppLink.server}/ride/rides/start_ride.php",
payload: {
'id': rideId.toString(),
'driver_id': box.read(BoxName.driverID).toString(),
'status': 'Begin',
"passengerToken": tokenPassenger.toString(),
},
).then((response) {
if (response['status'] == 'failure') {
Log.print("Server failed to start ride!");
isRideStarted = false;
isRideBegin = false;
box.write(BoxName.rideStatus, 'Apply');
isPassengerInfoWindow = true;
stopListeningStepNavigation();
mySnackeBarError("Failed to start ride. Please try again.");
update();
}
}).catchError((error) {
Log.print("Network/Server error starting ride: $error");
isRideStarted = false;
isRideBegin = false;
box.write(BoxName.rideStatus, 'Apply');
isPassengerInfoWindow = true;
stopListeningStepNavigation();
mySnackeBarError("Network error. Failed to start ride.");
update();
});
} else {
// --- حالة الرفض (بعيد جداً) ---
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
title: Text('You are far from passenger location'.tr),
content: Text(
'Please go closer to the passenger location (less than 150m)'.tr,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'.tr),
),
],
),
);
}
} catch (e) {
// تنظيف اللودينج في حال حدوث خطأ غير متوقع
if (Get.isDialogOpen == true) {
Get.back();
}
Log.print("Error starting ride: $e");
mySnackeBarError("Could not start ride. Please check internet.".tr);
}
}
calculateDistanceInMeter(LatLng prev, LatLng current) async {
double distance2 = Geolocator.distanceBetween(
prev.latitude,
prev.longitude,
current.latitude,
current.longitude,
);
return distance2;
}
double speedoMeter = 0;
Timer? _updateLocationTimer;
/// [Fix C-1] استبدال for loop الحلقة التكرارية بـ Timer.periodic
/// لمنع تسرب الذاكرة وufeni الـ Stack Overflow من الاستدعاء الذاتي المتكرر.
void startUpdateLocationTimer() {
_updateLocationTimer?.cancel();
_updateLocationTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (isClosed || !isRideBegin) {
timer.cancel();
return;
}
try {
safeAnimateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
bearing: Get.find<LocationController>().heading,
target: myLocation,
zoom: 17,
),
),
);
update();
} catch (error) {
debugPrint('Error listening to GPS: $error');
}
});
}
void stopUpdateLocationTimer() {
_updateLocationTimer?.cancel();
_updateLocationTimer = null;
}
Future<double> calculateDistanceBetweenDriverAndPassengerLocation() async {
final locationController = Get.isRegistered<LocationController>()
? Get.find<LocationController>()
: Get.put(LocationController());
try {
var res = await CRUD().get(
link: AppLink.getLatestLocationPassenger,
payload: {'rideId': (rideId)},
).timeout(const Duration(seconds: 5));
if (res != 'failure') {
var passengerLatestLocationString = jsonDecode(res)['message'];
double distance2 = Geolocator.distanceBetween(
double.parse(passengerLatestLocationString[0]['lat'].toString()),
double.parse(passengerLatestLocationString[0]['lng'].toString()),
locationController.myLocation.latitude,
locationController.myLocation.longitude,
);
return distance2;
}
} catch (_) {}
// Fallback to local coordinates in case of error/timeout
double distance2 = Geolocator.distanceBetween(
latLngPassengerLocation.latitude,
latLngPassengerLocation.longitude,
locationController.myLocation.latitude,
locationController.myLocation.longitude,
);
return distance2;
}
// [Fix C-3] دالة مساعدة مشتركة لتحليل المسافة النصية إلى متر
// تُستخدم في كلا الدالتين: finishRideFromDriver و _validateTripDistance
double _parseDistanceToMeters() {
String cleanDistance = distance.toString().replaceAll(
RegExp(r'[^0-9.]'),
'',
);
if (cleanDistance.isEmpty) cleanDistance = "0.0";
double numericDistance = double.parse(cleanDistance);
bool isMeters = distance.toString().contains('m') &&
!distance.toString().contains('km');
if (!isMeters && numericDistance > 100) isMeters = true;
return isMeters ? numericDistance : numericDistance * 1000;
}
/// [Fix M-6] دالة مساعدة لحساب التكلفة
/// ⚠️ ملاحظة: distanceBetweenDriverAndPassengerWhenConfirm بالكيلومتر
double _calculateWaitingCost() {
bool isEgypt = box.read(BoxName.countryCode) == 'Egypt';
double waitingMinutes = 5.0;
if (isEgypt) {
// معادلة مصر: (المسافة كم * 0.08) + (5 دقائق * 1)
return (distanceBetweenDriverAndPassengerWhenConfirm * 0.08) +
(waitingMinutes * 1.0);
} else {
// معادلة الأردن/أخرى: (المسافة كم * 11) + (5 دقائق * 0.06)
return (distanceBetweenDriverAndPassengerWhenConfirm * 11) +
(waitingMinutes * 0.06);
}
}
Future<void> addWaitingTimeCostFromPassengerToDriverWallet() async {
// ... (فحص المسافة واللودينج كما هو) ...
Get.dialog(
const Center(child: CircularProgressIndicator()),
barrierDismissible: false,
);
try {
// 1. حساب التكلفة
double costOfWaiting = _calculateWaitingCost();
double costForPassenger = costOfWaiting * -1; // بالسالب
// 2. تحديث البيانات التشغيلية (Main Server)
// هذا الطلب لا يحتاج توكنات مالية، فقط تحديث حالة
await CRUD().post(
link: "${AppLink.ride}/rides/update_ride_cancel_wait.php",
payload: {
'ride_id': rideId.toString(),
'driver_id': box.read(BoxName.driverID).toString(),
},
);
// 3. توليد التوكنات (Server-Side Logic Security)
// نحتاج توكن للسائق وتوكن للراكب
final tokens = await Future.wait([
generateTokenDriver(costOfWaiting.toString()),
generateTokenPassenger(costForPassenger.toString()),
]);
// 4. تنفيذ العملية المالية الموحدة (Payment Server)
var paymentResponse = await CRUD().postWallet(
link:
"${AppLink.paymentServer}/ride/passengerWallet/process_wait_compensation.php", // الرابط الجديد
payload: {
'ride_id': rideId.toString(),
'driver_id': box.read(BoxName.driverID).toString(),
'passenger_id': passengerId.toString(),
'amount': costOfWaiting.toString(), // المبلغ الموجب
'amount_passenger': costForPassenger.toString(), // المبلغ السالب
'token_driver': tokens[0],
'token_passenger': tokens[1],
},
);
if (paymentResponse['status'] == 'success') {
// النجاح
if (Get.isDialogOpen == true) Get.back();
mySnackbarSuccess(
'${'You gained'.tr} ${costOfWaiting.toStringAsFixed(2)} ${'in your wallet'.tr}',
);
box.write(BoxName.statusDriverLocation, 'off');
Get.delete<
HomeCaptainController>(); // clear old controller to fix map generation
Get.offAll(() => HomeCaptain());
} else {
throw Exception("Payment Transaction Failed");
}
} catch (e) {
if (Get.isDialogOpen == true) Get.back();
Log.print("Error: $e");
mySnackeBarError("Transaction failed, please try again.".tr);
}
}
/// **التحقق من إنهاء الرحلة (Validate & Finish Ride)**
///
/// تقوم هذه الدالة بحماية الرحلة من الإنهاء المبكر الخاطئ.
///
/// **آلية العمل:**
/// 1. تحسب المسافة الخطية (Displacement) التي قطعها السائق بعيداً عن نقطة الانطلاق.
/// 2. تقارن هذه المسافة مع "عتبة دنيا" (Minimum Threshold) وهي (1/5) من إجمالي مسافة الرحلة.
/// 3. **إذا تحقق الشرط:** تظهر نافذة تأكيد الإنهاء وتستدعي [finishRideFromDriver1].
/// 4. **إذا فشل الشرط:** تمنع الإنهاء وتصدر تنبيهاً صوتياً ونصياً بأن المسافة المقطوعة غير كافية.
/// **التحقق من إنهاء الرحلة (Validate & Finish Ride)**
///
/// [isFromSlider]: إذا كانت القيمة true، فهذا يعني أن السائق سحب الشريط
/// وبالتالي هو موافق ضمنياً، فلا داعي لعرض ديالوج "هل أنت متأكد؟"
Future<void> finishRideFromDriver({bool isFromSlider = false}) async {
// [Fix C-3] استخدام الدالة المساعدة المشتركة لتحليل المسافة
final double totalTripDistanceMeters = _parseDistanceToMeters();
// 2. حساب المسافة المقطوعة
final double displacementMeters = Geolocator.distanceBetween(
latLngPassengerLocation.latitude,
latLngPassengerLocation.longitude,
Get.find<LocationController>().myLocation.latitude,
Get.find<LocationController>().myLocation.longitude,
);
// 3. تحديد الحد الأدنى (الخمس)
final double minimumDistanceThreshold = totalTripDistanceMeters / 5;
Log.print('📏 Total Distance (m): $totalTripDistanceMeters');
Log.print('🚗 Moved Displacement (m): $displacementMeters');
// 4. اتخاذ القرار
if (displacementMeters > minimumDistanceThreshold) {
// ✅ الحالة سليمة
if (isFromSlider) {
// إذا جاء من السلايدر، نفذ فوراً
finishRideFromDriver1();
} else {
// [Fix C-3 v2] بعد التأكيد، نمرّر isFromSlider=true لتجنب الديالوج المكرر
MyDialog().getDialog('Are you sure to exit ride?'.tr, '', () {
Get.back();
finishRideFromDriver1(isFromSlider: true);
});
}
} else {
// ❌ الحالة مرفوضة: المسافة غير كافية
final textToSpeechController = Get.put(TextToSpeechController());
// إظهار رسالة خطأ حتى لو سحب السلايدر
if (Get.isDialogOpen == true) Get.back(); // إغلاق أي ديالوج سابق
MyDialog().getDialog(
"You haven't moved sufficiently!".tr,
"Please complete more distance before ending.".tr,
() => Get.back(),
);
await textToSpeechController.speakText(
"You haven't moved sufficiently!".tr,
);
}
}
String paymentToken = '';
Future<String> generateTokenDriver(String amount) async {
var res = await CRUD().postWallet(
link: AppLink.addPaymentTokenDriver,
payload: {
'driverID': box.read(BoxName.driverID).toString(),
'amount': amount.toString(),
},
);
var d = (res);
return d['message'];
}
String paymentTokenPassenger = '';
Future<String> generateTokenPassenger(String amount) async {
var res = await CRUD().postWallet(
link: AppLink.addPaymentTokenPassenger,
payload: {'passengerId': passengerId, 'amount': amount.toString()},
);
var d = (res);
return d['message'];
}
// ... other controller code ...
/// **إنهاء الرحلة ومعالجة المدفوعات (Finish Ride & Process Payments)**
///
/// تقوم هذه الدالة بإدارة عملية إنهاء الرحلة كاملة من جانب السائق.
///
/// **آلية العمل:**
/// 1. **تحديث الواجهة:** تخفي نافذة السعر وتعرض مؤشر تحميل لمنع التكرار.
/// 2. **حساب التكلفة:** تحسب السعر النهائي بناءً على نوع السيارة والمسافة، مع مراعاة الحد الأدنى للأجور.
/// 3. **تجهيز البيانات:** تحضر البيانات اللازمة لتحديث حالة الرحلة (`rides`) وتسجيل الدفع (`payments`).
/// 4. **التنفيذ المتوازي (Parallel Execution):** تستخدم `Future.wait` لإرسال طلب التحديث وطلب الدفع في آن واحد للسيرفر لتقليل وقت الانتظار.
/// 5. **التحقق (Validation):** تتأكد من نجاح كلا العمليتين (التحديث والدفع) قبل الانتقال.
/// 6. **الختام:** في حال النجاح، تحذف البيانات المؤقتة وتوجه السائق لصفحة التقييم. في حال الفشل، تعيد حالة الواجهة ليتمكن السائق من المحاولة مجدداً.
/// **إنهاء الرحلة (Finish Ride)**
///
/// التحسينات:
/// 1. التحقق من المسافة المقطوعة (Anti-Fraud).
/// 2. استخدام Future.wait لتنفيذ الطلبات بالتوازي (سرعة مضاعفة).
/// 3. استخدام `finally` لضمان إغلاق اللودينج دائماً.
Future<void> finishRideFromDriver1({bool isFromSlider = false}) async {
// 1. التحقق الأمني: هل تحرك السائق فعلاً؟
// (هذا الكود يجب أن يكون سريعاً ولا يعطل الواجهة)
if (!await _validateTripDistance(isFromSlider)) return;
// 2. إظهار لودينج (Blocking) لمنع التكرار
Get.dialog(
const Center(child: CircularProgressIndicator()),
barrierDismissible: false,
);
try {
try {
final AudioRecorderController audioRecorderController =
_getAudioController();
if (audioRecorderController.isRecording) {
await audioRecorderController.stopRecording();
}
} catch (e) {
Log.print("Error stopping audio recording: $e");
}
// تحديث الحالة المحلية لمنع أي تفاعلات أخرى أثناء الطلب
isRideFinished = true;
isRideStarted = false;
isPriceWindow = false;
box.write(BoxName.rideStatus, 'Finished');
box.write(BoxName.statusDriverLocation, 'off');
box.remove(BoxName.passengerID);
box.remove(BoxName.rideId);
// تجهيز البيانات الخام الموحدة للسيرفر ليقوم بمعالجة الدفع والإنهاء معاً بنظام المعاملة الواحدة
final finishPayload = {
'rideId': rideId.toString(),
'driver_id': box.read(BoxName.driverID).toString(),
'passengerId': passengerId.toString(),
'status': 'Finished',
'actualDistance': distance.toString(),
'actualDuration': duration.toString(),
'walletChecked': walletChecked.toString(),
'passengerWalletBurc': passengerWalletBurc.toString(),
'passengerToken': tokenPassenger.toString(),
'driver_token': box.read(BoxName.tokenDriver).toString(),
};
// إرسال طلب واحد موحد للسيرفر الرئيسي
final response = await CRUD().post(
link: "${AppLink.ride}/rides/finish_ride_updates.php",
payload: finishPayload,
);
if (response['status'] == 'success') {
// تنظيف البيانات محلياً عند النجاح الكامل
box.remove(BoxName.rideArguments);
box.remove(BoxName.rideArgumentsFromBackground);
box.remove('cached_trip_route');
// إغلاق اللودينج
if (Get.isDialogOpen == true) Get.back();
// إرسال تقرير السلوك (Fire and forget)
Get.put(
DriverBehaviorController(),
).sendSummaryToServer(driverId, rideId);
// الانتقال لصفحة التقييم بالسعر الذي حدده وحسبه السيرفر بأمان
Get.off(
() => RatePassenger(),
arguments: {
'passengerId': passengerId,
'rideId': rideId,
'price': response['price']?.toString() ?? paymentAmount.toString(),
'walletChecked': walletChecked.toString() ?? 'false',
},
);
} else {
throw Exception(response['error'] ?? "Unknown backend error");
}
} catch (e) {
if (Get.isDialogOpen == true) Get.back(); // إغلاق اللودينج
Log.print("Error finishing ride: $e");
mySnackeBarError("Failed to finish ride: $e");
// إعادة الحالة محلياً للسماح للمستخدم بالمحاولة مرة أخرى بأمان
isRideFinished = false;
isRideStarted = true;
box.write(BoxName.rideStatus, 'Begin');
update(); // تحديث الشريط ليعود للوضع النشط
}
}
// --- دوال مساعدة (Helpers) لتنظيف الكود ---
Future<bool> _validateTripDistance(bool isFromSlider) async {
// [Fix C-3] استخدام الدالة المساعدة المشتركة لتحليل المسافة
final double totalTripDistanceMeters = _parseDistanceToMeters();
final double displacementMeters = Geolocator.distanceBetween(
latLngPassengerLocation.latitude,
latLngPassengerLocation.longitude,
Get.find<LocationController>().myLocation.latitude,
Get.find<LocationController>().myLocation.longitude,
);
final double minimumThreshold = totalTripDistanceMeters / 5;
if (displacementMeters > minimumThreshold || isFromSlider) {
if (isFromSlider) return true;
// [Fix C-2 v2] استخدام AlertDialog مباشرة مع حماية Deadlock
// إذا أغلق المستخدم الديالوج بالزر الخلفي، نُكمل بـ false
final completer = Completer<bool>();
Get.dialog(
AlertDialog(
title: Text('Exit Ride?'.tr),
actions: [
TextButton(
onPressed: () {
if (!completer.isCompleted) completer.complete(true);
Get.back();
},
child: Text('OK'.tr),
),
],
),
barrierDismissible: true,
).then((_) {
if (!completer.isCompleted) completer.complete(false);
});
return await completer.future;
} else {
Get.find<TextToSpeechController>().speakText(
"You haven't moved sufficiently!".tr,
);
MyDialog().getDialog(
"Warning".tr,
"You haven't moved sufficiently!".tr,
() => Get.back(),
);
return false;
}
}
void cancelCheckRideFromPassenger() async {
try {
var res = await CRUD().get(
link: "${AppLink.endPoint}/ride/driver_order/getOrderCancelStatus.php",
payload: {'order_id': (rideId)},
);
if (res == 'failure' || res.isEmpty) return;
var response = jsonDecode(res);
if (response == null || response['data'] == null) return;
canelString = response['data']['status']?.toString() ?? 'yet';
update();
if (canelString == 'Cancel') {
remainingTimeTimerRideBegin = 0;
remainingTimeToShowPassengerInfoWindowFromDriver = 0;
remainingTimeToPassenger = 0;
isRideStarted = false;
isRideFinished = false;
isPassengerInfoWindow = false;
clearPolyline();
update();
box.remove('cached_trip_route');
MyDialog().getDialog(
'Order Cancelled'.tr,
'Order Cancelled by Passenger'.tr,
() {
Get.delete<
HomeCaptainController>(); // clear old controller to fix map generation
Get.offAll(HomeCaptain());
},
);
}
} catch (e) {
Log.print("Error checking ride cancel status: $e");
}
}
int rideTimerFromBegin = 0;
double price = 0;
DateTime currentTime = DateTime.now();
/// أثناء الرحلة: نعرض السعر لحظياً بدون مضاعفة العمولة في كل ثانية.
/// - نستخدم سعر الدقيقة حسب الوقت، مع قواعد الرحلات البعيدة:
/// >25كم أو >35كم => دقيقة = 600، سقف 60 دقيقة، ومع >35كم عفو 10 دقائق.
/// - سرعة طويلة: لو المسافة المخططة > 40كم نستخدم 2600 ل.س/كم للـ Speed،
//// ونطبق نفس نسبة التخفيض على Comfort/Electric/Van.
/// - نضيف فقط "الزيادة" فوق التسعيرة المقتبسة (وقت زائد + كم زائد).
/// - نعكس العمولة kazán مرة واحدة على الزيادة (وليس كل ثانية).
// =========================================================
// الدالة الرئيسية المعدلة (العداد)
// =========================================================
// متغيرات العداد الجديد
Timer? _rideTimer;
DateTime? _rideStartTime;
DateTime? get rideStartTime => _rideStartTime; // إضافة هذا السطر
double currentRideDistanceKm = 0.0; // لحساب المسافة
// متغيرات المراقبة
// ===========================================================================
// 4. منطق العداد والتسعير (The Engine)
// ===========================================================================
void rideIsBeginPassengerTimer() {
_rideTimer?.cancel();
_rideStartTime = DateTime.now();
currentRideDistanceKm = 0.0;
// جلب الاعتماديات
final loc = Get.find<LocationController>();
final hc = Get.find<HomeCaptainController>();
// إعداد متغيرات التسعير
final double perKmSpeedBase = hc.speedPrice;
final double perKmComfortRaw = hc.comfortPrice;
final double perKmDelivery = hc.deliveryPrice;
final double perKmVanRaw = hc.familyPrice;
const double electricUpliftKm = 400;
final double perKmElectricRaw = perKmComfortRaw + electricUpliftKm;
final double perMinNature = hc.naturePrice;
final double perMinLate = hc.latePrice;
final double perMinHeavy = hc.heavyPrice;
// القيم الأولية
final double basePassengerQuote = safeParseDouble(totalCost);
final int quotedMinutes =
safeParseInt(duration) != 0 ? safeParseInt(duration) : 20;
final double kazanPct = safeParseDouble(kazan) / 100.0;
double plannedKm = safeParseDouble(distance);
final double startKm = loc.totalDistance / 1000;
if (plannedKm <= 0) plannedKm = (startKm > 0) ? startKm : 0.0;
bool isAirport(String s) =>
s.toLowerCase().contains('airport') || s.contains('مطار');
final bool isAirportContext =
isAirport(startNameLocation ?? '') || isAirport(endNameLocation ?? '');
double lastKmForNoise = loc.totalDistance / 1000;
const double jitterKm = 0.01; // 10 متر = 0.01 كم [Fix M-1]
_rideTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (box.read(BoxName.rideStatus) != 'Begin') {
timer.cancel();
return;
}
try {
final now = DateTime.now();
final int elapsedSeconds = now.difference(_rideStartTime!).inSeconds;
// أ) حساب المسافة المقطوعة (Logic)
double currentTotalKm = loc.totalDistance / 1000;
double delta = currentTotalKm - lastKmForNoise;
if (delta.abs() > jitterKm) {
currentRideDistanceKm += delta;
lastKmForNoise = currentTotalKm;
}
// ب) حساب السعر (Brain)
price = _calculateCurrentPrice(
now: now,
elapsedSeconds: elapsedSeconds,
liveKm: currentRideDistanceKm,
plannedKm: plannedKm,
startKm: startKm,
quotedMinutes: quotedMinutes,
basePassengerQuote: basePassengerQuote,
kazanPct: kazanPct,
isAirportContext: isAirportContext,
perKmSpeedBase: perKmSpeedBase,
perKmComfortRaw: perKmComfortRaw,
perKmDelivery: perKmDelivery,
perKmVanRaw: perKmVanRaw,
perKmElectricRaw: perKmElectricRaw,
perMinNature: perMinNature,
perMinLate: perMinLate,
perMinHeavy: perMinHeavy,
);
// ج) تحديث واجهة المستخدم
stringRemainingTimeRideBegin =
"${currentRideDistanceKm.toStringAsFixed(2)} km";
speed = loc.speed * 3.6;
update(); // تحديث الشريط فقط
} catch (e) {
print("Timer Error: $e");
}
});
}
double _calculateCurrentPrice({
required DateTime now,
required int elapsedSeconds,
required double liveKm,
required double plannedKm,
required double startKm,
required int quotedMinutes,
required double basePassengerQuote,
required double kazanPct,
required bool isAirportContext,
required double perKmSpeedBase,
required double perKmComfortRaw,
required double perKmDelivery,
required double perKmVanRaw,
required double perKmElectricRaw,
required double perMinNature,
required double perMinLate,
required double perMinHeavy,
}) {
// 🛑 1. قاعدة السعر الثابت: تجميد السعر
if (carType == 'Speed' ||
carType == 'Fixed Price' ||
carType == 'Awfar Car') {
return basePassengerQuote;
}
// 🟢 2. المنطق المتغير
const double longTripPerMin = 600.0;
const double longDistThresholdKm = 35.0;
// نسبة التخفيض
double reductionPct = 0.0;
if (liveKm > 40.0 && perKmComfortRaw > 0) {
double r40 = (1.0 - (2600.0 / (perKmComfortRaw * 1.2))).clamp(0.0, 0.35);
reductionPct = r40;
if (liveKm > 100.0) {
reductionPct = (r40 + 0.07).clamp(0.0, 0.35);
}
}
// سعر الكيلومتر
double finalPerKmRate;
switch (carType) {
case 'Comfort':
case 'Mishwar Vip':
case 'Lady':
finalPerKmRate = perKmComfortRaw * (1.0 - reductionPct);
break;
case 'Electric':
finalPerKmRate = perKmElectricRaw * (1.0 - reductionPct);
break;
case 'Van':
finalPerKmRate = perKmVanRaw * (1.0 - reductionPct);
break;
case 'Delivery':
finalPerKmRate = perKmDelivery;
break;
default:
finalPerKmRate = perKmComfortRaw * (1.0 - reductionPct);
}
// سعر الدقيقة
double perMinRate;
if (liveKm > longDistThresholdKm) {
perMinRate = longTripPerMin;
} else {
final h = now.hour;
if (isAirportContext || h >= 21 || h < 1) {
perMinRate = perMinLate;
} else if (h >= 14 && h <= 17) {
perMinRate = perMinHeavy;
} else {
perMinRate = perMinNature;
}
}
// الفروقات
final int elapsedMinutes = (elapsedSeconds ~/ 60);
int extraMinutes = elapsedMinutes - quotedMinutes;
if (extraMinutes < 0) extraMinutes = 0;
double liveCostCalculation =
(liveKm * finalPerKmRate) + (elapsedMinutes * perMinRate);
double totalLivePrice = liveCostCalculation * (1.0 + kazanPct);
// القرار النهائي (الأعلى بين المتفق عليه والمحسوب)
return (totalLivePrice > basePassengerQuote)
? totalLivePrice
: basePassengerQuote;
}
double recentDistanceToDash = 0;
double recentAngelToMarker = 0;
double speed = 0;
void updateMarker() {
// 🔥 Car icon as a Map Marker — moves with GPS location on the map.
// MarkerId 'MyLocation' must match exactly with google_driver_map_page.dart.
markers.removeWhere((m) => m.markerId.value == 'MyLocation');
final locCtrl = Get.find<LocationController>();
myLocation = locCtrl.myLocation;
markers.add(
Marker(
markerId: const MarkerId('MyLocation'),
position: myLocation,
icon: carIcon,
rotation: locCtrl.heading,
anchor: const Offset(0.5, 0.5),
flat: true,
zIndex: 100,
),
);
update();
}
void addCustomCarIcon() {
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
update();
}
void addCustomStartIcon() async {
startIcon = InlqBitmap.fromAsset('assets/images/A.png');
update();
}
void addCustomEndIcon() {
endIcon = InlqBitmap.fromAsset('assets/images/b.png');
update();
}
void addCustomPassengerIcon() {
passengerIcon = InlqBitmap.fromAsset('assets/images/picker.png');
update();
}
var activeRouteSteps = <Map<String, dynamic>>[];
var traveledPathPoints = <LatLng>[]; // المسار المقطوع (رمادي)
var upcomingPathPoints = <LatLng>[]; // المسار المتبقي (أزرق/أحمر)
// --- متغيرات الأيقونات والمواقع ---
var heading = 0.0;
// ... يمكنك إضافة أيقونات البداية والنهاية هنا
// --- متغيرات الأداء ---
var updateInterval = 5.obs; // القيمة الافتراضية
// --- متغيرات داخلية للملاحة ---
var _stepBounds = <LatLngBounds>[];
var _stepEndPoints = <LatLng>[];
var _allPointsForActiveRoute = <LatLng>[];
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;
// Check if the point is outside the vertical bounds of the segment
if ((py < v1y && py < v2y) || (py > v1y && py > v2y)) {
return false;
}
// Calculate the intersection of the ray and the segment
double intersectX = v1x + (py - v1y) * (v2x - v1x) / (v2y - v1y);
// Check if the intersection is to the right of the point
return intersectX > px;
}
// Function to check if the point is inside the polygon
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]; // Loop back to the start
if (_rayIntersectsSegment(point, vertex1, vertex2)) {
intersections++;
}
}
// If the number of intersections is odd, the point is inside
return intersections % 2 != 0;
}
String getLocationArea(double latitude, double longitude) {
LatLng passengerPoint = LatLng(latitude, longitude);
// 1. فحص الأردن
if (isPointInPolygon(passengerPoint, CountryPolygons.jordanBoundary)) {
box.write(BoxName.countryCode, 'Jordan');
update(); // [Fix N-5]
// box.write(BoxName.serverChosen,
// AppLink.SiroSyriaServer); // مثال: اختر سيرفر سوريا للبيانات
return 'Jordan';
}
// 2. فحص سوريا
if (isPointInPolygon(passengerPoint, CountryPolygons.syriaBoundary)) {
box.write(BoxName.countryCode, 'Syria');
update(); // [Fix N-5]
return 'Syria';
}
// 3. فحص مصر
if (isPointInPolygon(passengerPoint, CountryPolygons.egyptBoundary)) {
box.write(BoxName.countryCode, 'Egypt');
update(); // [Fix N-5]
return 'Egypt';
}
// 4. الافتراضي (إذا كان خارج المناطق المخدومة)
box.write(BoxName.countryCode, 'Jordan');
update(); // [Fix N-5]
return 'Unknown Location (Defaulting to Jordan)';
}
/// **جلب ورسم المسار (OSRM - New Standard System)**
///
/// تستخدم السيرفر الجديد: https://routesy.intaleq.xyz/route/v1/driving
Future<void> getRoute({
required LatLng origin,
required LatLng destination,
required Color routeColor,
String? cachedResponse,
}) async {
if (mapController == null) return;
try {
dynamic response;
if (cachedResponse != null && cachedResponse.isNotEmpty) {
response = jsonDecode(cachedResponse);
Log.print("✅ Using cached route response to save API calls");
} else {
// 1. طلب المسار من السيرفر الموحد (SaaS) لضمان الدقة وتفادي الـ 401
final saasUrl = Uri.parse(AppLink.mapSaasRoute).replace(
queryParameters: {
'fromLat': origin.latitude.toString(),
'fromLng': origin.longitude.toString(),
'toLat': destination.latitude.toString(),
'toLng': destination.longitude.toString(),
'steps': 'true', // نحتاجها للملاحة والتوجيه
'alternatives': 'false',
'locale': 'ar',
},
);
final httpResponse = await http.get(
saasUrl,
headers: {
'x-api-key': Env.mapSaasKey,
'Content-Type': 'application/json',
},
);
if (httpResponse.statusCode != 200) {
throw Exception("Routing request failed: ${httpResponse.statusCode}");
}
response = jsonDecode(httpResponse.body);
}
// 2. التعامل مع الـ JSON المباشر (الذي أرسله المستخدم)
// إذا كان الـ response يحتوي على الحقول مباشرة في الجذر
final String? encodedPoints = response['points'];
if (encodedPoints == null) {
mySnackeBarError("No route points found".tr);
return;
}
// 🔥 فك التشفير باستخدام compute لضمان أداء ممتاز
List<LatLng> fullRoute = await compute(
PolylineUtils.decode,
encodedPoints,
);
if (fullRoute.isEmpty) {
mySnackeBarError("Failed to process route points".tr);
return;
}
// تحديث المسافة والوقت من الـ JSON الجديد
distance = (response['distance'] ?? 0).toString();
duration = (response['duration'] ?? 0).toString();
// ب) تهيئة المتغيرات
upcomingPathPoints.assignAll(fullRoute);
traveledPathPoints.clear();
_lastTraveledIndex = 0;
// ج) رسم المسار الأولي
polyLines.clear();
polyLines.add(
Polyline(
polylineId: const PolylineId("upcoming_route"),
points: fullRoute,
width: 8,
color: routeColor,
),
);
_updatePassengerWalkLine();
// د) معالجة الخطوات (Instructions) للسيرفر الموحد
final List<dynamic> instructions = response['instructions'] ?? [];
if (instructions.isNotEmpty) {
routeSteps = List<Map<String, dynamic>>.from(
instructions.map((e) {
int endIdx = (e['interval'] as List)[1];
// التأكد من أن الـ index لا يتجاوز طول المسار
if (endIdx >= fullRoute.length) endIdx = fullRoute.length - 1;
return {
'html_instructions': e['text'] ?? "",
'sign': e['sign'] ?? 0,
'end_location': {
'lat': fullRoute[endIdx].latitude,
'lng': fullRoute[endIdx].longitude,
},
};
}),
);
currentStepIndex = 0;
currentInstruction = routeSteps[0]['html_instructions'];
currentManeuverModifier = routeSteps[0]['sign'];
_nextInstructionSpoken = false;
if (Get.isRegistered<TextToSpeechController>() && isTtsEnabled) {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
} else {
routeSteps = [];
currentInstruction = "";
currentManeuverModifier = 0;
}
// هـ) تحريك الكاميرا لتشمل المسار أو الدخول في وضع الملاحة
if (isRideStarted) {
final locCtrl = Get.find<LocationController>();
safeAnimateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: origin,
zoom: 17.5,
bearing: locCtrl.heading,
tilt: 60,
),
),
);
} else if (fullRoute.isNotEmpty) {
final bounds = _boundsFromLatLngList(fullRoute);
final double distOriginDest = Geolocator.distanceBetween(
origin.latitude,
origin.longitude,
destination.latitude,
destination.longitude,
);
// When driver & passenger are on the same device, the
// C++ map engine crashes with std::domain_error because
// bounds have near-zero span. Show a dialog to inform
// the driver, then use a padded safe zoom instead.
// 🔥 [Fix Dialog] لا تُظهر التحذير إذا كانت الرحلة قد بدأت (isRideStarted)
// لأن عند بدء الرحلة، origin=موقع السائق و destination=وجهة الراكب
// وقد تكون بعيدة جداً — التحذير لا معنى له هنا
if (distOriginDest < 10 && !isRideStarted) {
_showSameDeviceWarning();
safeAnimateCamera(
CameraUpdate.newLatLngZoom(
LatLng(
(origin.latitude + destination.latitude) / 2,
(origin.longitude + destination.longitude) / 2,
),
17,
),
);
} else if (distOriginDest < 10 && isRideStarted) {
// نفس الجهاز لكن الرحلة بدأت — فقط zoom بدون تحذير
safeAnimateCamera(
CameraUpdate.newLatLngZoom(
LatLng(
(origin.latitude + destination.latitude) / 2,
(origin.longitude + destination.longitude) / 2,
),
15,
),
);
} else {
safeAnimateCamera(
CameraUpdate.newLatLngBounds(
bounds,
left: 80,
top: 80,
right: 80,
bottom: 80,
),
);
}
}
update();
} catch (e) {
Log.print("Route Error: $e");
}
}
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
assert(list.isNotEmpty);
double? x0, x1, y0, y1;
for (LatLng latLng in list) {
if (x0 == null) {
x0 = x1 = latLng.latitude;
y0 = y1 = latLng.longitude;
} else {
if (latLng.latitude > x1!) x1 = latLng.latitude;
if (latLng.latitude < x0) x0 = latLng.latitude;
if (latLng.longitude > y1!) y1 = latLng.longitude;
if (latLng.longitude < y0!) y0 = latLng.longitude;
}
}
// Guard against zero-span bounds which crash the native C++ map engine
// with std::domain_error when passed to CameraUpdate.newLatLngBounds.
double latSpan = (x1! - x0!).abs();
double lngSpan = (y1! - y0!).abs();
const double minSpan = 0.002; // ~220 m at equator
if (latSpan < minSpan) {
final double pad = (minSpan - latSpan) / 2;
x0 = x0! - pad;
x1 = x1! + pad;
}
if (lngSpan < minSpan) {
final double pad = (minSpan - lngSpan) / 2;
y0 = y0! - pad;
y1 = y1! + pad;
}
return LatLngBounds(
northeast: LatLng(x1!, y1!),
southwest: LatLng(x0!, y0!),
);
}
// داخل MapDriverController
Future<void> markDriverAsArrived() async {
// 1. إظهار لودينج فوراً لمنع التكرار وإشعار السائق
Get.dialog(
const Center(child: CircularProgressIndicator(color: AppColor.gold)),
barrierDismissible: false,
);
try {
// [Fix M-2] تغيير اسم المتغير المحلي لتجنب Variable Shadowing مع distance العام
double distToPassenger =
await calculateDistanceBetweenDriverAndPassengerLocation();
if (distToPassenger < 100) {
// 2. طلب الـ API مع مهلة 15 ثانية كأمان
await CRUD().post(
link: "${AppLink.ride}/rides/arrive_ride.php",
payload: {
"ride_id": rideId,
"driver_id": box.read(BoxName.driverID),
"passengerToken": tokenPassenger,
},
).timeout(const Duration(seconds: 15));
// 3. إغلاق اللودينج وتحديث الواجهة
if (Get.isDialogOpen == true) Get.back();
// منطق بدء العداد
startTimerToShowDriverWaitPassengerDuration();
isArrivedSend = false;
// تحديث فقط الجزء الخاص بمعلومات الراكب
update();
// رسم المسار للوجهة
getRoute(
origin: latLngPassengerLocation,
destination: latLngPassengerDestination,
routeColor: Colors.blue,
);
} else {
if (Get.isDialogOpen == true) Get.back();
mySnackeBarError("You must be closer than 100 meters to arrive".tr);
}
} catch (e) {
if (Get.isDialogOpen == true) Get.back();
mySnackeBarError("A connection error occurred".tr);
}
}
// [Fix P-3] _suggestOptimization() أزيلت — كانت ميتة ولا يستدعيها أحد
// =================================================================
// 5. دوال مساعدة (Helper Functions)
// =================================================================
String _parseInstruction(String html) =>
html.replaceAll(RegExp(r'<[^>]*>'), '');
/// Show a warning dialog when the driver and passenger are on the
/// same device (distance < 10 m) — the C++ map engine would crash with
/// std::domain_error if we tried to fit zero-span bounds.
void _showSameDeviceWarning() {
Get.dialog(
AlertDialog(
icon: const Icon(Icons.warning_amber_rounded,
color: Colors.orange, size: 48),
title: Text("Same device detected".tr),
content: Text(
"The rider and driver locations are very close (possibly on the same phone). "
"The map will show an approximate view."
.tr,
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text("OK".tr),
),
],
),
);
}
Future<void> _fitToBounds(LatLngBounds b, {double padding = 60}) async {
// Guard against zero-span bounds (crashes native C++ engine)
final double latSpan = (b.northeast.latitude - b.southwest.latitude).abs();
final double lngSpan =
(b.northeast.longitude - b.southwest.longitude).abs();
if (latSpan < 0.002 && lngSpan < 0.002) {
// Bounds are a single point — zoom instead of fit
await safeAnimateCamera(
CameraUpdate.newLatLngZoom(b.northeast, 16),
);
return;
}
await safeAnimateCamera(
CameraUpdate.newLatLngBounds(
b,
left: padding,
top: padding,
right: padding,
bottom: padding,
),
);
}
double distanceBetweenDriverAndPassengerWhenConfirm = 0;
/// [Fix M-4] هذه الدالة مكررة مع _checkNavigationStep().
/// نحتفظ بها للاستدعاء الخارجي (startListeningStepNavigation) ولكن logica مدمج.
void checkForNextStep(LatLng currentPosition) {
_checkNavigationStep(currentPosition);
}
/// Calculates the distance in meters between two latitude/longitude points.
double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
const double earthRadius = 6371000; // meters
double dLat = _degreesToRadians(lat2 - lat1);
double dLon = _degreesToRadians(lon2 - lon1);
double a = (sin(dLat / 2) * sin(dLat / 2)) +
cos(_degreesToRadians(lat1)) *
cos(_degreesToRadians(lat2)) *
(sin(dLon / 2) * sin(dLon / 2));
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
double distance = earthRadius * c;
return distance;
}
double _degreesToRadians(double degrees) {
return degrees * (3.1415926535897932 / 180.0);
}
bool isNearDestinationNotified = false;
// فحص الوصول للوجهة للإشعارات
void checkDestinationProximity() {
if (isNearDestinationNotified) return;
if (myLocation.latitude == 0 ||
myLocation.longitude == 0 ||
latLngPassengerDestination.latitude == 0 ||
latLngPassengerDestination.longitude == 0) return;
double dist = Geolocator.distanceBetween(
myLocation.latitude,
myLocation.longitude,
latLngPassengerDestination.latitude,
latLngPassengerDestination.longitude,
);
if (dist < 300) {
isNearDestinationNotified = true;
NotificationService.sendNotification(
target: tokenPassenger.toString(),
title: "You are near the destination".tr,
body: "The driver is approaching.".tr,
isTopic: false,
tone: 'ding',
driverList: [],
category: "Destination Proximity",
);
}
}
List<LatLngBounds> stepBounds = [];
List<LatLng> stepEndPoints = [];
List<String> stepInstructions = [];
StreamSubscription<Position>? _posSub;
DateTime _lastCameraUpdateTs = DateTime.fromMillisecondsSinceEpoch(0);
LatLngBounds _boundsFromPoints(List<LatLng> pts) {
double? minLat, maxLat, minLng, maxLng;
for (final p in pts) {
minLat = (minLat == null) ? p.latitude : math.min(minLat, p.latitude);
maxLat = (maxLat == null) ? p.latitude : math.max(maxLat, p.latitude);
minLng = (minLng == null) ? p.longitude : math.min(minLng, p.longitude);
maxLng = (maxLng == null) ? p.longitude : math.max(maxLng, p.longitude);
}
return LatLngBounds(
southwest: LatLng(minLat ?? 0, minLng ?? 0),
northeast: LatLng(maxLat ?? 0, maxLng ?? 0),
);
}
double _distanceMeters(LatLng a, LatLng b) {
// هافرساين مبسطة
const R = 6371000.0; // m
final dLat = _deg2rad(b.latitude - a.latitude);
final dLng = _deg2rad(b.longitude - a.longitude);
final s1 = math.sin(dLat / 2);
final s2 = math.sin(dLng / 2);
final aa = s1 * s1 +
math.cos(_deg2rad(a.latitude)) *
math.cos(_deg2rad(b.latitude)) *
s2 *
s2;
final c = 2 * math.atan2(math.sqrt(aa), math.sqrt(1 - aa));
return R * c;
}
double _deg2rad(double d) => d * math.pi / 180.0;
void updateCameraFromBoundsAfterGetMap(dynamic response) {
final bounds = response["routes"][0]["bounds"];
LatLng northeast = LatLng(
bounds['northeast']['lat'],
bounds['northeast']['lng'],
);
LatLng southwest = LatLng(
bounds['southwest']['lat'],
bounds['southwest']['lng'],
);
// Create the LatLngBounds object
LatLngBounds boundsData = LatLngBounds(
northeast: northeast,
southwest: southwest,
);
final latDiff =
(boundsData.northeast.latitude - boundsData.southwest.latitude).abs();
final lngDiff =
(boundsData.northeast.longitude - boundsData.southwest.longitude).abs();
if (latDiff < 0.0001 || lngDiff < 0.0001) {
final center = LatLng(
(boundsData.northeast.latitude + boundsData.southwest.latitude) / 2,
(boundsData.northeast.longitude + boundsData.southwest.longitude) / 2,
);
safeAnimateCamera(CameraUpdate.newCameraPosition(
CameraPosition(target: center, zoom: 17),
));
} else {
// Fit the camera to the bounds
var cameraUpdate = CameraUpdate.newLatLngBounds(
boundsData,
left: 140,
top: 140,
right: 140,
bottom: 140,
);
safeAnimateCamera(cameraUpdate);
}
}
void changePassengerInfoWindow() {
isPassengerInfoWindow = !isPassengerInfoWindow;
passengerInfoWindowHeight = isPassengerInfoWindow == true ? 200 : 0;
update();
}
double mpg = 0;
calculateConsumptionFuel() {
mpg = Get.find<HomeCaptainController>().fuelPrice /
12; //todo in register car add mpg in box
update();
}
argumentLoading() async {
// 🔥 [Fix Double-Init] منع التنفيذ المتزامن (onInit + postFrameCallback)
if (_argumentLoadingInProgress) {
Log.print(
"⏳ argumentLoading: Already in progress, skipping concurrent call.");
return;
}
// 🔥 Debounce: منع التنفيذ المتكرر السريع من build() callbacks
// نسمح بإعادة التنفيذ فقط بعد مرور 5 ثوانٍ من آخر تنفيذ ناجح
// (هذا يمنع الـ rapid rebuilds لكن يسمح بالعودة للرحلة من الهوم)
if (_lastArgumentLoadingTime != null) {
final elapsed = DateTime.now().difference(_lastArgumentLoadingTime!);
if (elapsed.inSeconds < 5) {
Log.print(
"⏭️ argumentLoading: Debounced (${elapsed.inMilliseconds}ms < 5000ms). Skipping.");
return;
}
}
_argumentLoadingInProgress = true;
// 🔥 إعادة تعيين flag المسار عند كل استدعاء لاحق
// (ضروري للعودة للرحلة من صفحة الهوم)
_isRouteRequested = false;
// 🛑 حماية: إذا لم تكن هناك arguments، لا تكمل
if (Get.arguments == null || Get.arguments is! Map) {
Log.print("❌ argumentLoading: No valid arguments found. Aborting.");
_argumentLoadingInProgress = false;
return;
}
try {
passengerLocation = Get.arguments['passengerLocation']?.toString() ?? '';
passengerDestination =
Get.arguments['passengerDestination']?.toString() ?? '';
duration = Get.arguments['Duration']?.toString() ?? '';
totalCost = Get.arguments['totalCost']?.toString() ?? '';
passengerId = Get.arguments['passengerId']?.toString() ?? '';
driverId = Get.arguments['driverId']?.toString() ?? '';
distance = Get.arguments['Distance']?.toString() ?? '0';
passengerName = Get.arguments['name']?.toString();
passengerEmail = Get.arguments['email']?.toString() ?? '';
totalPricePassenger = Get.arguments['totalPassenger']?.toString() ?? '';
passengerPhone = Get.arguments['phone']?.toString() ?? '';
walletChecked = Get.arguments['WalletChecked']?.toString() ?? '';
tokenPassenger = Get.arguments['tokenPassenger']?.toString() ?? '';
direction = Get.arguments['direction']?.toString() ?? '';
durationToPassenger =
Get.arguments['DurationToPassenger']?.toString() ?? '100';
rideId = Get.arguments['rideId']?.toString() ?? '';
durationOfRideValue =
Get.arguments['durationOfRideValue']?.toString() ?? '';
paymentAmount = Get.arguments['paymentAmount']?.toString() ?? '0';
paymentMethod = Get.arguments['paymentMethod']?.toString() ?? '';
// 🔥 حفظ البيانات في الذاكرة المحلية فوراً (لفصل السوكيت عن الكنترولر)
box.write(BoxName.passengerID, passengerId.toString());
box.write(BoxName.rideId, rideId.toString());
// Also save full args for return-to-ride scenarios
box.write(BoxName.rideArguments, Get.arguments);
isHaveSteps = Get.arguments['isHaveSteps']?.toString() ?? 'false';
// [Fix N-4] ملء القائمة بدلاً من 5 متغيرات منفصلة
steps = List.generate(5, (i) {
return Get.arguments['step$i']?.toString() ?? '';
});
passengerWalletBurc =
Get.arguments['passengerWalletBurc']?.toString() ?? '';
timeOfOrder = Get.arguments['timeOfOrder']?.toString() ?? '';
carType = Get.arguments['carType']?.toString() ?? '';
kazan = Get.arguments['kazan']?.toString() ?? '';
startNameLocation = Get.arguments['startNameLocation']?.toString() ?? '';
endNameLocation = Get.arguments['endNameLocation']?.toString() ?? '';
// Parse to double
latlng(passengerLocation, passengerDestination);
String lat =
Get.find<LocationController>().myLocation.latitude.toString();
String lng =
Get.find<LocationController>().myLocation.longitude.toString();
// Check which route to draw based on current ride status
String currentStatus = box.read(BoxName.rideStatus) ?? '';
// [Fix 5] إضافة await لضمان أن التأخير يعمل فعلاً قبل رسم المسار.
await Future.delayed(const Duration(seconds: 1));
// 🔥 [Fix Return-to-Ride] تعيين isRideStarted قبل getRoute لمنع onMapCreated
// من رسم المسار الأصفر (للراكب) بدلاً من الأزرق (للوجهة)
if (currentStatus == 'Begin') {
isRideStarted = true;
isRideBegin = true;
isPassengerInfoWindow = false;
}
if (currentStatus == 'Begin' &&
latLngPassengerDestination.latitude != 0 &&
latLngPassengerDestination.longitude != 0) {
// Ride already started — draw blue route to destination
await getRoute(
origin: Get.find<LocationController>().myLocation,
destination: latLngPassengerDestination,
routeColor: Colors.blue,
);
// 🔥 [Fix Return-to-Ride] إعادة تشغيل الخدمات المفقودة عند العودة للرحلة
// بدون هذا الاستدعاء، يُرسم المسار لكن:
// - عداد السعر (_rideTimer) لا يعمل
// - الملاحة (_navigationTimer) لا تعمل
// - الماركر لا يتحرك
if (_rideTimer == null || !_rideTimer!.isActive) {
rideIsBeginPassengerTimer();
}
await startListeningStepNavigation();
} else {
// Ride not yet started — draw yellow route to passenger
await getRoute(
origin: Get.find<LocationController>().myLocation,
destination: latLngPassengerLocation,
routeColor: Colors.yellow,
);
// 🔥 بدء الملاحة بعد تحميل المسار (تتبع حركة السائق نحو الراكب)
if (_navigationTimer == null || !_navigationTimer!.isActive) {
startListeningStepNavigation();
}
}
_isRouteRequested = true;
update();
} catch (e) {
Log.print("Error parsing arguments: $e");
} finally {
// 🔥 [Fix Double-Init] دائماً إعادة تعيين الـ flag عند الانتهاء
_argumentLoadingInProgress = false;
// سجّل وقت الانتهاء للـ debounce
_lastArgumentLoadingTime = DateTime.now();
}
}
void latlng(String passengerLocation, String passengerDestination) {
try {
// ── مكان الراكب ──────────────────────────────────────────────────
final List<String> locParts = passengerLocation.split(',');
double latPassengerLocation = locParts.length >= 1
? (double.tryParse(locParts[0].trim()) ?? 0.0)
: 0.0;
double lngPassengerLocation = locParts.length >= 2
? (double.tryParse(locParts[1].trim()) ?? 0.0)
: 0.0;
// ── وجهة الراكب ─────────────────────────────────────────────────
final List<String> destParts = passengerDestination.split(',');
double latPassengerDestination = destParts.length >= 1
? (double.tryParse(destParts[0].trim()) ?? 0.0)
: 0.0;
double lngPassengerDestination = destParts.length >= 2
? (double.tryParse(destParts[1].trim()) ?? 0.0)
: 0.0;
latLngPassengerLocation = LatLng(
latPassengerLocation,
lngPassengerLocation,
);
latLngPassengerDestination = LatLng(
latPassengerDestination,
lngPassengerDestination,
);
Log.print(
"📍 latlng() parsed => Pax: ($latPassengerLocation, $lngPassengerLocation) | Dest: ($latPassengerDestination, $lngPassengerDestination)");
} catch (e, stack) {
Log.print("❌ latlng() FormatException prevented: $e");
Log.print("Stack: $stack");
// قيم افتراضية آمنة لمنع الكراش
latLngPassengerLocation = LatLng(0, 0);
latLngPassengerDestination = LatLng(0, 0);
}
}
Duration durationToAdd = Duration.zero;
int hours = 0;
int minutes = 0;
String carType = '';
String kazan = '';
String startNameLocation = '';
String endNameLocation = '';
Future<void> runGoogleMapDirectly() async {
if (box.read(BoxName.googlaMapApp) == true) {
if (Platform.isAndroid) {
Bubble().startBubbleHead(sendAppToBackground: true);
}
await openGoogleMapFromDriverToPassenger();
}
}
@override
void onInit() async {
mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY);
await argumentLoading();
Get.put(FirebaseMessagesController());
runGoogleMapDirectly();
addCustomCarIcon();
addCustomPassengerIcon();
addCustomStartIcon();
addCustomEndIcon();
MarkerGenerator.createWalkMarker().then((icon) {
walkIcon = icon;
update();
});
if (!Get.isRegistered<TextToSpeechController>()) {
Get.put(TextToSpeechController(), permanent: true);
}
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
);
_animController!.addListener(() {
if (_oldLoc != null && _targetLoc != null) {
final t = _animController!.value;
final lat = lerpDouble(_oldLoc!.latitude, _targetLoc!.latitude, t)!;
final lng = lerpDouble(_oldLoc!.longitude, _targetLoc!.longitude, t)!;
smoothedLocation = LatLng(lat, lng);
smoothedHeading = _lerpAngle(_oldHeading, _targetHeading, t);
update();
}
});
_startLocationListening();
startTimerToShowPassengerInfoWindowFromDriver();
durationToAdd = Duration(seconds: parseDurationToInt(duration));
hours = durationToAdd.inHours;
minutes = (durationToAdd.inMinutes % 60).round();
calculateConsumptionFuel();
super.onInit();
}
void _startLocationListening() {
// Location stream is now centralized in LocationController to prevent device hanging.
// LocationController will call handleLocationUpdateFromCentral directly.
}
/// [Fix C-4] تحديث myLocation في المستمع الأساسي
void handleLocationUpdateFromCentral(
LatLng newLoc, double posSpeed, double posHeading) {
myLocation = newLoc; // ← [Fix C-4] تحديث الموقع الفوري
_oldLoc = smoothedLocation ?? newLoc;
_targetLoc = newLoc;
_oldHeading = smoothedHeading;
if (posSpeed > 0.5) {
_targetHeading = posHeading;
} else {
_targetHeading = _oldHeading;
}
_animController?.forward(from: 0.0);
if (routeSteps.isNotEmpty) {
_checkNavigationStep(newLoc);
}
}
double _lerpAngle(double from, double to, double t) {
final double diff = ((to - from + 540.0) % 360.0) - 180.0;
return (from + diff * t + 360.0) % 360.0;
}
void _checkNavigationStep(LatLng pos) {
if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length) return;
final step = routeSteps[currentStepIndex];
final stepLoc = step['end_location'];
if (stepLoc == null) return;
final double stepLat = stepLoc['lat'];
final double stepLng = stepLoc['lng'];
final distance = geo.Geolocator.distanceBetween(
pos.latitude,
pos.longitude,
stepLat,
stepLng,
);
distanceToNextStep = distance > 1000
? "${(distance / 1000).toStringAsFixed(1)} km"
: "${distance.toStringAsFixed(0)} m";
if (distance < 50 &&
!_nextInstructionSpoken &&
(currentStepIndex + 1) < routeSteps.length) {
final nextText =
routeSteps[currentStepIndex + 1]['html_instructions'] ?? "";
if (isTtsEnabled) {
Get.find<TextToSpeechController>().speakText(nextText);
}
_nextInstructionSpoken = true;
}
if (distance < 25) {
_advanceStep();
}
update();
}
IconData get currentManeuverIcon {
switch (currentManeuverModifier) {
case 4: // Arrive
return Icons.place_rounded;
case 6: // Roundabout
return Icons.roundabout_right_rounded;
case 2: // Right
return Icons.turn_right_rounded;
case 3: // Slight Right
return Icons.turn_slight_right_rounded;
case -2: // Left
return Icons.turn_left_rounded;
case -1: // Slight Left
return Icons.turn_slight_left_rounded;
case 7: // Keep Right
return Icons.turn_right_rounded;
case -7: // Keep Left
return Icons.turn_left_rounded;
case 0: // Straight
return Icons.straight_rounded;
default:
return Icons.straight_rounded;
}
}
void _advanceStep() {
currentStepIndex++;
if (currentStepIndex < routeSteps.length) {
currentInstruction =
routeSteps[currentStepIndex]['html_instructions'] ?? "";
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0;
_nextInstructionSpoken = false;
}
}
int parseDurationToInt(dynamic value) {
if (value == null) return 0;
String text = value.toString();
// حذف كل شيء ما عدا الأرقام (الإنجليزية)
String digits = text.replaceAll(RegExp(r'[^0-9]'), '');
if (digits.isEmpty) return 0;
return int.tryParse(digits) ?? 0;
}
Timer? _navigationTimer;
// أضف هذا المتغير في الكلاس
LatLng? _lastRecordedLocation;
// [Fix M-4] تم إزالة الكود المعلّق القديم لـ startListeningStepNavigation
void _animateCameraToNavigationMode(LatLng target, double bearing) {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: target,
bearing: bearing,
tilt: 45.0, // منظور ثلاثي الأبعاد (3D Perspective)
zoom: 18.0, // تقريب للملاحة
),
),
);
}
// متغير لتتبع آخر نقطة وصلنا لها
int _lastTraveledIndex = 0;
// تحديث المسار الذكي
void _updateTraveledPolylineSmart(LatLng currentPos) {
if (upcomingPathPoints.isEmpty) return;
// Bidirectional Sliding Window: نبحث 30 نقطة للخلف و30 نقطة للأمام لدعم الرجوع أو اتخاذ مسارات بديلة
int searchWindow = 60;
int halfWindow = searchWindow ~/ 2;
int startIndex = max(0, _lastTraveledIndex - halfWindow);
int endIndex =
min(_lastTraveledIndex + halfWindow, upcomingPathPoints.length);
double minDistance = double.infinity;
int closestIndex = _lastTraveledIndex;
bool foundCloser = false;
for (int i = startIndex; i < endIndex; i++) {
final point = upcomingPathPoints[i];
final dist = Geolocator.distanceBetween(
currentPos.latitude,
currentPos.longitude,
point.latitude,
point.longitude,
);
if (dist < minDistance) {
minDistance = dist;
closestIndex = i;
foundCloser = true;
}
}
if (foundCloser && minDistance < 50 && closestIndex != _lastTraveledIndex) {
_lastTraveledIndex = closestIndex;
final remaining = upcomingPathPoints.sublist(_lastTraveledIndex);
final traveled = upcomingPathPoints.sublist(0, _lastTraveledIndex + 1);
polyLines.removeWhere((p) => p.polylineId.value == 'upcoming_route');
polyLines.removeWhere((p) => p.polylineId.value == 'traveled_route');
// المسار المتبقي
polyLines.add(
Polyline(
polylineId: const PolylineId("upcoming_route"),
points: remaining,
width: 8,
color: isRideStarted ? Colors.blue : Colors.yellow,
),
);
// المسار المقطوع (رمادي)
polyLines.add(
Polyline(
polylineId: const PolylineId('traveled_route'),
points: traveled,
color: Colors.grey.withValues(alpha: 0.8),
width: 7,
zIndex: 1,
),
);
_updatePassengerWalkLine();
update();
}
}
void stopListeningStepNavigation() {
_posSub?.cancel();
_posSub = null;
// [Fix 1] إعادة تشغيل المستمع الأساسي للحركة السلسة بعد إيقاف الملاحة.
_startLocationListening();
}
// دالة لبناء الخط المنقط
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);
if (dist < 2) return []; // قريبة جداً، لا نرسم خطاً
const double dashLengthMeters = 8.0;
const double gapLengthMeters = 6.0;
double totalLength = 0.0;
int segmentCount = 0;
while (totalLength < dist) {
double startFraction = totalLength / dist;
double startLat =
start.latitude + (end.latitude - start.latitude) * startFraction;
double startLng =
start.longitude + (end.longitude - start.longitude) * startFraction;
totalLength += dashLengthMeters;
if (totalLength > dist) totalLength = dist;
double endFraction = totalLength / dist;
double endLat =
start.latitude + (end.latitude - start.latitude) * endFraction;
double endLng =
start.longitude + (end.longitude - start.longitude) * endFraction;
segments.add(
Polyline(
polylineId: PolylineId('${prefixId}_dash_$segmentCount'),
points: [LatLng(startLat, startLng), LatLng(endLat, endLng)],
color: color,
width: 4.0,
),
);
segmentCount++;
totalLength += gapLengthMeters;
}
return segments;
}
// تحديث ورسم الخط المنقط من نهاية الطريق إلى موقع الراكب الفعلي
void _updatePassengerWalkLine() {
// مسح خطوط المشي السابقة
polyLines.removeWhere(
(p) => p.polylineId.value.startsWith('passenger_walk_line'));
if (!isRideStarted &&
upcomingPathPoints.isNotEmpty &&
latLngPassengerLocation.latitude != 0) {
final LatLng lastRoadPt = upcomingPathPoints.last;
final walkDashes = _buildDashedLine(
lastRoadPt,
latLngPassengerLocation,
color: Colors.blueGrey, // لون أزرق رمادي مميز
prefixId: 'passenger_walk_line',
);
polyLines.addAll(walkDashes);
}
}
}
double safeParseDouble(dynamic value, {double defaultValue = 0.0}) {
if (value == null) return defaultValue;
if (value is double) return value;
if (value is int) return value.toDouble();
return double.tryParse(value.toString()) ?? defaultValue;
}
int safeParseInt(dynamic value, {int defaultValue = 0}) {
if (value == null) return defaultValue;
if (value is int) return value;
return int.tryParse(value.toString()) ?? defaultValue;
}