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

@ -22,7 +22,8 @@ These principles inform key architectural and product decisions:
visualizations and tools around their data.
- **Open source (MIT)**: The code is MIT licensed — both because of the
commitment to the open web, and out of respect for the open-source projects
that inspire trails.cool (BRouter, bikerouter.de, brouter-web).
that inspire trails.cool (BRouter, bikerouter.de, brouter-web, Organic Maps,
Endurain, wanderer).
- **AI-assisted development**: AI (Claude Code) and spec-driven development
(OpenSpec) are core to how this project is built. Human contributions are
very welcome. This is also an experiment in how far AI-assisted development

240
docs/inspirations.md Normal file
View file

@ -0,0 +1,240 @@
# 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).

View file

@ -37,11 +37,22 @@ trails.cool is MIT licensed. The entire codebase — Planner, Journal, shared
packages, infrastructure — is open source.
This is both a commitment to the open web and an acknowledgment of the
projects that made trails.cool possible:
projects that made trails.cool possible (what we learned from each is
recorded in [docs/inspirations.md](inspirations.md); keep the two lists in
sync):
- [BRouter](https://github.com/abrensch/brouter) — the routing engine
- [bikerouter.de](https://bikerouter.de) — inspiration for the Planner
- [brouter-web](https://github.com/nrenner/brouter-web) — the web client
- [Organic Maps](https://github.com/organicmaps/organicmaps) — inspiration for
outdoor-focused map presentation, elevation handling, hiking-time estimation
(Tobler), GPX robustness, and privacy-as-proof practices
- [Endurain](https://codeberg.org/endurain-project/endurain) — inspiration for
the Journal's activity privacy controls, account export, duplicate handling,
and self-hosting operator experience
- [wanderer](https://wanderer.to) — a self-hosted, federated trail database;
the closest neighbor to the Journal's federated route sharing and proof the
ActivityPub-for-trails idea works
We benefit from open source. We contribute back to it.