Files
intaleq/lib/controller/home/map_passenger_controller.dart
Hamza-Ayed 3e89e1f1f0 26-1-21/1
2026-01-21 17:01:45 +03:00

7957 lines
286 KiB
Dart

import 'dart:async';
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:convert';
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: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';
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:google_maps_flutter/google_maps_flutter.dart';
// import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
import 'package:intl/intl.dart';
import 'package:location/location.dart';
import 'package:Intaleq/constant/colors.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/country_polygons.dart';
import '../../constant/info.dart';
import '../../constant/links.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 '../../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 '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),
];
LatLngBounds? boundsdata;
List<Marker> markers = [];
List<Polyline> polyLines = [];
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 = [];
BitmapDescriptor markerIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor tripIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor motoIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor ladyIcon = BitmapDescriptor.defaultMarker;
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 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 = [];
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)
// ==============================================================================
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})
.setReconnectionAttempts(5)
.setReconnectionDelay(2000)
.build(),
);
socket.connect();
// ✅ معالج الاتصال
socket.onConnect((_) {
Log.print("✅ Socket Connected Successfully");
isSocketConnected = true;
_reconnectAttempts = 0; // إعادة تعيين عداد المحاولات
update();
});
// ⚠️ معالج الانقطاع
socket.onDisconnect((_) {
Log.print("⚠️ Socket Disconnected");
isSocketConnected = false;
// تفعيل Polling أسرع كـ Fallback
if (_isActiveRideState()) {
Log.print("🔄 Switching to Fast Polling Mode (6s interval)");
_startMasterTimerWithInterval(4);
}
update();
});
// ❌ معالج الأخطاء
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);
});
}
// دالة مساعدة
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 لتخفيف البيانات
String dynamicApiUrl =
'https://routesjo.intaleq.xyz/route/v1/driving/${driverPos.longitude},${driverPos.latitude};${passengerPos.longitude},${passengerPos.latitude}';
var uri = Uri.parse('$dynamicApiUrl?steps=false&overview=full');
Log.print('📍 Calculating Driver Route: $uri');
try {
final response = await http.get(uri).timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
final responseData = json.decode(response.body);
if (responseData['code'] == 'Ok' &&
responseData['routes'] != null &&
responseData['routes'].isNotEmpty) {
var routeData = responseData['routes'][0];
// 2. تحديث المتغيرات (المسافة والوقت)
// نستخدم المعامل kDurationScalar لضبط الوقت كما في getDirectionMap
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)
String pointsString = routeData['geometry'] ?? "";
if (pointsString.isNotEmpty) {
List<LatLng> decodedPoints =
await compute(decodePolylineIsolate, pointsString);
// حفظ نسخة للمقارنة
_currentDriverRoutePoints = decodedPoints;
// إزالة خط مسار السائق القديم فقط
polyLines
.removeWhere((p) => p.polylineId.value == 'driver_track_line');
// إضافة الخط الجديد (بستايل مميز للسائق)
polyLines.add(Polyline(
polylineId: const PolylineId('driver_track_line'),
points: decodedPoints,
color: Colors.black87, // لون مختلف عن مسار الرحلة الأساسي
width: 5,
jointType: JointType.round,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
patterns: [PatternItem.dash(10), PatternItem.gap(10)], // خط منقط
));
}
// 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 (_) {}
}
}
// إذا كانت القائمة فارغة، نحاول بناءها من البيانات المتفرقة (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] عند قيام السائق بإلغاء الرحلة.
/// تضمن عدم تضارب الإشعارات وتوحد تجربة المستخدم.
void processRideCancelledByDriver(dynamic data, {String source = "Unknown"}) {
if (_isCancelProcessed) return;
_isCancelProcessed = true;
stopAllTimers();
if (Get.isDialogOpen == true) Get.back();
Get.defaultDialog(
title: "Sorry 😔".tr, // استخدام المفتاح الإنجليزي
titleStyle:
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
barrierDismissible: false,
content: Column(
children: [
const Icon(Icons.cancel_presentation,
size: 50, color: Colors.redAccent),
const SizedBox(height: 10),
Text(
"The driver cancelled the trip for an emergency reason.\nDo you want to search for another driver immediately?"
.tr,
textAlign: TextAlign.center,
),
],
),
actions: [
TextButton(
onPressed: () {
Get.back();
handleNoDriverFound();
},
child: Text("Cancel Trip".tr,
style: const TextStyle(color: Colors.grey)),
),
ElevatedButton.icon(
style:
ElevatedButton.styleFrom(backgroundColor: AppColor.primaryColor),
icon: const Icon(Icons.refresh, color: Colors.white),
label: Text("Search for another driver".tr,
style: const TextStyle(color: Colors.white)),
onPressed: () {
Get.back();
retrySearchForDrivers();
},
),
],
);
}
void handleNoDriverFound() {
stopAllTimers();
_isCancelProcessed = false;
currentRideState.value = RideState.noRide;
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.ride}/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].
void startSearchingTimer() {
_searchTimer?.cancel();
int seconds = 0;
Log.print("⏳ Search Timer Started (90s)...");
_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).
void processDriverArrival(String source) {
// 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';
// 3. تشغيل واجهة الوصول والعداد
driverArrivePassengerDialoge();
startTimerDriverWaitPassenger5Minute();
update();
}
// متغير لمنع التكرار
bool _isFinishProcessed = false;
/// **معالجة إنهاء الرحلة الموحدة (Unified Ride Finish Handler)**
///
/// تستدعى عند استلام حدث النهاية من السوكيت أو FCM.
/// تقوم بإغلاق السوكيت، إيقاف التتبع، والانتقال لشاشة التقييم والدفع.
///
/// * [driverList]: قائمة البيانات [driverId, rideId, token, price].
void processRideFinished(List<dynamic> driverList,
{String source = "Unknown"}) {
// 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',
);
// 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");
}
}
// أضف هذا في دالة onClose الرئيسية للكنترولر
// override onClose() {
// disposeRideSocket();
// _stopDriverLocationPolling();
// super.onClose();
// }
// === معالج جديد لموقع السائق ===
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;
// تحديث الوقت المتبقي (إذا أرسله السيرفر)
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);
update();
} catch (e) {
Log.print('Error in handleDriverLocationUpdate: $e');
}
}
// دالة مساعدة لتحديث ماركر السائق
void _updateDriverMarker(LatLng position, double heading) {
final markerId = MarkerId('driver_location');
int existingIndex = markers.indexWhere((m) => m.markerId == markerId);
BitmapDescriptor icon = carIcon; // أو حسب نوع السيارة
if (existingIndex != -1) {
markers[existingIndex] = markers[existingIndex].copyWith(
positionParam: position,
rotationParam: heading,
);
} else {
markers.add(Marker(
markerId: markerId,
position: position,
rotation: heading,
icon: icon,
anchor: const Offset(0.5, 0.5),
infoWindow: InfoWindow(title: passengerName),
));
}
}
// === إضافة متغير للتحكم ===
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.add(Polyline(
polylineId: PolylineId('restored_trip'),
points: polylineCoordinates,
width: 10,
color: Colors.blue,
));
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);
}
// إشعارات (الأسعار، الأمان...)
_showRideStartNotifications();
update(); // تحديث الواجهة لإظهار بيانات السائق
// 5. 🔥 العمليات الجغرافية (المسار والوقت) 🔥
// أ) جلب موقع السائق الأولي
// await getDriverCarsLocationToPassengerAfterApplied();// stop this to use socket update
_startSocketWatchdog();
// ب) رسم المسار وحساب الوقت
if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) {
LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last;
// نستخدم الدالة الموحدة الجديدة للحساب والرسم
await calculateDriverToPassengerRoute(driverPos, passengerLocation);
// ج) تشغيل التايمر المحلي (للعد التنازلي فقط)
startTimerFromDriverToPassengerAfterApplied();
}
// 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: const TextStyle(color: AppColor.redColor)),
onPressed: () {
Get.back();
changeCancelRidePageShow();
// cancelRide(); // دالة إلغاء الرحلة
},
),
CupertinoDialogAction(
child: Text("Increase Fare".tr,
style: const 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] مرة واحدة فقط.
void processRideBegin({String source = "Unknown"}) {
// 1. الحارس: إذا بدأت الرحلة مسبقاً، تجاهل
if (currentRideState.value == RideState.inProgress ||
_isRideStartedProcessed) {
Log.print("✋ Ignored Start Request from $source. Already Started.");
return;
}
// شرط إضافي: يجب أن نكون في حالة "وصل السائق" أو "تم القبول" لبدء الرحلة
if (currentRideState.value != RideState.driverArrived &&
currentRideState.value != RideState.driverApplied) {
Log.print(
"⚠️ Start Request ignored due to invalid previous state: ${currentRideState.value}");
// يمكن السماح بها كحالة استثنائية (Fail-safe) إذا انقطع الاتصال سابقاً
// return;
}
_isRideStartedProcessed = true;
Log.print("🚀 Ride Started via $source! Processing...");
// 2. إغلاق أي ديالوج مفتوح (مثل ديالوج السائق وصل)
if (Get.isDialogOpen == true) Get.back();
// 3. تحديث الحالة
currentRideState.value = RideState.inProgress;
statusRide = 'Begin';
// 4. تصفير وإيقاف عدادات الانتظار
remainingTimeDriverWaitPassenger5Minute = 0;
_stopWaitPassengerTimer(); // دالة إيقاف تايمر الانتظار
// 5. بدء عداد وقت الرحلة الفعلي
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];
// استخراج الاسم من displayName.text أو بديله
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);
}
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);
update();
}
// final mainBottomMenuMap = GlobalKey<AnimatedContainer>();
void changeBottomSheetShown() {
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')}';
// جلب موقع السائق كل 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) {
// أ) شرط الإيقاف الحاسم: إذا انتهت الرحلة أو ألغيت
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')}';
// د) منطق الإشعارات (ربع الوقت)
if (progressTimerRideBegin >= 0.25 &&
progressTimerRideBegin < 0.26 &&
!_hasShownSpeedWarning) {
// يمكن إضافة منطق إشعار منتصف الرحلة هنا
}
// هـ) مراقبة السرعة (Speed Check)
// نستخدم المتغير _hasShownSpeedWarning لمنع تكرار الديالوج بشكل مزعج
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();
},
),
);
}
/// **مشاركة تفاصيل الرحلة للطوارئ (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();
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) {
print("Ride status: $status");
// Perform additional actions based on the status
}, onError: (error) {
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)});
// print(res);
Log.print('rideStatusFromStartApp: ${res}');
// print('1070');
if (res == 'failure') {
rideStatusFromStartApp = {
'data': {'status': 'NoRide', 'needsReview': false}
};
isStartAppHasRide = false;
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('begin trip'),
points: polylineCoordinates,
width: 10,
color: Colors.blue,
);
polyLines.add(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();
}));
}
Map<String, double>? extractCoordinatesFromLink(String link) {
try {
// Extract the URL part from the link by finding the first occurrence of "http"
int urlStartIndex = link.indexOf(RegExp(r'https?://'));
if (urlStartIndex == -1) {
throw const FormatException('No URL found in the provided link.');
}
// Extract the URL and clean it
link = link.substring(urlStartIndex).trim();
Uri uri = Uri.parse(link);
// Common coordinate query parameters
List<String> coordinateParams = ['q', 'cp', 'll'];
// Try to extract coordinates from query parameters
for (var param in coordinateParams) {
String? value = uri.queryParameters[param];
if (value != null && (value.contains(',') || value.contains('~'))) {
List<String> coordinates =
value.contains(',') ? value.split(',') : value.split('~');
if (coordinates.length == 2) {
double? latitude = double.tryParse(coordinates[0].trim());
double? longitude = double.tryParse(coordinates[1].trim());
if (latitude != null && longitude != null) {
return {
'latitude': latitude,
'longitude': longitude,
};
}
}
}
}
// Try to extract coordinates from the path
List<String> pathSegments = uri.pathSegments;
for (var segment in pathSegments) {
if (segment.contains(',')) {
List<String> coordinates = segment.split(',');
if (coordinates.length == 2) {
double? latitude = double.tryParse(coordinates[0].trim());
double? longitude = double.tryParse(coordinates[1].trim());
if (latitude != null && longitude != null) {
return {
'latitude': latitude,
'longitude': longitude,
};
}
}
}
}
} catch (e) {
print('Error parsing location link: $e');
}
return null;
}
double latitudeWhatsApp = 0;
double longitudeWhatsApp = 0;
void handleWhatsAppLink(String link) {
Map<String, double>? coordinates = extractCoordinatesFromLink(link);
if (coordinates != null) {
latitudeWhatsApp = coordinates['latitude']!;
longitudeWhatsApp = coordinates['longitude']!;
print(
'Extracted coordinates: Lat: $latitudeWhatsApp, Long: $longitudeWhatsApp');
// Use these coordinates in your app as needed
} else {
print('Failed to extract coordinates from the link');
}
}
void goToWhatappLocation() async {
if (sosFormKey.currentState!.validate()) {
changeIsWhatsAppOrder(true);
Get.back();
handleWhatsAppLink(whatsAppLocationText.text);
myDestination = LatLng(latitudeWhatsApp, longitudeWhatsApp);
await mapController?.animateCamera(CameraUpdate.newLatLng(
LatLng(passengerLocation.latitude, passengerLocation.longitude)));
changeMainBottomMenuMap();
passengerStartLocationFromMap = true;
isPickerShown = true;
update();
}
}
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');
}
}
int tick = 0; // Move tick outside the function to maintain its state
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: const 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('payload add_ride: ${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> delayAndFetchRideStatusForAllDriverAvailable(
// String rideId) async {
// int attemptCounter = 0;
// bool isApplied = false;
// tick = 0;
// await addRideToNotificationDriverAvailable();
// Timer.periodic(const Duration(seconds: 1), (timer) async {
// if (attemptCounter >= maxAttempts || isApplied == true) {
// timer.cancel();
// _rideStatusStreamController.close(); // Close the stream when done
// return;
// }
// attemptCounter++;
// tick++;
// try {
// var res = await getRideStatus(rideId);
// String rideStatusDelayed = res.toString();
// Log.print('rideStatusDelayed: $rideStatusDelayed');
// _rideStatusStreamController
// .add(rideStatusDelayed); // Emit the ride status
// // addRideToNotificationDriverString();
// if (rideStatusDelayed == 'Cancel') {
// timer.cancel();
// NotificationController().showNotification(
// "Order Cancelled".tr, "you canceled order".tr, 'ding');
// _rideStatusStreamController
// .close(); // Close stream after cancellation
// //
// //
// } else if (rideStatusDelayed == 'Apply' ||
// rideStatusDelayed == 'Applied') {
// isApplied = true;
// // timer.cancel();
// rideAppliedFromDriver(isApplied);
// timer.cancel();
// // Close stream after applying
// } else if (attemptCounter >= maxAttempts ||
// rideStatusDelayed == 'waiting') {
// // timer.cancel(); //todo
// // addRideToNotificationDriverString();
// // Show dialog to increase fee...
// // buildTimerForIncrease();
// Get.defaultDialog(
// title: 'Are you want to wait drivers to accept your order'.tr,
// middleText: '',
// onConfirm: () {
// Log.print('[DEBUG] User chose to wait again');
// Get.back();
// notifyAvailableDriversAgain();
// delayAndFetchRideStatusForAllDriverAvailable(rideId);
// // addRideToNotificationDriverAvailable();
// },
// onCancel: () {
// timer.cancel();
// Get.back();
// showCancelRideBottomSheet();
// },
// );
// // MyDialog().getDialog(
// // 'Are you want to wait drivers to accept your order'.tr, '', () {
// // Get.back();
// // addRideToNotificationDriverAvailable();
// // });
// update();
// _rideStatusStreamController
// .close(); // Close stream after max attempts
// }
// } catch (e) {
// _rideStatusStreamController.addError(e); // Handle errors in the stream
// }
// });
// }
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
.removeWhere((p) => p.polylineId.value == 'driver_track_line');
// إضافة الخط الجديد
polyLines.add(Polyline(
polylineId: const PolylineId('driver_track_line'),
points: decodedPoints,
color: Colors.black87, // لون مميز لمسار السائق
width: 5,
jointType: JointType.round,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
patterns: [
PatternItem.dash(10),
PatternItem.gap(10)
], // جعله منقطاً
));
// لا تستدعي update هنا، سيتم استدعاؤها في الدالة الأب (getDriverCars...) لتقليل عدد التحديثات
}
}
} catch (e) {
print('Error drawing driver path: $e');
}
}
// دالة مساعدة لضبط الكاميرا
// void _fitCameraToPoints(LatLng p1, LatLng p2) {
// 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);
// mapController?.animateCamera(
// CameraUpdate.newLatLngBounds(
// LatLngBounds(
// southwest: LatLng(minLat, minLng),
// northeast: LatLng(maxLat, maxLng),
// ),
// 100 // Padding
// ),
// );
// }
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),
),
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 (_) {}
}
}
// Listening to the Stream
void listenToRideStatusStream() {
rideStatusStream.listen((rideStatus) {
print("Ride Status: $rideStatus");
// Handle updates based on the ride status
}, onError: (error) {
print("Error in Ride Status Stream: $error");
// Handle stream errors
}, onDone: () {
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;
print('isCancelRidePageShown: $isCancelRidePageShown');
update();
}
Future<String> getRideStatus(String rideId) async {
final response = await CRUD().get(
link: "${AppLink.ride}/ride/rides/getRideStatus.php",
payload: {'id': rideId});
print(response);
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++;
if (reloadCount >= 3 || tick > 18 || reloadCount > 15) {
timer.cancel();
}
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
// // 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
// 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.IntaleqSyriaServer); // مثال: اختر سيرفر سوريا للبيانات
return 'Jordan';
}
// 2. فحص سوريا
if (isPointInPolygon(passengerPoint, CountryPolygons.syriaBoundary)) {
box.write(BoxName.countryCode, 'Syria');
box.write(BoxName.serverChosen, AppLink.IntaleqSyriaServer);
return 'Syria';
}
// 3. فحص مصر
if (isPointInPolygon(passengerPoint, CountryPolygons.egyptBoundary)) {
box.write(BoxName.countryCode, 'Egypt');
box.write(BoxName.serverChosen, AppLink.IntaleqAlexandriaServer);
return 'Egypt';
}
// 4. الافتراضي (إذا كان خارج المناطق المخدومة)
box.write(BoxName.countryCode, 'Jordan');
box.write(BoxName.serverChosen, AppLink.IntaleqSyriaServer);
return 'Unknown Location (Defaulting to Jordan)';
}
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(
MarkerId(carData['id'].toString()).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(
MarkerId(carData['id']).toString(),
LatLng(carData['latitude'], carData['longitude']),
carData['heading'],
_getIconForCar(carData),
);
}
}
BitmapDescriptor _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, BitmapDescriptor icon) {
Marker? existingMarker = markers.cast<Marker?>().firstWhere(
(m) => m?.markerId == MarkerId(markerId),
orElse: () => null,
);
if (existingMarker == null) {
markers.add(Marker(
markerId: MarkerId(markerId),
position: newPosition,
rotation: newHeading,
icon: icon,
));
} 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 لكي تتمكن من ترجمتها للعربية في ملفات اللغة إذا أردت، أو تركها إنجليزية
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);
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) {
print("Error parsing response: $res");
}
}
}
void handleResponse(Map<String, dynamic> res) {
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', "");
}
print(passengerLocationStringUnvirsity);
}
var polygons = <Polygon>{}.obs;
// Initialize polygons from UniversitiesPolygons
void _initializePolygons() {
List<List<LatLng>> universityPolygons =
UniversitiesPolygons.universityPolygons;
List<String> universityNames = UniversitiesPolygons.universityNames;
for (int i = 0; i < universityPolygons.length; i++) {
Polygon polygon = Polygon(
polygonId: PolygonId(universityNames[i]),
points: universityPolygons[i],
strokeColor: Colors.blueAccent,
fillColor: Colors.blueAccent.withOpacity(0.2),
strokeWidth: 2,
);
polygons.add(polygon); // Add polygon to observable set
}
}
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() {
Set<Marker> markersToRemove = markers
.where((marker) =>
marker.markerId != const MarkerId("start") &&
marker.markerId != const MarkerId("end"))
.toSet();
for (Marker marker in markersToRemove) {
markers.remove(marker);
}
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;
// تحديد الأيقونة
BitmapDescriptor icon;
if (driverData['model'].toString().contains('دراجة') ||
driverData['make'].toString().contains('دراجة')) {
icon = motoIcon;
} else if (driverData['gender'] == 'Female') {
icon = ladyIcon;
} else {
icon = carIcon;
}
// 2. البحث عن الماركر القديم وتحديثه أو إنشاء جديد
final MarkerId markerId = MarkerId(currentDriverMarkerId);
// التحقق هل الماركر موجود مسبقاً؟
final int existingIndex = markers.indexWhere((m) => m.markerId == markerId);
if (existingIndex != -1) {
// الماركر موجود، نقوم بتحريكه (Animation)
Marker oldMarker = markers[existingIndex];
_smoothlyUpdateMarker(oldMarker, newPosition, newHeading, icon);
} else {
// الماركر غير موجود، نقوم بإنشائه
markers.add(Marker(
markerId: markerId,
position: newPosition,
rotation: newHeading,
icon: icon,
anchor: const Offset(0.5, 0.5), // مهم لكي تدور السيارة حول مركزها
infoWindow: InfoWindow(title: driverName),
));
update(); // تحديث الخريطة
}
}
// التأكد من دالة التحريك السلس
void _smoothlyUpdateMarker(Marker oldMarker, LatLng newPosition,
double newHeading, BitmapDescriptor icon) {
// إذا كانت المسافة صغيرة جداً لا داعي للتحريك (لتقليل الوميض)
double distance = Geolocator.distanceBetween(
oldMarker.position.latitude,
oldMarker.position.longitude,
newPosition.latitude,
newPosition.longitude);
if (distance < 2.0) return;
final String markerIdKey = oldMarker.markerId.value;
// إلغاء أي أنيميشن سابق لنفس الماركر
_animationTimers[markerIdKey]?.cancel();
int ticks = 0;
const int totalSteps = 20; // عدد الخطوات (نعومة الحركة)
const int stepDuration = 50; // سرعة التحديث بالميلي ثانية (المجموع 1 ثانية)
double latStep =
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
double lngStep =
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
// معالجة مشكلة الدوران (مثلاً الانتقال من 350 درجة إلى 10 درجات)
if (headingStep.abs() > 180) {
// منطق لضبط الدوران في الاتجاه الأقرب (اختياري)
}
LatLng currentPos = oldMarker.position;
double currentHeading = oldMarker.rotation;
_animationTimers[markerIdKey] =
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
ticks++;
currentPos =
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
currentHeading += headingStep;
// تحديث القائمة
int index = markers.indexWhere((m) => m.markerId.value == markerIdKey);
if (index != -1) {
markers[index] = oldMarker.copyWith(
positionParam: currentPos,
rotationParam: currentHeading,
iconParam: icon, // تحديث الأيقونة في حال تغيرت
);
update(); // تحديث الواجهة في كل خطوة
}
if (ticks >= totalSteps) {
timer.cancel();
_animationTimers.remove(markerIdKey);
}
});
}
void _updateMarkerPosition(
LatLng newPosition, double newHeading, BitmapDescriptor icon) {
const String markerId = 'driverToPassengers';
Marker? existingMarker = markers.cast<Marker?>().firstWhere(
(m) => m?.markerId == const MarkerId(markerId),
orElse: () => null,
);
if (existingMarker == null) {
// If the marker doesn't exist, create it at the new position
markers.add(Marker(
markerId: const MarkerId(markerId),
position: newPosition,
rotation: newHeading,
icon: icon,
));
update();
} else {
// If the marker exists, check if the movement is significant enough to update
double distance =
_calculateDistance(existingMarker.position, newPosition);
if (distance >= minMovementThreshold) {
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
}
}
mapController?.animateCamera(CameraUpdate.newLatLng(newPosition));
}
@override
void onClose() {
print(
"--- MapPassengerController: Closing and cleaning up all resources. ---");
// 1. إلغاء المؤقتات الفردية (باستخدام ?. الآمن)
markerReloadingTimer?.cancel();
markerReloadingTimer1?.cancel();
markerReloadingTimer2?.cancel();
timerToPassengerFromDriverAfterApplied?.cancel();
_timer?.cancel();
_masterTimer?.cancel(); // (أضف المؤقت الرئيسي)
_camThrottle?.cancel(); // (أضف مؤقت الكاميرا)
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?.dispose();
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) {
print("⚠️ السائق انحرف عن المسار!");
}
}
void detectStops(Position currentPosition) {
if (currentPosition.speed < 0.5) {
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
changeCancelRidePageShow(); // إخفاء زر الإلغاء إن وجد
clearPlacesDestination();
clearPolyline();
data = [];
// إيقاف جميع التايمرات
stopAllTimers();
currentRideState.value = RideState.cancelled;
// 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();
}
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 {
// افترض وجود `placeDestinationController` و `passengerLocation` و `CRUD()` معرفة في الكنترولر
final q = placeDestinationController.text.trim();
if (q.isEmpty || q.length < 3) {
// يفضل عدم البحث قبل 3 أحرف
placesDestination = [];
update(); // افترض أنك تستخدم GetX أو أي State Management آخر
return;
}
final lat = passengerLocation.latitude;
final lng = passengerLocation.longitude;
// نصف قطر البحث بالكيلومتر
const radiusKm = 45.0;
// حساب النطاق الجغرافي (Bounding Box) لإرساله للسيرفر
final latDelta = _kmToLatDelta(radiusKm);
final lngDelta = _kmToLngDelta(radiusKm, lat);
final latMin = lat - latDelta;
final latMax = lat + latDelta;
final lngMin = lng - lngDelta;
final lngMax = lng + lngDelta;
try {
// استدعاء الـ API (تأكد من أن AppLink.getPlacesSyria يشير للسكريبت الجديد)
final response = await CRUD().post(
link: AppLink.getPlacesSyria,
payload: {
'query': q,
'lat_min': latMin.toString(),
'lat_max': latMax.toString(),
'lng_min': lngMin.toString(),
'lng_max': lngMax.toString(),
},
);
// --- [تم الإصلاح هنا] ---
// معالجة الاستجابة من السيرفر بشكل يوافق {"status":"success", "message":[...]}
List list;
if (response is Map) {
if (response['status'] == 'success' && response['message'] is List) {
list = List.from(response['message'] as List);
} else if (response['status'] == 'failure') {
print('Server Error: ${response['message']}');
return;
} else {
print('Unexpected Map shape from server');
return;
}
} else if (response is List) {
// للتعامل مع الحالات التي قد يرجع فيها السيرفر قائمة مباشرة
list = List.from(response);
} else {
print('Unexpected response shape from server');
return;
}
// --- هنا يبدأ عمل فلاتر: الترتيب النهائي الدقيق ---
// دالة مساعدة لاختيار أفضل اسم متاح
String _bestName(Map p) {
return (p['name_ar'] ?? p['name'] ?? p['name_en'] ?? '').toString();
}
// حساب المسافة والصلة والنقاط النهائية لكل نتيجة
for (final p in list) {
final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0;
final plng =
double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0;
final distance = _haversineKm(lat, lng, plat, plng);
final relevance = _relevanceScore(_bestName(p), q);
// معادلة الترتيب: (الأولوية للمسافة الأقرب) * (ثم الصلة الأعلى)
final score = (1.0 / (1.0 + distance)) * (1.0 + relevance);
p['distanceKm'] = distance;
p['relevance'] = relevance;
p['score'] = score;
}
// ترتيب القائمة النهائية حسب النقاط (الأعلى أولاً)
list.sort((a, b) {
final sa = (a['score'] ?? 0.0) as double;
final sb = (b['score'] ?? 0.0) as double;
return sb.compareTo(sa);
});
placesDestination = list;
print('Updated places: $placesDestination');
update();
} catch (e) {
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);
// print('response: ${response.statusCode} - ${response.body}');
// if (response.statusCode == 200) {
// final data = jsonDecode(response.body);
// placesDestination = data['places'] ?? [];
// update();
// } else {
// print('Error: ${response.statusCode} - ${response.reasonPhrase}');
// }
// } catch (e) {
// 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> getPlaces() async {
// var languageCode;
// // Check if `placeDestinationController.text` contains English characters
// if (RegExp(r'[a-zA-Z]').hasMatch(placeDestinationController.text)) {
// languageCode = 'en';
// } else {
// languageCode = 'ar';
// }
// // Construct the URL
// var url = Uri.parse(
// '${AppLink.searcMaps}?q=${Uri.encodeQueryComponent(placeDestinationController.text)}&limit=10&in=circle:${passengerLocation.latitude},${passengerLocation.longitude};r=50000&lang=$languageCode&apiKey=$k',
// );
// // Log the URL for debugging
// print(url);
// // box.remove(BoxName.placesDestination);
// try {
// // Make the API request
// var response = await CRUD().getHereMap(
// link: url.toString(),
// );
// // Log the response for debugging
// // Log.print('response: ${response}');
// // Check if the response is valid
// if (response != null && response['items'] != null) {
// placesDestination = response['items'];
// // Log.print('placesDestination: ${placesDestination}');
// placesDestination = response['items'];
// // box.write(BoxName.placesDestination, placesDestination);
// for (var i = 0; i < placesDestination.length; i++) {
// var res = placesDestination[i];
// // Extract fields with null safety
// var title = res['title']?.toString() ?? 'Unknown Place';
// var position = res['position'];
// var address = res['address']?['label'] ?? 'Unknown Address';
// if (position == null) {
// Log.print('Position is null for place: $title');
// continue; // Skip this place and continue with the next one
// }
// String latitude = position['lat']?.toString() ?? '0.0';
// String longitude = position['lng']?.toString() ?? '0.0';
// try {
// await savePlaceToServer(latitude, longitude, title, address);
// // Log.print('Place saved successfully: $title');
// } catch (e) {
// // Log.print('Failed to save place: $e');
// }
// } // todo save key in env then get key and use it
// } else {
// placesDestination = [];
// }
// } catch (e) {
// // Handle any errors that occur during the API request
// Log.print('Error fetching places: $e');
// placesDestination = [];
// }
// // Notify listeners that the state has changed
// update();
// }
Future<void> getPlacesStart() async {
// افترض وجود `placeDestinationController` و `passengerLocation` و `CRUD()` معرفة في الكنترولر
final q = placeStartController.text.trim();
if (q.isEmpty || q.length < 3) {
// يفضل عدم البحث قبل 3 أحرف
placesStart = [];
update(); // افترض أنك تستخدم GetX أو أي State Management آخر
return;
}
final lat = passengerLocation.latitude;
final lng = passengerLocation.longitude;
// نصف قطر البحث بالكيلومتر
const radiusKm = 200.0;
// حساب النطاق الجغرافي (Bounding Box) لإرساله للسيرفر
final latDelta = _kmToLatDelta(radiusKm);
final lngDelta = _kmToLngDelta(radiusKm, lat);
final latMin = lat - latDelta;
final latMax = lat + latDelta;
final lngMin = lng - lngDelta;
final lngMax = lng + lngDelta;
try {
// استدعاء الـ API (تأكد من أن AppLink.getPlacesSyria يشير للسكريبت الجديد)
final response = await CRUD().post(
link: AppLink.getPlacesSyria,
payload: {
'query': q,
'lat_min': latMin.toString(),
'lat_max': latMax.toString(),
'lng_min': lngMin.toString(),
'lng_max': lngMax.toString(),
},
);
// --- [تم الإصلاح هنا] ---
// معالجة الاستجابة من السيرفر بشكل يوافق {"status":"success", "message":[...]}
List list;
if (response is Map) {
if (response['status'] == 'success' && response['message'] is List) {
list = List.from(response['message'] as List);
} else if (response['status'] == 'failure') {
print('Server Error: ${response['message']}');
return;
} else {
print('Unexpected Map shape from server');
return;
}
} else if (response is List) {
// للتعامل مع الحالات التي قد يرجع فيها السيرفر قائمة مباشرة
list = List.from(response);
} else {
print('Unexpected response shape from server');
return;
}
// --- هنا يبدأ عمل فلاتر: الترتيب النهائي الدقيق ---
// دالة مساعدة لاختيار أفضل اسم متاح
String _bestName(Map p) {
return (p['name_ar'] ?? p['name'] ?? p['name_en'] ?? '').toString();
}
// حساب المسافة والصلة والنقاط النهائية لكل نتيجة
for (final p in list) {
final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0;
final plng =
double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0;
final distance = _haversineKm(lat, lng, plat, plng);
final relevance = _relevanceScore(_bestName(p), q);
// معادلة الترتيب: (الأولوية للمسافة الأقرب) * (ثم الصلة الأعلى)
final score = (1.0 / (1.0 + distance)) * (1.0 + relevance);
p['distanceKm'] = distance;
p['relevance'] = relevance;
p['score'] = score;
}
// ترتيب القائمة النهائية حسب النقاط (الأعلى أولاً)
list.sort((a, b) {
final sa = (a['score'] ?? 0.0) as double;
final sb = (b['score'] ?? 0.0) as double;
return sb.compareTo(sa);
});
placesStart = list;
print('Updated places: $placesDestination');
update();
} catch (e) {
print('Exception in getPlaces: $e');
}
}
Future getPlacesListsWayPoint(int index) async {
var languageCode = wayPoint0Controller.text;
// Regular expression to check for English alphabet characters
final englishRegex = RegExp(r'[a-zA-Z]');
// Check if text contains English characters
if (englishRegex.hasMatch(languageCode)) {
languageCode = 'en';
} else {
languageCode = 'ar';
}
var url =
'${AppLink.googleMapsLink}place/nearbysearch/json?keyword=${wayPoint0Controller.text}&location=${passengerLocation.latitude},${passengerLocation.longitude}&radius=250000&language=$languageCode&key=${AK.mapAPIKEY.toString()}';
try {
var response = await CRUD().getGoogleApi(link: url, payload: {});
if (response != null && response['results'] != null) {
wayPoint0 = response['results'];
placeListResponseAll[index] = response['results'];
update();
} else {
print('Error: Invalid response from Google Places API');
}
} catch (e) {
print('Error fetching places: $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) {
if (_camThrottle?.isActive ?? false) return;
_camThrottle = Timer(const Duration(milliseconds: 160), () {
// ضع فقط المنطق الضروري هنا لتقليل الحمل
int waypointsLength = Get.find<WayPointController>().wayPoints.length;
int index = wayPointIndex;
if (waypointsLength > 0) {
placesCoordinate[index] =
'${pos.target.latitude},${pos.target.longitude}';
}
newMyLocation = pos.target;
});
}
// تهيئة polylines خفيفة (استدعها بعد جلب المسار)
Set<Polyline> polyLinesLight = {};
List<LatLng> simplifyPolyline(List<LatLng> pts, double epsilonMeters) {
if (pts.length <= 2) return pts;
double _perpDist(LatLng p, LatLng a, LatLng b) {
// مسافة عمودية تقريبية بالأمتار
double _toRad(double d) => d * math.pi / 180.0;
// تحويل بسيط للمتر (تقريبي)
final x1 = a.longitude, y1 = a.latitude;
final x2 = b.longitude, y2 = b.latitude;
final x0 = p.longitude, y0 = p.latitude;
final num = ((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1).abs();
final den = math.sqrt(math.pow(y2 - y1, 2) + math.pow(x2 - x1, 2));
// تحويل درجات -> أمتار تقريبي (1 درجة ~ 111km)
final degDist = den == 0 ? 0 : num / den;
return degDist * 111000; // متر
}
List<LatLng> dp(int start, int end) {
double maxDist = 0;
int index = start;
for (int i = start + 1; i < end; i++) {
final d = _perpDist(pts[i], pts[start], pts[end]);
if (d > maxDist) {
maxDist = d;
index = i;
}
}
if (maxDist > epsilonMeters) {
final r1 = dp(start, index);
final r2 = dp(index, end);
return [...r1.sublist(0, r1.length - 1), ...r2];
} else {
return [pts[start], pts[end]];
}
}
return dp(0, pts.length - 1);
}
void buildLightPolylines(List<LatLng> originalPoints) {
final simplified = simplifyPolyline(originalPoints, lowPerf ? 12 : 3);
polyLinesLight = {
Polyline(
polylineId: const PolylineId('route_light'),
points: simplified,
width: lowPerf ? 4 : 6,
geodesic: true,
color: AppColor.primaryColor,
endCap: Cap.roundCap,
startCap: Cap.roundCap,
jointType: JointType.round,
),
};
update();
}
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) {
print('Error: $e');
}
}
// Future getPlacesListsWayPoint(int index) async {
// var url =
// '${AppLink.googleMapsLink}place/nearbysearch/json?keyword=${wayPoint0Controller.text}&location=${passengerLocation.latitude},${passengerLocation.longitude}&radius=80000&language=${}&key=${AK.mapAPIKEY.toString()}';
// var response = await CRUD().getGoogleApi(link: url, payload: {});
// wayPoint0 = response['results'];
// placeListResponseAll[index] = response['results'];
// update();
// }
LatLng fromString(String location) {
List<String> parts = location.split(',');
double lat = double.parse(parts[0]);
double lng = double.parse(parts[1]);
return LatLng(lat, lng);
}
void clearPolyline() {
polyLines = [];
polylineCoordinates.clear();
// polylineCoordinates = [];
polylineCoordinatesPointsAll[0].clear();
polylineCoordinatesPointsAll[1].clear();
polylineCoordinatesPointsAll[2].clear();
polylineCoordinatesPointsAll[3].clear();
polylineCoordinatesPointsAll[4].clear();
isMarkersShown = false;
update();
}
void addCustomPicker() {
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio
// scale: 1.0,
);
BitmapDescriptor.asset(
config,
'assets/images/picker.png',
// mipmaps: false,
).then((value) {
markerIcon = value;
update();
});
}
void addCustomStartIcon() async {
// Create the marker with the resized image
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio);
BitmapDescriptor.asset(
config,
'assets/images/A.png',
// mipmaps: false,
).then((value) {
startIcon = value;
update();
});
}
void addCustomEndIcon() {
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio);
BitmapDescriptor.asset(
config,
'assets/images/b.png',
// mipmaps: false,
).then((value) {
endIcon = value;
update();
});
}
void addCustomCarIcon() {
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio);
BitmapDescriptor.asset(
config,
'assets/images/car.png',
// mipmaps: false,
).then((value) {
carIcon = value;
update();
});
}
void addCustomMotoIcon() {
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio);
BitmapDescriptor.asset(
config,
'assets/images/moto1.png',
// mipmaps: false,
).then((value) {
motoIcon = value;
update();
});
}
void addCustomLadyIcon() {
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio);
BitmapDescriptor.asset(
config,
'assets/images/lady1.png',
// mipmaps: false,
).then((value) {
ladyIcon = value;
update();
});
}
void addCustomStepIcon() {
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio);
BitmapDescriptor.asset(
config,
'assets/images/brand.png',
// mipmaps: false,
).then((value) {
tripIcon = value;
update();
});
}
dialoge() {
Get.defaultDialog(
title: 'Location '.tr,
content: Container(
child: Column(
children: [
Text(
'We use location to get accurate and nearest driver for you'.tr,
style: AppStyle.title,
),
TextButton(
onPressed: () async {
// await Permission.location.request();
Get.back();
},
child: Text(
'Grant'.tr,
style: AppStyle.title,
),
)
],
),
),
);
}
double speed = 0;
Future<void> getLocation() async {
isLoading = true;
update();
bool serviceEnabled;
PermissionStatus permissionGranted;
// dialoge();
// Check if location services are enabled
serviceEnabled = await location.serviceEnabled();
if (!serviceEnabled) {
serviceEnabled = await location.requestService();
if (!serviceEnabled) {
// Location services are still not enabled, handle the error
return;
}
}
// 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
LocationData _locationData = await location.getLocation();
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;
// Log.print('passengerLocation: $passengerLocation');
speed = _locationData.speed!;
// //print location details
isLoading = false;
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),
);
}
GoogleMapController? mapController;
void onMapCreated(GoogleMapController controller) {
// myLocation = Get.find<LocationController>().location as LatLng;
// myLocation = myLocation;
mapController = controller;
controller.getVisibleRegion();
controller.animateCamera(
CameraUpdate.newLatLng(passengerLocation),
);
// Future.delayed(const Duration(milliseconds: 500), () {
// markers.forEach((marker) {
// controller.showMarkerInfoWindow(marker.markerId);
// });
// });
update();
}
// void startMarkerReloading() {
// int count = 0;
// markerReloadingTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
// reloadMarkers();
//
// count++;
// if (count == 10) {
// timer.cancel();
// }
// });
// }
bool reloadStartApp = false;
int reloadCount = 0;
startMarkerReloading() async {
if (reloadStartApp == false) {
Timer.periodic(const Duration(seconds: 3), (timer) async {
reloadCount++;
Log.print('reloadCount: $reloadCount');
if (rideConfirm == false) {
clearMarkersExceptStartEnd();
// _smoothlyUpdateMarker();
// startCarLocationSearch(box.read(BoxName.carType));
await getCarsLocationByPassengerAndReloadMarker();
await getNearestDriverByPassengerLocation();
// Log.print('reloadMarkers: from startMarkerReloading');
} else {
// runWhenRideIsBegin();
}
if (reloadCount >= 6) {
reloadStartApp = true;
timer.cancel(); // Stop the timer after 5 reloads
}
});
}
}
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 (polyLines.isEmpty || data.isEmpty) {
// return null; // Early return if data is empty
// }
// if (!rideConfirm) {
// if (dataCarsLocationByPassenger != 'failure') {
// if (dataCarsLocationByPassenger != null) {
// if (dataCarsLocationByPassenger['message'].length > 0) {
// double nearestDistance = double
// .infinity; // Initialize nearest distance to a large number
// CarLocation? nearestCar;
// for (var i = 0;
// i < dataCarsLocationByPassenger['message'].length;
// i++) {
// var carLocation = dataCarsLocationByPassenger['message'][i];
// // Calculate the 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 * 25 * (1000 / 3600)).round(); // 25 km/h in m/s
// // 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']),
// );
// // Update the UI with the nearest driver
// update();
// }
// }
// // Return the nearest car found
// return nearestCar;
// }
// }
// }
// }
// // Return null if no drivers are found or if ride is confirmed
// return null;
// }
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 lon = location.longitude;
final url =
'https://geocode.intaleq.xyz/reverse_geocode.php?lat=$lat&lon=$lon';
try {
final response = await http.get(Uri.parse(url));
// Log raw response for debugging
// Check if the request was successful
if (response.statusCode == 200) {
String body = response.body.trim();
// Validate it's actual JSON
if (body.startsWith('{') && body.endsWith('}')) {
final data = jsonDecode(body);
// Handle case where API returns {"status":"ok", ...}
if (data is Map && data['status'] == 'ok') {
String? name = data['display_name'] ?? data['neighbourhood'];
return name ?? 'Unknown Location'.tr;
} else {
// API returned JSON but not with "status: ok"
return 'Unknown Location'.tr;
}
} else {
// Not valid JSON (e.g., just "ok" or error message)
print('[WARNING] Invalid JSON: $body');
return 'Unknown Location'.tr;
}
} else {
print('[ERROR] HTTP ${response.statusCode}: ${response.body}');
return 'Unknown Location'.tr;
}
} catch (e) {
print('Error in Reverse Geocoding: $e');
return 'Unknown Location'.tr;
}
}
bool isDrawingRoute = false;
showDrawingBottomSheet() {
Get.bottomSheet(
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
color: AppColor.primaryColor,
),
const SizedBox(height: 15),
Text(
'Drawing route on map...'.tr,
style: AppStyle.title.copyWith(fontSize: 16),
),
const SizedBox(height: 5),
Text(
'Please wait while we prepare your trip.'.tr,
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
isDismissible: false,
enableDrag: false,
);
}
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.toString().split(',')[0]},${passengerLocation.longitude.toString().split(',')[1]}';
}
// 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) {
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') {
print('API returned an error: ${responseData['message']}');
isLoading = false;
update();
return; // خروج في حالة خطأ منطقي (مثل "no path")
}
} catch (e) {
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) {
isLoading = true;
isDrawingRoute = true;
update();
if (isDrawingRoute) showDrawingBottomSheet();
await getCarsLocationByPassengerAndReloadMarker();
}
// تجهيز الإحداثيات
if (origin.isEmpty) {
origin =
'${passengerLocation.latitude.toString().split(',')[0]},${passengerLocation.longitude.toString().split(',')[1]}';
}
var coordDestination = destination.split(',');
double latDest = double.parse(coordDestination[0]);
double lngDest = double.parse(coordDestination[1]);
myDestination = LatLng(latDest, lngDest);
// 2. الاتصال بالسيرفر - New OSRM format
var originCoords = origin.split(',');
String dynamicApiUrl =
'https://routesjo.intaleq.xyz/route/v1/driving/${originCoords[1]},${originCoords[0]};${lngDest},${latDest}';
var uri = Uri.parse('$dynamicApiUrl?steps=false&overview=full');
Log.print('Requesting Route URI (Attempt: ${attemptCount + 1}): ${uri}');
http.Response response;
Map<String, dynamic> responseData;
try {
response = await http.get(uri).timeout(const Duration(seconds: 20));
// تحقق من حالة الاستجابة - New format uses "code" instead of "status"
responseData = json.decode(response.body);
if (response.statusCode != 200 || responseData['code'] != 'Ok') {
if (attemptCount < 2) {
await _retryProcess(origin, destination, waypoints, attemptCount);
return;
}
_handleFatalError(
"Server Error".tr, "Connection failed. Please try again.".tr);
return;
}
// ============================================================
// 🛑 الفحص الأمني (Sanity Check) - Updated for new format
// ============================================================
// البيانات الآن داخل routes[0]
if (responseData['routes'] == null || responseData['routes'].isEmpty) {
if (attemptCount < 2) {
await _retryProcess(origin, destination, waypoints, attemptCount);
return;
}
_handleFatalError("Route Not Found".tr,
"No routes available for this destination.".tr);
return;
}
var routeData = responseData['routes'][0];
double apiDistanceMeters = (routeData['distance'] as num).toDouble();
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. معالجة البيانات (فقط في حال النجاح) - Updated field names
box.remove(BoxName.tripData);
box.write(BoxName.tripData, routeData);
// duration and distance من routes[0]
durationToRide =
((routeData['duration'] as num) * kDurationScalar).toInt();
double distanceOfTrip = (routeData['distance'] as num) / 1000.0;
distance = distanceOfTrip;
// steps الآن داخل legs[0].steps
data = routeData['legs'] != null && routeData['legs'].isNotEmpty
? (routeData['legs'][0]['steps'] ?? [])
: [];
// معالجة الرسم (Polyline) - الحقل الآن اسمه geometry بدلاً من polyline
String pointsString = routeData['geometry'] ?? "";
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);
// 4. جلب العناوين (Reverse Geocoding)
final LatLng startLoc = polylineCoordinates.first;
final LatLng endLoc = polylineCoordinates.last;
try {
final results = await Future.wait([
getReverseGeocoding(startLoc),
getReverseGeocoding(endLoc),
]);
startNameAddress = results[0];
endNameAddress = results[1];
} catch (e) {
startNameAddress = 'Start Point'.tr;
endNameAddress = 'Destination'.tr;
}
// 5. تحديث الكاميرا
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 (Get.isBottomSheetOpen ?? false) Get.back();
isDrawingRoute = false;
isLoading = false;
if (minLat != null) {
LatLngBounds boundsData = LatLngBounds(
northeast: LatLng(maxLat!, maxLng!),
southwest: LatLng(minLat!, minLng!));
mapController
?.animateCamera(CameraUpdate.newLatLngBounds(boundsData, 100));
}
// 6. إضافة الماركرز
durationToAdd = Duration(seconds: durationToRide);
hours = durationToAdd.inHours;
minutes = (durationToAdd.inMinutes % 60).round();
markers.clear();
markers.add(Marker(
markerId: const MarkerId('start'),
position: startLoc,
icon: startIcon,
infoWindow: InfoWindow(title: startNameAddress),
));
markers.add(Marker(
markerId: const MarkerId('end'),
position: endLoc,
icon: endIcon,
infoWindow: InfoWindow(
title: endNameAddress,
snippet:
'$distance ${'KM'.tr}${hours > 0 ? '$hours H $minutes m' : '$minutes m'}'),
));
// 7. رسم الخط (فقط في حال النجاح)
if (polyLines.isNotEmpty) clearPolyline();
bool isLowEndDevice = box.read(BoxName.lowEndMode) ?? true;
if (isLowEndDevice) {
polyLines.add(Polyline(
polylineId: const PolylineId('route_solid'),
points: polylineCoordinates,
width: 6,
color: AppColor.primaryColor,
endCap: Cap.roundCap,
startCap: Cap.roundCap,
jointType: JointType.round,
));
} else {
polyLines.addAll(_createGradientPolylines(polylineCoordinates,
const Color(0xFF00E5FF), const Color(0xFFFF4081)));
}
rideConfirm = false;
isMarkersShown = true;
update();
// إظهار الباتم شيت للسعر
bottomSheet();
} catch (e) {
// محاولة أخيرة عند حدوث Exception
if (attemptCount < 2) {
await _retryProcess(origin, destination, waypoints, attemptCount);
} else {
_handleFatalError("Connection Error".tr,
"Please check your internet and try again.".tr);
}
}
}
// --- دالة المساعدة لإعادة المحاولة ---
Future<void> _retryProcess(String origin, String dest, List<String> waypoints,
int currentAttempt) async {
Log.print(
"🔄 Exception or Error caught. Retrying in 1s... (Attempt ${currentAttempt + 1})");
await Future.delayed(const Duration(seconds: 1));
getDirectionMap(origin, dest, waypoints, currentAttempt + 1);
}
// -----------------------------------------------------------------------------------------
// 🛑 دالة الخطأ القاتل (تغلق كل شيء وتعيد المستخدم للخريطة)
// -----------------------------------------------------------------------------------------
void _handleFatalError(String title, String message) {
// 1. إغلاق شاشة التحميل (Drawing route...)
if (Get.isBottomSheetOpen ?? false) Get.back();
// 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());
},
),
);
}
/// دالة لتقسيم المسار الطويل إلى قطع صغيرة ملونة بتدرج
Set<Polyline> _createGradientPolylines(
List<LatLng> points, Color startColor, Color endColor) {
Set<Polyline> lines = {};
// إذا كانت النقاط قليلة جداً، نرسم خطاً عادياً
if (points.length < 2) return lines;
for (int i = 0; i < points.length - 1; i++) {
// حساب نسبة التقدم في المسار (من 0.0 إلى 1.0)
double t = i / points.length;
// دمج اللونين بناءً على النسبة للحصول على اللون الحالي
// هذا يخلق التدرج من البداية للنهاية
Color segmentColor = Color.lerp(startColor, endColor, t) ?? startColor;
lines.add(Polyline(
polylineId: PolylineId('route_segment_$i'),
points: [points[i], points[i + 1]], // وصل النقطة الحالية بالتالية فقط
width: 6, // سماكة الخط (اجعله سميكاً قليلاً ليظهر التدرج)
color: segmentColor,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
jointType: JointType.round,
zIndex: 2, // لضمان ظهوره فوق أي طبقات أخرى
));
}
// (اختياري) إضافة "وهج" أو Glow أسفل الخط الرئيسي
// يتم ذلك برسم خط واحد شفاف وعريض أسفل الخطوط الملونة
lines.add(Polyline(
polylineId: const PolylineId('route_glow'),
points: points,
width: 10, // أعرض من الخط الرئيسي
color: startColor.withOpacity(0.3), // لون شفاف
zIndex: 1, // أسفل الخط الملون
));
return lines;
}
// getDirectionMap(String origin, destination,
// [List<String> waypoints = const []]) async {
// isLoading = true;
// update();
// remainingTime = 25; //to make cancel every call
// // startCarLocationSearch(box.read(BoxName.carType));
// await getCarsLocationByPassengerAndReloadMarker(
// box.read(BoxName.carType), 5000);
// // await getCarsLocationByPassengerAndReloadMarker();
// var coordDestination = destination.split(',');
// double latPassengerDestination = double.parse(coordDestination[0]);
// double lngPassengerDestination = double.parse(coordDestination[1]);
// myDestination = LatLng(latPassengerDestination, lngPassengerDestination);
// if (origin.isEmpty) {
// origin =
// '${passengerLocation.latitude.toString().split(',')[0]},${passengerLocation.longitude.toString().split(',')[1]}'; //todo
// }
// isLoading = false;
// update();
// var url =
// ('${AppLink.googleMapsLink}directions/json?&language=${box.read(BoxName.lang) ?? 'ar'}&avoid=tolls|ferries&destination=$destination&origin=$origin&key=${AK.mapAPIKEY}');
// if (waypoints.isNotEmpty) {
// String formattedWaypoints = waypoints.join('|');
// url += '&waypoints=$formattedWaypoints';
// }
// var response = await CRUD().getGoogleApi(link: url, payload: {});
// data = response['routes'][0]['legs'];
// box.remove(BoxName.tripData);
// box.write(BoxName.tripData, response);
// startNameAddress = shortenAddress(data[0]['start_address']);
// // print('data[0][start_address]: ${data[0]['start_address']}');
// endNameAddress = shortenAddress(data[0]['end_address']);
// isLoading = false;
// newStartPointLocation = LatLng(
// data[0]["start_location"]['lat'], data[0]["start_location"]['lng']);
// durationToRide = data[0]['duration']['value'];
// 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++) {
// // double lat = decodedPoints[i][0].toDouble();
// // double lng = decodedPoints[i][1].toDouble();
// polylineCoordinates.add(decodedPoints[i]);
// }
// // Define the northeast and southwest coordinates
// // Define the northeast and southwest coordinates
// final bounds = response["routes"][0]["bounds"];
// LatLng northeast =
// LatLng(bounds['northeast']['lat'], bounds['northeast']['lng']);
// LatLng southwest =
// LatLng(bounds['southwest']['lat'], bounds['southwest']['lng']);
// // Create the LatLngBounds object
// LatLngBounds boundsData =
// LatLngBounds(northeast: northeast, southwest: southwest);
// // Fit the camera to the bounds
// var cameraUpdate = CameraUpdate.newLatLngBounds(boundsData, 130);
// mapController!.animateCamera(cameraUpdate);
// // getDistanceFromText(data[0]['distance']['text']);
// double distanceOfTrip = (data[0]['distance']['value']) / 1000;
// distance = distanceOfTrip;
// durationToAdd = Duration(seconds: durationToRide);
// hours = durationToAdd.inHours;
// minutes = (durationToAdd.inMinutes % 60).round();
// // updateCameraForDistanceAfterGetMap();
// markers.clear();
// update();
// markers.add(
// Marker(
// markerId: const MarkerId('start'),
// position: newStartPointLocation,
// icon: startIcon,
// infoWindow: InfoWindow(
// title: startNameAddress,
// snippet: '',
// ),
// ),
// );
// // Add end marker
// markers.add(
// Marker(
// markerId: const MarkerId('end'),
// position: LatLng(
// data[0]["end_location"]['lat'], data[0]["end_location"]['lng']),
// icon: endIcon,
// infoWindow: InfoWindow(
// title: endNameAddress,
// snippet:
// '$distance ${'KM'.tr} ⌛ ${hours > 0 ? '${'Your Ride Duration is '.tr}$hours ${'H and'.tr} $minutes ${'m'.tr}' : '${'Your Ride Duration is '.tr} $minutes ${'m'.tr}'}'),
// ),
// );
// // // Show info windows automatically
// // Future.delayed(const Duration(milliseconds: 500), () {
// // mapController?.showMarkerInfoWindow(const MarkerId('start'));
// // });
// // Future.delayed(const Duration(milliseconds: 500), () {
// // mapController?.showMarkerInfoWindow(const MarkerId('end'));
// // });
// // update();
// if (polyLines.isNotEmpty) {
// clearPolyline();
// } else {
// // الآن نقرأ القيمة ونحدد عدد النقاط بناءً عليها
// bool lowEndMode = box.read(BoxName.lowEndMode) ??
// false; // الأفضل أن يكون الافتراضي هو الجودة العالية
// // نمرر عدد النقاط المناسب هنا
// if (Platform.isIOS) {
// animatePolylineLayered(
// polylineCoordinates,
// maxPoints:
// lowEndMode ? 30 : 150, // 30 نقطة لوضع الأداء، 150 للوضع العادي
// );
// } else {
// polyLines.add(Polyline(
// polylineId: const PolylineId('route'),
// points: polylineCoordinates,
// width: 6,
// color: AppColor.primaryColor,
// endCap: Cap.roundCap,
// startCap: Cap.roundCap,
// jointType: JointType.round,
// ));
// }
// rideConfirm = false;
// isMarkersShown = true;
// update();
// }
// }
// 1) تقليل النقاط إلى حد أقصى 30 نقطة (مع بداية ونهاية محفوظة وتوزيع متساوٍ)
List<LatLng> _downsampleEven(List<LatLng> coords, {int maxPoints = 30}) {
if (coords.isEmpty) return const [];
if (coords.length <= maxPoints) return List<LatLng>.from(coords);
final int n = coords.length;
final int keep = maxPoints.clamp(2, n);
final List<int> idx = [];
for (int i = 0; i < keep; i++) {
final double pos = i * (n - 1) / (keep - 1);
idx.add(pos.round());
}
final seen = <int>{};
final List<LatLng> out = [];
for (final i in idx) {
if (seen.add(i)) out.add(coords[i]);
}
if (out.first != coords.first) out.insert(0, coords.first);
if (out.last != coords.last) out.add(coords.last);
while (out.length > maxPoints) {
out.removeAt(out.length ~/ 2);
}
return out;
}
// 2) رسم متدرّج بطبقات متراكبة (بدون حذف)، برونزي ↔ أخضر، مع zIndex وعرض مختلف
Future<void> animatePolylineLayered(List<LatLng> coordinates,
{int layersCount = 8, int stepDelayMs = 10, int maxPoints = 160}) async {
// امسح أي طبقات قديمة فقط الخاصة بالطريق
polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_layer_'));
update();
final List<LatLng> coords =
_downsampleEven(coordinates, maxPoints: maxPoints);
if (coords.length < 2) return;
// ألوان مع شفافية خفيفة للتمييز
Color bronze([int alpha = 220]) => AppColor.gold;
Color green([int alpha = 220]) => AppColor.primaryColor;
Color _layerColor(int layer) => (layer % 2 == 0) ? bronze() : green();
// عرض الخط: البرونزي أعرض، الأخضر أنحف
int _layerWidth(int layer) => (layer % 2 == 0) ? 6 : 4;
// لكل طبقة: أنشئ Polyline بهوية فريدة وزي إندكس أعلى من السابقة
for (int layer = 0; layer < layersCount; layer++) {
final id = PolylineId('route_layer_$layer');
polyLines.add(Polyline(
polylineId: id,
points: const [],
width: _layerWidth(layer),
color: _layerColor(layer),
zIndex: layer, // مهم لإظهار جميع الطبقات
endCap: Cap.roundCap,
startCap: Cap.roundCap,
geodesic: true,
visible: true,
));
}
update();
// نبني كل طبقة تدريجيًا فوق التي قبلها — بدون مسح الطبقات السابقة
for (int layer = 0; layer < layersCount; layer++) {
final id = PolylineId('route_layer_$layer');
final List<LatLng> growing = [];
for (int i = 0; i < coords.length; i++) {
growing.add(coords[i]);
// حدّث فقط هذه الطبقة
polyLines.removeWhere((p) => p.polylineId == id);
polyLines.add(Polyline(
polylineId: id,
points: List<LatLng>.from(growing),
width: _layerWidth(layer),
color: _layerColor(layer),
zIndex: layer,
endCap: Cap.roundCap,
startCap: Cap.roundCap,
geodesic: true,
visible: true,
));
update();
await Future.delayed(Duration(milliseconds: stepDelayMs));
}
// مهلة خفيفة بين الطبقات عشان يبان التبديل
await Future.delayed(const Duration(milliseconds: 120));
}
}
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.isNotEmpty) {
// clearPolyline();
} else {
var polyline = Polyline(
polylineId: PolylineId(response["routes"][0]["summary"]),
points: polylineCoordinatesPointsAll[index],
width: 10,
color: Colors.blue,
);
polyLines.add(polyline);
rideConfirm = false;
// isMarkersShown = true;
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, 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;
final int totalMinutes = (durationToRide / 60).floor();
// ====== أدوات مساعدة ======
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, // <-- ⚠️ غيّر هذا للمتغير الصحيح
myDestination.longitude, // <-- ⚠️ غيّر هذا للمتغير الصحيح
);
final bool isInDamascusAirportBoundCtx = _isInsideDamascusAirportBounds(
newMyLocation.latitude, // <-- ⚠️ غيّر هذا للمتغير الصحيح
newMyLocation.longitude, // <-- ⚠️ غيّر هذا للمتغير الصحيح
);
// --- ⬆️ نهاية الإضافة ⬆️ ---
// ====== مسافة مفوترة ======
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 (_) {}
update();
changeBottomSheetShown();
}
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
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
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.deepLinkLatLng, (LatLng? latLng) {
if (latLng != null) {
print('MapPassengerController detected deep link LatLng: $latLng');
// عندما يتم استلام إحداثيات جديدة، عينها كوجهة
myDestination = latLng;
// قم بتشغيل المنطق الخاص بك لعرض المسار
// (تأكد من أن `passengerLocation` لديها قيمة أولاً)
if (passengerLocation != null) {
getDirectionMap(
'${passengerLocation.latitude},${passengerLocation.longitude}',
'${myDestination.latitude},${myDestination.longitude}');
} else {
// يمكنك إظهار رسالة للمستخدم لتمكين الموقع أولاً
print(
'Cannot process deep link route yet, passenger location is null.');
}
// إعادة تعيين القيمة إلى null لمنع التشغيل مرة أخرى عند إعادة بناء الواجهة
_deepLinkController.deepLinkLatLng.value = null;
}
});
}
@override
void onInit() async {
super.onInit();
// // --- إضافة جديدة: تهيئة وحدة التحكم في الروابط العميقة ---
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);
// ابدأ الخريطة الآن (الشاشة ظهرت للمستخدم)
// لاحقاً: استخدم SchedulerBinding.instance.addPostFrameCallback إذا احتجت.
// مرحلة 1: مهام ضرورية للتسعير لكن غير حرجة لظهور UI
unawaited(_stagePricingAndState());
// مرحلة 2: تحسينات/كماليات بالخلفية
unawaited(_stageNiceToHave());
// ابدأ إعادة تحميل الماركر لكن بثروتل داخلي
// startMarkerReloading(); // تأكد أنه مَخنوق التحديث (throttled)
_startMasterTimer();
}
// === Helpers ===
Future<void> _initMinimalIcons() async {
addCustomStartIcon();
addCustomEndIcon();
// أجّل باقي الأيقونات:
// addCustomCarIcon(), addCustomLadyIcon(), addCustomMotoIcon(), addCustomStepIcon()
}
Future<void> _stagePricingAndState() async {
try {
await getKazanPercent(); // أسعار السيرفر
} catch (_) {}
try {
_checkInitialRideStatus(); // تحقق من حالة الرحلة الحالية
} catch (_) {}
// لو عندك ضبط “وضع خفيف” حسب الجهاز:
_applyLowEndModeIfNeeded();
}
Future<void> _stageNiceToHave() async {
// نفّذ بالتوازي
await Future.wait([
Future(() async {
try {
getFavioratePlaces();
} catch (_) {}
}),
Future(() async {
try {
readyWayPoints();
} catch (_) {}
}),
Future(() async {
try {
addCustomPicker();
} catch (_) {}
}),
Future(() async {
try {
addCustomCarIcon();
} catch (_) {}
}),
Future(() async {
try {
addCustomLadyIcon();
} catch (_) {}
}),
Future(() async {
try {
addCustomMotoIcon();
} catch (_) {}
}),
Future(() async {
try {
addCustomStepIcon();
} catch (_) {}
}),
Future(() async {
try {
getPassengerRate();
} catch (_) {}
}),
Future(() async {
try {
firstTimeRunToGetCoupon();
} catch (_) {}
}),
]);
try {
cardNumber = await SecureStorage().readData(BoxName.cardNumber);
} catch (_) {}
}
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');
}
}
}
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,
});
}