Files
intaleq_driver/lib/controller/home/navigation/navigation_controller.dart
Hamza-Ayed 3c0ae4cf2f 26-1-20/1
2026-01-20 10:11:10 +03:00

706 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}