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:
parent
be4f7e4ae8
commit
5dd4968626
80 changed files with 2600 additions and 2 deletions
2
openspec/changes/hiking-time-estimate/.openspec.yaml
Normal file
2
openspec/changes/hiking-time-estimate/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-07-05
|
||||
64
openspec/changes/hiking-time-estimate/design.md
Normal file
64
openspec/changes/hiking-time-estimate/design.md
Normal 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.
|
||||
29
openspec/changes/hiking-time-estimate/proposal.md
Normal file
29
openspec/changes/hiking-time-estimate/proposal.md
Normal 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.
|
||||
|
|
@ -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
|
||||
23
openspec/changes/hiking-time-estimate/tasks.md
Normal file
23
openspec/changes/hiking-time-estimate/tasks.md
Normal 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`
|
||||
Loading…
Add table
Add a link
Reference in a new issue