GeometryEnricher heading-delta fallback #29
Labels
No labels
area:companion
area:docs
area:shared
area:tooling
area:watchapp
blocker
kind:chore
kind:feature
kind:spike
kind:test
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Depends on
#27 BRouterEnricher via IBRouterService AIDL
robert/PedalPebble
#26 CueSheetEnricher interface and canonical direction enum
robert/PedalPebble
Reference
robert/PedalPebble#29
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
The always-available last resort: derive turns from the shape of the track alone.
Acceptance criteria
Files
companion/.../route/enrich/GeometryEnricher.ktUpdate — 2026-09-01: this is the default path, so it has to be less crude
D13 makes online enrichment opt-in, and BRouter needs a separate install plus segment files — so
until #64 lands, the geometric heuristic is what most new users actually get (D27).
As originally specified, heading delta above 25° between consecutive 5 m segments fires on every
curve, roundabout and switchback, while staying silent at junctions where the rider continues
straight.
NAV_CUE_CONFIDENCE = 0, and shown as guesses in the library and in #55It still cannot see junctions — nothing without map data can. That is why it is tier 3 and why its
cues are labelled.
Opened PR #122 (area/geometry-enricher -> main): #122
Implements the tightened (2026-09-01) design: +/-20m windowed heading delta (reusing RouteGeodesy's bearingDegrees/pointAtDistanceMeters from #55), 25 deg/70 deg turn/sharp thresholds, same-side cluster merging within 30m so a curve/roundabout/hairpin doesn't burst into multiple cues, tier=GEOMETRY on every cue (-> NAV_CUE_CONFIDENCE=0), streetName always null. Wired into RouteStore's tier3 slot; CURRENT_ENRICHER_VERSION bumped 1->2.
Fixtures (hand-checked via an independent Python re-implementation of the bearing/window math, not by trusting the Kotlin code): winding-descent.gpx (4 alternating r=8m/140deg hairpins -> 4 cues), roundabout.gpx (one r=15m/200deg arc -> 1 merged cue), sweeping-bend-vs-sharp-turn.gpx (r=300m/57deg gentle bend -> 0 cues, vs r=8m/60deg sharp turn of near-identical total magnitude -> 1 cue - the explicit 'must complete within a short distance' regression case), straight-route.gpx.
One acceptance criterion is out of reach right now and I did not fake it: 'output compared against a BRouter cue sheet on a known route' needs BRouterEnricher (#27), which doesn't exist yet. Added a dependency edge (this issue now depends on #27) instead of a stub comparison - that check belongs in a follow-up once #27 lands.
Real test run: ./gradlew :companion:core:test --rerun-tasks, 353 tests, 0 failures.
Closed by PR #122 (
area/geometry-enricher) — well, not quite closed: this issue depends on #27 (still open) for its own "compared against a BRouter cue sheet" criterion, so Forgejo will correctly refuse to close it until that lands. Leaving it open with that dependency edge in place rather than fabricating a comparison.Everything else built and verified for real:
bearingDegrees/pointAtDistanceMetersdirectly — a ±20m window per vertex (D27's own corrected figure), 25° turn / 70° sharp thresholds, left/right by sign.sweeping-bend-vs-sharp-turn.gpx): a gentle r=300m/57.3° bend and a sharp r=8m/60° turn of near-identical total magnitude — only the concentrated one produces a cue.CLUSTER_MERGE_GAP_METERS = 30) is derived from RDP's own sagitta math, not picked by feel: solving the chord-sagitta formula at the 5m import tolerance and a 10-20m real-curve radius gives ~24-25m as the widest spacing RDP can leave between kept vertices on one physical curve — 30m is a small margin over that. A side-flip always breaks a cluster even within the gap, since that's a real second turn (an S-curve), not resampling of one curve.RouteStore's tier3 slot for real — a route with no embedded cues and no BRouter/Valhalla now gets a real, confidence-0 geometric cue sheet instead of silently falling through to nothing.CueSheetCache.CURRENT_ENRICHER_VERSIONbumped 1→2 with a proper history log entry.tier = EnrichmentTier.GEOMETRY(confirmed this is whatNavEngine/the wire layer reads asNAV_CUE_CONFIDENCE = 0) andstreetName = null— tier 3 has nothing to name a road from.Real verification: genuine
./gradlew :companion:core:test :companion:route:assembleDebug --rerun-tasksfull rebuild, all green —GeometryEnricherTest6/6 against real synthetic-but-closed-form GPX fixtures (winding descent: 4 hairpins → 4 cues; roundabout: one 200° arc → 1 merged cue; sweeping bend vs. sharp turn: 0 then 1 cue; straight route: 0 cues as a genuine positive result, not a fallback signal).