trails/docs/inspirations.md
Ullrich Schäfer 5dd4968626 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>
2026-07-06 07:23:00 +02:00

240 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Inspirations & prior-art research
Durable record of the open-source projects we've studied, what we learned,
and which OpenSpec changes each insight spawned. This is the source of truth
for acknowledgments: the **credit line** under each project is the canonical
one-liner reused by `docs/philosophy.md` (Open Source section) and the
`/about/credits` page (see `openspec/changes/credits-page/`). Update all
three together when adding a project.
Research method: shallow-clone the repo, fan out parallel exploration agents
over focused areas (data model, integrations, product, ops, community), then
synthesize trails-relevant learnings and turn the actionable ones into
OpenSpec proposals. Clones live in ephemeral scratch space; this document is
what survives.
---
## BRouter, bikerouter.de, brouter-web (founding inspirations)
> **Credit line:** BRouter — the routing engine · bikerouter.de — inspiration
> for the Planner · brouter-web — the web client
The projects that made the Planner conceivable. BRouter is also a runtime
dependency (self-hosted routing engine, `docker/brouter/`). Not "researched"
in the sense below — they're in the product's DNA.
---
## Organic Maps — reviewed 2026-07-05
> **Credit line:** Organic Maps — inspiration for outdoor-focused map
> presentation, elevation handling, hiking-time estimation (Tobler), GPX
> robustness, and privacy-as-proof practices
Offline, privacy-absolutist OSM app (C++, [github.com/organicmaps/organicmaps](https://github.com/organicmaps/organicmaps)),
~10 years of map-domain experience inherited from MAPS.ME. Different
architecture (native offline vs web/federated), but it has solved many
problems on our roadmap.
### Key learnings (with their source locations)
- **Elevation profile hardening** (`libs/map/elevation_info.cpp`):
opposite-sign slope-outlier despiking (single-point spikes interpolated
out; monotonic climbs never touched), dual raw/threshold-filtered
ascent totals (hysteresis accumulation), Douglas-Peucker profile
simplification. Their Easy/Medium/Hard track difficulty rating is an
unimplemented `@todo` — a feature we could ship first.
- **Hiking ETA** (`libs/routing/edge_estimator.cpp`): Tobler's hiking
function for pedestrian speed-by-slope, downhill penalized at 0.35× of
uphill, altitude penalty above 2500 m. Also the weight-speed vs ETA-speed
split: route *choice* uses inflated speeds for nice paths; time estimates
use realistic ones.
- **GPX robustness** (`libs/kml/serdes_gpx.cpp` + fixture corpus in
`data/test_data/gpx/`): timestamp repair (>50% invalid per segment →
drop all; else linear interpolation), leading `+` in coordinates,
`<rte>` imported as tracks, `<cmt>`/desc merging with dedup,
triple-namespace track-color export (Garmin gpxx + gpx_style + osmand)
for interop. Their fixture corpus is real files from OsmAnd, gpx.studio,
OpenTracks, Garmin.
- **Outdoor map presentation** (`data/styles/outdoors/`,
`generator/osm2type.cpp`): a separate Outdoors style that promotes peaks,
saddles, springs, drinking water to low zooms; `sac_scale` +
`trail_visibility` collapsed into just two path grades (difficult/expert)
for rendering — two values, big safety payoff. OSM route-relation
membership shown on a path's page ("E5, GR5").
- **Own the data pipeline** (`tools/python/maps_generator/`,
`generator/`): one batch pipeline from planet file to per-region packs
carrying rendering + search + routing + elevation; precompute per-segment
elevation at build time instead of querying a DEM live; `.poly`-file
region splitting; category-as-pseudo-language search-trie trick (category
IDs indexed as a fake language alongside names).
- **Practices**: privacy proven by third-party audit evidence published
in-repo (Exodus reports) rather than asserted; opt-in email-with-log
diagnostics instead of telemetry; excellent `docs/PR_GUIDE.md` (small-PR
norm, LLM-use disclosure); anti-roadmap stance (milestones over a
ROADMAP file); root CLAUDE.md with AGENTS.md symlink + nested per-domain
CLAUDE.md files; GPS track-recording filter (`libs/map/gps_track_filter.cpp`:
accuracy >250 m rejected, acceleration >2 m/s², bearing-flip rejection,
file-backed ring buffer) — reference for `docs/ideas/mobile-activity-recording`.
### Changes spawned
`elevation-profile-hardening`, `gpx-parser-robustness`,
`hiking-time-estimate`, `poi-index` (pipeline pattern + evidence),
`hiking-foot-profile` (the review exposed the trekking-labeled-"Hiking"
mismatch).
### Explicitly not pursued
Deep-link codec (session URLs cover it), track-color export (nothing
renders colors), MWM/offline formats (wrong architecture for web).
---
## Endurain — reviewed 2026-07-06
> **Credit line:** Endurain — inspiration for the Journal's activity privacy
> controls, account export, duplicate handling, and self-hosting operator
> experience
Self-hosted Strava alternative ([codeberg.org/endurain-project/endurain](https://codeberg.org/endurain-project/endurain);
FastAPI + Vue, AGPL, single maintainer). Overlaps the Journal only; its
federation is frontend scaffolding — trails is ahead there.
### Key learnings (with their source locations)
- **Granular privacy** (`users/users_privacy_settings/models.py`,
`activities/activity/models.py`, `apply_visibility_mask()`): ~13
independent per-signal hide flags (start time, location, map, HR, power,
pace, laps, gear…), per-user defaults stamped at import, per-activity
overrides, one server-side mask, one frontend gate component.
- **Account export/import** (`users/users_profile/export_service.py`):
streaming, memory-bounded ZIP of everything including original uploaded
FIT/GPX files; matching import with ID remapping.
- **Cross-source duplicate handling** (`activities/activity/crud.py`):
same-start-time match → store anyway, flag `is_hidden`, notify for manual
review. Flag-and-review over silent auto-merge.
- **FIT parsing quirks catalog** (`activity_file_import/utils_fit.py`,
1,147 lines of fitdecode handling): multi-session files → sliced
per-session records, semicircle→degrees, timezone via TimezoneFinder +
device-offset fallback for indoor, zero-HR sensor-dropout exclusion,
`enhanced_*` fields preferred, cursor resets across pauses,
`timestamp_16` rollover. Tests: `backend/tests/.../test_utils_fit.py`.
- **Elevation smoothing convergence** (`compute_elevation_gain_and_loss`):
median(6) → moving-average(3) → 0.1 m threshold — independent
confirmation that naive delta-summing overstates ascent.
- **Gear tracking** (`gears/`): types + retirement + mileage offset,
components/consumables with `expected_kms` wear thresholds, time-based
wear for racquets, default gear per activity type.
- **Operator experience** (`docs/getting-started/advanced-started.md`,
three compose variants incl. Docker-secrets): ~45-variable documented env
table; BYO-OAuth-credentials pattern (each self-hoster registers their
own provider app). Auth stack is mature (refresh rotation, TOTP, OIDC,
API keys, SSRF allowlists).
- **Contrasts that validate trails' choices**: unofficial password-based
Garmin login with unlink-on-expiry (vs our official-API path), polling
without webhooks (vs our Wahoo webhooks), no health endpoint/metrics, no
changelog, offset pagination. `TRADEMARK.md` (AGPL code + registered
word mark restricting commercial hosting) is an interesting governance
read.
### Changes spawned
`account-export`, `activity-duplicate-review`, `fit-parsing-hardening`,
`activity-locations` (their feature, our privacy-preserving offline
redesign), `self-hosting-guide`, `activity-privacy-controls`.
---
## wanderer — reviewed 2026-07-06
> **Credit line:** wanderer — a self-hosted, federated trail database; the
> closest neighbor to the Journal's federated route sharing and proof the
> ActivityPub-for-trails idea works
Self-hosted federated trail database ([wanderer.to](https://wanderer.to),
[github.com/Flomp/wanderer](https://github.com/Flomp/wanderer); AGPL,
Go/PocketBase + SvelteKit + Meilisearch + WASM provider plugins). Same
route-vs-activity split as trails ("trails" + "summit logs"). The only
studied project that has shipped ActivityPub for trail content.
### Key learnings (with their source locations)
- **Federation is real and Mastodon-interops** (`db/federation/*.go`,
`db/util/activitypub.go`, protocol doc
`docs/src/content/docs/develop/federation.md` with full JSON examples):
per-user RSA keys, signed fetch ("authorized fetch"), WebFinger,
Follow/auto-Accept, Likes, comments as `Note` + `inReplyTo`, summit logs
federate as first-class objects. **Object model: everything is a plain
`Note`** — metrics ride in bespoke `tag[]` note entries, GPX as a
`Document` attachment (3-photo cap, lossy unit encoding). Interop with
trails' Fedify journal is feasible at the social layer; exchanging
actual route data would need a wanderer-specific adapter. Their
Note-with-attachments shape *is* why trails render in Mastodon — worth
considering a Note-compatible representation alongside structured
objects.
- **Federation weaknesses to avoid**: fire-and-forget delivery (detached
goroutine, semaphore(5), no queue/retry — activities lost on transient
failures), no inbound dedup/replay defense, no moderation or
instance-blocking at all, no shared instance actor, signature
verification coupled to `X-Forwarded-Path` proxy headers. Their SSRF-safe
outbound HTTP client (`db/util/network.go`: private-IP blocking,
30 req/min/origin) is worth copying.
- **Discovery at scale** (`db/util/meilisearch.go`,
`db/routes/search_token.go`, `web/src/routes/api/v1/search/trails/cluster/`):
Meilisearch with `_geo` + precomputed bbox scalars (antimeridian-aware
viewport filters); **access control enforced at the index layer via
tenant tokens** (anonymous → `public=true`; authed → public OR author OR
shared-with, minted per session); server-side `supercluster` clustering
where the largest-bbox trails render as polylines and the rest cluster;
random-offset sampling for recommendations; remote/federated trails
indexed as local stubs so cross-instance content is searchable.
- **Trail data model** (`db/migrations/1747064968_collections_snapshot.go`,
`web/src/lib/models/`): polyline + bbox computed server-side on GPX
change; stats recomputed from files, never trusted; dual-threshold
elevation filter (5 m horizontal anchor / 5 m vertical, raw + smoothed
kept — third independent confirmation that naive ascent summing is
wrong); **optional DEM elevation correction via self-hosted Valhalla
height service** (`gpx.correctElevation()`) — an idea trails hasn't
considered; all formats (FIT/TCX/KML/KMZ) normalized to GPX at import.
- **Product features**: ordered trail lists with per-actor view/edit
shares; **link-share tokens** (view/edit) for private content;
9 notification types with **per-type web/email preferences**;
**print view with map, elevation profile, scale bar, and QR code**
(jsPDF) — paper backup for hikes; collection export as ZIP (GPX or JSON,
photos + summit logs); hashed long-lived **API tokens** for third
parties; sandboxed **WASM provider plugins** (Strava/Komoot/Hammerhead —
plugins can't open sockets; a host executor injects auth and enforces
policy; `plugins/README.md` is an excellent architecture doc).
- **Ops practices**: sectioned semver CHANGELOG with GHSA/PR references;
boot-time hard failure on missing/default encryption keys; search index
treated as rebuildable cache and excluded from backups; explicit
AI-assisted-contribution policy in CONTRIBUTING. Cautionary tales:
real default secrets committed in `docker-compose.yml`, no
reverse-proxy/TLS setup docs, backup restore only supported within the
same minor version.
### Changes spawned
`federation-hardening` (durable Fedify queue replacing the in-process one,
replay dedup, instance blocklist, FEDERATION.md), `link-share-tokens`
(revocable tokenized view links for private content).
### Changes updated with these findings
`route-federation` (prior-art section: Note-model contrast, interop
verdict, dual-representation consideration, hardening dependency),
`route-discovery` (prior-art section: query-layer access control,
clustering/recommendation patterns), `self-hosting-guide` (no default
secrets + boot fail-fast, rebuildable-state backup guidance,
restore-version rule). Not yet acted on: per-type notification channel
preferences (future `notifications` delta), print view with QR code,
hashed API tokens, Valhalla-style DEM elevation correction.
### Explicitly not pursued
PocketBase/SQLite single-binary direction (Postgres+PostGIS gives native
geo without a mandatory search sidecar); WASM plugin system (in-app
providers are simpler at trails' provider count); Meilisearch itself
(PostGIS + SQL covers trails' discovery scale for now).