Refresh the Flutter snapshot: v3 tickets V3-04 through V3-16, plus a fresh installable APK
V3-04 through V3-07, V3-10, V3-16 shipped complete; V3-11/V3-12/V3-14 shipped code-complete pending device/account verification; V3-08/V3-09 deferred behind a new V3-17 (self-hosted OSRM investigation). 316 tests passing, up from 221. The APK is a fresh release build (debug-signed, no release signing config exists yet) with two build fixes applied: core library desugaring enabled for flutter_local_notifications, and sentry_flutter bumped to 9.27.0 (8.14.2's bundled Kotlin plugin was incompatible with this project's Kotlin 2.4.0 toolchain).
This commit is contained in:
106
rippr-flutter-src/docs/v3/V3-07-route-drawing.md
Normal file
106
rippr-flutter-src/docs/v3/V3-07-route-drawing.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# V3-07 — Route drawing (pins and straight lines)
|
||||
|
||||
**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
|
||||
foundation for V3-08, deliberately shipped without a routing engine.
|
||||
|
||||
## Context
|
||||
Pre-ride planning is a **genuinely new mode**, not an extension of recording. It needs no
|
||||
ride in progress, no server, and nothing else in v3 — the most independent thing in the
|
||||
backlog.
|
||||
|
||||
Split from road-snapped routing (V3-08) on purpose: pins, storage, editing and the map
|
||||
interaction are all needed either way, and none of them require choosing a routing vendor.
|
||||
That decision should not block a usable feature.
|
||||
|
||||
## Design
|
||||
**New entities, separate from `Trip`.** A plan is not a recording, and conflating them
|
||||
would put unridden kilometres into ride totals.
|
||||
|
||||
```
|
||||
Route(id, name, createdAt, activity?, distanceM, estimatedMillis?, geometry?)
|
||||
Waypoint(id, routeId, ordinal, latitude, longitude, name?)
|
||||
```
|
||||
|
||||
`geometry` is null in this ticket; V3-08 fills it with the road-snapped polyline.
|
||||
`ordinal` rather than relying on insertion id, so waypoints can be reordered.
|
||||
|
||||
Straight-line distance reuses `haversineMeters` — already ported and parity-proven.
|
||||
|
||||
## Implementation
|
||||
1. Drift tables + **migration** (the second one; V3-01 proves the path)
|
||||
2. `RouteRepository` mirroring `TripRepository`'s shape
|
||||
3. `RoutePlannerScreen` — tap to add a pin, drag to move, tap a pin to delete, reorder
|
||||
4. Straight-line polyline between pins, visibly distinct from a recorded path
|
||||
5. Routes list, reachable from the record screen alongside Rides
|
||||
6. Name, rename, delete
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] Pins can be added, moved, reordered and deleted
|
||||
- [ ] Distance updates live as pins change
|
||||
- [ ] A saved route survives an app restart
|
||||
- [ ] Routes never appear in the rides list, and never contribute to ride totals
|
||||
- [ ] Deleting a route cascades to its waypoints
|
||||
|
||||
## Tests
|
||||
- Repository: create, reorder, delete, cascade — in-memory, like the trip tests
|
||||
- Distance matches `pathLengthMeters` over the same points
|
||||
- Widget: tapping the map adds a pin; the distance label updates
|
||||
- **A test asserting routes are absent from `watchCompletedTrips`**
|
||||
|
||||
## Risks
|
||||
The main one is scope drift into V3-08. Ship straight lines first; they are genuinely
|
||||
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).
|
||||
Reference in New Issue
Block a user