Spike C: BRouter AIDL enrichment of a real komoot GPX #6

Open
opened 2026-08-31 17:14:35 +02:00 by robert · 1 comment
robert commented 2026-08-31 17:14:35 +02:00 (Migrated from git.butzei.de)

Goal

BRouter is the preferred offline cue-sheet backend. Confirm it can turn a komoot GPX (geometry only, no turn data) into usable turn instructions before Phase 3 depends on it.

Acceptance criteria

  • BRouter app plus segment files for the local region installed
  • IBRouterService bound successfully from a scratch Android app
  • A real komoot GPX export passed in as lonlats with a bike profile and turnInstructionMode set
  • Returned GPX inspected: voice hints (TL, TR, KR, ...) present and positioned at the correct junctions
  • Street names checked for availability
  • Behaviour recorded when segment files for the area are missing
  • Result written up: BRouter viable as primary, or fall back to map matching

Notes

AIDL definition: http://brouter.de/brouter/IBRouterService.aidl - docs at https://zod.github.io/brouter/developers/android_service.html . OsmAnd's BRouter adapter is a working reference implementation.

Update — 2026-09-01: BRouter is a router, not a map-matcher

This spike passed on "the returned GPX carries usable voice hints". That is necessary and nowhere
near sufficient, and 19 issues rest on it (D25).

Given via points, BRouter computes its own route between them, under its own profile. It does not
snap a track to OSM the way GraphHopper's or Valhalla's matching endpoints do. Three consequences:

  • Divergence. BRouter's line can differ from the imported line. The cue sheet then describes a
    route the rider is not on — the worst failure a navigation device has, because it is confidently
    wrong rather than obviously broken.
  • Profile refusal. komoot will happily route down a track or footpath that a bike profile
    declines. BRouter detours, silently, and the cues follow the detour.
  • Cost. A 100 km route at 5 m spacing is ~20 000 points. Whatever decimated subset becomes via
    points, BRouter routes once per leg under a running-time cap. NFR-P5 was filed as "needs a number";
    it is not a number problem, it is an unvalidated feasibility problem.

Added criteria:

  • Geometric divergence measured against the source GPX (discrete Fréchet or Hausdorff), on at
    least three real routes including one urban and one rural
  • Divergence rejection threshold of 20 m validated — beyond it, enrichment is discarded rather
    than shown
  • Via-point count / runtime / fidelity curve recorded for a 100 km route, so the decimation is
    chosen from evidence and NFR-P5 gets a real number
  • A route that deliberately uses a path the bike profile dislikes, to observe the refusal
    behaviour
  • BRouter's maxRunningTime / timeout behaviour on a long route recorded
  • Whether BRouter's reference-track mode is reachable over AIDL. If it is, that is the correct
    API and #27 changes shape entirely
  • Recommendation recorded: via-point decimation, or reference-track mode, or BRouter is not the
    right tool

The 20 m divergence check ships in BRouterEnricher regardless of what this spike finds. It is the
only thing between a bad enrichment and a rider being told to turn where there is no turn.

Labelled blocker. It gates twelve issues and was not marked.

Update — 2026-09-02: divergence is per leg, and the stakes are lower than they were

Per-leg rejection (D41). The 20 m threshold above reads as a whole-route check, and NFR-P8 stated
it as one. Applied that way, one bad 50 m stretch discards the cue sheet for a 100 km route and
drops the rider to the geometric heuristic for the entire ride — a worse outcome than the failure it
protects against.

  • Divergence measured per leg, between consecutive via points, not once across the route
  • Cues in legs within threshold are kept; cues in legs beyond it are dropped and the stretch
    marked unenriched, so the watch shows no turn rather than a wrong one
  • The failing-leg fraction beyond which an enrichment is rejected wholesale is established
    here — a route where most legs fail is not a good route with a few gaps

