## 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` — 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.