2435 lines
86 KiB
Dart
Executable File
2435 lines
86 KiB
Dart
Executable File
import 'dart:async';
|
|
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:intl/intl.dart';
|
|
import 'package:sefer_driver/controller/firebase/local_notification.dart';
|
|
import 'package:sefer_driver/controller/home/captin/behavior_controller.dart';
|
|
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
|
|
import 'package:sefer_driver/controller/home/navigation/decode_polyline_isolate.dart';
|
|
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
|
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
|
import 'package:bubble_head/bubble.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
|
import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
import '../../../constant/api_key.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 '../../../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';
|
|
|
|
class MapDriverController extends GetxController {
|
|
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;
|
|
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
|
BitmapDescriptor passengerIcon = BitmapDescriptor.defaultMarker;
|
|
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
|
|
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker;
|
|
final List<LatLng> polylineCoordinates = [];
|
|
final List<LatLng> polylineCoordinatesDestination = [];
|
|
List<Polyline> polyLines = [];
|
|
List<Polyline> polyLinesDestination = [];
|
|
Set<Marker> markers = {};
|
|
late String passengerLocation;
|
|
late String passengerDestination;
|
|
late String step0;
|
|
late String step1;
|
|
late String step2;
|
|
late String step3;
|
|
late String step4;
|
|
late String passengerWalletBurc;
|
|
late String timeOfOrder;
|
|
late String duration;
|
|
late String totalCost;
|
|
String distance = '0';
|
|
String? passengerName;
|
|
late String passengerEmail;
|
|
late String totalPricePassenger;
|
|
late String passengerPhone;
|
|
late String rideId;
|
|
late String isHaveSteps;
|
|
String paymentAmount = '0';
|
|
late String paymentMethod;
|
|
late String passengerId;
|
|
late String driverId;
|
|
late String tokenPassenger;
|
|
String durationToPassenger = '100';
|
|
late String walletChecked;
|
|
late String direction;
|
|
late String durationOfRideValue;
|
|
late 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;
|
|
bool isDriverNearPassengerStart = false;
|
|
GoogleMapController? mapController;
|
|
late LatLng myLocation;
|
|
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);
|
|
late LatLng latLngPassengerDestination = LatLng(0, 0);
|
|
|
|
List<Map<String, dynamic>> routeSteps = [];
|
|
String currentInstruction = "";
|
|
int currentStepIndex = 0;
|
|
bool isTtsEnabled = false;
|
|
|
|
// في 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);
|
|
}
|
|
}
|
|
|
|
void disposeEverything() {
|
|
onClose();
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
print("--- KILLING ALL DRIVER TIMERS ---");
|
|
_rideTimer?.cancel();
|
|
_rideTimer = null;
|
|
_waitingTimer?.cancel();
|
|
timer?.cancel();
|
|
timer = null;
|
|
|
|
_navigationTimer?.cancel();
|
|
_navigationTimer = null;
|
|
|
|
_posSub?.cancel();
|
|
_posSub = null;
|
|
|
|
mapController?.dispose();
|
|
super.onClose();
|
|
}
|
|
|
|
void onMapCreated(GoogleMapController controller) {
|
|
mapController = controller;
|
|
if (Get.isRegistered<LocationController>()) {
|
|
myLocation = Get.find<LocationController>().myLocation;
|
|
controller.animateCamera(CameraUpdate.newLatLngZoom(myLocation, 16));
|
|
}
|
|
// بدء الاستماع للموقع للملاحة وتحديث الماركر
|
|
startListeningStepNavigation();
|
|
}
|
|
|
|
bool isCameraLocked = true; // للتحكم في تتبع الكاميرا
|
|
|
|
Future<void> startListeningStepNavigation() async {
|
|
_posSub?.cancel();
|
|
|
|
_posSub = Geolocator.getPositionStream(
|
|
locationSettings: LocationSettings(
|
|
accuracy: LocationAccuracy.bestForNavigation, // دقة عالية للملاحة
|
|
distanceFilter: 5, // تحديث كل 5 أمتار
|
|
),
|
|
).listen((position) {
|
|
LatLng newLoc = LatLng(position.latitude, position.longitude);
|
|
|
|
// فلتر الاهتزاز البسيط (Jitter Filter)
|
|
if (_lastRecordedLocation != null) {
|
|
double dist = Geolocator.distanceBetween(
|
|
newLoc.latitude,
|
|
newLoc.longitude,
|
|
_lastRecordedLocation!.latitude,
|
|
_lastRecordedLocation!.longitude);
|
|
if (dist < 2.0) return; // تجاهل الحركة الطفيفة
|
|
}
|
|
|
|
_lastRecordedLocation = newLoc;
|
|
myLocation = newLoc;
|
|
double heading = position.heading;
|
|
double speedKmh = position.speed * 3.6;
|
|
|
|
if (!isClosed) {
|
|
// 1. تحديث الماركر
|
|
updateMarker();
|
|
|
|
// 2. تحديث المسار (Smart Snapping)
|
|
if (upcomingPathPoints.isNotEmpty) {
|
|
_updateTraveledPolylineSmart(myLocation);
|
|
}
|
|
|
|
// 3. تحريك الكاميرا (Navigation Mode)
|
|
// فقط إذا كان "القفل" مفعلاً والسرعة > 5 كم (لتجنب الدوران العشوائي عند الوقوف)
|
|
if (isCameraLocked && mapController != null) {
|
|
double bearing = (speedKmh > 5) ? heading : 0.0;
|
|
// ملاحظة: يمكنك تخزين آخر bearing معروف واستخدامه عند التوقف لتحسين التجربة
|
|
_animateCameraToNavigationMode(newLoc, heading);
|
|
}
|
|
|
|
// 4. فحص التعليمات الصوتية
|
|
checkForNextStep(myLocation);
|
|
|
|
// 5. فحص الوصول للوجهة (للإشعار)
|
|
checkDestinationProximity();
|
|
|
|
update();
|
|
}
|
|
});
|
|
}
|
|
|
|
void changeStatusDriver() {
|
|
status = 'On';
|
|
update();
|
|
}
|
|
|
|
void changeDriverEndPage() {
|
|
remainingTimeTimerRideBegin < 60 ? driverEndPage = 160 : 100;
|
|
update();
|
|
}
|
|
|
|
takeSnapMap() {
|
|
mapController!.takeSnapshot();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
print("--- KILLING ALL DRIVER TIMERS ---");
|
|
_stopAllServices();
|
|
super.dispose();
|
|
}
|
|
|
|
void _stopAllServices() {
|
|
_rideTimer?.cancel();
|
|
_passengerTimer?.cancel();
|
|
_waitingTimer?.cancel();
|
|
_posSub?.cancel();
|
|
mapController?.dispose();
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
Log.print("🚫 Ride Cancelled by Passenger via $source. Reason: $reason");
|
|
|
|
// 1. إيقاف التوجيه والتايمرات
|
|
// stopNavigation();
|
|
// stopAllTimers();
|
|
|
|
// 2. تنظيف الحالة
|
|
box.write(BoxName.rideStatus, 'Canceled'); // أو الحالة الافتراضية
|
|
box.remove(BoxName.rideArguments);
|
|
|
|
// 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.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');
|
|
|
|
// عرض رسالة العقوبة
|
|
Get.snackbar("تم تقييد حسابك مؤقتاً ⛔",
|
|
"بسبب كثرة الإلغاءات (3 مرات)، تم إيقاف استقبال الطلبات لمدة 4 ساعات.",
|
|
duration: Duration(seconds: 8),
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
snackPosition: SnackPosition.BOTTOM);
|
|
} else {
|
|
// تحذير فقط
|
|
int count = response['cancel_count'] ?? 0;
|
|
Get.snackbar("تنبيه",
|
|
"لقد ألغيت $count رحلات اليوم. الوصول لـ 3 سيعرضك للإيقاف المؤقت.",
|
|
backgroundColor: Colors.orange);
|
|
}
|
|
|
|
// تنظيف البيانات
|
|
box.remove(BoxName.rideArgumentsFromBackground);
|
|
box.remove(BoxName.rideArguments);
|
|
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 (_) {}
|
|
|
|
Get.find<HomeCaptainController>().getRefusedOrderByCaptain();
|
|
|
|
if (Get.isDialogOpen == true) Get.back();
|
|
Get.offAll(
|
|
() => HomeCaptain()); // العودة للرئيسية ليتم تطبيق الحظر هناك
|
|
} else {
|
|
if (Get.isDialogOpen == true) Get.back();
|
|
Get.snackbar("Error", "Failed to cancel ride");
|
|
}
|
|
} 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;
|
|
}
|
|
|
|
// 2. تسجيل العملية (تسجيل آمن)
|
|
if (isSocialPressed == true && passengerId != null && rideId != null) {
|
|
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() {
|
|
// if (box.read(BoxName.rideStatus) == 'Begin') {
|
|
changeRideToBeginToPassenger();
|
|
isPassengerInfoWindow = false;
|
|
isRideStarted = true;
|
|
isRideFinished = false;
|
|
remainingTimeInPassengerLocatioWait = 0;
|
|
timeWaitingPassenger = 0;
|
|
box.write(BoxName.statusDriverLocation, 'on');
|
|
update();
|
|
// }
|
|
|
|
rideIsBeginPassengerTimer();
|
|
}
|
|
|
|
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 < 100) {
|
|
// زدت المسافة قليلاً لمرونة أكبر (150م)
|
|
|
|
// --- أ) تحديث الحالة المحلية (Optimistic Update) ---
|
|
changeRideToBeginToPassenger(); // تغيير المتغيرات الداخلية
|
|
isPassengerInfoWindow = false;
|
|
isRideStarted = true;
|
|
isRideFinished = false;
|
|
remainingTimeInPassengerLocatioWait = 0;
|
|
timeWaitingPassenger = 0;
|
|
|
|
// الحفظ في التخزين المحلي
|
|
box.write(BoxName.statusDriverLocation, 'on');
|
|
box.write(BoxName.rideStatus, 'Begin');
|
|
|
|
// بدء الخدمات (الملاحة والعداد)
|
|
await startListeningStepNavigation();
|
|
rideIsBeginPassengerTimer();
|
|
|
|
// --- ب) تحديث الواجهة (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') {
|
|
// Revert logic if needed (نادر الحدوث)
|
|
Log.print("Server failed to start ride!");
|
|
}
|
|
});
|
|
} else {
|
|
// --- حالة الرفض (بعيد جداً) ---
|
|
MyDialog().getDialog(
|
|
'You are far from passenger location'.tr,
|
|
'Please go closer to the passenger location (less than 150m)'.tr,
|
|
() => Get.back() // إغلاق الديالوج فقط
|
|
);
|
|
}
|
|
} catch (e) {
|
|
// تنظيف اللودينج في حال حدوث خطأ غير متوقع
|
|
if (Get.isDialogOpen == true) Get.back();
|
|
Log.print("Error starting ride: $e");
|
|
Get.snackbar("Error", "Could not start ride. Please check internet.");
|
|
}
|
|
}
|
|
|
|
calculateDistanceInMeter(LatLng prev, LatLng current) async {
|
|
double distance2 = Geolocator.distanceBetween(
|
|
prev.latitude,
|
|
prev.longitude,
|
|
current.latitude,
|
|
current.longitude,
|
|
);
|
|
return distance2;
|
|
}
|
|
|
|
double speedoMeter = 0;
|
|
void updateLocation() async {
|
|
try {
|
|
for (var i = 0; i < remainingTimeTimerRideBegin; i++) {
|
|
await Future.delayed(const Duration(seconds: 3));
|
|
|
|
await safeAnimateCamera(
|
|
CameraUpdate.newCameraPosition(
|
|
CameraPosition(
|
|
bearing: Get.find<LocationController>().heading,
|
|
target: myLocation,
|
|
zoom: 17,
|
|
),
|
|
),
|
|
);
|
|
// });
|
|
update();
|
|
}
|
|
|
|
// Stop listening after ride finishes
|
|
if (!isRideBegin) {}
|
|
} catch (error) {
|
|
debugPrint('Error listening to GPS: $error');
|
|
// Handle GPS errors gracefully
|
|
}
|
|
|
|
// Periodically call updateLocation again
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
updateLocation();
|
|
}
|
|
|
|
calculateDistanceBetweenDriverAndPassengerLocation() async {
|
|
Get.put(LocationController());
|
|
var res = await CRUD().get(
|
|
link: AppLink.getLatestLocationPassenger,
|
|
payload: {'rideId': (rideId)});
|
|
if (res != 'failure') {
|
|
var passengerLatestLocationString = jsonDecode(res)['message'];
|
|
|
|
double distance2 = Geolocator.distanceBetween(
|
|
double.parse(passengerLatestLocationString[0]['lat'].toString()),
|
|
double.parse(passengerLatestLocationString[0]['lng'].toString()),
|
|
Get.find<LocationController>().myLocation.latitude,
|
|
Get.find<LocationController>().myLocation.longitude,
|
|
);
|
|
return distance2;
|
|
} else {
|
|
double distance2 = Geolocator.distanceBetween(
|
|
latLngPassengerLocation.latitude,
|
|
latLngPassengerLocation.longitude,
|
|
Get.find<LocationController>().myLocation.latitude,
|
|
Get.find<LocationController>().myLocation.longitude,
|
|
);
|
|
return distance2;
|
|
}
|
|
}
|
|
|
|
/// دالة مساعدة لحساب التكلفة (Logic Helper)
|
|
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();
|
|
|
|
Get.snackbar(
|
|
'Compensation Received'.tr,
|
|
'${'You gained'.tr} ${costOfWaiting.toStringAsFixed(2)} ${'in your wallet'.tr}',
|
|
backgroundColor: AppColor.deepPurpleAccent,
|
|
);
|
|
|
|
box.write(BoxName.statusDriverLocation, 'off');
|
|
Get.offAll(() => HomeCaptain());
|
|
} else {
|
|
throw Exception("Payment Transaction Failed");
|
|
}
|
|
} catch (e) {
|
|
if (Get.isDialogOpen == true) Get.back();
|
|
Log.print("Error: $e");
|
|
Get.snackbar("Error", "Transaction failed, please try again.");
|
|
}
|
|
}
|
|
|
|
/// **التحقق من إنهاء الرحلة (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 {
|
|
// 1. تحويل مسافة الرحلة الكلية
|
|
|
|
// 1. نقوم أولاً بتنظيف النص من أي حروف (مثل 'km' أو 'كم') ونبقي الأرقام والنقطة فقط
|
|
String cleanDistance =
|
|
distance.toString().replaceAll(RegExp(r'[^0-9.]'), '');
|
|
|
|
// 2. حماية إضافية: إذا كان النص فارغاً بعد التنظيف نعتبره صفر
|
|
if (cleanDistance.isEmpty) cleanDistance = "0.0";
|
|
|
|
// 3. الآن التحويل سيتم بنجاح بدون أخطاء
|
|
final double totalTripDistanceMeters = double.parse(cleanDistance) * 1000;
|
|
// 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 {
|
|
// إذا جاء من زر عادي (إن وجد)، اطلب التأكيد
|
|
MyDialog().getDialog(
|
|
'Are you sure to exit ride?'.tr,
|
|
'',
|
|
() {
|
|
Get.back();
|
|
finishRideFromDriver1();
|
|
},
|
|
);
|
|
}
|
|
} 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 {
|
|
// 3. تحديث الحالة المحلية لمنع أي تفاعلات أخرى
|
|
isRideFinished = true;
|
|
isRideStarted = false;
|
|
isPriceWindow = false;
|
|
box.write(BoxName.rideStatus, 'Finished');
|
|
box.write(BoxName.statusDriverLocation, 'off');
|
|
|
|
// 4. حساب التكلفة النهائية (Logic)
|
|
_calculateFinalTotalCost();
|
|
|
|
// 5. تجهيز البيانات (Payloads)
|
|
final rideUpdatePayload = {
|
|
'rideId': rideId.toString(),
|
|
'driver_id': box.read(BoxName.driverID).toString(),
|
|
'status': 'Finished',
|
|
'price': totalCost,
|
|
'passengerId': passengerId.toString(),
|
|
'driver_token': box.read(BoxName.tokenDriver).toString(),
|
|
'passengerToken': tokenPassenger.toString(),
|
|
};
|
|
|
|
// توليد توكن الدفع أولاً
|
|
final String paymentAuthToken =
|
|
await generateTokenDriver(paymentAmount.toString());
|
|
|
|
final paymentProcessingPayload = {
|
|
'rideId': rideId.toString(),
|
|
'driverId': box.read(BoxName.driverID).toString(),
|
|
'passengerId': passengerId.toString(),
|
|
'paymentAmount': paymentAmount,
|
|
'paymentMethod': paymentMethod,
|
|
'walletChecked': walletChecked.toString(),
|
|
'passengerWalletBurc': passengerWalletBurc.toString(),
|
|
'authToken': paymentAuthToken,
|
|
};
|
|
|
|
// 6. التنفيذ المتوازي (Parallel Execution) - الأسرع
|
|
final results = await Future.wait([
|
|
CRUD().post(
|
|
link: "${AppLink.ride}/rides/finish_ride_updates.php",
|
|
payload: rideUpdatePayload),
|
|
CRUD().postWallet(
|
|
link:
|
|
"${AppLink.paymentServer}/ride/payment/process_ride_payments.php",
|
|
payload: paymentProcessingPayload),
|
|
]);
|
|
|
|
final rideRes = results[0];
|
|
final payRes = results[1];
|
|
|
|
// 7. التحقق من النجاح
|
|
if (rideRes['status'] == 'success' && payRes['status'] == 'success') {
|
|
// تنظيف البيانات
|
|
box.remove(BoxName.rideArguments);
|
|
box.remove(BoxName.rideArgumentsFromBackground);
|
|
|
|
// إغلاق اللودينج
|
|
if (Get.isDialogOpen == true) Get.back();
|
|
|
|
// إرسال تقرير السلوك (Fire and forget)
|
|
Get.put(DriverBehaviorController())
|
|
.sendSummaryToServer(driverId, rideId);
|
|
|
|
// الانتقال لصفحة التقييم
|
|
Get.off(() => RatePassenger(), arguments: {
|
|
'passengerId': passengerId,
|
|
'rideId': rideId,
|
|
'price': paymentAmount.toString(),
|
|
'walletChecked': walletChecked.toString() ?? 'false'
|
|
});
|
|
} else {
|
|
throw Exception(
|
|
"Server Error: Ride=${rideRes['status']}, Payment=${payRes['status']}");
|
|
}
|
|
} catch (e) {
|
|
// 8. معالجة الأخطاء (Revert State)
|
|
if (Get.isDialogOpen == true) Get.back(); // إغلاق اللودينج
|
|
|
|
Log.print("Error finishing ride: $e");
|
|
Get.snackbar(
|
|
"Error".tr, "Failed to finish ride. Please check internet.".tr,
|
|
backgroundColor: Colors.red, colorText: Colors.white);
|
|
|
|
// إعادة الحالة للسماح للمستخدم بالمحاولة مرة أخرى
|
|
isRideFinished = false;
|
|
isRideStarted = true;
|
|
box.write(BoxName.rideStatus, 'Begin');
|
|
update(); // تحديث الشريط ليعود للوضع النشط
|
|
}
|
|
}
|
|
|
|
// --- دوال مساعدة (Helpers) لتنظيف الكود ---
|
|
|
|
Future<bool> _validateTripDistance(bool isFromSlider) async {
|
|
// منطق التحقق من المسافة المقطوعة (كما هو موجود لديك)
|
|
String cleanDistance =
|
|
distance.toString().replaceAll(RegExp(r'[^0-9.]'), '');
|
|
if (cleanDistance.isEmpty) cleanDistance = "0.0";
|
|
|
|
final double totalTripDistanceMeters = double.parse(cleanDistance) * 1000;
|
|
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;
|
|
|
|
// إذا لم يكن من السلايدر، نعرض تأكيد
|
|
bool confirmed = false;
|
|
MyDialog().getDialog('Exit Ride?'.tr, '', () {
|
|
confirmed = true;
|
|
Get.back();
|
|
});
|
|
return confirmed;
|
|
} 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 _calculateFinalTotalCost() {
|
|
// منطق حساب السعر (كما هو موجود لديك)
|
|
if (price < 172) {
|
|
totalCost = (carType == 'Comfort' ||
|
|
carType == 'Mishwar Vip' ||
|
|
carType == 'Lady')
|
|
? '200'
|
|
: '172';
|
|
} else if (price < double.parse(totalPricePassenger)) {
|
|
totalCost = totalPricePassenger;
|
|
} else {
|
|
totalCost = (carType == 'Comfort' ||
|
|
carType == 'Mishwar Vip' ||
|
|
carType == 'Lady')
|
|
? price.toStringAsFixed(2)
|
|
: totalPricePassenger;
|
|
}
|
|
paymentAmount = totalCost;
|
|
}
|
|
|
|
void cancelCheckRideFromPassenger() async {
|
|
var res = await CRUD().get(
|
|
link: "${AppLink.endPoint}/ride/driver_order/getOrderCancelStatus.php",
|
|
payload: {
|
|
'order_id': (rideId),
|
|
}); //.then((value) {
|
|
var response = jsonDecode(res);
|
|
canelString = response['data']['status'];
|
|
update();
|
|
if (canelString == 'Cancel') {
|
|
remainingTimeTimerRideBegin = 0;
|
|
remainingTimeToShowPassengerInfoWindowFromDriver = 0;
|
|
remainingTimeToPassenger = 0;
|
|
isRideStarted = false;
|
|
isRideFinished = false;
|
|
isPassengerInfoWindow = false;
|
|
clearPolyline();
|
|
update();
|
|
MyDialog().getDialog(
|
|
'Order Cancelled'.tr,
|
|
'Order Cancelled by Passenger'.tr,
|
|
() {
|
|
Get.offAll(HomeCaptain());
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
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 jitterMeters = 0.01; // 10 متر
|
|
|
|
_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() > jitterMeters) {
|
|
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 mediumDistThresholdKm = 25.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() {
|
|
// استخدم الماركر الذي يتحرك مع الخريطة
|
|
markers.removeWhere((m) => m.markerId.value == 'MyLocation');
|
|
final locCtrl = Get.find<LocationController>();
|
|
myLocation = locCtrl.myLocation;
|
|
|
|
markers.add(
|
|
Marker(
|
|
markerId: MarkerId('MyLocation'),
|
|
position: myLocation,
|
|
icon: carIcon,
|
|
rotation: locCtrl.heading,
|
|
anchor: const Offset(0.5, 0.5),
|
|
flat: true,
|
|
zIndex: 2,
|
|
),
|
|
);
|
|
update();
|
|
}
|
|
|
|
void addCustomCarIcon() {
|
|
ImageConfiguration config = ImageConfiguration(
|
|
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio);
|
|
BitmapDescriptor.asset(
|
|
config,
|
|
'assets/images/car.png',
|
|
// mipmaps: false,
|
|
).then((value) {
|
|
carIcon = value;
|
|
update();
|
|
});
|
|
}
|
|
|
|
void addCustomStartIcon() async {
|
|
// Create the marker with the resized image
|
|
|
|
ImageConfiguration config = ImageConfiguration(
|
|
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio);
|
|
BitmapDescriptor.asset(
|
|
config,
|
|
'assets/images/A.png',
|
|
).then((value) {
|
|
startIcon = value;
|
|
update();
|
|
});
|
|
}
|
|
|
|
void addCustomEndIcon() {
|
|
ImageConfiguration config = ImageConfiguration(
|
|
size: const Size(25, 25), devicePixelRatio: Get.pixelRatio);
|
|
BitmapDescriptor.asset(
|
|
config,
|
|
'assets/images/b.png',
|
|
).then((value) {
|
|
endIcon = value;
|
|
update();
|
|
});
|
|
}
|
|
|
|
void addCustomPassengerIcon() {
|
|
ImageConfiguration config = ImageConfiguration(
|
|
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio
|
|
// scale: 1.0,
|
|
);
|
|
BitmapDescriptor.asset(
|
|
config,
|
|
'assets/images/picker.png',
|
|
).then((value) {
|
|
passengerIcon = value;
|
|
update();
|
|
});
|
|
}
|
|
|
|
var activeRouteSteps = <Map<String, dynamic>>[];
|
|
var traveledPathPoints = <LatLng>[]; // المسار المقطوع (رمادي)
|
|
var upcomingPathPoints = <LatLng>[]; // المسار المتبقي (أزرق/أحمر)
|
|
|
|
// --- متغيرات الأيقونات والمواقع ---
|
|
var heading = 0.0;
|
|
// ... يمكنك إضافة أيقونات البداية والنهاية هنا
|
|
|
|
// --- متغيرات قياس الأداء الذكي ---
|
|
final List<int> _performanceReadings = [];
|
|
final int _readingsToCollect = 10; // اجمع 10 قراءات
|
|
bool _hasMadeDecision = false;
|
|
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');
|
|
// يمكنك تعيين AppLink.endPoint هنا إذا كان منطقك الداخلي لا يزال يعتمد عليه
|
|
// box.write(BoxName.serverChosen,
|
|
// AppLink.IntaleqSyriaServer); // مثال: اختر سيرفر سوريا للبيانات
|
|
return 'Jordan';
|
|
}
|
|
|
|
// 2. فحص سوريا
|
|
if (isPointInPolygon(passengerPoint, CountryPolygons.syriaBoundary)) {
|
|
box.write(BoxName.countryCode, 'Syria');
|
|
// box.write(BoxName.serverChosen, AppLink.IntaleqSyriaServer);
|
|
return 'Syria';
|
|
}
|
|
|
|
// 3. فحص مصر
|
|
if (isPointInPolygon(passengerPoint, CountryPolygons.egyptBoundary)) {
|
|
box.write(BoxName.countryCode, 'Egypt');
|
|
// box.write(BoxName.serverChosen, AppLink.IntaleqAlexandriaServer);
|
|
return 'Egypt';
|
|
}
|
|
|
|
// 4. الافتراضي (إذا كان خارج المناطق المخدومة)
|
|
box.write(BoxName.countryCode, 'Jordan');
|
|
// box.write(BoxName.serverChosen, AppLink.IntaleqSyriaServer);
|
|
return 'Unknown Location (Defaulting to Jordan)';
|
|
}
|
|
|
|
/// **جلب ورسم المسار (OSRM - New Standard System)**
|
|
///
|
|
/// تستخدم السيرفر الجديد: https://routesjo.intaleq.xyz/route/v1/driving
|
|
Future<void> getRoute({
|
|
required LatLng origin,
|
|
required LatLng destination,
|
|
required Color routeColor,
|
|
}) async {
|
|
// 1. استخدام الرابط الجديد والإعدادات الصحيحة
|
|
String coordinates =
|
|
'${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}';
|
|
// استخدام الرابط من الكلاس المرجعي لأنه أحدث
|
|
var url =
|
|
"https://routesjo.intaleq.xyz/route/v1/driving/$coordinates?steps=true&overview=full";
|
|
|
|
try {
|
|
var response = await http.get(Uri.parse(url));
|
|
|
|
if (response.statusCode == 200) {
|
|
var decoded = jsonDecode(response.body);
|
|
|
|
if (decoded['code'] != 'Ok' || (decoded['routes'] as List).isEmpty) {
|
|
mySnackeBarError("لم يتم العثور على مسار");
|
|
return;
|
|
}
|
|
|
|
var route = decoded['routes'][0];
|
|
|
|
// أ) تشغيل الـ Isolate لفك التشفير (ممتاز، ابقِ عليه)
|
|
final String pointsString = route["geometry"];
|
|
List<LatLng> fullRoute =
|
|
await compute(decodePolylineIsolate, pointsString);
|
|
|
|
// ب) تهيئة المتغيرات
|
|
upcomingPathPoints.assignAll(fullRoute);
|
|
traveledPathPoints.clear();
|
|
_lastTraveledIndex = 0;
|
|
|
|
// ج) رسم المسار الأولي
|
|
polyLines.clear();
|
|
polyLines.add(Polyline(
|
|
polylineId: const PolylineId("upcoming_route"),
|
|
points: fullRoute,
|
|
width: 8,
|
|
color: routeColor,
|
|
startCap: Cap.roundCap,
|
|
endCap: Cap.roundCap,
|
|
));
|
|
|
|
// د) معالجة الخطوات (Legs & Steps)
|
|
List<dynamic> legs = route['legs'];
|
|
if (legs.isNotEmpty) {
|
|
final stepsList = List<Map<String, dynamic>>.from(legs[0]['steps']);
|
|
|
|
// 🔥 استخدام دالة الترجمة المحسنة من الكلاس المرجعي
|
|
for (var step in stepsList) {
|
|
step['html_instructions'] =
|
|
_createInstructionFromManeuverSmart(step);
|
|
// تصحيح مواقع المناورات
|
|
if (step['maneuver'] != null &&
|
|
step['maneuver']['location'] != null) {
|
|
var loc = step['maneuver']['location'];
|
|
step['end_location'] = {'lat': loc[1], 'lng': loc[0]};
|
|
}
|
|
}
|
|
|
|
routeSteps = stepsList;
|
|
currentStepIndex = 0;
|
|
|
|
// نطق أول تعليمة
|
|
if (routeSteps.isNotEmpty) {
|
|
currentInstruction = routeSteps[0]['html_instructions'];
|
|
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
|
}
|
|
}
|
|
|
|
// هـ) تحريك الكاميرا لتشمل المسار
|
|
if (fullRoute.isNotEmpty) {
|
|
final bounds = _boundsFromLatLngList(fullRoute);
|
|
safeAnimateCamera(CameraUpdate.newLatLngBounds(bounds, 80));
|
|
}
|
|
|
|
update();
|
|
}
|
|
} catch (e) {
|
|
Log.print("Route Error: $e");
|
|
}
|
|
}
|
|
|
|
// 🔥 دالة الترجمة المحسنة (من NavigationController)
|
|
String _createInstructionFromManeuverSmart(Map<String, dynamic> step) {
|
|
if (step['maneuver'] == null) return "تابع المسير";
|
|
|
|
final maneuver = step['maneuver'];
|
|
final type = maneuver['type'] ?? 'continue';
|
|
final modifier = maneuver['modifier'] ?? 'straight';
|
|
final name = step['name'] ?? '';
|
|
|
|
String instruction = "";
|
|
|
|
switch (type) {
|
|
case 'depart':
|
|
instruction = "انطلق";
|
|
break;
|
|
case 'arrive':
|
|
return "لقد وصلت إلى وجهتك، $name";
|
|
case 'turn':
|
|
case 'fork':
|
|
case 'roundabout':
|
|
case 'merge':
|
|
case 'on ramp':
|
|
case 'off ramp':
|
|
case 'end of road':
|
|
instruction =
|
|
_getTurnInstruction(modifier); // استخدم نفس دالتك المساعدة هنا
|
|
break;
|
|
default:
|
|
instruction = "تابع المسير";
|
|
}
|
|
|
|
if (name.isNotEmpty) {
|
|
if (type == 'continue') {
|
|
instruction += " على $name";
|
|
} else {
|
|
instruction += " نحو $name";
|
|
}
|
|
}
|
|
return instruction;
|
|
}
|
|
|
|
String _createInstructionFromManeuver(Map<String, dynamic> step) {
|
|
final maneuver = step['maneuver'];
|
|
final type = maneuver['type'] ?? 'continue';
|
|
final modifier = maneuver['modifier'] ?? 'straight';
|
|
final name = step['name'] ?? '';
|
|
|
|
String instruction = "";
|
|
|
|
switch (type) {
|
|
case 'depart':
|
|
instruction = "انطلق";
|
|
break;
|
|
case 'arrive':
|
|
instruction = "لقد وصلت إلى وجهتك";
|
|
if (name.isNotEmpty) instruction += "، $name";
|
|
return instruction;
|
|
case 'turn':
|
|
case 'fork':
|
|
case 'off ramp':
|
|
case 'on ramp':
|
|
case 'roundabout':
|
|
instruction = _getTurnInstruction(modifier);
|
|
break;
|
|
case 'continue':
|
|
instruction = "استمر";
|
|
break;
|
|
default:
|
|
instruction = "اتجه";
|
|
}
|
|
|
|
if (name.isNotEmpty) {
|
|
if (instruction == "استمر") {
|
|
instruction += " على $name";
|
|
} else {
|
|
instruction += " إلى $name";
|
|
}
|
|
} else if (type == 'continue' && modifier == 'straight') {
|
|
instruction = "استمر بشكل مستقيم";
|
|
}
|
|
|
|
return instruction;
|
|
}
|
|
|
|
/**
|
|
* دالة مساعدة لترجمة تعليمات الانعطاف
|
|
*/
|
|
String _getTurnInstruction(String modifier) {
|
|
switch (modifier) {
|
|
case 'uturn':
|
|
return "قم بالاستدارة والعودة";
|
|
case 'sharp right':
|
|
return "انعطف يمينًا بحدة";
|
|
case 'right':
|
|
return "انعطف يمينًا";
|
|
case 'slight right':
|
|
return "انعطف يمينًا قليلاً";
|
|
case 'straight':
|
|
return "استمر بشكل مستقيم";
|
|
case 'slight left':
|
|
return "انعطف يسارًا قليلاً";
|
|
case 'left':
|
|
return "انعطف يسارًا";
|
|
case 'sharp left':
|
|
return "انعطف يسارًا بحدة";
|
|
default:
|
|
return "اتجه";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* دالة لحساب حدود الخريطة (Bounds) من قائمة نقاط
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
return LatLngBounds(
|
|
northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!));
|
|
}
|
|
|
|
// الدالة التي يتم استدعاؤها من خدمة الموقع كل 5 ثوان (أو حسب الفترة المحددة)
|
|
void onLocationUpdated(Position newPosition) {
|
|
myLocation = LatLng(newPosition.latitude, newPosition.longitude);
|
|
heading = newPosition.heading;
|
|
|
|
// -->> منطق قياس الأداء يبدأ هنا <<--
|
|
final stopwatch = Stopwatch()..start();
|
|
|
|
// -->> منطق الملاحة وتحديث المسار <<--
|
|
_onLocationTick(myLocation);
|
|
|
|
stopwatch.stop();
|
|
|
|
// -->> تحليل الأداء واتخاذ القرار <<--
|
|
if (!_hasMadeDecision) {
|
|
_performanceReadings.add(stopwatch.elapsedMilliseconds);
|
|
if (_performanceReadings.length >= _readingsToCollect) {
|
|
_analyzePerformance();
|
|
_hasMadeDecision = true;
|
|
}
|
|
}
|
|
}
|
|
// =================================================================
|
|
// 3. منطق الملاحة الداخلي (Internal Navigation Logic)
|
|
// =================================================================
|
|
|
|
void _onLocationTick(LatLng pos) {
|
|
if (activeRouteSteps.isEmpty || currentStepIndex >= _stepBounds.length) {
|
|
return;
|
|
}
|
|
|
|
final double dToEnd =
|
|
_distanceMeters(pos, _stepEndPoints[currentStepIndex]);
|
|
if (dToEnd <= 35) {
|
|
// 35 متر عتبة للوصول لنهاية الخطوة
|
|
_advanceStep();
|
|
}
|
|
}
|
|
|
|
void _advanceStep() {
|
|
if (currentStepIndex >= _stepBounds.length - 1) {
|
|
// وصل للنهاية
|
|
currentInstruction = "لقد وصلت إلى وجهتك";
|
|
return;
|
|
}
|
|
|
|
currentStepIndex++;
|
|
currentInstruction = _parseInstruction(
|
|
activeRouteSteps[currentStepIndex]['html_instructions']);
|
|
Get.isRegistered<TextToSpeechController>()
|
|
? Get.find<TextToSpeechController>().speakText(currentInstruction)
|
|
: Get.put(TextToSpeechController()).speakText(currentInstruction);
|
|
|
|
// -->> هنا يتم تحديث لون المسار <<--
|
|
_updateTraveledPath();
|
|
|
|
_fitToBounds(_stepBounds[currentStepIndex],
|
|
padding: 80); // تقريب الكاميرا على الخطوة التالية
|
|
update();
|
|
}
|
|
// داخل MapDriverController
|
|
|
|
Future<void> markDriverAsArrived() async {
|
|
// 1. إظهار لودينج فوراً لمنع التكرار وإشعار السائق
|
|
Get.dialog(
|
|
const Center(
|
|
child: CircularProgressIndicator(
|
|
color: AppColor.gold,
|
|
)),
|
|
barrierDismissible: false);
|
|
|
|
try {
|
|
double distance =
|
|
await calculateDistanceBetweenDriverAndPassengerLocation();
|
|
|
|
if (distance < 100) {
|
|
// 2. طلب الـ API
|
|
await CRUD()
|
|
.post(link: "${AppLink.ride}/rides/arrive_ride.php", payload: {
|
|
"ride_id": rideId,
|
|
"driver_id": box.read(BoxName.driverID),
|
|
"passengerToken": tokenPassenger
|
|
});
|
|
|
|
// 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("يجب أن تكون أقرب من 100 متر للوصول");
|
|
}
|
|
} catch (e) {
|
|
if (Get.isDialogOpen == true) Get.back();
|
|
mySnackeBarError("حدث خطأ في الاتصال");
|
|
}
|
|
}
|
|
|
|
void _updateTraveledPath() {
|
|
// استخراج كل النقاط للخطوات التي تم اجتيازها
|
|
List<LatLng> pointsForTraveledSteps = [];
|
|
for (int i = 0; i < currentStepIndex; i++) {
|
|
final stepPolyline = activeRouteSteps[i]['polyline']['points'];
|
|
pointsForTraveledSteps.addAll(decodePolylineToLatLng(stepPolyline));
|
|
}
|
|
traveledPathPoints.assignAll(pointsForTraveledSteps);
|
|
}
|
|
|
|
void _prepareStepData(List<Map<String, dynamic>> steps) {
|
|
_stepBounds.clear();
|
|
_stepEndPoints.clear();
|
|
|
|
for (final s in steps) {
|
|
// 1. استخراج نقطة النهاية (الكود الحالي سليم)
|
|
final end = s['end_location'];
|
|
_stepEndPoints.add(LatLng(
|
|
(end['lat'] as num).toDouble(),
|
|
(end['lng'] as num).toDouble(),
|
|
));
|
|
|
|
// 2. فك تشفير البوليلاين الخاص بالخطوة وتحويله إلى LatLng
|
|
// -->> هنا تم التصحيح <<--
|
|
List<LatLng> pts = decodePolyline(s['polyline']['points'])
|
|
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
|
|
.toList();
|
|
|
|
// أضف نقاط البداية والنهاية إذا لم تكن موجودة في البوليلاين لضمان دقة الحدود
|
|
if (pts.isNotEmpty) {
|
|
final start = s['start_location'];
|
|
final startLatLng = LatLng(
|
|
(start['lat'] as num).toDouble(), (start['lng'] as num).toDouble());
|
|
if (pts.first != startLatLng) {
|
|
pts.insert(0, startLatLng);
|
|
}
|
|
}
|
|
|
|
_stepBounds.add(_boundsFromPoints(pts));
|
|
}
|
|
}
|
|
|
|
// A helper function to decode and convert the polyline string
|
|
List<LatLng> decodePolylineToLatLng(String polylineString) {
|
|
// 1. Decode the string into a list of number lists (e.g., [[lat, lng], ...])
|
|
List<List<num>> decodedPoints = decodePolyline(polylineString);
|
|
|
|
// 2. Map each [lat, lng] pair to a LatLng object, ensuring conversion to double
|
|
List<LatLng> latLngPoints = decodedPoints
|
|
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
|
|
.toList();
|
|
|
|
return latLngPoints;
|
|
}
|
|
|
|
// =================================================================
|
|
// 4. منطق الأداء الذكي (Smart Performance Logic)
|
|
// =================================================================
|
|
void _analyzePerformance() {
|
|
final int sum = _performanceReadings.reduce((a, b) => a + b);
|
|
final double averageTime = sum / _performanceReadings.length;
|
|
if (averageTime > 1000) {
|
|
// إذا كانت العملية تستغرق أكثر من ثانية
|
|
_suggestOptimization();
|
|
}
|
|
}
|
|
|
|
void _suggestOptimization() {
|
|
Get.snackbar(
|
|
"تحسين أداء التطبيق",
|
|
"لضمان أفضل تجربة، نقترح تعديل الإعدادات لتناسب جهازك. هل تود المتابعة؟",
|
|
duration: const Duration(seconds: 15),
|
|
mainButton: TextButton(
|
|
child: const Text("نعم، قم بالتحسين"),
|
|
onPressed: () {
|
|
updateInterval.value = 8; // غير الفترة إلى 8 ثوانٍ
|
|
// save setting to shared_preferences
|
|
box.write(BoxName.updateInterval, 8);
|
|
Get.back();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// =================================================================
|
|
// 5. دوال مساعدة (Helper Functions)
|
|
// =================================================================
|
|
void _resetRouteState() {
|
|
activeRouteSteps.clear();
|
|
traveledPathPoints.clear();
|
|
upcomingPathPoints.clear();
|
|
_allPointsForActiveRoute.clear();
|
|
currentStepIndex = 0;
|
|
}
|
|
|
|
String _parseInstruction(String html) =>
|
|
html.replaceAll(RegExp(r'<[^>]*>'), '');
|
|
|
|
Future<void> _fitToBounds(LatLngBounds b, {double padding = 60}) async {
|
|
// نستخدم الدالة الآمنة التي أنشأناها
|
|
await safeAnimateCamera(CameraUpdate.newLatLngBounds(b, padding));
|
|
}
|
|
|
|
double distanceBetweenDriverAndPassengerWhenConfirm = 0;
|
|
|
|
// فحص التعليمات الصوتية
|
|
void checkForNextStep(LatLng currentPosition) {
|
|
if (currentStepIndex >= routeSteps.length) return;
|
|
|
|
final step = routeSteps[currentStepIndex];
|
|
final endLocation = step['end_location'];
|
|
final endLatLng = LatLng(endLocation['lat'], endLocation['lng']);
|
|
|
|
final distance = Geolocator.distanceBetween(
|
|
currentPosition.latitude,
|
|
currentPosition.longitude,
|
|
endLatLng.latitude,
|
|
endLatLng.longitude,
|
|
);
|
|
|
|
if (distance < 50) {
|
|
// 50 متر
|
|
currentStepIndex++;
|
|
if (currentStepIndex < routeSteps.length) {
|
|
currentInstruction = routeSteps[currentStepIndex]['html_instructions'];
|
|
playVoiceInstruction(currentInstruction);
|
|
update();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
|
|
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),
|
|
);
|
|
}
|
|
|
|
bool _contains(LatLngBounds b, LatLng p) {
|
|
final south = math.min(b.southwest.latitude, b.northeast.latitude);
|
|
final north = math.max(b.southwest.latitude, b.northeast.latitude);
|
|
final west = math.min(b.southwest.longitude, b.northeast.longitude);
|
|
final east = math.max(b.southwest.longitude, b.northeast.longitude);
|
|
return (p.latitude >= south &&
|
|
p.latitude <= north &&
|
|
p.longitude >= west &&
|
|
p.longitude <= east);
|
|
}
|
|
|
|
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);
|
|
|
|
// Fit the camera to the bounds
|
|
var cameraUpdate = CameraUpdate.newLatLngBounds(boundsData, 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 {
|
|
try {
|
|
passengerLocation = Get.arguments['passengerLocation'];
|
|
passengerDestination = Get.arguments['passengerDestination'];
|
|
duration = Get.arguments['Duration'];
|
|
totalCost = Get.arguments['totalCost'];
|
|
passengerId = Get.arguments['passengerId'];
|
|
driverId = Get.arguments['driverId'];
|
|
distance = Get.arguments['Distance'];
|
|
passengerName = Get.arguments['name'];
|
|
passengerEmail = Get.arguments['email'];
|
|
totalPricePassenger = Get.arguments['totalPassenger'];
|
|
passengerPhone = Get.arguments['phone'];
|
|
walletChecked = Get.arguments['WalletChecked'];
|
|
tokenPassenger = Get.arguments['tokenPassenger'];
|
|
direction = Get.arguments['direction'];
|
|
durationToPassenger = Get.arguments['DurationToPassenger'];
|
|
rideId = Get.arguments['rideId'];
|
|
durationOfRideValue = Get.arguments['durationOfRideValue'];
|
|
paymentAmount = Get.arguments['paymentAmount'];
|
|
paymentMethod = Get.arguments['paymentMethod'];
|
|
isHaveSteps = Get.arguments['isHaveSteps'];
|
|
step0 = Get.arguments['step0'];
|
|
step1 = Get.arguments['step1'];
|
|
step2 = Get.arguments['step2'];
|
|
step3 = Get.arguments['step3'];
|
|
step4 = Get.arguments['step4'];
|
|
passengerWalletBurc = Get.arguments['passengerWalletBurc'];
|
|
timeOfOrder = Get.arguments['timeOfOrder'];
|
|
carType = Get.arguments['carType'];
|
|
kazan = Get.arguments['kazan'];
|
|
startNameLocation = Get.arguments['startNameLocation'];
|
|
endNameLocation = Get.arguments['endNameLocation'];
|
|
|
|
// Parse to double
|
|
latlng(passengerLocation, passengerDestination);
|
|
|
|
String lat =
|
|
Get.find<LocationController>().myLocation.latitude.toString();
|
|
String lng =
|
|
Get.find<LocationController>().myLocation.longitude.toString();
|
|
String origin = '$lat,$lng';
|
|
// Set the origin and destination coordinates for the Google Maps directions request.
|
|
Future.delayed(const Duration(seconds: 1));
|
|
getRoute(
|
|
origin: Get.find<LocationController>().myLocation,
|
|
destination: latLngPassengerLocation,
|
|
routeColor: Colors.yellow // أو أي لون
|
|
);
|
|
update();
|
|
} catch (e) {
|
|
Log.print("Error parsing arguments: $e");
|
|
}
|
|
}
|
|
|
|
latlng(String passengerLocation, passengerDestination) {
|
|
double latPassengerLocation =
|
|
double.parse(passengerLocation.toString().split(',')[0]);
|
|
double lngPassengerLocation =
|
|
double.parse(passengerLocation.toString().split(',')[1]);
|
|
double latPassengerDestination =
|
|
double.parse(passengerDestination.toString().split(',')[0]);
|
|
double lngPassengerDestination =
|
|
double.parse(passengerDestination.toString().split(',')[1]);
|
|
latLngPassengerLocation =
|
|
LatLng(latPassengerLocation, lngPassengerLocation);
|
|
latLngPassengerDestination =
|
|
LatLng(latPassengerDestination, lngPassengerDestination);
|
|
}
|
|
|
|
late Duration durationToAdd;
|
|
int hours = 0;
|
|
int minutes = 0;
|
|
String carType = '';
|
|
late String kazan;
|
|
late String startNameLocation;
|
|
late 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);
|
|
// Get the passenger location from the arguments.
|
|
await argumentLoading();
|
|
Get.put(FirebaseMessagesController());
|
|
runGoogleMapDirectly();
|
|
addCustomCarIcon();
|
|
addCustomPassengerIcon();
|
|
addCustomStartIcon();
|
|
addCustomEndIcon();
|
|
if (!Get.isRegistered<TextToSpeechController>()) {
|
|
Get.put(TextToSpeechController(), permanent: true);
|
|
// permanent: true تمنع حذفه عند تغيير الصفحات
|
|
}
|
|
// updateMarker();
|
|
// updateLocation();
|
|
startTimerToShowPassengerInfoWindowFromDriver();
|
|
// durationToAdd = Duration(seconds: int.parse(duration));
|
|
durationToAdd = Duration(seconds: parseDurationToInt(duration));
|
|
hours = durationToAdd.inHours;
|
|
minutes = (durationToAdd.inMinutes % 60).round();
|
|
calculateConsumptionFuel();
|
|
// updateLocation();// for now to test it
|
|
// cancelCheckRidefromPassenger();
|
|
// checkIsDriverNearPassenger();
|
|
super.onInit();
|
|
}
|
|
|
|
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;
|
|
|
|
// Future<void> startListeningStepNavigation() async {
|
|
// _posSub?.cancel();
|
|
// _navigationTimer?.cancel();
|
|
|
|
// _posSub = Geolocator.getPositionStream(
|
|
// locationSettings: const LocationSettings(
|
|
// accuracy: LocationAccuracy.high,
|
|
// distanceFilter: 5, // قللناها لـ 5 أمتار لدقة أعلى
|
|
// ),
|
|
// ).listen((position) {
|
|
// LatLng newLoc = LatLng(position.latitude, position.longitude);
|
|
|
|
// // 🔥 Jitter Filter: تجاهل التحركات الطفيفة جداً (أقل من 2 متر)
|
|
// if (_lastRecordedLocation != null) {
|
|
// double dist = Geolocator.distanceBetween(
|
|
// newLoc.latitude,
|
|
// newLoc.longitude,
|
|
// _lastRecordedLocation!.latitude,
|
|
// _lastRecordedLocation!.longitude);
|
|
// if (dist < 2.0) return;
|
|
// }
|
|
|
|
// _lastRecordedLocation = newLoc;
|
|
// myLocation = newLoc;
|
|
// heading = position.heading;
|
|
|
|
// if (!isClosed) {
|
|
// // نحدث الماركر فوراً
|
|
// updateMarker();
|
|
|
|
// // 🔥 تحديث المسار بذكاء (Smart Route Snapping)
|
|
// if (upcomingPathPoints.isNotEmpty) {
|
|
// _updateTraveledPolylineSmart(myLocation);
|
|
// }
|
|
// // 🔥🔥 الجزء الأهم: تحريك الكاميرا لتتبع السيارة 🔥🔥
|
|
// if (mapController != null) {
|
|
// _animateCameraToNavigationMode(position);
|
|
// }
|
|
// // التحقق من الخطوة التالية
|
|
// checkForNextStep(myLocation);
|
|
|
|
// update();
|
|
// }
|
|
// });
|
|
// }
|
|
|
|
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;
|
|
|
|
// Sliding Window: نبحث فقط في الـ 60 نقطة القادمة
|
|
int searchWindow = 60;
|
|
int startIndex = _lastTraveledIndex;
|
|
int endIndex = min(startIndex + searchWindow, upcomingPathPoints.length);
|
|
|
|
double minDistance = double.infinity;
|
|
int closestIndex = startIndex;
|
|
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,
|
|
color: Colors.blue,
|
|
width: 8,
|
|
zIndex: 2,
|
|
startCap: Cap.roundCap,
|
|
endCap: Cap.roundCap,
|
|
));
|
|
|
|
// المسار المقطوع (رمادي)
|
|
polyLines.add(Polyline(
|
|
polylineId: const PolylineId('traveled_route'),
|
|
points: traveled,
|
|
color: Colors.grey.withOpacity(0.8),
|
|
width: 7,
|
|
zIndex: 1,
|
|
));
|
|
|
|
update();
|
|
}
|
|
}
|
|
|
|
void stopListeningStepNavigation() {
|
|
_posSub?.cancel();
|
|
_posSub = null;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|