@@ -8,10 +8,12 @@ import 'package:flutter/material.dart';
88import 'package:flutter_riverpod/flutter_riverpod.dart' ;
99import 'package:geolocator/geolocator.dart' ;
1010import 'package:maplibre_gl/maplibre_gl.dart' hide UserLocation;
11+ import 'package:maplibre_gl/maplibre_gl.dart' as ml show UserLocation;
1112
1213import '../l10n/generated/app_localizations.dart' ;
1314import '../models/location.dart' ;
1415import '../models/route_state.dart' ;
16+ import '../navigation/camera_controller.dart' ;
1517import '../navigation/location_converter.dart' ;
1618import '../navigation/nav_constants.dart' ;
1719import '../providers/brush_provider.dart' ;
@@ -22,6 +24,7 @@ import '../providers/location_provider.dart';
2224import '../providers/map_bearing_provider.dart' ;
2325import '../providers/rating_overlay_provider.dart' ;
2426import '../providers/route_provider.dart' ;
27+ import '../providers/user_location_provider.dart' ;
2528import '../services/brush_overlay.dart' ;
2629import '../services/error_reporter.dart' ;
2730import '../services/haptics.dart' ;
@@ -69,21 +72,9 @@ class _MapScreenState extends ConsumerState<MapScreen> {
6972 final l10n = AppLocalizations .of (context)! ;
7073 final notifier = ref.read (routeControllerProvider.notifier);
7174 if (ref.read (routeControllerProvider).origin == null ) {
72- Position ? pos;
73- try {
74- pos = await Geolocator .getLastKnownPosition () ??
75- await Geolocator .getCurrentPosition ();
76- } catch (_) {}
75+ final origin = await _resolveCurrentOriginLocation (l10n);
7776 if (! mounted) return ;
78- notifier.setOrigin (
79- Location (
80- id: 'gps' ,
81- name: l10n.locationCurrent,
82- label: l10n.locationCurrent,
83- lng: pos? .longitude ?? 13.4533 ,
84- lat: pos? .latitude ?? 52.5065 ,
85- ),
86- );
77+ notifier.setOrigin (origin);
8778 }
8879 notifier.setDestination (
8980 Location (
@@ -194,19 +185,9 @@ class _MapScreenState extends ConsumerState<MapScreen> {
194185 final l10n = AppLocalizations .of (context)! ;
195186 final notifier = ref.read (routeControllerProvider.notifier);
196187 if (ref.read (routeControllerProvider).origin == null ) {
197- Position ? pos;
198- try {
199- pos = await Geolocator .getLastKnownPosition () ??
200- await Geolocator .getCurrentPosition ();
201- } catch (_) {}
188+ final origin = await _resolveCurrentOriginLocation (l10n);
202189 if (! mounted) return ;
203- notifier.setOrigin (Location (
204- id: 'gps' ,
205- name: l10n.locationCurrent,
206- label: l10n.locationCurrent,
207- lng: pos? .longitude ?? 13.4533 ,
208- lat: pos? .latitude ?? 52.5065 ,
209- ));
190+ notifier.setOrigin (origin);
210191 }
211192 notifier.setDestination (Location (
212193 id: home.id,
@@ -223,14 +204,27 @@ class _MapScreenState extends ConsumerState<MapScreen> {
223204 await _homeMarker.update (controller, home);
224205 }
225206
226- Future <void > _handleBrowseLocationUpdate (double lat, double lng) async {
207+ /// Single entry point for MapLibre's user-location callback. Caches the
208+ /// fix in userLocationProvider (used to seed nav, pick route origins, and
209+ /// fall back recenter when ferrostar hasn't snapped yet), auto-centers in
210+ /// browse mode on first fix, and — edge case — promotes the camera into
211+ /// following mode if nav started before any fix was cached.
212+ Future <void > _onUserLocationUpdated (ml.UserLocation loc) async {
213+ final uloc = maplibreToUserLocation (loc);
214+ ref.read (userLocationProvider.notifier).state = uloc;
215+ if (ref.read (navigationSessionProvider)) {
216+ final cam = ref.read (navigationCameraControllerProvider);
217+ if (cam.mode == CameraMode .awaitingFirstFix) {
218+ await _activateFollowingCamera (uloc);
219+ }
220+ return ;
221+ }
227222 if (_browseAutocentered) return ;
228- if (ref.read (navigationSessionProvider)) return ;
229223 final controller = _mapController;
230224 if (controller == null ) return ;
231225 _browseAutocentered = true ;
232226 await controller.animateCamera (
233- CameraUpdate .newLatLngZoom (LatLng (lat, lng), 16 ),
227+ CameraUpdate .newLatLngZoom (LatLng (uloc. lat, uloc. lng), 16 ),
234228 );
235229 }
236230
@@ -244,13 +238,16 @@ class _MapScreenState extends ConsumerState<MapScreen> {
244238 }
245239 debugPrint ('nav: start ${origin .name } -> ${destination .name }' );
246240 final service = ref.read (navigationServiceProvider);
247- // Fetch last-known position synchronously (no GPS wait) so the controller
248- // has an initial fix and NavigationState emits immediately.
249- UserLocation ? initial;
250- try {
251- final pos = await Geolocator .getLastKnownPosition ();
252- if (pos != null ) initial = positionToUserLocation (pos);
253- } catch (_) {}
241+ // Prefer the live MapLibre fix (same source as the blue dot, written by
242+ // onUserLocationUpdated). Fall back to Geolocator's cache only if MapLibre
243+ // hasn't emitted yet — its cache can lag behind by 20-30m at cycling speed.
244+ UserLocation ? initial = ref.read (userLocationProvider);
245+ if (initial == null ) {
246+ try {
247+ final pos = await Geolocator .getLastKnownPosition ();
248+ if (pos != null ) initial = positionToUserLocation (pos);
249+ } catch (_) {}
250+ }
254251 try {
255252 await service.start (
256253 origin: WaypointInput (lat: origin.lat, lng: origin.lng),
@@ -259,6 +256,12 @@ class _MapScreenState extends ConsumerState<MapScreen> {
259256 initialLocation: initial,
260257 );
261258 if (mounted) _speakNav (AppLocalizations .of (context)! .navTtsDeparting);
259+ if (initial != null ) {
260+ // We already have a fix — skip awaitingFirstFix entirely. Without this
261+ // the camera waits for ferrostar to emit a `.navigating` state with
262+ // snapped_location, which won't happen until a stream tick arrives.
263+ await _activateFollowingCamera (initial);
264+ }
262265 } catch (e, st) {
263266 reportError (e, st, context: 'nav.start' );
264267 }
@@ -296,11 +299,15 @@ class _MapScreenState extends ConsumerState<MapScreen> {
296299 }
297300 }
298301
299- Future <void > _handleFirstFix (UserLocation loc) async {
300- debugPrint ('nav: first fix' );
301- AppHaptics .firstFix ();
302+ /// Transitions the camera into following mode for a nav session that has a
303+ /// known starting location. Idempotent: safe to call again on the first
304+ /// onUserLocationUpdated during nav as a fallback for the no-cache edge case.
305+ Future <void > _activateFollowingCamera (UserLocation loc) async {
302306 final cam = ref.read (navigationCameraControllerProvider);
303- cam.onFirstFix ();
307+ if (cam.mode != CameraMode .awaitingFirstFix) return ;
308+ debugPrint ('nav: activating following camera' );
309+ AppHaptics .firstFix ();
310+ cam.onNavStart ();
304311 final controller = _mapController;
305312 if (controller == null ) return ;
306313 // Enable tracking first so maplibre drives the camera target, then
@@ -340,12 +347,16 @@ class _MapScreenState extends ConsumerState<MapScreen> {
340347 Future <void > _handleRecenterTap () async {
341348 final controller = _mapController;
342349 if (controller == null ) return ;
350+ // Prefer ferrostar's snapped position so the camera lands on the route
351+ // line, not the raw fix. Fall back to the latest MapLibre fix when nav
352+ // hasn't produced a snapped state (e.g. before the first ferrostar tick).
343353 final snapped = ref.read (navigationStateProvider).value? .snappedLocation;
344- if (snapped == null ) return ;
354+ final loc = snapped ?? ref.read (userLocationProvider);
355+ if (loc == null ) return ;
345356 final cam = ref.read (navigationCameraControllerProvider);
346357 cam.onRecenterTapped ();
347358 await controller.animateCamera (CameraUpdate .newLatLngZoom (
348- LatLng (snapped .lat, snapped .lng), cam.followZoom));
359+ LatLng (loc .lat, loc .lng), cam.followZoom));
349360 if (! mounted) return ;
350361 await controller
351362 .updateMyLocationTrackingMode (MyLocationTrackingMode .trackingCompass);
@@ -359,11 +370,6 @@ class _MapScreenState extends ConsumerState<MapScreen> {
359370 final nextState = next.value;
360371 if (nextState == null ) return ;
361372
362- if (prevState? .snappedLocation == null &&
363- nextState.snappedLocation != null ) {
364- _handleFirstFix (nextState.snappedLocation! );
365- }
366-
367373 if (prevState? .status != TripStatus .complete &&
368374 nextState.status == TripStatus .complete) {
369375 _handleArrival ();
@@ -390,26 +396,60 @@ class _MapScreenState extends ConsumerState<MapScreen> {
390396 }
391397
392398 Future <void > _refreshPreviewFromGps () async {
393- Position ? pos;
394- try {
395- pos = await Geolocator .getLastKnownPosition () ??
396- await Geolocator .getCurrentPosition ();
397- } catch (e) {
398- debugPrint ('nav: refresh-preview GPS error: $e ' );
399+ final cached = ref.read (userLocationProvider);
400+ double ? lat = cached? .lat;
401+ double ? lng = cached? .lng;
402+ if (lat == null || lng == null ) {
403+ try {
404+ final pos = await Geolocator .getLastKnownPosition () ??
405+ await Geolocator .getCurrentPosition ();
406+ lat = pos.latitude;
407+ lng = pos.longitude;
408+ } catch (e) {
409+ debugPrint ('nav: refresh-preview GPS error: $e ' );
410+ }
399411 }
400- if (! mounted || pos == null ) return ;
412+ if (! mounted || lat == null || lng == null ) return ;
401413 final l10n = AppLocalizations .of (context)! ;
402414 ref.read (routeControllerProvider.notifier).setOrigin (
403415 Location (
404416 id: 'gps' ,
405417 name: l10n.locationCurrent,
406418 label: l10n.locationCurrent,
407- lat: pos.latitude ,
408- lng: pos.longitude ,
419+ lat: lat ,
420+ lng: lng ,
409421 ),
410422 );
411423 }
412424
425+ /// Resolves a "current location" Location for use as a route origin. Prefers
426+ /// the cached MapLibre fix (live, written from onUserLocationUpdated), then
427+ /// Geolocator's last-known/current, and finally a Berlin-center fallback.
428+ Future <Location > _resolveCurrentOriginLocation (AppLocalizations l10n) async {
429+ final cached = ref.read (userLocationProvider);
430+ if (cached != null ) {
431+ return Location (
432+ id: 'gps' ,
433+ name: l10n.locationCurrent,
434+ label: l10n.locationCurrent,
435+ lat: cached.lat,
436+ lng: cached.lng,
437+ );
438+ }
439+ Position ? pos;
440+ try {
441+ pos = await Geolocator .getLastKnownPosition () ??
442+ await Geolocator .getCurrentPosition ();
443+ } catch (_) {}
444+ return Location (
445+ id: 'gps' ,
446+ name: l10n.locationCurrent,
447+ label: l10n.locationCurrent,
448+ lat: pos? .latitude ?? 52.5065 ,
449+ lng: pos? .longitude ?? 13.4533 ,
450+ );
451+ }
452+
413453 Future <TapFeature ?> _probeRatingFeature (LatLng coords) async {
414454 final controller = _mapController;
415455 if (controller == null ) return null ;
@@ -613,8 +653,7 @@ class _MapScreenState extends ConsumerState<MapScreen> {
613653 onMapCreated: (controller) {
614654 _mapController = controller;
615655 },
616- onUserLocationUpdated: (loc) => _handleBrowseLocationUpdate (
617- loc.position.latitude, loc.position.longitude),
656+ onUserLocationUpdated: _onUserLocationUpdated,
618657 onStyleLoadedCallback: () async {
619658 // Attach rating overlay AFTER style is loaded — MapLibre
620659 // silently ignores addGeoJsonSource / addLayer calls
0 commit comments