docs+openspec: prior-art research (Organic Maps, Endurain, wanderer) and 15 proposals

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>
This commit is contained in:
Ullrich Schäfer 2026-07-06 07:23:00 +02:00
parent be4f7e4ae8
commit 5dd4968626
80 changed files with 2600 additions and 2 deletions

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-05

View file

@ -0,0 +1,64 @@
## 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 `DayStage`s 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 `totalTime` exists 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; sum `distance / 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 `estimatedTimeSec` is present.
- Multi-day: each day section header/row in `DayBreakdown.tsx` shows 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.
- [`DayStage` shape 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.totalTime` for cycling profiles, and whether to add a genuine hiking `.brf` to the BRouter image (and relabel profiles honestly), are follow-up proposals.

View file

@ -0,0 +1,29 @@
## Why
The planner shows distance and ascent for a planned route but no time estimate, even though "how long will this take?" is the first question a hiker asks of a plan — and even though BRouter already returns elevation-tagged geometry that makes a good estimate cheap to compute. Tobler's hiking function (speed as a function of slope, the model Organic Maps uses for pedestrian ETA) turns the existing route geometry into a credible walking-time estimate with a small pure function. Bonus: the `multi-day-routes` spec already requires per-day "estimated duration", which was never implemented — this change closes that drift.
## What Changes
- New pure function in `@trails-cool/gpx`: Tobler-based walking time over a sequence of points with elevation (`speed = 6·e^(3.5·|slope+0.05|)` km/h per segment, flat-ground fallback when elevation is missing).
- The planner sidebar shows **estimated walking time** alongside distance and ascent when the routing profile is the foot profile (`trekking`, labeled "Hiking" in the UI). Cycling/car profiles show no estimate (out of scope for now).
- `computeDays` gains an opt-in per-day `estimatedTimeSec` so the multi-day day breakdown shows a time per day (planner only; journal's `computeDays` call is unaffected).
- Estimates are labeled as walking time without breaks (signpost convention), localized in English and German.
- Not in scope: cycling time estimates (BRouter's `total-time` is already summed in `EnrichedRoute.totalTime` and could be surfaced later), journal route detail display, rest-break padding (DIN 33466-style), and any change to routing itself.
## Capabilities
### New Capabilities
- `hiking-time-estimate`: Tobler-based walking-time estimation for planned routes — the model, where it appears (whole-route stats, per-day stats), when it applies (foot profile), and its fallback behavior without elevation data.
### Modified Capabilities
<!-- none — multi-day-routes already requires per-day estimated duration (currently unimplemented); this change fulfills that requirement rather than changing it. The new capability spec covers the estimation model and display details. -->
## Impact
- `packages/gpx/src/hiking-time.ts` (new) + tests — the Tobler function and a cumulative variant for day splits.
- `packages/gpx/src/compute-days.ts` — optional `estimatedTimeSec` on `DayStage` behind an opt-in flag; existing callers unchanged.
- `apps/planner/app/lib/use-routing.ts` — compute `estimatedTimeSec` into route stats when the profile is `trekking`, from the enriched route's elevation-tagged coordinates.
- `apps/planner/app/components/WaypointSidebar.tsx` + `DayBreakdown.tsx` — display whole-route and per-day time.
- `packages/i18n` — new planner strings (en + de).
- No API, schema, or BRouter changes.

View file

@ -0,0 +1,44 @@
## ADDED Requirements
### Requirement: Tobler-based walking time model
The GPX package SHALL provide a pure walking-time estimator that applies Tobler's hiking function (`speed = 6·e^(3.5·|slope+0.05|)` km/h) per segment over a sequence of points, using haversine horizontal distance and elevation-derived slope. Segments lacking elevation on either endpoint SHALL be estimated at flat-ground speed. Slope SHALL be clamped to ±100% before applying the formula.
#### Scenario: Flat route baseline
- **WHEN** a 5 km route with constant elevation is estimated
- **THEN** the estimate is approximately one hour (flat Tobler speed ≈ 5 km/h)
#### Scenario: Uphill slower than downhill
- **WHEN** the same distance is estimated once at +20% slope and once at 20% slope
- **THEN** the uphill estimate is greater than the downhill estimate
- **AND** both are greater than the flat estimate
#### Scenario: Missing elevation falls back to flat speed
- **WHEN** a route's points carry no elevation values
- **THEN** an estimate is still produced using flat-ground speed for every segment
### Requirement: Whole-route estimate in the planner for the foot profile
The Planner SHALL display the estimated walking time alongside distance and ascent when the active routing profile is the foot profile, computed from the routed geometry's elevation-tagged coordinates. Other profiles SHALL NOT display a walking-time estimate. The display SHALL mark the value as approximate and use localized strings and hours/minutes formatting.
#### Scenario: Estimate shown for foot profile
- **WHEN** a route is computed with the foot ("Hiking") profile selected
- **THEN** the sidebar stats include an approximate walking time (e.g. "≈ 4 h 20 min")
#### Scenario: No estimate for cycling profiles
- **WHEN** the same route is computed with a cycling profile selected
- **THEN** no walking-time estimate is displayed
#### Scenario: Estimate updates with the route
- **WHEN** a waypoint is added and the route recomputes
- **THEN** the displayed estimate reflects the new geometry
### Requirement: Per-day estimates in the multi-day breakdown
`computeDays` SHALL support opt-in per-day walking-time estimation, populating an optional `estimatedTimeSec` on each day stage such that day estimates sum to the whole-route estimate; callers not opting in SHALL receive unchanged output. The Planner's day breakdown SHALL display each day's estimated walking time when the foot profile is active.
#### Scenario: Day times displayed and consistent
- **WHEN** a foot-profile route has overnight waypoints splitting it into days
- **THEN** each day section shows its estimated walking time
- **AND** the day estimates sum to the whole-route estimate
#### Scenario: Journal call sites unaffected
- **WHEN** `computeDays` is called without the opt-in (journal route detail)
- **THEN** day stages carry no `estimatedTimeSec` and existing behavior is unchanged

View file

@ -0,0 +1,23 @@
## 1. Model function
- [ ] 1.1 Create `packages/gpx/src/hiking-time.ts`: `toblerSpeedKmh(slope)`, `hikingTimeSeconds(points)`, `cumulativeHikingTimeSeconds(points)` — haversine horizontal distance, elevation slope with ±100% clamp, flat-speed fallback for missing elevation
- [ ] 1.2 `hiking-time.test.ts`: flat 5 km ≈ 1 h; 5% slope is the fastest; ±20% asymmetry (uphill > downhill > flat); missing-elevation fallback; clamp prevents near-zero speeds; cumulative array matches total
- [ ] 1.3 Export from `packages/gpx/src/index.ts`
## 2. Per-day support
- [ ] 2.1 Extend `computeDays(waypoints, tracks, opts?)` with `opts.estimateHikingTime` populating optional `DayStage.estimatedTimeSec` from the cumulative time array between stage boundaries
- [ ] 2.2 Tests: day times sum to route total; without opt-in the output is deep-equal to before; single-day route gets one stage with the full estimate
## 3. Planner integration
- [ ] 3.1 In `use-routing.ts`, when the active profile is the foot profile (`hiking` if the `hiking-foot-profile` change has landed, otherwise `trekking`), compute `estimatedTimeSec` from `enriched.coordinates` and include it in the route stats written for the sidebar
- [ ] 3.2 Display in `WaypointSidebar.tsx`: add the time to the compact stat line and the stats grid when present, with "≈" and h/min formatting
- [ ] 3.3 Display per-day times in `DayBreakdown.tsx` (pass the opt-in flag to the planner's `computeDays` call when the foot profile is active)
- [ ] 3.4 Add i18n strings (en: "Est. walking time" etc., de: "Gehzeit ca.") to `packages/i18n` planner namespaces — no hardcoded strings
## 4. Verification
- [ ] 4.1 Extend the planner e2e suite: with the Hiking profile a computed route shows a walking-time stat; switching to a cycling profile hides it
- [ ] 4.2 Manual sanity check against a known hike (e.g. a local route with signposted time) — estimate should be in the right ballpark
- [ ] 4.3 Run `pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2e`