V3-07: route drawing (pins and straight lines)

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.
This commit is contained in:
2026-08-17 19:15:18 -05:00
parent 5013b7002f
commit a14f5b0134
15 changed files with 2694 additions and 5 deletions

View File

@@ -23,7 +23,7 @@ backup are v4 — see [../BACKLOG.md](../BACKLOG.md).
| [V3-04](V3-04-live-map.md) | Live map on the recording screen | M | — | Done |
| [V3-05](V3-05-mounted-mode.md) | Mounted (handlebar) mode | M | V3-04 | Done |
| [V3-06](V3-06-notification-stats.md) | Live stats in the notification | S | — | Done |
| [V3-07](V3-07-route-drawing.md) | Route drawing (pins, straight lines) | M | — | Not started |
| [V3-07](V3-07-route-drawing.md) | Route drawing (pins, straight lines) | M | — | Done |
| [V3-08](V3-08-road-routing.md) | Road-snapped routing and ETA | L | V3-07, V3-01 | Not started |
| [V3-09](V3-09-route-following.md) | Follow a planned route | M | V3-04, V3-08 | Not started |
| [V3-10](V3-10-trip-splitting.md) | Trip splitting | S | — | Not started |

View File

@@ -1,6 +1,6 @@
# V3-07 — Route drawing (pins and straight lines)
**Phase** Route planning · **Depends on** nothing · **Size** M · **Status** Not started
**Phase** Route planning · **Depends on** nothing · **Size** M · **Status** Done
## Goal
Drop pins on a map to sketch a route, see the straight-line distance, and save it. The
@@ -56,3 +56,51 @@ useful for a rough plan.
## Out of scope
Road snapping, ETA, following a route while riding. Import of existing GPX routes.
## Outcome
Shipped as designed, with one naming deviation and two real testing traps worth recording
for V3-08/V3-09.
**Named `RoutePlan`, not `Route`.** The ticket's own design sketch used `Route`, but that
collides with `dart:ui`/`package:flutter`'s own `Route<T>` (the navigator's page-transition
class) and with `go_router`'s `GoRoute`. Renaming up front avoided constant `hide`/`as`
import juggling across every file that touches both navigation and route plans.
Schema: `route_plans`/`waypoints` tables, schema version 2→3, following V3-01's migration
pattern exactly (`m.createTable` for brand-new tables needs no backfill, unlike V3-01's
`addColumn`). `RoutePlanRepository` mirrors `TripRepository`'s shape but has no state
machine — every mutating call ends by recomputing `distanceM` via `geo.pathLengthMeters`,
so "distance always matches the current waypoints" holds with no exceptions to remember,
including after a pure reorder that doesn't change it.
`RoutePlannerScreen`: tap-to-add via `MapOptions.onTap`, tap-a-pin-to-delete, and
drag-to-move implemented by hand against `MapCamera.latLngToScreenOffset`/
`screenOffsetToLatLng` (flutter_map has no built-in draggable-marker widget). The straight
line is dashed and uses the planning accent, visibly distinct from `RideMap`'s
speed-bucketed solid polyline, satisfying the acceptance criterion without a design pass.
**Real bug found by testing, not review:** the map's `initialCenter`/`initialZoom` are
read exactly once, at `FlutterMap` construction. Building the map before the waypoints
stream delivered its first value froze the camera on null-island permanently, even once
real waypoints arrived — invisible in manual testing (a route sketched from empty always
starts empty) but immediate in a test that opens a planner for a route with existing
waypoints. Fixed by gating the map behind the stream's first emission, and by switching
from a fixed-zoom guess to `CameraFit.bounds` (matching `RideMap`'s own established
pattern) so pins can't be culled off-camera either.
**Real testing trap, likely to recur in V3-08/V3-09:** `await db.watchRoutePlans().first`
inside a `testWidgets` body hung for a genuine ten minutes (the framework's own internal
timeout, not a guess) — a fresh `Stream.first` subscription on a Drift `.watch()` query
depends on a `Timer` inside Drift's stream-query store that flutter_test's fake test zone
never advances without an explicit pump. `repo`-level `Future`-returning calls
(`routePlanById`, etc.) have no such dependency and are what every other assertion in this
suite already used correctly. Documented inline in the test as a trap for the next ticket
that watches a stream from inside `testWidgets`.
16 new tests: 8 in `route_plan_repository_test.dart` (create, live distance on
add/move/delete, ordinal-gap closing, reordering, rename, cascade delete, and the
ticket-mandated "never appears in ride totals" check), 1 migration test (v2→v3, tables
created and usable, existing trip untouched), 7 in `route_planner_screen_test.dart`
(empty state, create-and-open, delete, tap-to-add-and-distance-updates, tap-to-delete,
rename, missing-route fallback), plus 1 in `widget_test.dart` for the Routes entry point
on the record screen. `flutter analyze` clean; full suite green (254 tests, up from 237).