stopNavigation method
Implementation
Future<void> stopNavigation() async {
_ticker?.stop();
_ticker?.dispose();
_ticker = null;
_gpsAnimationTicker?.stop();
_gpsAnimationTicker?.dispose();
_gpsAnimationTicker = null;
_compassSubscription?.cancel();
_locationTracker.stopTracking();
_locationTracker.currentLocation.removeListener(_onGpsLocationUpdated);
_flutterTts.stop();
_smoothedGps = null;
_nearDestSince = null;
_hasTriggeredArrival = false;
_isCameraTransitioning = false;
_isReturningToVehicle = false;
_lastSegmentIndex = 0; // D+1: Reset
_lastGpsUpdateTime = null; // D+2: Reset
_playedDistanceAnnouncements.clear(); // D+3: Reset
_inTurnApproach = false;
_speedLimitService.reset();
_unregisterLifecycleObserver();
stateNotifier.value = null;
if (_vehicleSymbolAdded) {
try {
_controller.mapController.removeLayer("vehicle-hitbox-layer");
_controller.mapController.removeLayer("vehicle-layer");
_controller.mapController.removeSource("vehicle-source");
} catch (e) {
// Ignored if layer or source is already removed
}
_vehicleSymbolAdded = false;
}
// C5: Remove traveled route layer and off-route connector layer
if (_traveledRouteAdded) {
try {
_controller.mapController.removeLayer('traveled-route-layer');
_controller.mapController.removeSource('traveled-route-source');
} catch (_) {}
_traveledRouteAdded = false;
_traveledPoints.clear();
}
if (_offRouteConnectorAdded) {
try {
_controller.mapController.removeLayer('offroute-connector-layer');
_controller.mapController.removeSource('offroute-connector-source');
} catch (_) {}
_offRouteConnectorAdded = false;
}
// Clean up active navigation turn guidance arrows
try {
await _controller.routing.disposeStepTurnArrows();
} catch (_) {}
// Restore user location marker (Ghost Marker prevention)
_controller.showUserLocation(true);
// Unregister tap listener
_controller.mapController.onFeatureTapped.remove(_handleFeatureTapped);
// Restore camera and Original Route
final routeToRestore = _originalRoute;
_originalRoute = null; // Clear before use to allow fresh navigation
if (routeToRestore != null && routeToRestore.geometry.isNotEmpty) {
// Restore route line visually
_controller.routing.drawSavedRoute(routeToRestore.geometry);
// 2-step smooth exit animation:
// Step 1 — instantly flatten map to 2D at current vehicle position.
// MUST be instant: MapLibre calculates wrong viewport if tilt > 0.
final currentCam = _controller.mapController.cameraPosition;
if (currentCam != null) {
_controller.mapController.moveCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: currentCam.target,
zoom: currentCam.zoom,
tilt: 0.0,
bearing: 0.0,
),
),
);
}
// Step 2 — smooth 800ms animate back to pre-navigation overview.
if (routeToRestore.geometry.length >= 2) {
double minLat = routeToRestore.geometry.first.latitude;
double maxLat = minLat;
double minLng = routeToRestore.geometry.first.longitude;
double maxLng = minLng;
for (final p in routeToRestore.geometry) {
if (p.latitude < minLat) minLat = p.latitude;
if (p.latitude > maxLat) maxLat = p.latitude;
if (p.longitude < minLng) minLng = p.longitude;
if (p.longitude > maxLng) maxLng = p.longitude;
}
await Future.delayed(const Duration(milliseconds: 16));
_controller.mapController.animateCamera(
CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
),
left: 50, right: 50, top: 80, bottom: 200,
),
duration: const Duration(milliseconds: 800),
);
} else {
final restoreTarget = _savedCenter ?? routeToRestore.geometry.first;
final restoreZoom = _savedZoom ?? 14.0;
await Future.delayed(const Duration(milliseconds: 16)); // one frame gap
_controller.mapController.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: restoreTarget,
zoom: restoreZoom,
tilt: 0.0,
bearing: 0.0,
),
),
duration: const Duration(milliseconds: 800),
);
}
} else {
// Fallback: just flatten the tilt
_controller.mapController.animateCamera(CameraUpdate.tiltTo(0.0));
}
_savedCenter = null;
_savedZoom = null;
}