25-10-11/1
This commit is contained in:
@@ -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)));
|
||||
|
||||
Reference in New Issue
Block a user