Lower stakes (D40, D42). Two things moved out from under this spike:

  • bikerouter.de is BRouter's own front end and exports GPX with turn instructions, so tier 0 (#63)
    reaches BRouter-quality cues with no BRouter install at all. This spike now decides the komoot
    path
    , not navigation as a whole.
  • Off-route guidance is a bearing arrow (#57), not a mid-ride re-route, so BRouter is no longer on
    the mid-ride critical path
    . Its blocked-by edges to the re-routing work can be dropped.

Keeps blocker: import-time enrichment for geometry-only routes still rests on it.

Update — 2026-09-03: criteria rewritten from measurement; several questions pre-answered

A server-side dry run of this spike's experiment against live BRouter 1.7.10 (D58–D62 in
docs/DECISIONS.md) answers several criteria in advance and gives the rest concrete numbers.

Struck from this issue (already answered):

  • Reference-track mode over AIDL — unreachable: rawTrackPath is set by the service, not
    the caller, and it is a recalculation cache, not matching. D25's open question is closed.
  • Street-name availability — no, in any format at any tier. One-line on-device confirmation
    only, not an investigation.
  • Whole-route 20 m validation — unachievable even in the ideal case (BRouter misses its own
    output by 27.8 m at 100 m via spacing). Replaced by the per-leg curve below.
  • "Gates twelve issues" — by reachability (D57) this issue gates #27, #64, #73 only. Still a
    risk blocker; no longer a schedule one.

