706 lines
22 KiB
Dart
706 lines
22 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'dart:math';
|
||
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/controller/functions/crud.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/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: 1);
|
||
LatLng? _lastRecordedLocation;
|
||
|
||
// --- متغيرات البحث ---
|
||
List<dynamic> placesDestination = [];
|
||
Timer? _debounce;
|
||
|
||
// --- متغيرات الملاحة (Navigation) ---
|
||
LatLng? _finalDestination;
|
||
List<Map<String, dynamic>> routeSteps = [];
|
||
|
||
List<LatLng> _fullRouteCoordinates = [];
|
||
int _lastTraveledIndexInFullRoute = 0;
|
||
|
||
bool _nextInstructionSpoken = false;
|
||
String currentInstruction = "";
|
||
String nextInstruction = "";
|
||
int currentStepIndex = 0;
|
||
|
||
double currentSpeed = 0.0;
|
||
String distanceToNextStep = "";
|
||
|
||
// الرابط الجديد
|
||
static const String _routeApiBaseUrl =
|
||
"https://routesjo.intaleq.xyz/route/v1/driving";
|
||
|
||
@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();
|
||
}
|
||
|
||
// =======================================================================
|
||
// ✅ دوال الخريطة الأساسية
|
||
// =======================================================================
|
||
|
||
void onMapCreated(GoogleMapController controller) {
|
||
mapController = controller;
|
||
if (myLocation != null) {
|
||
animateCameraToPosition(myLocation!);
|
||
}
|
||
}
|
||
|
||
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: 'الموقع المحدد');
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// =======================================================================
|
||
// ١. النظام الذكي للموقع (Optimized)
|
||
// =======================================================================
|
||
|
||
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!);
|
||
_startLocationTimer();
|
||
} catch (e) {
|
||
Log.print("Error getting initial location: $e");
|
||
}
|
||
}
|
||
|
||
void _startLocationTimer() {
|
||
_locationUpdateTimer?.cancel();
|
||
_locationUpdateTimer = Timer.periodic(_currentUpdateInterval, (timer) {
|
||
_updateLocationAndProcess();
|
||
});
|
||
}
|
||
|
||
Future<void> _updateLocationAndProcess() async {
|
||
try {
|
||
final position = await Geolocator.getCurrentPosition(
|
||
desiredAccuracy: LocationAccuracy.high);
|
||
|
||
final newLoc = LatLng(position.latitude, position.longitude);
|
||
|
||
// فلتر الاهتزاز (Jitter Filter)
|
||
if (_lastRecordedLocation != null) {
|
||
double dist = Geolocator.distanceBetween(
|
||
newLoc.latitude,
|
||
newLoc.longitude,
|
||
_lastRecordedLocation!.latitude,
|
||
_lastRecordedLocation!.longitude);
|
||
if (dist < 2.0) return; // تجاهل الحركة الأقل من 2 متر
|
||
}
|
||
|
||
myLocation = newLoc;
|
||
_lastRecordedLocation = newLoc;
|
||
heading = position.heading;
|
||
currentSpeed = position.speed * 3.6;
|
||
|
||
_updateCarMarker();
|
||
|
||
if (polylines.isNotEmpty && _fullRouteCoordinates.isNotEmpty) {
|
||
animateCameraToPosition(
|
||
myLocation!,
|
||
bearing: heading,
|
||
zoom: 18.0,
|
||
);
|
||
|
||
_updateTraveledPolylineSmart(myLocation!);
|
||
_checkNavigationStep(myLocation!);
|
||
}
|
||
|
||
update();
|
||
} catch (e) {
|
||
// Log.print("Loc update error: $e");
|
||
}
|
||
}
|
||
|
||
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,
|
||
zIndex: 2,
|
||
),
|
||
);
|
||
}
|
||
|
||
void animateCameraToPosition(LatLng position,
|
||
{double zoom = 17.0, double bearing = 0.0}) {
|
||
mapController?.animateCamera(
|
||
CameraUpdate.newCameraPosition(
|
||
CameraPosition(
|
||
target: position, zoom: zoom, bearing: bearing, tilt: 45.0),
|
||
),
|
||
);
|
||
}
|
||
|
||
// =======================================================================
|
||
// ٢. خوارزمية رسم المسار الذكية (Sliding Window) - مخففة للجهاز
|
||
// =======================================================================
|
||
|
||
void _updateTraveledPolylineSmart(LatLng currentPos) {
|
||
if (_fullRouteCoordinates.isEmpty) return;
|
||
|
||
// نبحث فقط في الـ 60 نقطة القادمة لتخفيف الحمل على المعالج
|
||
int searchWindow = 60;
|
||
int startIndex = _lastTraveledIndexInFullRoute;
|
||
int endIndex = min(startIndex + searchWindow, _fullRouteCoordinates.length);
|
||
|
||
double minDistance = double.infinity;
|
||
int closestIndex = startIndex;
|
||
bool foundCloser = false;
|
||
|
||
for (int i = startIndex; i < endIndex; i++) {
|
||
final point = _fullRouteCoordinates[i];
|
||
final dist = Geolocator.distanceBetween(currentPos.latitude,
|
||
currentPos.longitude, point.latitude, point.longitude);
|
||
|
||
if (dist < minDistance) {
|
||
minDistance = dist;
|
||
closestIndex = i;
|
||
foundCloser = true;
|
||
}
|
||
}
|
||
|
||
// شرط التحديث: وجدنا نقطة أقرب، وهي أمامنا (وليست خلفنا)، والمسافة منطقية
|
||
if (foundCloser &&
|
||
minDistance < 50 &&
|
||
closestIndex > _lastTraveledIndexInFullRoute) {
|
||
_lastTraveledIndexInFullRoute = closestIndex;
|
||
|
||
// استخدام sublist وهو سريع جداً في Dart
|
||
final remaining =
|
||
_fullRouteCoordinates.sublist(_lastTraveledIndexInFullRoute);
|
||
final traveled =
|
||
_fullRouteCoordinates.sublist(0, _lastTraveledIndexInFullRoute + 1);
|
||
|
||
_updatePolylinesSets(traveled, remaining);
|
||
}
|
||
}
|
||
|
||
void _updatePolylinesSets(List<LatLng> traveled, List<LatLng> remaining) {
|
||
// إزالة الخطوط القديمة وإضافة الجديدة بدلاً من تحديث الكل
|
||
polylines.removeWhere((p) => p.polylineId.value == 'remaining_route');
|
||
polylines.removeWhere((p) => p.polylineId.value == 'traveled_route');
|
||
|
||
// المسار المتبقي (أزرق واضح)
|
||
polylines.add(Polyline(
|
||
polylineId: const PolylineId('remaining_route'),
|
||
points: remaining,
|
||
color: const Color(0xFF0D47A1),
|
||
width: 6,
|
||
startCap: Cap.roundCap,
|
||
endCap: Cap.roundCap,
|
||
jointType: JointType.round,
|
||
));
|
||
|
||
// المسار المقطوع (رمادي)
|
||
polylines.add(Polyline(
|
||
polylineId: const PolylineId('traveled_route'),
|
||
points: traveled,
|
||
color: Colors.grey.shade400,
|
||
width: 6,
|
||
jointType: JointType.round,
|
||
));
|
||
}
|
||
|
||
// =======================================================================
|
||
// ٣. التحكم في التوجيهات
|
||
// =======================================================================
|
||
|
||
void _checkNavigationStep(LatLng currentPosition) {
|
||
if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length) return;
|
||
|
||
final step = routeSteps[currentStepIndex];
|
||
// في OSRM، يجب التأكد من وجود maneuver location
|
||
final maneuver = step['maneuver'];
|
||
final List<dynamic> location = maneuver['location']; // [lng, lat]
|
||
final endLatLng = LatLng(location[1], location[0]);
|
||
|
||
final distance = Geolocator.distanceBetween(
|
||
currentPosition.latitude,
|
||
currentPosition.longitude,
|
||
endLatLng.latitude,
|
||
endLatLng.longitude,
|
||
);
|
||
|
||
distanceToNextStep = distance > 1000
|
||
? "${(distance / 1000).toStringAsFixed(1)} كم"
|
||
: "${distance.toStringAsFixed(0)} متر";
|
||
|
||
// نطق التعليمات قبل 50 متر
|
||
if (distance < 50 &&
|
||
!_nextInstructionSpoken &&
|
||
nextInstruction.isNotEmpty) {
|
||
Get.find<TextToSpeechController>().speakText(nextInstruction);
|
||
_nextInstructionSpoken = true;
|
||
}
|
||
|
||
// الانتقال للخطوة التالية عند الاقتراب (20 متر)
|
||
if (distance < 20) {
|
||
_advanceStep();
|
||
}
|
||
}
|
||
|
||
void _advanceStep() {
|
||
currentStepIndex++;
|
||
if (currentStepIndex < routeSteps.length) {
|
||
currentInstruction = routeSteps[currentStepIndex]['instruction_text'];
|
||
|
||
// تجهيز التعليمات القادمة
|
||
if ((currentStepIndex + 1) < routeSteps.length) {
|
||
nextInstruction =
|
||
"ثم ${routeSteps[currentStepIndex + 1]['instruction_text']}";
|
||
} else {
|
||
nextInstruction = "ستصل إلى وجهتك";
|
||
}
|
||
|
||
_nextInstructionSpoken = false;
|
||
update();
|
||
} else {
|
||
_finishNavigation();
|
||
}
|
||
}
|
||
|
||
void _finishNavigation() {
|
||
currentInstruction = "لقد وصلت إلى وجهتك";
|
||
nextInstruction = "";
|
||
distanceToNextStep = "";
|
||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||
update();
|
||
}
|
||
|
||
// =======================================================================
|
||
// ٤. جلب المسار (🔥 تم التحديث للرابط الجديد 🔥)
|
||
// =======================================================================
|
||
|
||
Future<void> getRoute(LatLng origin, LatLng destination) async {
|
||
// 🔥 بناء الرابط حسب التنسيق: /driving/lng,lat;lng,lat
|
||
String coords =
|
||
"${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}";
|
||
|
||
// 🔥 تفعيل steps=true لجلب الخطوات
|
||
String url =
|
||
"$_routeApiBaseUrl/$coords?steps=true&overview=full&geometries=polyline";
|
||
|
||
try {
|
||
// الرابط الجديد قد لا يحتاج API Key في الهيدر، ولكن نتركه إذا كان السيرفر يطلبه
|
||
// إذا كان الرابط عام، يمكن إزالة الهيدر
|
||
final response = await http.get(Uri.parse(url));
|
||
|
||
if (response.statusCode != 200) {
|
||
Get.snackbar('تنبيه', 'تعذر الاتصال بخدمة التوجيه.');
|
||
return;
|
||
}
|
||
|
||
final responseData = jsonDecode(response.body);
|
||
|
||
// التحقق حسب هيكلية OSRM
|
||
if (responseData['code'] != 'Ok' ||
|
||
(responseData['routes'] as List).isEmpty) {
|
||
Get.snackbar('عذراً', 'لم يتم العثور على مسار.');
|
||
return;
|
||
}
|
||
|
||
var route = responseData['routes'][0];
|
||
|
||
// 1. فك تشفير Polyline
|
||
final pointsString = route['geometry'];
|
||
_fullRouteCoordinates = await compute(
|
||
decodePolylineIsolate as ComputeCallback<dynamic, List<LatLng>>,
|
||
pointsString);
|
||
|
||
// تهيئة الرسم
|
||
_lastTraveledIndexInFullRoute = 0;
|
||
_updatePolylinesSets([], _fullRouteCoordinates);
|
||
|
||
// 2. استخراج الخطوات (داخل Legs في OSRM)
|
||
var legs = route['legs'] as List;
|
||
if (legs.isNotEmpty) {
|
||
var steps = legs[0]['steps'] as List;
|
||
routeSteps = List<Map<String, dynamic>>.from(steps);
|
||
} else {
|
||
routeSteps = [];
|
||
}
|
||
|
||
// 3. معالجة النصوص العربية للخطوات
|
||
for (var step in routeSteps) {
|
||
step['instruction_text'] = _createInstructionFromManeuver(step);
|
||
}
|
||
|
||
// 4. بدء الملاحة
|
||
currentStepIndex = 0;
|
||
_nextInstructionSpoken = false;
|
||
if (routeSteps.isNotEmpty) {
|
||
currentInstruction = routeSteps[0]['instruction_text'];
|
||
if (routeSteps.length > 1) {
|
||
nextInstruction = "ثم ${routeSteps[1]['instruction_text']}";
|
||
} else {
|
||
nextInstruction = "الوجهة النهائية";
|
||
}
|
||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||
}
|
||
|
||
// 5. ضبط الكاميرا
|
||
if (_fullRouteCoordinates.isNotEmpty) {
|
||
final bounds = _boundsFromLatLngList(_fullRouteCoordinates);
|
||
mapController
|
||
?.animateCamera(CameraUpdate.newLatLngBounds(bounds, 80.0));
|
||
}
|
||
|
||
update();
|
||
} catch (e) {
|
||
Log.print("GetRoute Error: $e");
|
||
Get.snackbar('خطأ', 'حدث خطأ غير متوقع.');
|
||
}
|
||
}
|
||
|
||
// 🔥 تحويل تعليمات OSRM إلى العربية
|
||
String _createInstructionFromManeuver(Map<String, dynamic> step) {
|
||
if (step['maneuver'] == null) return "تابع المسير";
|
||
|
||
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':
|
||
return "لقد وصلت إلى وجهتك، $name";
|
||
case 'turn':
|
||
case 'fork':
|
||
case 'roundabout':
|
||
case 'merge':
|
||
case 'on ramp':
|
||
case 'off ramp':
|
||
case 'end of road':
|
||
instruction = _getTurnInstruction(modifier);
|
||
break;
|
||
case 'new name':
|
||
instruction = "تابع المسير";
|
||
break;
|
||
default:
|
||
instruction = "تابع المسير";
|
||
}
|
||
|
||
if (name.isNotEmpty) {
|
||
if (type == 'new name' || type == 'continue') {
|
||
instruction += " على $name";
|
||
} else {
|
||
instruction += " نحو $name";
|
||
}
|
||
}
|
||
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 "اتجه";
|
||
}
|
||
}
|
||
|
||
// =======================================================================
|
||
// ٥. أدوات مساعدة
|
||
// =======================================================================
|
||
|
||
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);
|
||
} finally {
|
||
isLoading = false;
|
||
update();
|
||
}
|
||
}
|
||
|
||
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}) {
|
||
if (!isNewRoute) {
|
||
markers.removeWhere((m) => m.markerId.value == 'destination');
|
||
_finalDestination = null;
|
||
polylines.clear();
|
||
}
|
||
routeSteps.clear();
|
||
_fullRouteCoordinates.clear();
|
||
_lastTraveledIndexInFullRoute = 0;
|
||
currentInstruction = "";
|
||
nextInstruction = "";
|
||
distanceToNextStep = "";
|
||
update();
|
||
}
|
||
|
||
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(30, 30)), 'assets/images/b.png');
|
||
}
|
||
|
||
Future<void> getPlaces() async {
|
||
final q = placeDestinationController.text.trim();
|
||
if (q.length < 3) {
|
||
placesDestination = [];
|
||
update();
|
||
return;
|
||
}
|
||
|
||
if (myLocation == null) return;
|
||
|
||
final lat = myLocation!.latitude;
|
||
final lng = myLocation!.longitude;
|
||
const radiusKm = 200.0;
|
||
final latDelta = _kmToLatDelta(radiusKm);
|
||
final lngDelta = _kmToLngDelta(radiusKm, lat);
|
||
|
||
final payload = {
|
||
'query': q,
|
||
'lat_min': (lat - latDelta).toString(),
|
||
'lat_max': (lat + latDelta).toString(),
|
||
'lng_min': (lng - lngDelta).toString(),
|
||
'lng_max': (lng + lngDelta).toString(),
|
||
};
|
||
|
||
try {
|
||
final response =
|
||
await CRUD().post(link: AppLink.getPlacesSyria, payload: payload);
|
||
final responseData = (response);
|
||
|
||
List list;
|
||
if (responseData is Map && responseData['status'] == 'success') {
|
||
list = List.from(responseData['message'] as List);
|
||
} else if (responseData is List) {
|
||
list = List.from(responseData);
|
||
} else {
|
||
return;
|
||
}
|
||
|
||
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);
|
||
p['distanceKm'] = distance;
|
||
}
|
||
|
||
list.sort((a, b) =>
|
||
(a['distanceKm'] as double).compareTo(b['distanceKm'] as double));
|
||
placesDestination = list;
|
||
update();
|
||
} catch (e) {
|
||
print('Exception in getPlaces: $e');
|
||
}
|
||
}
|
||
|
||
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());
|
||
await startNavigationTo(LatLng(lat, lng),
|
||
infoWindowTitle: place['name'] ?? 'وجهة');
|
||
}
|
||
|
||
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 a = sin(dLat / 2) * sin(dLat / 2) +
|
||
cos(lat1 * pi / 180) *
|
||
cos(lat2 * pi / 180) *
|
||
sin(dLon / 2) *
|
||
sin(dLon / 2);
|
||
return R * 2 * atan2(sqrt(a), sqrt(1 - a));
|
||
}
|
||
|
||
double _kmToLatDelta(double km) => km / 111.32;
|
||
double _kmToLngDelta(double km, double lat) =>
|
||
km / (111.32 * cos(lat * pi / 180));
|
||
|
||
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
|
||
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!));
|
||
}
|
||
|
||
// --- دوال التحقق من الدولة ---
|
||
String getLocationArea(double latitude, double longitude) {
|
||
LatLng p = LatLng(latitude, longitude);
|
||
if (isPointInPolygon(p, CountryPolygons.jordanBoundary)) {
|
||
box.write(BoxName.countryCode, 'Jordan');
|
||
return 'Jordan';
|
||
}
|
||
if (isPointInPolygon(p, CountryPolygons.syriaBoundary)) {
|
||
box.write(BoxName.countryCode, 'Syria');
|
||
return 'Syria';
|
||
}
|
||
if (isPointInPolygon(p, CountryPolygons.egyptBoundary)) {
|
||
box.write(BoxName.countryCode, 'Egypt');
|
||
return 'Egypt';
|
||
}
|
||
return 'Unknown';
|
||
}
|
||
|
||
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];
|
||
if (_rayIntersectsSegment(point, vertex1, vertex2)) intersections++;
|
||
}
|
||
return intersections % 2 != 0;
|
||
}
|
||
|
||
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;
|
||
|
||
if ((py < v1y && py < v2y) || (py > v1y && py > v2y)) return false;
|
||
double intersectX = v1x + (py - v1y) * (v2x - v1x) / (v2y - v1y);
|
||
return intersectX > px;
|
||
}
|
||
}
|