From f30f0063d05d6dcd3ac75890913ed95d955ae8fe Mon Sep 17 00:00:00 2001 From: uhryniuk Date: Mon, 24 Aug 2026 16:44:09 -0500 Subject: [PATCH] FB-01: fix ambient GPS's first fix not correcting the initial zoom-2 Emulator verification caught a real bug the widget tests missed: the shell background map builds before any location fix has resolved, so ambientPosition starts null and the initial camera locks at zoom 2. The first fix to arrive only re-centered (preserving that stale zoom forever) instead of also jumping to ambientZoom. --- lib/src/ui/components/ride_map.dart | 13 ++++++++-- test/ride_map_test.dart | 39 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/src/ui/components/ride_map.dart b/lib/src/ui/components/ride_map.dart index 5be48c8..09381cc 100644 --- a/lib/src/ui/components/ride_map.dart +++ b/lib/src/ui/components/ride_map.dart @@ -153,10 +153,19 @@ class _RideMapState extends State with WidgetsBindingObserver { } else if (widget.ambientPosition != null && widget.ambientPosition != old.ambientPosition) { // FB-01: no recorded path yet -- chase the ambient GPS fix instead, the same way - // a recording is chased above. + // a recording is chased above. The *first* fix has to also correct the zoom, not + // just the center: the very first build (before any fix has arrived) has no + // ambient position yet either, so `bounds == null` locks the initial camera at + // zoom 2 -- without this, every later fix would re-center at that same + // never-corrected zoom 2 forever, since a plain re-center only ever preserves + // whatever zoom the map is already at. + final isFirstFix = old.ambientPosition == null; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || !_following) return; - _controller.move(widget.ambientPosition!, _controller.camera.zoom); + _controller.move( + widget.ambientPosition!, + isFirstFix ? ambientZoom : _controller.camera.zoom, + ); }); } } diff --git a/test/ride_map_test.dart b/test/ride_map_test.dart index 46b8769..f782005 100644 --- a/test/ride_map_test.dart +++ b/test/ride_map_test.dart @@ -250,6 +250,45 @@ void main() { expect(center.longitude, closeTo(fix2.longitude, 1e-9)); }); + testWidgets('the first ambient fix to arrive after construction corrects the ' + 'zoom, not just the center -- a plain re-center would otherwise leave the ' + 'map at the initial zoom-2 forever', (tester) async { + // Reproduces a real device bug found in emulator verification: the shell + // background map builds before any location permission/fix has resolved, so + // `ambientPosition` starts null and the initial camera locks at zoom 2 (the + // `bounds == null` fallback). The first fix to actually arrive must jump to + // `ambientZoom`, not just re-center at whatever zoom the map already happens + // to be sitting at. + const fix = ll.LatLng(51.0, -114.0); + + Widget build(ll.LatLng? ambient) => MaterialApp( + theme: ripprTheme(), + home: Scaffold( + body: RideMap( + points: const [], + segments: const [], + showEmptyLabel: false, + follow: true, + ambientPosition: ambient, + ), + ), + ); + + await tester.pumpWidget(build(null)); + await tester.pump(); + + var map = tester.widget(find.byType(FlutterMap)); + expect(map.mapController!.camera.zoom, 2); + + await tester.pumpWidget(build(fix)); + await tester.pump(); + + map = tester.widget(find.byType(FlutterMap)); + expect(map.mapController!.camera.zoom, ambientZoom); + expect(map.mapController!.camera.center.latitude, closeTo(fix.latitude, 1e-9)); + expect(map.mapController!.camera.center.longitude, closeTo(fix.longitude, 1e-9)); + }); + testWidgets('a manual pan cancels ambient following the same way it cancels ' 'recording-follow', (tester) async { const fix1 = ll.LatLng(51.0, -114.0);