Files
intaleq_driver/lib/controller/home/navigation/navigation_controller.dart
Hamza-Ayed 1cc66029a3 25-10-5/1
2025-10-05 14:57:32 +03:00

685 lines
24 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: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)));
}
}