Files
intaleq_driver/lib/controller/home/navigation/navigation_controller.dart
2025-09-21 15:02:12 +03:00

659 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:math';
import 'dart:math' as 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 '../../../print.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'<[^>]*>'), ' ');
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
const R = 6371.0; // km
final dLat = (lat2 - lat1) * math.pi / 180.0;
final dLon = (lon2 - lon1) * math.pi / 180.0;
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
math.cos(lat1 * math.pi / 180.0) *
math.cos(lat2 * math.pi / 180.0) *
math.sin(dLon / 2) *
math.sin(dLon / 2);
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return R * c;
}
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
double _kmToLatDelta(double km) => km / 111.0;
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض)
double _kmToLngDelta(double km, double atLat) =>
km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9);
/// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة)
double _relevanceScore(String name, String query) {
final n = name.toLowerCase();
final parts =
query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2);
double s = 0.0;
for (final p in parts) {
if (n.startsWith(p)) {
s += 2.0;
} else if (n.contains(p)) {
s += 1.0;
}
}
return s;
}
Future<void> getPlaces() async {
final q = placeDestinationController.text.trim();
if (q.isEmpty) {
placesDestination = [];
update();
return;
}
final lat = myLocation!.latitude;
final lng = myLocation!.longitude;
// نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك)
const radiusKm = 200.0;
// حساب الباوند الصحيح (درجات، وليس 2.2 درجة ثابتة)
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 {
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(),
},
);
// يدعم شكلي استجابة: إما {"...","message":[...]} أو قائمة مباشرة [...]
List list;
if (response is Map && response['message'] is List) {
list = List.from(response['message'] as List);
} else if (response is List) {
list = List.from(response);
} else {
print('Unexpected response shape');
return;
}
// جهّز الحقول المحتملة للأسماء
String _bestName(Map p) {
return (p['name'] ?? p['name_ar'] ?? p['name_en'] ?? '').toString();
}
// احسب المسافة ودرجة التطابق والنقاط
for (final p in list) {
final plat = double.tryParse(p['latitude']?.toString() ?? '') ?? 0.0;
final plng = double.tryParse(p['longitude']?.toString() ?? '') ?? 0.0;
final d = _haversineKm(lat, lng, plat, plng);
final rel = _relevanceScore(_bestName(p), q);
// معادلة ترتيب ذكية: مسافة أقل + تطابق أعلى = نقاط أعلى
// تضيف +1 لضمان عدم وصول الوزن للصفر عند عدم وجود تطابق
final score = (1.0 / (1.0 + d)) * (1.0 + rel);
p['distanceKm'] = d;
p['relevance'] = rel;
p['score'] = score;
}
// رتّب حسب score تنازليًا، ثم المسافة تصاعديًا كحسم
list.sort((a, b) {
final sa = (a['score'] ?? 0.0) as double;
final sb = (b['score'] ?? 0.0) as double;
final cmp = sb.compareTo(sa);
if (cmp != 0) return cmp;
final da = (a['distanceKm'] ?? 1e9) as double;
final db = (b['distanceKm'] ?? 1e9) as double;
return da.compareTo(db);
});
// خذ أول 1015 للعرض (اختياري)، أو اعرض الكل
placesDestination = list.take(15).toList();
Log.print('placesDestination: $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());
}
}