2026-04-06 redesign splash screen and drawer menu

This commit is contained in:
Hamza-Ayed
2026-04-06 22:00:13 +03:00
parent 531c6d07ef
commit 45222d2887
8 changed files with 1112 additions and 310 deletions

View File

@@ -34,6 +34,12 @@ class NavigationController extends GetxController {
/// Minimum metres the device must move between general location ticks.
static const double _minMoveToProcess = 2.0;
/// Metres off-route before the auto-recalculate countdown starts.
static const double _offRouteThresholdM = 25.0;
/// Seconds the user must remain off-route before auto-recalculate fires.
static const int _offRouteTriggerSeconds = 6;
// ==========================================================================
// ── Map state ─────────────────────────────────────────────────────────────
// ==========================================================================
@@ -46,6 +52,11 @@ class NavigationController extends GetxController {
LatLng? myLocation;
double heading = 0.0;
/// Smoothed heading used for the car icon and camera bearing.
/// Updated every tick via angle-aware lerp to eliminate snap/jitter.
double _smoothedHeading = 0.0;
double currentSpeed = 0.0; // km/h
double totalDistance = 0.0; // metres accumulated this session
@@ -85,6 +96,19 @@ class NavigationController extends GetxController {
// Camera
bool isNavigating = false;
bool _cameraLockedToUser = true;
bool _mapReady = false; // true only after layout has settled
// ==========================================================================
// ── Off-route auto-recalculate ────────────────────────────────────────────
// ==========================================================================
/// Wall-clock time when the user first went more than [_offRouteThresholdM]
/// metres away from the nearest route point. Null means on-route.
DateTime? _offRouteStartTime;
/// True while an auto-recalculate triggered from off-route detection is in
/// progress — prevents a second trigger from firing.
bool _autoRecalcInProgress = false;
// ==========================================================================
// ── Batch location tracking ───────────────────────────────────────────────
@@ -167,14 +191,22 @@ class NavigationController extends GetxController {
isStyleLoaded = true;
await _loadCustomIcons();
if (myLocation != null) {
animateCameraToPosition(myLocation!);
_updateCarMarker();
}
if (_fullRouteCoordinates.isNotEmpty) {
_updatePolylinesSets([], _fullRouteCoordinates);
}
// Wait one full frame for the native MapLibre view to finish layout.
// Without this, ANY animateCamera call throws std::domain_error on iOS
// because the view still has zero pixel dimensions at this point.
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 300));
if (!_mapReady) {
_mapReady = true;
if (myLocation != null) {
animateCameraToPosition(myLocation!);
_updateCarMarker();
}
if (_fullRouteCoordinates.isNotEmpty) {
_updatePolylinesSets([], _fullRouteCoordinates);
}
}
});
}
Future<void> onMapLongPressed(Point<double> point, LatLng tappedPoint) async {
@@ -217,6 +249,7 @@ class NavigationController extends GetxController {
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
myLocation = LatLng(position.latitude, position.longitude);
_smoothedHeading = position.heading; // seed so first lerp is instant
update();
if (isStyleLoaded) animateCameraToPosition(myLocation!);
_startLocationTimer();
@@ -264,6 +297,13 @@ class NavigationController extends GetxController {
myLocation = newLoc;
_lastProcessedLocation = newLoc;
heading = position.heading;
// ── Smooth the heading with an angle-aware exponential lerp ──────────
// Factor 0.25 means ~75 % of the old angle is kept each tick, giving a
// ~4-tick (≈4 s) settling time — smooth enough to look fluid on screen
// while still reacting quickly to real turns.
_smoothedHeading = _lerpAngle(_smoothedHeading, heading, 0.25);
currentSpeed = position.speed * 3.6;
if (isStyleLoaded) _updateCarMarker();
@@ -271,17 +311,86 @@ class NavigationController extends GetxController {
if (_fullRouteCoordinates.isNotEmpty) {
if (_cameraLockedToUser) {
animateCameraToPosition(myLocation!,
bearing: heading, zoom: _targetZoom, tilt: _targetTilt);
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
}
_updateTraveledPolylineSmart(myLocation!);
_checkNavigationStep(myLocation!);
_recomputeETA();
// ── Off-route auto-recalculate ─────────────────────────────────────
_checkOffRoute(myLocation!);
}
update();
} catch (_) {}
}
// ==========================================================================
// ── Heading utilities ─────────────────────────────────────────────────────
// ==========================================================================
/// Lerps from [from] to [to] by factor [t], correctly handling the 0/360
/// wrap-around so we never spin the wrong way (e.g. 350° → 10° goes +20°,
/// not 340°).
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;
}
// ==========================================================================
// ── Off-route detection ───────────────────────────────────────────────────
// ==========================================================================
/// Called every tick while navigating. Measures the distance from [pos] to
/// the nearest upcoming route coordinate. If the driver stays more than
/// [_offRouteThresholdM] metres away for at least [_offRouteTriggerSeconds]
/// seconds, an automatic route recalculation is triggered.
void _checkOffRoute(LatLng pos) {
if (_autoRecalcInProgress || isLoading) return;
if (_fullRouteCoordinates.isEmpty) return;
// Search a window ahead of the last tracked index for the nearest point.
const int searchWindow = 80;
final int start = _lastTraveledIndexInFullRoute;
final int end = min(start + searchWindow, _fullRouteCoordinates.length);
double minDist = double.infinity;
for (int i = start; i < end; i++) {
final d = Geolocator.distanceBetween(
pos.latitude,
pos.longitude,
_fullRouteCoordinates[i].latitude,
_fullRouteCoordinates[i].longitude,
);
if (d < minDist) minDist = d;
}
if (minDist > _offRouteThresholdM) {
// Driver is off the route.
if (_offRouteStartTime == null) {
_offRouteStartTime = DateTime.now();
Log.print('⚠️ Off-route detected (${minDist.toStringAsFixed(0)} m). '
'Countdown started.');
} else {
final elapsed =
DateTime.now().difference(_offRouteStartTime!).inSeconds;
if (elapsed >= _offRouteTriggerSeconds) {
Log.print('🔄 Auto-recalculate triggered after ${elapsed}s '
'off-route (${minDist.toStringAsFixed(0)} m).');
_offRouteStartTime = null;
_autoRecalcInProgress = true;
recalculateRoute().then((_) => _autoRecalcInProgress = false);
}
}
} else {
// Back on (or close enough to) the route — reset the clock.
if (_offRouteStartTime != null) {
Log.print('✅ Back on route — off-route timer reset.');
}
_offRouteStartTime = null;
}
}
// ==========================================================================
// ── Batch tracking: record every 3 s, upload every 2 min ─────────────────
// ==========================================================================
@@ -389,12 +498,15 @@ class NavigationController extends GetxController {
geometry: myLocation,
iconImage: 'car_icon',
iconSize: 1.0,
iconRotate: heading,
iconRotate: _smoothedHeading, // ← use smoothed heading
));
} else {
mapController!.updateSymbol(
carSymbol!,
SymbolOptions(geometry: myLocation, iconRotate: heading),
SymbolOptions(
geometry: myLocation,
iconRotate: _smoothedHeading, // ← use smoothed heading
),
);
}
}
@@ -405,7 +517,10 @@ class NavigationController extends GetxController {
void animateCameraToPosition(LatLng position,
{double? zoom, double bearing = 0.0, double tilt = 0.0}) {
mapController?.animateCamera(
// Guard: skip if the native view is not ready yet
if (!_mapReady || mapController == null) return;
mapController!.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: position,
@@ -426,7 +541,9 @@ class NavigationController extends GetxController {
_cameraLockedToUser = true;
if (myLocation != null) {
animateCameraToPosition(myLocation!,
bearing: heading, zoom: _targetZoom, tilt: _targetTilt);
bearing: _smoothedHeading, // ← use smoothed heading
zoom: _targetZoom,
tilt: _targetTilt);
}
update();
}
@@ -561,6 +678,9 @@ class NavigationController extends GetxController {
isNavigating = true;
_cameraLockedToUser = true;
// Reset off-route state after a successful recalculation
_offRouteStartTime = null;
if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['instruction_text'];
nextInstruction = routeSteps.length > 1
@@ -569,10 +689,20 @@ class NavigationController extends GetxController {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
if (_fullRouteCoordinates.isNotEmpty) {
if (_fullRouteCoordinates.length >= 2) {
final bounds = _boundsFromLatLngList(_fullRouteCoordinates);
mapController?.animateCamera(CameraUpdate.newLatLngBounds(bounds,
bottom: 220, top: 150, left: 50, right: 50));
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);
}
}
update();
@@ -638,6 +768,10 @@ class NavigationController extends GetxController {
}
Future<void> clearRoute({bool isNewRoute = false}) async {
// Reset off-route state whenever the route is cleared
_offRouteStartTime = null;
_autoRecalcInProgress = false;
if (!isNewRoute) {
if (destinationSymbol != null && mapController != null) {
await mapController!.removeSymbol(destinationSymbol!);