Files
intaleq/lib/controller/home/map_passenger_controller.dart

7877 lines
284 KiB
Dart

import 'dart:async';
import 'package:Intaleq/services/offline_map_service.dart';
import 'package:Intaleq/services/emergency_signal_service.dart';
import 'dart:convert';
import 'dart:io';
import 'dart:math' show Random, atan2, cos, max, min, pi, pow, sin, sqrt;
import 'dart:math' as math;
import 'dart:ui';
import 'dart:typed_data';
import 'package:image/image.dart' as img;
import 'package:Intaleq/services/ride_live_notification.dart';
import 'package:crypto/crypto.dart';
import 'package:Intaleq/views/Rate/rate_captain.dart';
import 'package:Intaleq/views/Rate/rating_driver_bottom.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:Intaleq/constant/univeries_polygon.dart';
import 'package:Intaleq/controller/firebase/local_notification.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_confetti/flutter_confetti.dart' hide Circle;
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:vector_math/vector_math.dart' show radians;
import 'package:Intaleq/controller/functions/tts.dart';
import 'package:Intaleq/views/home/map_page_passenger.dart';
import 'package:Intaleq/views/widgets/my_textField.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
// import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
import 'package:intl/intl.dart';
import 'package:location/location.dart';
import 'package:Intaleq/constant/country_polygons.dart';
import 'package:Intaleq/constant/links.dart';
import 'package:Intaleq/constant/style.dart';
import 'package:Intaleq/controller/home/points_for_rider_controller.dart';
import 'package:Intaleq/views/home/map_widget.dart/form_serch_multiy_point.dart';
import '../../constant/api_key.dart';
import '../../constant/box_name.dart';
import '../../constant/colors.dart';
import '../../constant/info.dart';
import '../../constant/table_names.dart';
import '../../env/env.dart';
import '../../main.dart';
import '../../models/model/locations.dart';
import '../../models/model/painter_copoun.dart';
import '../../print.dart';
import '../../services/pip_service.dart';
import '../../views/home/map_widget.dart/cancel_raide_page.dart';
import '../../views/home/map_widget.dart/car_details_widget_to_go.dart';
import '../../views/home/map_widget.dart/select_driver_mishwari.dart';
import '../../views/widgets/elevated_btn.dart';
import '../../views/widgets/error_snakbar.dart';
import '../../views/widgets/mydialoug.dart';
import '../firebase/firbase_messge.dart';
import '../firebase/notification_service.dart';
import '../functions/audio_record1.dart';
import '../functions/crud.dart';
import '../functions/launch.dart';
import '../functions/package_info.dart';
import '../functions/secure_storage.dart';
import '../payment/payment_controller.dart';
import 'decode_polyline_isolate.dart';
import 'deep_link_controller.dart';
import 'device_performance.dart';
import 'ios_live_activity_service.dart';
import 'vip_waitting_page.dart';
enum RideState {
noRide, // لا يوجد رحلة جارية، عرض واجهة البحث
cancelled, // تم إلغاء الرحلة
preCheckReview, // يوجد رحلة منتهية، تحقق من التقييم
searching, // جاري البحث عن كابتن
driverApplied, // تم قبول الطلب
driverArrived, // وصل السائق
inProgress, // الرحلة بدأت بالفعل
finished, // انتهت الرحلة (سيتم تحويلها إلى preCheckReview)
}
class MapPassengerController extends GetxController {
bool isLoading = true;
TextEditingController placeDestinationController = TextEditingController();
TextEditingController increasFeeFromPassenger = TextEditingController();
TextEditingController placeStartController = TextEditingController();
TextEditingController wayPoint0Controller = TextEditingController();
TextEditingController wayPoint1Controller = TextEditingController();
TextEditingController wayPoint2Controller = TextEditingController();
TextEditingController wayPoint3Controller = TextEditingController();
TextEditingController wayPoint4Controller = TextEditingController();
TextEditingController sosPhonePassengerProfile = TextEditingController();
TextEditingController whatsAppLocationText = TextEditingController();
TextEditingController messageToDriver = TextEditingController();
final sosFormKey = GlobalKey<FormState>();
final promoFormKey = GlobalKey<FormState>();
final messagesFormKey = GlobalKey<FormState>();
final increaseFeeFormKey = GlobalKey<FormState>();
List data = [];
List<LatLng> bounds = [];
List placesStart = [];
List<String> driversToken = [];
LatLng previousLocationOfDrivers = const LatLng(0, 0);
double angleDegrees = 0;
LatLng currentLocationOfDrivers = const LatLng(0, 0);
List<TextEditingController> allTextEditingPlaces = [];
List placesDestination = [];
List wayPoint0 = [];
List wayPoint1 = [];
List wayPoint2 = [];
List wayPoint3 = [];
List wayPoint4 = [];
final firebaseMessagesController =
Get.isRegistered<FirebaseMessagesController>()
? Get.find<FirebaseMessagesController>()
: Get.put(FirebaseMessagesController());
List<List<dynamic>> placeListResponseAll = [];
List<Widget> placeListResponse = [
formSearchPlaces(0),
formSearchPlaces(1),
formSearchPlaces(2),
formSearchPlaces(3),
];
IntaleqMapController? mapController;
bool isStyleLoaded = false;
Set<Marker> markers = {};
Set<Polyline> polyLines = {};
Set<Polygon> polygons = {};
Set<Circle> circles = {};
double speed = 0;
PermissionStatus? permissionGranted;
LatLngBounds? lastComputedBounds;
late LatLng passengerLocation = const LatLng(32, 34);
late LatLng newMyLocation = const LatLng(32.115295, 36.064773);
late LatLng newStartPointLocation = const LatLng(32.115295, 36.064773);
late LatLng newPointLocation0 = const LatLng(32.115295, 36.064773);
late LatLng newPointLocation1 = const LatLng(32.115295, 36.064773);
late LatLng newPointLocation2 = const LatLng(32.115295, 36.064773);
late LatLng newPointLocation3 = const LatLng(32.115295, 36.064773);
late LatLng newPointLocation4 = const LatLng(32.115295, 36.064773);
late LatLng myDestination;
List<LatLng> polylineCoordinates = [];
List<LatLng> polylineCoordinates0 = [];
List<LatLng> polylineCoordinates1 = [];
List<LatLng> polylineCoordinates2 = [];
List<LatLng> polylineCoordinates3 = [];
List<LatLng> polylineCoordinates4 = [];
List<List<LatLng>> polylineCoordinatesPointsAll = [];
List carsLocationByPassenger = [];
List<LatLng> driverCarsLocationToPassengerAfterApplied = [];
String markerIcon = "marker_icon";
String tripIcon = "trip_icon";
String startIcon = "start_icon";
String endIcon = "end_icon";
String carIcon = "car_icon";
String motoIcon = "moto_icon";
String ladyIcon = "lady_icon";
double height = 150;
DateTime currentTime = DateTime.now();
final location = Location();
late LocationData currentLocation;
double heightMenu = 0;
double widthMenu = 0;
double heightPickerContainer = 90;
double heightPointsPageForRider = 0;
double mainBottomMenuMapHeight = Get.height * .2;
double wayPointSheetHeight = 0;
String stringRemainingTimeToPassenger = '';
String stringRemainingTimeDriverWaitPassenger5Minute = '';
bool isDriverInPassengerWay = false;
bool isDriverArrivePassenger = false;
bool startLocationFromMap = false;
bool isAnotherOreder = false;
bool isWhatsAppOrder = false;
bool passengerStartLocationFromMap = false;
bool workLocationFromMap = false;
bool homeLocationFromMap = false;
bool isPassengerRideLocationWidget = false;
bool startLocationFromMap0 = false;
bool startLocationFromMap1 = false;
bool startLocationFromMap2 = false;
bool startLocationFromMap3 = false;
bool startLocationFromMap4 = false;
List startLocationFromMapAll = [];
double latePrice = 0;
double fuelPrice = 0;
double heavyPrice = 0;
double naturePrice = 0;
bool heightMenuBool = false;
String statusRide = 'wait';
String statusRideVip = 'wait';
bool statusRideFromStart = false;
bool isPickerShown = false;
bool isPointsPageForRider = false;
bool isBottomSheetShown = false;
bool mapType = false;
bool reloadStartApp = false;
bool mapTrafficON = false;
bool isCancelRidePageShown = false;
bool isCashConfirmPageShown = false;
bool isPaymentMethodPageShown = false;
bool isRideFinished = false;
bool rideConfirm = false;
bool isMarkersShown = false;
bool isMainBottomMenuMap = true;
Timer? markerReloadingTimer2 = Timer(Duration.zero, () {});
Timer? markerReloadingTimer1 = Timer(Duration.zero, () {});
int durationToPassenger = 0;
bool isWayPointSheet = false;
bool isWayPointStopsSheet = false;
bool isWayPointStopsSheetUtilGetMap = false;
double heightBottomSheetShown = 0;
double cashConfirmPageShown = 250;
late String driverId = '';
late String gender = '';
double widthMapTypeAndTraffic = 50;
double paymentPageShown = Get.height * .6;
late LatLng southwest;
late LatLng northeast;
List<CarLocationModel> carLocationsModels = <CarLocationModel>[];
var dataCarsLocationByPassenger;
var datadriverCarsLocationToPassengerAfterApplied;
CarLocation? nearestCar;
late Timer? markerReloadingTimer = Timer(Duration.zero, () {});
bool shouldFetch = true; // Flag to determine if fetch should be executed
int selectedPassengerCount = 1;
double progress = 0;
double progressTimerToPassengerFromDriverAfterApplied = 0;
double progressTimerDriverWaitPassenger5Minute = 0;
int durationTimer = 9;
int durationToRide = 0;
int remainingTime = 25;
int remainingTimeToPassengerFromDriverAfterApplied = 60;
int remainingTimeDriverWaitPassenger5Minute = 60;
int timeToPassengerFromDriverAfterApplied = 0;
Timer? timerToPassengerFromDriverAfterApplied;
bool rideTimerBegin = false;
double progressTimerRideBegin = 0;
int remainingTimeTimerRideBegin = 60;
String stringRemainingTimeRideBegin = '';
String hintTextStartPoint = 'Search for your Start point'.tr;
String hintTextwayPoint0 = 'Search for waypoint'.tr;
String hintTextwayPoint1 = 'Search for waypoint'.tr;
String hintTextwayPoint2 = 'Search for waypoint'.tr;
String hintTextwayPoint3 = 'Search for waypoint'.tr;
String hintTextwayPoint4 = 'Search for waypoint'.tr;
String currentLocationString = 'Current Location'.tr;
String currentLocationString0 = 'Current Location'.tr;
String currentLocationString1 = 'Add Location 1'.tr;
String currentLocationString2 = 'Add Location 2'.tr;
String currentLocationString3 = 'Add Location 3'.tr;
String currentLocationString4 = 'Add Location 4'.tr;
String placesCoordinate0 = ''.tr;
String placesCoordinate1 = ''.tr;
String placesCoordinate2 = ''.tr;
String placesCoordinate3 = ''.tr;
String placesCoordinate4 = ''.tr;
List<String> currentLocationStringAll = [];
List<String> hintTextwayPointStringAll = [];
var placesCoordinate = <String>[];
String hintTextDestinationPoint = 'Select your destination'.tr;
late String rideId = 'yet';
bool noCarString = false;
bool isCashSelectedBeforeConfirmRide = false;
bool isPassengerChosen = false;
bool isSearchingWindow = false;
bool currentLocationToFormPlaces = false;
bool currentLocationToFormPlaces0 = false;
bool currentLocationToFormPlaces1 = false;
bool currentLocationToFormPlaces2 = false;
bool currentLocationToFormPlaces3 = false;
bool currentLocationToFormPlaces4 = false;
List currentLocationToFormPlacesAll = [];
// ── Multi-Waypoint (max 2 stops) ──────────────────────────────────────────
List<LatLng?> menuWaypoints = [null, null];
List<String> menuWaypointNames = ['', ''];
int activeMenuWaypointCount = 0;
bool isPickingWaypoint = false;
int pickingWaypointIndex = -1;
late String driverToken = '';
int carsOrder = 0;
int wayPointIndex = 0;
late double kazan = 8;
String? mapAPIKEY;
late double totalME = 0;
late double tax = 0;
late double totalPassenger = 0;
late double totalCostPassenger = 0;
late double totalPassengerComfort = 0;
late double totalPassengerComfortDiscount = 0;
late double totalPassengerElectricDiscount = 0;
late double totalPassengerLadyDiscount = 0;
late double totalPassengerSpeedDiscount = 0;
late double totalPassengerBalashDiscount = 0;
late double totalPassengerRaihGaiDiscount = 0;
late double totalPassengerScooter = 0;
late double totalPassengerVan = 0;
late double totalDriver = 0;
late double averageDuration = 0;
late double costDuration = 0;
late double costDistance = 0;
late double distance = 0;
late double duration = 0;
bool _isDriverAppliedLogicExecuted = false; // فلاج لمنع التنفيذ المتكرر
bool _isDriverArrivedLogicExecuted = false;
bool _isRideBeginLogicExecuted = false;
DateTime? _searchStartTime; // لتتبع مدة البحث
DateTime? _lastDriversNotifyTime; // لتتبع آخر مرة تم إرسال إشعار للسائقين
final int _masterTimerIntervalSeconds = 5; // فاصل زمني ثابت للمؤقت الرئيسي
final int _searchTimeoutSeconds = 60; // مهلة البحث قبل عرض خيار زيادة السعر
final int _notifyDriversIntervalSeconds =
25; // إرسال إشعار للسائقين كل 25 ثانية
// متغير لمنع أي عمليات تحديث أثناء التقييم
bool _isRatingScreenOpen = false;
// --- إضافة جديدة: متغيرات لإدارة البحث المتوسع ---
int _currentSearchPhase = 0; // لتتبع المرحلة الحالية للبحث
bool _isFetchingDriverLocation = false; // متغير لمنع تكرار الطلب
// === استبدل initSocket بالكامل ===
late IO.Socket socket;
bool isSocketConnected = false;
int _reconnectAttempts = 0;
final int _maxReconnectAttempts = 5;
Timer? _reconnectTimer;
var currentRideId;
// لتخزين نقاط مسار السائق الحالية للمقارنة
List<LatLng> _currentDriverRoutePoints = [];
final Map<RideState, int> _pollingIntervals = {
RideState.noRide: 6,
RideState.searching: 5,
RideState.driverApplied: 10,
RideState.driverArrived: 8,
RideState.inProgress: 6,
RideState.cancelled: 3600,
RideState.finished: 3600,
RideState.preCheckReview: 3600,
};
// لمنع التكرار (عشان ما يعمل 100 طلب في نفس اللحظة)
bool _isRecalculatingRoute = false;
// متغير لمراقبة صحة السوكيت
DateTime? _lastSocketLocationTime;
// مسافة السماحية (مثلاً 150 متر) قبل اعتبار السائق "خارج المسار"
final double _deviationThresholdMeters = 150.0;
// ... (باقي الـ Imports)
// متغيرات التحكم
Timer? _locationPollingTimer; // تايمر مخصص للموقع فقط
// ==============================================================================
// 1. الدالة الرئيسية لتأسيس الاتصال (تستدعى عند بدء البحث startSearchingForDriver)
// ==============================================================================
Timer? _heartbeatTimer;
void initConnectionWithSocket() {
if (isSocketConnected && socket != null) return;
String passengerId = box.read(BoxName.passengerID).toString();
Log.print("🔌 Initializing Socket for Passenger: $passengerId");
socket = IO.io(
AppLink.serverSocket,
IO.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.setQuery({'id': passengerId})
// ✅ [FIX] إعادة اتصال شبه-لانهائية (999 محاولة) بدلاً من 20
.setReconnectionAttempts(20)
// ✅ [FIX] تأخير أقل (1.5 ثانية) مع حد أقصى (8 ثواني) للتسريع
.setReconnectionDelay(1500)
.setReconnectionDelayMax(8000)
.enableReconnection()
.setExtraHeaders({'Connection': 'Upgrade'})
.build(),
);
socket.connect();
// ✅ معالج الاتصال الأول
socket.onConnect((_) {
Log.print("✅ Socket Connected Successfully");
isSocketConnected = true;
_reconnectAttempts = 0;
_startHeartbeat();
// ✅ [FIX] الاشتراك مجدداً في أحداث الرحلة عند كل اتصال
if (rideId != null && rideId != 'yet' && driverId.isNotEmpty) {
socket.emit('subscribe_driver_location', {
'ride_id': rideId,
'driver_id': driverId,
});
Log.print("📡 Re-subscribed to driver location after connect");
}
update();
});
// ⚠️ معالج الانقطاع
socket.onDisconnect((_) {
Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it");
isSocketConnected = false;
// تفعيل Polling أسرع كـ Fallback مؤقت (سيتم إيقافه عند عودة الاتصال)
if (_isActiveRideState()) {
Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect...");
_startMasterTimerWithInterval(4);
}
update();
});
// 🔁 [FIX] معالج إعادة الاتصال الناجحة
socket.onReconnect((_) {
Log.print("🔁 Socket Reconnected Successfully!");
isSocketConnected = true;
_reconnectAttempts = 0;
// استئناف النبضة فوراً
_startHeartbeat();
// إعادة الاشتراك في أحداث الرحلة
if (rideId != null && rideId != 'yet' && driverId.isNotEmpty) {
socket.emit('subscribe_driver_location', {
'ride_id': rideId,
'driver_id': driverId,
});
Log.print("📡 Re-subscribed to driver location after reconnect");
}
// ✅ [FIX] إيقاف الـ Fast Polling لأن السوكيت عاد
if (_isActiveRideState()) {
Log.print("✅ Socket back online — stopping Fast Polling Fallback");
_masterTimer?.cancel();
_masterTimer = null;
}
update();
});
// 🔄 [FIX] معالج محاولات إعادة الاتصال (للتشخيص)
socket.onReconnectAttempt((attemptNumber) {
Log.print("🔄 Socket Reconnect Attempt #$attemptNumber...");
});
// ❌ معالج الأخطاء
socket.onError((error) {
Log.print("❌ Socket Error: $error");
isSocketConnected = false;
});
// 📩 معالج تحديثات الحالة
socket.on('ride_status_change', (data) {
Log.print("📩 Socket Event: ride_status_change -> $data");
_handleRideStatusChangeWithSocket(data);
});
// 📍 معالج موقع السائق
socket.on('driver_location_update', (data) {
handleDriverLocationUpdate(data);
});
}
void _startHeartbeat() {
_heartbeatTimer?.cancel();
_heartbeatTimer = Timer.periodic(const Duration(seconds: 25), (timer) {
if (isSocketConnected && socket.connected) {
socket.emit('heartbeat',
{'passenger_id': box.read(BoxName.passengerID).toString()});
}
});
}
// دالة مساعدة
bool _isActiveRideState() {
return currentRideState.value == RideState.searching ||
currentRideState.value == RideState.driverApplied ||
currentRideState.value == RideState.driverArrived ||
currentRideState.value == RideState.inProgress;
}
// ==============================================================================
// 2. العقل المدبر: توجيه الحالات
// ==============================================================================
void _handleRideStatusChangeWithSocket(dynamic data) {
if (data == null || data['status'] == null) return;
String newStatus = data['status'].toString().toLowerCase();
Log.print("🔔 Socket Status Update: $newStatus");
// استخراج بيانات السائق إذا توفرت (تأتي من acceptRide.php)
Map<String, dynamic>? driverInfo;
if (data['driver_info'] != null && data['driver_info'] is Map) {
driverInfo = Map<String, dynamic>.from(data['driver_info']);
}
switch (newStatus) {
case 'accepted': // أو apply/applied حسب تسمية السيرفر
_onDriverAcceptedWithSocket(data, driverData: driverInfo);
break;
case 'arrived':
_onDriverArrivedWithSocket();
break;
case 'started': // أو begin
_onRideStartedWithSocket();
break;
case 'finished': // أو ended
_onRideFinishedWithSocket(data);
break;
case 'cancelled':
_onRideCancelledWithSocket(data);
break;
case 'no_drivers_found':
showNoDriverDialog();
break;
}
}
// ==============================================================================
// 3. دوال المعالجة التفصيلية (Actions)
// ==============================================================================
void showNoDriverDialog() {
Get.defaultDialog(
title: "No Drivers Found".tr,
middleText:
"Sorry, there are no cars available of this type right now.".tr,
textConfirm: "Refresh Map".tr,
textCancel: "Cancel".tr,
confirmTextColor: Colors.white,
onConfirm: () {
Get.back(); // إغلاق الديالوج
restCounter();
stopAllTimers();
Get.offAll(() => MapPagePassenger()); // إعادة تحميل صفحة الخريطة
},
);
}
// أ) عند قبول السائق للرحلة
// أ) عند قبول السائق للرحلة (معدلة)
// دالة الاستقبال من السوكيت (تصبح مجرد محول)
void _onDriverAcceptedWithSocket(dynamic data,
{Map<String, dynamic>? driverData}) {
// استخراج البيانات وتمريرها للدالة الموحدة
Map<String, dynamic>? info = driverData;
// دعم الهيكلية الجديدة
if (info == null && data['driver_info'] != null) {
info = Map<String, dynamic>.from(data['driver_info']);
}
// دعم الهيكلية القديمة (إن وجدت)
else if (info == null && data['driverList'] != null) {
// تحويل driverList إلى map إذا لزم الأمر
}
processRideAcceptance(driverData: info, source: "Socket");
}
void _fillDriverDataLocally(Map<String, dynamic> data) {
try {
// تعبئة المتغيرات بناءً على أسماء الحقول في acceptRide.php
driverId = data['driver_id']?.toString() ?? '';
driverPhone = data['phone']?.toString() ?? '';
String fName = data['first_name']?.toString() ?? '';
String lName = data['last_name']?.toString() ?? '';
passengerName = lName.isNotEmpty
? "$fName $lName"
: fName; // (هنا المتغير اسمه passengerName لكنه يحمل اسم السائق في الكود لديك)
driverName = passengerName;
make = data['make']?.toString() ?? '';
model = data['model']?.toString() ?? '';
carColor = data['color']?.toString() ?? '';
colorHex = data['color_hex']?.toString() ?? '';
licensePlate = data['car_plate']?.toString() ?? '';
carYear = data['year']?.toString() ?? '';
driverRate = data['ratingDriver']?.toString() ?? '5.0';
driverToken = data['token']?.toString() ?? '';
// إذا كان هناك أي بيانات أخرى تحتاجها الواجهة
update();
} catch (e) {
Log.print("Error parsing socket driver data: $e");
}
}
// دالة موحدة: تجلب المسار + الوقت + المسافة + ترسم الخط + تضبط الكاميرا
Future<void> calculateDriverToPassengerRoute(
LatLng driverPos, LatLng passengerPos) async {
// 1. تجهيز الرابط (نفس API الـ Direction)
// نستخدم overview=full للحصول على الرسمة، و steps=false لتخفيف البيانات
final Map<String, String> queryParams = {
'fromLat': driverPos.latitude.toString(),
'fromLng': driverPos.longitude.toString(),
'toLat': passengerPos.latitude.toString(),
'toLng': passengerPos.longitude.toString(),
};
final uri =
Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
Log.print('📍 Calculating Driver Route: $uri');
try {
final response = await http.get(uri, headers: {
'x-api-key': Env.mapSaasKey,
}).timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
final responseData = json.decode(response.body);
// Support both old format (routes[0]) and new SaaS format (top-level)
var routeData = responseData['routes'] != null
? responseData['routes'][0]
: responseData;
// 2. تحديث المتغيرات (المسافة والوقت)
double durationSecondsRaw = (routeData['duration'] as num).toDouble();
int finalDurationSeconds =
(durationSecondsRaw * kDurationScalar).toInt();
double distanceMeters = (routeData['distance'] as num).toDouble();
timeToPassengerFromDriverAfterApplied = finalDurationSeconds;
remainingTimeToPassengerFromDriverAfterApplied = finalDurationSeconds;
distanceByPassenger = distanceMeters.toStringAsFixed(0);
// تحديث نصوص الواجهة
int minutes = (finalDurationSeconds / 60).floor();
int seconds = finalDurationSeconds % 60;
stringRemainingTimeToPassenger =
'$minutes:${seconds.toString().padLeft(2, '0')}';
Log.print(
'✅ Driver Route Info: $minutes min, ${distanceMeters.toInt()} m');
// 3. معالجة الرسم (Polyline)
// SaaS uses 'points', OSRM uses 'geometry'
String pointsString =
routeData['points'] ?? routeData['geometry'] ?? "";
if (pointsString.isNotEmpty) {
List<LatLng> decodedPoints =
await compute(decodePolylineIsolate, pointsString);
// حفظ نسخة للمقارنة
_currentDriverRoutePoints = decodedPoints;
// إزالة خط مسار السائق القديم فقط
polyLines = polyLines
.where((p) => p.polylineId.value != 'driver_route')
.toSet();
// إضافة الخط الجديد (بستايل مميز للسائق)
polyLines = {
...polyLines,
Polyline(
polylineId: const PolylineId('driver_route'),
points: decodedPoints,
color:
const Color(0xFF333333), // لون مختلف عن مسار الرحلة الأساسي
width: 5,
)
};
}
// 4. ضبط الكاميرا لتشمل السائق والراكب
_fitCameraToPoints(driverPos, passengerPos);
update(); // تحديث واحد للكل
}
} catch (e) {
Log.print('❌ Error calculating driver route: $e');
}
}
Future<void> _checkAndRecalculateIfDeviated(LatLng driverPos) async {
// 1. شروط الخروج السريع
if (_isRecalculatingRoute || _currentDriverRoutePoints.isEmpty) return;
// 2. حساب المسافة لأقرب نقطة في المسار (خوارزمية سريعة)
// نستخدم مكتبة Geolocator أو حساب رياضي بسيط
double minDistance = 100000.0;
// لتقليل الحمل، لا نفحص كل النقاط، نفحص عينة (كل 5 نقاط مثلاً) أو الكل إذا المسار قصير
for (var point in _currentDriverRoutePoints) {
double dist = Geolocator.distanceBetween(driverPos.latitude,
driverPos.longitude, point.latitude, point.longitude);
if (dist < minDistance) minDistance = dist;
}
// 3. اتخاذ القرار
if (minDistance > _deviationThresholdMeters) {
Log.print("⚠️ Driver deviated ($minDistance m). Recalculating route...");
_isRecalculatingRoute = true;
// إعادة حساب المسار من موقع السائق الجديد
await calculateDriverToPassengerRoute(driverPos, passengerLocation);
_isRecalculatingRoute = false;
}
}
// ب) عند وصول السائق
void _onDriverArrivedWithSocket() {
Log.print("🚖 Driver Arrived (Socket)");
processDriverArrival("Socket");
}
// ج) عند بدء الرحلة
void _onRideStartedWithSocket() {
Log.print("🚀 Ride Started (Socket)");
processRideBegin(source: "Socket");
}
// ربط السوكيت
// د) عند انتهاء الرحلة (Socket Listener)
void _onRideFinishedWithSocket(dynamic data) {
Log.print("🏁 Ride Finished (Socket)");
// نحاول استخراج DriverList من البيلود القادم من PHP
// في finish_ride_updates.php أسميناه 'DriverList'
var rawList = data['DriverList'];
List<dynamic> listToSend = [];
if (rawList != null) {
if (rawList is List) {
listToSend = rawList;
} else if (rawList is String) {
// احتياطاً لو وصل كنص
try {
listToSend = jsonDecode(rawList);
} catch (e) {
Log.print("Error: $e");
}
}
}
// إذا كانت القائمة فارغة، نحاول بناءها من البيانات المتفرقة (Fallback)
if (listToSend.isEmpty && data['price'] != null) {
listToSend = [
driverId, // 0
rideId, // 1
driverToken, // 2
data['price'].toString() // 3
];
}
// استدعاء المعالج الموحد
processRideFinished(listToSend, source: "Socket");
}
// هـ) عند الإلغاء
void _onRideCancelledWithSocket(dynamic data) {
processRideCancelledByDriver(data, source: "Socket");
}
// ==============================================================================
// 4. إدارة تتبع الموقع (Polling) - مفصولة عن السوكيت
// ==============================================================================
// متغير لمنع التكرار (Race Condition Guard)
bool _isCancelProcessed = false;
/// **معالجة إلغاء الرحلة الموحدة (Gatekeeper)**
///
/// تستدعى من [Socket] أو [FCM] عند قيام السائق بإلغاء الرحلة.
/// تضمن عدم تضارب الإشعارات وتوحد تجربة المستخدم.
Future<void> processRideCancelledByDriver(dynamic data,
{String source = "Unknown"}) async {
if (_isCancelProcessed) return;
_isCancelProcessed = true;
stopAllTimers();
if (Get.isDialogOpen == true) Get.back();
await RideLiveNotification.cancel();
IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر
PipService.disablePip(); // ✅ إيقاف PiP عند انتهاء الرحلة
if (Get.isDialogOpen == true) Get.back();
await RideLiveNotification.cancel();
Get.defaultDialog(
title: "Sorry 😔".tr, // استخدام المفتاح الإنجليزي
titleStyle:
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
barrierDismissible: false,
content: Column(
children: [
const Icon(Icons.cancel_presentation,
size: 50, color: Colors.redAccent),
const SizedBox(height: 10),
Text(
"The driver cancelled the trip for an emergency reason.\nDo you want to search for another driver immediately?"
.tr,
textAlign: TextAlign.center,
),
],
),
actions: [
TextButton(
onPressed: () {
Get.back();
handleNoDriverFound();
},
child: Text("Cancel Trip".tr,
style: const TextStyle(color: Colors.grey)),
),
ElevatedButton.icon(
style:
ElevatedButton.styleFrom(backgroundColor: AppColor.primaryColor),
icon: const Icon(Icons.refresh, color: Colors.white),
label: Text("Search for another driver".tr,
style: const TextStyle(color: Colors.white)),
onPressed: () {
Get.back();
retrySearchForDrivers();
},
),
],
);
}
Future<void> handleNoDriverFound() async {
stopAllTimers();
await RideLiveNotification.cancel();
IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر
PipService.disablePip(); // ✅ إيقاف PiP
_isCancelProcessed = false;
currentRideState.value = RideState.noRide;
resetAllMapStates();
Get.offAll(() => const MapPagePassenger());
Get.defaultDialog(
title: "We apologize 😔".tr,
middleText: "No drivers found at the moment.\nPlease try again later.".tr,
confirm: ElevatedButton(
onPressed: () => Get.back(),
child: Text("Ok".tr),
),
);
}
/// **إعادة البحث عن سائقين (Retry Search)**
///
/// تقوم باستدعاء السكربت لإعادة تفعيل الرحلة وبدء عداد البحث من جديد.
void retrySearchForDrivers() async {
_isCancelProcessed = false;
isSearchingWindow = true;
currentRideState.value = RideState.searching;
driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr;
update();
try {
Log.print("🔄 Retrying search for ride ID: $rideId");
// تجهيز البيانات المخزنة للإرسال
var payload = {
"ride_id": rideId.toString(),
"passenger_id": box.read(BoxName.passengerID).toString(),
"passenger_name": box.read(BoxName.name).toString(),
"passenger_phone": box.read(BoxName.phone).toString(),
"passenger_email": box.read(BoxName.email).toString(),
"passenger_token": box.read(BoxName.tokenFCM).toString(),
"passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(),
"passenger_rating": "5.0", // أو قراءة التقييم الحقيقي إن وجد
// قراءة البيانات من المتغيرات المحفوظة في الكنترولر أو الـ Box
"start_lat": startLocation.latitude.toString(),
"start_lng": startLocation.longitude.toString(),
"end_lat": endLocation.latitude.toString(),
"end_lng": endLocation.longitude.toString(),
"start_name": startNameAddress,
"end_name": endNameAddress,
"distance": distance.toString(),
"distance_text": distanceByPassenger ?? "",
"duration_text": durationToPassenger.toString(),
"price": totalPassenger.toString(),
"price_for_driver": costForDriver.toString(),
"car_type": box.read(BoxName.carType).toString(),
"is_wallet": Get.find<PaymentController>().isWalletChecked.toString(),
// الخطوات (اختياري)
"has_steps": Get.find<WayPointController>().wayPoints.length > 1
? "true"
: "false",
// يمكنك إضافة الخطوات إذا كانت لديك في مصفوفة
};
var response = await CRUD().post(
link: "${AppLink.rideServerSide}/rides/retry_search_drivers.php",
payload: payload,
);
if (response['status'] == 'success') {
Log.print("✅ Search reset successfully.");
startSearchingTimer();
} else {
mySnackbarWarning("Failed to search, please try again later".tr);
handleNoDriverFound();
}
} catch (e) {
Log.print("Error retrying search: $e");
mySnackbarWarning("Please check your internet connection".tr);
handleNoDriverFound();
}
}
Timer? _searchTimer;
/// **بدء مؤقت البحث (Search Timer)**
///
/// يبدأ عداداً (مثلاً 90 ثانية). إذا لم يتم قبول الرحلة خلال هذه المدة،
/// يتم إنهاء البحث واستدعاء [handleNoDriverFound].
Future<void> startSearchingTimer() async {
_searchTimer?.cancel();
int seconds = 0;
Log.print("⏳ Search Timer Started (90s)...");
await RideLiveNotification.showSearching(driversStatusForSearchWindow);
_searchTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
seconds++;
// إذا تغيرت الحالة (مثلاً سائق قبل الرحلة)، نوقف العداد
if (currentRideState.value != RideState.searching) {
timer.cancel();
return;
}
// انتهاء الوقت (90 ثانية)
if (seconds >= 90) {
timer.cancel();
handleNoDriverFound(); // ⏳ انتهى الوقت ولم نجد سائقاً
}
});
}
// متغير لمنع التكرار (Race Condition Guard)
bool _isArrivalProcessed = false;
/// **معالجة وصول السائق الموحدة (Unified Driver Arrival Handler)**
///
/// تقوم هذه الدالة بإدارة حدث وصول السائق إلى موقع الراكب، وتعمل كـ "حارس بوابة" (Gatekeeper)
/// لتوحيد المصادر سواء كانت من **WebSocket** أو **Firebase Notification**.
///
/// **المهام التي تقوم بها:**
/// 1. **منع التضارب (Race Condition Guard):** تتأكد أن الحدث لم يتم تنفيذه مسبقاً لتجنب تكرار الإشعارات أو إعادة ضبط العدادات.
/// 2. **تحديث الحالة:** تغير حالة الرحلة فوراً إلى [RideState.driverArrived].
/// 3. **تفعيل الواجهة:** تظهر ديالوج "السائق وصل" وتبدأ عداد الانتظار المجاني (5 دقائق).
///
/// * [source] : نص يوضح مصدر الاستدعاء (مثل "Socket" أو "FCM") لأغراض التتبع (Logging).
Future<void> processDriverArrival(String source) async {
// 1. الحارس: إذا تم التنفيذ سابقاً، توقف
if (currentRideState.value == RideState.driverArrived ||
_isArrivalProcessed) {
Log.print("✋ Ignored Arrival from $source. Already processed.");
return;
}
_isArrivalProcessed = true;
Log.print("🚖 Driver Arrived via $source! Processing...");
// 2. تحديث الحالة
currentRideState.value = RideState.driverArrived;
statusRide = 'Arrived';
await RideLiveNotification.showDriverArrived(driverName ?? '');
// 3. تشغيل واجهة الوصول والعداد
driverArrivePassengerDialoge();
startTimerDriverWaitPassenger5Minute();
update();
}
// متغير لمنع التكرار
bool _isFinishProcessed = false;
/// **معالجة إنهاء الرحلة الموحدة (Unified Ride Finish Handler)**
///
/// تستدعى عند استلام حدث النهاية من السوكيت أو FCM.
/// تقوم بإغلاق السوكيت، إيقاف التتبع، والانتقال لشاشة التقييم والدفع.
///
/// * [driverList]: قائمة البيانات [driverId, rideId, token, price].
Future<void> processRideFinished(List<dynamic> driverList,
{String source = "Unknown"}) async {
// 1. الحارس: منع التكرار
if (currentRideState.value == RideState.finished || _isFinishProcessed) {
Log.print("✋ Ignored Finish Request from $source. Already Finished.");
return;
}
_isFinishProcessed = true;
Log.print(
"🏁 Ride Finished via $source. Price: ${driverList.length > 3 ? driverList[3] : 'N/A'}");
// 2. تحديث الحالة
currentRideState.value = RideState.finished;
// 3. تنظيف الموارد
disposeRideSocket(); // إغلاق السوكيت
_stopDriverLocationPolling(); // إيقاف تتبع الموقع
if (Get.isRegistered<AudioRecorderController>()) {
Get.find<AudioRecorderController>().stopRecording();
}
// إغلاق أي ديالوج سابق
if (Get.isDialogOpen == true) Get.back();
// 4. إشعار "لا تنسى أغراضك"
NotificationController().showNotification(
'Alert'.tr,
"Please make sure not to leave any personal belongings in the car.".tr,
'tone1',
);
IosLiveActivityService.endRideActivity();
PipService.disablePip(); // ✅ إيقاف PiP
await RideLiveNotification.cancel();
// 5. استخراج البيانات والانتقال
if (driverList.length >= 4) {
String price = driverList[3].toString();
// الانتقال لصفحة التقييم
Get.offAll(() => RateDriverFromPassenger(), arguments: {
'driverId': driverList[0].toString(),
'rideId': driverList[1].toString(),
'price': price
});
}
}
void _startDriverLocationPollingWithTimer() {
Log.print("📍 Starting Driver Location Polling (6s interval)");
_locationPollingTimer?.cancel();
// استدعاء فوري لأول مرة
// getDriverCarsLocationToPassengerAfterApplied();
_locationPollingTimer = Timer.periodic(Duration(seconds: 6), (timer) {
// شرط التوقف: إذا انتهت الرحلة أو ألغيت
if (currentRideState.value == RideState.finished ||
currentRideState.value == RideState.cancelled ||
currentRideState.value == RideState.noRide) {
timer.cancel();
return;
}
// جلب الموقع وتحديث الماركر
getDriverCarsLocationToPassengerAfterApplied();
});
}
void _stopDriverLocationPolling() {
Log.print("🛑 Stopping Location Polling");
_locationPollingTimer?.cancel();
_locationPollingTimer = null;
}
// ==============================================================================
// 5. التنظيف والإغلاق
// ==============================================================================
void disposeRideSocket() {
if (socket != null) {
socket!.disconnect();
socket!.dispose();
// socket = null;
isSocketConnected = false;
Log.print("🔌 Socket Disposed");
}
}
/// ==============================================================================
/// 6. معالجة تحديث موقع السائق من السوكيت
/// ==============================================================================
void handleDriverLocationUpdate(dynamic data) {
if (!isSocketConnected || data == null) return;
// 🔥 1. تسجيل وقت استلام البيانات (تغذية الـ Watchdog)
_lastSocketLocationTime = DateTime.now();
try {
double lat = double.tryParse(data['latitude']?.toString() ?? '0') ?? 0;
double lng = double.tryParse(data['longitude']?.toString() ?? '0') ?? 0;
double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0;
if (lat == 0 || lng == 0) return;
LatLng newPos = LatLng(lat, lng);
// تحديث القائمة (نفس المنطق القديم)
if (driverCarsLocationToPassengerAfterApplied.isEmpty) {
driverCarsLocationToPassengerAfterApplied.add(newPos);
} else {
driverCarsLocationToPassengerAfterApplied[0] = newPos;
}
currentLocationOfDrivers = newPos;
// ------------------------------------------------------------------
// 🔥 تحديث الكاميرا: ضمان بقاء السيارة في منتصف الخريطة
// ------------------------------------------------------------------
// ملاحظة: تأكد من أن mapController قد تم تهيئته (initialized)
if (mapController != null) {
// نستخدم animateCamera لحركة ناعمة بدلاً من moveCamera القاسية
mapController!.animateCamera(CameraUpdate.newLatLng(newPos));
}
// ------------------------------------------------------------------
// تحديث الوقت المتبقي (إذا أرسله السيرفر)
if (data['eta_seconds'] != null) {
int etaSeconds = int.tryParse(data['eta_seconds'].toString()) ?? 0;
if (etaSeconds > 0) {
remainingTimeToPassengerFromDriverAfterApplied = etaSeconds;
int minutes = (etaSeconds / 60).floor();
int seconds = etaSeconds % 60;
stringRemainingTimeToPassenger =
'$minutes:${seconds.toString().padLeft(2, '0')}';
}
}
// تحديث الماركر
_updateDriverMarker(newPos, heading);
} catch (e) {
Log.print('Error in handleDriverLocationUpdate: $e');
}
}
// دالة مساعدة لتحديث ماركر السائق
void _updateDriverMarker(LatLng position, double heading) {
const String markerId = 'driver_location';
final mId = MarkerId(markerId);
final existingMarker = markers.cast<Marker?>().firstWhere(
(m) => m?.markerId == mId,
orElse: () => null,
);
if (existingMarker != null) {
_smoothlyUpdateMarker(existingMarker, position, heading, carIcon);
} else {
markers = {
...markers,
Marker(
markerId: mId,
position: position,
icon: InlqBitmap.fromStyleImage(carIcon),
rotation: heading,
anchor: const Offset(0.5, 0.5),
),
};
update();
}
}
// === إضافة متغير للتحكم ===
bool _isUsingFallback = false;
void _startPollingFallback() {
if (_isUsingFallback) return;
Log.print('🔄 Starting Polling Fallback Mode');
_isUsingFallback = true;
// استخدام _handleRideState الموجود (كما كان)
_startMasterTimer();
}
void handleActiveRideOnStartup(dynamic data) {
try {
if (data == null || data['has_active_ride'] != true) {
Log.print('[Startup] No active ride');
currentRideState.value = RideState.noRide;
_startMasterTimer();
return;
}
Log.print('[Startup] ✅ Active ride found!');
var rideData = data['ride'];
rideId = rideData['ride_id'].toString();
currentRideId = rideId;
driverId = rideData['driver_id']?.toString() ?? '';
String status = rideData['status']?.toString().toLowerCase() ?? '';
// تحديد الحالة
if (status == 'waiting' || status == 'searching') {
currentRideState.value = RideState.searching;
isSearchingWindow = true;
} else if (status == 'apply' || status == 'applied') {
currentRideState.value = RideState.driverApplied;
statusRide = 'Apply';
// الاشتراك في موقع السائق
socket.emit('subscribe_driver_location', {
'ride_id': rideId,
'driver_id': driverId,
});
// استعادة بيانات السائق
if (rideData['driver_info'] != null) {
var dInfo = rideData['driver_info'];
passengerName = dInfo['first_name']?.toString() ?? '';
driverPhone = dInfo['phone']?.toString() ?? '';
model = dInfo['model']?.toString() ?? '';
licensePlate = dInfo['license_plate']?.toString() ?? '';
}
} else if (status == 'arrived') {
currentRideState.value = RideState.driverArrived;
statusRide = 'Arrived';
isDriverArrivePassenger = true;
} else if (status == 'begin' || status == 'started') {
currentRideState.value = RideState.inProgress;
statusRide = 'Begin';
rideTimerBegin = true;
// استعادة Polyline
if (rideData['polyline'] != null) {
_restorePolyline(rideData['polyline']);
}
rideIsBeginPassengerTimer();
}
update();
_startMasterTimer(); // للـ Safety Checks
} catch (e) {
Log.print('[Startup] Error: $e');
currentRideState.value = RideState.noRide;
_startMasterTimer();
}
}
Future<void> _restorePolyline(String polylineString) async {
try {
List<LatLng> points =
await compute(decodePolylineIsolate, polylineString);
polylineCoordinates.clear();
polylineCoordinates.addAll(points);
polyLines.clear();
polyLines = {
...polyLines,
Polyline(
polylineId: const PolylineId('route_direct'),
points: polylineCoordinates,
color: const Color(0xFF2196F3),
width: 6,
)
};
update();
} catch (e) {
Log.print('Error restoring polyline: $e');
}
}
// متغير لمنع التكرار (Race Condition Guard)
bool _isAcceptanceProcessed = false;
// ==============================================================================
// الدالة الموحدة لمعالجة القبول (من السوكيت أو FCM)
// ==============================================================================
Future<void> processRideAcceptance(
{Map<String, dynamic>? driverData, required String source}) async {
// 1. الحماية: إذا تم المعالجة مسبقاً، تجاهل
// نستثني حالة واحدة: إذا كنا في وضع البحث (Searching) نقبل الأمر
// إذا كنا في driverApplied، نتجاهل الأمر
if (currentRideState.value != RideState.searching &&
_isAcceptanceProcessed) {
Log.print("✋ Ignored Acceptance from $source. Already processed.");
return;
}
_isAcceptanceProcessed = true; // قفل الباب
Log.print("🚀 Winner: $source triggered acceptance! Processing...");
// 2. إيقاف جميع التايمرات القديمة فوراً
_masterTimer?.cancel();
// stopSearchingTimer(); // إذا كان لديك تايمر للبحث
// 3. تحديث الحالة في الواجهة
currentRideState.value = RideState.driverApplied;
statusRide = 'Apply';
isSearchingWindow = false;
// 4. معالجة البيانات (تعبئة المتغيرات)
if (driverData != null && driverData.isNotEmpty) {
Log.print("📥 Populating Data from $source payload...");
_fillDriverDataLocally(driverData);
} else {
Log.print("⚠️ No Data in Payload. Fallback to API.");
await getUpdatedRideForDriverApply(rideId);
}
// أضف هذا بعد السطر الذي تستدعي فيه RideTrackingNative
await IosLiveActivityService.startRideActivity(
rideId: rideId,
driverName: driverName ?? 'السائق',
carDetails: '$make$carColor',
etaText: stringRemainingTimeToPassenger,
progress: 0.0,
);
// إشعارات (الأسعار، الأمان...)
_showRideStartNotifications();
final etaText = stringRemainingTimeToPassenger; // مثال: "8 دقائق"
final carInfo = '$make$model$licensePlate';
await RideLiveNotification.showDriverOnWay(
driverName: driverName,
etaText: etaText,
carInfo: carInfo,
);
update(); // تحديث الواجهة لإظهار بيانات السائق
// 5. 🔥 العمليات الجغرافية (المسار والوقت) 🔥
// أ) جلب موقع السائق الأولي
// await getDriverCarsLocationToPassengerAfterApplied();// stop this to use socket update
_startSocketWatchdog();
// ب) رسم المسار وحساب الوقت
if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) {
LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last;
// نستخدم الدالة الموحدة الجديدة للحساب والرسم
await calculateDriverToPassengerRoute(driverPos, passengerLocation);
// ج) تشغيل التايمر المحلي (للعد التنازلي فقط)
startTimerFromDriverToPassengerAfterApplied();
}
final int timeToPassengerSeconds =
timeToPassengerFromDriverAfterApplied; // مثلاً من السيرفر
final double distanceDriverToPassengerMeters =
double.parse(distanceByPassenger);
// [PiP] تم تعطيل الإشعار المستمر القديم (Foreground Service) واستبداله بـ PiP
// await RideTrackingNative.updateRideTracking(
// driverName: driverName,
// driverPhone: driverPhone,
// carDetails: '$make • $carColor • $licensePlate',
// driverLat: driverCarsLocationToPassengerAfterApplied.last.latitude,
// driverLng: driverCarsLocationToPassengerAfterApplied.last.longitude,
// passengerLat: passengerLocation.latitude,
// passengerLng: passengerLocation.longitude,
// destLat: myDestination.latitude,
// destLng: myDestination.longitude,
// rideState: 'waiting',
// estimatedTimeMinutes: (timeToPassengerSeconds / 60).round(),
// totalDistanceMeters: distanceDriverToPassengerMeters,
// );
// [PiP] تفعيل PiP عند بدء الرحلة (سيدخل وضع النافذة العائمة عند خروج المستخدم)
PipService.enablePip();
// 6. بدء تتبع الموقع الدوري (Polling Backup + Smart Rerouting)
// سيبدأ العمل بعد 6 ثواني
_startDriverLocationPollingWithTimer();
}
Timer? _watchdogTimer;
void _startSocketWatchdog() {
_watchdogTimer?.cancel();
Log.print("👀 Starting Socket Watchdog (Hybrid Mode)...");
// نفحص كل 5 ثواني
_watchdogTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
// شروط إيقاف الحارس (إذا انتهت الرحلة)
if (currentRideState.value != RideState.driverApplied &&
currentRideState.value != RideState.inProgress) {
timer.cancel();
return;
}
// 1. حساب الزمن المنقضي منذ آخر تحديث سوكيت
final lastTime = _lastSocketLocationTime ??
DateTime.now().subtract(const Duration(minutes: 1));
final difference = DateTime.now().difference(lastTime).inSeconds;
// 2. القرار
if (difference < 15 && isSocketConnected) {
// ✅ الوضع ممتاز: وصلنا تحديث في آخر 10 ثواني والسوكيت متصل
// لا تفعل شيئاً (وفر السيرفر والبطارية)
// Log.print("✅ Socket is healthy. Skipping API poll.");
} else {
// ⚠️ الوضع حرج: السوكيت معلق أو مفصول لأكثر من 10 ثواني
Log.print("⚠️ Socket silent for ${difference}s. Forcing API Poll...");
// استدعاء دالة البولينغ (لمرة واحدة)
await getDriverCarsLocationToPassengerAfterApplied();
}
});
}
// قائمة بأنصاف الأقطار (بالأمتار) لكل مرحلة
final List<int> _searchRadii = [
2400,
3000,
3100
]; // 0 ثانية، 30 ثانية، 60 ثانية
// المدة الزمنية لكل مرحلة بحث (بالثواني)
final int _searchPhaseDurationSeconds = 30;
// المهلة الإجمالية للبحث قبل عرض خيار زيادة السعر
final int _totalSearchTimeoutSeconds = 90; // 90 ثانية
// --- noRide throttling ---
int _noRideSearchCount = 0;
final int _noRideMaxTries = 3; // نفذ البحث 6 مرات فقط
final int _noRideIntervalSec = 5; // بين كل محاولة وأخرى 5 ثواني
DateTime? _noRideNextAllowed; // متى نسمح بالمحاولة التالية
bool _noRideSearchCapped = false; // وصلنا للحد وتوقفنا
// ============== new design to manage ride state ==============
// === 1. حالة الرحلة والمؤقت الرئيسي (Single Source of Truth) ===
Rx<RideState> currentRideState = RideState.noRide.obs;
Timer? _masterTimer;
final int _pollingIntervalSeconds = 13; // فاصل زمني موحد للاستعلام
void _startMasterTimer() {
// نضمن أن مؤقت واحد فقط يعمل في أي وقت
_masterTimer?.cancel();
_masterTimer =
Timer.periodic(Duration(seconds: _pollingIntervalSeconds), (_) {
_handleRideState(currentRideState.value);
});
}
void stopAllTimers() {
Log.print('🛑 FORCE STOP: Stopping ALL Timers and Streams 🛑');
// 1. إيقاف الماكينة الرئيسية
_masterTimer?.cancel();
_masterTimer = null;
// 2. إيقاف مؤقتات تتبع السائق
timerToPassengerFromDriverAfterApplied?.cancel();
_timer?.cancel();
_uiCountdownTimer?.cancel();
// 3. إيقاف مؤقتات الخريطة
markerReloadingTimer?.cancel();
markerReloadingTimer1?.cancel();
markerReloadingTimer2?.cancel();
_animationTimers.forEach((key, timer) => timer.cancel());
_animationTimers.clear();
// 4. إغلاق الستريمز
if (!_rideStatusStreamController.isClosed)
_rideStatusStreamController.close();
if (!_beginRideStreamController.isClosed)
_beginRideStreamController.close();
if (!timerController.isClosed) timerController.close();
// 5. تصفير العدادات لمنع إعادة الدخول
isTimerRunning = false;
isBeginRideFromDriverRunning = false;
_isFetchingDriverLocation = false;
update();
}
final int _maxNoRideSearch = 3; // عدد المرات القصوى
final int _noRideDelaySeconds = 6; // الفاصل الزمني بين كل بحث
//
//
// !!! يرجى استبدال الدالة القديمة بالكامل بهذه الدالة الجديدة !!!
//
//
bool _isStateProcessing = false;
Future<void> _handleRideState(RideState state) async {
if (_isRatingScreenOpen) {
Log.print('⛔ Rating Screen is Open. Skipping Logic.');
stopAllTimers(); // تأكيد إضافي للإيقاف
return;
}
Log.print('Handling state: $state');
// int effectivePollingInterval = _pollingIntervalSeconds;
// الحصول على الفاصل الزمني من الخريطة
int effectivePollingInterval =
_pollingIntervals[state] ?? _pollingIntervalSeconds;
switch (state) {
case RideState.noRide:
final now = DateTime.now();
if (_noRideSearchCount >= _noRideMaxTries) {
if (!_noRideSearchCapped) {
_noRideSearchCapped = true;
Log.print('[noRide] search capped at $_noRideMaxTries attempts');
}
break;
}
if (_noRideNextAllowed != null && now.isBefore(_noRideNextAllowed!)) {
break;
}
_noRideSearchCount++;
Log.print('_noRideSearchCount: $_noRideSearchCount');
_noRideNextAllowed = now.add(Duration(seconds: _noRideIntervalSec));
String currentCarType = box.read(BoxName.carType) ?? 'yet';
getCarsLocationByPassengerAndReloadMarker();
getNearestDriverByPassengerLocation();
break;
case RideState.cancelled:
Log.print('[handleRideState] Ride cancelled. Stopping polling.');
stopAllTimers();
// effectivePollingInterval = 3600;
break;
case RideState.preCheckReview:
stopAllTimers();
_checkLastRideForReview();
break;
case RideState.searching:
// effectivePollingInterval = 5;
// 1. التحقق من حالة الطلب (هل قبله أحد؟)
try {
String statusFromServer = await getRideStatus(rideId);
if (statusFromServer == 'Apply' || statusFromServer == 'Applied') {
await processRideAcceptance(source: "Polling");
break;
}
} catch (e) {
Log.print('Error polling getRideStatus: $e');
}
final now = DateTime.now();
final int elapsedSeconds = now.difference(_searchStartTime!).inSeconds;
// انتهاء وقت البحث الكلي
if (elapsedSeconds > _totalSearchTimeoutSeconds) {
stopAllTimers();
currentRideState.value = RideState.noRide;
isSearchingWindow = false;
update();
_showIncreaseFeeDialog();
break;
}
// 2. إدارة مراحل البحث (توسيع النطاق)
// السيناريو الجديد: لا نقوم بالقصف العشوائي، نرسل بناء على المرحلة أو مرور وقت كافٍ لدخول سائقين جدد
int targetPhase =
(elapsedSeconds / _searchPhaseDurationSeconds).floor();
if (targetPhase >= _searchRadii.length) {
targetPhase = _searchRadii.length - 1;
}
// هل تغيرت المرحلة (توسع النطاق)؟ أو هل مر 10 ثواني منذ آخر محاولة إرسال؟
// هذا يمنع إرسال الإشعار في كل دورة (كل 5 ثواني) ويقلل الازعاج
bool isNewPhase = targetPhase > _currentSearchPhase;
bool timeToScanForNewDrivers =
(elapsedSeconds % 15 == 0); // كل 15 ثانية نفحص الدخول الجديد
if (isNewPhase || timeToScanForNewDrivers || elapsedSeconds < 5) {
_currentSearchPhase = targetPhase;
int currentRadius = _searchRadii[_currentSearchPhase];
Log.print(
'[Search Logic] Scanning for drivers. Phase: $_currentSearchPhase, Radius: $currentRadius');
// استدعاء دالة الإشعار الذكية
// _findAndNotifyNearestDrivers(currentRadius);
}
// تحديث نصوص الواجهة
if (elapsedSeconds < 5) {
driversStatusForSearchWindow = 'Your order is being prepared'.tr;
} else if (elapsedSeconds < 15) {
driversStatusForSearchWindow = 'Your order sent to drivers'.tr;
} else {
driversStatusForSearchWindow =
'The drivers are reviewing your request'.tr;
}
update();
break;
case RideState.driverApplied:
// effectivePollingInterval = 10;
if (!_isDriverAppliedLogicExecuted) {
Log.print('[handleRideState] Execution driverApplied logic.');
rideAppliedFromDriver(true);
_isDriverAppliedLogicExecuted = true;
}
try {
String statusFromServer = await getRideStatus(rideId);
if (statusFromServer == 'Arrived') {
currentRideState.value = RideState.driverArrived;
break;
} else if (statusFromServer == 'Begin' ||
statusFromServer == 'inProgress') {
processRideBegin();
break;
}
} catch (e) {
Log.print('Error polling for Arrived/Begin status: $e');
}
getDriverCarsLocationToPassengerAfterApplied();
break;
case RideState.driverArrived:
if (!_isDriverArrivedLogicExecuted) {
_isDriverArrivedLogicExecuted = true;
startTimerDriverWaitPassenger5Minute();
driverArrivePassengerDialoge();
}
// effectivePollingInterval = 8;
break;
case RideState.inProgress:
// effectivePollingInterval = 6;
try {
String statusFromServer = await getRideStatus(rideId);
// !!! هنا التغيير الجذري !!!
if (statusFromServer == 'Finished' ||
statusFromServer == 'finished') {
Log.print(
'🏁 DETECTED FINISHED: Killing processes and forcing Review.');
// 1. قتل العمليات فوراً
stopAllTimers();
// 2. تغيير الحالة الداخلية لمنع أي كود آخر من العمل
currentRideState.value = RideState.preCheckReview;
// 3. تنظيف الواجهة
tripFinishedFromDriver();
// 4. استدعاء شاشة التقييم فوراً
_checkLastRideForReview();
return; // خروج نهائي من الدالة لمنع أي كود بالأسفل من التنفيذ
}
} catch (e) {
Log.print('Error polling status: $e');
}
// بقية كود التتبع العادي (لن يتم الوصول إليه إذا انتهت الرحلة)
if (!_isRideBeginLogicExecuted) {
_isRideBeginLogicExecuted = true;
_executeBeginRideLogic();
}
getDriverCarsLocationToPassengerAfterApplied();
break;
case RideState.finished:
tripFinishedFromDriver();
stopAllTimers();
effectivePollingInterval = 3600;
break;
}
// تحديث الماكينة الرئيسية إذا تغير الفاصل الزمني
_startMasterTimerWithInterval(effectivePollingInterval);
}
int _masterIntervalSeconds = -1;
void _startMasterTimerWithInterval(int seconds) {
// نفس الانترفَل؟ لا تعمل شيء
if (_masterTimer != null && _masterIntervalSeconds == seconds) return;
_masterIntervalSeconds = seconds;
_masterTimer?.cancel();
_masterTimer = Timer.periodic(Duration(seconds: seconds), (_) {
_handleRideState(currentRideState.value);
});
}
Future<void> _checkInitialRideStatus() async {
// 1. جلب الحالة من السيرفر (باستخدام getRideStatusFromStartApp)
await getRideStatusFromStartApp();
String _status = rideStatusFromStartApp['data']['status'];
// Log.print('rideStatusFromStartApp: ${rideStatusFromStartApp}');
// Log.print('_status: ${_status}');
if (_status == 'waiting' || _status == 'Apply' || _status == 'Begin') {
// رحلة جارية
rideId = rideStatusFromStartApp['data']['rideId'].toString();
currentRideState.value = _status == 'waiting'
? RideState.searching
: _status == 'Apply'
? RideState.driverApplied
: _status == 'Begin'
? RideState.inProgress
: _status == 'Cancel'
? RideState.cancelled
: RideState.noRide;
} else if (_status == 'Finished') {
// رحلة منتهية/ملغاة
if (rideStatusFromStartApp['data']['needsReview'] == 1) {
currentRideState.value = RideState.preCheckReview;
} else {
currentRideState.value = RideState.noRide;
}
} else {
currentRideState.value = RideState.noRide;
}
// بدء المعالجة الفورية
_handleRideState(currentRideState.value);
}
Future<void> _checkLastRideForReview() async {
Log.print('⭐ FORCE OPEN RATING PAGE (Get.to mode)');
// جلب البيانات
await getRideStatusFromStartApp();
if (rideStatusFromStartApp['data'] == null) {
currentRideState.value = RideState.noRide;
_startMasterTimer();
return;
}
String needsReview =
rideStatusFromStartApp['data']['needsReview'].toString();
if (needsReview == '1') {
_isRatingScreenOpen = true;
// 1. تجهيز البيانات (Arguments)
var args = {
'driverId': rideStatusFromStartApp['data']['driver_id'].toString(),
'rideId': rideStatusFromStartApp['data']['rideId'].toString(),
'driverName': rideStatusFromStartApp['data']['driverName'],
'price': rideStatusFromStartApp['data']['price'],
};
// 2. استخدام Get.to مع await (هذا هو الحل الجذري)
// الكود سيتوقف هنا ولن يكمل التنفيذ حتى يتم إغلاق صفحة التقييم
await Get.to(
() => RatingDriverBottomSheet(),
arguments: args, // تمرير البيانات بالطريقة التي تريدها
preventDuplicates: true, // لمنع فتح الصفحة مرتين
popGesture: false, // لمنع السحب للرجوع (في iOS)
);
// 3. هذا الكود لن يتنفذ إلا بعد أن يضغط المستخدم "تم" في التقييم ويغلق الصفحة
Log.print('✅ Rating Page Closed. Resetting App.');
_isRatingScreenOpen = false;
restCounter();
currentRideState.value = RideState.noRide;
_startMasterTimer(); // إعادة تشغيل البحث الآن فقط
} else {
currentRideState.value = RideState.noRide;
_startMasterTimer();
}
}
void startSearchingForDriver() async {
// ✅ منع الضغط المزدوج
if (currentRideState.value == RideState.searching) {
return;
}
// 1. تحديث الحالة الأولية
isSearchingWindow = true;
currentRideState.value = RideState.searching;
driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr;
update();
// 2. إرسال الطلب للسيرفر (add_ride.php)
bool rideCreated = await postRideDetailsToServer();
if (!rideCreated) {
// فشل الإنشاء
isSearchingWindow = false;
currentRideState.value = RideState.noRide;
mySnackbarWarning("Could not create ride. Please try again.".tr);
update();
return;
}
// 3. نجاح الإنشاء: إضافة لجدول الانتظار المحلي (اختياري حسب منطقك)
_addRideToWaitingTable();
// 4. 🔥 الاتصال بالسوكيت فوراً وانتظار الرد الحقيقي 🔥
// نغلق أي اتصال سابق ونبدأ اتصالاً جديداً مخصصاً لهذه الرحلة
initConnectionWithSocket();
// تشغيل الماكينة الرئيسية للمراقبة (كحماية إضافية)
// _startMasterTimer();
}
/// دالة لإظهار النافذة المنبثقة لزيادة السعر
void _showIncreaseFeeDialog() {
Get.dialog(
CupertinoAlertDialog(
title: Text("No drivers accepted your request yet".tr),
content: Text(
"Increasing the fare might attract more drivers. Would you like to increase the price?"
.tr),
actions: [
CupertinoDialogAction(
child: Text("Cancel Ride".tr,
style: TextStyle(color: AppColor.redColor)),
onPressed: () {
Get.back();
changeCancelRidePageShow();
// cancelRide(); // دالة إلغاء الرحلة
},
),
CupertinoDialogAction(
child: Text("Increase Fare".tr,
style: TextStyle(color: AppColor.greenColor)),
onPressed: () {
Get.back();
// هنا يمكنك عرض نافذة أخرى لإدخال السعر الجديد
// وبعدها استدعاء الدالة التالية
// كمثال، سنزيد السعر بنسبة 10%
double newPrice = totalPassenger * 1.10;
increasePriceAndRestartSearch(newPrice);
},
),
],
),
barrierDismissible: false,
);
}
/// دالة لتحديث السعر وإعادة بدء البحث
Future<void> increasePriceAndRestartSearch(double newPrice) async {
totalPassenger = newPrice;
update();
// await CRUD().post(link: AppLink.updateRides, payload: {
// "id": rideId,
// "price": newPrice.toStringAsFixed(2),
// });
CRUD().post(link: "${AppLink.server}/ride/rides/update.php", payload: {
"id": rideId,
"price": newPrice.toStringAsFixed(2),
});
// تصفير القائمة لأن السعر تغير، ويجب إبلاغ الجميع (حتى من وصله الإشعار سابقاً)
Log.print(
'[increasePrice] Price changed. Clearing notified list to resend.');
notifiedDrivers.clear();
_searchStartTime = DateTime.now();
_currentSearchPhase = 0;
isSearchingWindow = true;
update();
_startMasterTimer();
}
/// (دالة جديدة موحدة)
/// هذه هي الدالة الوحيدة المسؤولة عن بدء عملية قبول الرحلة.
/// يتم استدعاؤها إما من إشعار Firebase أو من البحث الدوري (Polling).
/// هي تحتوي على "حارس البوابة" لمنع تضارب السباق.
/// (دالة موحدة وصارمة)
/// تستدعى من FCM أو من Polling عند اكتشاف قبول السائق
/// تمنع تضارب السباق وتضمن تنفيذ المنطق مرة واحدة فقط
/// متغير لمنع التكرار (Race Condition Guard)
Future<dynamic> driverArrivePassengerDialoge() {
return Get.defaultDialog(
barrierDismissible: false,
title: 'Hi ,I Arrive your site'.tr,
titleStyle: AppStyle.title,
middleText: 'Please go to Car Driver'.tr,
middleTextStyle: AppStyle.title,
confirm: MyElevatedButton(
title: 'Ok I will go now.'.tr,
onPressed: () {
NotificationService.sendNotification(
target: driverToken.toString(),
title: 'Hi ,I will go now'.tr,
body: 'I will go now'.tr,
isTopic: false, // Important: this is a token
tone: 'ding',
driverList: [],
category: 'Hi ,I will go now',
);
Get.back();
remainingTime = 0;
update();
}));
}
/// (دالة خاصة جديدة)
/// تحتوي على كل المنطق الفعلي لبدء الرحلة.
///
Timer? _waitPassengerTimer;
static const int _waitPassengerTotalSeconds = 300;
int _waitPassengerElapsedSeconds = 0;
/// **إيقاف عداد انتظار الراكب (Stop Wait Timer)**
///
/// تقوم هذه الدالة بإلغاء التايمر النشط فوراً لتحرير الموارد ومنع تسريب الذاكرة.
///
/// * [resetUI]: (اختياري) عند وضعه `true`، يتم تصفير العدادات وتحديث الواجهة لإخفاء التوقيت القديم.
void _stopWaitPassengerTimer({bool resetUI = false}) {
// 1. الإلغاء الآمن للتايمر (Safe Cancellation)
_waitPassengerTimer?.cancel();
_waitPassengerTimer = null;
// 2. تصفير قيم الواجهة (Reset State)
if (resetUI) {
progressTimerDriverWaitPassenger5Minute = 0.0;
remainingTimeDriverWaitPassenger5Minute = 0;
stringRemainingTimeDriverWaitPassenger5Minute = '00:00';
// ✅ تحديث الواجهة فوراً (GetX)
update();
}
}
void _executeBeginRideLogic() {
Log.print('[executeBeginRideLogic] تنفيذ منطق بدء الرحلة...');
_stopWaitPassengerTimer(resetUI: true); // <-- إضافة
// 1. تصفير كل عدادات ما قبل الرحلة
timeToPassengerFromDriverAfterApplied = 0;
remainingTime = 0;
remainingTimeToPassengerFromDriverAfterApplied = 0;
remainingTimeDriverWaitPassenger5Minute = 0;
// 2. تحديث الحالة والواجهة
rideTimerBegin = true;
statusRide = 'Begin';
isDriverInPassengerWay = false;
isDriverArrivePassenger = false; // لإخفاء واجهة "السائق وصل"
// 3. (من كود الإشعار الخاص بك)
box.write(BoxName.passengerWalletTotal, '0');
update(); // تحديث الواجهة قبل بدء المؤقتات
// 4. بدء مؤقتات الرحلة الفعلية
rideIsBeginPassengerTimer(); // مؤقت عداد مدة الرحلة
// runWhenRideIsBegin(); // مؤقت تتبع موقع السائق أثناء الرحلة
// 5. إشعار الراكب (من كود الإشعار الخاص بك)
NotificationController().showNotification(
'Trip is Begin'.tr,
'The trip has started! Feel free to contact emergency numbers, share your trip, or activate voice recording for the journey'
.tr,
'start');
}
// متغير لمنع التكرار
bool _isRideStartedProcessed = false;
/// **معالجة بدء الرحلة الموحدة (Unified Ride Start Handler)**
///
/// تستدعى عند استلام حدث بدء الرحلة سواء من السوكيت أو FCM.
/// تضمن انتقال التطبيق لحالة [RideState.inProgress] مرة واحدة فقط.
Future<void> processRideBegin({String source = "Unknown"}) async {
// منطقك الحالي
if (currentRideState.value == RideState.inProgress ||
_isRideStartedProcessed) {
return;
}
_isRideStartedProcessed = true;
currentRideState.value = RideState.inProgress;
statusRide = 'Begin';
// إيقاف مؤقت الانتظار
remainingTimeDriverWaitPassenger5Minute = 0;
_stopWaitPassengerTimer();
// 1) بيانات السائق والرحلة
final driverName = this.driverName ?? 'السائق';
final driverPhone = this.driverPhone ?? '';
final carBrand = this.make ?? '';
final carColor = this.carColor ?? '';
final carPlate = this.licensePlate ?? '';
final carDetails = '$carBrand$carColor$carPlate';
final driverLat = driverCarsLocationToPassengerAfterApplied.last.latitude;
final driverLng = driverCarsLocationToPassengerAfterApplied.last.longitude;
final passengerLat = passengerLocation.latitude;
final passengerLng = passengerLocation.longitude;
final destLat = myDestination.latitude ?? 0.0;
final destLng = myDestination.longitude ?? 0.0;
final int timeToDestinationSeconds =
durationToRide; // موجود عندك من التايمر
final double totalDistanceMeters = double.parse(distanceByPassenger);
// [PiP] تم تعطيل الإشعار المستمر القديم (Foreground Service) واستبداله بـ PiP
// await RideTrackingNative.updateRideTracking(
// driverName: driverName,
// driverPhone: driverPhone,
// carDetails: carDetails,
// driverLat: driverLat,
// driverLng: driverLng,
// passengerLat: passengerLat,
// passengerLng: passengerLng,
// destLat: destLat,
// destLng: destLng,
// rideState: 'inProgress',
// estimatedTimeMinutes: (timeToDestinationSeconds / 60).round(),
// totalDistanceMeters: totalDistanceMeters,
// );
// 3) بدء التايمر الداخلي الخاص بك (للـ ETA داخل التطبيق نفسه)
rideIsBeginPassengerTimer();
update();
}
late Duration durationToAdd;
late DateTime newTime = DateTime.now();
int hours = 0;
int minutes = 0;
// --- إضافة جديدة: للوصول إلى وحدة التحكم بالروابط ---
final DeepLinkController _deepLinkController =
Get.isRegistered<DeepLinkController>()
? Get.find<DeepLinkController>()
: Get.put(DeepLinkController());
// ------------------------------------------------
void onChangedPassengerCount(int newValue) {
selectedPassengerCount = newValue;
update();
}
void onChangedPassengersChoose() {
isPassengerChosen = true;
update();
}
void getCurrentLocationFormString() async {
currentLocationToFormPlaces = true;
currentLocationString = 'Waiting for your location'.tr;
await getLocation();
currentLocationString = passengerLocation.toString();
newStartPointLocation = passengerLocation;
update();
}
List<String> coordinatesWithoutEmpty = [];
void getMapPointsForAllMethods() async {
clearPolyline();
isMarkersShown = false;
isWayPointStopsSheetUtilGetMap = false;
isWayPointSheet = false;
durationToRide = 0;
distanceOfDestination = 0;
wayPointSheetHeight = 0;
remainingTime = 25;
haveSteps = true;
// Filter out empty value
coordinatesWithoutEmpty =
placesCoordinate.where((coord) => coord.isNotEmpty).toList();
latestPosition = LatLng(
double.parse(coordinatesWithoutEmpty.last.split(',')[0]),
double.parse(coordinatesWithoutEmpty.last.split(',')[1]));
for (var i = 0; i < coordinatesWithoutEmpty.length; i++) {
if ((i + 1) < coordinatesWithoutEmpty.length) {
await getMapPoints(
coordinatesWithoutEmpty[i].toString(),
coordinatesWithoutEmpty[i + 1].toString(),
i,
);
if (i == 0) {
startNameAddress = data[0]['start_address'];
}
if (i == coordinatesWithoutEmpty.length) {
endNameAddress = data[0]['end_address'];
}
}
}
// isWayPointStopsSheet = false;
if (haveSteps) {
String latestWaypoint =
placesCoordinate.lastWhere((coord) => coord.isNotEmpty);
latestPosition = LatLng(
double.parse(latestWaypoint.split(',')[0]),
double.parse(latestWaypoint.split(',')[1]),
);
}
updateCameraForDistanceAfterGetMap();
changeWayPointStopsSheet();
bottomSheet();
showBottomSheet1();
update();
}
void convertHintTextStartNewPlaces(int index) {
if (placesStart.isEmpty) {
hintTextStartPoint = 'Search for your Start point'.tr;
update();
} else {
var res = placesStart[index];
hintTextStartPoint = res['displayName']?['text'] ??
res['formattedAddress'] ??
'Unknown Place';
double? lat = res['location']?['latitude'];
double? lng = res['location']?['longitude'];
if (lat != null && lng != null) {
newStartPointLocation = LatLng(lat, lng);
}
update();
}
}
void convertHintTextPlaces(int index, var res) {
if (placeListResponseAll[index].isEmpty) {
placeListResponseAll[index] = res;
hintTextwayPointStringAll[index] = 'Search for your Start point'.tr;
update();
} else {
hintTextwayPointStringAll[index] = res['name'];
currentLocationStringAll[index] = res['name'];
placesCoordinate[index] =
'${res['geometry']['location']['lat']},${res['geometry']['location']['lng']}';
placeListResponseAll[index] = [];
allTextEditingPlaces[index].clear();
// double lat = wayPoint0[index]['geometry']['location']['lat'];
// double lng = wayPoint0[index]['geometry']['location']['lng'];
// newPointLocation0 = LatLng(lat, lng);
update();
Get.back();
}
}
void convertHintTextPlaces1(int index) {
if (wayPoint1.isEmpty) {
hintTextwayPoint1 = 'Search for your Start point'.tr;
update();
} else {
hintTextwayPoint1 = wayPoint1[index]['name'];
currentLocationString1 = wayPoint1[index]['name'];
double lat = wayPoint1[index]['geometry']['location']['lat'];
double lng = wayPoint1[index]['geometry']['location']['lng'];
newPointLocation1 = LatLng(lat, lng);
update();
}
}
void convertHintTextPlaces2(int index) {
if (wayPoint1.isEmpty) {
hintTextwayPoint2 = 'Search for your Start point'.tr;
update();
} else {
hintTextwayPoint2 = wayPoint2[index]['name'];
currentLocationString2 = wayPoint1[index]['name'];
double lat = wayPoint2[index]['geometry']['location']['lat'];
double lng = wayPoint2[index]['geometry']['location']['lng'];
newPointLocation2 = LatLng(lat, lng);
update();
}
}
void convertHintTextPlaces3(int index) {
if (wayPoint1.isEmpty) {
hintTextwayPoint3 = 'Search for your Start point'.tr;
update();
} else {
hintTextwayPoint3 = wayPoint3[index]['name'];
currentLocationString3 = wayPoint1[index]['name'];
double lat = wayPoint3[index]['geometry']['location']['lat'];
double lng = wayPoint3[index]['geometry']['location']['lng'];
newPointLocation3 = LatLng(lat, lng);
update();
}
}
void convertHintTextPlaces4(int index) {
if (wayPoint1.isEmpty) {
hintTextwayPoint4 = 'Search for your Start point'.tr;
update();
} else {
hintTextwayPoint4 = wayPoint4[index]['name'];
currentLocationString4 = wayPoint1[index]['name'];
double lat = wayPoint4[index]['geometry']['location']['lat'];
double lng = wayPoint4[index]['geometry']['location']['lng'];
newPointLocation4 = LatLng(lat, lng);
update();
}
}
void convertHintTextDestinationNewPlaces(int index) {
if (placesDestination.isEmpty) {
hintTextDestinationPoint = 'Search for your destination'.tr;
update();
} else {
var res = placesDestination[index];
hintTextDestinationPoint = res['displayName']?['text'] ??
res['formattedAddress'] ??
'Unknown Place';
double? lat = res['location']?['latitude'];
double? lng = res['location']?['longitude'];
if (lat != null && lng != null) {
newMyLocation = LatLng(lat, lng);
// 🔥 الحل: تحريك الكاميرا فوراً للهدف حتى لا يتم مسحه عند إغلاق الكيبورد 🔥
mapController
?.animateCamera(CameraUpdate.newLatLngZoom(newMyLocation, 16));
}
update();
}
}
void convertHintTextDestinationNewPlacesFromRecent(
List recentLocations, int index) {
hintTextDestinationPoint = recentLocations[index]['name'];
double lat = recentLocations[index]['latitude'];
double lng = recentLocations[index]['longitude'];
newMyLocation = LatLng(lat, lng);
// 🔥 تحريك الكاميرا فوراً 🔥
mapController?.animateCamera(CameraUpdate.newLatLngZoom(newMyLocation, 16));
update();
}
// final mainBottomMenuMap = GlobalKey<AnimatedContainer>();
void changeBottomSheetShown({bool? forceValue}) {
if (forceValue != null) {
isBottomSheetShown = forceValue;
} else {
isBottomSheetShown = !isBottomSheetShown;
}
heightBottomSheetShown = isBottomSheetShown == true ? 250 : 0;
update();
}
void changeCashConfirmPageShown() {
isCashConfirmPageShown = !isCashConfirmPageShown;
isCashSelectedBeforeConfirmRide = true;
cashConfirmPageShown = isCashConfirmPageShown == true ? 250 : 0;
// to get or sure picker point for origin //todo
// isPickerShown = true;
// clickPointPosition();
update();
}
void changePaymentMethodPageShown() {
isPaymentMethodPageShown = !isPaymentMethodPageShown;
paymentPageShown = isPaymentMethodPageShown == true ? Get.height * .6 : 0;
update();
}
void changeMapType() {
mapType = !mapType;
// heightButtomSheetShown = isButtomSheetShown == true ? 240 : 0;
update();
}
void changeMapTraffic() {
mapTrafficON = !mapTrafficON;
update();
}
void changeisAnotherOreder(bool val) {
isAnotherOreder = val;
update();
}
void changeIsWhatsAppOrder(bool val) {
isWhatsAppOrder = val;
update();
}
void sendSMS(String to) async {
// Get the driver's phone number.
String driverPhone =
(dataCarsLocationByPassenger['message'][carsOrder]['phone'].toString());
// Format the message.
String message =
'Hi! This is ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n I am using ${box.read(AppInformation.appName)} to ride with $passengerName as the driver. $passengerName \nis driving a $model\n with license plate $licensePlate.\n I am currently located at $passengerLocation.\n If you need to reach me, please contact the driver directly at\n\n $driverPhone.';
// Launch the URL to send the SMS.
launchCommunication('sms', to, message);
}
String formatSyrianPhone(String phone) {
// Remove spaces and +
phone = phone.replaceAll(' ', '').replaceAll('+', '');
// If starts with 00963 → remove 00 → 963
if (phone.startsWith('00963')) {
phone = phone.replaceFirst('00963', '963');
}
// If starts with 0963 (common mistake) → fix it
if (phone.startsWith('0963')) {
phone = phone.replaceFirst('0963', '963');
}
// If starts with 963 (already correct)
if (phone.startsWith('963')) {
return phone; // nothing to do
}
// If starts with 09 → remove leading 0 → add 963
if (phone.startsWith('09')) {
return '963' + phone.substring(1); // 9xxxxxxxxx
}
// If starts with 9xxxxxxxxx (no country code)
if (phone.startsWith('9') && phone.length == 9) {
return '963' + phone;
}
// Otherwise return raw phone
return phone;
}
void sendWhatsapp(String to) async {
// Normalize phone number before sending
String formattedPhone = formatSyrianPhone(to);
// Message body
String message =
'${'${'Hi! This is'.tr} ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n${' I am using'.tr}'} ${AppInformation.appName}${' to ride with'.tr} $passengerName${' as the driver.'.tr} $passengerName \n${'is driving a '.tr}$model\n${' with license plate '.tr}$licensePlate.\n${' I am currently located at '.tr} https://www.google.com/maps/place/${passengerLocation.latitude},${passengerLocation.longitude}.\n${' If you need to reach me, please contact the driver directly at'.tr}\n\n $driverPhone.';
// Send WhatsApp message
launchCommunication('whatsapp', formattedPhone, message);
}
void changeCancelRidePageShow() {
showCancelRideBottomSheet();
isCancelRidePageShown = !isCancelRidePageShown;
// : cancelRide();
update();
}
void getDrawerMenu() {
heightMenuBool = !heightMenuBool;
widthMapTypeAndTraffic = heightMenuBool == true ? 0 : 50;
heightMenu = heightMenuBool == true ? 80 : 0;
widthMenu = heightMenuBool == true ? 110 : 0;
update();
}
calcualateDistsanceInMetet(LatLng prev, current) async {
double distance2 = Geolocator.distanceBetween(
prev.latitude,
prev.longitude,
current.latitude,
current.longitude,
);
return distance2;
}
StreamController<int> _timerStreamController = StreamController<int>();
Stream<int> get timerStream => _timerStreamController.stream;
bool isTimerFromDriverToPassengerAfterAppliedRunning = true;
bool isTimerRunning = false; // Flag to track if the timer is running
int beginRideInterval = 10; // Interval in seconds for getBeginRideFromDriver
void startTimerFromDriverToPassengerAfterApplied() {
stopTimerFromDriverToPassengerAfterApplied();
if (isTimerRunning) return;
isTimerRunning = true;
isTimerFromDriverToPassengerAfterAppliedRunning = true;
int secondsElapsed = 0;
// استدعاء فوري لأول مرة
// getDriverCarsLocationToPassengerAfterApplied();
Timer.periodic(const Duration(seconds: 1), (timer) {
// --- التغيير الجوهري هنا ---
// شرط الإيقاف: نتوقف فقط إذا انتهت الرحلة أو ألغيت، أو تم إيقاف التايمر يدوياً
// لم نعد نعتمد على تجاوز الوقت المقدر (timeToPassenger) كشرط للإيقاف
bool isRideActive = (statusRide == 'Apply' ||
statusRide == 'Arrived' ||
statusRide == 'Begin' ||
currentRideState.value == RideState.driverApplied ||
currentRideState.value == RideState.driverArrived ||
currentRideState.value == RideState.inProgress);
if (!isRideActive || !isTimerFromDriverToPassengerAfterAppliedRunning) {
timer.cancel();
isTimerRunning = false;
if (!_timerStreamController.isClosed) {
_timerStreamController.close();
}
return;
}
secondsElapsed++;
if (!_timerStreamController.isClosed) {
_timerStreamController.add(secondsElapsed);
}
// تحديث الواجهة للوقت المتبقي (شكلياً فقط للراكب)
// حتى لو أصبح الوقت سالباً (تأخر السائق)، سنظهره كـ 00:00 أو نتركه سالباً
remainingTimeToPassengerFromDriverAfterApplied =
timeToPassengerFromDriverAfterApplied - secondsElapsed;
if (remainingTimeToPassengerFromDriverAfterApplied < 0) {
remainingTimeToPassengerFromDriverAfterApplied = 0;
}
int minutes =
(remainingTimeToPassengerFromDriverAfterApplied / 60).floor();
int seconds = remainingTimeToPassengerFromDriverAfterApplied % 60;
stringRemainingTimeToPassenger =
'$minutes:${seconds.toString().padLeft(2, '0')}';
// تحويل الوقت أو المسافة إلى نسبة من 0.0 إلى 1.0
double currentProgress = 1 -
(remainingTimeToPassengerFromDriverAfterApplied /
timeToPassengerFromDriverAfterApplied);
// 🔴 التعديل هنا: نحدث الآيفون كل 5 ثواني فقط للحفاظ على البطارية وتجنب حظر أبل
if (secondsElapsed % 5 == 0) {
double currentProgress = 1 -
(remainingTimeToPassengerFromDriverAfterApplied /
(timeToPassengerFromDriverAfterApplied == 0
? 1
: timeToPassengerFromDriverAfterApplied));
IosLiveActivityService.updateRideActivity(
status: 'waiting',
driverName: driverName ?? 'السائق',
carDetails:
'$make$model$carColor', // من الأفضل إظهار اللون أيضاً
etaText: stringRemainingTimeToPassenger,
progress: currentProgress.clamp(0.0, 1.0),
);
}
// جلب موقع السائق كل 4 ثواني (Polling) ما دامت الرحلة نشطة
if (secondsElapsed % beginRideInterval == 0) {
// 2. تحديث موقع الراكب للسائق
uploadPassengerLocation();
} else {
update();
}
});
}
// Function to stop the timer
void stopTimerFromDriverToPassengerAfterApplied() {
isTimerFromDriverToPassengerAfterAppliedRunning = false;
update();
}
void startTimerDriverWaitPassenger5Minute() {
// لا تبدأ إلا إذا فعلاً وصلنا
if (currentRideState.value != RideState.driverArrived) return;
// 1) أوقف أي عداد سابق (تتبع وصول السائق)
stopTimerFromDriverToPassengerAfterApplied();
isTimerRunning = false;
// 2) أوقف عداد الانتظار إن كان شغال من قبل (منع تكرار)
_stopWaitPassengerTimer();
// 3) جهّز UI الانتظار
isDriverArrivePassenger = true;
isDriverInPassengerWay = false;
timeToPassengerFromDriverAfterApplied = 0;
_waitPassengerElapsedSeconds = 0;
remainingTimeDriverWaitPassenger5Minute = _waitPassengerTotalSeconds;
progressTimerDriverWaitPassenger5Minute = 0;
int m = (remainingTimeDriverWaitPassenger5Minute / 60).floor();
int s = remainingTimeDriverWaitPassenger5Minute % 60;
stringRemainingTimeDriverWaitPassenger5Minute =
'$m:${s.toString().padLeft(2, '0')}';
update();
// 4) ابدأ Timer.periodic (يمكن إلغاؤه فوراً)
_waitPassengerTimer = Timer.periodic(const Duration(seconds: 1), (t) {
// أول ما تتحول إلى inProgress (أو أي حالة غير arrived) أوقف فوراً
if (currentRideState.value != RideState.driverArrived) {
_stopWaitPassengerTimer(resetUI: true);
// إخفاء واجهة "السائق وصل" إذا بدأت الرحلة
if (currentRideState.value == RideState.inProgress) {
isDriverArrivePassenger = false;
}
update();
return;
}
_waitPassengerElapsedSeconds++;
int remaining = _waitPassengerTotalSeconds - _waitPassengerElapsedSeconds;
if (remaining < 0) remaining = 0;
remainingTimeDriverWaitPassenger5Minute = remaining;
progressTimerDriverWaitPassenger5Minute =
_waitPassengerElapsedSeconds / _waitPassengerTotalSeconds;
int minutes = (remaining / 60).floor();
int seconds = remaining % 60;
stringRemainingTimeDriverWaitPassenger5Minute =
'$minutes:${seconds.toString().padLeft(2, '0')}';
update();
if (remaining == 0) {
_stopWaitPassengerTimer();
// هنا إذا بدك: طبّق غرامة انتظار / اعرض رسالة / إلخ
}
});
}
// Create a StreamController to manage the timer values
final timerController = StreamController<int>();
// Start the timer when the ride begins
void beginRideTimer() {
// Set up the timer to run every second
Timer.periodic(const Duration(seconds: 1), (timer) {
// Update the timer value and notify listeners
timerController.add(timer.tick);
update();
});
}
// Stop the timer when the ride ends
void stopRideTimer() {
timerController.close();
update();
}
Timer? _rideProgressTimer;
bool _hasShownSpeedWarning = false; // متغير لحالة التنبيه
/// **بدء مؤقت الرحلة للراكب (Passenger Ride Timer)**
///
/// تقوم هذه الدالة بإدارة العداد الزمني للرحلة بمجرد بدئها (حالة [RideState.inProgress]).
///
/// **المهام الرئيسية:**
/// 1. **دقة التوقيت:** تعتمد على فرق الوقت الحقيقي (`DateTime.difference`) لضمان دقة العداد حتى لو خرج المستخدم من التطبيق وعاد.
/// 2. **مراقبة السرعة:** تفحص سرعة المركبة كل ثانية، وتطلق تحذيراً [_triggerSpeedWarning] إذا تجاوزت 100 كم/س.
/// 3. **تحديث الواجهة:** تقوم بتحديث شريط التقدم والوقت المتبقي لحظياً.
/// 4. **الإيقاف التلقائي:** تتوقف تلقائياً عند انتهاء الوقت أو تغير حالة الرحلة.
void rideIsBeginPassengerTimer() {
// 1. تنظيف أي تايمر سابق
_rideProgressTimer?.cancel();
_hasShownSpeedWarning = false; // تصفير حالة التنبيه
// 2. تحديد وقت الوصول المتوقع بدقة
DateTime now = DateTime.now();
DateTime expectedArrivalTime = now.add(Duration(seconds: durationToRide));
// تنسيق وقت الوصول للعرض
var arrivalTime = DateFormat('hh:mm a').format(expectedArrivalTime);
box.write(BoxName.arrivalTime, arrivalTime);
Log.print("⏳ Ride Timer Started. Duration: $durationToRide sec");
// 3. بدء التايمر الدوري
_rideProgressTimer =
Timer.periodic(const Duration(seconds: 1), (timer) async {
// أ) شرط الإيقاف الحاسم: إذا انتهت الرحلة أو ألغيت
if (currentRideState.value != RideState.inProgress) {
timer.cancel();
return;
}
// ب) حساب الوقت المتبقي بناءً على الساعة الحالية (أدق من العد)
DateTime currentNow = DateTime.now();
int remainingSeconds =
expectedArrivalTime.difference(currentNow).inSeconds;
if (remainingSeconds < 0) remainingSeconds = 0;
// تحديث المتغيرات
remainingTimeTimerRideBegin = remainingSeconds;
// حساب النسبة المئوية (حماية من القسمة على صفر)
progressTimerRideBegin =
durationToRide > 0 ? 1 - (remainingSeconds / durationToRide) : 1.0;
// ج) تنسيق الوقت للعرض
int minutes = (remainingSeconds / 60).floor();
int seconds = remainingSeconds % 60;
stringRemainingTimeRideBegin =
'$minutes:${seconds.toString().padLeft(2, '0')}';
// نحول progressTimerRideBegin (0..1) إلى نسبة (0..100)
final percent = (progressTimerRideBegin * 100).clamp(0, 100).toInt();
// ==============================================================
// 🔔 د) تحديث الإشعارات (هنا تم حل مشكلة الإزعاج)
// ==============================================================
// 1. تحديث الآيفون (Live Activity): يمكن تحديثه كل 5 ثواني لأنه "تحديث صامت" للشاشة فقط ولا يصدر صوتاً
if (remainingSeconds % 5 == 0 || remainingSeconds == 0) {
IosLiveActivityService.updateRideActivity(
status: 'ongoing', // ['waiting', 'ongoing']
driverName: driverName ?? '',
carDetails: '$make$model$carColor',
etaText: stringRemainingTimeRideBegin,
progress: progressTimerRideBegin.clamp(0.0, 1.0),
);
}
// 2. تحديث إشعار الهاتف العادي (RideLiveNotification):
// نحدثه كل دقيقة (60 ثانية) بدلاً من 5 ثواني حتى لا يزعج الراكب بالرنين المستمر!
if (remainingSeconds % 60 == 0 || remainingSeconds == 0) {
await RideLiveNotification.showTripInProgress(
percentage: percent,
etaText: stringRemainingTimeRideBegin,
);
}
// ==============================================================
// هـ) منطق الإشعارات لمنتصف الرحلة (يصدر تنبيه مرة واحدة فقط)
if (progressTimerRideBegin >= 0.25 &&
progressTimerRideBegin < 0.26 &&
!_hasShownSpeedWarning) {
// يمكن إضافة منطق إشعار منتصف الرحلة هنا
}
// و) مراقبة السرعة (Speed Check)
if (speed > 100 && !_hasShownSpeedWarning) {
_hasShownSpeedWarning = true; // ✅ قفل التنبيه حتى لا يتكرر
_triggerSpeedWarning();
}
// إعادة تفعيل التنبيه إذا انخفضت السرعة (إعادة ضبط الأمان)
if (speed < 80 && _hasShownSpeedWarning) {
_hasShownSpeedWarning = false;
}
// ز) إنهاء التايمر إذا انتهى الوقت
if (remainingSeconds <= 0) {
timer.cancel();
}
update();
});
}
/// **عرض تحذير السرعة الزائدة (Speed Warning Trigger)**
///
/// تظهر نافذة منبثقة (Dialog) وإشعاراً محلياً لتحذير الراكب عند اكتشاف سرعة عالية (> 100 كم/س).
///
/// **الخيارات المتاحة للمستخدم:**
/// * **مشاركة التفاصيل:** لإرسال رسالة استغاثة عبر واتساب.
/// * **أنا بخير:** لإغلاق التنبيه والاستمرار في الرحلة.
void _triggerSpeedWarning() {
NotificationController().showNotification("Warning: Speeding detected!".tr,
'You can call or record audio of this trip'.tr, 'tone1');
Get.defaultDialog(
barrierDismissible: false,
title: "Warning: Speeding detected!".tr,
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
content: Column(
children: [
Icon(Icons.speed, size: 50, color: AppColor.redColor),
const SizedBox(height: 10),
Text(
"We noticed the speed is exceeding 100 km/h. Please slow down for your safety..."
.tr,
textAlign: TextAlign.center,
style: AppStyle.title,
),
],
),
confirm: MyElevatedButton(
title: "Share Trip Details".tr,
kolor: AppColor.redColor,
onPressed: () {
Get.back();
_shareTripDetailsSOS();
},
),
cancel: MyElevatedButton(
title: "I'm Safe".tr,
kolor: AppColor.greenColor,
onPressed: () {
Get.back();
},
),
);
}
/// **تفعيل وضع الطوارئ للمركبة (sosPassenger)**
///
/// تقوم بإظهار حوار تأكيدي للمستخدم لسؤاله عما إذا كان يرغب في إرسال
/// إشارة استغاثة عبر واتساب.
void sosPassenger() {
Get.defaultDialog(
barrierDismissible: false,
title: "Emergency SOS".tr,
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
content: Column(
children: [
Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor),
const SizedBox(height: 10),
Text(
"Do you want to send an emergency message to your SOS contact?".tr,
textAlign: TextAlign.center,
style: AppStyle.title,
),
],
),
confirm: MyElevatedButton(
title: "Send SOS".tr,
kolor: AppColor.redColor,
onPressed: () {
Get.back();
_shareTripDetailsSOS();
},
),
cancel: MyElevatedButton(
title: "I'm Safe".tr,
kolor: AppColor.greenColor,
onPressed: () {
Get.back();
},
),
);
}
/// **مشاركة تفاصيل الرحلة للطوارئ (SOS Share)**
///
/// تقوم بتجهيز رسالة نصية مفصلة تحتوي على بيانات الرحلة الحالية وإرسالها
/// عبر تطبيق WhatsApp لرقم الطوارئ المحفوظ.
///
/// **البيانات المرسلة:**
/// * موقع الانطلاق والوصول.
/// * اسم السائق، رقم الهاتف، ونوع السيارة.
/// * رابط مباشر للموقع الحالي على خرائط جوجل.
void _shareTripDetailsSOS() {
String message = "**Emergency SOS from Passenger:**\n";
String origin = startNameAddress;
String destination = endNameAddress;
message += "* ${'Origin'.tr}: $origin\n";
message += "* ${'Destination'.tr}: $destination\n";
message += "* ${'Driver Name'.tr}: $driverName\n";
message += "* ${'Car'.tr}: $make - $model - $licensePlate\n";
message += "* ${'Phone'.tr}: $driverPhone\n\n";
// رابط جوجل مابس صحيح
message +=
"${'Location'.tr}: https://www.google.com/maps/search/?api=1&query=${passengerLocation.latitude},${passengerLocation.longitude}\n";
message += "Please help! Contact me as soon as possible.".tr;
launchCommunication(
'whatsapp', box.read(BoxName.sosPhonePassenger), message);
}
int progressTimerRideBeginVip = 0;
int elapsedTimeInSeconds = 0; // Timer starts from 0
String stringElapsedTimeRideBegin = '0:00';
String stringElapsedTimeRideBeginVip = '0:00';
bool rideInProgress = true; // To control when to stop the timer
void rideIsBeginPassengerTimerVIP() async {
rideInProgress = true; // Start the ride timer
bool sendSOS = false;
while (rideInProgress) {
await Future.delayed(const Duration(seconds: 1));
// Increment elapsed time
elapsedTimeInSeconds++;
// Update the time display
int minutes = (elapsedTimeInSeconds / 60).floor();
int seconds = elapsedTimeInSeconds % 60;
stringElapsedTimeRideBeginVip =
'$minutes:${seconds.toString().padLeft(2, '0')}';
// Check for speed and SOS conditions
if (speed > 100 && !sendSOS) {
Get.defaultDialog(
barrierDismissible: false,
title: "Warning: Speeding detected!".tr,
titleStyle: AppStyle.title,
content: Text(
"We noticed the speed is exceeding 100 km/h. Please slow down for your safety. If you feel unsafe, you can share your trip details with a contact or call the police using the red SOS button."
.tr,
style: AppStyle.title,
),
confirm: MyElevatedButton(
title: "Share Trip Details".tr,
onPressed: () {
Get.back();
// Implement sharing trip details logic here
String message = "**Emergency SOS from Passenger:**\n";
// Get trip details from GetX or relevant provider
String origin = passengerLocation.toString();
String destination = myDestination.toString();
String driverName = passengerName;
String driverCarPlate = licensePlate;
// Add trip details to the message
message += "* ${'Origin'.tr}: $origin\n";
message += "* ${'Destination'.tr}: $destination\n";
message += "* ${'Driver Name'.tr}: $driverName\n";
message += "* ${'Driver Car Plate'.tr}: $driverCarPlate\n\n";
message += "* ${'Driver Phone'.tr}: $driverPhone\n\n";
// Add current location
message +=
"${'Current Location'.tr}:https://www.google.com/maps/place/${passengerLocation.latitude},${passengerLocation.longitude} \n";
// Append a call to action
message += "Please help! Contact me as soon as possible.".tr;
// Launch WhatsApp communication
launchCommunication(
'whatsapp', box.read(BoxName.sosPhonePassenger), message);
sendSOS = true;
},
kolor: AppColor.redColor,
),
cancel: MyElevatedButton(
title: "Cancel".tr,
onPressed: () {
Get.back();
},
kolor: AppColor.greenColor,
),
);
}
// Update the UI
update();
}
}
Future<void> tripFinishedFromDriver() async {
Log.print('🧹 Cleaning UI for Finish');
// إغلاق أي ديالوج مفتوح
if (Get.isDialogOpen == true) Get.back();
if (Get.isBottomSheetOpen == true) Get.back();
statusRide = 'Finished';
currentRideState.value = RideState.finished; // تثبيت الحالة
// إيقاف البحث والعدادات
isSearchingWindow = false;
rideTimerBegin = false;
shouldFetch = false;
// إيقاف التايمرات
stopAllTimers();
resetAllMapStates();
clearPolyline();
clearMarkersExceptStartEnd();
markers.clear();
update();
}
StreamController<String> _beginRideStreamController =
StreamController<String>.broadcast();
Stream<String> get beginRideStream => _beginRideStreamController.stream;
bool isBeginRideFromDriverRunning = false;
// Call this method to listen to the stream
void listenToBeginRideStream() {
beginRideStream.listen((status) {
Log.print("Ride status: $status");
// Perform additional actions based on the status
}, onError: (error) {
Log.print("Error in Begin Ride Stream: $error");
});
}
begiVIPTripFromPassenger() async {
timeToPassengerFromDriverAfterApplied = 0;
remainingTime = 0;
isBottomSheetShown = false;
remainingTimeToPassengerFromDriverAfterApplied = 0;
remainingTimeDriverWaitPassenger5Minute = 0;
rideTimerBegin = true;
statusRideVip = 'Begin';
isDriverInPassengerWay = false;
isDriverArrivePassenger = false;
update();
// isCancelRidePageShown = true;
rideIsBeginPassengerTimerVIP();
runWhenRideIsBegin();
}
Map rideStatusFromStartApp = {};
bool isStartAppHasRide = false;
getRideStatusFromStartApp() async {
try {
var res = await CRUD().get(
link: AppLink.getRideStatusFromStartApp,
payload: {'passenger_id': box.read(BoxName.passengerID)});
// Log.print(res);
Log.print('rideStatusFromStartApp: $res');
// Log.print('1070');
if (res == 'failure') {
rideStatusFromStartApp = {
'data': {'status': 'NoRide', 'needsReview': false}
};
isStartAppHasRide = false;
Log.print(
"No rides found for the given passenger ID within the last hour.");
}
rideStatusFromStartApp = jsonDecode(res);
if (rideStatusFromStartApp['data']['status'] == 'Begin' ||
rideStatusFromStartApp['data']['status'] == 'Apply' ||
rideStatusFromStartApp['data']['status'] == 'Applied') {
statusRide = rideStatusFromStartApp['data']['status'];
isStartAppHasRide = true;
RideState.inProgress;
driverId = rideStatusFromStartApp['data']['driver_id'];
passengerName = rideStatusFromStartApp['data']['driverName'];
driverRate = rideStatusFromStartApp['data']['rateDriver'].toString();
statusRideFromStart = true;
update();
Map<String, dynamic> tripData =
box.read(BoxName.tripData) as Map<String, dynamic>;
final String pointsString = tripData['polyline'];
List<LatLng> decodedPoints =
await compute(decodePolylineIsolate, pointsString);
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
for (int i = 0; i < decodedPoints.length; i++) {
polylineCoordinates.add(decodedPoints[i]);
}
var polyline = Polyline(
polylineId: const PolylineId('main_route'),
points: polylineCoordinates,
width: 6,
color: const Color(0xFF2196F3),
);
polyLines = {...polyLines, polyline};
timeToPassengerFromDriverAfterApplied = 0;
remainingTime = 0;
remainingTimeToPassengerFromDriverAfterApplied = 0;
remainingTimeDriverWaitPassenger5Minute = 0;
rideTimerBegin = true;
isDriverInPassengerWay = false;
isDriverArrivePassenger = false;
// update();
// isCancelRidePageShown = true;
durationToAdd = tripData['distance_m'];
rideIsBeginPassengerTimer();
runWhenRideIsBegin();
update();
}
} catch (e) {
// Handle the error or perform any necessary actions
}
}
void driverArrivePassenger() {
timeToPassengerFromDriverAfterApplied = 0;
remainingTime = 0;
// isCancelRidePageShown = true;
update();
rideIsBeginPassengerTimer();
// runWhenRideIsBegin();
}
void cancelTimerToPassengerFromDriverAfterApplied() {
timerToPassengerFromDriverAfterApplied?.cancel();
}
void clearPlacesDestination() {
placesDestination = [];
hintTextDestinationPoint = 'Search for your destination'.tr;
update();
}
void clearPlacesStart() {
placesStart = [];
hintTextStartPoint = 'Search for your Start point'.tr;
update();
}
void clearPlaces(int index) {
placeListResponseAll[index] = [];
hintTextwayPointStringAll[index] = 'Search for waypoint'.tr;
update();
}
void clearPlaces1() {
wayPoint1 = [];
hintTextwayPoint1 = 'Search for waypoint'.tr;
update();
}
void clearPlaces2() {
wayPoint2 = [];
hintTextwayPoint2 = 'Search for waypoint'.tr;
update();
}
void clearPlaces3() {
wayPoint3 = [];
hintTextwayPoint3 = 'Search for waypoint'.tr;
update();
}
void clearPlaces4() {
wayPoint4 = [];
hintTextwayPoint4 = 'Search for waypoint'.tr;
update();
}
int selectedReason = -1;
String? cancelNote;
void selectReason0(int index, String note) {
selectedReason = index;
cancelNote = note;
update();
}
void getDialog(String title, String? midTitle, VoidCallback onPressed) {
final textToSpeechController = Get.find<TextToSpeechController>();
Get.defaultDialog(
title: title,
titleStyle: AppStyle.title,
middleTextStyle: AppStyle.title,
content: Column(
children: [
IconButton(
onPressed: () async {
await textToSpeechController.speakText(title ?? midTitle!);
},
icon: const Icon(Icons.headphones)),
Text(
midTitle!,
style: AppStyle.title,
)
],
),
confirm: MyElevatedButton(
title: 'Ok'.tr,
onPressed: onPressed,
kolor: AppColor.greenColor,
),
cancel: MyElevatedButton(
title: 'Cancel',
kolor: AppColor.redColor,
onPressed: () {
Get.back();
}));
}
Future<Map<String, double>?> extractCoordinatesFromLinkAsync(
String link) async {
try {
int urlStartIndex = link.indexOf(RegExp(r'https?://'));
if (urlStartIndex == -1) return null;
String cleanLink = link.substring(urlStartIndex).trim();
Uri uri = Uri.parse(cleanLink);
String finalUrl = cleanLink;
// فك التوجيه للروابط المختصرة
if (cleanLink.contains('goo.gl') ||
cleanLink.contains('maps.google.com')) {
try {
var response =
await http.get(uri).timeout(const Duration(seconds: 5));
finalUrl = response.request?.url.toString() ?? cleanLink;
} catch (e) {
Log.print('Redirect logic failed, using original: $e');
}
}
// الأنماط المشتركة لخرائط جوجل (تكون دائماً Lat ثم Lng)
RegExp regex = RegExp(r'(-?\d+\.\d+)[,/~](-?\d+\.\d+)');
var match = regex.firstMatch(finalUrl);
if (match != null) {
double lat = double.parse(match.group(1)!);
double lng = double.parse(match.group(2)!);
// 🔥 منطق التصحيح الذاتي (Smart Swap) للمنطقة (سوريا/الأردن/مصر)
// إذا كان الرقم الأول أكبر من الرقم الثاني بشكل واضح، فهذا يعني أن الرابط مقلوب أو أننا نحتاج للتأكد
// في منطقتنا Latitude حوالي 30-35 و Longitude حوالي 36-44
if (lat > 40 && lat > lng) {
Log.print("⚠️ Detected Swapped Coordinates in Link. Correcting...");
double temp = lat;
lat = lng;
lng = temp;
}
return {
'latitude': lat,
'longitude': lng,
};
}
} catch (e) {
Log.print('Error parsing location link: $e');
}
return null;
}
double latitudeWhatsApp = 0;
double longitudeWhatsApp = 0;
void handleWhatsAppLink(String link) async {
Map<String, double>? coordinates =
await extractCoordinatesFromLinkAsync(link);
if (coordinates != null) {
latitudeWhatsApp = coordinates['latitude']!;
longitudeWhatsApp = coordinates['longitude']!;
Log.print(
'Extracted coordinates: Lat: $latitudeWhatsApp, Long: $longitudeWhatsApp');
// Use these coordinates in your app as needed
} else {
Log.print('Failed to extract coordinates from the link');
}
}
void goToWhatappLocation() async {
if (sosFormKey.currentState!.validate()) {
// 1. استخراج الإحداثيات أولاً بشكل محلي لضمان عدم حدوث سباق بيانات (Race Condition)
Map<String, double>? coordinates =
await extractCoordinatesFromLinkAsync(whatsAppLocationText.text);
if (coordinates != null) {
latitudeWhatsApp = coordinates['latitude']!;
longitudeWhatsApp = coordinates['longitude']!;
Log.print(
'📍 Final Coordinates for OSM: Lat: $latitudeWhatsApp, Lng: $longitudeWhatsApp');
changeIsWhatsAppOrder(true);
Get.back();
// إعداد الوجهة
myDestination = LatLng(latitudeWhatsApp, longitudeWhatsApp);
// تحريك الكاميرا لموقع الراكب (البداية) وليس الوجهة فوراً لضمان تحميل الخريطة
if (passengerLocation != null) {
await mapController?.animateCamera(CameraUpdate.newLatLng(
LatLng(passengerLocation.latitude, passengerLocation.longitude)));
}
changeMainBottomMenuMap();
passengerStartLocationFromMap = true;
isPickerShown = true;
update();
} else {
mySnackbarWarning('لم نتمكن من استخراج الموقع من الرابط');
}
}
}
int currentTimeSearchingCaptainWindow = 0;
late String driverPhone = '';
late String driverRate = '';
late String passengerName = '';
late String carColor = '';
late String colorHex = '';
late String carYear = '';
late String model = '';
late String make = '';
late String licensePlate = '';
String driverOrderStatus = 'yet';
bool isDriversTokensSend = false;
Set<String> notifiedDrivers = {};
/// [إضافة جديدة]
/// دالة مخصصة لإضافة الرحلة إلى جدول الانتظار (waiting_ride)
Future<void> _addRideToWaitingTable() async {
try {
await CRUD().post(link: AppLink.addWaitingRide, payload: {
'id': rideId.toString(),
"start_location":
'${startLocation.latitude},${startLocation.longitude}',
"end_location": '${endLocation.latitude},${endLocation.longitude}',
"date": DateTime.now().toString(),
"time": DateTime.now().toString(),
"price": totalPassenger.toStringAsFixed(2),
'passenger_id': box.read(BoxName.passengerID).toString(),
'status': 'waiting', // الحالة الرئيسية لجدول الانتظار
'carType': box.read(BoxName.carType),
'passengerRate': passengerRate.toStringAsFixed(2),
'price_for_passenger': totalME.toStringAsFixed(2),
'distance': distance.toStringAsFixed(1),
'duration': duration.toStringAsFixed(1),
'payment_method':
Get.find<PaymentController>().isWalletChecked ? 'wallet' : 'cash',
"passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(),
});
Log.print('[WaitingTable] Ride $rideId added to waiting_ride table.');
} catch (e) {
Log.print('Error adding ride to waiting_ride table: $e');
}
}
String driversStatusForSearchWindow = '';
bool isDriversDataValid() {
return dataCarsLocationByPassenger != 'failure' &&
dataCarsLocationByPassenger != null &&
dataCarsLocationByPassenger.containsKey('message') &&
dataCarsLocationByPassenger['message'] != null;
}
void showNoDriversDialog() {
Get.dialog(
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: CupertinoAlertDialog(
title: Text("No Car or Driver Found in your area.".tr,
style: AppStyle.title
.copyWith(fontSize: 20, fontWeight: FontWeight.bold)),
content: Text("No Car or Driver Found in your area.".tr,
style: AppStyle.title.copyWith(fontSize: 16)),
actions: [
CupertinoDialogAction(
onPressed: () {
Get.back();
Get.offAll(() => const MapPagePassenger());
},
child:
Text('OK'.tr, style: TextStyle(color: AppColor.greenColor)),
),
],
),
),
barrierDismissible: false,
);
}
Future<bool> postRideDetailsToServer() async {
// التأكد من وجود مسار
if (polylineCoordinates.isEmpty) return false;
startLocation = polylineCoordinates.first;
endLocation = polylineCoordinates.last;
// تجهيز البيانات الكاملة (Data Enrichment) لإرسالها للـ PHP
Map<String, dynamic> payload = {
// 1. البيانات الأساسية
"start_location": '${startLocation.latitude},${startLocation.longitude}',
"end_location": '${endLocation.latitude},${endLocation.longitude}',
"date": DateTime.now().toString(),
"time": DateTime.now().toString(),
"endtime": "00:00:00", // أو حسب حساباتك
"price": totalPassenger.toStringAsFixed(2),
"passenger_id": box.read(BoxName.passengerID).toString(),
"driver_id": "0", // لم يحدد بعد
"status": "waiting",
"carType": box.read(BoxName.carType),
"price_for_driver": totalPassenger.toString(), // أو المعادلة الخاصة بك
"price_for_passenger": totalME.toString(),
"distance": distance.toString(),
// 2. بيانات الراكب (ليستخدمها PHP لبناء الـ Payload دون استعلام)
"passenger_name": box.read(BoxName.name).toString(),
"passenger_phone": box.read(BoxName.phone).toString(),
"passenger_token": box.read(BoxName.tokenFCM).toString(),
"passenger_email": box.read(BoxName.email).toString(),
"passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(),
"passenger_rating": (passengerRate ?? 5.0).toString(),
// 3. بيانات الواجهة الإضافية
"start_name": startNameAddress,
"end_name": endNameAddress,
"duration_text": "${(durationToRide / 60).floor()}", // نص الوقت
"distance_text": "$distance", // نص المسافة
"is_wallet": Get.find<PaymentController>().isWalletChecked.toString(),
"has_steps": Get.find<WayPointController>().wayPoints.length > 1
? 'true'
: 'false',
// نقاط التوقف (إذا وجدت)
"step0": placesCoordinate.length > 0 ? placesCoordinate[0] : "",
"step1": placesCoordinate.length > 1 ? placesCoordinate[1] : "",
"step2": placesCoordinate.length > 2 ? placesCoordinate[2] : "",
"step3": placesCoordinate.length > 3 ? placesCoordinate[3] : "",
"step4": placesCoordinate.length > 4 ? placesCoordinate[4] : "",
};
Log.print(
'🏁 Ride Registration Detail: $startNameAddress -> $endNameAddress');
Log.print(' 📦 Payload: $payload');
try {
// الاتصال بـ add_ride.php
var response = await CRUD().post(
link: "${AppLink.server}/ride/rides/add_ride.php", // تأكد من المسار
payload: payload);
var jsonResponse = (response is String) ? jsonDecode(response) : response;
if (jsonResponse['status'] == 'success') {
rideId = jsonResponse['message'].toString(); // حفظ ID الرحلة
Log.print("✅ Ride Created ID: $rideId");
return true;
} else {
Log.print("❌ Ride Creation Failed: $response");
return false;
}
} catch (e) {
Log.print("❌ Exception in postRide: $e");
return false;
}
}
late LatLng endLocation;
late LatLng startLocation;
StreamController<String> _rideStatusStreamController =
StreamController<String>.broadcast();
Stream<String> get rideStatusStream => _rideStatusStreamController.stream;
int maxAttempts = 28;
Future<void> rideAppliedFromDriver(bool isApplied) async {
Log.print('[rideAppliedFromDriver] 🚀 Starting logic...');
// 1. جلب بيانات السائق والسيارة المحدثة من السيرفر
await getUpdatedRideForDriverApply(rideId);
// تنبيهات الأسعار حسب نوع السيارة
if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) {
NotificationController().showNotification('Fixed Price'.tr,
'The captain is responsible for the route.'.tr, 'ding');
} else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) {
NotificationController().showNotification('Attention'.tr,
'The price may increase if the route changes.'.tr, 'ding');
}
isApplied = true;
statusRide = 'Apply';
rideConfirm = false;
isSearchingWindow = false;
_isDriverAppliedLogicExecuted = true; // ضمان عدم التكرار
update(); // تحديث أولي
// 2. جلب موقع السائق الأولي فوراً (Blocking await)
await getDriverCarsLocationToPassengerAfterApplied();
// 3. إذا توفر الموقع: حساب المسافة/الزمن ورسم المسار
if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) {
LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last;
Log.print(
'[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation');
// أ) استدعاء API لحساب المسافة والزمن الدقيق (بدون رسم)
await getInitialDriverDistanceAndDuration(driverPos, passengerLocation);
// ب) رسم خط المسار (Visual only)
await drawDriverPathOnly(driverPos, passengerLocation);
// ج) ضبط الكاميرا لتشمل السائق والراكب
_fitCameraToPoints(driverPos, passengerLocation);
} else {
Log.print(
'[rideAppliedFromDriver] ⚠️ Warning: Driver location not found yet.');
}
// 4. تشغيل تايمر التتبع المستمر (الذي سيقوم بتناقص الوقت الذي جلبناه من API)
startTimerFromDriverToPassengerAfterApplied();
// إغلاق الستريم القديم
if (!_rideStatusStreamController.isClosed)
_rideStatusStreamController.close();
}
/// دالة لجلب المسافة والزمن بين السائق والراكب عند قبول الطلب
/// تستخدم API سريع (overview=false)
Future<void> getInitialDriverDistanceAndDuration(
LatLng driverPos, LatLng passengerPos) async {
final String apiUrl = 'https://routec.intaleq.xyz/route';
final String apiKey = Env.mapKeyOsm;
final String origin = '${driverPos.latitude},${driverPos.longitude}';
final String dest = '${passengerPos.latitude},${passengerPos.longitude}';
// الرابط المطلوب: steps=false&overview=false (سريع جداً للبيانات فقط)
final Uri uri = Uri.parse(
'$apiUrl?origin=$origin&destination=$dest&steps=false&overview=false');
try {
Log.print('[InitialCalc] Fetching distance/duration from: $uri');
final response = await http.get(uri, headers: {'X-API-KEY': apiKey});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['status'] == 'ok') {
// 1. استخراج الزمن (بالثواني)
// نستخدم المعامل 1.5348 أو 1.4 حسب منطقك السابق لتقدير الوقت الواقعي
double durationSecondsRaw = (data['duration_s'] as num).toDouble();
int finalDurationSeconds = (durationSecondsRaw * kDurationScalar)
.toInt(); // kDurationScalar = 1.5348
// 2. استخراج المسافة (بالأمتار)
double distanceMeters = (data['distance_m'] as num).toDouble();
// 3. تحديث المتغيرات في الكنترولر
timeToPassengerFromDriverAfterApplied = finalDurationSeconds;
remainingTimeToPassengerFromDriverAfterApplied = finalDurationSeconds;
distanceByPassenger =
(distanceMeters).toStringAsFixed(0); // المسافة نصاً
// يمكنك أيضاً تحديث durationToPassenger إذا كنت تستخدمها
durationToPassenger = finalDurationSeconds;
Log.print(
'[InitialCalc] ✅ Success: Duration=${finalDurationSeconds}s, Distance=${distanceMeters}m');
update(); // تحديث الواجهة لعرض الوقت الجديد فوراً
}
} else {
Log.print('[InitialCalc] ❌ API Error: ${response.statusCode}');
}
} catch (e) {
Log.print('[InitialCalc] 💥 Exception: $e');
}
}
// دالة خفيفة وسريعة لرسم خط المسار فقط (بدون أسعار أو خطوات)
Future<void> drawDriverPathOnly(LatLng driverPos, LatLng passengerPos) async {
final String apiUrl = 'https://routec.intaleq.xyz/route';
final String apiKey = Env.mapKeyOsm;
final String origin = '${driverPos.latitude},${driverPos.longitude}';
final String dest = '${passengerPos.latitude},${passengerPos.longitude}';
// استخدام overview=full للدقة، و steps=false للسرعة
final Uri uri = Uri.parse(
'$apiUrl?origin=$origin&destination=$dest&steps=false&overview=full');
try {
final response = await http.get(uri, headers: {'X-API-KEY': apiKey});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['status'] == 'ok' && data['polyline'] != null) {
final String pointsString = data['polyline'];
// فك التشفير
List<LatLng> decodedPoints =
await compute(decodePolylineIsolate, pointsString);
// إزالة خط مسار السائق القديم فقط
polyLines = polyLines
.where((p) => p.polylineId.value != 'driver_route')
.toSet();
// إضافة الخط الجديد
polyLines = {
...polyLines,
Polyline(
polylineId: const PolylineId('driver_route'),
points: decodedPoints,
color: const Color(0xFF333333), // لون مميز لمسار السائق
width: 5,
)
};
// لا تستدعي update هنا، سيتم استدعاؤها في الدالة الأب (getDriverCars...) لتقليل عدد التحديثات
}
}
} catch (e) {
Log.print('Error drawing driver path: $e');
}
}
// دالة مساعدة لضبط الكاميرا
void _fitCameraToPoints(LatLng p1, LatLng p2) async {
if (mapController == null) return;
// 1. معالجة حالة النقاط المتطابقة (تمنع الكراش في Android)
if (p1.latitude == p2.latitude && p1.longitude == p2.longitude) {
try {
mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 17));
} catch (e) {
Log.print("Error animating to single point: $e");
}
return;
}
// 2. حساب الحدود
double minLat = min(p1.latitude, p2.latitude);
double maxLat = max(p1.latitude, p2.latitude);
double minLng = min(p1.longitude, p2.longitude);
double maxLng = max(p1.longitude, p2.longitude);
// 3. تقليل الهوامش لتجنب خطأ "View size too small"
// نستخدم 50 بدلاً من 100 ليكون آمناً مع الخرائط الصغيرة
double padding = 50.0;
try {
await mapController?.animateCamera(
CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
),
left: padding,
top: padding,
right: padding,
bottom: padding,
),
);
} catch (e) {
Log.print("Error animating bounds (Map might be resizing): $e");
// محاولة بديلة آمنة: تحريك الكاميرا للمنتصف فقط دون Bounds
try {
LatLng center = LatLng((minLat + maxLat) / 2, (minLng + maxLng) / 2);
mapController?.animateCamera(CameraUpdate.newLatLngZoom(center, 14));
} catch (e) {
Log.print("Error: $e");
}
}
}
// Listening to the Stream
void listenToRideStatusStream() {
rideStatusStream.listen((rideStatus) {
Log.print("Ride Status: $rideStatus");
// Handle updates based on the ride status
}, onError: (error) {
Log.print("Error in Ride Status Stream: $error");
// Handle stream errors
}, onDone: () {
Log.print("Ride status stream closed.");
});
}
void start15SecondTimer(String rideId) {
Timer(const Duration(seconds: 15), () {
// delayAndFetchRideStatusForAllDriverAvailable(rideId);
});
}
// Replaces void startTimer()
Timer?
_uiCountdownTimer; // Add this variable to your class to manage lifecycle
void startUiCountdown() {
// Cancel any existing timer to avoid duplicates
_uiCountdownTimer?.cancel();
// Reset variables
progress = 0;
remainingTime = durationTimer;
_uiCountdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
// Logic from your loop, but non-blocking
int i = timer.tick; // current tick
progress = i / durationTimer;
remainingTime = durationTimer - i;
if (remainingTime <= 0) {
timer.cancel(); // Stop this specific timer
rideConfirm = false;
// Add the duration to the tracking time logic
timeToPassengerFromDriverAfterApplied += durationToPassenger;
// Note: We do NOT call startTimerFromDriverToPassengerAfterApplied() here
// because we already started it in rideAppliedFromDriver!
timerEnded(); // Call your existing completion logic
}
update(); // Update the UI progress bar
});
}
void timerEnded() async {
runEvery30SecondsUntilConditionMet();
isCancelRidePageShown = false;
Log.print('isCancelRidePageShown: $isCancelRidePageShown');
update();
}
Future<String> getRideStatus(String rideId) async {
final response = await CRUD().get(
link: "${AppLink.rideServerSide}/ride/rides/getRideStatus.php",
payload: {'id': rideId});
Log.print(response);
Log.print('2176');
return jsonDecode(response)['data'];
}
late String driverCarModel,
driverCarMake,
driverLicensePlate,
driverName = '';
Future<void> getUpdatedRideForDriverApply(String rideId) async {
// حماية مبدئية: إذا كان المعرف غير صالح لا تكمل
if (rideId == 'yet' || rideId.isEmpty) return;
try {
final res = await CRUD().get(
link: "${AppLink.server}/ride/rides/getRideOrderID.php",
payload: {'passengerID': box.read(BoxName.passengerID).toString()});
if (res != 'failure') {
var response = jsonDecode(res);
Log.print('getUpdatedRideForDriverApply Response: $response');
// [هام] التحقق من أن data عبارة عن Map وليست false أو null
// هذا يمنع الخطأ: Class 'bool' has no instance method '[]'
if (response['status'] == 'success' &&
response['data'] != null &&
response['data'] is Map) {
var data = response['data'];
// استخدام ?.toString() ?? '' للحماية من القيم الفارغة (Null Safety)
driverId = data['driver_id']?.toString() ?? '';
driverPhone = data['phone']?.toString() ?? '';
driverCarMake = data['make']?.toString() ?? '';
model = data['model']?.toString() ?? '';
colorHex = data['color_hex']?.toString() ?? '';
carColor = data['color']?.toString() ?? '';
make = data['make']?.toString() ?? '';
licensePlate = data['car_plate']?.toString() ?? '';
// دمج الاسم الأول والأخير للراكب
String firstName = data['passengerName']?.toString() ?? '';
String lastName = data['last_name']?.toString() ?? '';
passengerName =
lastName.isNotEmpty ? "$firstName $lastName" : firstName;
driverName = data['driverName']?.toString() ?? '';
// [هام] التوكن ضروري للإشعارات
driverToken = data['token']?.toString() ?? '';
carYear = data['year']?.toString() ?? '';
driverRate = data['ratingDriver']?.toString() ?? '5.0';
update(); // تحديث الواجهة بالبيانات الجديدة
} else {
Log.print(
"Warning: Ride data not found or invalid (data is false/null)");
// اختياري: يمكنك هنا التعامل مع حالة عدم العثور على السائق بعد
}
}
} catch (e) {
Log.print("Error in getUpdatedRideForDriverApply: $e");
}
}
late LatLng currentDriverLocation;
late double headingList;
// Future getCarsLocationByPassengerAndReloadMarker() async {
// if (statusRide == 'wait') {
// carsLocationByPassenger = [];
// LatLngBounds bounds = calculateBounds(
// passengerLocation.latitude, passengerLocation.longitude, 7000);
// var res;
// if (box.read(BoxName.carType) == 'Lady') {
// res = await CRUD()
// .get(link: AppLink.getFemalDriverLocationByPassenger, payload: {
// 'southwestLat': bounds.southwest.latitude.toString(),
// 'southwestLon': bounds.southwest.longitude.toString(),
// 'northeastLat': bounds.northeast.latitude.toString(),
// 'northeastLon': bounds.northeast.longitude.toString(),
// });
// } else if (box.read(BoxName.carType) == 'Speed') {
// res = await CRUD().get(
// link: AppLink.getCarsLocationByPassengerSpeed,
// payload: {
// 'southwestLat': bounds.southwest.latitude.toString(),
// 'southwestLon': bounds.southwest.longitude.toString(),
// 'northeastLat': bounds.northeast.latitude.toString(),
// 'northeastLon': bounds.northeast.longitude.toString(),
// },
// );
// } else if (box.read(BoxName.carType) == 'Delivery') {
// res = await CRUD().get(
// link: AppLink.getCarsLocationByPassengerDelivery,
// payload: {
// 'southwestLat': bounds.southwest.latitude.toString(),
// 'southwestLon': bounds.southwest.longitude.toString(),
// 'northeastLat': bounds.northeast.latitude.toString(),
// 'northeastLon': bounds.northeast.longitude.toString(),
// },
// );
// } else {
// res = await CRUD()
// .get(link: AppLink.getCarsLocationByPassenger, payload: {
// 'southwestLat': bounds.southwest.latitude.toString(),
// 'southwestLon': bounds.southwest.longitude.toString(),
// 'northeastLat': bounds.northeast.latitude.toString(),
// 'northeastLon': bounds.northeast.longitude.toString(),
// });
// }
// if (res == 'failure') {
// noCarString = true;
// dataCarsLocationByPassenger = res;
// update();
// } else {
// // Get.snackbar('no car', 'message');
// noCarString = false;
// dataCarsLocationByPassenger = jsonDecode(res);
// // if (dataCarsLocationByPassenger.length > carsOrder) {
// driverId = dataCarsLocationByPassenger['message'][carsOrder]
// ['driver_id']
// .toString();
// gender = dataCarsLocationByPassenger['message'][carsOrder]['gender']
// .toString();
// // }
// carsLocationByPassenger.clear(); // Clear existing markers
// // late LatLng lastDriverLocation; // Initialize a variable for last location
// for (var i = 0;
// i < dataCarsLocationByPassenger['message'].length;
// i++) {
// var json = dataCarsLocationByPassenger['message'][i];
// // CarLocationModel model = CarLocationModel.fromJson(json);
// if (carLocationsModels.length < i + 1) {
// // carLocationsModels.add(model);
// markers.add(
// Marker(
// markerId: MarkerId(json['latitude']),
// position: LatLng(
// double.parse(json['latitude']),
// double.parse(json['longitude']),
// ),
// rotation: double.parse(json['heading']),
// icon: json['model'].toString().contains('دراجة')
// ? motoIcon
// : json['gender'] == 'Male'.tr
// ? carIcon
// : ladyIcon,
// ),
// );
// driversToken.add(json['token']);
// // driversToken = json['token'];
// } else {
// // carLocationsModels[i] = model;
// markers[i] = Marker(
// markerId: MarkerId(json['latitude']),
// position: LatLng(
// double.parse(json['latitude']),
// double.parse(json['longitude']),
// ),
// rotation: double.parse(json['heading']),
// icon: json['model'].contains('دراجة')
// ? motoIcon
// : json['gender'] == 'Male'.tr
// ? carIcon
// : ladyIcon,
// );
// // driversToken = json['token'];
// driversToken.add(json['token']);
// }
// }
// }
// update();
// }
// }
Map<String, Timer> _animationTimers = {};
final int updateIntervalMs = 100; // Update every 100ms
final double minMovementThreshold =
10; // Minimum movement in meters to trigger update
Future getCarForFirstConfirm(String carType) async {
bool foundCars = false;
int attempt = 0;
// Set up the periodic timer
Timer? timer = Timer.periodic(const Duration(seconds: 4), (Timer t) async {
// Attempt to get car location
foundCars = await getCarsLocationByPassengerAndReloadMarker();
Log.print('foundCars: $foundCars');
if (foundCars) {
// If cars are found, cancel the timer and exit the search
t.cancel();
} else if (attempt >= 4) {
// After 4 attempts, stop the search
t.cancel();
// No cars found after 4 attempts
// MyDialog().getDialog(
// "No Car or Driver Found in your area.".tr,
// "No Car or Driver Found in your area.".tr,
// () {
// Get.back();
// },
// );
if (!foundCars) {
noCarString = true;
dataCarsLocationByPassenger = 'failure';
}
update();
}
attempt++; // Increment attempt
});
}
void startCarLocationSearch(String carType) {
int searchInterval = 5; // Interval in seconds
Log.print('searchInterval: $searchInterval');
int boundIncreaseStep = 2500; // Initial bounds in meters
Log.print('boundIncreaseStep: $boundIncreaseStep');
int maxAttempts = 3; // Maximum attempts to increase bounds
int maxBoundIncreaseStep = 6000; // Maximum bounds increase step
int attempt = 0; // Current attempt
Log.print('initial attempt: $attempt');
Timer.periodic(Duration(seconds: searchInterval), (Timer timer) async {
Log.print('Current attempt: $attempt'); // Log current attempt
bool foundCars = false;
if (attempt >= maxAttempts) {
timer.cancel();
if (foundCars == false) {
noCarString = true;
// dataCarsLocationByPassenger = 'failure';
update();
}
// return;
} else if (reloadStartApp == true) {
Log.print('reloadStartApp: $reloadStartApp');
foundCars = await getCarsLocationByPassengerAndReloadMarker();
Log.print('foundCars: $foundCars');
if (foundCars) {
timer.cancel();
} else {
attempt++;
Log.print(
'Incrementing attempt to: $attempt'); // Log incremented attempt
if (boundIncreaseStep < maxBoundIncreaseStep) {
boundIncreaseStep += 1500; // Increase bounds
if (boundIncreaseStep > maxBoundIncreaseStep) {
boundIncreaseStep =
maxBoundIncreaseStep; // Ensure it does not exceed the maximum
}
Log.print(
'New boundIncreaseStep: $boundIncreaseStep'); // Log new bounds
}
}
}
});
}
// String getLocationArea(double latitude, double longitude) {
// final locations = box.read(BoxName.locationName) ?? [];
// for (final location in locations) {
// final locationData = location as Map<String, dynamic>;
// // Debugging: Print location data
// // Log.print('Location Data: $locationData');
// // Convert string values to double
// final minLatitude =
// double.tryParse(locationData['min_latitude'].toString()) ?? 0.0;
// final maxLatitude =
// double.tryParse(locationData['max_latitude'].toString()) ?? 0.0;
// final minLongitude =
// double.tryParse(locationData['min_longitude'].toString()) ?? 0.0;
// final maxLongitude =
// double.tryParse(locationData['max_longitude'].toString()) ?? 0.0;
// // Debugging: Print converted values
// Log.print(
// 'Converted Values: minLatitude=$minLatitude, maxLatitude=$maxLatitude, minLongitude=$minLongitude, maxLongitude=$maxLongitude');
// if (latitude >= minLatitude &&
// latitude <= maxLatitude &&
// longitude >= minLongitude &&
// longitude <= maxLongitude) {
// box.write(BoxName.serverChosen, (locationData['server_link']));
// // Log.print(
// // 'locationData----server_link: ${(locationData['server_link'])}');
// return locationData['name'];
// }
// }
// // Default case
// box.write(BoxName.serverChosen, AppLink.IntaleqSyriaServer);
// return 'Cairo';
// }
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.server); // مثال: اختر سيرفر سوريا للبيانات
return 'Jordan';
}
// 2. فحص سوريا
if (isPointInPolygon(passengerPoint, CountryPolygons.syriaBoundary)) {
box.write(BoxName.countryCode, 'Syria');
box.write(BoxName.serverChosen, AppLink.server);
return 'Syria';
}
// 3. فحص مصر
if (isPointInPolygon(passengerPoint, CountryPolygons.egyptBoundary)) {
box.write(BoxName.countryCode, 'Egypt');
box.write(BoxName.serverChosen, AppLink.server);
return 'Egypt';
}
// 4. الافتراضي (إذا كان خارج المناطق المخدومة)
box.write(BoxName.countryCode, 'Jordan');
box.write(BoxName.serverChosen, AppLink.server);
return 'Unknown Location (Defaulting to Jordan)';
}
Future<bool> getCarsLocationByPassengerAndReloadMarker() async {
// 1. تنظيف القائمة والماركرز
carsLocationByPassenger = [];
if (passengerLocation.latitude == 0 && passengerLocation.longitude == 0) {
return false; // لا يوجد موقع للراكب
}
// 2. طلب بسيط ومباشر (أنا هنا، أعطني السائقين حولي)
var res = await CRUD().get(
link: AppLink.getCarsLocationByPassenger,
payload: {
'lat': passengerLocation.latitude.toString(),
'lng': passengerLocation.longitude.toString(),
'radius': '5', // نصف القطر ثابت (مثلاً 5 كم) أو يمكنك جعله ديناميكياً
'limit': '50', // أقصى عدد سيارات للعرض
},
);
if (res == 'failure') {
noCarString = true;
update();
return false;
}
// 3. معالجة البيانات
noCarString = false;
var responseData = jsonDecode(res);
// دعم التنسيقين (data أو message) لضمان عدم حدوث كراش
List driversList = [];
if (responseData['status'] == true && responseData['data'] != null) {
driversList = responseData['data'];
} else if (responseData['message'] != null) {
driversList = responseData['message']; // للكود القديم احتياطاً
}
if (driversList.isEmpty) {
carsLocationByPassenger.clear();
update();
return false;
}
carsLocationByPassenger.clear(); // تنظيف الماركرز القديمة
// 4. رسم السيارات على الخريطة
for (var i = 0; i < driversList.length; i++) {
var carData = driversList[i];
// التحقق من الإحداثيات لضمان عدم رسم سيارة في المحيط
double lat = double.tryParse(carData['latitude'].toString()) ?? 0.0;
double lng = double.tryParse(carData['longitude'].toString()) ?? 0.0;
double heading = double.tryParse(carData['heading'].toString()) ?? 0.0;
if (lat == 0.0 || lng == 0.0) continue;
_updateOrCreateMarker(
carData['id'].toString(),
LatLng(lat, lng),
heading,
// الدالة هذه تقرر شكل الأيقونة بناءً على نوع السيارة القادم من السيرفر
_getIconForCar(carData),
);
}
update();
return true;
}
final List<Map<String, dynamic>> fakeCarData = [];
void _addFakeCarMarkers(LatLng center, int count) {
if (fakeCarData.isEmpty) {
Random random = Random();
double radiusInKm = 2.5; // 3 km diameter, so 1.5 km radius
for (int i = 0; i < count; i++) {
// Generate a random angle and distance within the circle
double angle = random.nextDouble() * 2 * pi;
double distance = sqrt(random.nextDouble()) * radiusInKm;
// Convert distance to latitude and longitude offsets
double latOffset = (distance / 111.32); // 1 degree lat ≈ 111.32 km
double lonOffset =
(distance / (111.32 * cos(radians(center.latitude))));
// Calculate new position
double lat = center.latitude + (latOffset * cos(angle));
double lon = center.longitude + (lonOffset * sin(angle));
double heading = random.nextDouble() * 360;
fakeCarData.add({
'id': 'fake_$i',
'latitude': lat,
'longitude': lon,
'heading': heading,
'gender': 'Male', // Randomize gender
});
}
}
for (var carData in fakeCarData) {
_updateOrCreateMarker(
carData['id'].toString(),
LatLng(carData['latitude'], carData['longitude']),
carData['heading'],
_getIconForCar(carData),
);
}
}
String _getIconForCar(Map<String, dynamic> carData) {
if (carData['model'].toString().contains('دراجة')) {
return motoIcon;
} else if (carData['gender'] == 'Female') {
return ladyIcon;
} else {
return carIcon;
}
}
void _updateOrCreateMarker(
String markerId, LatLng newPosition, double newHeading, String icon) {
final mId = MarkerId(markerId);
final existingMarker = markers.cast<Marker?>().firstWhere(
(m) => m?.markerId == mId,
orElse: () => null,
);
if (existingMarker == null) {
markers = {
...markers,
Marker(
markerId: mId,
position: newPosition,
rotation: newHeading,
icon: InlqBitmap.fromStyleImage(icon),
anchor: const Offset(0.5, 0.5),
),
};
update();
} else {
double distance =
_calculateDistance(existingMarker.position, newPosition);
if (distance >= minMovementThreshold) {
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
}
}
}
double _calculateDistance(LatLng start, LatLng end) {
// Implement distance calculation (e.g., Haversine formula)
// For simplicity, this is a placeholder. Replace with actual implementation.
return 1000 *
sqrt(pow(start.latitude - end.latitude, 2) +
pow(start.longitude - end.longitude, 2));
}
String formatSyrianPhoneNumber(String phoneNumber) {
// Trim any whitespace from the input.
String trimmedPhone = phoneNumber.trim();
// If the number starts with '09', remove the leading '0' and prepend '963'.
if (trimmedPhone.startsWith('09')) {
return '963${trimmedPhone.substring(1)}';
}
// If the number already starts with '963', return it as is to avoid duplication.
if (trimmedPhone.startsWith('963')) {
return trimmedPhone;
}
// For any other case (e.g., number starts with '9' without a '0'),
// prepend '963' to ensure the correct format.
return '963$trimmedPhone';
}
String generateTrackingLink(String rideId, String driverId) {
String cleanRideId = rideId.toString().trim();
String cleanDriverId = driverId.toString().trim();
// الكلمة السرية للمطابقة مع السيرفر
const String secretSalt = "Intaleq_Secure_Track_2025";
// الدمج والتشفير
String rawString = "$cleanRideId$cleanDriverId$secretSalt";
var bytes = utf8.encode(rawString);
var digest = md5.convert(bytes);
String token = digest.toString();
// الرابط المباشر لصفحة التتبع
return "https://intaleqapp.com/track/index.php?id=$cleanRideId&token=$token";
}
// 2. الدالة الرئيسية (تم تعديلها لإرسال واتساب بدلاً من الإشعارات)
Future shareTripWithFamily() async {
// التحقق أولاً: هل الرقم موجود؟
String? storedPhone = box.read(BoxName.sosPhonePassenger);
if (storedPhone == null) {
// --- (نفس المنطق القديم: فتح ديالوج لإضافة الرقم) ---
Get.defaultDialog(
title: 'Add SOS Phone'.tr,
titleStyle: AppStyle.title,
content: Form(
key: sosFormKey,
child: MyTextForm(
controller: sosPhonePassengerProfile,
label: 'insert sos phone'.tr,
hint: 'e.g. 0912345678'.tr,
type: TextInputType.phone,
),
),
confirm: MyElevatedButton(
title: 'Add SOS Phone'.tr,
onPressed: () async {
if (sosFormKey.currentState!.validate()) {
Get.back();
// تنسيق الرقم
var numberPhone =
formatSyrianPhoneNumber(sosPhonePassengerProfile.text);
// حفظ في السيرفر
await CRUD().post(
link: AppLink.updateprofile,
payload: {
'id': box.read(BoxName.passengerID),
'sosPhone': numberPhone,
},
);
// حفظ محلياً
box.write(BoxName.sosPhonePassenger, numberPhone);
// استدعاء الدالة مرة أخرى للمتابعة
shareTripWithFamily();
}
}));
return;
}
// --- (المنطق الجديد: إرسال واتساب مباشرة) ---
// 1. التأكد من وجود بيانات للرحلة
if (rideId == 'yet' || driverId.isEmpty) {
Get.snackbar("Alert".tr, "Wait for the trip to start first".tr);
return;
}
// 2. تنسيق الرقم
var numberPhone = formatSyrianPhoneNumber(storedPhone);
// 3. توليد الرابط
String trackingLink = generateTrackingLink(rideId, driverId);
// 4. تجهيز الرسالة (بالإنجليزية وجاهزة للترجمة)
// لاحظ: استخدمت المتغيرات الموجودة في الكنترولر (passengerName هنا عادة يحمل اسم السائق في الكنترولر الخاص بك حسب الكود السابق)
String message = """
مرحباً، تابع رحلتي مباشرة على تطبيق انطلق 🚗
يمكنك تتبع مسار الرحلة من هنا:
$trackingLink
السائق: $passengerName
السيارة: $model - $licensePlate
شكراً لاستخدامك انطلق!
"""
.tr;
String messageEn = """Hello, follow my trip live on Intaleq 🚗
Track my ride here:
$trackingLink
Driver: $passengerName
Car: $model - $licensePlate
Thank you for using Intaleq!
""";
// اختر الرسالة بناءً على اللغة المفضلة (مثال بسيط)
String userLanguage = box.read(BoxName.lang) ?? 'ar';
message = (userLanguage == 'ar') ? message : messageEn;
// وضعنا .tr لكي تتمكن من ترجمتها للعربية في ملفات اللغة إذا أردت، أو تركها إنجليزية
Log.print("Sending WhatsApp to: $numberPhone");
// 5. فتح واتساب
launchCommunication('whatsapp', numberPhone, message);
// (اختياري) حفظ أن التتبع مفعل لتغيير حالة الأيقونة في الواجهة
box.write(BoxName.parentTripSelected, true);
update();
}
Future getTokenForParent() async {
// 1. التحقق أولاً: هل الرقم موجود؟
String? storedPhone = box.read(BoxName.sosPhonePassenger);
if (storedPhone == null) {
// --- حالة الرقم غير موجود: نفتح الديالوج فقط ---
Get.defaultDialog(
title: 'Add SOS Phone'.tr,
titleStyle: AppStyle.title,
content: Form(
key: sosFormKey,
child: MyTextForm(
controller: sosPhonePassengerProfile,
label: 'insert sos phone'.tr,
hint: 'e.g. 0912345678'.tr,
type: TextInputType.phone,
),
),
confirm: MyElevatedButton(
title: 'Add SOS Phone'.tr,
onPressed: () async {
if (sosFormKey.currentState!.validate()) {
// إغلاق الديالوج الحالي
Get.back();
// تنسيق الرقم (تأكد أن هذا التنسيق يطابق ما تم تخزينه عند تسجيل الراكب)
var numberPhone =
formatSyrianPhoneNumber(sosPhonePassengerProfile.text);
// حفظ الرقم في السيرفر (تحديث البروفايل)
await CRUD().post(
link: AppLink.updateprofile,
payload: {
'id': box.read(BoxName.passengerID),
'sosPhone': numberPhone,
},
);
// حفظ الرقم محلياً
box.write(BoxName.sosPhonePassenger, numberPhone);
// استدعاء الدالة مرة أخرى
getTokenForParent();
}
}));
return;
}
generateTrackingLink(rideId, driverId);
// --- حالة الرقم موجود: نكمل التنفيذ ---
var numberPhone = formatSyrianPhoneNumber(storedPhone);
Log.print("Searching for Parent Token with Phone: $numberPhone");
// استدعاء السكريبت (استخدم POST بدلاً من GET)
var res = await CRUD()
.post(link: AppLink.getTokenParent, payload: {'phone': numberPhone});
// التعامل مع الاستجابة
if (res is Map<String, dynamic>) {
handleResponse(res);
} else {
try {
// var jsonRes = jsonDecode(res);
handleResponse(res);
} catch (e) {
Log.print("Error parsing response: $res");
}
}
}
void handleResponse(Map<String, dynamic> res) {
Log.print("Handle Response: $res"); // للتأكد من دخول الدالة
// الحالة 1: الرقم غير مسجل (Failure)
if (res['status'] == 'failure') {
// إذا كان هناك أي ديالوج تحميل مفتوح، نغلقه أولاً، لكن بحذر
if (Get.isDialogOpen ?? false) Get.back();
Get.defaultDialog(
title: "No user found".tr, // اختصرت العنوان ليظهر بشكل أفضل
titleStyle: AppStyle.title,
content: Column(
children: [
Text(
"No passenger found for the given phone number".tr,
style: AppStyle.title, // غيرت الستايل ليكون أصغر قليلاً
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Text(
"Send Intaleq app to him".tr,
style: AppStyle.title
.copyWith(color: AppColor.greenColor, fontSize: 14),
textAlign: TextAlign.center,
)
],
),
confirm: MyElevatedButton(
title: 'Send Invite'.tr,
onPressed: () {
Get.back(); // إغلاق الديالوج
var rawPhone = box.read(BoxName.sosPhonePassenger);
// تأكد أن rawPhone ليس null
if (rawPhone == null) return;
var phone = formatSyrianPhoneNumber(rawPhone);
// تصحيح نص الرسالة
var message = '''Dear Friend,
🚀 I have just started an exciting trip on Intaleq!
Download the app to track my ride:
👉 Android: https://play.google.com/store/apps/details?id=com.Intaleq.intaleq&hl=en-US
👉 iOS: https://apps.apple.com/st/app/intaleq-rider/id6748075179
See you there!
Intaleq Team''';
launchCommunication('whatsapp', phone, message);
}),
cancel: MyElevatedButton(
title: 'Cancel'.tr,
onPressed: () {
Get.back();
}));
}
// الحالة 2: نجاح (Success)
else if (res['status'] == 'success') {
// إغلاق أي ديالوج سابق (مثل Loading)
if (Get.isDialogOpen ?? false) Get.back();
Get.snackbar("Success".tr, "The invitation was sent successfully".tr,
backgroundColor: AppColor.greenColor, colorText: Colors.white);
List tokensData = res['data'];
for (var device in tokensData) {
String tokenParent = device['token'];
NotificationService.sendNotification(
category: "Trip Monitoring",
target: tokenParent,
title: "Trip Monitoring".tr,
body: "Click to track the trip".tr,
isTopic: false,
tone: 'tone1',
driverList: [rideId, driverId],
);
// حفظ آخر توكن
box.write(BoxName.tokenParent, tokenParent);
}
box.write(BoxName.parentTripSelected, true);
}
}
// 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;
}
// Helper function to check if a ray from the point intersects with a polygon segment
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;
}
bool isInUniversity = false;
// Function to check if the passenger is in any university polygon
// Function to check if the passenger is in any university polygon and return the university name
String checkPassengerLocation(LatLng passengerLocation,
List<List<LatLng>> universityPolygons, List<String> universityNames) {
for (int i = 0; i < universityPolygons.length; i++) {
if (isPointInPolygon(passengerLocation, universityPolygons[i])) {
isInUniversity = true;
return "Passenger is in ${universityNames[i]}";
}
}
return "Passenger is not in any university";
}
String passengerLocationStringUnvirsity = 'unKnown';
void getPassengerLocationUniversity() {
// Check if the passenger is inside any of the university polygons and get the university name
passengerLocationStringUnvirsity = checkPassengerLocation(
passengerLocation,
UniversitiesPolygons.universityPolygons,
UniversitiesPolygons.universityNames,
);
if (passengerLocationStringUnvirsity != 'unKnown') {
// Get.snackbar('you are in $passengerLocationStringUnvirsity', "");
}
Log.print(passengerLocationStringUnvirsity);
}
// Initialize polygons from UniversitiesPolygons
void _initializePolygons() {
List<List<LatLng>> universityPolygons =
UniversitiesPolygons.universityPolygons;
for (int i = 0; i < universityPolygons.length; i++) {
Polygon polygon = Polygon(
polygonId: PolygonId('univ_$i'),
points: universityPolygons[i],
fillColor: Colors.blueAccent.withOpacity(0.2),
strokeColor: Colors.blueAccent,
strokeWidth: 2,
);
polygons.add(polygon);
}
update();
}
LatLng driverLocationToPassenger = const LatLng(32, 35);
Future getDriverCarsLocationToPassengerAfterApplied() async {
// driverCarsLocationToPassengerAfterApplied
// 1. الشرط الأمني: تتبع فقط إذا كانت الرحلة نشطة
bool isRideActive = (statusRide == 'Apply' ||
statusRide == 'Arrived' ||
statusRide == 'Begin' ||
currentRideState.value == RideState.driverApplied ||
currentRideState.value == RideState.driverArrived ||
currentRideState.value == RideState.inProgress);
if (!isRideActive ||
statusRide == 'Finished' ||
statusRide == 'Cancel' ||
currentRideState.value == RideState.finished ||
currentRideState.value == RideState.noRide ||
currentRideState.value == RideState.preCheckReview) {
return;
}
// 2. منع التداخل (Blocking)
if (_isFetchingDriverLocation) return;
_isFetchingDriverLocation = true;
try {
var res = await CRUD().get(
link: AppLink.getDriverCarsLocationToPassengerAfterApplied,
payload: {'driver_id': driverId});
if (res != 'failure') {
datadriverCarsLocationToPassengerAfterApplied = jsonDecode(res);
if (datadriverCarsLocationToPassengerAfterApplied['message'] != null &&
datadriverCarsLocationToPassengerAfterApplied['message']
.isNotEmpty) {
var _data =
datadriverCarsLocationToPassengerAfterApplied['message'][0];
LatLng newDriverPos = LatLng(
double.parse(_data['latitude'].toString()),
double.parse(_data['longitude'].toString()));
// أضف هذا السطر لتقليل استهلاك الذاكرة
if (driverCarsLocationToPassengerAfterApplied.length > 10) {
driverCarsLocationToPassengerAfterApplied.removeAt(0);
}
driverLocationToPassenger = newDriverPos;
driverCarsLocationToPassengerAfterApplied.add(newDriverPos);
// 🔥 الإضافة هنا أيضاً 🔥
// 🔥 تحديث التوقيت حتى لو جاءت من API لكي يهدأ الحارس قليلاً
_lastSocketLocationTime = DateTime.now();
_checkAndRecalculateIfDeviated(newDriverPos);
// [تعديل هام] تنظيف آمن: لا نحذف ماركر السائق الحالي
clearMarkersExceptStartEndAndDriver();
// تحريك الماركر
reloadMarkerDriverCarsLocationToPassengerAfterApplied();
}
}
update();
} catch (e) {
Log.print('Error fetching driver location: $e');
} finally {
_isFetchingDriverLocation = false;
}
}
Future runEvery30SecondsUntilConditionMet() async {
// Calculate the duration of the trip in minutes.
double tripDurationInMinutes = durationToPassenger / 5;
int loopCount = tripDurationInMinutes.ceil();
// If the trip duration is less than or equal to 50 minutes, then break the loop.
for (var i = 0; i < loopCount; i++) {
// Wait for 50 seconds.
await Future.delayed(const Duration(seconds: 5));
if (rideTimerBegin == true || statusRide == 'Apply') {
await getDriverCarsLocationToPassengerAfterApplied();
reloadMarkerDriverCarsLocationToPassengerAfterApplied();
}
}
}
Future runWhenRideIsBegin() async {
// Calculate the duration of the trip in minutes.
double tripDurationInMinutes = durationToRide / 6;
int loopCount = tripDurationInMinutes.ceil();
// If the trip duration is less than or equal to 50 minutes, then break the loop.
clearMarkersExceptStartEnd();
for (var i = 0; i < loopCount; i++) {
// Wait for 50 seconds.
await Future.delayed(const Duration(seconds: 4));
// if (rideTimerBegin == true && statusRide == 'Apply') {
await getDriverCarsLocationToPassengerAfterApplied();
// }
reloadMarkerDriverCarsLocationToPassengerAfterApplied();
}
}
Timer? _timer;
// final int updateIntervalMs = 100; // Update every 100ms
// final double minMovementThreshold =
// 1.0; // Minimum movement in meters to trigger update
void clearMarkersExceptStartEndAndDriver() {
markers.removeWhere((marker) {
String id = marker.markerId.value;
// لا تحذف نقطة البداية
if (id == 'start') return false;
// لا تحذف نقطة النهاية
if (id == 'end') return false;
// لا تحذف السائق الحالي
if (id == currentDriverMarkerId) return false;
// احذف أي شيء آخر (مثل السيارات التي ظهرت وقت البحث)
return true;
});
// ملاحظة: لا نستدعي update() هنا لأننا سنستدعيها في نهاية الدالة الرئيسية
}
void clearMarkersExceptStartEnd() {
markers.removeWhere((marker) {
String id = marker.markerId.value;
return id != 'start' && id != 'end';
});
update();
}
// 1. تعريف ID ثابت للسائق طوال الرحلة
String get currentDriverMarkerId => 'driver_marker_$driverId';
void reloadMarkerDriverCarsLocationToPassengerAfterApplied() {
if (datadriverCarsLocationToPassengerAfterApplied == null ||
datadriverCarsLocationToPassengerAfterApplied['message'] == null ||
datadriverCarsLocationToPassengerAfterApplied['message'].isEmpty) {
return;
}
var driverData =
datadriverCarsLocationToPassengerAfterApplied['message'][0];
// جلب الإحداثيات الجديدة
LatLng newPosition = LatLng(double.parse(driverData['latitude'].toString()),
double.parse(driverData['longitude'].toString()));
double newHeading =
double.tryParse(driverData['heading'].toString()) ?? 0.0;
// تحديد الأيقونة
String icon;
if (driverData['model'].toString().contains('دراجة') ||
driverData['make'].toString().contains('دراجة')) {
icon = motoIcon;
} else if (driverData['gender'] == 'Female') {
icon = ladyIcon;
} else {
icon = carIcon;
}
// 2. البحث عن الماركر الجديد وتحديثه أو إنشاء جديد
final String markerId = currentDriverMarkerId;
final mId = MarkerId(markerId);
final existingMarker = markers.cast<Marker?>().firstWhere(
(m) => m?.markerId == mId,
orElse: () => null,
);
if (existingMarker != null) {
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
} else {
markers = {
...markers,
Marker(
markerId: mId,
position: newPosition,
rotation: newHeading,
icon: InlqBitmap.fromStyleImage(icon),
anchor: const Offset(0.5, 0.5),
),
};
update();
}
}
// التأكد من دالة التحريك السلس
void _smoothlyUpdateMarker(
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
double distance = Geolocator.distanceBetween(
oldMarker.position.latitude,
oldMarker.position.longitude,
newPosition.latitude,
newPosition.longitude);
if (distance < 2.0) return;
final MarkerId markerIdKey = oldMarker.markerId;
_animationTimers[markerIdKey.value]?.cancel();
int ticks = 0;
const int totalSteps = 20;
const int stepDuration = 50;
double latStep =
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
double lngStep =
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
LatLng currentPos = oldMarker.position;
double currentHeading = oldMarker.rotation;
_animationTimers[markerIdKey.value] =
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
ticks++;
currentPos =
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
currentHeading += headingStep;
// Update the marker in the set
final updatedMarker = oldMarker.copyWith(
position: currentPos,
rotation: currentHeading,
icon: InlqBitmap.fromStyleImage(icon),
);
markers = {
...markers.where((m) => m.markerId != markerIdKey),
updatedMarker,
};
// Native update through controller to avoid UI rebuild
if (mapController != null) {
mapController!.animateCamera(CameraUpdate.newLatLng(
currentPos)); // Optional: Follow car if needed
// Note: IntaleqMapController doesn't expose raw symbol update yet for Marker object,
// but declarative update via GetBuilder is fast.
}
update();
if (ticks >= totalSteps) {
timer.cancel();
_animationTimers.remove(markerIdKey.value);
}
});
}
void _updateMarkerPosition(
LatLng newPosition, double newHeading, String icon) {
const String markerId = 'driverToPassengers';
final mId = MarkerId(markerId);
final existingMarker = markers.cast<Marker?>().firstWhere(
(m) => m?.markerId == mId,
orElse: () => null,
);
if (existingMarker != null) {
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
} else {
markers = {
...markers,
Marker(
markerId: mId,
position: newPosition,
rotation: newHeading,
icon: InlqBitmap.fromStyleImage(icon),
anchor: const Offset(0.5, 0.5),
),
};
update();
}
mapController?.animateCamera(CameraUpdate.newLatLng(newPosition));
}
@override
void onClose() {
Log.print(
"--- MapPassengerController: Closing and cleaning up all resources. ---");
// 1. إلغاء المؤقتات الفردية (باستخدام ?. الآمن)
markerReloadingTimer?.cancel();
markerReloadingTimer1?.cancel();
markerReloadingTimer2?.cancel();
timerToPassengerFromDriverAfterApplied?.cancel();
_timer?.cancel();
_masterTimer?.cancel(); // (أضف المؤقت الرئيسي)
_camThrottle?.cancel(); // (أضف مؤقت الكاميرا)
_heartbeatTimer?.cancel();
EmergencySignalService.instance.stopListening();
if (isSocketConnected) {
socket.emit('unsubscribe_all',
{'passenger_id': box.read(BoxName.passengerID).toString()});
socket.disconnect();
socket.dispose();
}
// 2. إلغاء جميع المؤقتات في الخريطة (للتحريكات السلسة)
_animationTimers.forEach((key, timer) {
timer.cancel();
});
_animationTimers.clear();
// 3. إغلاق متحكمات البث (StreamControllers) لمنع تسريب الذاكرة
if (!_timerStreamController.isClosed) {
_timerStreamController.close();
}
if (!_beginRideStreamController.isClosed) {
_beginRideStreamController.close();
}
if (!_rideStatusStreamController.isClosed) {
_rideStatusStreamController.close();
}
if (!timerController.isClosed) {
timerController.close();
}
// 4. التخلص من متحكم الخريطة (ممارسة جيدة)
mapController = null;
Log.print("--- Cleanup complete. ---");
super.onClose();
}
restCounter() {
clearPlacesDestination();
clearPolyline();
data = [];
rideConfirm = false;
shouldFetch = false;
timeToPassengerFromDriverAfterApplied = 0;
update();
}
//driver behaviour
double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
double deltaLon = lon2 - lon1;
double y = sin(deltaLon) * cos(lat2);
double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(deltaLon);
double bearing = atan2(y, x);
return (bearing * 180 / pi + 360) % 360; // تحويل إلى درجات
}
void analyzeBehavior(Position currentPosition, List<LatLng> routePoints) {
double actualBearing = currentPosition.heading; // الاتجاه الفعلي من GPS
double expectedBearing = calculateBearing(
routePoints[0].latitude,
routePoints[0].longitude,
routePoints[1].latitude,
routePoints[1].longitude,
);
double bearingDifference = (expectedBearing - actualBearing).abs();
if (bearingDifference > 30) {
Log.print("⚠️ السائق انحرف عن المسار!");
}
}
void detectStops(Position currentPosition) {
if (currentPosition.speed < 0.5) {
Log.print("🚦 السائق توقف في موقع غير متوقع!");
}
}
Future<void> cancelRideAfterRejectFromAll() async {
clearPlacesDestination();
clearPolyline();
data = [];
await CRUD().post(
link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php",
payload: {
"ride_id": rideId.toString(), // Convert to String
"reason": 'notApplyFromAnyDriver'
});
rideConfirm = false;
statusRide == 'Cancel';
isSearchingWindow = false;
shouldFetch = false;
isPassengerChosen = false;
isCashConfirmPageShown = false;
// totalStepDurations = 0;
isCashSelectedBeforeConfirmRide = false;
timeToPassengerFromDriverAfterApplied = 0;
changeCancelRidePageShow();
remainingTime = 0;
update();
}
// متغيرات أسباب الإلغاء
int selectedReasonIndex = -1;
String selectedReasonText = "";
TextEditingController otherReasonController = TextEditingController();
/// تحديث السبب المختار
void selectReason(int index, String reason) {
selectedReasonIndex = index;
selectedReasonText = reason;
update();
}
/// **دالة إلغاء الرحلة (النهائية)**
Future<void> cancelRide() async {
// 1. التحقق من اختيار سبب
if (selectedReasonIndex == -1) {
Get.snackbar(
'Attention'.tr,
'Please select a reason first'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
colorText: Colors.white,
);
return;
}
// 2. تجهيز نص السبب النهائي
String finalReason = selectedReasonText;
if (finalReason == "Other".tr) {
if (otherReasonController.text.trim().isEmpty) {
Get.snackbar("Attention".tr, "Please write the reason...".tr,
backgroundColor: Colors.red, colorText: Colors.white);
return;
}
finalReason = otherReasonController.text.trim();
}
// 3. التنظيف المحلي الفوري (UX Optimization)
Get.back(); // إغلاق الـ BottomSheet
if (isCancelRidePageShown)
changeCancelRidePageShow(); // إخفاء زر الإلغاء إن وجد
// 🔥 استدعاء دالة التنظيف الشاملة هنا 🔥
resetAllMapStates();
// إيقاف جميع التايمرات
// إيقاف جميع التايمرات
stopAllTimers();
currentRideState.value = RideState.cancelled;
await RideLiveNotification.cancel(); // إغلاق أندرويد
IosLiveActivityService.endRideActivity(); // ✅ إغلاق iOS
PipService.disablePip(); // ✅ إيقاف PiP عند الإلغاء
// 4. الاتصال بالسيرفر لإلغاء الرحلة وإبلاغ السائق
if (rideId != 'yet' && rideId != null) {
Log.print(
'📡 Sending Cancel Request to Server with Reason: $finalReason');
try {
await CRUD().post(
link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php",
payload: {
"ride_id": rideId.toString(),
"reason": finalReason // ✅ إرسال السبب للسيرفر
},
);
// لا داعي لإرسال FCM أو Socket يدوياً من هنا، PHP يقوم بذلك
} catch (e) {
Log.print("Error cancelling on server: $e");
}
}
// 5. العودة للصفحة الرئيسية
Get.offAll(() => const MapPagePassenger());
}
void changePickerShown() {
isPickerShown = !isPickerShown;
heightPickerContainer = isPickerShown == true ? 150 : 90;
update();
}
// ── Multi-Waypoint Methods ──────────────────────────────────────────────────
void addMenuWaypoint() {
if (activeMenuWaypointCount >= 2) return;
activeMenuWaypointCount++;
// Increase expanded bottom menu height to accommodate new waypoint row
mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56);
update();
}
void removeMenuWaypoint(int index) {
if (index < 0 || index >= 2) return;
// Shift items if removing first waypoint while second exists
if (index == 0 && activeMenuWaypointCount == 2) {
menuWaypoints[0] = menuWaypoints[1];
menuWaypointNames[0] = menuWaypointNames[1];
}
menuWaypoints[activeMenuWaypointCount - 1] = null;
menuWaypointNames[activeMenuWaypointCount - 1] = '';
activeMenuWaypointCount--;
mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56);
update();
}
void clearAllMenuWaypoints() {
menuWaypoints = [null, null];
menuWaypointNames = ['', ''];
activeMenuWaypointCount = 0;
isPickingWaypoint = false;
pickingWaypointIndex = -1;
update();
}
void startPickingWaypointOnMap(int index) {
pickingWaypointIndex = index;
isPickingWaypoint = true;
isPickerShown = true;
heightPickerContainer = 150;
// Close the expanded menu to show the map picker
isMainBottomMenuMap = true;
mainBottomMenuMapHeight = Get.height * .22;
update();
}
void setMenuWaypointFromMap(int index, LatLng position) {
Log.print('📍 setMenuWaypointFromMap called: index=$index, pos=$position');
if (index < 0 || index >= 2) return;
menuWaypoints[index] = position;
menuWaypointNames[index] =
'${position.latitude.toStringAsFixed(4)}, ${position.longitude.toStringAsFixed(4)}';
isPickingWaypoint = false;
pickingWaypointIndex = -1;
isPickerShown = false;
// Re-open expanded menu
isMainBottomMenuMap = false;
mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56);
update();
}
void setMenuWaypointFromSearch(int index, LatLng pos, String name) {
if (index < 0 || index >= 2) return;
menuWaypoints[index] = pos;
menuWaypointNames[index] = name;
update();
}
/// Build OSRM waypoint coordinate string for the route URL
String _buildOsrmWaypointCoords() {
String coords = '';
for (int i = 0; i < activeMenuWaypointCount; i++) {
final wp = menuWaypoints[i];
if (wp != null) {
coords += ';${wp.longitude},${wp.latitude}';
}
}
return coords;
}
void changeHeightPointsPageForRider() {
isPointsPageForRider = !isPointsPageForRider;
heightPointsPageForRider = isPointsPageForRider == true ? Get.height : 0;
update();
}
getCoordinateFromMapWayPoints(int index) {
placesCoordinate[index] = newStartPointLocation.toString();
update();
}
// --- ابدأ الإضافة هنا ---
// 1. قائمة لتخزين نقاط التوقف
List<Map<String, dynamic>> waypoints = [];
// 2. دالة لإضافة نقطة توقف جديدة
void addWaypoint(Map<String, dynamic> placeDetails) {
// يمكنك إضافة منطق للتحقق من عدد نقاط التوقف المسموح بها هنا
waypoints.add(placeDetails);
update(); // لتحديث الواجهة
// TODO: أضف هنا استدعاء دالة إعادة رسم المسار مع نقاط التوقف الجديدة
// getDirectionMapWithWaypoints();
}
// 3. دالة لحذف نقطة توقف
void removeWaypoint(int index) {
if (index >= 0 && index < waypoints.length) {
waypoints.removeAt(index);
update(); // لتحديث الواجهة
// TODO: أضف هنا استدعاء دالة إعادة رسم المسار بعد حذف النقطة
// getDirectionMapWithWaypoints();
}
}
// --- انتهى ---
void changeMainBottomMenuMap() {
if (isWayPointStopsSheetUtilGetMap == true) {
changeWayPointSheet();
} else {
isMainBottomMenuMap = !isMainBottomMenuMap;
mainBottomMenuMapHeight =
isMainBottomMenuMap == true ? Get.height * .22 : Get.height * .6;
isWayPointSheet = false;
if (heightMenuBool == true) {
getDrawerMenu();
}
initilizeGetStorage();
update();
}
}
void downPoints() {
if (Get.find<WayPointController>().wayPoints.length < 2) {
isWayPointStopsSheetUtilGetMap = false;
isWayPointSheet = false;
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
// changeWayPointStopsSheet();
update();
}
// changeWayPointStopsSheet();
// isWayPointSheet = false;
update();
}
void changeWayPointSheet() {
isWayPointSheet = !isWayPointSheet;
wayPointSheetHeight = isWayPointSheet == false ? 0 : Get.height * .45;
// if (heightMenuBool == true) {
// getDrawerMenu();
// }
update();
}
void changeWayPointStopsSheet() {
// int waypointsLength = Get.find<WayPointController>().wayPoints.length;
if (wayPointIndex > -1) {
isWayPointStopsSheet = true;
isWayPointStopsSheetUtilGetMap = true;
}
isWayPointStopsSheet = !isWayPointStopsSheet;
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
// if (heightMenuBool == true) {
// getDrawerMenu();
// }
update();
}
changeHeightPlaces() {
if (placesDestination.isEmpty) {
height = 0;
update();
}
height = 150;
update();
}
changeHeightStartPlaces() {
if (placesStart.isEmpty) {
height = 0;
update();
}
height = 150;
update();
}
changeHeightPlacesAll(int index) {
if (placeListResponseAll[index].isEmpty) {
height = 0;
update();
}
height = 150;
update();
}
changeHeightPlaces1() {
if (wayPoint1.isEmpty) {
height = 0;
update();
}
height = 150;
update();
}
changeHeightPlaces2() {
if (wayPoint2.isEmpty) {
height = 0;
update();
}
height = 150;
update();
}
changeHeightPlaces3() {
if (wayPoint3.isEmpty) {
height = 0;
update();
}
height = 150;
update();
}
changeHeightPlaces4() {
if (wayPoint4.isEmpty) {
height = 0;
update();
}
height = 150;
update();
}
hidePlaces() {
height = 0;
update();
}
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
// double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
// const R = 6371.0; // km
// final dLat = (lat2 - lat1) * math.pi / 180.0;
// final dLon = (lon2 - lon1) * math.pi / 180.0;
// final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
// math.cos(lat1 * math.pi / 180.0) *
// math.cos(lat2 * math.pi / 180.0) *
// math.sin(dLon / 2) *
// math.sin(dLon / 2);
// final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
// return R * c;
// }
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
// double _kmToLatDelta(double km) => km / 111.0;
// /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض)
// double _kmToLngDelta(double km, double atLat) =>
// km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9);
/// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة)
// double _relevanceScore(String name, String query) {
// final n = name.toLowerCase();
// final parts =
// query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2);
// double s = 0.0;
// for (final p in parts) {
// if (n.startsWith(p)) {
// s += 2.0;
// } else if (n.contains(p)) {
// s += 1.0;
// }
// }
// return s;
// }
// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك
// -----------------------------------------------------------------
// --== الدالة الرئيسية للبحث ==--
// -----------------------------------------------------------------
/// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك
// -----------------------------------------------------------------
// --== الدالة الرئيسية للبحث ==--
// -----------------------------------------------------------------
/// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
Future<void> getPlaces() async {
final q = placeDestinationController.text.trim();
if (q.isEmpty || q.length < 3) {
placesDestination = [];
update();
return;
}
final lat = passengerLocation.latitude;
final lng = passengerLocation.longitude;
final country = CountryPolygons.getCountryName(passengerLocation);
try {
final url =
'${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country';
final response = await CRUD().getMapSaas(link: url);
if (response != null && response['results'] is List) {
List results = List.from(response['results']);
final List filteredResults = [];
final Set<String> seenPlaces = {};
for (final p in results) {
final name = p['name_ar'] ?? p['name'] ?? '';
final district = p['district'] ?? '';
final plat = p['latitude']?.toString() ?? '0';
final plng = p['longitude']?.toString() ?? '0';
final dedupeKey =
"${name.trim().toLowerCase()}_${district.trim().toLowerCase()}";
if (!seenPlaces.contains(dedupeKey)) {
seenPlaces.add(dedupeKey);
p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0;
p['latitude'] = plat;
p['longitude'] = plng;
p['name'] = name;
p['address'] = p['full_address'] ??
(district.isNotEmpty
? "$district، ${p['governorate'] ?? ''}"
: (p['governorate'] ?? ''));
filteredResults.add(p);
}
}
placesDestination = filteredResults;
update();
}
} catch (e) {
Log.print('Exception in getPlaces: $e');
}
}
// -----------------------------------------------------------------
// --== دوال مساعدة ==--
// -----------------------------------------------------------------
/// تحسب المسافة بين نقطتين بالكيلومتر (معادلة هافرساين)
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
const R = 6371.0; // نصف قطر الأرض بالكيلومتر
final dLat = (lat2 - lat1) * (pi / 180.0);
final dLon = (lon2 - lon1) * (pi / 180.0);
final rLat1 = lat1 * (pi / 180.0);
final rLat2 = lat2 * (pi / 180.0);
final a = sin(dLat / 2) * sin(dLat / 2) +
cos(rLat1) * cos(rLat2) * sin(dLon / 2) * sin(dLon / 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
return R * c;
}
/// تحسب درجة تطابق بسيطة بين اسم المكان وكلمة البحث
double _relevanceScore(String placeName, String query) {
if (placeName.isEmpty || query.isEmpty) return 0.0;
final pLower = placeName.toLowerCase();
final qLower = query.toLowerCase();
if (pLower.startsWith(qLower)) return 1.0; // تطابق كامل في البداية
if (pLower.contains(qLower)) return 0.5; // تحتوي على الكلمة
return 0.0;
}
/// تحويل كيلومتر إلى فرق درجات لخط العرض
double _kmToLatDelta(double km) {
const kmInDegree = 111.32;
return km / kmInDegree;
}
/// تحويل كيلومتر إلى فرق درجات لخط الطول (يعتمد على خط العرض الحالي)
double _kmToLngDelta(double km, double latitude) {
const kmInDegree = 111.32;
return km / (kmInDegree * cos(latitude * (pi / 180.0)));
}
// var languageCode;
// // تحديد اللغة حسب الإدخال
// if (RegExp(r'[a-zA-Z]').hasMatch(placeDestinationController.text)) {
// languageCode = 'en';
// } else {
// languageCode = 'ar';
// }
// final bool isTextEmpty = placeDestinationController.text.trim().isEmpty;
// var key = Platform.isAndroid ? AK.mapAPIKEY : AK.mapAPIKEYIOS;
// final Uri url = Uri.parse(
// isTextEmpty
// ? 'https://places.googleapis.com/v1/places:searchNearby?key=$key'
// : 'https://places.googleapis.com/v1/places:searchText?key=$key',
// );
// Log.print('url: $url');
// // بناء الجسم حسب نوع الطلب
// final body = isTextEmpty
// ? jsonEncode({
// "languageCode": languageCode,
// "locationRestriction": {
// "circle": {
// "center": {
// "latitude": passengerLocation.latitude,
// "longitude": passengerLocation.longitude
// },
// "radius": 40000 // 40 كم
// }
// },
// "maxResultCount": 10
// })
// : jsonEncode({
// "textQuery": placeDestinationController.text,
// "languageCode": languageCode,
// "maxResultCount": 10,
// "locationBias": {
// "circle": {
// "center": {
// "latitude": passengerLocation.latitude,
// "longitude": passengerLocation.longitude
// },
// "radius": 40000
// }
// }
// });
// final headers = {
// 'Content-Type': 'application/json',
// 'X-Goog-Api-Key': AK.mapAPIKEY,
// 'X-Goog-FieldMask':
// 'places.displayName,places.formattedAddress,places.location'
// };
// try {
// final response = await http.post(url, headers: headers, body: body);
// Log.print('response: ${response.statusCode} - ${response.body}');
// if (response.statusCode == 200) {
// final data = jsonDecode(response.body);
// placesDestination = data['places'] ?? [];
// update();
// } else {
// Log.print('Error: ${response.statusCode} - ${response.reasonPhrase}');
// }
// } catch (e) {
// Log.print('Exception: $e');
// }
// }
getAIKey(String key) async {
var res =
await CRUD().get(link: AppLink.getapiKey, payload: {"keyName": key});
if (res != 'failure') {
var d = jsonDecode(res)['message'];
return d[key].toString();
} else {}
}
Future<void> getPlacesStart() async {
final q = placeStartController.text.trim();
if (q.isEmpty || q.length < 3) {
placesStart = [];
update();
return;
}
final lat = passengerLocation.latitude;
final lng = passengerLocation.longitude;
final country = CountryPolygons.getCountryName(passengerLocation);
try {
final url =
'${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country';
final response = await CRUD().getMapSaas(link: url);
if (response != null && response['results'] is List) {
List list = List.from(response['results']);
for (final p in list) {
p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0;
p['latitude'] = p['latitude'].toString();
p['longitude'] = p['longitude'].toString();
p['name'] = p['name_ar'] ?? p['name'] ?? '';
p['address'] = p['full_address'] ??
(p['district'] != null
? "${p['district']}، ${p['governorate'] ?? ''}"
: (p['governorate'] ?? ''));
}
placesStart = list;
update();
}
} catch (e) {
Log.print('Exception in getPlacesStart: $e');
}
}
Future<void> getPlacesListsWayPoint(int index) async {
final q = wayPoint0Controller.text.trim();
if (q.length < 3) return;
final lat = passengerLocation.latitude;
final lng = passengerLocation.longitude;
final country = CountryPolygons.getCountryName(passengerLocation);
try {
final url =
'${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country';
final response = await CRUD().getMapSaas(link: url);
if (response != null && response['results'] is List) {
List list = List.from(response['results']);
for (final p in list) {
p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0;
p['latitude'] = p['latitude'].toString();
p['longitude'] = p['longitude'].toString();
p['name'] = p['name_ar'] ?? p['name'] ?? '';
p['address'] = p['full_address'] ??
(p['district'] != null
? "${p['district']}، ${p['governorate'] ?? ''}"
: (p['governorate'] ?? ''));
}
wayPoint0 = list;
placeListResponseAll[index] = list;
update();
}
} catch (e) {
Log.print('Error fetching places in WayPoint: $e');
}
}
// داخل MapPassengerController
bool lowPerf = false;
Timer? _camThrottle;
DateTime _lastUiUpdate = DateTime.fromMillisecondsSinceEpoch(0);
Future<void> detectPerfMode() async {
try {
if (GetPlatform.isAndroid) {
final info = await DeviceInfoPlugin().androidInfo;
final sdk = info.version.sdkInt ?? 0;
final ram = info.availableRamSize ?? 0;
lowPerf = (sdk < 28) || (ram > 0 && ram < 3 * 1024 * 1024 * 1024);
} else {
lowPerf = false;
}
} catch (_) {
lowPerf = false;
}
update();
}
// تحديث الكاميرا بثروتل
void onCameraMoveThrottled(CameraPosition pos) {
_camThrottle?.cancel();
_camThrottle = Timer(const Duration(milliseconds: 160), () {
Log.print('📸 onCameraMoveThrottled: ${pos.target}');
// ضع فقط المنطق الضروري هنا لتقليل الحمل
int waypointsLength = Get.find<WayPointController>().wayPoints.length;
int index = wayPointIndex;
if (waypointsLength > 0) {
placesCoordinate[index] =
'${pos.target.latitude},${pos.target.longitude}';
}
newMyLocation = pos.target;
});
}
// Removed legacy light polylines since MapLibre vectors handle high-point geometries natively.
Future<void> savePlaceToServer(
String latitude, String longitude, String name, String rate) async {
var data = {
'latitude': latitude,
'longitude': longitude,
'name': name,
'rate': rate,
};
try {
CRUD().post(
link: AppLink.savePlacesServer,
payload: data,
);
} catch (e) {
Log.print('Error: $e');
}
}
Future<void> getLocation() async {
Log.print('🛰️ getLocation() called');
// Check if the app has permission to access location
permissionGranted = await location.hasPermission();
if (permissionGranted == PermissionStatus.denied) {
permissionGranted = await location.requestPermission();
if (permissionGranted != PermissionStatus.granted) {
// Location permission is still not granted, handle the error
return;
}
}
// Configure location accuracy
// LocationAccuracy desiredAccuracy = LocationAccuracy.high;
// Get the current location with a timeout to prevent hanging UI
LocationData? _locationData;
try {
_locationData = await location.getLocation().timeout(
const Duration(seconds: 5),
onTimeout: () {
Log.print("⚠️ Location fetch timed out after 5s.");
return LocationData.fromMap({
"latitude": passengerLocation.latitude,
"longitude": passengerLocation.longitude,
"speed": 0.0
});
},
);
} catch (e) {
Log.print("⚠️ Error fetching location: $e");
}
if (_locationData == null) {
isLoading = false;
update();
return;
}
passengerLocation =
(_locationData.latitude != null && _locationData.longitude != null
? LatLng(_locationData.latitude!, _locationData.longitude!)
: null)!;
// getLocationArea(passengerLocation.latitude, passengerLocation.longitude);
// Log.print('AppLink.endPoint: ${AppLink.endPoint}');
// Log.print('BoxName.serverChosen: ${box.read(BoxName.serverChosen)}');
newStartPointLocation = passengerLocation;
newMyLocation = passengerLocation;
// Resolve current location address
try {
getReverseGeocoding(passengerLocation).then((address) {
currentLocationString = address;
update();
});
} catch (e) {
Log.print('Error resolving current location: $e');
}
// Trigger offline map caching for a 10km radius
OfflineMapService.instance
.downloadRegion(passengerLocation, radiusKm: 10.0);
speed = _locationData.speed!;
// //print location details
isLoading = false;
update();
}
void clearPolyline() {
polyLines.clear();
update();
}
LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) {
const double earthRadius = 6378137.0; // Earth's radius in meters
double latDelta = (radiusInMeters / earthRadius) * (180 / pi);
double lngDelta =
(radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi);
double minLat = lat - latDelta;
double maxLat = lat + latDelta;
double minLng = lng - lngDelta;
double maxLng = lng + lngDelta;
// Ensure the latitude is between -90 and 90
minLat = max(-90.0, minLat);
maxLat = min(90.0, maxLat);
// Ensure the longitude is between -180 and 180
minLng = (minLng + 180) % 360 - 180;
maxLng = (maxLng + 180) % 360 - 180;
// Ensure the bounds are in the correct order
if (minLng > maxLng) {
double temp = minLng;
minLng = maxLng;
maxLng = temp;
}
return LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
);
}
void onMapCreated(IntaleqMapController controller) {
mapController = controller;
update();
}
void onStyleLoaded() async {
Log.print('🗺️ Intaleq Map Style Loaded. Initializing...');
isStyleLoaded = true;
_loadMapIcons();
// Smart Camera Reset logic:
if (mapController != null) {
if (markers.isNotEmpty && lastComputedBounds != null) {
await _safeAnimateCameraBounds(lastComputedBounds);
} else {
mapController!.animateCamera(
CameraUpdate.newLatLng(passengerLocation),
);
}
}
update();
}
/// Safe wrapper for animateCamera Bounds to prevent native std::domain_error crash on iOS.
Future<void> _safeAnimateCameraBounds(LatLngBounds? bounds,
{double left = 60,
double top = 60,
double right = 60,
double bottom = 60}) async {
if (bounds == null || mapController == null) return;
try {
// Ensure the coordinates are valid
if (bounds.northeast.latitude == bounds.southwest.latitude &&
bounds.northeast.longitude == bounds.southwest.longitude) {
Log.print(
'⚠️ _safeAnimateCameraBounds: Bounds are a single point, zooming to point instead.');
await mapController
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 15));
return;
}
// Small delay to ensure iOS view layout is fully ready
await Future.delayed(const Duration(milliseconds: 200));
await mapController?.animateCamera(
CameraUpdate.newLatLngBounds(
bounds,
left: left,
top: top,
right: right,
bottom: bottom,
),
);
} catch (e) {
Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED: $e');
// Final fallback to prevent device freeze
try {
await mapController
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14));
} catch (_) {}
}
}
Future<void> _loadMapIcons() async {
// Wait up to 3 seconds for the map style to finish loading
for (int i = 0; i < 15; i++) {
if (mapController != null && isStyleLoaded) break;
await Future.delayed(const Duration(milliseconds: 200));
}
if (mapController == null || !isStyleLoaded) {
Log.print(
'⚠️ _loadMapIcons: mapController or style not ready. Icons may not load.');
}
await _addMapImage(startIcon, 'assets/images/A.png');
await _addMapImage(endIcon, 'assets/images/b.png');
await _addMapImage(carIcon, 'assets/images/car.png');
await _addMapImage(motoIcon, 'assets/images/moto.png');
await _addMapImage(ladyIcon, 'assets/images/lady.png');
await _addMapImage('picker_icon', 'assets/images/picker.png');
// Waypoint markers - use moto1 & lady1 as colored waypoint icons
await _addMapImage('orange_marker', 'assets/images/moto1.png');
await _addMapImage('violet_marker', 'assets/images/lady1.png');
}
Future<void> _addMapImage(String id, String path) async {
try {
final ByteData bytes = await rootBundle.load(path);
// Resize car icons for better visibility on map (e.g. 120px)
final size = _getImageSize(id);
if (size != null && (id == carIcon || id == motoIcon || id == ladyIcon)) {
final resized = await _resizeImage(bytes.buffer.asUint8List(), size);
await mapController?.addImage(id, resized);
Log.print(
'✅ Successfully added resized map image: $id (${size}x${size})');
} else {
await mapController?.addImage(id, bytes.buffer.asUint8List());
Log.print('✅ Successfully added map image: $id');
}
} catch (e) {
Log.print('❌ Error loading map icon $id: $e');
}
}
int? _getImageSize(String id) {
if (id == carIcon || id == motoIcon || id == ladyIcon) return 120;
return null;
}
Future<Uint8List> _resizeImage(Uint8List bytes, int size) async {
return await compute((Uint8List data) {
final image = img.decodeImage(data);
if (image == null) return data;
final resized = img.copyResize(image, width: size, height: size);
return Uint8List.fromList(img.encodePng(resized));
}, bytes);
}
// Wait up to 3 seconds for the map style to finish loading
void updateCurrentLocationFromCamera(LatLng target) {
Log.print('📍 updateCurrentLocationFromCamera: $target');
newMyLocation = target;
if (startLocationFromMap == true) {
Log.print('📍 Updating startLocationFromMap to $target');
newStartPointLocation = target;
} else if (passengerStartLocationFromMap == true) {
Log.print('📍 Updating passengerStartLocationFromMap to $target');
newStartPointLocation = target;
}
int waypointsLength = Get.find<WayPointController>().wayPoints.length;
if (waypointsLength > 0 &&
wayPointIndex >= 0 &&
wayPointIndex < placesCoordinate.length) {
Log.print('📍 Updating wayPointIndex $wayPointIndex to $target');
placesCoordinate[wayPointIndex] =
'${target.latitude},${target.longitude}';
}
update();
}
String durationByPassenger = '';
late DateTime newTime1 = DateTime.now();
late DateTime timeFromDriverToPassenger = DateTime.now();
String distanceByPassenger = '';
late Duration durationFromDriverToPassenger;
double nearestDistance = double.infinity;
Future<CarLocation?> getNearestDriverByPassengerLocation() async {
if (!rideConfirm) {
if (dataCarsLocationByPassenger != 'failure' &&
dataCarsLocationByPassenger != null &&
dataCarsLocationByPassenger['message'] != null &&
dataCarsLocationByPassenger['message'].length > 0) {
double nearestDistance = double.infinity; // Initialize nearest distance
CarLocation? nearestCar;
for (var i = 0;
i < dataCarsLocationByPassenger['message'].length;
i++) {
var carLocation = dataCarsLocationByPassenger['message'][i];
// Log.print('carLocation: $carLocation');
try {
// Calculate distance between passenger's location and current driver's location
final distance = Geolocator.distanceBetween(
passengerLocation.latitude,
passengerLocation.longitude,
double.parse(carLocation['latitude']),
double.parse(carLocation['longitude']),
);
// Calculate duration assuming an average speed of 25 km/h (adjust as needed)
int durationToPassenger = (distance / 1000 / 25 * 3600).round();
// Log.print('distance: $distance');
// Log.print('durationToPassenger: $durationToPassenger');
// Log.print('passengerLocation: $passengerLocation');
// Log.print('carLocation: $carLocation');
// Log.print('distance: $distance meters');
// Log.print('durationToPassenger: $durationToPassenger seconds');
// Update the UI with the distance and duration for each car
update();
// If this distance is smaller than the nearest distance found so far, update nearestCar
if (distance < nearestDistance) {
nearestDistance = distance;
nearestCar = CarLocation(
distance: distance,
duration: durationToPassenger.toDouble(),
id: carLocation['driver_id'],
latitude: double.parse(carLocation['latitude']),
longitude: double.parse(carLocation['longitude']),
);
// Log.print('nearestCar: $nearestCar');
// Update the UI with the nearest driver
update();
}
} catch (e) {
Log.print('Error calculating distance/duration: $e');
}
}
// Return the nearest car found
return nearestCar;
}
}
// Return null if no drivers are found or if ride is confirmed
return null;
}
getNearestDriverByPassengerLocationAPIGOOGLE() async {
if (polyLines.isEmpty || data.isEmpty) {
return null; // Early return if data is empty
}
if (!rideConfirm) {
double nearestDistance = double.infinity;
if (dataCarsLocationByPassenger != 'failure') {
if (dataCarsLocationByPassenger['message'].length > 0) {
for (var i = 0;
i < dataCarsLocationByPassenger['message'].length;
i++) {
var carLocation = dataCarsLocationByPassenger['message'][i];
// }
// isloading = true;
update();
// Make API request to get exact distance and duration
String apiUrl =
'${AppLink.googleMapsLink}distancematrix/json?destinations=${carLocation['latitude']},${carLocation['longitude']}&origins=${passengerLocation.latitude},${passengerLocation.longitude}&units=metric&key=${AK.mapAPIKEY}';
var response = await CRUD().getGoogleApi(link: apiUrl, payload: {});
if (response != null && response['status'] == "OK") {
var data = response;
// Extract distance and duration from the response and handle accordingly
int distance1 =
data['rows'][0]['elements'][0]['distance']['value'];
distanceByPassenger =
data['rows'][0]['elements'][0]['distance']['text'];
durationToPassenger =
data['rows'][0]['elements'][0]['duration']['value'];
durationFromDriverToPassenger =
Duration(seconds: durationToPassenger.toInt());
newTime1 = currentTime.add(durationFromDriverToPassenger);
timeFromDriverToPassenger =
newTime1.add(Duration(minutes: 2.toInt()));
durationByPassenger =
data['rows'][0]['elements'][0]['duration']['text'];
update();
if (distance1 < nearestDistance) {
nearestDistance = distance1.toDouble();
nearestCar = CarLocation(
distance: distance1.toDouble(),
duration: durationToPassenger.toDouble(),
id: carLocation['driver_id'],
latitude: double.parse(carLocation['latitude']),
longitude: double.parse(carLocation['longitude']),
);
// isloading = false;
update();
}
}
// Handle the distance and duration as needed
else {
// 'Failed to retrieve distance and duration: ${response['status']}');
Log.print('${response['status']}: ${response['status']}}');
// Handle the failure case
}
}
}
}
}
}
calculateDistanceBetweenPassengerAndDriverBeforeCancelRide() async {
await getDriverCarsLocationToPassengerAfterApplied();
double distance = Geolocator.distanceBetween(
passengerLocation.latitude,
passengerLocation.longitude,
driverCarsLocationToPassengerAfterApplied.last.latitude,
driverCarsLocationToPassengerAfterApplied.last.longitude,
);
if (distance > 500) {
isCancelRidePageShown = true;
update();
} else {
Get.defaultDialog(
barrierDismissible: false,
title: 'The Driver Will be in your location soon .'.tr,
middleText: 'The distance less than 500 meter.'.tr,
confirm: Column(
children: [
MyElevatedButton(
kolor: AppColor.greenColor,
title: 'Ok'.tr,
onPressed: () {
Get.back();
},
),
MyElevatedButton(
kolor: AppColor.redColor,
title: 'No, I want to cancel this trip'.tr,
onPressed: () {
Get.back();
MyDialog().getDialog(
'Attention'.tr,
'You will be charged for the cost of the driver coming to your location.'
.tr,
() async {
Get.back();
Get.find<PaymentController>()
.payToDriverForCancelAfterAppliedAndHeNearYou(rideId);
// isCancelRidePageShown = true;
// update();
},
);
},
),
],
),
);
// cancel: MyElevatedButton(
// title: 'No.Iwant Cancel Trip.'.tr, onPressed: () {}));
}
}
List<double> headingAngles = [];
double calculateAngleBetweenLocations(LatLng start, LatLng end) {
double startLat = start.latitude * math.pi / 180;
double startLon = start.longitude * math.pi / 180;
double endLat = end.latitude * math.pi / 180;
double endLon = end.longitude * math.pi / 180;
double dLon = endLon - startLon;
double y = math.sin(dLon) * cos(endLat);
double x = cos(startLat) * math.sin(endLat) -
math.sin(startLat) * cos(endLat) * cos(dLon);
double angle = math.atan2(y, x);
double angleDegrees = angle * 180 / math.pi;
return angleDegrees;
}
late LatLngBounds boundsData;
late String startNameAddress = '';
late String endNameAddress = '';
List<Map<String, dynamic>> stopPoints = [];
void removeStop(Map<String, dynamic> stop) {
stopPoints.remove(stop);
update(); // Trigger a rebuild of the UI
}
Future<String> getReverseGeocoding(LatLng location) async {
final lat = location.latitude;
final lng = location.longitude;
final url = '${AppLink.reverseGeocoding}?lat=$lat&lng=$lng';
try {
final response = await CRUD().getMapSaas(link: url);
if (response != null && response is List && response.isNotEmpty) {
final data = response[0];
String name = data['name_ar'] ?? data['name'] ?? 'Unknown Location'.tr;
return name;
}
return 'Unknown Location'.tr;
} catch (e) {
Log.print('ReverseGeocoding Exception: $e');
return 'Unknown Location'.tr;
}
}
bool isDrawingRoute = false;
void showDrawingBottomSheet() {
Log.print(
'🔔 showDrawingBottomSheet called. isDrawingRoute: $isDrawingRoute');
// استخدام addPostFrameCallback لضمان ظهور الإشعار بعد انتهاء بناء الإطار
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.rawSnackbar(
titleText: Text(
'Drawing route on map...'.tr,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
messageText: Text(
'Please wait while we prepare your trip.'.tr,
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
),
),
showProgressIndicator: true,
progressIndicatorBackgroundColor: Colors.white24,
progressIndicatorValueColor:
const AlwaysStoppedAnimation<Color>(Colors.white),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: AppColor.primaryColor.withOpacity(0.9),
duration: const Duration(seconds: 3),
margin: const EdgeInsets.fromLTRB(16, 0, 16, 110),
borderRadius: 16,
icon: const Icon(Icons.map_outlined, color: Colors.white),
isDismissible: true,
);
});
}
String dynamicApiUrl = 'https://routec.intaleq.xyz/route';
Future<void> getDistanceFromDriverAfterAcceptedRide(
String origin, String destination) async {
String apiKey = Env.mapKeyOsm; // مفتاح API الخاص بك
if (origin.isEmpty) {
origin = '${passengerLocation.latitude},${passengerLocation.longitude}';
}
// 2. بناء الرابط (URI)
// Waypoints غير مدعومة حالياً في OSRM، لذلك تم تجاهلها
var uri = Uri.parse(
'$dynamicApiUrl?origin=$origin&destination=$destination&steps=false&overview=false');
Log.print('uri: $uri');
// 3. إرسال الطلب مع الهيدر
http.Response response;
Map<String, dynamic> responseData;
try {
response = await http.get(
uri,
headers: {
'X-API-KEY': apiKey,
},
).timeout(const Duration(seconds: 20)); // تايم آوت 20 ثانية
if (response.statusCode != 200) {
Log.print('Error from API: ${response.statusCode}');
isLoading = false;
update();
return; // خروج في حالة الخطأ
}
if (Get.isBottomSheetOpen ?? false) {
Get.back(); // لإغلاق شاشة "جاري الرسم"
}
isDrawingRoute = false; // Reset state
responseData = json.decode(response.body);
Log.print('responseData: $responseData');
if (responseData['status'] != 'ok') {
Log.print('API returned an error: ${responseData['message']}');
isLoading = false;
update();
return; // خروج في حالة خطأ منطقي (مثل "no path")
}
} catch (e) {
Log.print('Failed to get directions: $e');
isLoading = false;
update();
return; // خروج عند فشل الاتصال
}
}
// (b = 1.5348) هو المعامل الذي تم حسابه من مقارنة 60 رحلة بين Google و OSRM
double kDurationScalar =
1.5348; //this from colab 60 random locations from google and routec
// -----------------------------------------------------------------------------------------
// GET DIRECTION MAP (FULL)
// -----------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------
// GET DIRECTION MAP (With Auto-Retry Logic)
// -----------------------------------------------------------------------------------------
// أضفنا attemptCount لتتبع عدد المحاولات
// -----------------------------------------------------------------------------------------
// GET DIRECTION MAP (Retry or Fail Strict Logic)
// -----------------------------------------------------------------------------------------
Future<void> getDirectionMap(String origin, String destination,
[List<String> waypoints = const [], int attemptCount = 0]) async {
// 1. إظهار التحميل فقط في المحاولة الأولى
if (attemptCount == 0) {
// NOTE: Do NOT set isLoading = true here!
// isLoading destroys the MapLibreMap widget entirely (replaced by spinner),
// which means markers/polylines cannot be added to the new map instance
// until its style finishes loading asynchronously — causing a race condition.
// The showDrawingBottomSheet() overlay provides sufficient user feedback.
isDrawingRoute = true;
update();
if (isDrawingRoute) showDrawingBottomSheet();
await getCarsLocationByPassengerAndReloadMarker();
}
// تجهيز الإحداثيات
if (origin.isEmpty) {
origin = '${passengerLocation.latitude},${passengerLocation.longitude}';
}
var coordDestination = destination.split(',');
double latDest = double.parse(coordDestination[0]);
double lngDest = double.parse(coordDestination[1]);
myDestination = LatLng(latDest, lngDest);
// ── 2. Unified SaaS Routing Strategy ──────────────────────────
final bool isSaaSRequest = true;
Uri uri;
var originCoords = origin.split(',');
final Map<String, String> queryParams = {
'fromLat': originCoords[0].trim(),
'fromLng': originCoords[1].trim(),
'toLat': latDest.toString(),
'toLng': lngDest.toString(),
};
// Add multi-stop waypoints to the query parameters
for (int i = 0; i < activeMenuWaypointCount; i++) {
final wp = menuWaypoints[i];
if (wp != null) {
queryParams['stop${i + 1}Lat'] = wp.latitude.toString();
queryParams['stop${i + 1}Lng'] = wp.longitude.toString();
}
}
uri = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
Log.print(
'Requesting Route URI (${isSaaSRequest ? "SaaS" : "OSRM"}, Attempt: ${attemptCount + 1}): $uri');
http.Response response;
Map<String, dynamic> responseData;
try {
response = await http.get(uri, headers: {
'x-api-key': Env.mapSaasKey,
}).timeout(const Duration(seconds: 20));
responseData = json.decode(response.body);
// Validation: SaaS returns 200 with data, OSRM returns code: 'Ok'
bool isRequestValid = response.statusCode == 200 &&
(isSaaSRequest || responseData['code'] == 'Ok');
if (!isRequestValid) {
if (attemptCount < 2) {
await _retryProcess(origin, destination, waypoints, attemptCount);
return;
}
_handleFatalError(
"Server Error".tr, "Connection failed. Please try again.".tr);
return;
}
// ============================================================
// 🛑 الفحص الأمني (Sanity Check)
// ============================================================
double apiDistanceMeters;
String pointsString;
dynamic routeData;
// SaaS parsing
apiDistanceMeters = (responseData['distance'] as num).toDouble();
pointsString = responseData['points'] ?? "";
routeData = responseData; // For box storage
var originCoords = origin.split(',');
double startLat = double.parse(originCoords[0]);
double startLng = double.parse(originCoords[1]);
// المسافة الجوية
double aerialDistance =
Geolocator.distanceBetween(startLat, startLng, latDest, lngDest);
if (apiDistanceMeters < 50.0 && aerialDistance > 200.0) {
Log.print(
"⚠️ Suspicious Route detected! Server: $apiDistanceMeters m | Aerial: $aerialDistance m");
if (attemptCount < 2) {
Log.print("🔄 Retrying request (Attempt ${attemptCount + 2})...");
await Future.delayed(const Duration(seconds: 1));
await getDirectionMap(
origin, destination, waypoints, attemptCount + 1);
return;
} else {
Log.print("❌ All retries failed. Calculating Route is impossible.");
_handleFatalError(
"Route Not Found".tr,
"We couldn't find a valid route to this destination. Please try selecting a different point."
.tr);
return;
}
}
// 3. معالجة البيانات
box.remove(BoxName.tripData);
box.write(BoxName.tripData, routeData);
durationToRide =
((routeData['duration'] as num) * kDurationScalar).toInt();
double distanceOfTrip = apiDistanceMeters / 1000.0;
distance = distanceOfTrip;
data = routeData['legs'] != null && routeData['legs'].isNotEmpty
? (routeData['legs'][0]['steps'] ?? [])
: [];
List<LatLng> decodedPoints = [];
if (pointsString.isNotEmpty) {
decodedPoints = await compute(decodePolylineIsolate, pointsString);
}
if (decodedPoints.isEmpty) {
_handleFatalError("Map Error".tr, "Received empty route data.".tr);
return;
}
polylineCoordinates.clear();
polylineCoordinates.addAll(decodedPoints);
final LatLng startLoc = polylineCoordinates.first;
final LatLng endLoc = polylineCoordinates.last;
// ── 4. العناوين والتحديثات ──────────────────────────────────
startNameAddress = responseData['startName'] ?? 'Start Point'.tr;
endNameAddress = responseData['endName'] ?? 'Destination'.tr;
Log.print('📍 ROUTE START: $startNameAddress');
Log.print('📍 ROUTE END: $endNameAddress');
// ── 5. Bounds Calculation (SaaS bbox vs OSRM manual) ──────────
if (isSaaSRequest && responseData['bbox'] != null) {
List<dynamic> bbox = responseData['bbox'];
if (bbox.length == 4) {
// SaaS format: [minLng, minLat, maxLng, maxLat]
lastComputedBounds = LatLngBounds(
southwest: LatLng(bbox[1], bbox[0]),
northeast: LatLng(bbox[3], bbox[2]),
);
}
} else {
double? minLat, maxLat, minLng, maxLng;
for (LatLng point in polylineCoordinates) {
minLat =
minLat == null ? point.latitude : min(minLat, point.latitude);
maxLat =
maxLat == null ? point.latitude : max(maxLat, point.latitude);
minLng =
minLng == null ? point.longitude : min(minLng, point.longitude);
maxLng =
maxLng == null ? point.longitude : max(maxLng, point.longitude);
}
if (minLat != null) {
lastComputedBounds = LatLngBounds(
northeast: LatLng(maxLat!, maxLng!),
southwest: LatLng(minLat!, minLng!));
}
}
// isDrawingRoute = false;
// 5b. Reset state when finished
if (isDrawingRoute) {
Log.print('🔔 Finalizing route drawing state');
isDrawingRoute = false;
isLoading = false;
update();
}
// 6. إضافة الماركرز
durationToAdd = Duration(seconds: durationToRide);
hours = durationToAdd.inHours;
minutes = (durationToAdd.inMinutes % 60).round();
markers = {
Marker(
markerId: const MarkerId('start'),
position: startLoc,
icon: InlqBitmap.fromStyleImage('orange_marker'),
infoWindow: const InfoWindow(title: 'A'),
anchor: const Offset(0.5, 1.0),
),
Marker(
markerId: const MarkerId('end'),
position: endLoc,
icon: InlqBitmap.fromStyleImage('violet_marker'),
infoWindow: const InfoWindow(title: 'B'),
anchor: const Offset(0.5, 1.0),
),
};
for (int i = 0; i < activeMenuWaypointCount; i++) {
final wp = menuWaypoints[i];
if (wp != null) {
final bool isFirstWaypoint = i == 0;
markers.add(Marker(
markerId: MarkerId('waypoint_$i'),
position: wp,
icon: InlqBitmap.fromStyleImage(
isFirstWaypoint ? 'orange_marker' : 'violet_marker'),
infoWindow:
InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'),
anchor: const Offset(0.5, 1.0),
));
}
}
// 7. رسم الخط
if (polyLines.isNotEmpty) clearPolyline();
rideConfirm = false;
isMarkersShown = true;
update(); // تحديث أولي لإظهار الخريطة والماركرز
// إظهار الباتم شيت للسعر
await bottomSheet();
// تشغيل الأنيميشن الخفيف لومضات المسار + fit camera after
await _playRouteAnimation(polylineCoordinates, lastComputedBounds);
} catch (e, stackTrace) {
// 🚨 Cleanup on error to prevent UI freeze
if (isDrawingRoute) {
isDrawingRoute = false;
isLoading = false;
update();
}
Log.print('🚨 CRITICAL ERROR IN getDirectionMap: $e');
Log.print('🚨 STACKTRACE: $stackTrace');
if (attemptCount < 2) {
await _retryProcess(origin, destination, waypoints, attemptCount);
} else {
_handleFatalError("Connection Error".tr,
"Please check your internet and try again.".tr);
}
}
}
// --- رسم المسار النهائي مع تقسيم ملون حسب نقاط التوقف ---
Future<void> _playRouteAnimation(
List<LatLng> coords, LatLngBounds? bounds) async {
// Segment colors matching UI dots: green → amber → purple → red
const List<Color> segmentColors = [
Color(0xFF109642), // Green (start → stop 1)
Color(0xFFF59E0B), // Amber (stop 1 → stop 2)
Color(0xFF7C3AED), // Purple (last segment → dest)
Color(0xFFEF4444), // Red (fallback)
];
// ── Build final polyline segments ───────────────────────────────────
// Build all segments in a temporary Set first, then assign once
Set<Polyline> newPolylines = {};
if (activeMenuWaypointCount > 0) {
List<int> splitIndices = [];
for (int w = 0; w < activeMenuWaypointCount; w++) {
final wp = menuWaypoints[w];
if (wp == null) continue;
int bestIdx = 0;
double bestDist = double.infinity;
for (int j = 0; j < coords.length; j++) {
final dx = coords[j].latitude - wp.latitude;
final dy = coords[j].longitude - wp.longitude;
final d = dx * dx + dy * dy;
if (d < bestDist) {
bestDist = d;
bestIdx = j;
}
}
splitIndices.add(bestIdx);
}
splitIndices.sort();
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
for (int s = 0; s < boundaries.length - 1; s++) {
int from = boundaries[s];
int to = boundaries[s + 1] + 1;
if (to > coords.length) to = coords.length;
if (from >= to - 1) continue;
final segCoords = coords.sublist(from, to);
if (segCoords.length < 2) continue;
final color = segmentColors[s % segmentColors.length];
newPolylines.add(Polyline(
polylineId: PolylineId('segment_$s'),
points: segCoords,
color: color,
width: 6,
));
}
} else {
newPolylines.add(Polyline(
polylineId: const PolylineId('route_primary'),
points: coords,
color: AppColor.primaryColor,
width: 6,
));
}
polyLines = newPolylines;
update();
Log.print(
'🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map');
update();
// ── Fit camera to full route bounds ────────────────────────────────
if (bounds != null) {
await _safeAnimateCameraBounds(bounds);
}
}
// --- دالة المساعدة لإعادة المحاولة ---
Future<void> _retryProcess(String origin, String dest, List<String> waypoints,
int currentAttempt) async {
Log.print(
"🔄 Exception or Error caught. Retrying in 1s... (Attempt ${currentAttempt + 1})");
await Future.delayed(const Duration(seconds: 1));
getDirectionMap(origin, dest, waypoints, currentAttempt + 1);
}
// --- دالة جديدة لتنظيف الخريطة بالكامل ومنع تداخل الرحلات ---
void resetAllMapStates() {
Log.print('🧹 Resetting all map states to prevent sticky location bug');
clearPlacesDestination();
clearPlacesStart();
clearPolyline();
data = [];
passengerStartLocationFromMap = false;
startLocationFromMap = false;
isPickerShown = false;
workLocationFromMap = false;
homeLocationFromMap = false;
isAnotherOreder = false;
isWhatsAppOrder = false;
// ✅ أضف هذا: reset الوجهة لموقع الراكب حتى لا تبقى قيمة الرحلة القديمة
myDestination = passengerLocation;
hintTextDestinationPoint = 'Select your destination'.tr;
placeDestinationController.clear();
placeStartController.clear();
rideConfirm = false;
shouldFetch = false;
isDrawingRoute = false;
isLoading = false;
update();
}
// -----------------------------------------------------------------------------------------
// 🛑 دالة الخطأ القاتل (تغلق كل شيء وتعيد المستخدم للخريطة)
// -----------------------------------------------------------------------------------------
void _handleFatalError(String title, String message) {
// 1. إغلاق شاشة التحميل (Drawing route...)
if (Get.isBottomSheetOpen == true || Get.isDialogOpen == true) {
Get.back();
}
if (Get.isSnackbarOpen) Get.closeCurrentSnackbar();
// 2. تصفير المتغيرات
isDrawingRoute = false;
isLoading = false;
update();
// 3. إظهار الديالوج الإجباري
Get.defaultDialog(
title: title,
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
middleText: message,
middleTextStyle: AppStyle.subtitle,
barrierDismissible: false, // لا يمكن إغلاقه بالضغط خارجاً
confirm: MyElevatedButton(
title: "Close".tr,
kolor: AppColor.redColor,
onPressed: () {
Get.back(); // إغلاق الديالوج
// 4. إعادة تحميل الصفحة بالكامل (تنظيف الحالة)
// تأكد من استيراد MapPagePassenger
Get.offAll(() => const MapPagePassenger());
},
),
);
}
// Legacy gradient and layered animations removed for MapLibre migration
String shortenAddress(String fullAddress) {
// Split the address into parts
List<String> parts = fullAddress.split('،');
// Remove any leading or trailing whitespace from each part
parts = parts.map((part) => part.trim()).toList();
// Remove any empty parts
parts = parts.where((part) => part.isNotEmpty).toList();
// Initialize the short address
String shortAddress = '';
if (parts.isNotEmpty) {
// Add the first part (usually the most specific location)
shortAddress += parts[0];
}
if (parts.length > 2) {
// Add the district or area name (usually the third part in Arabic format)
shortAddress += '، ${parts[2]}';
} else if (parts.length > 1) {
// Add the second part for English or shorter addresses
shortAddress += '، ${parts[1]}';
}
// Add the country (usually the last part)
if (parts.length > 1) {
shortAddress += '، ${parts.last}';
}
// Remove any part that's just numbers (like postal codes)
shortAddress = shortAddress
.split('،')
.where((part) => !RegExp(r'^[0-9 ]+$').hasMatch(part.trim()))
.join('،');
// Check if the address is in English
bool isEnglish =
RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(shortAddress.replaceAll('،', ''));
if (isEnglish) {
// Further processing for English addresses
List<String> englishParts = shortAddress.split('،');
if (englishParts.length > 2) {
shortAddress =
'${englishParts[0]}، ${englishParts[1]}، ${englishParts.last}';
} else if (englishParts.length > 1) {
shortAddress = '${englishParts[0]}، ${englishParts.last}';
}
}
return shortAddress;
}
double distanceOfDestination = 0;
bool haveSteps = false;
late LatLng latestPosition;
getMapPoints(String originSteps, String destinationSteps, int index) async {
isWayPointStopsSheetUtilGetMap = false;
// haveSteps = true;
// startCarLocationSearch(box.read(BoxName.carType));
await getCarsLocationByPassengerAndReloadMarker();
// await getCarsLocationByPassengerAndReloadMarker();
// isLoading = true;
update();
var url =
('${AppLink.googleMapsLink}directions/json?&language=${box.read(BoxName.lang)}&avoid=tolls|ferries&destination=$destinationSteps&origin=$originSteps&key=${AK.mapAPIKEY}');
var response = await CRUD().getGoogleApi(link: url, payload: {});
data = response['routes'][0]['legs'];
// isLoading = false;
int durationToRide0 = data[0]['duration']['value'];
durationToRide = durationToRide + durationToRide0;
distance = distanceOfDestination + (data[0]['distance']['value']) / 1000;
update();
// final points =
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
final String pointsString =
response['routes'][0]["overview_polyline"]["points"];
List<LatLng> decodedPoints =
await compute(decodePolylineIsolate, pointsString);
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
for (int i = 0; i < decodedPoints.length; i++) {
polylineCoordinates.add(decodedPoints[i]);
}
// Define the northeast and southwest coordinates
if (polyLines.isEmpty) {
var polyline = Polyline(
polylineId: PolylineId('route_$index'),
points: polylineCoordinatesPointsAll[index],
width: 6,
color: const Color(0xFF2196F3),
);
polyLines = {...polyLines, polyline};
rideConfirm = false;
update();
}
}
void updateCameraForDistanceAfterGetMap() {
LatLng coord1 = LatLng(
double.parse(coordinatesWithoutEmpty.first.split(',')[0]),
double.parse(coordinatesWithoutEmpty.first.split(',')[1]));
LatLng coord2 = LatLng(
double.parse(coordinatesWithoutEmpty.last.split(',')[0]),
double.parse(coordinatesWithoutEmpty.last.split(',')[1]));
LatLng northeast;
LatLng southwest;
if (coord1.latitude > coord2.latitude) {
northeast = coord1;
southwest = coord2;
} else {
northeast = coord2;
southwest = coord1;
}
// Create the LatLngBounds object
LatLngBounds bounds =
LatLngBounds(northeast: northeast, southwest: southwest);
// Fit the camera to the bounds
var cameraUpdate = CameraUpdate.newLatLngBounds(bounds,
left: 180, top: 180, right: 180, bottom: 180);
mapController!.animateCamera(cameraUpdate);
update();
}
int selectedIndex = -1; // Initialize with no selection
void selectCarFromList(int index) {
selectedIndex = index; // Update selected index
carTypes.forEach(
(element) => element.isSelected = false); // Reset selection flags
carTypes[index].isSelected = true;
update();
}
showBottomSheet1() async {
await bottomSheet();
isBottomSheetShown = true;
heightBottomSheetShown = 250;
update();
}
final promo = TextEditingController();
bool promoTaken = false;
void applyPromoCodeToPassenger(BuildContext context) async {
if (promoTaken == true) {
MyDialog().getDialog(
'Promo Already Used'.tr,
'You have already used this promo code.'.tr,
() => Get.back(),
);
return;
}
if (!promoFormKey.currentState!.validate()) return;
// العتبات بالليرة السورية
const double minPromoLowSYP = 172; // Speed / Balash
const double minPromoHighSYP = 200; // Comfort / Electric / Lady
try {
final value = await CRUD().get(
link: AppLink.getPassengersPromo,
payload: {'promo_code': promo.text},
);
if (value == 'failure') {
MyDialog().getDialog(
'Promo Ended'.tr,
'The promotion period has ended.'.tr,
() => Get.back(),
);
return;
}
// هل يوجد فئة مؤهلة أصلاً قبل الخصم؟
final bool eligibleNow = (totalPassengerSpeed >= minPromoLowSYP) ||
(totalPassengerBalash >= minPromoLowSYP) ||
(totalPassengerComfort >= minPromoHighSYP) ||
(totalPassengerElectric >= minPromoHighSYP) ||
(totalPassengerLady >= minPromoHighSYP);
if (!eligibleNow) {
Get.snackbar(
'Lowest Price Achieved'.tr,
'Cannot apply further discounts.'.tr,
backgroundColor: AppColor.yellowColor,
);
return;
}
final decode = jsonDecode(value);
if (decode["status"] != "success") {
MyDialog().getDialog(
'Promo Ended'.tr,
'The promotion period has ended.'.tr,
() => Get.back(),
);
return;
}
Get.snackbar('Promo Code Accepted'.tr, '',
backgroundColor: AppColor.greenColor);
final firstElement = decode["message"][0];
final int discountPercentage =
int.tryParse(firstElement['amount'].toString()) ?? 0;
// قيمة المحفظة - قد تكون سالبة
final double walletVal = double.tryParse(
box.read(BoxName.passengerWalletTotal)?.toString() ?? '0') ??
0.0;
final bool isWalletNegative = walletVal < 0;
// --------------------------
// دالة تُطبّق الخصم دون النزول تحت الحد الأدنى
// --------------------------
double _applyDiscountPerTier({
required double fare,
required double minThreshold,
required bool isWalletNegative,
}) {
if (fare < minThreshold) return fare; // غير مؤهل أصلاً
final double discount = fare * (discountPercentage / 100.0);
double result;
if (isWalletNegative) {
double neg = (-1) * walletVal; // walletVal < 0 => neg positive
result = fare + neg - discount;
} else {
result = fare - discount;
}
// لا نسمح بالنزول دون الحد الأدنى
if (result < minThreshold) {
result = minThreshold;
}
// ولا نسمح بمبلغ سالب
return result.clamp(0.0, double.infinity);
}
// Comfort
totalPassengerComfort = _applyDiscountPerTier(
fare: totalPassengerComfort,
minThreshold: minPromoHighSYP,
isWalletNegative: isWalletNegative,
);
// Electric
totalPassengerElectric = _applyDiscountPerTier(
fare: totalPassengerElectric,
minThreshold: minPromoHighSYP,
isWalletNegative: isWalletNegative,
);
// Lady
totalPassengerLady = _applyDiscountPerTier(
fare: totalPassengerLady,
minThreshold: minPromoHighSYP,
isWalletNegative: isWalletNegative,
);
// Speed
totalPassengerSpeed = _applyDiscountPerTier(
fare: totalPassengerSpeed,
minThreshold: minPromoLowSYP,
isWalletNegative: isWalletNegative,
);
// Balash
totalPassengerBalash = _applyDiscountPerTier(
fare: totalPassengerBalash,
minThreshold: minPromoLowSYP,
isWalletNegative: isWalletNegative,
);
// تعديل دخل السائق وفق نسبة الخصم
totalDriver = totalDriver - (totalDriver * discountPercentage / 100.0);
promoTaken = true;
update();
// مؤثرات
Confetti.launch(
context,
options: const ConfettiOptions(particleCount: 100, spread: 70, y: 0.6),
);
Get.back();
await Future.delayed(const Duration(milliseconds: 120));
} catch (e) {
Get.snackbar('Error'.tr, e.toString(),
backgroundColor: AppColor.redColor);
}
}
double getDistanceFromText(String distanceText) {
// Remove any non-digit characters from the distance text
String distanceValue = distanceText.replaceAll(RegExp(r'[^0-9.]+'), '');
// Parse the extracted numerical value as a double
double distance = double.parse(distanceValue);
return distance;
}
double costForDriver = 0;
double totalPassengerSpeed = 0;
double totalPassengerBalash = 0;
double totalPassengerElectric = 0;
double totalPassengerLady = 0;
double totalPassengerRayehGai = 0;
double totalPassengerRayehGaiComfort = 0;
double totalPassengerRayehGaiBalash = 0;
Future bottomSheet() async {
// if (data.isEmpty) return;
// === إعدادات عامة ===
const double minFareSYP = 160; // حد أدنى
const double minBillableKm = 0.3; // حد أدنى للمسافة المفوترة
const double ladyFlatAddon = 20; // إضافة ثابتة لـ Lady
const double airportAddonSYP = 200; // إضافة المطار
// --- ⬇️ الإضافة الجديدة: إضافة حدود مطار دمشق ⬇️ ---
const double damascusAirportBoundAddon = 1400; // إضافة المطار (حدود)
// --- ⬆️ نهاية الإضافة ⬆️ ---
// كهرباء
const double electricPerKmUplift = 4; // زيادة/كم
const double electricFlatAddon = 10; // زيادة ثابتة
// Long Speed
const double longSpeedThresholdKm = 40.0;
const double longSpeedPerKm = 26.0; // Speed عند >40كم
// قواعد الرحلات البعيدة للدقائق (تعمل لكل الأوقات)
const double mediumDistThresholdKm = 25.0; // >25كم
const double longDistThresholdKm = 35.0; // >35كم
const double longTripPerMin = 6.0;
const int minuteCapMedium = 60; // سقف دقائق عند >25كم
const int minuteCapLong = 80; // سقف دقائق عند >35كم
const int freeMinutesLong = 10; // عفو 10 دقائق عند >35كم
// تخفيضات المسافات الكبيرة للفئات غير Speed
const double extraReduction100 = 0.07; // +7% فوق تخفيض >40كم للرحلات >100كم
const double maxReductionCap = 0.35; // سقف 35% كحد أقصى
// ====== زمن الرحلة ======
durationToAdd = Duration(seconds: durationToRide);
hours = durationToAdd.inHours;
minutes = (durationToAdd.inMinutes % 60).round();
final DateTime currentTime = DateTime.now();
newTime = currentTime.add(durationToAdd);
averageDuration = (durationToRide / 60) / distance;
// +5 minutes per waypoint stop surcharge
final int waypointSurchargeMinutes = activeMenuWaypointCount * 5;
final int totalMinutes =
(durationToRide / 60).floor() + waypointSurchargeMinutes;
// ====== أدوات مساعدة ======
bool _isAirport(String s) {
final t = s.toLowerCase();
return t.contains('airport') ||
s.contains('مطار') ||
s.contains('المطار');
}
bool _isClub(String s) {
final t = s.toLowerCase();
return t.contains('club') ||
t.contains('nightclub') ||
t.contains('night club') ||
s.contains('ديسكو') ||
s.contains('ملهى ليلي');
}
// --- ⬇️ الإضافة الجديدة: دالة التحقق من حدود المطار ⬇️ ---
// (P1: 33.415313, 36.499687) (P2: 33.400265, 36.531505)
bool _isInsideDamascusAirportBounds(double lat, double lng) {
final double northLat = 33.415313;
final double southLat = 33.400265;
final double eastLng = 36.531505;
final double westLng = 36.499687;
// التحقق من خط العرض (بين الشمال والجنوب)
bool isLatInside = (lat <= northLat) && (lat >= southLat);
// التحقق من خط الطول (بين الشرق والغرب)
bool isLngInside = (lng <= eastLng) && (lng >= westLng);
return isLatInside && isLngInside;
}
// --- ⬆️ نهاية الإضافة ⬆️ ---
// أسعار الدقيقة من السيرفر
final double naturePerMin = naturePrice; // طبيعي
final double latePerMin = latePrice; // ليل
final double heavyPerMin = heavyPrice; // ذروة
// سعر الدقيقة حسب الوقت (أساس قبل قواعد المسافة)
double _perMinuteByTime(DateTime now, bool clubCtx) {
final h = now.hour;
if (h >= 21 || h < 1) return latePerMin; // ليل
if (h >= 1 && h < 5) return clubCtx ? (latePerMin * 2) : latePerMin;
if (h >= 14 && h <= 17) return heavyPerMin; // ذروة
return naturePerMin; // طبيعي
}
// حد أدنى
double _applyMinFare(double fare) =>
(fare < minFareSYP) ? minFareSYP : fare;
// عمولة الراكب (kazan من السيرفر)
double _withCommission(double base) =>
(base * (1 + kazan / 100)).ceilToDouble();
// ====== سياق ======
final bool airportCtx =
_isAirport(startNameAddress) || _isAirport(endNameAddress);
final bool clubCtx = _isClub(startNameAddress) || _isClub(endNameAddress);
// --- ⬇️ الإضافة الجديدة: التحقق من سياق حدود المطار ⬇️ ---
// !! ⚠️ تأكد من أن هذه هي المتغيرات الصحيحة لإحداثيات نقطة النهاية !!
final bool damascusAirportBoundCtx = _isInsideDamascusAirportBounds(
myDestination.latitude.toDouble(), // <-- ⚠️ غيّر هذا للمتغير الصحيح
myDestination.longitude.toDouble(), // <-- ⚠️ غيّر هذا للمتغير الصحيح
);
final bool isInDamascusAirportBoundCtx = _isInsideDamascusAirportBounds(
newMyLocation.latitude.toDouble(), // <-- ⚠️ غيّر هذا للمتغير الصحيح
newMyLocation.longitude.toDouble(), // <-- ⚠️ غيّر هذا للمتغير الصحيح
);
// --- ⬆️ نهاية الإضافة ⬆️ ---
// ====== مسافة مفوترة ======
final double billableDistance =
(distance < minBillableKm) ? minBillableKm : distance;
// ====== Speed (قصير/طويل) ======
final bool isLongSpeed = billableDistance > longSpeedThresholdKm;
final double perKmSpeedBaseFromServer =
speedPrice; // مثال: 2900 يأتي من السيرفر
final double perKmSpeed =
isLongSpeed ? longSpeedPerKm : perKmSpeedBaseFromServer;
// ====== تخفيضات الفئات الأخرى حسب بُعد الرحلة ======
// ... (الكود كما هو) ...
double reductionPct40 = 0.0;
if (perKmSpeedBaseFromServer > 0) {
reductionPct40 = (1.0 - (longSpeedPerKm / perKmSpeedBaseFromServer))
.clamp(0.0, maxReductionCap);
}
final double reductionPct100 =
(reductionPct40 + extraReduction100).clamp(0.0, maxReductionCap);
double distanceReduction = 0.0;
if (billableDistance > 100.0) {
distanceReduction = reductionPct100;
} else if (billableDistance > 40.0) {
distanceReduction = reductionPct40;
}
// ====== منطق الدقيقة يعمل لكل الأوقات ويتكيّف مع المسافة ======
// ... (الكود كما هو) ...
double effectivePerMin = _perMinuteByTime(currentTime, clubCtx);
int billableMinutes = totalMinutes;
if (billableDistance > longDistThresholdKm) {
effectivePerMin = longTripPerMin;
final int capped =
(billableMinutes > minuteCapLong) ? minuteCapLong : billableMinutes;
billableMinutes = capped - freeMinutesLong;
if (billableMinutes < 0) billableMinutes = 0;
} else if (billableDistance > mediumDistThresholdKm) {
effectivePerMin = longTripPerMin;
billableMinutes = (billableMinutes > minuteCapMedium)
? minuteCapMedium
: billableMinutes;
}
// ====== أسعار/كم قبل التخفيض ======
// ... (الكود كما هو) ...
final double perKmComfortRaw = comfortPrice;
final double perKmDelivery = deliveryPrice;
final double perKmVanRaw =
(familyPrice > 0 ? familyPrice : (speedPrice + 13));
final double perKmElectricRaw = perKmComfortRaw + electricPerKmUplift;
// ====== تطبيق التخفيضات على الفئات (نفس نسبة Speed للبعيد) ======
// ... (الكود كما هو) ...
double perKmComfort = perKmComfortRaw * (1.0 - distanceReduction);
double perKmElectric = perKmElectricRaw * (1.0 - distanceReduction);
double perKmVan = perKmVanRaw * (1.0 - distanceReduction);
perKmComfort = perKmComfort.clamp(0, double.infinity);
perKmElectric = perKmElectric.clamp(0, double.infinity);
perKmVan = perKmVan.clamp(0, double.infinity);
final double perKmBalash = (perKmSpeed - 5).clamp(0, double.infinity);
// ====== دوال الاحتساب ======
double _oneWayFare({
required double perKm,
required bool isLady,
double flatAddon = 0,
}) {
double fare = billableDistance * perKm;
fare +=
billableMinutes * effectivePerMin; // دقائق بعد السقف/العفو إن وُجد
fare += flatAddon;
if (isLady) fare += ladyFlatAddon;
if (airportCtx) fare += airportAddonSYP;
// --- ⬇️ الإضافة الجديدة: تطبيق إضافة حدود المطار ⬇️ ---
if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) {
fare += damascusAirportBoundAddon;
}
// --- ⬆️ نهاية الإضافة ⬆️ ---
return _applyMinFare(fare);
}
double _roundTripFare({required double perKm}) {
// خصم 40% لمسافة إياب واحدة + زمن مضاعف (بنفس قواعد الدقيقة المعدّلة)
double distPart =
(billableDistance * 2 * perKm) - ((billableDistance * perKm) * 0.4);
double timePart = (billableMinutes * 2) * effectivePerMin;
double fare = distPart + timePart;
if (airportCtx) fare += airportAddonSYP;
// --- ⬇️ الإضافة الجديدة: تطبيق إضافة حدود المطار ⬇️ ---
// تنطبق أيضاً على رحلات الذهاب والعودة لأنها "تصل" إلى الوجهة
if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) {
fare += damascusAirportBoundAddon;
}
// --- ⬆️ نهاية الإضافة ⬆️ ---
return _applyMinFare(fare);
}
// ====== حساب كل الفئات (Base قبل العمولة) ======
final double costSpeed = _oneWayFare(perKm: perKmSpeed, isLady: false);
final double costBalash = _oneWayFare(perKm: perKmBalash, isLady: false);
final double costComfort = _oneWayFare(perKm: perKmComfort, isLady: false);
final double costElectric = _oneWayFare(
perKm: perKmElectric, isLady: false, flatAddon: electricFlatAddon);
final double costDelivery =
_oneWayFare(perKm: perKmDelivery, isLady: false);
final double costLady = _oneWayFare(
perKm: perKmComfort,
isLady: true); // Lady تعتمد Comfort بعد التخفيض + إضافة ثابتة
final double costVan = _oneWayFare(perKm: perKmVan, isLady: false);
final double costRayehGai = _roundTripFare(perKm: perKmSpeed);
final double costRayehGaiComfort = _roundTripFare(perKm: perKmComfort);
final double costRayehGaiBalash = _roundTripFare(perKm: perKmBalash);
// ====== أسعار الراكب بعد العمولة (kazan من السيرفر) ======
totalPassengerSpeed = _withCommission(costSpeed);
totalPassengerBalash = _withCommission(costBalash);
totalPassengerComfort = _withCommission(costComfort);
totalPassengerElectric = _withCommission(costElectric);
totalPassengerLady = _withCommission(costLady);
totalPassengerScooter = _withCommission(costDelivery);
totalPassengerVan = _withCommission(costVan);
totalPassengerRayehGai = _withCommission(costRayehGai);
totalPassengerRayehGaiComfort = _withCommission(costRayehGaiComfort);
totalPassengerRayehGaiBalash = _withCommission(costRayehGaiBalash);
// افتراضي للعرض
totalPassenger = totalPassengerSpeed;
totalCostPassenger = totalPassenger;
// ====== دعم رصيد محفظة سلبي ======
try {
final walletStr = box.read(BoxName.passengerWalletTotal).toString();
final walletVal = double.tryParse(walletStr) ?? 0.0;
if (walletVal < 0) {
final neg = (-1) * walletVal;
totalPassenger += neg;
totalPassengerComfort += neg;
totalPassengerElectric += neg;
totalPassengerLady += neg;
totalPassengerBalash += neg;
totalPassengerScooter += neg;
totalPassengerRayehGai += neg;
totalPassengerRayehGaiComfort += neg;
totalPassengerRayehGaiBalash += neg;
totalPassengerVan += neg;
}
} catch (e) {
Log.print("Error: $e");
}
update();
changeBottomSheetShown(forceValue: true);
}
List<LatLng> polylineCoordinate = [];
String? cardNumber;
void readyWayPoints() {
hintTextwayPointStringAll = [
hintTextwayPoint0,
hintTextwayPoint1,
hintTextwayPoint2,
hintTextwayPoint3,
hintTextwayPoint4,
];
polylineCoordinatesPointsAll = [
polylineCoordinates0,
polylineCoordinates1,
polylineCoordinates2,
polylineCoordinates3,
polylineCoordinates4,
];
allTextEditingPlaces = [
wayPoint0Controller,
wayPoint1Controller,
wayPoint2Controller,
wayPoint3Controller,
wayPoint4Controller,
];
currentLocationToFormPlacesAll = [
currentLocationToFormPlaces0,
currentLocationToFormPlaces1,
currentLocationToFormPlaces2,
currentLocationToFormPlaces3,
currentLocationToFormPlaces4,
];
placeListResponseAll = [
wayPoint0,
wayPoint1,
wayPoint2,
wayPoint3,
wayPoint4
];
startLocationFromMapAll = [
startLocationFromMap0,
startLocationFromMap1,
startLocationFromMap2,
startLocationFromMap3,
startLocationFromMap4,
];
currentLocationStringAll = [
currentLocationString0,
currentLocationString1,
currentLocationString2,
currentLocationString3,
currentLocationString4,
];
placesCoordinate = [
placesCoordinate0,
placesCoordinate1,
placesCoordinate2,
placesCoordinate3,
placesCoordinate4,
];
update();
}
List driversForMishwari = [];
Future selectDriverAndCarForMishwariTrip() async {
// Calculate the bounds for 12km range
double latitudeOffset = 0.1; // 20km range in latitude
double longitudeOffset = 0.12; // 20km range in longitude
// Calculate bounding box based on passenger's location
double southwestLat = passengerLocation.latitude - latitudeOffset;
double northeastLat = passengerLocation.latitude + latitudeOffset;
double southwestLon = passengerLocation.longitude - longitudeOffset;
double northeastLon = passengerLocation.longitude + longitudeOffset;
// Create the payload with calculated bounds
var payload = {
'southwestLat': southwestLat.toString(),
'northeastLat': northeastLat.toString(),
'southwestLon': southwestLon.toString(),
'northeastLon': northeastLon.toString(),
};
try {
// Fetch data from the API
var res = await CRUD().get(
link: AppLink.selectDriverAndCarForMishwariTrip, payload: payload);
if (res != 'failure') {
// Check if response is valid JSON
try {
var d = jsonDecode(res);
driversForMishwari = d['message'];
Log.print('driversForMishwari: $driversForMishwari');
update();
} catch (e) {
// Handle invalid JSON format
Log.print("Error decoding JSON: $e");
return 'Server returned invalid data. Please try again later.';
}
} else {
return 'No driver available now, try again later. Thanks for using our app.'
.tr;
}
} catch (e) {
// Handle network or other exceptions
Log.print("Error fetching data: $e");
return 'There was an issue connecting to the server. Please try again later.'
.tr;
}
}
final Rx<DateTime> selectedDateTime = DateTime.now().obs;
void updateDateTime(DateTime newDateTime) {
selectedDateTime.value = newDateTime;
}
Future mishwariOption() async {
isLoading = true;
update();
// add dialoug for select driver and car
await selectDriverAndCarForMishwariTrip();
Future.delayed(Duration.zero);
isLoading = false;
update();
Get.to(() => CupertinoDriverListWidget());
// changeCashConfirmPageShown();
}
var driverIdVip = '';
Future<void> saveTripData(
Map<String, dynamic> driver, DateTime tripDateTime) async {
try {
// Prepare trip data
Map<String, dynamic> tripData = {
'id': driver['driver_id'].toString(), // Ensure the id is a string
'phone': driver['phone'],
'gender': driver['gender'],
'name': driver['NAME'],
'name_english': driver['name_english'],
'address': driver['address'],
'religion': driver['religion'] ?? 'UnKnown',
'age': driver['age'].toString(), // Convert age to String
'education': driver['education'] ?? 'UnKnown', //startlocationname
'license_type': driver['license_type'] ?? 'UnKnown',
'national_number': driver['national_number'] ?? 'UnKnown',
'car_plate': driver['car_plate'],
'make': driver['make'],
'model': driver['model'],
'year': driver['year'].toString(), // Convert year to String
'color': driver['color'],
'color_hex': driver['color_hex'],
'displacement': driver['displacement'],
'fuel': driver['fuel'],
'token': driver['token'],
'rating': driver['rating'].toString(), // Convert rating to String
'countRide':
driver['ride_count'].toString(), // Convert countRide to String
'passengerId': box.read(BoxName.passengerID),
'timeSelected': tripDateTime.toIso8601String(),
'status': 'pending',
'startNameAddress': startNameAddress.toString(),
'locationCoordinate':
'${data[0]["start_location"]['lat']},${data[0]["start_location"]['lng']}',
};
Log.print('tripData: $tripData');
// Send data to server
var response =
await CRUD().post(link: AppLink.addMishwari, payload: tripData);
// Log.print('response: $response');
if (response != 'failure') {
// Trip saved successfully
// Get.snackbar('Success'.tr, 'Trip booked successfully'.tr);
var id = response['message']['id'].toString();
await CRUD()
.post(link: '${AppLink.server}/ride/rides/add.php', payload: {
"start_location":
'${data[0]["start_location"]['lat']},${data[0]["start_location"]['lng']}',
"end_location":
'${data[0]["start_location"]['lat']},${data[0]["start_location"]['lng']}',
"date": DateTime.now().toString(),
"time": DateTime.now().toString(),
"endtime": DateTime.now().add(const Duration(hours: 2)).toString(),
"price": '50',
"passenger_id": box.read(BoxName.passengerID).toString(),
"driver_id": driver['driver_id'].toString(),
"status": "waiting",
'carType': 'vip',
"price_for_driver": '50',
"price_for_passenger": '50',
"distance": '20',
"paymentMethod": 'cash',
}).then((value) {
if (value is String) {
final parsedValue = jsonDecode(value);
rideId = parsedValue['message'];
} else if (value is Map) {
rideId = value['message'];
} else {
Log.print('Unexpected response type: ${value.runtimeType}');
}
});
driverIdVip = driver['driver_id'].toString();
driverId = driver['driver_id'].toString();
DateTime timeSelected = DateTime.parse(tripDateTime.toIso8601String());
Get.find<NotificationController>().scheduleNotificationsForTimeSelected(
"Your trip is scheduled".tr,
"Don't forget your ride!".tr,
"tone1",
timeSelected);
await NotificationService.sendNotification(
category: 'OrderVIP',
target: driver['token'].toString(),
title: 'OrderVIP'.tr,
body: '$rideId - VIP Trip',
isTopic: false, // Important: this is a token
tone: 'tone1',
driverList: [
id,
rideId,
driver['id'],
passengerLocation.latitude.toString(),
startNameAddress.toString(),
passengerLocation.longitude.toString(),
(box.read(BoxName.name).toString().split(' ')[0]).toString(),
box.read(BoxName.passengerID).toString(),
box.read(BoxName.phone).toString(),
box.read(BoxName.email).toString(),
box.read(BoxName.passengerPhotoUrl).toString(),
box.read(BoxName.tokenFCM).toString(),
(driver['token'].toString()),
],
);
if (response['message'] == "Trip updated successfully") {
mySnackbarSuccess("Trip updated successfully".tr);
Log.print(
'previous_driver_token: ${response['previous_driver_token']}');
await NotificationService.sendNotification(
category: 'Order VIP Canceld',
target: response['previous_driver_token'].toString(),
title: 'Order VIP Canceld'.tr,
body: 'Passenger cancel order'.tr,
isTopic: false, // Important: this is a token
tone: 'cancel',
driverList: [],
);
}
// data = [];
isBottomSheetShown = false;
update();
Get.to(() => VipWaittingPage());
} else {
throw Exception('Failed to save trip');
}
} catch (e) {
// Show error message with more details for debugging
Get.snackbar('Error'.tr, 'Failed to book trip: $e'.tr,
backgroundColor: AppColor.redColor);
Log.print('Error: $e');
}
}
Future<void> cancelVip(String token, tripId) async {
var res = await CRUD()
.post(link: AppLink.cancelMishwari, payload: {'id': tripId});
if (res != 'failur') {
Get.back();
mySnackbarSuccess('You canceled VIP trip'.tr);
}
}
void sendToDriverAgain(String token) {
NotificationService.sendNotification(
category: 'Order VIP Canceld',
target: token.toString(),
title: 'Order VIP Canceld'.tr,
body: 'Passenger cancel order'.tr,
isTopic: false, // Important: this is a token
tone: 'cancel',
driverList: [],
);
}
// دالة الفحص عند بدء التطبيق
Future<void> detectAndCacheDeviceTier() async {
// 1. استخدام الكلاس الذي أنشأناه سابقاً للفحص
bool isHighEnd = await DevicePerformanceManager.isHighEndDevice();
// 2. طباعة النتيجة للتأكد
Log.print("Device Analysis - Is Flagship/HighEnd? $isHighEnd");
// 3. تخزين النتيجة بشكل منطقي صحيح
// إذا كان الجهاز قوياً (true)، فإن وضع الـ LowEnd يكون (false)
// والعكس صحيح
box.write(BoxName.lowEndMode, !isHighEnd);
}
initilizeGetStorage() async {
if (box.read(BoxName.addWork) == null) {
box.write(BoxName.addWork, 'addWork');
}
if (box.read(BoxName.addHome) == null) {
box.write(BoxName.addHome, 'addHome');
}
if (box.read(BoxName.lowEndMode) == null) {
detectAndCacheDeviceTier();
}
}
late List recentPlaces = [];
getFavioratePlaces0() async {
recentPlaces = await sql.getCustomQuery(
'SELECT DISTINCT latitude, longitude, name, rate FROM ${TableName.recentLocations}');
}
getFavioratePlaces() async {
recentPlaces = await sql.getCustomQuery(
'SELECT * FROM ${TableName.recentLocations} ORDER BY createdAt DESC');
// Log.print('recentPlaces: ${recentPlaces}');
}
double passengerRate = 5;
double comfortPrice = 45;
double speedPrice = 40;
double mashwariPrice = 40;
double familyPrice = 55;
double deliveryPrice = 1.2;
double minFareSYP = 16000; // حد أدنى للأجرة (سوريا)
double minBillableKm = 1.0; // حد أدنى للمسافة المفوترة
double commissionPct = 15; // عمولة التطبيق % (راكب)
getKazanPercent() async {
var res = await CRUD().get(
link: AppLink.getKazanPercent,
payload: {'country': box.read(BoxName.countryCode).toString()},
);
if (res != 'failure') {
var json = jsonDecode(res);
kazan = double.parse(json['message'][0]['kazan']);
naturePrice = double.parse(json['message'][0]['naturePrice']);
heavyPrice = double.parse(json['message'][0]['heavyPrice']);
latePrice = double.parse(json['message'][0]['latePrice']);
comfortPrice = double.parse(json['message'][0]['comfortPrice']);
speedPrice = double.parse(json['message'][0]['speedPrice']);
deliveryPrice = double.parse(json['message'][0]['deliveryPrice']);
mashwariPrice = double.parse(json['message'][0]['freePrice']);
familyPrice = double.parse(json['message'][0]['familyPrice']);
fuelPrice = double.parse(json['message'][0]['fuelPrice']);
}
}
getPassengerRate() async {
var res = await CRUD().get(
link: AppLink.getPassengerRate,
payload: {'passenger_id': box.read(BoxName.passengerID)});
if (res != 'failure') {
var message = jsonDecode(res)['message'];
if (message['rating'] == null) {
passengerRate = 5.0; // Default rating
} else {
// Safely parse the rating to double
var rating = message['rating'];
if (rating is String) {
passengerRate =
double.tryParse(rating) ?? 5.0; // Default if parsing fails
} else if (rating is num) {
passengerRate =
rating.toDouble(); // Already a number, convert to double
} else {
passengerRate = 5.0; // Default for unexpected data types
}
}
} else {
passengerRate = 5.0; // Default rating for failure
}
}
addFingerPrint() async {
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
await CRUD().postWallet(link: AppLink.addFingerPrint, payload: {
'token': (box.read(BoxName.tokenFCM.toString())),
'passengerID': box.read(BoxName.passengerID).toString(),
"fingerPrint": fingerPrint
});
}
firstTimeRunToGetCoupon() async {
// Check if it's the first time and the app is installed and gift token is available
if (box.read(BoxName.isFirstTime).toString() == '0' &&
box.read(BoxName.isInstall).toString() == '1' &&
box.read(BoxName.isGiftToken).toString() == '0') {
var promo, discount, validity;
var resPromo = await CRUD().get(link: AppLink.getPromoFirst, payload: {
"passengerID": box.read(BoxName.passengerID).toString(),
});
if (resPromo != 'failure') {
var d1 = jsonDecode(resPromo);
promo = d1['message']['promo_code'];
discount = d1['message']['amount'];
validity = d1['message']['validity_end_date'];
}
box.write(BoxName.isFirstTime, '1');
// Show a full-screen modal styled as an ad
Get.dialog(
AlertDialog(
contentPadding:
EdgeInsets.zero, // Removes the padding around the content
content: SizedBox(
width: 300, // Match the width of PromoBanner
// height: 250, // Match the height of PromoBanner
child: PromoBanner(
promoCode: promo,
discountPercentage: discount,
validity: validity,
),
),
),
);
}
}
// --- دالة جديدة للاستماع ومعالجة الرابط ---
void _listenForDeepLink() {
ever(_deepLinkController.rawDeepLink, (String? link) async {
if (link != null && link.isNotEmpty) {
Log.print('📍 MapPassengerController processing link: $link');
// 1. استخراج الإحداثيات باستخدام الدالة الموجودة لديك مسبقاً
Map<String, double>? coordinates =
await extractCoordinatesFromLinkAsync(link);
if (coordinates != null) {
double destLat = coordinates['latitude']!;
double destLng = coordinates['longitude']!;
myDestination = LatLng(destLat, destLng);
// 2. التحقق من موقع الراكب الحالي
if (passengerLocation == null ||
(passengerLocation.latitude == 0 &&
passengerLocation.longitude == 0)) {
Log.print('⏳ Waiting for current location to calculate route...');
await getLocation(); // جلب موقع الراكب إذا لم يكن متاحاً
}
if (passengerLocation != null) {
String originStr =
'${passengerLocation.latitude},${passengerLocation.longitude}';
String destStr = '$destLat,$destLng';
Log.print(
'🚀 Drawing route from Deep Link: $originStr to $destStr');
// 3. مسح أي مسارات ونقاط توقف سابقة
clearPolyline();
waypoints.clear();
clearAllMenuWaypoints();
// 4. استدعاء دالة رسم المسار وحساب التكلفة التي برمجتها
await getDirectionMap(originStr, destStr);
// 5. إظهار الواجهة السفلية للرحلة ليكون الطلب جاهزاً بنقرة واحدة
isBottomSheetShown = true;
heightBottomSheetShown = 250;
update();
Get.snackbar(
'Location Received'.tr,
'Route and prices have been calculated successfully!'.tr,
backgroundColor: AppColor.greenColor,
colorText: Colors.white,
);
}
} else {
Log.print('⚠️ Could not extract valid coordinates from link: $link');
}
// تفريغ الرابط بعد معالجته حتى لا يتم استدعاؤه مرة أخرى بالخطأ
_deepLinkController.rawDeepLink.value = null;
}
});
// معالجة الرابط إذا كان موجوداً مسبقاً (Cold Start) قبل تفعيل المستمع
if (_deepLinkController.rawDeepLink.value != null &&
_deepLinkController.rawDeepLink.value!.isNotEmpty) {
String link = _deepLinkController.rawDeepLink.value!;
_deepLinkController.rawDeepLink.value = null;
// نؤجل التنفيذ قليلاً لضمان تحميل الخريطة
Future.delayed(const Duration(milliseconds: 500), () async {
Log.print(
'📍 MapPassengerController processing link (Cold Start): $link');
Map<String, double>? coordinates =
await extractCoordinatesFromLinkAsync(link);
if (coordinates != null) {
double destLat = coordinates['latitude']!;
double destLng = coordinates['longitude']!;
myDestination = LatLng(destLat, destLng);
if (passengerLocation == null ||
(passengerLocation.latitude == 0 &&
passengerLocation.longitude == 0)) {
await getLocation();
}
if (passengerLocation != null) {
String originStr =
'${passengerLocation.latitude},${passengerLocation.longitude}';
String destStr = '$destLat,$destLng';
clearPolyline();
waypoints.clear();
clearAllMenuWaypoints();
await getDirectionMap(originStr, destStr);
isBottomSheetShown = true;
heightBottomSheetShown = 250;
update();
}
}
});
}
}
@override
void onInit() async {
super.onInit();
_checkAndRefreshMapStyle(); // Verify style version and clear cache if needed
// // --- إضافة جديدة: تهيئة وحدة التحكم في الروابط العميقة ---
Get.put(DeepLinkController(), permanent: true);
// // ----------------------------------------------------
// مرحلة 0: الضروري جداً لعرض الخريطة سريعاً
// mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY);
await initilizeGetStorage(); // إعداد سريع
await _initMinimalIcons(); // start/end فقط
// await addToken(); // لو لازم للمصادقة
_listenForDeepLink();
// initSocket();
await getLocation(); // لتحديد الكاميرا
box.write(BoxName.carType, 'yet');
box.write(BoxName.tipPercentage, '0');
// await detectAndCacheDeviceTier();
// لا تُنشئ Controllers الثقيلة الآن:
Get.lazyPut<TextToSpeechController>(() => TextToSpeechController(),
fenix: true);
Get.lazyPut<FirebaseMessagesController>(() => FirebaseMessagesController(),
fenix: true);
Get.lazyPut<AudioRecorderController>(() => AudioRecorderController(),
fenix: true);
// ابدأ الخريطة الآن (الشاشة ظهرت للمستخدم)
Future.delayed(const Duration(seconds: 4), () {
if (isLoading) {
isLoading = false;
update();
}
});
// مرحلة 1: مهام ضرورية للتسعير لكن غير حرجة لظهور UI
unawaited(_stagePricingAndState());
// مرحلة 2: تحسينات/كماليات بالخلفية
unawaited(_stageNiceToHave());
// ابدأ إعادة تحميل الماركر لكن بثروتل داخلي
// startMarkerReloading(); // تأكد أنه مَخنوق التحديث (throttled)
_startMasterTimer();
// Start listening to emergency shake gestures
EmergencySignalService.instance.startListening(() {
if (statusRide == 'Begin' || statusRide == 'start') {
Log.print("🚨 Emergency shake verified! Prompting SOS...");
if (isBottomSheetShown) {
sosPassenger();
} else {
Get.snackbar(
'Emergency Mode Triggered'.tr,
'Stay calm. We are here to help.'.tr,
backgroundColor: AppColor.redColor,
colorText: Colors.white,
duration: const Duration(seconds: 4),
);
sosPassenger();
}
}
});
}
// === Helpers ===
Future<void> _initMinimalIcons() async {
// Icons are now loaded dynamically via MapLibre's _loadMapIcons onStyleLoaded
}
Future<void> _stagePricingAndState() async {
try {
await getKazanPercent(); // أسعار السيرفر
} catch (e) {
Log.print("Error: $e");
}
try {
_checkInitialRideStatus(); // تحقق من حالة الرحلة الحالية
} catch (e) {
Log.print("Error: $e");
}
// لو عندك ضبط “وضع خفيف” حسب الجهاز:
_applyLowEndModeIfNeeded();
}
Future<void> _stageNiceToHave() async {
Log.print('🚀 MapPassengerController: Starting _stageNiceToHave');
// 🔥 Fix: Future.wait uses ONE argument (the list).
await Future.wait([
Future(() async {
try {
Log.print('🔍 Loading Favorites...');
getFavioratePlaces();
} catch (e) {
Log.print("Error: $e");
}
}),
Future(() async {
try {
Log.print('🔍 Loading Waypoints...');
readyWayPoints();
} catch (e) {
Log.print("Error: $e");
}
}),
Future(() async {
try {
Log.print('🔍 Loading Rate...');
getPassengerRate();
} catch (e) {
Log.print("Error: $e");
}
}),
Future(() async {
try {
Log.print('🔍 Loading Coupons...');
firstTimeRunToGetCoupon();
} catch (e) {
Log.print("Error: $e");
}
}),
]);
Log.print('✅ MapPassengerController: _stageNiceToHave complete');
try {
cardNumber = await SecureStorage().readData(BoxName.cardNumber);
} catch (e) {
Log.print("Error: $e");
}
}
void _applyLowEndModeIfNeeded() {
// مثال بسيط: يمكنك حفظ فلاج بنظامك (من السيرفر/الإعدادات/الكاش) لتفعيل وضع خفيف
// لاحقاً فعّل: map.trafficEnabled=false, buildingsEnabled=false, تبسيط polylines...
// controller.lowEndMode = true;
}
uploadPassengerLocation() async {
await CRUD().post(link: AppLink.addpassengerLocation, payload: {
"passengerId": box.read(BoxName.passengerID),
"lat": passengerLocation.latitude.toString(),
"lng": passengerLocation.longitude.toString(),
"rideId": rideId.toString()
});
}
void _showRideStartNotifications() {
// تنبيهات الأسعار حسب نوع السيارة
if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) {
NotificationController().showNotification('Fixed Price'.tr,
'The captain is responsible for the route.'.tr, 'ding');
} else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) {
NotificationController().showNotification('Attention'.tr,
'The price may increase if the route changes.'.tr, 'ding');
}
}
/// Checks the current version of assets/style.json and purges the map cache if it has changed.
Future<void> _checkAndRefreshMapStyle() async {
try {
final String styleJson = await rootBundle.loadString('assets/style.json');
final Map<String, dynamic> decoded = json.decode(styleJson);
final String? currentVersion =
decoded['metadata'] != null ? decoded['metadata']['version'] : null;
if (currentVersion == null) return;
final String lastVersion = box.read(BoxName.styleVersion) ?? "0.0.0";
if (currentVersion != lastVersion) {
Log.print(
"♻️ Map Style Version mismatch ($lastVersion -> $currentVersion). Purging offline cache...");
await OfflineMapService.instance.clearCache();
// Final verification check: give native engine time to flush
await Future.delayed(const Duration(milliseconds: 500));
box.write(BoxName.styleVersion, currentVersion);
Log.print("✅ Style Version updated to $currentVersion");
}
} catch (e) {
Log.print("⚠️ Style version check failed: $e");
}
}
}
class CarLocation {
final String id;
final double latitude;
final double longitude;
final double distance;
final double duration;
CarLocation({
required this.id,
required this.latitude,
required this.longitude,
this.distance = 10000,
this.duration = 10000,
});
}