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

8.3 KiB
Raw Blame History

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 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:

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.