25-10-11/1

This commit is contained in:
Hamza-Ayed
2025-11-06 12:29:17 +03:00
parent 14484fcd8f
commit a69e4c6912
46 changed files with 14145 additions and 13529 deletions

View File

@@ -1,19 +1,23 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart'; // <<<--- إضافة مهمة لاستخدام دالة compute
import 'dart:convert'; // <<<--- إضافة جديدة
import 'package:flutter/foundation.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:http/http.dart' as http; // <<<--- إضافة جديدة
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/env/env.dart';
import 'package:sefer_driver/controller/functions/crud.dart';
// استخدام نفس مسارات الاستيراد التي قدمتها
import '../../../constant/api_key.dart';
import '../../../constant/box_name.dart';
import '../../../constant/country_polygons.dart';
import '../../../constant/links.dart';
import '../../../env/env.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../functions/crud.dart';
// import '../../functions/crud.dart'; // <<<--- تم إلغاء الاعتماد عليه
import '../../functions/tts.dart';
import 'decode_polyline_isolate.dart';
@@ -33,9 +37,8 @@ class NavigationController extends GetxController {
BitmapDescriptor destinationIcon = BitmapDescriptor.defaultMarker;
// --- متغيرات النظام الذكي للتحديث ---
Timer? _locationUpdateTimer; // المؤقت الرئيسي للتحكم في التحديثات
Duration _currentUpdateInterval =
const Duration(seconds: 2); // القيمة الافتراضية
Timer? _locationUpdateTimer;
Duration _currentUpdateInterval = const Duration(seconds: 2);
// --- متغيرات البحث عن الأماكن ---
List<dynamic> placesDestination = [];
@@ -45,16 +48,22 @@ class NavigationController extends GetxController {
LatLng? _finalDestination;
List<Map<String, dynamic>> routeSteps = [];
List<LatLng> _fullRouteCoordinates = [];
List<List<LatLng>> _stepPolylines = []; // لتخزين نقاط كل خطوة على حدة
List<List<LatLng>> _stepPolylines = [];
bool _nextInstructionSpoken = false;
String currentInstruction = "";
String nextInstruction = "";
int currentStepIndex = 0;
// <<<--- [تعديل] ---: متغير جديد لتتبع المسار المقطوع بدقة
int _lastTraveledIndexInFullRoute = 0;
double currentSpeed = 0.0;
String distanceToNextStep = "";
final List<LatLngBounds> _stepBounds = [];
// --- ثوابت الـ API الجديد ---
static const String _routeApiBaseUrl = 'https://routec.intaleq.xyz/';
static final String _routeApiKey = Env.mapKeyOsm;
@override
void onInit() {
super.onInit();
@@ -71,7 +80,7 @@ class NavigationController extends GetxController {
@override
void onClose() {
_locationUpdateTimer?.cancel(); // إيقاف المؤقت عند إغلاق الصفحة
_locationUpdateTimer?.cancel();
mapController?.dispose();
_debounce?.cancel();
placeDestinationController.dispose();
@@ -82,32 +91,100 @@ class NavigationController extends GetxController {
// ١. النظام الذكي لتحديد الموقع والتحديث
// =======================================================================
// 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;
}
// Function to check if the point is inside the polygon
bool isPointInPolygon(LatLng point, List<LatLng> polygon) {
int intersections = 0;
for (int i = 0; i < polygon.length; i++) {
LatLng vertex1 = polygon[i];
LatLng vertex2 =
polygon[(i + 1) % polygon.length]; // Loop back to the start
if (_rayIntersectsSegment(point, vertex1, vertex2)) {
intersections++;
}
}
// If the number of intersections is odd, the point is inside
return intersections % 2 != 0;
}
String getLocationArea(double latitude, double longitude) {
LatLng passengerPoint = LatLng(latitude, longitude);
// 1. فحص الأردن
if (isPointInPolygon(passengerPoint, CountryPolygons.jordanBoundary)) {
box.write(BoxName.countryCode, 'Jordan');
// يمكنك تعيين AppLink.endPoint هنا إذا كان منطقك الداخلي لا يزال يعتمد عليه
// box.write(BoxName.serverChosen,
// AppLink.IntaleqSyriaServer); // مثال: اختر سيرفر سوريا للبيانات
return 'Jordan';
}
// 2. فحص سوريا
if (isPointInPolygon(passengerPoint, CountryPolygons.syriaBoundary)) {
box.write(BoxName.countryCode, 'Syria');
// box.write(BoxName.serverChosen, AppLink.IntaleqSyriaServer);
return 'Syria';
}
// 3. فحص مصر
if (isPointInPolygon(passengerPoint, CountryPolygons.egyptBoundary)) {
box.write(BoxName.countryCode, 'Egypt');
// box.write(BoxName.serverChosen, AppLink.IntaleqAlexandriaServer);
return 'Egypt';
}
// 4. الافتراضي (إذا كان خارج المناطق المخدومة)
box.write(BoxName.countryCode, 'Jordan');
// box.write(BoxName.serverChosen, AppLink.IntaleqSyriaServer);
return 'Unknown Location (Defaulting to Jordan)';
}
Future<void> _getCurrentLocationAndStartUpdates() async {
try {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
myLocation = LatLng(position.latitude, position.longitude);
getLocationArea(myLocation!.latitude, myLocation!.longitude);
update();
animateCameraToPosition(myLocation!);
// بدء التحديثات باستخدام المؤقت بدلاً من الـ Stream
_startLocationTimer();
} catch (e) {
print("Error getting location: $e");
}
}
// --- تم استبدال الـ Stream بمؤقت للتحكم الكامل ---
void _startLocationTimer() {
_locationUpdateTimer?.cancel(); // إلغاء أي مؤقت قديم
_locationUpdateTimer?.cancel();
_locationUpdateTimer = Timer.periodic(_currentUpdateInterval, (timer) {
_updateLocationAndProcess();
});
}
// --- هذه الدالة هي التي تعمل الآن بشكل دوري ---
Future<void> _updateLocationAndProcess() async {
try {
// طلب موقع واحد فقط عند كل مرة يعمل فيها المؤقت
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
myLocation = LatLng(position.latitude, position.longitude);
@@ -131,27 +208,20 @@ class NavigationController extends GetxController {
}
}
// --- الدالة المسؤولة عن تغيير سرعة التحديث ديناميكياً ---
void _adjustUpdateInterval() {
if (currentStepIndex >= routeSteps.length) return;
final currentStepDistance =
routeSteps[currentStepIndex]['distance']['value'];
final currentStepDistance = routeSteps[currentStepIndex]['distance'];
// إذا كانت الخطوة الحالية طويلة (شارع سريع > 1.5 كم)
if (currentStepDistance > 1500) {
_currentUpdateInterval = const Duration(seconds: 4);
}
// إذا كانت الخطوة قصيرة (منعطفات داخل المدينة < 1.5 كم)
else {
} else {
_currentUpdateInterval = const Duration(seconds: 2);
}
// إعادة تشغيل المؤقت بالسرعة الجديدة
_startLocationTimer();
}
// ... باقي دوال إعداد الخريطة ...
void onMapCreated(GoogleMapController controller) {
mapController = controller;
if (myLocation != null) {
@@ -193,11 +263,13 @@ class NavigationController extends GetxController {
currentStepIndex >= routeSteps.length ||
_finalDestination == null) return;
// <<<--- [تعديل] ---: استدعاء الدالة الجديدة والمحسنة
_updateTraveledPolyline(currentPosition);
final step = routeSteps[currentStepIndex];
final endLatLng =
LatLng(step['end_location']['lat'], step['end_location']['lng']);
final distance = Geolocator.distanceBetween(
currentPosition.latitude,
currentPosition.longitude,
@@ -224,15 +296,12 @@ class NavigationController extends GetxController {
void _advanceStep() {
currentStepIndex++;
if (currentStepIndex < routeSteps.length) {
currentInstruction =
_parseInstruction(routeSteps[currentStepIndex]['html_instructions']);
currentInstruction = routeSteps[currentStepIndex]['instruction_text'];
nextInstruction = ((currentStepIndex + 1) < routeSteps.length)
? _parseInstruction(
routeSteps[currentStepIndex + 1]['html_instructions'])
? routeSteps[currentStepIndex + 1]['instruction_text']
: "الوجهة النهائية";
_nextInstructionSpoken = false;
// **هنا يتم تعديل سرعة التحديث عند الانتقال لخطوة جديدة**
_adjustUpdateInterval();
if (currentStepIndex < _stepBounds.length) {
@@ -244,7 +313,7 @@ class NavigationController extends GetxController {
currentInstruction = "لقد وصلت إلى وجهتك.";
nextInstruction = "";
distanceToNextStep = "";
_locationUpdateTimer?.cancel(); // إيقاف التحديثات عند الوصول
_locationUpdateTimer?.cancel();
Get.find<TextToSpeechController>().speakText(currentInstruction);
update();
}
@@ -254,41 +323,51 @@ class NavigationController extends GetxController {
// ٣. تحسين خوارزمية البحث ورسم المسار المقطوع
// =======================================================================
// <<<--- [تعديل] ---: تم إعادة كتابة الدالة بالكامل
void _updateTraveledPolyline(LatLng currentPosition) {
// **التحسين:** البحث فقط في الخطوة الحالية والخطوة التالية
int searchEndIndex = (currentStepIndex + 1 < _stepPolylines.length)
? currentStepIndex + 1
: currentStepIndex;
// 1. التأكد من أن المسار الكامل محمل
if (_fullRouteCoordinates.isEmpty) return;
int overallClosestIndex = -1;
double minDistance = double.infinity;
// 2. ابدأ البحث دائماً من النقطة الأخيرة التي تم الوصول إليها
int newClosestIndex = _lastTraveledIndexInFullRoute;
// البحث في نقاط الخطوة الحالية والتالية فقط
for (int i = currentStepIndex; i <= searchEndIndex; i++) {
for (int j = 0; j < _stepPolylines[i].length; j++) {
final distance = Geolocator.distanceBetween(
currentPosition.latitude,
currentPosition.longitude,
_stepPolylines[i][j].latitude,
_stepPolylines[i][j].longitude);
if (distance < minDistance) {
minDistance = distance;
// نحتاج إلى حساب الفهرس العام في القائمة الكاملة
overallClosestIndex = _getOverallIndex(i, j);
}
// 3. ابحث للأمام فقط (من آخر نقطة مسجلة إلى نهاية المسار)
for (int i = _lastTraveledIndexInFullRoute;
i < _fullRouteCoordinates.length;
i++) {
final point = _fullRouteCoordinates[i];
final distance = Geolocator.distanceBetween(
currentPosition.latitude,
currentPosition.longitude,
point.latitude,
point.longitude,
);
if (distance < minDistance) {
minDistance = distance;
newClosestIndex = i;
} else if (distance > minDistance + 50) {
// 4. تحسين: إذا بدأت المسافة بالزيادة، فتوقف عن البحث
// هذا يعني أننا تجاوزنا أقرب نقطة (50 متر هامش أمان)
break;
}
}
if (overallClosestIndex == -1) return;
// 5. قم بتحديث آخر نقطة مسجلة
_lastTraveledIndexInFullRoute = newClosestIndex;
// 6. ارسم المسار المقطوع (من البداية إلى أقرب نقطة)
List<LatLng> traveledPoints =
_fullRouteCoordinates.sublist(0, overallClosestIndex + 1);
traveledPoints.add(currentPosition);
_fullRouteCoordinates.sublist(0, newClosestIndex + 1);
traveledPoints.add(currentPosition); // أضف الموقع الحالي لنعومة الخط
// 7. ارسم المسار المتبقي (من أقرب نقطة إلى النهاية)
List<LatLng> remainingPoints =
_fullRouteCoordinates.sublist(overallClosestIndex);
remainingPoints.insert(0, currentPosition);
_fullRouteCoordinates.sublist(newClosestIndex);
remainingPoints.insert(0, currentPosition); // ابدأ من الموقع الحالي
// 8. تحديث الخطوط على الخريطة
polylines.removeWhere((p) => p.polylineId.value == 'traveled_route');
polylines.add(Polyline(
polylineId: const PolylineId('traveled_route'),
@@ -306,33 +385,31 @@ class NavigationController extends GetxController {
));
}
// دالة مساعدة لحساب الفهرس العام
int _getOverallIndex(int stepIndex, int pointInStepIndex) {
int overallIndex = 0;
for (int i = 0; i < stepIndex; i++) {
overallIndex += _stepPolylines[i].length;
}
return overallIndex + pointInStepIndex;
}
// <<<--- [إلغاء] ---: لم نعد بحاجة لهذه الدالة المعقدة
// int _getOverallIndex(int stepIndex, int pointInStepIndex) {
// int overallIndex = 0;
// for (int i = 0; i < stepIndex; i++) {
// overallIndex += _stepPolylines[i].length;
// }
// return overallIndex + pointInStepIndex;
// }
// =======================================================================
// ٤. دوال مساعدة وتجهيز البيانات
// =======================================================================
// <<<--- التعديل الأول: تغيير الدالة لتكون async
Future<void> _prepareStepData() async {
_stepBounds.clear();
_stepPolylines.clear();
if (routeSteps.isEmpty) return;
for (final step in routeSteps) {
final pointsString = step['polyline']['points'];
// <<<--- التعديل الثاني: استخدام compute لفك التشفير في خيط منفصل
// وتصحيح طريقة التعامل مع القائمة المُرجعة
final pointsString = step['geometry'];
final List<LatLng> polylineCoordinates = await compute(
decodePolylineIsolate as ComputeCallback<dynamic, List<LatLng>>,
pointsString);
_stepPolylines.add(polylineCoordinates); // تخزين نقاط الخطوة
_stepPolylines.add(polylineCoordinates);
_stepBounds.add(_boundsFromLatLngList(polylineCoordinates));
}
}
@@ -400,73 +477,97 @@ class NavigationController extends GetxController {
}
}
// --- (تعديل) ---: تم تعديل الدالة لاستخدام http.get
Future<void> getRoute(LatLng origin, LatLng destination) async {
final String key = Env.mapAPIKEY;
final url =
'${AppLink.googleMapsLink}directions/json?language=ar&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${key}&mode=driving';
var response = await CRUD().getGoogleApi(link: url, payload: {});
// Log.print('response: ${response}');
'$_routeApiBaseUrl/route?origin=${origin.latitude},${origin.longitude}&destination=${destination.latitude},${destination.longitude}&steps=true&overview=full';
if (response == null || response['routes'].isEmpty) {
Get.snackbar('خطأ', 'لم يتم العثور على مسار.');
return;
try {
final response = await http.get(
Uri.parse(url),
headers: {'X-API-KEY': _routeApiKey},
);
if (response.statusCode != 200) {
print("Error from route API: ${response.statusCode}");
Get.snackbar('خطأ', 'لم يتم العثور على مسار.');
return;
}
final responseData = jsonDecode(response.body);
if (responseData == null || responseData['status'] != 'ok') {
Get.snackbar('خطأ', 'لم يتم العثور على مسار.');
return;
}
polylines.clear();
final pointsString = responseData['polyline'];
_fullRouteCoordinates = await compute(
decodePolylineIsolate as ComputeCallback<dynamic, List<LatLng>>,
pointsString);
// <<<--- [تعديل] ---: تصفير الـ index عند بدء مسار جديد
_lastTraveledIndexInFullRoute = 0;
polylines.add(
Polyline(
polylineId: const PolylineId('remaining_route'),
points: _fullRouteCoordinates,
color: const Color(0xFF4A80F0),
width: 7,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
),
);
polylines.add(
const Polyline(
polylineId: PolylineId('traveled_route'),
points: [],
color: Colors.grey,
width: 7,
),
);
routeSteps = List<Map<String, dynamic>>.from(responseData['steps']);
await _prepareStepData();
for (int i = 0; i < routeSteps.length; i++) {
var step = routeSteps[i];
if (i < _stepPolylines.length && _stepPolylines[i].isNotEmpty) {
LatLng endLocation = _stepPolylines[i].last;
step['end_location'] = {
'lat': endLocation.latitude,
'lng': endLocation.longitude
};
} else {
var loc = step['maneuver']['location']; // [lng, lat]
step['end_location'] = {'lat': loc[1], 'lng': loc[0]};
}
step['instruction_text'] = _createInstructionFromManeuver(step);
}
currentStepIndex = 0;
_nextInstructionSpoken = false;
if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['instruction_text'];
nextInstruction = (routeSteps.length > 1)
? routeSteps[1]['instruction_text']
: "الوجهة النهائية";
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
_adjustUpdateInterval();
if (_fullRouteCoordinates.isNotEmpty) {
final bounds = _boundsFromLatLngList(_fullRouteCoordinates);
mapController
?.animateCamera(CameraUpdate.newLatLngBounds(bounds, 100.0));
}
} catch (e) {
print("Exception in getRoute: $e");
Get.snackbar('خطأ', 'حدث خطأ في الشبكة.');
}
polylines.clear();
final pointsString = response['routes'][0]['overview_polyline']['points'];
// <<<--- التعديل الثالث: استخدام compute هنا أيضًا للمسار الرئيسي
_fullRouteCoordinates = await compute(
decodePolylineIsolate as ComputeCallback<dynamic, List<LatLng>>,
pointsString);
polylines.add(
Polyline(
polylineId: const PolylineId('remaining_route'),
points: _fullRouteCoordinates,
color: const Color(0xFF4A80F0),
width: 7,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
),
);
polylines.add(
const Polyline(
polylineId: PolylineId('traveled_route'),
points: [],
color: Colors.grey,
width: 7,
),
);
routeSteps = List<Map<String, dynamic>>.from(
response['routes'][0]['legs'][0]['steps']);
// <<<--- التعديل الرابع: انتظار انتهاء الدالة بعد تحويلها إلى async
await _prepareStepData();
currentStepIndex = 0;
_nextInstructionSpoken = false;
if (routeSteps.isNotEmpty) {
currentInstruction =
_parseInstruction(routeSteps[0]['html_instructions']);
nextInstruction = (routeSteps.length > 1)
? _parseInstruction(routeSteps[1]['html_instructions'])
: "الوجهة النهائية";
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
_adjustUpdateInterval(); // تحديد سرعة التحديث لأول مرة
final boundsData = response['routes'][0]['bounds'];
mapController?.animateCamera(CameraUpdate.newLatLngBounds(
LatLngBounds(
northeast: LatLng(
boundsData['northeast']['lat'], boundsData['northeast']['lng']),
southwest: LatLng(
boundsData['southwest']['lat'], boundsData['southwest']['lng']),
),
100.0,
));
}
Future<void> recalculateRoute() async {
@@ -501,7 +602,10 @@ class NavigationController extends GetxController {
_fullRouteCoordinates.clear();
_stepPolylines.clear();
_nextInstructionSpoken = false;
_locationUpdateTimer?.cancel(); // إيقاف التحديثات عند إلغاء المسار
_locationUpdateTimer?.cancel();
// <<<--- [تعديل] ---: تصفير الـ index عند إلغاء المسار
_lastTraveledIndexInFullRoute = 0;
update();
}
@@ -530,14 +634,77 @@ class NavigationController extends GetxController {
const ImageConfiguration(size: Size(25, 25)), 'assets/images/b.png');
}
String _parseInstruction(String html) =>
html.replaceAll(RegExp(r'<[^>]*>'), ' ');
String _createInstructionFromManeuver(Map<String, dynamic> step) {
final maneuver = step['maneuver'];
final type = maneuver['type'] ?? 'continue';
final modifier = maneuver['modifier'] ?? 'straight';
final name = step['name'] ?? '';
String instruction = "";
switch (type) {
case 'depart':
instruction = "انطلق";
break;
case 'arrive':
instruction = "لقد وصلت إلى وجهتك";
if (name.isNotEmpty) instruction += "، $name";
return instruction;
case 'turn':
case 'fork':
case 'off ramp':
case 'on ramp':
case 'roundabout':
instruction = _getTurnInstruction(modifier);
break;
case 'continue':
instruction = "استمر";
break;
default:
instruction = "اتجه";
}
if (name.isNotEmpty) {
if (instruction == "استمر") {
instruction += " على $name";
} else {
instruction += " إلى $name";
}
} else if (type == 'continue' && modifier == 'straight') {
instruction = "استمر بشكل مستقيم";
}
return instruction;
}
String _getTurnInstruction(String modifier) {
switch (modifier) {
case 'uturn':
return "قم بالاستدارة والعودة";
case 'sharp right':
return "انعطف يمينًا بحدة";
case 'right':
return "انعطف يمينًا";
case 'slight right':
return "انعطف يمينًا قليلاً";
case 'straight':
return "استمر بشكل مستقيم";
case 'slight left':
return "انعطف يسارًا قليلاً";
case 'left':
return "انعطف يسارًا";
case 'sharp left':
return "انعطف يسارًا بحدة";
default:
return "اتجه";
}
}
// =======================================================================
// ٥. دالة البحث عن الأماكن المحدثة والدوال المساعدة لها
// =======================================================================
/// الدالة المحدثة للبحث عن الأماكن
// --- (تعديل) ---: تم تعديل الدالة لاستخدام http.post
Future<void> getPlaces() async {
final q = placeDestinationController.text.trim();
if (q.isEmpty || q.length < 3) {
@@ -546,7 +713,6 @@ class NavigationController extends GetxController {
return;
}
// التأكد من أن الموقع الحالي ليس null
if (myLocation == null) {
print('myLocation is null, cannot search for places.');
return;
@@ -554,58 +720,53 @@ class NavigationController extends GetxController {
final lat = myLocation!.latitude;
final lng = myLocation!.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
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(),
},
);
// تجهيز البيانات لإرسالها كـ JSON
final payload = {
'query': q,
'lat_min': latMin.toString(),
'lat_max': latMax.toString(),
'lng_min': lngMin.toString(),
'lng_max': lngMax.toString(),
};
try {
final response =
await CRUD().post(link: AppLink.getPlacesSyria, payload: payload);
// إرسال البيانات كـ JSON
final responseData = (response);
// معالجة الاستجابة من السيرفر بشكل يوافق {"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']}');
if (responseData is Map) {
if (responseData['status'] == 'success' &&
responseData['message'] is List) {
list = List.from(responseData['message'] as List);
} else if (responseData['status'] == 'failure') {
print('Server Error: ${responseData['message']}');
return;
} else {
print('Unexpected Map shape from server');
return;
}
} else if (response is List) {
// للتعامل مع الحالات التي قد يرجع فيها السيرفر قائمة مباشرة
list = List.from(response);
} else if (responseData is List) {
list = List.from(responseData);
} 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 =
@@ -613,8 +774,6 @@ class NavigationController extends GetxController {
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;
@@ -622,7 +781,6 @@ class NavigationController extends GetxController {
p['score'] = score;
}
// ترتيب القائمة النهائية حسب النقاط (الأعلى أولاً)
list.sort((a, b) {
final sa = (a['score'] ?? 0.0) as double;
final sb = (b['score'] ?? 0.0) as double;
@@ -646,9 +804,8 @@ class NavigationController extends GetxController {
// --== دوال مساعدة (محدثة) ==--
// -----------------------------------------------------------------
/// تحسب المسافة بين نقطتين بالكيلومتر (معادلة هافرساين)
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
const R = 6371.0; // نصف قطر الأرض بالكيلومتر
const R = 6371.0;
final dLat = (lat2 - lat1) * (pi / 180.0);
final dLon = (lon2 - lon1) * (pi / 180.0);
final rLat1 = lat1 * (pi / 180.0);
@@ -660,23 +817,20 @@ class NavigationController extends GetxController {
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; // تحتوي على الكلمة
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)));

View File

@@ -1,19 +1,22 @@
// lib/views/navigation_view.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'dart:ui';
import 'dart:ui'; // For BackdropFilter
import 'navigation_controller.dart'; // For BackdropFilter
import 'navigation_controller.dart';
// استخدام نفس مسار الاستيراد الذي قدمته
// ملاحظة: افترضتُ أن لديك لوناً أساسياً في هذا الملف
// import 'package:sefer_driver/constant/colors.dart';
// سأستخدم اللون الأزرق كبديل مؤقت
const Color kPrimaryColor = Color(0xFF0D47A1);
class NavigationView extends StatelessWidget {
const NavigationView({super.key});
@override
Widget build(BuildContext context) {
// استخدام Get.find() بدلاً من Get.put() لضمان أن الكونترولر مُهيأ مسبقاً
// إذا كانت هذه هي نقطة الدخول، Get.put() صحيح.
final NavigationController controller = Get.put(NavigationController());
return Scaffold(
@@ -23,7 +26,6 @@ class NavigationView extends StatelessWidget {
// --- الخريطة ---
GoogleMap(
onMapCreated: controller.onMapCreated,
// --- السطر المضاف والمهم هنا ---
onLongPress: controller.onMapLongPressed,
initialCameraPosition: CameraPosition(
target: controller.myLocation ??
@@ -37,27 +39,33 @@ class NavigationView extends StatelessWidget {
compassEnabled: false,
zoomControlsEnabled: false,
// تعديل الـ padding لإعطاء مساحة للعناصر العلوية والسفلية
// مساحة أكبر في الأعلى للبحث + النتائج، ومساحة أكبر بالأسفل للملاحة
padding: EdgeInsets.only(
bottom: controller.currentInstruction.isNotEmpty ? 130 : 0,
top: 140),
bottom: controller.currentInstruction.isNotEmpty ? 170 : 0,
top: 150,
),
),
// --- واجهة البحث ونتائجه ---
_buildSearchUI(controller),
// --- واجهة البحث (تصميم زجاجي) ---
_buildGlassSearchUI(controller),
// --- إرشادات الملاحة المطورة ---
// --- إرشادات الملاحة (تصميم عائم) ---
if (controller.currentInstruction.isNotEmpty)
_buildNavigationInstruction(controller),
_buildFloatingNavigationUI(controller),
// --- أزرار التحكم على الخريطة ---
_buildMapControls(controller),
// --- أزرار التحكم (تصميم عائم) ---
_buildFloatingMapControls(controller),
// --- مؤشر التحميل ---
if (controller.isLoading)
Container(
color: Colors.black.withOpacity(0.5),
child: const Center(
child: CircularProgressIndicator(color: Colors.white)),
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 3,
),
),
),
],
),
@@ -65,94 +73,75 @@ class NavigationView extends StatelessWidget {
);
}
// --- ويدجت خاصة بواجهة البحث ---
Widget _buildSearchUI(NavigationController controller) {
/// --- 1. واجهة البحث بالتصميم الزجاجي المطور ---
Widget _buildGlassSearchUI(NavigationController controller) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12.0),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Column(
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 10,
offset: const Offset(0, 5),
// --- شريط البحث ---
ClipRRect(
borderRadius: BorderRadius.circular(28.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Container(
height: 56,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.85),
borderRadius: BorderRadius.circular(28.0),
border: Border.all(color: Colors.white.withOpacity(0.4)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
const Padding(
padding: EdgeInsets.only(left: 18.0, right: 10.0),
child: Icon(Icons.search,
color: kPrimaryColor, size: 24),
),
Expanded(
child: TextField(
controller: controller.placeDestinationController,
onChanged: controller.onSearchChanged,
textInputAction: TextInputAction.search,
style: const TextStyle(
fontSize: 16, color: Colors.black87),
decoration: InputDecoration(
hintText: 'إلى أين تريد الذهاب؟',
hintStyle: const TextStyle(
color: Colors.black45, fontSize: 16),
border: InputBorder.none,
contentPadding: const EdgeInsets.only(bottom: 2),
),
),
),
// زر المسح أو إلغاء المسار
if (controller
.placeDestinationController.text.isNotEmpty)
_buildClearButton(controller)
else if (controller.polylines.isNotEmpty)
_buildCancelRouteButton(controller),
],
),
],
),
child: TextField(
controller: controller.placeDestinationController,
onChanged: (val) {
controller.onSearchChanged(val);
},
decoration: InputDecoration(
hintText: 'إلى أين تريد الذهاب؟',
prefixIcon: const Icon(Icons.search, color: Colors.grey),
suffixIcon: controller
.placeDestinationController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, color: Colors.grey),
onPressed: () {
controller.placeDestinationController.clear();
controller.placesDestination = [];
controller.update();
},
)
: (controller.polylines.isNotEmpty
? IconButton(
icon:
const Icon(Icons.close, color: Colors.red),
tooltip: 'إلغاء المسار',
onPressed: () => controller.clearRoute(),
)
: null),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
),
),
),
const SizedBox(height: 8),
const SizedBox(height: 10),
// --- قائمة النتائج ---
if (controller.placesDestination.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
constraints: const BoxConstraints(maxHeight: 220),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.85),
borderRadius: BorderRadius.circular(15.0),
),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: controller.placesDestination.length,
itemBuilder: (context, index) {
final place = controller.placesDestination[index];
return ListTile(
title: Text(place['name'] ?? 'اسم غير معروف',
style: const TextStyle(
fontWeight: FontWeight.bold)),
subtitle: Text(place['address'] ?? '',
maxLines: 1, overflow: TextOverflow.ellipsis),
leading: const Icon(Icons.location_on_outlined,
color: Colors.blue),
onTap: () => controller.selectDestination(place),
);
},
),
),
),
),
_buildSearchResultsList(controller),
],
),
),
@@ -160,28 +149,136 @@ class NavigationView extends StatelessWidget {
);
}
// --- ويدجت خاصة بأزرار التحكم ---
Widget _buildMapControls(NavigationController controller) {
Widget _buildClearButton(NavigationController controller) {
return IconButton(
icon: const Icon(Icons.clear, color: Colors.grey, size: 22),
onPressed: () {
controller.placeDestinationController.clear();
controller.placesDestination = [];
controller.update();
},
);
}
Widget _buildCancelRouteButton(NavigationController controller) {
return IconButton(
tooltip: 'إلغاء المسار',
icon: const Icon(Icons.close, color: Colors.redAccent, size: 22),
onPressed: () => controller.clearRoute(),
);
}
Widget _buildSearchResultsList(NavigationController controller) {
return ClipRRect(
borderRadius: BorderRadius.circular(24.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Container(
constraints: const BoxConstraints(maxHeight: 220),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.85),
borderRadius: BorderRadius.circular(24.0),
border: Border.all(color: Colors.white.withOpacity(0.4)),
),
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8.0),
itemCount: controller.placesDestination.length,
itemBuilder: (context, index) {
final place = controller.placesDestination[index];
final distance = place['distanceKm'] as double?;
final address = (place['address'] ?? '').toString();
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => controller.selectDestination(place),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 12.0),
child: Row(
children: [
// أيقونة الموقع
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: kPrimaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.location_on_outlined,
color: kPrimaryColor, size: 20),
),
const SizedBox(width: 14),
// الاسم والعنوان
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
place['name'] ?? 'اسم غير معروف',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.black87),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (address.isNotEmpty)
Text(
address,
style: const TextStyle(
color: Colors.black54, fontSize: 13),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 10),
// المسافة
if (distance != null)
Text(
'${distance.toStringAsFixed(1)} كم',
style: const TextStyle(
color: kPrimaryColor,
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
],
),
),
),
);
},
),
),
),
);
}
/// --- 2. أزرار التحكم بالتصميم العائم ---
Widget _buildFloatingMapControls(NavigationController controller) {
return Positioned(
bottom: controller.currentInstruction.isNotEmpty ? 150 : 20,
right: 12,
// اجعلها تطفو فوق لوحة الملاحة
bottom: controller.currentInstruction.isNotEmpty ? 190 : 24,
right: 16,
child: Column(
children: [
if (controller.polylines.isNotEmpty) ...[
FloatingActionButton(
heroTag: 'rerouteBtn',
mini: true,
backgroundColor: Colors.white,
tooltip: 'إعادة حساب المسار',
elevation: 6,
onPressed: () => controller.recalculateRoute(),
child: const Icon(Icons.sync_alt, color: Colors.blue),
tooltip: 'إعادة حساب المسار',
child: const Icon(Icons.sync_alt, color: kPrimaryColor, size: 24),
),
const SizedBox(height: 10),
const SizedBox(height: 12),
],
FloatingActionButton(
heroTag: 'gpsBtn',
mini: true,
backgroundColor: Colors.white,
elevation: 6,
onPressed: () {
if (controller.myLocation != null) {
controller.animateCameraToPosition(
@@ -191,102 +288,134 @@ class NavigationView extends StatelessWidget {
);
}
},
child: const Icon(Icons.gps_fixed, color: Colors.black54),
child: const Icon(Icons.gps_fixed, color: Colors.black54, size: 24),
),
],
),
);
}
// --- ويدجت خاصة بإرشادات الطريق المطورة ---
Widget _buildNavigationInstruction(NavigationController controller) {
/// --- 3. واجهة الملاحة بالتصميم العائم المطور ---
Widget _buildFloatingNavigationUI(NavigationController controller) {
return Positioned(
bottom: 0,
left: 0,
right: 0,
bottom: 16,
left: 16,
right: 16,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade900, Colors.blue.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
gradient: const LinearGradient(
colors: [Color(0xFF1E88E5), Color(0xFF0D47A1)], // أزرق متدرج
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 15,
offset: const Offset(0, -5),
color: Colors.black.withOpacity(0.3),
blurRadius: 25,
offset: const Offset(0, 10),
),
],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 20),
padding: const EdgeInsets.fromLTRB(22, 20, 22, 22),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// --- الصف العلوي: السرعة والمسافة ---
// --- الصف العلوي: الإرشاد والمسافة ---
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// الأيقونة
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(Icons.navigation_rounded,
color: Colors.white, size: 28),
),
const SizedBox(width: 16),
// الإرشاد
Expanded(
child: Text(
controller.currentInstruction,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
height: 1.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 16),
// المسافة
Text(
controller.distanceToNextStep,
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold),
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
],
),
// --- فاصل ---
if (controller.nextInstruction.isNotEmpty ||
controller.currentSpeed > 0)
const Padding(
padding: EdgeInsets.symmetric(vertical: 14.0),
child: Divider(color: Colors.white30, height: 1),
),
// --- الصف السفلي: الإرشاد التالي والسرعة ---
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// الإرشاد التالي
Expanded(
child: controller.nextInstruction.isNotEmpty
? Text(
'التالي: ${controller.nextInstruction}',
style: const TextStyle(
color: Colors.white70,
fontSize: 15,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: const SizedBox(), // يترك مساحة فارغة إذا لم يكن هناك إرشاد تالي
),
// السرعة
Row(
children: [
Text(
controller.currentSpeed.toStringAsFixed(0),
style: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold),
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
const SizedBox(width: 6),
const Text(
"كم/س",
style: TextStyle(color: Colors.white70, fontSize: 14),
'كم/س',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
const Divider(color: Colors.white38, height: 20, thickness: 0.8),
// --- الصف السفلي: الإرشاد القادم ---
Row(
children: [
const Icon(Icons.navigation_rounded,
color: Colors.white, size: 32),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("الخطوة التالية",
style:
TextStyle(color: Colors.white70, fontSize: 12)),
Text(
controller.nextInstruction.isNotEmpty
? controller.nextInstruction
: controller.currentInstruction,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
],
),
),

View File

@@ -0,0 +1,145 @@
// lib/controllers/navigation/route_matcher_worker.dart
import 'dart:async';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'dart:math';
/// Worker entrypoint (spawnUri/spawn).
/// Messages:
/// - init: {'type':'init','coords': Float64List}
/// - match: {'type':'match','id': int, 'lat': double, 'lng': double, 'lastIndex': int, 'window': int}
/// - dispose: {'type':'dispose'}
///
/// Responses are sent back as Map via SendPort:
/// - {'type':'ready'}
/// - {'type':'matchResult','id': id, 'index': overallIndex, 'lat': lat, 'lng': lng, 'dist': meters}
void routeMatcherIsolateEntry(SendPort sendPort) {
final ReceivePort port = ReceivePort();
sendPort.send({'type': 'ready', 'port': port.sendPort});
Float64List? flat; // [lat,lng,lat,lng,...]
int nPoints = 0;
port.listen((dynamic message) {
try {
if (message is Map<String, dynamic>) {
final type = message['type'] as String? ?? '';
if (type == 'init') {
final data = message['coords'] as Float64List?;
if (data != null) {
flat = data;
nPoints = flat!.length ~/ 2;
sendPort.send({'type': 'inited', 'points': nPoints});
} else {
sendPort.send({'type': 'error', 'message': 'init missing coords'});
}
} else if (type == 'match') {
if (flat == null) {
sendPort.send({'type': 'error', 'message': 'not inited'});
return;
}
final int id = message['id'] as int;
final double lat = (message['lat'] as num).toDouble();
final double lng = (message['lng'] as num).toDouble();
final int lastIndex = (message['lastIndex'] as int?) ?? 0;
final int window = (message['window'] as int?) ?? 120;
final result =
_findClosestWindowInternal(flat!, lat, lng, lastIndex, window);
sendPort.send({
'type': 'matchResult',
'id': id,
'index': result['index'],
'lat': result['lat'],
'lng': result['lng'],
'dist': result['dist']
});
} else if (type == 'dispose') {
port.close();
sendPort.send({'type': 'disposed'});
} else {
sendPort.send({'type': 'error', 'message': 'unknown message type'});
}
}
} catch (e, st) {
sendPort.send(
{'type': 'error', 'message': e.toString(), 'stack': st.toString()});
}
});
}
/// Internal helper: projection on segments, windowed search.
/// Returns Map {index, lat, lng, dist}
Map<String, dynamic> _findClosestWindowInternal(
Float64List flat, double lat, double lng, int lastIndex, int window) {
final int n = flat.length ~/ 2;
final int start = max(0, lastIndex - window);
final int end = min(n - 1, lastIndex + window);
double minDist = double.infinity;
int bestIdx = lastIndex;
double bestLat = flat[lastIndex * 2];
double bestLng = flat[lastIndex * 2 + 1];
for (int i = start; i < end; i++) {
final double aLat = flat[i * 2];
final double aLng = flat[i * 2 + 1];
final double bLat = flat[(i + 1) * 2];
final double bLng = flat[(i + 1) * 2 + 1];
final proj = _closestPointOnSegmentLatLng(lat, lng, aLat, aLng, bLat, bLng);
final double d = proj['dist'] as double;
if (d < minDist) {
minDist = d;
bestLat = proj['lat'] as double;
bestLng = proj['lng'] as double;
// choose overall index: i or i+1 depending on t
final double t = proj['t'] as double;
bestIdx = i + (t > 0.5 ? 1 : 0);
}
}
return {'index': bestIdx, 'lat': bestLat, 'lng': bestLng, 'dist': minDist};
}
/// Projection math on geodetic points approximated in degrees (good for short distances).
Map<String, dynamic> _closestPointOnSegmentLatLng(
double px, double py, double ax, double ay, double bx, double by) {
// Here px=lat, py=lng; ax=lat, ay=lng, etc.
final double x0 = px;
final double y0 = py;
final double x1 = ax;
final double y1 = ay;
final double x2 = bx;
final double y2 = by;
final double dx = x2 - x1;
final double dy = y2 - y1;
double t = 0.0;
final double len2 = dx * dx + dy * dy;
if (len2 > 0) {
t = ((x0 - x1) * dx + (y0 - y1) * dy) / len2;
if (t < 0) t = 0;
if (t > 1) t = 1;
}
final double projX = x1 + t * dx;
final double projY = y1 + t * dy;
final double distMeters = _haversineDistanceMeters(x0, y0, projX, projY);
return {'lat': projX, 'lng': projY, 't': t, 'dist': distMeters};
}
/// Haversine distance (meters)
double _haversineDistanceMeters(
double lat1, double lng1, double lat2, double lng2) {
final double R = 6371000.0;
final double dLat = _deg2rad(lat2 - lat1);
final double dLon = _deg2rad(lng2 - lng1);
final double a = sin(dLat / 2) * sin(dLat / 2) +
cos(_deg2rad(lat1)) * cos(_deg2rad(lat2)) * sin(dLon / 2) * sin(dLon / 2);
final double c = 2 * atan2(sqrt(a), sqrt(1 - a));
return R * c;
}
double _deg2rad(double deg) => deg * pi / 180.0;