Files
rippr/docs/v3/V3-08-road-routing.md
Dylan 342a8f8382 Break v3 into 16 tickets
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>
2026-08-17 10:41:35 -05:00

3.4 KiB

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.