Files
intaleq_driver/lib/controller/home/navigation/navigation_controller.dart
2025-09-01 19:04:50 +03:00

575 lines
19 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:math';
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:google_polyline_algorithm/google_polyline_algorithm.dart';
import 'package:sefer_driver/constant/colors.dart';
// استخدام نفس مسارات الاستيراد التي قدمتها
import '../../../constant/api_key.dart';
import '../../../constant/links.dart';
import '../../functions/crud.dart';
import '../../functions/tts.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;
}
// =======================================================================
// ٤. دوال مساعدة وتجهيز البيانات
// =======================================================================
void _prepareStepData() {
_stepBounds.clear();
_stepPolylines.clear();
if (routeSteps.isEmpty) return;
for (final step in routeSteps) {
final pointsString = step['polyline']['points'];
final List<List<num>> points =
decodePolyline(pointsString).cast<List<num>>();
final polylineCoordinates = points
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
.toList();
_stepPolylines.add(polylineCoordinates); // تخزين نقاط الخطوة
_stepBounds.add(_boundsFromLatLngList(polylineCoordinates));
}
}
// ... باقي دوال الكنترولر بدون تغيير ...
// (selectDestination, onMapLongPressed, startNavigationTo, getRoute, etc.)
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 url =
'${AppLink.googleMapsLink}directions/json?language=ar&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}';
var response = await CRUD().getGoogleApi(link: url, payload: {});
if (response == null || response['routes'].isEmpty) {
Get.snackbar('خطأ', 'لم يتم العثور على مسار.');
return;
}
polylines.clear();
final pointsString = response['routes'][0]['overview_polyline']['points'];
final List<List<num>> points =
decodePolyline(pointsString).cast<List<num>>();
_fullRouteCoordinates = points
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
.toList();
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']);
_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 {
if (placeDestinationController.text.trim().isEmpty) {
placesDestination = [];
update();
return;
}
if (myLocation == null) {
Get.snackbar('انتظر', 'جاري تحديد موقعك الحالي...');
return;
}
final query = placeDestinationController.text.trim();
final lat = myLocation!.latitude;
final lng = myLocation!.longitude;
const double range = 2.2;
final lat_min = lat - range,
lat_max = lat + range,
lng_min = lng - range,
lng_max = lng + range;
try {
final response = await CRUD().post(
link: AppLink.getPlacesSyria,
payload: {
'query': query,
'lat_min': lat_min.toString(),
'lat_max': lat_max.toString(),
'lng_min': lng_min.toString(),
'lng_max': lng_max.toString(),
},
);
if (response != 'failure') {
placesDestination = response['message'] ?? [];
} else {
placesDestination = [];
}
} catch (e) {
print('Exception in getPlaces: $e');
} finally {
update();
}
}
void onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 700), () => getPlaces());
}
}