Pass criteria (concrete):

  • Bind with setClassName("btools.routingapp","btools.routingapp.BRouterService") after adding
    <queries><package android:name="btools.routingapp"/></queries> — without it, bindService
    returns false indistinguishably from "not installed"
  • Call with lonlats, profile=trekking, timode=9, acceptCompressedResult=true,
    pathToFileResult=null, engineMode=0, explicit maxRunningTime
  • Per-leg fidelity: symmetric discrete Fréchet distance, per-leg ENU frame, per-leg
    cos(lat₀). Keep threshold 20 m. Wholesale rejection: >10% legs failing OR contiguous failing
    run > 500 m. Via spacing ≤ 100 m (2% failing legs adversarial; 250 m gives 20%)
  • Test correctMisplacedViaPoints (via extraParams) and named via points
    (lon,lat,"name") — the two levers against divergence
  • Longest fixture returns through Binder without TransactionTooLargeException; byte size
    recorded
  • Runtime: 100 km enriched on-device, cold cache; target ≤ 60 s wall clock (NFR-P5)
  • Clear <baseDir>/brouter/modes/*_rawtrack.dat between runs — the shared per-profile raw-track
    cache makes consecutive runs non-independent
  • Use the GitHub AIDL (abrensch/brouter brouter-routing-app/.../IBRouterService.aidl), not
    the brouter.de/brouter URL in the old Notes — that interface has no timode/profile and
    would produce cues silently absent

Fixtures required (the real blocker; #38 needs them anyway):

  1. ≥3 real komoot exports from one unlocked region: urban, rural, and one komoot routed over a
    surface a bike profile dislikes
  2. One komoot export ≥ 100 km (runtime + Binder size)
  3. One out-and-back or figure-eight (cue-to-index alignment ambiguity)
  4. bikerouter.de per-encoding fixtures — ti_1.gpx…ti_9.gpx already generated from live
    BRouter 1.7.10 and ready to commit as tier-0 fixtures
  5. One RideWithGPS export (needs an account — ask Robert) + its public .json as ground truth
  6. One Garmin Connect course export, GPX and TCX — currently unverified for FR-N15
  7. A route outside downloaded segment tiles (missing-coverage behaviour)

Verdict from the dry run: with the cuts above this is one focused day. Blocked on fixtures 1–3,
not on engineering.

## Goal BRouter is the preferred offline cue-sheet backend. Confirm it can turn a komoot GPX (geometry only, no turn data) into usable turn instructions before Phase 3 depends on it. ## Acceptance criteria - [ ] BRouter app plus segment files for the local region installed - [ ] `IBRouterService` bound successfully from a scratch Android app - [ ] A real komoot GPX export passed in as `lonlats` with a bike profile and `turnInstructionMode` set - [ ] Returned GPX inspected: voice hints (`TL`, `TR`, `KR`, ...) present and positioned at the correct junctions - [ ] Street names checked for availability - [ ] Behaviour recorded when segment files for the area are missing - [ ] Result written up: BRouter viable as primary, or fall back to map matching ## Notes AIDL definition: http://brouter.de/brouter/IBRouterService.aidl - docs at https://zod.github.io/brouter/developers/android_service.html . OsmAnd's BRouter adapter is a working reference implementation. ## Update — 2026-09-01: BRouter is a router, not a map-matcher This spike passed on "the returned GPX carries usable voice hints". That is necessary and nowhere near sufficient, and 19 issues rest on it (D25). Given via points, BRouter computes **its own route between them, under its own profile**. It does not snap a track to OSM the way GraphHopper's or Valhalla's matching endpoints do. Three consequences: - **Divergence.** BRouter's line can differ from the imported line. The cue sheet then describes a route the rider is not on — the worst failure a navigation device has, because it is confidently wrong rather than obviously broken. - **Profile refusal.** komoot will happily route down a track or footpath that a bike profile declines. BRouter detours, silently, and the cues follow the detour. - **Cost.** A 100 km route at 5 m spacing is ~20 000 points. Whatever decimated subset becomes via points, BRouter routes once per leg under a running-time cap. NFR-P5 was filed as "needs a number"; it is not a number problem, it is an unvalidated feasibility problem. Added criteria: - [ ] **Geometric divergence measured** against the source GPX (discrete Fréchet or Hausdorff), on at least three real routes including one urban and one rural - [ ] Divergence **rejection threshold of 20 m** validated — beyond it, enrichment is discarded rather than shown - [ ] **Via-point count / runtime / fidelity curve** recorded for a 100 km route, so the decimation is chosen from evidence and NFR-P5 gets a real number - [ ] A route that deliberately uses a path the bike profile dislikes, to observe the refusal behaviour - [ ] BRouter's `maxRunningTime` / timeout behaviour on a long route recorded - [ ] **Whether BRouter's reference-track mode is reachable over AIDL.** If it is, that is the correct API and #27 changes shape entirely - [ ] Recommendation recorded: via-point decimation, or reference-track mode, or BRouter is not the right tool The 20 m divergence check ships in `BRouterEnricher` regardless of what this spike finds. It is the only thing between a bad enrichment and a rider being told to turn where there is no turn. **Labelled `blocker`.** It gates twelve issues and was not marked. ## Update — 2026-09-02: divergence is per leg, and the stakes are lower than they were **Per-leg rejection (D41).** The 20 m threshold above reads as a whole-route check, and NFR-P8 stated it as one. Applied that way, **one bad 50 m stretch discards the cue sheet for a 100 km route** and drops the rider to the geometric heuristic for the entire ride — a worse outcome than the failure it protects against. - [ ] Divergence measured **per leg**, between consecutive via points, not once across the route - [ ] Cues in legs within threshold are kept; cues in legs beyond it are dropped and the stretch marked unenriched, so the watch shows no turn rather than a wrong one - [ ] The **failing-leg fraction** beyond which an enrichment is rejected wholesale is established here — a route where most legs fail is not a good route with a few gaps **Lower stakes (D40, D42).** Two things moved out from under this spike: - bikerouter.de is BRouter's own front end and exports GPX with turn instructions, so tier 0 (#63) reaches BRouter-quality cues with no BRouter install at all. This spike now decides the **komoot path**, not navigation as a whole. - Off-route guidance is a bearing arrow (#57), not a mid-ride re-route, so **BRouter is no longer on the mid-ride critical path**. Its blocked-by edges to the re-routing work can be dropped. Keeps `blocker`: import-time enrichment for geometry-only routes still rests on it. ## Update — 2026-09-03: criteria rewritten from measurement; several questions pre-answered A server-side dry run of this spike's experiment against live BRouter 1.7.10 (D58–D62 in docs/DECISIONS.md) answers several criteria in advance and gives the rest concrete numbers. **Struck from this issue (already answered):** - ~~Reference-track mode over AIDL~~ — **unreachable**: `rawTrackPath` is set by the service, not the caller, and it is a recalculation cache, not matching. D25's open question is closed. - ~~Street-name availability~~ — **no**, in any format at any tier. One-line on-device confirmation only, not an investigation. - ~~Whole-route 20 m validation~~ — **unachievable even in the ideal case** (BRouter misses its own output by 27.8 m at 100 m via spacing). Replaced by the per-leg curve below. - ~~"Gates twelve issues"~~ — by reachability (D57) this issue gates **#27, #64, #73** only. Still a risk blocker; no longer a schedule one. **Pass criteria (concrete):** - [ ] Bind with `setClassName("btools.routingapp","btools.routingapp.BRouterService")` after adding `<queries><package android:name="btools.routingapp"/></queries>` — without it, `bindService` returns false indistinguishably from "not installed" - [ ] Call with `lonlats`, `profile=trekking`, `timode=9`, `acceptCompressedResult=true`, `pathToFileResult=null`, `engineMode=0`, explicit `maxRunningTime` - [ ] **Per-leg fidelity**: symmetric discrete Fréchet distance, per-leg ENU frame, per-leg `cos(lat₀)`. Keep threshold 20 m. Wholesale rejection: >10% legs failing OR contiguous failing run > 500 m. Via spacing ≤ 100 m (2% failing legs adversarial; 250 m gives 20%) - [ ] Test `correctMisplacedViaPoints` (via `extraParams`) and **named** via points (`lon,lat,"name"`) — the two levers against divergence - [ ] Longest fixture returns through Binder without `TransactionTooLargeException`; byte size recorded - [ ] Runtime: 100 km enriched on-device, cold cache; target ≤ 60 s wall clock (NFR-P5) - [ ] Clear `<baseDir>/brouter/modes/*_rawtrack.dat` between runs — the shared per-profile raw-track cache makes consecutive runs non-independent - [ ] Use the **GitHub AIDL** (abrensch/brouter `brouter-routing-app/.../IBRouterService.aidl`), not the brouter.de/brouter URL in the old Notes — that interface has no `timode`/`profile` and would produce cues silently absent **Fixtures required (the real blocker; #38 needs them anyway):** 1. ≥3 real komoot exports from one unlocked region: urban, rural, and one komoot routed over a surface a bike profile dislikes 2. One komoot export ≥ 100 km (runtime + Binder size) 3. One out-and-back or figure-eight (cue-to-index alignment ambiguity) 4. bikerouter.de per-encoding fixtures — `ti_1.gpx`…`ti_9.gpx` already generated from live BRouter 1.7.10 and ready to commit as tier-0 fixtures 5. One RideWithGPS export (needs an account — ask Robert) + its public `.json` as ground truth 6. One Garmin Connect course export, GPX and TCX — currently unverified for FR-N15 7. A route outside downloaded segment tiles (missing-coverage behaviour) **Verdict from the dry run:** with the cuts above this is one focused day. Blocked on fixtures 1–3, not on engineering.
Owner

PR #95 (merged) supplies one of this spike's named fixture requirements: a real komoot export >= 100 km (fixture list item 2, ~119km). Fixture list items 1 (>=3 real exports: urban/rural/profile-disliked surface), 3 (out-and-back or figure-eight), 5 (RideWithGPS export + ground truth), 6 (Garmin Connect export), and 7 (route outside segment coverage) are all still needed \u2014 and the spike itself still requires actually binding IBRouterService on a real device with BRouter + segment files installed, which cannot happen in this sandbox regardless of fixtures. Not closing.

PR #95 (merged) supplies one of this spike's named fixture requirements: a real komoot export >= 100 km (fixture list item 2, ~119km). Fixture list items 1 (>=3 real exports: urban/rural/profile-disliked surface), 3 (out-and-back or figure-eight), 5 (RideWithGPS export + ground truth), 6 (Garmin Connect export), and 7 (route outside segment coverage) are all still needed \u2014 and the spike itself still requires actually binding IBRouterService on a real device with BRouter + segment files installed, which cannot happen in this sandbox regardless of fixtures. Not closing.
Sign in to join this conversation.
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
robert/PedalPebble#6
No description provided.