Per request: V3-08's routing-engine direction is decided (self-hosted OSRM over a hosted API or the public demo instance) but standing one up is split into its own ticket, V3-17, since provisioning a server is different work from the app code that calls it. V3-08 and V3-09 are marked Deferred pending V3-17. V3-17 also names a real tension worth flagging rather than quietly ignoring: this backlog's own organizing principle is 'v3 is everything buildable with no server,' and a self-hosted OSRM instance is a server -- so V3-08/V3-09 sit oddly under docs/v3/ once V3-17 exists. Left unresolved by design (a documentation call for later), noted in both V3-17 and BACKLOG.md's v4 section rather than silently renumbering tickets that are already cross-referenced throughout the docs.
3.8 KiB
V3-08 — Road-snapped routing and ETA
Phase Route planning · Depends on V3-07, benefits from V3-01, blocked on V3-17 · Size L · Status Deferred
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.
Decided, not deferred: self-hosted OSRM (see the table below). What's actually
deferred is standing one up — that's V3-17, split out on
request because provisioning a server is a different kind of work from building the app
code that calls it, and this ticket cannot start until V3-17 has something to point
RoutingService at.
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
- Decide the engine. Write the decision and its reasoning into this file.
RoutingServiceinterface + implementation +FakeRoutingService- Resolve on pin change, debounced — not on every drag frame
- Persist geometry, distance and duration on
Route - Personal ETA from
Triphistory, once ≥5 rides of that activity exist - 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
FakeRoutingServicedrives 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.