One file per feature, in the v2 shape that worked: goal, context, design, implementation, acceptance criteria, tests, risks, out of scope -- written before implementing so the risks are on paper rather than walked into. Three carry more weight than their size suggests. V3-01 ships the port's first real migration, and since the destructive fallback is gone, getting addColumn and a v1-database test right matters more than the feature. V3-08 forces a routing-engine decision with ongoing cost, so it sits behind a RoutingService interface mirroring what LocationSource did for GPS. V3-13 is not code at all -- it answers the three questions open since v2, and V3-15 may close unbuilt as a result, which is a legitimate outcome. Several tickets record constraints that are easy to lose: no activity picker in front of Start, because the founding premise is press-and-go with gloves; the Route-to-Trip foreign key must not cascade, or deleting an old plan deletes the ride; V3-14 will deliberately break the parity harness by adding GPX <type>, and that expectation should be updated rather than the check dropped; and V3-16 must not regress the explicit text colours that exist because of the black-on-black bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
77 lines
3.4 KiB
Markdown
77 lines
3.4 KiB
Markdown
# V3-08 — Road-snapped routing and ETA
|
|
|
|
**Phase** Route planning · **Depends on** V3-07, benefits from V3-01 · **Size** L · **Status** Not started
|
|
|
|
## Goal
|
|
Resolve the actual shortest path along roads between pins, and estimate how long the ride
|
|
will take.
|
|
|
|
## Context
|
|
This is what Dylan asked for. It is also the ticket with a **decision that cannot be
|
|
deferred**: road-snapped routing needs a routing engine over OpenStreetMap data, and the
|
|
choice has ongoing consequences.
|
|
|
|
## Design
|
|
|
|
### Choosing an engine — decide before writing code
|
|
|
|
| Option | Trade-off |
|
|
|---|---|
|
|
| **Public OSRM demo** | Free, zero setup, **explicitly not for production**, rate-limited. Prototype only. |
|
|
| **Self-hosted OSRM** | Fast, well understood. Needs a machine and a regional OSM extract (a province is a few GB). |
|
|
| **GraphHopper** | Self-hostable, good cycling/motorcycle profiles, friendlier ETAs |
|
|
| **Valhalla** | Best multi-modal profiles, heaviest to run |
|
|
| **Mapbox / Google** | No ops, per-request billing, an API key shipped in the app |
|
|
|
|
**Profiles matter more here than usual.** A motorcycle route and a bicycle route between
|
|
the same pins genuinely differ — cycling engines avoid motorways, and a motorcyclist often
|
|
wants the twisty road rather than the fast one. This is where V3-01 pays off: the activity
|
|
selects the profile.
|
|
|
|
Put it behind a `RoutingService` interface with a fake, exactly as `LocationSource` did for
|
|
GPS. That seam is what made swapping the location engine cheap, and the same argument
|
|
applies here.
|
|
|
|
### ETA is a promise, and easy to get wrong
|
|
Engines estimate from posted speed limits. That is not how long *you* take. Once there is
|
|
history, the rider's own average moving speed for that activity — already stored on every
|
|
`Trip` — is a better predictor.
|
|
|
|
Show the engine's estimate, then replace it with a personal one once there is enough
|
|
history to justify it. Label which is which.
|
|
|
|
### Caching
|
|
Cache the returned polyline on `Route.geometry`. A saved plan must open offline and must
|
|
not re-bill a request every time it is viewed.
|
|
|
|
## Implementation
|
|
1. Decide the engine. Write the decision and its reasoning into this file.
|
|
2. `RoutingService` interface + implementation + `FakeRoutingService`
|
|
3. Resolve on pin change, debounced — not on every drag frame
|
|
4. Persist geometry, distance and duration on `Route`
|
|
5. Personal ETA from `Trip` history, once ≥5 rides of that activity exist
|
|
6. Graceful offline behaviour: fall back to straight lines and say so
|
|
|
|
## Acceptance criteria
|
|
- [ ] Pins resolve to a road-following polyline
|
|
- [ ] Distance reflects the road path, not the straight line
|
|
- [ ] Activity changes the profile and can change the route
|
|
- [ ] A saved route renders offline from cached geometry, with no network call
|
|
- [ ] Offline with no cache degrades to straight lines with a visible explanation
|
|
- [ ] No API key is committed to the repository
|
|
|
|
## Tests
|
|
- `FakeRoutingService` drives every path: success, failure, offline, empty
|
|
- Cached geometry means no second request — assert the fake is called once
|
|
- Personal ETA maths against fixed history
|
|
- **No live network calls in any test**
|
|
|
|
## Risks
|
|
- **Vendor lock-in and cost.** The interface is the mitigation.
|
|
- Debouncing matters: dragging a pin could otherwise fire dozens of requests.
|
|
- OSM route quality varies. It will occasionally suggest something daft; that is the data,
|
|
not a bug to chase.
|
|
|
|
## Out of scope
|
|
Turn-by-turn navigation and voice guidance. That is a different product.
|