Add docs/inspirations.md as the durable record of the 2026-07-05/06 prior-art research — per-project learnings with source paths, canonical credit lines, and the changes each spawned — and extend the acknowledgment lists in philosophy.md/architecture.md (Organic Maps, Endurain, wanderer). New OpenSpec changes (proposal/design/specs/tasks each): - Organic Maps: elevation-profile-hardening, gpx-parser-robustness, hiking-time-estimate, poi-index, hiking-foot-profile - Endurain: account-export, activity-duplicate-review, fit-parsing-hardening, activity-locations, self-hosting-guide, activity-privacy-controls - wanderer: federation-hardening, link-share-tokens - credits-page (user-visible acknowledgments) Updated in-flight changes with wanderer prior-art sections: route-federation, route-discovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.4 KiB
Context
The planner's route stats (WaypointSidebar.tsx) show distance and ascent from EnrichedRoute (route-merge.ts), whose coordinates are [lon, lat, ele] triples from BRouter — elevation is already on every point. BRouter also returns a total-time (summed into EnrichedRoute.totalTime but displayed nowhere); it is computed by the routing profile's speed model. The profile the UI labels "Hiking" is BRouter's trekking.brf — a trekking-bike profile — so its total-time is a cycling time and unusable for hikers. A slope-aware walking model applied to the geometry is both more honest and independent of the profile mislabel.
Organic Maps uses Tobler's hiking function for pedestrian ETA (libs/routing/edge_estimator.cpp, reviewed 2026-07-05): walking speed 6·e^(−3.5·|dh/dx + 0.05|) km/h — max ~6 km/h at a 5% descent, ~5 km/h flat, dropping steeply uphill. It is the standard published model (Tobler 1993) and needs only per-segment horizontal distance and elevation change.
computeDays (packages/gpx/src/compute-days.ts) splits a route into DayStages for the multi-day sidebar; the multi-day-routes spec requires per-day "estimated duration" but DayStage has no time field — unimplemented spec, closed by this change. computeDays is also called by the journal's route detail page, so any extension must be opt-in.
Goals / Non-Goals
Goals:
- A believable walking-time estimate for the whole route and per day, visible in the planner when planning on foot.
- One pure, tested model function in
@trails-cool/gpx, reusable later by the journal. - Honest labeling: walking time excluding breaks (mountain-signpost convention).
Non-Goals:
- Cycling/car time estimates (BRouter's
totalTimeexists for a later change; mixing two estimation sources in one UI needs its own design). - Journal route/activity pages (follow-up; the primitive will be ready).
- Rest-break or pack-weight adjustments (DIN 33466, Naismith corrections), altitude penalties (OM's >2500 m model), and fixing the
trekking-labeled-"Hiking" profile mismatch — the last is a separate routing-profile decision (e.g. adding a real foot profile to the BRouter image) that this change must not block on.
Decisions
1. Model: pure Tobler, per segment, in @trails-cool/gpx
New packages/gpx/src/hiking-time.ts:
toblerSpeedKmh(slope)— the bare function, exported for tests.hikingTimeSeconds(points: Array<{lat, lon, ele?}>)— haversine horizontal distance per segment; slope = Δele / horizontal distance; sumdistance / toblerSpeed. Segments where either endpoint lacks elevation use slope 0 (flat ≈ 5.04 km/h) — a planned route without DEM data still gets a distance-based estimate rather than nothing.cumulativeHikingTimeSeconds(points)— running totals per point index, for day splitting.- Slope clamped to ±100% before applying the formula so a data glitch can't produce a near-zero speed that dominates the total.
Lives in @trails-cool/gpx beside moving-time.ts/compute-days.ts because its input shape is the track-point shape and its consumers are the same. Not map-core (constants only, per conventions).
Alternative: use/patch BRouter's total-time — rejected: for the foot case it's a bike time today, and correcting it means maintaining a custom .brf; a geometry-based model is profile-independent and testable in TypeScript.
2. Applies to the foot profile only, keyed at the planner layer
use-routing.ts computes estimatedTimeSec from enriched.coordinates only when the active profile is the foot profile — hiking once the hiking-foot-profile change lands, else the UI's current "Hiking" (trekking) — and writes it into the route stats object next to distance/elevationGain. The gate lives in the planner, not the library — the library function is sport-agnostic; what "counts as on foot" is a planner UI concept. Profile changes already trigger a routing recompute, so the estimate stays in sync.
3. Per-day times via opt-in computeDays extension
computeDays(waypoints, tracks, opts?) gains opts.estimateHikingTime; when set, each DayStage includes estimatedTimeSec derived from the cumulative time array between the stage's track-point boundaries (same cumulative-array pattern the function already uses for ascent). Optional field + opt-in flag means the journal's existing call sites compile and behave identically, and day times sum exactly to the whole-route total by construction.
Alternative: compute day times in the planner component from DayStage waypoint indices — rejected: the track-point boundary mapping lives inside computeDays; re-deriving it outside duplicates logic the multi-day spec already anchors there.
4. Display: alongside existing stats, labeled as walking time
- Single-day: the sidebar's stat line and stats grid gain a time entry (e.g. "≈ 4 h 20 min") when
estimatedTimeSecis present. - Multi-day: each day section header/row in
DayBreakdown.tsxshows its day time. - Strings via
useTranslation(en: "Est. walking time", de: "Gehzeit ca."), formatted h/min; no seconds. The "≈" and the label communicate estimate-without-breaks; a tooltip/help text can note "excluding breaks".
Risks / Trade-offs
- [Estimate feels wrong to some users — Tobler models a fit walker on decent terrain] → Label as an approximation; Tobler is the same baseline OM ships and matches signpost times reasonably. Surface/terrain multipliers (BRouter gives per-point surface tags we already extract) are a natural follow-up refinement, not v1.
- [Foot profile is actually BRouter's trekking-bike profile, so the route may not be the path a hiker would take] → Pre-existing issue, orthogonal to the estimate; documented here so the follow-up (real foot
.brf) is discoverable. The time estimate is correct for whatever geometry is shown. - [Points without elevation degrade to flat-speed] → Acceptable; BRouter always supplies elevation, so this path only triggers for degraded data.
- [
DayStageshape change ripples to journal types] → Field is optional and absent unless opted in; journal render code never sees it.
Migration Plan
Library + planner-only UI change; ships with the normal app deploy. No schema/API changes. Rollback = revert PR.
Open Questions
- None blocking. Whether to also surface
EnrichedRoute.totalTimefor cycling profiles, and whether to add a genuine hiking.brfto the BRouter image (and relabel profiles honestly), are follow-up proposals.