new backend 29-04-2026

This commit is contained in:
Hamza-Ayed
2026-04-30 01:42:57 +03:00
parent b92db3bb39
commit 4385ef5a99
20 changed files with 796 additions and 708 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
@@ -10,6 +11,7 @@ import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:bubble_head/bubble.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart' as geo;
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
@@ -20,6 +22,7 @@ import '../../../constant/colors.dart';
import '../../../constant/country_polygons.dart';
import '../../../constant/links.dart';
import '../../../constant/table_names.dart';
import '../../../env/env.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/Rate/rate_passenger.dart';
@@ -31,7 +34,8 @@ import '../../functions/location_controller.dart';
import '../../functions/tts.dart';
import 'behavior_controller.dart';
class MapDriverController extends GetxController {
class MapDriverController extends GetxController
with GetSingleTickerProviderStateMixin {
bool isLoading = true;
final formKey1 = GlobalKey<FormState>();
final formKey2 = GlobalKey<FormState>();
@@ -97,6 +101,26 @@ class MapDriverController extends GetxController {
int remainingTimeToShowPassengerInfoWindowFromDriver = 25;
int remainingTimeToPassenger = 60;
int remainingTimeInPassengerLocatioWait = 60;
// ─── Navigation & Smoothing ──────────────────────────────────────────
AnimationController? _animController;
LatLng? smoothedLocation;
double smoothedHeading = 0.0;
LatLng? _oldLoc;
LatLng? _targetLoc;
double _oldHeading = 0.0;
double _targetHeading = 0.0;
List<Map<String, dynamic>> routeSteps = [];
int currentStepIndex = 0;
String currentInstruction = "";
String nextInstruction = "";
String distanceToNextStep = "";
int currentManeuverModifier = 0;
bool _nextInstructionSpoken = false;
bool isTtsEnabled = true;
StreamSubscription<geo.Position>? _locationSubscription;
// ─────────────────────────────────────────────────────────────────────
bool isDriverNearPassengerStart = false;
IntaleqMapController? mapController;
late LatLng myLocation;
@@ -111,10 +135,7 @@ class MapDriverController extends GetxController {
LatLng latLngPassengerLocation = LatLng(0, 0);
late LatLng latLngPassengerDestination = LatLng(0, 0);
List<Map<String, dynamic>> routeSteps = [];
String currentInstruction = "";
int currentStepIndex = 0;
bool isTtsEnabled = false;
// في MapDriverController
@@ -152,7 +173,9 @@ class MapDriverController extends GetxController {
_posSub?.cancel();
_posSub = null;
// mapController?.dispose();
_animController?.dispose();
_locationSubscription?.cancel();
super.onClose();
}
@@ -224,7 +247,7 @@ class MapDriverController extends GetxController {
if (isCameraLocked && mapController != null) {
double bearing = (speedKmh > 5) ? heading : 0.0;
// ملاحظة: يمكنك تخزين آخر bearing معروف واستخدامه عند التوقف لتحسين التجربة
_animateCameraToNavigationMode(newLoc, heading);
_animateCameraToNavigationMode(newLoc, bearing);
}
// 4. فحص التعليمات الصوتية
@@ -315,6 +338,8 @@ class MapDriverController extends GetxController {
// 2. تنظيف الحالة
box.write(BoxName.rideStatus, 'Canceled'); // أو الحالة الافتراضية
box.remove(BoxName.rideArguments);
box.remove(BoxName.passengerID);
box.remove(BoxName.rideId);
// 3. عرض رسالة للسائق
if (Get.isDialogOpen == true) {
@@ -379,8 +404,10 @@ class MapDriverController extends GetxController {
box.write(BoxName.statusDriverLocation, 'blocked');
// عرض رسالة العقوبة
Get.snackbar("Your account is temporarily restricted ⛔".tr,
"Due to excessive cancellations (3 times), receiving orders has been suspended for 4 hours.".tr,
Get.snackbar(
"Your account is temporarily restricted ⛔".tr,
"Due to excessive cancellations (3 times), receiving orders has been suspended for 4 hours."
.tr,
duration: Duration(seconds: 8),
backgroundColor: Colors.red,
colorText: Colors.white,
@@ -396,6 +423,8 @@ class MapDriverController extends GetxController {
// تنظيف البيانات
box.remove(BoxName.rideArgumentsFromBackground);
box.remove(BoxName.rideArguments);
box.remove(BoxName.passengerID);
box.remove(BoxName.rideId);
box.write(BoxName.rideStatus, 'Cancel');
// تسجيل محلي (اختياري)
@@ -726,10 +755,10 @@ class MapDriverController extends GetxController {
// إغلاق مؤشر التحميل لأننا حصلنا على النتيجة
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
Get.back();
}
if (distanceToPassenger < 100) {
if (distanceToPassenger < 150) {
// زدت المسافة قليلاً لمرونة أكبر (150م)
// --- أ) تحديث الحالة المحلية (Optimistic Update) ---
@@ -772,16 +801,26 @@ class MapDriverController extends GetxController {
});
} else {
// --- حالة الرفض (بعيد جداً) ---
MyDialog().getDialog('You are far from passenger location'.tr,
'Please go closer to the passenger location (less than 150m)'.tr,
() {
// الديالوج يغلق نفسه الآن تلقائياً
});
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
title: Text('You are far from passenger location'.tr),
content: Text(
'Please go closer to the passenger location (less than 150m)'
.tr),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'.tr),
),
],
),
);
}
} catch (e) {
// تنظيف اللودينج في حال حدوث خطأ غير متوقع
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
Get.back();
}
Log.print("Error starting ride: $e");
Get.snackbar("Error", "Could not start ride. Please check internet.");
@@ -1066,6 +1105,8 @@ class MapDriverController extends GetxController {
isPriceWindow = false;
box.write(BoxName.rideStatus, 'Finished');
box.write(BoxName.statusDriverLocation, 'off');
box.remove(BoxName.passengerID);
box.remove(BoxName.rideId);
// 4. حساب التكلفة النهائية (Logic)
_calculateFinalTotalCost();
@@ -1382,7 +1423,7 @@ class MapDriverController extends GetxController {
// 🟢 2. المنطق المتغير
const double longTripPerMin = 600.0;
const double mediumDistThresholdKm = 25.0;
const double longDistThresholdKm = 35.0;
// نسبة التخفيض
@@ -1588,9 +1629,26 @@ class MapDriverController extends GetxController {
if (mapController == null) return;
try {
// 1. طلب المسار من الباكيج
final response =
await mapController!.getDirections(origin, destination, steps: true);
// 1. طلب المسار من السيرفر الموحد (SaaS) لضمان الدقة وتفادي الـ 401
final saasUrl = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: {
'fromLat': origin.latitude.toString(),
'fromLng': origin.longitude.toString(),
'toLat': destination.latitude.toString(),
'toLng': destination.longitude.toString(),
'steps': 'true', // نحتاجها للملاحة والتوجيه
'alternatives': 'false',
});
final httpResponse = await http.get(saasUrl, headers: {
'x-api-key': Env.mapSaasKey,
'Content-Type': 'application/json',
});
if (httpResponse.statusCode != 200) {
throw Exception("Routing request failed: ${httpResponse.statusCode}");
}
final response = jsonDecode(httpResponse.body);
// 2. التعامل مع الـ JSON المباشر (الذي أرسله المستخدم)
// إذا كان الـ response يحتوي على الحقول مباشرة في الجذر
@@ -1628,41 +1686,36 @@ class MapDriverController extends GetxController {
color: routeColor,
));
// د) معالجة الخطوات (Legs & Steps) إذا توفرت في الـ JSON
List<dynamic> legs = response['legs'] ?? [];
if (legs.isNotEmpty) {
final stepsList =
List<Map<String, dynamic>>.from(legs[0]['steps'] ?? []);
// د) معالجة الخطوات (Instructions) للسيرفر الموحد
final List<dynamic> instructions = response['instructions'] ?? [];
if (instructions.isNotEmpty) {
routeSteps = List<Map<String, dynamic>>.from(instructions.map((e) {
int endIdx = (e['interval'] as List)[1];
// التأكد من أن الـ index لا يتجاوز طول المسار
if (endIdx >= fullRoute.length) endIdx = fullRoute.length - 1;
for (var step in stepsList) {
step['html_instructions'] = _createInstructionFromManeuverSmart(step);
if (step['maneuver'] != null &&
step['maneuver']['location'] != null) {
var loc = step['maneuver']['location'];
// التعامل مع تنسيق OSRM [lng, lat]
if (loc is List && loc.length >= 2) {
step['end_location'] = {'lat': loc[1], 'lng': loc[0]};
} else if (loc is Map) {
step['end_location'] = loc;
return {
'html_instructions': e['text'] ?? "",
'sign': e['sign'] ?? 0,
'end_location': {
'lat': fullRoute[endIdx].latitude,
'lng': fullRoute[endIdx].longitude,
}
}
}
};
}));
routeSteps = stepsList;
currentStepIndex = 0;
currentInstruction = routeSteps[0]['html_instructions'];
currentManeuverModifier = routeSteps[0]['sign'];
_nextInstructionSpoken = false;
// نطق أول تعليمة
if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['html_instructions'];
if (Get.isRegistered<TextToSpeechController>()) {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
if (Get.isRegistered<TextToSpeechController>() && isTtsEnabled) {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
} else {
// في حال عدم وجود steps، نقوم بتصفيرها
routeSteps = [];
currentInstruction = "";
currentManeuverModifier = 0;
}
// هـ) تحريك الكاميرا لتشمل المسار
@@ -1678,119 +1731,7 @@ class MapDriverController extends GetxController {
}
}
// 🔥 دالة الترجمة المحسنة (من NavigationController)
String _createInstructionFromManeuverSmart(Map<String, dynamic> step) {
if (step['maneuver'] == null) return "Continue straight".tr;
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 = "Go".tr;
break;
case 'arrive':
return "You have arrived at your destination, @name".trParams({'name': name});
case 'turn':
case 'fork':
case 'roundabout':
case 'merge':
case 'on ramp':
case 'off ramp':
case 'end of road':
instruction =
_getTurnInstruction(modifier); // استخدم نفس دالتك المساعدة هنا
break;
default:
instruction = "Continue straight".tr;
}
if (name.isNotEmpty) {
if (type == 'continue') {
instruction += " ${"on".tr} $name";
} else {
instruction += " ${"towards".tr} $name";
}
}
return instruction;
}
String _createInstructionFromManeuver(Map<String, dynamic> step) {
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 = "Go".tr;
break;
case 'arrive':
instruction = "You have arrived at your destination".tr;
if (name.isNotEmpty) instruction += "، $name";
return instruction;
case 'turn':
case 'fork':
case 'off ramp':
case 'on ramp':
case 'roundabout':
instruction = _getTurnInstruction(modifier);
break;
case 'continue':
instruction = "Continue".tr;
break;
default:
instruction = "Head".tr;
}
if (name.isNotEmpty) {
if (instruction == "استمر") {
instruction += " ${"on".tr} $name";
} else {
instruction += " ${"to".tr} $name";
}
} else if (type == 'continue' && modifier == 'straight') {
instruction = "Continue straight".tr;
}
return instruction;
}
/**
* دالة مساعدة لترجمة تعليمات الانعطاف
*/
String _getTurnInstruction(String modifier) {
switch (modifier) {
case 'uturn':
return "Make a U-turn".tr;
case 'sharp right':
return "Turn sharp right".tr;
case 'right':
return "Turn right".tr;
case 'slight right':
return "Turn slight right".tr;
case 'straight':
return "Continue straight".tr;
case 'slight left':
return "Turn slight left".tr;
case 'left':
return "Turn left".tr;
case 'sharp left':
return "Turn sharp left".tr;
default:
return "Head".tr;
}
}
/**
* دالة لحساب حدود الخريطة (Bounds) من قائمة نقاط
*/
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
assert(list.isNotEmpty);
double? x0, x1, y0, y1;
@@ -1809,66 +1750,7 @@ class MapDriverController extends GetxController {
northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!));
}
// الدالة التي يتم استدعاؤها من خدمة الموقع كل 5 ثوان (أو حسب الفترة المحددة)
void onLocationUpdated(Position newPosition) {
myLocation = LatLng(newPosition.latitude, newPosition.longitude);
heading = newPosition.heading;
// -->> منطق قياس الأداء يبدأ هنا <<--
final stopwatch = Stopwatch()..start();
// -->> منطق الملاحة وتحديث المسار <<--
_onLocationTick(myLocation);
stopwatch.stop();
// -->> تحليل الأداء واتخاذ القرار <<--
if (!_hasMadeDecision) {
_performanceReadings.add(stopwatch.elapsedMilliseconds);
if (_performanceReadings.length >= _readingsToCollect) {
_analyzePerformance();
_hasMadeDecision = true;
}
}
}
// =================================================================
// 3. منطق الملاحة الداخلي (Internal Navigation Logic)
// =================================================================
void _onLocationTick(LatLng pos) {
if (activeRouteSteps.isEmpty || currentStepIndex >= _stepBounds.length) {
return;
}
final double dToEnd =
_distanceMeters(pos, _stepEndPoints[currentStepIndex]);
if (dToEnd <= 35) {
// 35 متر عتبة للوصول لنهاية الخطوة
_advanceStep();
}
}
void _advanceStep() {
if (currentStepIndex >= _stepBounds.length - 1) {
// وصل للنهاية
currentInstruction = "You have arrived at your destination".tr;
return;
}
currentStepIndex++;
currentInstruction = _parseInstruction(
activeRouteSteps[currentStepIndex]['html_instructions']);
Get.isRegistered<TextToSpeechController>()
? Get.find<TextToSpeechController>().speakText(currentInstruction)
: Get.put(TextToSpeechController()).speakText(currentInstruction);
// -->> هنا يتم تحديث لون المسار <<--
_updateTraveledPath();
_fitToBounds(_stepBounds[currentStepIndex],
padding: 80); // تقريب الكاميرا على الخطوة التالية
update();
}
// داخل MapDriverController
Future<void> markDriverAsArrived() async {
@@ -1918,53 +1800,7 @@ class MapDriverController extends GetxController {
}
}
void _updateTraveledPath() {
// استخراج كل النقاط للخطوات التي تم اجتيازها
List<LatLng> pointsForTraveledSteps = [];
for (int i = 0; i < currentStepIndex; i++) {
final stepPolyline = activeRouteSteps[i]['polyline']['points'];
pointsForTraveledSteps.addAll(decodePolylineToLatLng(stepPolyline));
}
traveledPathPoints.assignAll(pointsForTraveledSteps);
}
void _prepareStepData(List<Map<String, dynamic>> steps) {
_stepBounds.clear();
_stepEndPoints.clear();
for (final s in steps) {
// 1. استخراج نقطة النهاية (الكود الحالي سليم)
final end = s['end_location'];
_stepEndPoints.add(LatLng(
(end['lat'] as num).toDouble(),
(end['lng'] as num).toDouble(),
));
// 2. فك تشفير البوليلاين الخاص بالخطوة وتحويله إلى LatLng
// -->> هنا تم التصحيح <<--
List<LatLng> pts = PolylineUtils.decode(s['polyline']['points']);
// أضف نقاط البداية والنهاية إذا لم تكن موجودة في البوليلاين لضمان دقة الحدود
if (pts.isNotEmpty) {
final start = s['start_location'];
final startLatLng = LatLng(
(start['lat'] as num).toDouble(), (start['lng'] as num).toDouble());
if (pts.first != startLatLng) {
pts.insert(0, startLatLng);
}
}
_stepBounds.add(_boundsFromPoints(pts));
}
}
// A helper function to decode and convert the polyline string
List<LatLng> decodePolylineToLatLng(String polylineString) {
// 1. Decode the string into a list of number lists (e.g., [[lat, lng], ...])
List<LatLng> decodedPoints = PolylineUtils.decode(polylineString);
return decodedPoints;
}
// =================================================================
// 4. منطق الأداء الذكي (Smart Performance Logic)
@@ -1981,7 +1817,8 @@ class MapDriverController extends GetxController {
void _suggestOptimization() {
Get.snackbar(
"Improve app performance".tr,
"To ensure the best experience, we suggest adjusting the settings to suit your device. Would you like to proceed?".tr,
"To ensure the best experience, we suggest adjusting the settings to suit your device. Would you like to proceed?"
.tr,
duration: const Duration(seconds: 15),
mainButton: TextButton(
child: Text("Yes, optimize".tr),
@@ -1998,13 +1835,7 @@ class MapDriverController extends GetxController {
// =================================================================
// 5. دوال مساعدة (Helper Functions)
// =================================================================
void _resetRouteState() {
activeRouteSteps.clear();
traveledPathPoints.clear();
upcomingPathPoints.clear();
_allPointsForActiveRoute.clear();
currentStepIndex = 0;
}
String _parseInstruction(String html) =>
html.replaceAll(RegExp(r'<[^>]*>'), '');
@@ -2108,16 +1939,7 @@ class MapDriverController extends GetxController {
);
}
bool _contains(LatLngBounds b, LatLng p) {
final south = math.min(b.southwest.latitude, b.northeast.latitude);
final north = math.max(b.southwest.latitude, b.northeast.latitude);
final west = math.min(b.southwest.longitude, b.northeast.longitude);
final east = math.max(b.southwest.longitude, b.northeast.longitude);
return (p.latitude >= south &&
p.latitude <= north &&
p.longitude >= west &&
p.longitude <= east);
}
double _distanceMeters(LatLng a, LatLng b) {
// هافرساين مبسطة
@@ -2188,6 +2010,10 @@ class MapDriverController extends GetxController {
durationOfRideValue = Get.arguments['durationOfRideValue'];
paymentAmount = Get.arguments['paymentAmount'];
paymentMethod = Get.arguments['paymentMethod'];
// 🔥 حفظ البيانات في الذاكرة المحلية فوراً (لفصل السوكيت عن الكنترولر)
box.write(BoxName.passengerID, passengerId.toString());
box.write(BoxName.rideId, rideId.toString());
isHaveSteps = Get.arguments['isHaveSteps'];
step0 = Get.arguments['step0'];
step1 = Get.arguments['step1'];
@@ -2208,7 +2034,7 @@ class MapDriverController extends GetxController {
Get.find<LocationController>().myLocation.latitude.toString();
String lng =
Get.find<LocationController>().myLocation.longitude.toString();
String origin = '$lat,$lng';
// Set the origin and destination coordinates for the Google Maps directions request.
Future.delayed(const Duration(seconds: 1));
getRoute(
@@ -2222,7 +2048,7 @@ class MapDriverController extends GetxController {
}
}
latlng(String passengerLocation, passengerDestination) {
void latlng(String passengerLocation, String passengerDestination) {
double latPassengerLocation =
double.parse(passengerLocation.toString().split(',')[0]);
double lngPassengerLocation =
@@ -2257,7 +2083,6 @@ class MapDriverController extends GetxController {
@override
void onInit() async {
mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY);
// Get the passenger location from the arguments.
await argumentLoading();
Get.put(FirebaseMessagesController());
runGoogleMapDirectly();
@@ -2265,24 +2090,141 @@ class MapDriverController extends GetxController {
addCustomPassengerIcon();
addCustomStartIcon();
addCustomEndIcon();
if (!Get.isRegistered<TextToSpeechController>()) {
Get.put(TextToSpeechController(), permanent: true);
// permanent: true تمنع حذفه عند تغيير الصفحات
}
// updateMarker();
// updateLocation();
_animController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
_animController!.addListener(() {
if (_oldLoc != null && _targetLoc != null) {
final t = _animController!.value;
final lat = lerpDouble(_oldLoc!.latitude, _targetLoc!.latitude, t)!;
final lng = lerpDouble(_oldLoc!.longitude, _targetLoc!.longitude, t)!;
smoothedLocation = LatLng(lat, lng);
smoothedHeading = _lerpAngle(_oldHeading, _targetHeading, t);
update();
}
});
_startLocationListening();
startTimerToShowPassengerInfoWindowFromDriver();
// durationToAdd = Duration(seconds: int.parse(duration));
durationToAdd = Duration(seconds: parseDurationToInt(duration));
hours = durationToAdd.inHours;
minutes = (durationToAdd.inMinutes % 60).round();
calculateConsumptionFuel();
// updateLocation();// for now to test it
// cancelCheckRidefromPassenger();
// checkIsDriverNearPassenger();
super.onInit();
}
void _startLocationListening() {
_locationSubscription?.cancel();
_locationSubscription = geo.Geolocator.getPositionStream(
locationSettings: const geo.LocationSettings(
accuracy: geo.LocationAccuracy.bestForNavigation,
distanceFilter: 2,
),
).listen((geo.Position pos) {
_handleLocationUpdate(pos);
});
}
void _handleLocationUpdate(geo.Position pos) {
final newLoc = LatLng(pos.latitude, pos.longitude);
_oldLoc = smoothedLocation ?? newLoc;
_targetLoc = newLoc;
_oldHeading = smoothedHeading;
if (pos.speed > 0.5) {
_targetHeading = pos.heading;
} else {
_targetHeading = _oldHeading;
}
_animController?.forward(from: 0.0);
if (routeSteps.isNotEmpty) {
_checkNavigationStep(newLoc);
}
}
double _lerpAngle(double from, double to, double t) {
final double diff = ((to - from + 540.0) % 360.0) - 180.0;
return (from + diff * t + 360.0) % 360.0;
}
void _checkNavigationStep(LatLng pos) {
if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length) return;
final step = routeSteps[currentStepIndex];
final stepLoc = step['end_location'];
if (stepLoc == null) return;
final double stepLat = stepLoc['lat'];
final double stepLng = stepLoc['lng'];
final distance = geo.Geolocator.distanceBetween(
pos.latitude, pos.longitude, stepLat, stepLng);
distanceToNextStep = distance > 1000
? "${(distance / 1000).toStringAsFixed(1)} km"
: "${distance.toStringAsFixed(0)} m";
if (distance < 50 &&
!_nextInstructionSpoken &&
(currentStepIndex + 1) < routeSteps.length) {
final nextText =
routeSteps[currentStepIndex + 1]['html_instructions'] ?? "";
if (isTtsEnabled) {
Get.find<TextToSpeechController>().speakText(nextText);
}
_nextInstructionSpoken = true;
}
if (distance < 25) {
_advanceStep();
}
update();
}
IconData get currentManeuverIcon {
switch (currentManeuverModifier) {
case 4: // Arrive
return Icons.place_rounded;
case 6: // Roundabout
return Icons.roundabout_right_rounded;
case 2: // Right
return Icons.turn_right_rounded;
case 3: // Slight Right
return Icons.turn_slight_right_rounded;
case -2: // Left
return Icons.turn_left_rounded;
case -1: // Slight Left
return Icons.turn_slight_left_rounded;
case 7: // Keep Right
return Icons.turn_right_rounded;
case -7: // Keep Left
return Icons.turn_left_rounded;
case 0: // Straight
return Icons.straight_rounded;
default:
return Icons.straight_rounded;
}
}
void _advanceStep() {
currentStepIndex++;
if (currentStepIndex < routeSteps.length) {
currentInstruction =
routeSteps[currentStepIndex]['html_instructions'] ?? "";
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0;
_nextInstructionSpoken = false;
}
}
int parseDurationToInt(dynamic value) {
if (value == null) return 0;
String text = value.toString();