trails/openspec/changes/osm-overlays/design.md
Ullrich Schäfer f414c97887
Update specs with POI-waypoint integration, resilience, z-index
Proposal: Added POI→waypoint, POI snapping, and metadata storage.
Design: Added D10 (POI-waypoint integration), D11 (Overpass fallback),
D12 (z-index layering). Updated risks/trade-offs.
Tasks: Added sections 11 (POI-waypoint, 5 tasks) and 12 (resilience,
5 tasks). Fixed Overpass test to match updated query format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 02:09:44 +02:00

9.6 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.

D10: POI-to-waypoint integration

POIs can become waypoints through three paths, all using the same snap logic:

  1. "Add as waypoint" button in POI popup → appends to end of route
  2. Click near a POI (within 50m) → new waypoint snaps to POI position
  3. Drag waypoint near a POI → snaps on drop, clears name if dragged away

When snapping, the waypoint's Yjs Y.Map stores:

  • osmId: OSM node ID for future cross-referencing
  • poiTags: Key tags (phone, address, website, opening hours, amenity type)

This metadata persists through Yjs and will be available for Journal display.

D11: Overpass endpoint fallback

Primary endpoint is overpass.kumi.systems (higher rate limits, same as brouter-web). Falls back to overpass-api.de if the primary fails. The Overpass API sometimes returns rate limit errors as HTTP 200 with "rate_limited" in the body — the client checks for this pattern.

D12: Z-index layering

Marker z-index offsets centralized in apps/planner/app/lib/z-index.ts:

  • Cursors (-1000) < Ghost waypoint (-100) < Waypoints (1000) < Waypoint highlighted (1200) < POI markers (1500) < Highlight dot (2000)

Risks / Trade-offs

  • Overpass API availability: Public endpoint, no SLA. If down, POI overlays fail gracefully (show message, tile overlays still work). Fallback endpoint (overpass.kumi.systemsoverpass-api.de) implemented.
  • 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. → Zoom threshold (>=10) + 100 element limit mitigate this. Marker clustering deferred — can add later if density becomes an issue.
  • Overpass query cost: Combining many categories into one query is efficient but returns large payloads. → Only query enabled categories, not all. 1MB maxsize limit, 10s timeout.
  • Routing rate limit: Multi-waypoint routes with POI snapping can trigger rapid recomputes. Increased rate limit from 60 to 300/hour to accommodate.