Epic: M4 — BRouter as router #5

Open
opened 2026-07-27 11:03:57 +00:00 by jmz · 4 comments
Owner

Goal: evaluate routing on BRouter (custom IGN + trail-running profiles) instead of Valhalla, unifying the two planners and improving trail-following.

  • Investigate upstream feat/pluginRouting branch — is routing pluggable? (plugin vs overlay)
  • Prototype a BRouter routing adapter
  • Compare route quality vs Valhalla on Mercantour trails
  • Decide plugin vs overlay; implement
**Goal:** evaluate routing on BRouter (custom IGN + trail-running profiles) instead of Valhalla, unifying the two planners and improving trail-following. - [ ] Investigate upstream `feat/pluginRouting` branch — is routing pluggable? (plugin vs overlay) - [ ] Prototype a BRouter routing adapter - [ ] Compare route quality vs Valhalla on Mercantour trails - [ ] Decide plugin vs overlay; implement
Author
Owner

M4 progress — Phases 1 & 2 shipped & deployed (2026-07-29)

Delivered the pluggable routing port and BRouter integration, deployed to prod (wanderer-web:deploy-1124b306, homelab-flux PR #250; BROUTER_URL=http://brouter:17777 wired). Verified the deployed BRouter proxy end-to-end: POST /api/v1/brouter/route for a Mercantour pair → distanceM 4235, D+ 765, 258 pts, profile mercantour-run.

Phase 1 — routing port + Valhalla adapter (behavior-preserving refactor):

  • RoutingPort hexagonal interface + domain types (web/src/lib/routing/port.ts): RouteRequest/RoutingPreferences/RouteResult/RouterCapabilities, per-engine escape hatch (backend.{valhalla,brouter}).
  • ValhallaAdapter extracted from valhalla_store; calculateRouteBetween routes through the port with byte-identical output (characterization test).
  • getRouter() factory + persisted behavior.router setting.

Phase 2 — BRouter adapter + technicality + escape hatch:

  • Server proxy /api/v1/brouter/route ($lib/server/brouter.ts): validated param-substituted profile upload mechanism (inline URL params are ignored by BRouter) with hash-cache; typed 422 unroutable.
  • BRouterAdapter + translateBRouter(prefs) (SAC technicality → SAC_scale_preferred/limit; avoidRoads→path_preference; elevationBias→consider_elevation); escape hatch merges backend.brouter.params/rawProfile over translated prefs.
  • Planner UI: router selector (Valhalla/BRouter), technicality control (capability-greyed), collapsible Advanced BRouter panel (param rows + raw-profile).

Phase 3 — loop generator (remaining). De-risked (prototype docs/prototypes/loopgen-brouter.py validated: adaptive via-count n=max(3,round(dist/4000)) reaches 20–25 km at <3% error with D+ steering). One prerequisite tracked: the proxy's profile-upload cache can mislabel a stale/evicted custom_<id> as 422-unroutable — invalidate-and-retry (or TTL) before the generator stresses it with many uploads.

Adjacent feature also shipped (same deploy): per-segment off-road (beeline) routing (Komoot-style) — sticky Track/Off-road toggle + Shift one-off + editable per segment + persisted in trkseg extensions + dashed rendering. Built on this port. Spec/plan in docs/superpowers/{specs,plans}/2026-07-29-per-segment-offroad-routing*.

Spec/plan for the port + generator: docs/superpowers/{specs,plans}/2026-07-29-routing-port-and-generator*. All on branch deploy.

## M4 progress — Phases 1 & 2 shipped & deployed (2026-07-29) Delivered the pluggable routing port and BRouter integration, deployed to prod (`wanderer-web:deploy-1124b306`, homelab-flux PR #250; `BROUTER_URL=http://brouter:17777` wired). Verified the deployed BRouter proxy end-to-end: `POST /api/v1/brouter/route` for a Mercantour pair → distanceM 4235, D+ 765, 258 pts, profile `mercantour-run`. **Phase 1 — routing port + Valhalla adapter (behavior-preserving refactor):** - `RoutingPort` hexagonal interface + domain types (`web/src/lib/routing/port.ts`): `RouteRequest`/`RoutingPreferences`/`RouteResult`/`RouterCapabilities`, per-engine escape hatch (`backend.{valhalla,brouter}`). - `ValhallaAdapter` extracted from `valhalla_store`; `calculateRouteBetween` routes through the port with **byte-identical** output (characterization test). - `getRouter()` factory + persisted `behavior.router` setting. **Phase 2 — BRouter adapter + technicality + escape hatch:** - Server proxy `/api/v1/brouter/route` (`$lib/server/brouter.ts`): validated param-substituted **profile upload** mechanism (inline URL params are ignored by BRouter) with hash-cache; typed 422 `unroutable`. - `BRouterAdapter` + `translateBRouter(prefs)` (SAC technicality → `SAC_scale_preferred/limit`; `avoidRoads`→`path_preference`; `elevationBias`→`consider_elevation`); escape hatch merges `backend.brouter.params`/`rawProfile` over translated prefs. - Planner UI: router selector (Valhalla/BRouter), technicality control (capability-greyed), collapsible Advanced BRouter panel (param rows + raw-profile). **Phase 3 — loop generator (remaining).** De-risked (prototype `docs/prototypes/loopgen-brouter.py` validated: adaptive via-count `n=max(3,round(dist/4000))` reaches 20–25 km at <3% error with D+ steering). One prerequisite tracked: the proxy's profile-upload cache can mislabel a stale/evicted `custom_<id>` as 422-unroutable — invalidate-and-retry (or TTL) before the generator stresses it with many uploads. **Adjacent feature also shipped (same deploy):** per-segment **off-road (beeline) routing** (Komoot-style) — sticky Track/Off-road toggle + Shift one-off + editable per segment + persisted in `trkseg` extensions + dashed rendering. Built on this port. Spec/plan in `docs/superpowers/{specs,plans}/2026-07-29-per-segment-offroad-routing*`. Spec/plan for the port + generator: `docs/superpowers/{specs,plans}/2026-07-29-routing-port-and-generator*`. All on branch `deploy`.
Author
Owner

Off-road (beeline) segments shipped + deployed (2026-07-29, deploy-8477efc9)

Per-segment off-road routing is live and user-verified: sticky Track/Off-road toggle, Shift+click one-off override, per-segment mode editing, persisted in trkseg extensions, dashed rendering. Off-road = straight beeline with elevation sampled along the line at 10 m (Valhalla /height).

Post-deploy fixes from user testing: (1) disabled MapLibre boxZoom so Shift+click reaches the editor; (2) densified the beeline before /height (was sampling only the 2 endpoints → linear ramp); (3) a mixed-mode "elevation chart jumps back in distance" symptom resolved once 2-point off-road segments were densified.

Next: Phase 3 loop generator. Prereq being fixed first — the BRouter proxy profile-upload cache can reuse a stale custom_<id> after a BRouter restart and mislabel it as 422-unroutable (the generator reuses one profile across ~40 calls, so it would fail wholesale).

## Off-road (beeline) segments shipped + deployed (2026-07-29, deploy-8477efc9) Per-segment off-road routing is live and user-verified: sticky Track/Off-road toggle, **Shift+click** one-off override, per-segment mode editing, persisted in `trkseg` extensions, dashed rendering. Off-road = straight beeline with elevation **sampled along the line** at 10 m (Valhalla `/height`). Post-deploy fixes from user testing: (1) disabled MapLibre `boxZoom` so Shift+click reaches the editor; (2) densified the beeline before `/height` (was sampling only the 2 endpoints → linear ramp); (3) a mixed-mode "elevation chart jumps back in distance" symptom resolved once 2-point off-road segments were densified. Next: **Phase 3 loop generator**. Prereq being fixed first — the BRouter proxy profile-upload cache can reuse a stale `custom_<id>` after a BRouter restart and mislabel it as 422-unroutable (the generator reuses one profile across ~40 calls, so it would fail wholesale).
Author
Owner

M4 COMPLETE — loop generator shipped + deployed (2026-07-29, deploy-5256c491)

Phase 3 (loop generator) done and live, completing the M4 "BRouter as router" epic end-to-end: routing port → Valhalla/BRouter adapters → technicality → per-segment off-road → loop generator.

  • Generator service web/src/lib/routing/generator.ts — generateLoops(start, target, prefs, port), router-agnostic (port-only), ports the validated prototype: via-count n=max(3,round(dist/4000)), radius dist/2π circle over 8 rotations, proportional-radius convergence, D+ steering by seed selection, degenerate-loop rejection (areaRatio<0.15), hard ~40-call budget, deterministic, best-effort (returns out-of-tolerance candidates + honest "X% off").
  • Generator UI loop_generator.svelte + planner wiring — distance/D+/tolerance/technicality inputs, Generate (BRouter-backed), loading + soft-cancel, ranked candidates, map preview, and Accept (interleaved anchor placement → fully editable loop, undoable).
  • Prereq fixed: BRouter proxy profile-cache now self-heals (re-uploads a stale custom_<id> on the live-verified HTTP-500 case; unroutable 400 → 422 unchanged and never retried).

v1 limitations (tracked, non-blocking): generator has no AbortSignal (soft-cancel only); Accept clears the undo stack (GPX-import precedent); pairwise re-route on Accept can differ slightly from the whole-loop preview.

The M4 epic can be closed once you've exercised the generator in the browser.

## M4 COMPLETE — loop generator shipped + deployed (2026-07-29, deploy-5256c491) Phase 3 (loop generator) done and live, completing the M4 "BRouter as router" epic end-to-end: **routing port → Valhalla/BRouter adapters → technicality → per-segment off-road → loop generator.** - **Generator service** `web/src/lib/routing/generator.ts` — `generateLoops(start, target, prefs, port)`, router-agnostic (port-only), ports the validated prototype: via-count `n=max(3,round(dist/4000))`, radius `dist/2π` circle over 8 rotations, proportional-radius convergence, D+ steering by seed selection, degenerate-loop rejection (`areaRatio<0.15`), **hard ~40-call budget**, deterministic, best-effort (returns out-of-tolerance candidates + honest "X% off"). - **Generator UI** `loop_generator.svelte` + planner wiring — distance/D+/tolerance/technicality inputs, Generate (BRouter-backed), loading + soft-cancel, ranked candidates, map preview, and Accept (interleaved anchor placement → fully editable loop, undoable). - **Prereq fixed:** BRouter proxy profile-cache now self-heals (re-uploads a stale `custom_<id>` on the live-verified HTTP-500 case; unroutable 400 → 422 unchanged and never retried). v1 limitations (tracked, non-blocking): generator has no `AbortSignal` (soft-cancel only); Accept clears the undo stack (GPX-import precedent); pairwise re-route on Accept can differ slightly from the whole-loop preview. The M4 epic can be closed once you've exercised the generator in the browser.
Author
Owner

Loop-generator roadmap COMPLETE (2026-08-03, deploy-fd08015b) — Features C/D/E all shipped

The full 8-item loop-generator enhancement roadmap is now shipped & deployed on top of M4. Beyond the routing port + BRouter + generator (Phases 1–3) and generator-v2 (direction bias, strict loops, planner UX), the three differentiators:

  • C — Waypoint/POI inclusion (deploy-bfb298e4): force a generated loop through chosen trail waypoints + map-clicked points (spliced into the via-ring as fixed vias, distance still converges; accepted as editable anchors).
  • E — Area prioritize/deprioritize (deploy-2ec43660): draw a polygon on the map, mark prefer/avoid, soft candidate-scoring by fraction-of-loop-inside (ray-cast point-in-polygon). Introduced the reusable polyline-overlap primitive.
  • D — Novelty/familiarity bias (deploy-fd08015b, THIS): bias loops by overlap with your own past tracks — novelty favors new ground, familiarity favors known trails; strength slider + source (completed/planned/both). Fetches your nearby tracks from the Meili trails index (author + _geoRadius, precision-5 polyline decode) and scores overlap via the grid-proximity primitive. All these are capabilities Komoot/Garmin lack.

Engineering notes: the generator stayed router- and data-agnostic throughout — it scores against provided geometries; all Meili coupling lives in a store. Each feature is additive (off = today's behavior byte-identical), deterministic, port-only. A live-Meili gotcha caught pre-ship: the trails index has _geo filterable but NOT sortable (migration 1749831369), so a _geoPoint sort 500s — removed; validated live (query-shape returns cleanly, the sorted variant 500s).

Owed before heavy use: a browser smoke pass of the Familiarity/Area/Must-visit panels (all UI is type-gated only), and confirming the author filter returns your own tracks under a real session.

Specs/plans: docs/superpowers/{specs,plans}/2026-07-30-* (C, E) + 2026-08-03-* (D). All on branch deploy.

## Loop-generator roadmap COMPLETE (2026-08-03, deploy-fd08015b) — Features C/D/E all shipped The full 8-item loop-generator enhancement roadmap is now shipped & deployed on top of M4. Beyond the routing port + BRouter + generator (Phases 1–3) and generator-v2 (direction bias, strict loops, planner UX), the three differentiators: - **C — Waypoint/POI inclusion** (`deploy-bfb298e4`): force a generated loop through chosen trail waypoints + map-clicked points (spliced into the via-ring as fixed vias, distance still converges; accepted as editable anchors). - **E — Area prioritize/deprioritize** (`deploy-2ec43660`): draw a polygon on the map, mark prefer/avoid, soft candidate-scoring by fraction-of-loop-inside (ray-cast point-in-polygon). Introduced the reusable polyline-overlap primitive. - **D — Novelty/familiarity bias** (`deploy-fd08015b`, THIS): bias loops by overlap with **your own** past tracks — novelty favors new ground, familiarity favors known trails; strength slider + source (completed/planned/both). Fetches your nearby tracks from the Meili `trails` index (`author + _geoRadius`, precision-5 polyline decode) and scores overlap via the grid-proximity primitive. All these are capabilities Komoot/Garmin lack. **Engineering notes:** the generator stayed router- **and** data-agnostic throughout — it scores against *provided* geometries; all Meili coupling lives in a store. Each feature is additive (off = today's behavior byte-identical), deterministic, port-only. A live-Meili gotcha caught pre-ship: the trails index has `_geo` **filterable but NOT sortable** (migration 1749831369), so a `_geoPoint` sort 500s — removed; validated live (query-shape returns cleanly, the sorted variant 500s). **Owed before heavy use:** a browser smoke pass of the Familiarity/Area/Must-visit panels (all UI is type-gated only), and confirming the `author` filter returns your own tracks under a real session. Specs/plans: `docs/superpowers/{specs,plans}/2026-07-30-*` (C, E) + `2026-08-03-*` (D). All on branch `deploy`.
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
public/wanderer#5
No description provided.