Add a Hiking trails overlay (Waymarked Trails, online only)
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.
This commit is contained in:
1 parent
c894706140
commit
9c45e7275c
5 files changed
+86
-2
No files matched your search
@@ -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
|
* [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)
|
** [ ] Terrain slope readout for parking (numeric, not just visual shading)
|
||||||
* [ ] Contour lines from OSM for hikes and mountain passes
|
* [ ] 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] 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] 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
|
** [x] Park4Night — private plugin (`data/plugins/p4n2gpx/`, outside this repo per the "ToS-sensitive scraper" note above); iOverlander/Freecampsites still open, same story
|
||||||
|
|||||||
@@ -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 © <a href="https://waymarkedtrails.org" target="_blank">Waymarked Trails</a> (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 = `
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="overlays-panel__toggle">
|
||||||
|
<span class="overlays-panel__icon">${WALKING_ICON}</span>
|
||||||
|
<span>Hiking trails</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -328,7 +328,11 @@ const FISHING_ICON = `
|
|||||||
const SCENIC_VIEWPOINT_ICON = `
|
const SCENIC_VIEWPOINT_ICON = `
|
||||||
<svg viewBox="0 0 22 22" fill="currentColor"><g transform="translate(4.000000, 0.000000)"><path d="M3.2,11c0,1.2-0.9,2.2-2.1,2.2c-1.2,0-2.1-1-2.1-2.2 c0-1.2,0.9-2.2,2.1-2.2C2.2,8.8,3.2,9.8,3.2,11"/><path d="M4.6,10.4v1.3l10.6,0.8v-3L4.6,10.4"/><path d="M2.3,7.5l1.1,0.7L7.7,2L5.3,0.5L2.3,7.5"/><path d="M4.4,9.8l7.9-3.8l-1.4-2.6L3.8,8.6L4.4,9.8z"/><path d="M2.3,14.5l1.1-0.7l4.3,6.2l-2.4,1.5L2.3,14.5"/><path d="M3.8,13.4l0.6-1.2l7.9,3.8l-1.4,2.6L3.8,13.4"/></g></svg>`;
|
<svg viewBox="0 0 22 22" fill="currentColor"><g transform="translate(4.000000, 0.000000)"><path d="M3.2,11c0,1.2-0.9,2.2-2.1,2.2c-1.2,0-2.1-1-2.1-2.2 c0-1.2,0.9-2.2,2.1-2.2C2.2,8.8,3.2,9.8,3.2,11"/><path d="M4.6,10.4v1.3l10.6,0.8v-3L4.6,10.4"/><path d="M2.3,7.5l1.1,0.7L7.7,2L5.3,0.5L2.3,7.5"/><path d="M4.4,9.8l7.9-3.8l-1.4-2.6L3.8,8.6L4.4,9.8z"/><path d="M2.3,14.5l1.1-0.7l4.3,6.2l-2.4,1.5L2.3,14.5"/><path d="M3.8,13.4l0.6-1.2l7.9,3.8l-1.4,2.6L3.8,13.4"/></g></svg>`;
|
||||||
|
|
||||||
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 = `
|
||||||
<svg viewBox="0 0 22 22" fill="currentColor"><path d="M10.4,3.6c1,0,1.8-0.8,1.7-1.8c0-1-0.8-1.8-1.8-1.7c-1,0-1.8,0.8-1.7,1.8C8.6,2.8,9.5,3.6,10.4,3.6z"/><path d="M16.5,9.5L13.9,8l-2.2-3.4c-0.3-0.4-0.9-0.8-1.5-0.7C9.7,4,9.3,4.2,9,4.4L5.8,7.7C5.7,7.8,5.6,8,5.6,8.1l-0.4,3.6 c0,0,0,0.1,0,0.1c0,0.4,0.4,0.8,0.8,0.8c0.4,0,0.7-0.4,0.8-0.8l0.3-3.1l1.1-1.1l-1,8.6l-1.9,4.1c-0.1,0.1-0.1,0.3-0.1,0.5 c0,0.7,0.6,1.2,1.2,1.2c0.5,0,0.9-0.3,1.1-0.7l2-4.4c0-0.1,0.2-0.4,0.2-0.5l0.2-2.1l2.1,6.9c0.2,0.5,0.6,0.8,1.2,0.8 c0.7,0,1.2-0.6,1.2-1.2c0-0.1,0-0.1,0-0.2L11.5,11l0.4-3.1l0.8,1.3c0.1,0.1,0.1,0.2,0.2,0.2l2.8,1.6c0.1,0,0.2,0.1,0.4,0.1 c0.4,0,0.8-0.4,0.8-0.8C16.9,9.8,16.7,9.6,16.5,9.5z"/></svg>`;
|
<svg viewBox="0 0 22 22" fill="currentColor"><path d="M10.4,3.6c1,0,1.8-0.8,1.7-1.8c0-1-0.8-1.8-1.8-1.7c-1,0-1.8,0.8-1.7,1.8C8.6,2.8,9.5,3.6,10.4,3.6z"/><path d="M16.5,9.5L13.9,8l-2.2-3.4c-0.3-0.4-0.9-0.8-1.5-0.7C9.7,4,9.3,4.2,9,4.4L5.8,7.7C5.7,7.8,5.6,8,5.6,8.1l-0.4,3.6 c0,0,0,0.1,0,0.1c0,0.4,0.4,0.8,0.8,0.8c0.4,0,0.7-0.4,0.8-0.8l0.3-3.1l1.1-1.1l-1,8.6l-1.9,4.1c-0.1,0.1-0.1,0.3-0.1,0.5 c0,0.7,0.6,1.2,1.2,1.2c0.5,0,0.9-0.3,1.1-0.7l2-4.4c0-0.1,0.2-0.4,0.2-0.5l0.2-2.1l2.1,6.9c0.2,0.5,0.6,0.8,1.2,0.8 c0.7,0,1.2-0.6,1.2-1.2c0-0.1,0-0.1,0-0.2L11.5,11l0.4-3.1l0.8,1.3c0.1,0.1,0.1,0.2,0.2,0.2l2.8,1.6c0.1,0,0.2,0.1,0.4,0.1 c0.4,0,0.8-0.4,0.8-0.8C16.9,9.8,16.7,9.6,16.5,9.5z"/></svg>`;
|
||||||
|
|
||||||
const GUIDED_TOURS_ICON = `
|
const GUIDED_TOURS_ICON = `
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { svgToImageData, badgeImageId, escapeHtml } from "/js/gpx.js";
|
|||||||
import { RadarControl } from "/js/radar.js";
|
import { RadarControl } from "/js/radar.js";
|
||||||
import { ShadowControl } from "/js/shadow.js";
|
import { ShadowControl } from "/js/shadow.js";
|
||||||
import { ContourControl } from "/js/contour.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_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
|
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._radar = new RadarControl(map);
|
||||||
this._shadow = new ShadowControl(map);
|
this._shadow = new ShadowControl(map);
|
||||||
this._contour = new ContourControl(map);
|
this._contour = new ContourControl(map);
|
||||||
|
this._hikingTrails = new HikingTrailsControl(map);
|
||||||
this._wikiEnabled = false; // the checkbox's own state
|
this._wikiEnabled = false; // the checkbox's own state
|
||||||
this._wikiMoveHandler = null; // the moveend listener driving its live refetch, while enabled
|
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
|
this._wikiRequestSeq = 0; // ignores an in-flight refetch a newer one has already superseded
|
||||||
@@ -217,13 +219,15 @@ export class OverlaysControl {
|
|||||||
<li class="overlays-panel__item overlays-panel__item--expandable"></li>
|
<li class="overlays-panel__item overlays-panel__item--expandable"></li>
|
||||||
<li class="overlays-panel__item overlays-panel__item--expandable"></li>
|
<li class="overlays-panel__item overlays-panel__item--expandable"></li>
|
||||||
<li class="overlays-panel__item"></li>
|
<li class="overlays-panel__item"></li>
|
||||||
|
<li class="overlays-panel__item"></li>
|
||||||
`;
|
`;
|
||||||
const [hillshadeItem, wikiItem, radarItem, shadowItem, contourItem] = list.children;
|
const [hillshadeItem, wikiItem, radarItem, shadowItem, contourItem, hikingTrailsItem] = list.children;
|
||||||
this._hillshade.mount(hillshadeItem);
|
this._hillshade.mount(hillshadeItem);
|
||||||
this._mountWiki(wikiItem);
|
this._mountWiki(wikiItem);
|
||||||
this._radar.mount(radarItem);
|
this._radar.mount(radarItem);
|
||||||
this._shadow.mount(shadowItem);
|
this._shadow.mount(shadowItem);
|
||||||
this._contour.mount(contourItem);
|
this._contour.mount(contourItem);
|
||||||
|
this._hikingTrails.mount(hikingTrailsItem);
|
||||||
|
|
||||||
onlyOneSidePanelOpen(this._panel, () => this._icon.classList.remove("active"));
|
onlyOneSidePanelOpen(this._panel, () => this._icon.classList.remove("active"));
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"/js/dem.js": "/js/dem.js?v=<%= ASSET_VERSION %>",
|
"/js/dem.js": "/js/dem.js?v=<%= ASSET_VERSION %>",
|
||||||
"/js/gnss.js": "/js/gnss.js?v=<%= ASSET_VERSION %>",
|
"/js/gnss.js": "/js/gnss.js?v=<%= ASSET_VERSION %>",
|
||||||
"/js/gpx.js": "/js/gpx.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/icons.js": "/js/icons.js?v=<%= ASSET_VERSION %>",
|
||||||
"/js/livetrack.js": "/js/livetrack.js?v=<%= ASSET_VERSION %>",
|
"/js/livetrack.js": "/js/livetrack.js?v=<%= ASSET_VERSION %>",
|
||||||
"/js/locate.js": "/js/locate.js?v=<%= ASSET_VERSION %>",
|
"/js/locate.js": "/js/locate.js?v=<%= ASSET_VERSION %>",
|
||||||
|
|||||||
Reference in new issue
Block a user