7957 lines
286 KiB
Dart
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,
|
|
});
|
|
}
|