fix marker rendering & modernize riding widgets for dark mode - 2026-04-11

This commit is contained in:
Hamza-Ayed
2026-04-11 01:14:09 +03:00
parent 3f03f25142
commit 454276d1e0
88 changed files with 50376 additions and 23310 deletions

View File

@@ -16,8 +16,12 @@ import '../../../controller/functions/tts.dart';
import '../../../controller/home/decode_polyline_isolate.dart';
import '../../../main.dart';
import '../../../print.dart';
import 'dart:ui';
class NavigationController extends GetxController {
import '../../../services/offline_map_service.dart';
class NavigationController extends GetxController
with GetSingleTickerProviderStateMixin {
// ==========================================================================
// ── Tunables ──────────────────────────────────────────────────────────────
// ==========================================================================
@@ -57,11 +61,17 @@ class NavigationController extends GetxController {
/// Updated every tick via angle-aware lerp to eliminate snap/jitter.
double _smoothedHeading = 0.0;
// Animation for smooth tracking
AnimationController? _animController;
LatLng? _oldLoc;
LatLng? _targetLoc;
double currentSpeed = 0.0; // km/h
double totalDistance = 0.0; // metres accumulated this session
// MapLibre objects
Symbol? carSymbol;
Symbol? originSymbol;
Symbol? destinationSymbol;
Line? remainingRouteLine;
Line? traveledRouteLine;
@@ -155,14 +165,31 @@ class NavigationController extends GetxController {
@override
void onInit() {
super.onInit();
_animController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
_animController!.addListener(() {
if (_oldLoc != null && _targetLoc != null && _mapReady) {
final t = _animController!.value;
final lat = lerpDouble(_oldLoc!.latitude, _targetLoc!.latitude, t)!;
final lng = lerpDouble(_oldLoc!.longitude, _targetLoc!.longitude, t)!;
myLocation = LatLng(lat, lng);
if (isStyleLoaded) {
_updateCarMarker();
if (_fullRouteCoordinates.isNotEmpty && _cameraLockedToUser) {
animateCameraToPosition(myLocation!,
bearing: _smoothedHeading,
zoom: _targetZoom,
tilt: _targetTilt);
}
}
}
});
_initialize();
}
Future<void> _initialize() async {
await _getCurrentLocationAndStartUpdates();
if (!Get.isRegistered<TextToSpeechController>()) {
Get.put(TextToSpeechController());
}
}
@override
@@ -171,6 +198,7 @@ class NavigationController extends GetxController {
_recordTimer?.cancel();
_uploadBatchTimer?.cancel();
_debounce?.cancel();
_animController?.dispose();
mapController?.dispose();
placeDestinationController.dispose();
@@ -294,7 +322,10 @@ class NavigationController extends GetxController {
}
_lastDistanceLocation = newLoc;
myLocation = newLoc;
_oldLoc = myLocation ?? newLoc;
_targetLoc = newLoc;
_animController?.forward(from: 0.0);
_lastProcessedLocation = newLoc;
heading = position.heading;
@@ -306,23 +337,22 @@ class NavigationController extends GetxController {
currentSpeed = position.speed * 3.6;
if (isStyleLoaded) _updateCarMarker();
// Initial visual update if map is fresh
if (isStyleLoaded && myLocation == null) _updateCarMarker();
if (_fullRouteCoordinates.isNotEmpty) {
if (_cameraLockedToUser) {
animateCameraToPosition(myLocation!,
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
}
_updateTraveledPolylineSmart(myLocation!);
_checkNavigationStep(myLocation!);
_updateTraveledPolylineSmart(newLoc);
_checkNavigationStep(newLoc);
_recomputeETA();
// ── Off-route auto-recalculate ─────────────────────────────────────
_checkOffRoute(myLocation!);
_checkOffRoute(newLoc);
}
update();
} catch (_) {}
} catch (e) {
Log.print("Error occurred: $e");
}
}
// ==========================================================================
@@ -498,14 +528,14 @@ class NavigationController extends GetxController {
geometry: myLocation,
iconImage: 'car_icon',
iconSize: 1.0,
iconRotate: _smoothedHeading, // ← use smoothed heading
iconRotate: _smoothedHeading,
));
} else {
mapController!.updateSymbol(
carSymbol!,
SymbolOptions(
geometry: myLocation,
iconRotate: _smoothedHeading, // ← use smoothed heading
iconRotate: _smoothedHeading,
),
);
}
@@ -532,6 +562,50 @@ class NavigationController extends GetxController {
);
}
/// Safe wrapper for animateCamera Bounds to prevent native std::domain_error crash on iOS.
Future<void> _safeAnimateCameraBounds(LatLngBounds? bounds,
{double left = 60,
double top = 60,
double right = 60,
double bottom = 60}) async {
if (bounds == null || mapController == null) return;
try {
// Ensure the coordinates are valid (at least a small span)
final latSpan =
(bounds.northeast.latitude - bounds.southwest.latitude).abs();
final lngSpan =
(bounds.northeast.longitude - bounds.southwest.longitude).abs();
if (latSpan < 0.0001 && lngSpan < 0.0001) {
Log.print(
'⚠️ _safeAnimateCameraBounds: Point-sized bounds, zooming to center.');
mapController
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 16));
return;
}
// Small delay for view stabilization
await Future.delayed(const Duration(milliseconds: 200));
await mapController?.animateCamera(
CameraUpdate.newLatLngBounds(
bounds,
left: left,
top: top,
right: right,
bottom: bottom,
),
);
} catch (e) {
Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED in Nav: $e');
try {
await mapController
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14));
} catch (_) {}
}
}
void onUserPanned() {
_cameraLockedToUser = false;
update();
@@ -625,38 +699,90 @@ class NavigationController extends GetxController {
// ==========================================================================
Future<void> getRoute(LatLng origin, LatLng destination) async {
// ── Routing Decision: Normal Points -> SaaS, Multi-Stop -> OSRM ──
// Note: NavigationController usually handles the active trip (normal points).
final Map<String, String> queryParams = {
'fromLat': origin.latitude.toString(),
'fromLng': origin.longitude.toString(),
'toLat': destination.latitude.toString(),
'toLng': destination.longitude.toString(),
};
final saasUri =
Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
// Fallback OSRM URL
final coords = "${origin.longitude},${origin.latitude};"
"${destination.longitude},${destination.latitude}";
final url =
final osrmUrl =
"$_routeApiBaseUrl/$coords?steps=true&overview=full&geometries=polyline";
try {
final response = await http.get(Uri.parse(url));
// 1. Try SaaS first
http.Response response = await http.get(saasUri, headers: {
'x-api-key': 'intaleq_secret_2026',
});
bool useSaaS = response.statusCode == 200;
if (!useSaaS) {
Log.print("⚠️ SaaS Route failed. Falling back to OSRM...");
response = await http.get(Uri.parse(osrmUrl));
}
if (response.statusCode != 200) {
mySnackbarWarning('تعذر الاتصال بخدمة التوجيه.');
return;
}
final data = jsonDecode(response.body);
if (data['code'] != 'Ok' || (data['routes'] as List).isEmpty) {
final bool isSaaS = useSaaS;
// ── 2. Data Extraction Logic ──────────────────────────────────
String pointsString = "";
dynamic mainRoute;
if (isSaaS) {
pointsString = data['points']?.toString() ?? "";
mainRoute = data; // SaaS structure is top-level
} else {
if (data['code'] != 'Ok' || (data['routes'] as List).isEmpty) {
mySnackbarWarning('لم يتم العثور على مسار.');
return;
}
mainRoute = data['routes'][0];
pointsString = mainRoute['geometry']?.toString() ?? "";
}
if (pointsString.isEmpty) {
mySnackbarWarning('لم يتم العثور على مسار.');
return;
}
final route = data['routes'][0];
_fullRouteCoordinates = await compute<String, List<LatLng>>(
decodePolylineIsolate, route['geometry'].toString());
decodePolylineIsolate, pointsString);
_lastTraveledIndexInFullRoute = 0;
if (isStyleLoaded) _updatePolylinesSets([], _fullRouteCoordinates);
final legs = route['legs'] as List;
if (legs.isNotEmpty) {
routeSteps = List<Map<String, dynamic>>.from(legs[0]['steps'] as List);
// ── Offline Cache: Ensure destination area is stored in memory/disk ───
if (_fullRouteCoordinates.isNotEmpty) {
OfflineMapService.instance
.downloadRegion(_fullRouteCoordinates.last, radiusKm: 2.0);
}
// Handle legs/steps & totals
final legs = mainRoute['legs'] as List?;
if (legs != null && legs.isNotEmpty) {
routeSteps = List<Map<String, dynamic>>.from(legs[0]['steps'] as List);
_routeTotalDistanceM = (legs[0]['distance'] as num).toDouble();
_routeTotalDurationS = (legs[0]['duration'] as num).toDouble();
} else {
// Fallback for SaaS which might have top-level distance/duration
routeSteps = [];
_routeTotalDistanceM = (mainRoute['distance'] as num).toDouble();
_routeTotalDurationS = (mainRoute['duration'] as num).toDouble();
}
if (_routeTotalDistanceM > 0) {
totalDistanceRemaining = _routeTotalDistanceM > 1000
? "${(_routeTotalDistanceM / 1000).toStringAsFixed(1)} كم"
: "${_routeTotalDistanceM.toStringAsFixed(0)} م";
@@ -665,8 +791,6 @@ class NavigationController extends GetxController {
estimatedTimeRemaining = minutes > 60
? "${(minutes / 60).floor()} س ${minutes % 60} د"
: "$minutes د";
} else {
routeSteps = [];
}
for (final step in routeSteps) {
@@ -689,20 +813,18 @@ class NavigationController extends GetxController {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
// ── 5. Camera Update (Safe) ───────────────────────────────────
if (_fullRouteCoordinates.length >= 2) {
final bounds = _boundsFromLatLngList(_fullRouteCoordinates);
final bounds =
data['bbox'] != null && (data['bbox'] as List).length == 4
? LatLngBounds(
southwest: LatLng(data['bbox'][1], data['bbox'][0]),
northeast: LatLng(data['bbox'][3], data['bbox'][2]),
)
: _boundsFromLatLngList(_fullRouteCoordinates);
final latDiff =
(bounds.northeast.latitude - bounds.southwest.latitude).abs();
final lngDiff =
(bounds.northeast.longitude - bounds.southwest.longitude).abs();
if (latDiff > 0.0001 || lngDiff > 0.0001) {
mapController?.animateCamera(CameraUpdate.newLatLngBounds(bounds,
bottom: 220, top: 150, left: 50, right: 50));
} else {
animateCameraToPosition(_fullRouteCoordinates.first, zoom: 15.0);
}
await _safeAnimateCameraBounds(bounds,
bottom: 220, top: 150, left: 50, right: 50);
}
update();
@@ -741,6 +863,7 @@ class NavigationController extends GetxController {
await clearRoute(isNewRoute: true);
if (isStyleLoaded && mapController != null) {
// Destination Marker (B)
destinationSymbol = await mapController!.addSymbol(SymbolOptions(
geometry: destination,
iconImage: 'dest_icon',
@@ -748,6 +871,15 @@ class NavigationController extends GetxController {
textField: infoWindowTitle,
textOffset: const Offset(0, 2),
));
// Start Marker (A)
if (myLocation != null) {
originSymbol = await mapController!.addSymbol(SymbolOptions(
geometry: myLocation,
iconImage: 'start_icon',
iconSize: 1.0,
));
}
}
if (myLocation != null) await getRoute(myLocation!, destination);
@@ -777,6 +909,10 @@ class NavigationController extends GetxController {
await mapController!.removeSymbol(destinationSymbol!);
destinationSymbol = null;
}
if (originSymbol != null && mapController != null) {
await mapController!.removeSymbol(originSymbol!);
originSymbol = null;
}
if (remainingRouteLine != null && mapController != null) {
await mapController!.removeLine(remainingRouteLine!);
remainingRouteLine = null;
@@ -807,8 +943,11 @@ class NavigationController extends GetxController {
Future<void> _loadCustomIcons() async {
if (mapController == null) return;
final carBytes = await rootBundle.load('assets/images/car.png');
final startBytes = await rootBundle.load('assets/images/A.png');
final destBytes = await rootBundle.load('assets/images/b.png');
await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
await mapController!.addImage('start_icon', startBytes.buffer.asUint8List());
await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
}