trails/openspec/changes/osm-overlays/design.md
Ullrich Schäfer 0a330e4466
Break up route-features into focused specs, add new changes
Archive the monolithic route-features spec and replace with 9 focused
OpenSpec changes: multi-day-routes, waypoint-notes (with POI snapping),
undo-redo, local-dev-stack, route-sharing, route-discovery,
activity-photos, osm-overlays, plus the existing changelog and
komoot-import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 09:53:41 +02:00

186 lines
8.3 KiB
Markdown
Raw 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.

## Context
The Planner currently has three base tile layers (OSM, OpenTopoMap, CyclOSM) in
`packages/map/src/layers.ts`, rendered via Leaflet's `LayersControl`. There are
no overlay layers and no POI display. brouter-web offers hillshading, Waymarked
Trails networks, and ~60 Overpass-powered POI categories — a model worth
adopting selectively.
The Planner is stateless (Yjs CRDT), collaborative, and uses BRouter for
routing. Overlay state should sync across participants.
## Goals / Non-Goals
**Goals:**
- Add tile-based overlays (hillshading, waymarked trails) to the layer switcher
- Add viewport-scoped POI overlays from Overpass API with per-category toggling
- Auto-suggest relevant overlays based on routing profile
- Build a reusable Overpass client shared with waypoint-notes POI snap
**Non-Goals:**
- Custom tile server or self-hosted overlays (use public tile services)
- Full brouter-web layer catalog (50+ layers, most country-specific)
- Offline/cached tile data
- Vector tile overlays (MVT) — stick with raster for now
- POI editing or contributing back to OSM
## Decisions
### D1: Tile overlay definitions
Add an `overlayLayers` export to `packages/map/src/layers.ts` alongside
existing `baseLayers`. Each overlay is a transparent tile layer rendered on top
of the base layer.
Initial overlays:
| Id | Name | URL | Attribution |
|----|------|-----|-------------|
| `hillshading` | Hillshading | `https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png` via [Leaflet.TileLayer.Terrarium](https://github.com/pka/leaflet-terrarium-hillshading) or pre-rendered from `https://tiles.wmflabs.org/hillshading/{z}/{x}/{y}.png` | SRTM/Mapzen |
| `waymarked-cycling` | Cycling Routes | `https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png` | Waymarked Trails (CC-BY-SA) |
| `waymarked-hiking` | Hiking Routes | `https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png` | Waymarked Trails (CC-BY-SA) |
| `waymarked-mtb` | MTB Routes | `https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png` | Waymarked Trails (CC-BY-SA) |
These are all free, public tile endpoints used by brouter-web and other OSM
tools. No API keys needed.
**Alternative considered**: Thunderforest Outdoors or OpenCycleMap — requires
API key, limited free tier. Not worth the complexity.
### D2: Overlay toggle in LayersControl
Leaflet's `LayersControl` already supports overlays natively via
`LayersControl.Overlay` (react-leaflet). Add overlay tile layers as checkboxes
alongside the existing base layer radio buttons. No custom UI needed for Phase 1.
### D3: POI category system
Define POI categories as a typed configuration mapping OSM tags to display
properties. Inspired by brouter-web's `layers/overpass/` structure but
simplified to the categories most relevant for route planning:
```typescript
interface PoiCategory {
id: string;
name: string; // i18n key
icon: string; // emoji or SVG icon id
color: string; // marker color
query: string; // Overpass QL fragment, e.g. "nwr[amenity=drinking_water]"
profiles?: string[]; // routing profiles where this is auto-enabled
}
```
**Initial categories** (curated from brouter-web's full list):
| Category | POI types (OSM tags) | Icon | Auto-enable for |
|----------|---------------------|------|-----------------|
| Drinking water | `amenity=drinking_water`, `amenity=water_point` | 💧 | all |
| Shelter | `amenity=shelter`, `tourism=wilderness_hut` | 🛖 | hiking |
| Camping | `tourism=camp_site`, `tourism=caravan_site`, `tourism=picnic_site` | ⛺ | all |
| Food & drink | `amenity=restaurant`, `amenity=cafe`, `amenity=fast_food`, `amenity=pub`, `amenity=biergarten` | 🍽️ | — |
| Groceries | `shop=supermarket`, `shop=convenience`, `shop=bakery` | 🛒 | — |
| Bike infrastructure | `amenity=bicycle_parking`, `amenity=bicycle_repair_station`, `amenity=bicycle_rental` | 🔧 | cycling |
| Accommodation | `tourism=hotel`, `tourism=hostel`, `tourism=guest_house` | 🏨 | — |
| Viewpoints | `tourism=viewpoint` | 👁️ | hiking |
| Toilets | `amenity=toilets` | 🚻 | — |
**Not included** (from brouter-web but too niche): ATMs, banks, benches,
telephones, kneipp water cures, car parking, railway stations, art galleries,
museums, ice cream shops, BBQs. Can be added later by extending the config.
### D4: Overpass client
Create `apps/planner/app/lib/overpass.ts` with:
- `queryPois(bbox, categories): Promise<Poi[]>` — builds Overpass QL query
combining all enabled categories into one request (union query), returns
parsed GeoJSON features
- **Bbox query**: `[bbox:south,west,north,east]` in Overpass QL, scoped to
current Leaflet viewport
- **Endpoint**: `https://overpass-api.de/api/interpreter` (public, no key)
- **Response format**: Request `[out:json]` for easier parsing than XML
- **Deduplication**: Overpass may return same node via multiple tags — dedup by
OSM id
This client is also used by the waypoint-notes POI snap feature (smaller radius
query around a single waypoint).
### D5: POI caching and rate limiting
Overpass API is public and rate-limited. Must be respectful:
- **Debounce**: 500ms after map `moveend` before querying
- **Abort**: Cancel in-flight requests when viewport changes (AbortController)
- **Tile-based caching**: Quantize viewport to grid tiles (e.g., 0.1° cells),
cache results per tile. Reuse cached tiles that overlap new viewport.
- **TTL**: 10 minutes for cached tiles (POI data changes slowly)
- **Max concurrent**: 1 request at a time
- **429 handling**: Exponential backoff, disable auto-refresh temporarily, show
"POI data unavailable" message
- **Zoom threshold**: Only query POIs at zoom >= 12 (avoids massive result sets
at country-level zoom)
### D6: POI overlay panel
A collapsible panel (not the LayersControl — too many items) for toggling POI
categories. Positioned below the layer switcher on the right side of the map.
- Toggle button with POI icon to open/close
- Checkbox per category with icon and name
- "Loading..." indicator while Overpass query is in flight
- Category count badge showing number of visible POIs
- Panel state (which categories are enabled) synced via Yjs so all participants
see the same POIs
### D7: POI marker rendering
- Use Leaflet `L.Marker` with `L.DivIcon` for each POI (not CircleMarker —
need icons)
- Icon shows the category emoji/icon at 20×20px
- Popup on click showing: name, category, opening hours (if available), website
link (if available), OSM link
- **Clustering**: Use `leaflet.markercluster` at low zoom levels to avoid
thousands of markers. Cluster by category color.
- **Z-index**: POI markers below route and waypoint markers
### D8: Profile-aware overlay defaults
When the routing profile changes (via Yjs `routeOptions.profile`), suggest
relevant overlays:
- **Cycling profiles** (cycling-safe, cycling-fast, etc.): Auto-enable
Waymarked Cycling overlay + Bike infrastructure POIs
- **Hiking profiles**: Auto-enable Waymarked Hiking + Shelter + Viewpoints
- **MTB profiles**: Auto-enable Waymarked MTB + Bike infrastructure
"Auto-enable" means toggling on when profile changes, not forcing — users can
still disable. Only auto-enable on profile change, not on page load (respect
user's previous choice stored in Yjs).
### D9: Yjs overlay state
Store enabled overlays in Yjs `routeOptions` Y.Map:
```
routeOptions.overlays = ["hillshading", "waymarked-cycling"]
routeOptions.poiCategories = ["drinking_water", "camping", "bike_infra"]
```
Array of string IDs. Changes sync to all participants. Persisted in crash
recovery localStorage snapshot.
## Risks / Trade-offs
- **Overpass API availability**: Public endpoint, no SLA. If down, POI overlays
fail gracefully (show message, tile overlays still work). → Could add
fallback endpoint (`overpass.kumi.systems`) later.
- **Tile service availability**: Waymarked Trails and hillshading tiles are
community-run. → Degrade gracefully if tiles 404. Consider self-hosting tiles
if usage grows.
- **Performance with many POIs**: Dense areas (cities) may return hundreds of
POIs. → Marker clustering + zoom threshold mitigate this. Limit Overpass
response to 200 elements per category.
- **leaflet.markercluster dependency**: Adds ~40KB. → Only load when POI
overlays are enabled (dynamic import).
- **Overpass query cost**: Combining many categories into one query is efficient
but returns large payloads. → Only query enabled categories, not all.