A brand-new route (or one with fewer than two pins) now falls back to the
ambient GPS position for its initial map center instead of (0, 0) in the
middle of the ocean, reusing FB-01's ambientPositionProvider. Routes with
one or more real waypoints are unaffected. Falls back to (0, 0) as before
when no ambient fix is available.
Adds three widget tests covering the fixed-fix, no-fix, and
real-waypoint-wins cases. Emulator verification was attempted but the
Android emulator became unresponsive partway through and was abandoned
per the ticket's conservative-use guidance; details in the ticket's
Outcome section.
Add an isClosedLoop toggle to route plans (schema v3 -> v4 migration),
threaded through the domain model, repository distance recomputation,
the overflow menu, and the drawn polyline, so a route can loop back to
its first waypoint.
routePlanProvider's AsyncLoading state collapsed with "route doesn't exist"
via .valueOrNull, so a brand-new route's screen briefly rendered "This route
no longer exists." with no FlutterMap in the tree before the DB stream's
first emission arrived. Add a hasValue guard mirroring waypointsAsync's
existing pattern, and replace the route planner's hardcoded initialZoom of
14 with FB-01's ambientZoom constant for street-level parity with the rest
of the app.
Replaces RoutePlannerScreen's AppBar-driven layout with a headerless,
full-opacity map canvas restyled toward Map HUD rather than copied from
this screen's own (drifted) Stitch export: a floating "tap to drop a pin"
tooltip before any pins exist, FloatingPill (Distance/Est. Time/Pins) once
they do, a "Start Route" floating button, a GlassPanel overflow menu
replacing the old app-bar actions (rename/delete/download), and a
pin-drop ripple micro-interaction. RoutesListScreen keeps its existing
list-with-+-button entry point (the IA question in the ticket's Risk
section, deferred as explicitly permitted) but restyles its rows as
GlassPanel cards.
"Start Route" gets real behavior instead of shipping as a dead button:
turn-by-turn route following doesn't exist yet (V3-09), so tapping it
starts an ordinary recording and switches to the Map tab -- useful today,
not a placeholder for a feature that isn't there. Est. Time honestly
shows "--" rather than fabricating an estimate the data model doesn't
back. Marching-ants route-line animation was dropped from scope: it isn't
in the acceptance criteria, and animating a dash phase under live map
pan/zoom is real complexity for a purely decorative effect.
Fixes a real, pre-existing bug found while working in this file: the
offline-tile download dialog still fetched from tile.openstreetmap.org
directly, a leftover from before UI-09 switched the live map to CARTO's
dark tiles -- a "successful" download would have cached tiles under keys
the dark-tile cache never reads from.
Also fixes a live Flutter framework warning the new GlassPanel-wrapped
ListTile surfaced (ink splashes need a Material ancestor, which
GlassPanel's DecoratedBox was blocking) and a real test bug (two pins
placed one directly under the new floating Start Route button in a
fixed test camera, causing a tap meant for the pin to hit the button
instead).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
Three pure modules: tile_math.dart (tilesForBounds/tilesAlongRoute, hard cap enforced by throwing), tile_cache.dart (FileTileCache with LRU eviction before any write that would exceed the cap), tile_downloader.dart (sequential, rate-limited, cooperatively cancellable, one bad tile doesn't abort the rest). CachedTileProvider wires the cache into flutter_map via a custom ImageProvider and now backs RideMap and RoutePlannerScreen's tile layers, so ordinary viewing write-throughs into the same capped cache.
RoutePlannerScreen gained a route-corridor download action (no rectangle-selection UI -- the ticket names the corridor as strictly better and V3-07 already exists to hang it off of), with a count/size confirmation before any request and a cancellable progress dialog. Fixed a real bug before shipping: a StatefulBuilder-based progress dialog would have started a new overlapping download subscription on every single progress tick; moved to a dedicated StatefulWidget that subscribes once in initState.
Marked partially done: aeroplane-mode verification on a real device is the ticket's own acceptance criterion and needs a phone this environment doesn't have.
Route plans are their own entity (RoutePlan/Waypoint, schema v3), never joined into ride totals or the rides list. RoutePlanRepository recomputes straight-line distance after every waypoint mutation. RoutePlannerScreen supports tap-to-add, tap-to-delete, and hand-rolled drag-to-move via MapCamera's screen/latlng conversions.
Fixed a real bug found by testing: FlutterMap's initialCenter/initialZoom are read once at construction, so building the map before the waypoints stream's first emission froze the camera on null-island. Also documents a genuine ten-minute test hang traced to Stream.first on a Drift watch() query inside testWidgets, which needs a pump-driven Timer that flutter_test's fake zone never advances on its own.