Navigation engine: windowed snapping to the route polyline (issue #31) #114
No reviewers
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/PedalPebble!114
Loading…
Reference in a new issue
No description provided.
Delete branch "area/navigation-snapping"
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?
Closes #31.
What this adds
RouteSnapper(companion/core/.../route/nav/RouteSnapper.kt), which locates a live GPS fix along an importedGpxRoute's polyline.Reuse, not reimplementation
This reuses
route.enrich.locateAlongPolyline(#26/PR #79) directly - no changes to that function. It is already a forward-only, bounded-horizon point-to-segment projection with a monotonically advancing search cursor, built for #63's tier-0 cue placement, and its own KDoc already documents the exact out-and-back/lollipop disambiguation problem this issue is about.The one behavioural difference a live snapper needs versus a cue-matcher - reporting a genuinely large offset instead of discarding a bad match - comes for free by calling it with
maxOffsetMeters = Double.MAX_VALUE.locateAlongPolylineonly rejects a match past that threshold; a live snapper wants the opposite (the large offset is the useful signal for #32), so nothing in that function needed to change.RouteSnappersupplies the two things a live-GPS caller has that a one-shot cue-import pass doesn't:searchCursorIndex), starting at the route's own start and advancing to wherever the previous fix matched.NORMAL_SEARCH_HORIZON_METERS(150 m) at ordinary 1 Hz cadence.elapsed seconds x MAX_PLAUSIBLE_SPEED_METERS_PER_SECOND(20 m/s) after a GPS gap longer thanGAP_THRESHOLD_SECONDS(5 s).OFF_ROUTE_REACQUISITION_HORIZON_METERS(500 m) when the previous fix's offset exceededREACQUISITION_OFFSET_THRESHOLD_METERS(50 m) - this internal threshold is purely a window-sizing decision, not #32's off-route determination.RouteSnap(the output type) carriessegmentStartIndex,distanceAlongRouteMeters, andoffsetMeters- the last one deliberately unfiltered and unclamped, so #32's off-route detection has a real number to run its own hysteresis against. Deciding "is the rider off-route" is explicitly out of scope here.Monotonicity (per the issue's 2026-09-02 update)
D42 removed mid-ride re-routing from Phase 3, so there is no route swap this engine has to survive. Distance-along-route is non-decreasing across on-route fixes by construction: the search cursor never looks behind its own previous result.
RouteSnapperTestasserts this directly over a sampled real GPX route, per the issue's "asserted in tests, not merely intended" wording.Tests (
./gradlew :companion:core:test, all passing, 287 tests total in the module)real-komoot-havelchaussee-glienicker-bruecke.gpx), sampled every 20th point, confirming exact matches and non-decreasing distance-along-route.https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt