685 lines
24 KiB
Dart
685 lines
24 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'dart:math';
|
||
import 'package:flutter/foundation.dart'; // <<<--- إضافة مهمة لاستخدام دالة compute
|
||
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:sefer_driver/constant/colors.dart';
|
||
import 'package:sefer_driver/env/env.dart';
|
||
|
||
// استخدام نفس مسارات الاستيراد التي قدمتها
|
||
import '../../../constant/api_key.dart';
|
||
import '../../../constant/links.dart';
|
||
import '../../../print.dart';
|
||
import '../../functions/crud.dart';
|
||
import '../../functions/tts.dart';
|
||
import 'decode_polyline_isolate.dart';
|
||
|
||
class NavigationController extends GetxController {
|
||
// --- متغيرات الحالة العامة ---
|
||
bool isLoading = false;
|
||
GoogleMapController? mapController;
|
||
final TextEditingController placeDestinationController =
|
||
TextEditingController();
|
||
|
||
// --- متغيرات الخريطة والموقع ---
|
||
LatLng? myLocation;
|
||
double heading = 0.0;
|
||
final Set<Marker> markers = {};
|
||
final Set<Polyline> polylines = {};
|
||
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
||
BitmapDescriptor destinationIcon = BitmapDescriptor.defaultMarker;
|
||
|
||
// --- متغيرات النظام الذكي للتحديث ---
|
||
Timer? _locationUpdateTimer; // المؤقت الرئيسي للتحكم في التحديثات
|
||
Duration _currentUpdateInterval =
|
||
const Duration(seconds: 2); // القيمة الافتراضية
|
||
|
||
// --- متغيرات البحث عن الأماكن ---
|
||
List<dynamic> placesDestination = [];
|
||
Timer? _debounce;
|
||
|
||
// --- متغيرات الملاحة (Navigation) ---
|
||
LatLng? _finalDestination;
|
||
List<Map<String, dynamic>> routeSteps = [];
|
||
List<LatLng> _fullRouteCoordinates = [];
|
||
List<List<LatLng>> _stepPolylines = []; // لتخزين نقاط كل خطوة على حدة
|
||
bool _nextInstructionSpoken = false;
|
||
|
||
String currentInstruction = "";
|
||
String nextInstruction = "";
|
||
int currentStepIndex = 0;
|
||
double currentSpeed = 0.0;
|
||
String distanceToNextStep = "";
|
||
final List<LatLngBounds> _stepBounds = [];
|
||
|
||
@override
|
||
void onInit() {
|
||
super.onInit();
|
||
_initialize();
|
||
}
|
||
|
||
Future<void> _initialize() async {
|
||
await _loadCustomIcons();
|
||
await _getCurrentLocationAndStartUpdates();
|
||
if (!Get.isRegistered<TextToSpeechController>()) {
|
||
Get.put(TextToSpeechController());
|
||
}
|
||
}
|
||
|
||
@override
|
||
void onClose() {
|
||
_locationUpdateTimer?.cancel(); // إيقاف المؤقت عند إغلاق الصفحة
|
||
mapController?.dispose();
|
||
_debounce?.cancel();
|
||
placeDestinationController.dispose();
|
||
super.onClose();
|
||
}
|
||
|
||
// =======================================================================
|
||
// ١. النظام الذكي لتحديد الموقع والتحديث
|
||
// =======================================================================
|
||
|
||
Future<void> _getCurrentLocationAndStartUpdates() async {
|
||
try {
|
||
Position position = await Geolocator.getCurrentPosition(
|
||
desiredAccuracy: LocationAccuracy.high);
|
||
myLocation = LatLng(position.latitude, position.longitude);
|
||
update();
|
||
animateCameraToPosition(myLocation!);
|
||
// بدء التحديثات باستخدام المؤقت بدلاً من الـ Stream
|
||
_startLocationTimer();
|
||
} catch (e) {
|
||
print("Error getting location: $e");
|
||
}
|
||
}
|
||
|
||
// --- تم استبدال الـ Stream بمؤقت للتحكم الكامل ---
|
||
void _startLocationTimer() {
|
||
_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);
|
||
heading = position.heading;
|
||
currentSpeed = position.speed * 3.6;
|
||
|
||
_updateCarMarker();
|
||
|
||
if (polylines.isNotEmpty && myLocation != null) {
|
||
animateCameraToPosition(
|
||
myLocation!,
|
||
bearing: heading,
|
||
zoom: 18.5,
|
||
);
|
||
|
||
_checkNavigationStep(myLocation!);
|
||
}
|
||
update();
|
||
} catch (e) {
|
||
print("Error in _updateLocationAndProcess: $e");
|
||
}
|
||
}
|
||
|
||
// --- الدالة المسؤولة عن تغيير سرعة التحديث ديناميكياً ---
|
||
void _adjustUpdateInterval() {
|
||
if (currentStepIndex >= routeSteps.length) return;
|
||
|
||
final currentStepDistance =
|
||
routeSteps[currentStepIndex]['distance']['value'];
|
||
|
||
// إذا كانت الخطوة الحالية طويلة (شارع سريع > 1.5 كم)
|
||
if (currentStepDistance > 1500) {
|
||
_currentUpdateInterval = const Duration(seconds: 4);
|
||
}
|
||
// إذا كانت الخطوة قصيرة (منعطفات داخل المدينة < 1.5 كم)
|
||
else {
|
||
_currentUpdateInterval = const Duration(seconds: 2);
|
||
}
|
||
|
||
// إعادة تشغيل المؤقت بالسرعة الجديدة
|
||
_startLocationTimer();
|
||
}
|
||
|
||
// ... باقي دوال إعداد الخريطة ...
|
||
void onMapCreated(GoogleMapController controller) {
|
||
mapController = controller;
|
||
if (myLocation != null) {
|
||
animateCameraToPosition(myLocation!);
|
||
}
|
||
}
|
||
|
||
void _updateCarMarker() {
|
||
if (myLocation == null) return;
|
||
markers.removeWhere((m) => m.markerId.value == 'myLocation');
|
||
markers.add(
|
||
Marker(
|
||
markerId: const MarkerId('myLocation'),
|
||
position: myLocation!,
|
||
icon: carIcon,
|
||
rotation: heading,
|
||
anchor: const Offset(0.5, 0.5),
|
||
flat: true,
|
||
),
|
||
);
|
||
}
|
||
|
||
void animateCameraToPosition(LatLng position,
|
||
{double zoom = 16.0, double bearing = 0.0}) {
|
||
mapController?.animateCamera(
|
||
CameraUpdate.newCameraPosition(
|
||
CameraPosition(
|
||
target: position, zoom: zoom, bearing: bearing, tilt: 45.0),
|
||
),
|
||
);
|
||
}
|
||
|
||
// =======================================================================
|
||
// ٢. الملاحة والتحقق من الخطوات
|
||
// =======================================================================
|
||
|
||
void _checkNavigationStep(LatLng currentPosition) {
|
||
if (routeSteps.isEmpty ||
|
||
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,
|
||
endLatLng.latitude,
|
||
endLatLng.longitude,
|
||
);
|
||
|
||
distanceToNextStep = (distance > 1000)
|
||
? "${(distance / 1000).toStringAsFixed(1)} كم"
|
||
: "${distance.toStringAsFixed(0)} متر";
|
||
|
||
if (distance < 30 &&
|
||
!_nextInstructionSpoken &&
|
||
nextInstruction.isNotEmpty) {
|
||
Get.find<TextToSpeechController>().speakText(nextInstruction);
|
||
_nextInstructionSpoken = true;
|
||
}
|
||
|
||
if (distance < 20) {
|
||
_advanceStep();
|
||
}
|
||
}
|
||
|
||
void _advanceStep() {
|
||
currentStepIndex++;
|
||
if (currentStepIndex < routeSteps.length) {
|
||
currentInstruction =
|
||
_parseInstruction(routeSteps[currentStepIndex]['html_instructions']);
|
||
nextInstruction = ((currentStepIndex + 1) < routeSteps.length)
|
||
? _parseInstruction(
|
||
routeSteps[currentStepIndex + 1]['html_instructions'])
|
||
: "الوجهة النهائية";
|
||
_nextInstructionSpoken = false;
|
||
|
||
// **هنا يتم تعديل سرعة التحديث عند الانتقال لخطوة جديدة**
|
||
_adjustUpdateInterval();
|
||
|
||
if (currentStepIndex < _stepBounds.length) {
|
||
mapController?.animateCamera(
|
||
CameraUpdate.newLatLngBounds(_stepBounds[currentStepIndex], 70.0));
|
||
}
|
||
update();
|
||
} else {
|
||
currentInstruction = "لقد وصلت إلى وجهتك.";
|
||
nextInstruction = "";
|
||
distanceToNextStep = "";
|
||
_locationUpdateTimer?.cancel(); // إيقاف التحديثات عند الوصول
|
||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||
update();
|
||
}
|
||
}
|
||
|
||
// =======================================================================
|
||
// ٣. تحسين خوارزمية البحث ورسم المسار المقطوع
|
||
// =======================================================================
|
||
|
||
void _updateTraveledPolyline(LatLng currentPosition) {
|
||
// **التحسين:** البحث فقط في الخطوة الحالية والخطوة التالية
|
||
int searchEndIndex = (currentStepIndex + 1 < _stepPolylines.length)
|
||
? currentStepIndex + 1
|
||
: currentStepIndex;
|
||
|
||
int overallClosestIndex = -1;
|
||
double minDistance = double.infinity;
|
||
|
||
// البحث في نقاط الخطوة الحالية والتالية فقط
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (overallClosestIndex == -1) return;
|
||
|
||
List<LatLng> traveledPoints =
|
||
_fullRouteCoordinates.sublist(0, overallClosestIndex + 1);
|
||
traveledPoints.add(currentPosition);
|
||
|
||
List<LatLng> remainingPoints =
|
||
_fullRouteCoordinates.sublist(overallClosestIndex);
|
||
remainingPoints.insert(0, currentPosition);
|
||
|
||
polylines.removeWhere((p) => p.polylineId.value == 'traveled_route');
|
||
polylines.add(Polyline(
|
||
polylineId: const PolylineId('traveled_route'),
|
||
points: traveledPoints,
|
||
color: Colors.grey.shade600,
|
||
width: 7,
|
||
));
|
||
|
||
polylines.removeWhere((p) => p.polylineId.value == 'remaining_route');
|
||
polylines.add(Polyline(
|
||
polylineId: const PolylineId('remaining_route'),
|
||
points: remainingPoints,
|
||
color: const Color(0xFF4A80F0),
|
||
width: 7,
|
||
));
|
||
}
|
||
|
||
// دالة مساعدة لحساب الفهرس العام
|
||
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 List<LatLng> polylineCoordinates = await compute(
|
||
decodePolylineIsolate as ComputeCallback<dynamic, List<LatLng>>,
|
||
pointsString);
|
||
|
||
_stepPolylines.add(polylineCoordinates); // تخزين نقاط الخطوة
|
||
_stepBounds.add(_boundsFromLatLngList(polylineCoordinates));
|
||
}
|
||
}
|
||
|
||
Future<void> selectDestination(dynamic place) async {
|
||
placeDestinationController.clear();
|
||
placesDestination = [];
|
||
|
||
final double lat = double.parse(place['latitude'].toString());
|
||
final double lng = double.parse(place['longitude'].toString());
|
||
final LatLng destination = LatLng(lat, lng);
|
||
|
||
await startNavigationTo(destination,
|
||
infoWindowTitle: place['name'] ?? 'وجهة محددة');
|
||
}
|
||
|
||
Future<void> onMapLongPressed(LatLng tappedPoint) async {
|
||
Get.dialog(
|
||
AlertDialog(
|
||
title: const Text('بدء الملاحة؟'),
|
||
content: const Text('هل تريد الذهاب إلى هذا الموقع المحدد؟'),
|
||
actionsAlignment: MainAxisAlignment.spaceBetween,
|
||
actions: [
|
||
TextButton(
|
||
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
|
||
onPressed: () => Get.back(),
|
||
),
|
||
TextButton(
|
||
child: const Text('اذهب الآن'),
|
||
onPressed: () {
|
||
Get.back();
|
||
startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد');
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> startNavigationTo(LatLng destination,
|
||
{String infoWindowTitle = ''}) async {
|
||
isLoading = true;
|
||
update();
|
||
|
||
try {
|
||
_finalDestination = destination;
|
||
clearRoute(isNewRoute: true);
|
||
|
||
markers.add(
|
||
Marker(
|
||
markerId: const MarkerId('destination'),
|
||
position: destination,
|
||
icon: destinationIcon,
|
||
infoWindow: InfoWindow(title: infoWindowTitle),
|
||
),
|
||
);
|
||
|
||
await getRoute(myLocation!, destination);
|
||
} catch (e) {
|
||
Get.snackbar('خطأ', 'حدث خطأ أثناء تحديد الوجهة.');
|
||
print("Error starting navigation: $e");
|
||
} finally {
|
||
isLoading = false;
|
||
update();
|
||
}
|
||
}
|
||
|
||
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}');
|
||
|
||
if (response == null || response['routes'].isEmpty) {
|
||
Get.snackbar('خطأ', 'لم يتم العثور على مسار.');
|
||
return;
|
||
}
|
||
|
||
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 {
|
||
if (myLocation == null || _finalDestination == null || isLoading) return;
|
||
|
||
isLoading = true;
|
||
update();
|
||
|
||
Get.snackbar(
|
||
'إعادة التوجيه',
|
||
'جاري حساب مسار جديد من موقعك الحالي...',
|
||
backgroundColor: AppColor.goldenBronze,
|
||
);
|
||
await getRoute(myLocation!, _finalDestination!);
|
||
|
||
isLoading = false;
|
||
update();
|
||
}
|
||
|
||
void clearRoute({bool isNewRoute = false}) {
|
||
polylines.clear();
|
||
if (!isNewRoute) {
|
||
markers.removeWhere((m) => m.markerId.value == 'destination');
|
||
_finalDestination = null;
|
||
}
|
||
routeSteps.clear();
|
||
currentInstruction = "";
|
||
nextInstruction = "";
|
||
distanceToNextStep = "";
|
||
currentSpeed = 0.0;
|
||
_stepBounds.clear();
|
||
_fullRouteCoordinates.clear();
|
||
_stepPolylines.clear();
|
||
_nextInstructionSpoken = false;
|
||
_locationUpdateTimer?.cancel(); // إيقاف التحديثات عند إلغاء المسار
|
||
update();
|
||
}
|
||
|
||
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
|
||
assert(list.isNotEmpty);
|
||
double? x0, x1, y0, y1;
|
||
for (LatLng latLng in list) {
|
||
if (x0 == null) {
|
||
x0 = x1 = latLng.latitude;
|
||
y0 = y1 = latLng.longitude;
|
||
} else {
|
||
if (latLng.latitude > x1!) x1 = latLng.latitude;
|
||
if (latLng.latitude < x0) x0 = latLng.latitude;
|
||
if (latLng.longitude > y1!) y1 = latLng.longitude;
|
||
if (latLng.longitude < y0!) y0 = latLng.longitude;
|
||
}
|
||
}
|
||
return LatLngBounds(
|
||
northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!));
|
||
}
|
||
|
||
Future<void> _loadCustomIcons() async {
|
||
carIcon = await BitmapDescriptor.fromAssetImage(
|
||
const ImageConfiguration(size: Size(40, 40)), 'assets/images/car.png');
|
||
destinationIcon = await BitmapDescriptor.fromAssetImage(
|
||
const ImageConfiguration(size: Size(25, 25)), 'assets/images/b.png');
|
||
}
|
||
|
||
String _parseInstruction(String html) =>
|
||
html.replaceAll(RegExp(r'<[^>]*>'), ' ');
|
||
|
||
// =======================================================================
|
||
// ٥. دالة البحث عن الأماكن المحدثة والدوال المساعدة لها
|
||
// =======================================================================
|
||
|
||
/// الدالة المحدثة للبحث عن الأماكن
|
||
Future<void> getPlaces() async {
|
||
final q = placeDestinationController.text.trim();
|
||
if (q.isEmpty || q.length < 3) {
|
||
placesDestination = [];
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// التأكد من أن الموقع الحالي ليس null
|
||
if (myLocation == null) {
|
||
print('myLocation is null, cannot search for places.');
|
||
return;
|
||
}
|
||
|
||
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(),
|
||
},
|
||
);
|
||
|
||
// معالجة الاستجابة من السيرفر بشكل يوافق {"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;
|
||
Log.print('Updated places: $placesDestination');
|
||
update();
|
||
} catch (e) {
|
||
print('Exception in getPlaces: $e');
|
||
}
|
||
}
|
||
|
||
void onSearchChanged(String query) {
|
||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||
_debounce = Timer(const Duration(milliseconds: 700), () => getPlaces());
|
||
}
|
||
|
||
// -----------------------------------------------------------------
|
||
// --== دوال مساعدة (محدثة) ==--
|
||
// -----------------------------------------------------------------
|
||
|
||
/// تحسب المسافة بين نقطتين بالكيلومتر (معادلة هافرساين)
|
||
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)));
|
||
}
|
||
}
|