From 9c45e7275c001c14de3b8866f4ed08ddf7ce4c96 Mon Sep 17 00:00:00 2001 From: Roman Priesol Date: Sat, 12 Sep 2026 22:36:10 +0200 Subject: [PATCH] Add a Hiking trails overlay (Waymarked Trails, online only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Overlays panel toggle, same plain on/off shape as Contour lines — raster tiles from waymarkedtrails.org, which already color each route by its OSM colour tag (the KST/KČT paint-blaze colors used in Slovakia/Czechia, plain red elsewhere), so no styling work needed here. Online only for now; offline (baked into offline.pmtiles) needs a custom Planetiler profile for route relations, deferred until there's a route planner/router to pair it with. --- TODO.adoc | 1 + public/js/hiking_trails.js | 74 ++++++++++++++++++++++++++++++++++++++ public/js/icons.js | 6 +++- public/js/overlays.js | 6 +++- views/layout.erb | 1 + 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 public/js/hiking_trails.js diff --git a/TODO.adoc b/TODO.adoc index 1ccbf03..e67740c 100644 --- a/TODO.adoc +++ b/TODO.adoc @@ -12,6 +12,7 @@ independence from connectivity. * [x] Shaded relief / DEM layer — hillshade toggle in the sidebar, sourced from public Terrarium tiles online; reads `data/terrain.pmtiles` for offline if one's been generated (see docs/development.adoc), otherwise the toggle is online-only for now ** [ ] Terrain slope readout for parking (numeric, not just visual shading) * [ ] Contour lines from OSM for hikes and mountain passes +* [x] Hiking trail overlay — Waymarked Trails raster tiles (`public/js/hiking_trails.js`), colored per-route by the OSM `colour` tag they already render (the KST/KČT paint-blaze colors in Slovakia/Czechia, plain red elsewhere), so no styling logic needed here. Online only for now — an offline copy (baked into `offline.pmtiles`, needing a custom Planetiler profile for route relations) is deferred until there's a route planner/router to pair it with * [x] Overpass/OSM scripts to extract POIs (drinking water, dump stations, LPG, public toilets, laundromats, shops, springs) — `rake overpass:pois`, see docs/development.adoc; writes a `.geojson` to drop in `data/gpx/` or upload. Also runnable from inside the app itself now, see the import plugins entry below (`plugins/overpass-pois` wraps this same query). Each category now carries a real `POI_ICONS` glyph and color (not p4n2gpx's plain red marker) plus a popup description built from opening_hours/operator/website tags * [x] Import plugins: a pluggable way to add more data sources (Park4Night, iOverlander, Freecampsites, ...) without their scrapers necessarily living in this (public) repo — sidebar icon (logged in only) lists whatever's under `plugins/`, "Run for this area" uses the current map viewport as the bbox, no separate area-picker needed. Contract: a `plugin.json` manifest + a script that reads `--south`/`--west`/`--north`/`--east` and prints a GeoJSON `FeatureCollection` to stdout (`lib/maptool/plugins.rb`, `GET /api/plugins`, `POST /api/plugins/:name/run`) — output saved the same way an upload would be. `plugins/overpass-pois` ships as the reference example; a private/ToS-sensitive scraper just drops its own directory in locally and is never `git add`ed ** [x] Park4Night — private plugin (`data/plugins/p4n2gpx/`, outside this repo per the "ToS-sensitive scraper" note above); iOverlander/Freecampsites still open, same story diff --git a/public/js/hiking_trails.js b/public/js/hiking_trails.js new file mode 100644 index 0000000..e1201bd --- /dev/null +++ b/public/js/hiking_trails.js @@ -0,0 +1,74 @@ +import { WALKING_ICON } from "/js/icons.js"; +import { firstSymbolLayerId } from "/js/radar.js"; + +const SOURCE_ID = "hiking-trails"; +const LAYER_ID = "hiking-trails-line"; + +// Pre-rendered raster tiles, not vector — waymarkedtrails.org already +// colors each route by its OSM `colour` tag (the KST/KČT-style paint-blaze +// colors used in Slovakia/Czechia), falling back to plain red where a route +// has none, so there's no styling to do here ourselves. Online only, same +// "quietly renders nothing without a network" story as the Wikipedia/Radar/ +// Contour lines overlays — an offline copy (baked into offline.pmtiles) is +// a TODO for once there's a route planner to pair it with, see TODO.adoc. +const TILE_URL = "https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png"; +const ATTRIBUTION = + 'Trails © Waymarked Trails (CC-BY-SA), map data © OpenStreetMap contributors'; +const OPACITY = 0.85; + +// A plain on/off Overlays item, same shape as ContourControl — one static +// raster overlay, no scrub/forecast controls to expand into. +export class HikingTrailsControl { + constructor(map) { + this._map = map; + this._active = false; + + // setStyle() (switching Online/Offline) tears down all custom sources/ + // layers — put this back if it was on, same as every other Overlays + // control. + map.on("style.load", () => { + if (this._active) this._addLayer(); + }); + } + + mount(li) { + li.innerHTML = ` + + `; + this._checkbox = li.querySelector(".overlays-panel__toggle"); + this._checkbox.addEventListener("change", (event) => this._toggle(event.target.checked)); + } + + _toggle(active) { + this._active = active; + if (active) this._addLayer(); + else this._removeLayer(); + } + + _addLayer() { + if (this._map.getSource(SOURCE_ID)) return; + + this._map.addSource(SOURCE_ID, { + type: "raster", + tiles: [TILE_URL], + tileSize: 256, + maxzoom: 17, + attribution: ATTRIBUTION, + }); + // Below labels (same firstSymbolLayerId spot contour.js/radar.js use), + // so place names/POI icons stay readable on top of the trail lines. + this._map.addLayer( + { id: LAYER_ID, type: "raster", source: SOURCE_ID, paint: { "raster-opacity": OPACITY } }, + firstSymbolLayerId(this._map), + ); + } + + _removeLayer() { + if (this._map.getLayer(LAYER_ID)) this._map.removeLayer(LAYER_ID); + if (this._map.getSource(SOURCE_ID)) this._map.removeSource(SOURCE_ID); + } +} diff --git a/public/js/icons.js b/public/js/icons.js index 545f630..7cfd76b 100644 --- a/public/js/icons.js +++ b/public/js/icons.js @@ -328,7 +328,11 @@ const FISHING_ICON = ` const SCENIC_VIEWPOINT_ICON = ` `; -const WALKING_ICON = ` +// Exported (unlike the other POI glyphs around it) — also used as the +// Overlays panel's Hiking trails glyph (hiking_trails.js), same "reuse an +// existing NPS-sourced pictogram" story as WILDERNESS_ICON above rather +// than hand-drawing another one. +export const WALKING_ICON = ` `; const GUIDED_TOURS_ICON = ` diff --git a/public/js/overlays.js b/public/js/overlays.js index 06549ac..996c3f8 100644 --- a/public/js/overlays.js +++ b/public/js/overlays.js @@ -5,6 +5,7 @@ import { svgToImageData, badgeImageId, escapeHtml } from "/js/gpx.js"; import { RadarControl } from "/js/radar.js"; import { ShadowControl } from "/js/shadow.js"; import { ContourControl } from "/js/contour.js"; +import { HikingTrailsControl } from "/js/hiking_trails.js"; const ICON_IMAGE_SIZE = 48; // rasterized once at this size, rendered much smaller by icon-size below const ICON_SIZE = 0.3; // 48 * 0.3 = ~14px, sized to sit inside the marker badge below @@ -184,6 +185,7 @@ export class OverlaysControl { this._radar = new RadarControl(map); this._shadow = new ShadowControl(map); this._contour = new ContourControl(map); + this._hikingTrails = new HikingTrailsControl(map); this._wikiEnabled = false; // the checkbox's own state this._wikiMoveHandler = null; // the moveend listener driving its live refetch, while enabled this._wikiRequestSeq = 0; // ignores an in-flight refetch a newer one has already superseded @@ -217,13 +219,15 @@ export class OverlaysControl {
  • +
  • `; - const [hillshadeItem, wikiItem, radarItem, shadowItem, contourItem] = list.children; + const [hillshadeItem, wikiItem, radarItem, shadowItem, contourItem, hikingTrailsItem] = list.children; this._hillshade.mount(hillshadeItem); this._mountWiki(wikiItem); this._radar.mount(radarItem); this._shadow.mount(shadowItem); this._contour.mount(contourItem); + this._hikingTrails.mount(hikingTrailsItem); onlyOneSidePanelOpen(this._panel, () => this._icon.classList.remove("active")); diff --git a/views/layout.erb b/views/layout.erb index 76e4089..b570715 100644 --- a/views/layout.erb +++ b/views/layout.erb @@ -35,6 +35,7 @@ "/js/dem.js": "/js/dem.js?v=<%= ASSET_VERSION %>", "/js/gnss.js": "/js/gnss.js?v=<%= ASSET_VERSION %>", "/js/gpx.js": "/js/gpx.js?v=<%= ASSET_VERSION %>", + "/js/hiking_trails.js": "/js/hiking_trails.js?v=<%= ASSET_VERSION %>", "/js/icons.js": "/js/icons.js?v=<%= ASSET_VERSION %>", "/js/livetrack.js": "/js/livetrack.js?v=<%= ASSET_VERSION %>", "/js/locate.js": "/js/locate.js?v=<%= ASSET_VERSION %>",