Add a hillshade/terrain-shading toggle to the map
Sidebar toggle (new HillshadeControl) adds a raster-dem hillshade layer: public Terrarium tiles when online, or a local data/terrain.pmtiles when present (mirrors offline.pmtiles' optional- local-file pattern, new /api/terrain-tiles + /tiles/terrain.pmtiles endpoints). No offline terrain data is bundled, so the toggle is online-only until one is generated — see docs/development.adoc for how.
This commit is contained in:
1 parent
f49e44800a
commit
77a4c96b1c
5 files changed
+115
-4
No files matched your search
@@ -9,8 +9,9 @@ independence from connectivity.
|
||||
== Offline data and cartography
|
||||
|
||||
* [ ] Offline vector tiles (PMTiles/MBTiles) for Europe/world via Planetiler or MapTiler
|
||||
* [ ] Shaded relief / DEM layer (terrain slope for parking)
|
||||
* [ ] Hillshade & contour lines from OSM for hikes and mountain passes
|
||||
* [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
|
||||
* [ ] Overpass/OSM scripts to extract POIs (drinking water, dump stations, LPG, public toilets, laundromats, shops, springs)
|
||||
* [ ] Import Park4Night / iOverlander / Freecampsites data
|
||||
* [ ] Kiwix/ZIM offline Wikipedia linked to POI GPS coordinates
|
||||
@@ -25,7 +26,7 @@ independence from connectivity.
|
||||
** [x] Filter panel as a "Unigrid Matrix" — an icon grid to toggle categories on/off, styled like the legend on an official NPS paper map, instead of plain dropdowns
|
||||
* [ ] Custom vector style (MVT/MapLibre) using the NPS color palette — muted natural tones for forests, water, and contours — so POIs and routes stand out at high zoom
|
||||
** [ ] Landuse & protections: distinct styling for national parks, nature reserves (translucent green/gray-green), military zones, and private land (important for wild camping)
|
||||
** [ ] Elevation-tinted terrain combined with offline hillshade for a characteristic relief look
|
||||
** [ ] Elevation-tinted terrain (color by height band, not just shading — see the hillshade toggle above)
|
||||
** [x] Road hierarchy: clearly distinguish paved roads from unpaved/gravel/track — critical for knowing if the camper can get there
|
||||
|
||||
== Main menu reorganization (4 modules)
|
||||
|
||||
@@ -58,6 +58,7 @@ module Maptool
|
||||
DATA_DIR = ENV.fetch("MAPTOOL_DATA_DIR") { File.join(__dir__, "data") }
|
||||
|
||||
OFFLINE_PMTILES_PATH = File.join(DATA_DIR, "offline.pmtiles")
|
||||
TERRAIN_PMTILES_PATH = File.join(DATA_DIR, "terrain.pmtiles")
|
||||
GPX_STORE = GpxStore.new(File.join(DATA_DIR, "gpx"))
|
||||
LIVETRACK_STORE = LiveTrackStore.new(File.join(DATA_DIR, "livetrack.json"))
|
||||
|
||||
@@ -185,6 +186,19 @@ module Maptool
|
||||
send_file OFFLINE_PMTILES_PATH, type: "application/octet-stream"
|
||||
end
|
||||
|
||||
# Same optional-local-file story as offline.pmtiles above — see
|
||||
# docs/development.adoc for how to generate one, until then hillshade
|
||||
# just stays unavailable while the offline basemap is active.
|
||||
get "/api/terrain-tiles" do
|
||||
json available: File.exist?(TERRAIN_PMTILES_PATH)
|
||||
end
|
||||
|
||||
get "/tiles/terrain.pmtiles" do
|
||||
halt 404 unless File.exist?(TERRAIN_PMTILES_PATH)
|
||||
|
||||
send_file TERRAIN_PMTILES_PATH, type: "application/octet-stream"
|
||||
end
|
||||
|
||||
get "/api/gpx/tree" do
|
||||
json GPX_STORE.tree
|
||||
end
|
||||
|
||||
@@ -43,6 +43,22 @@ build one locally:
|
||||
Once the file is in place, `GET /api/offline-tiles` reports
|
||||
`available: true` and the map defaults to the offline style.
|
||||
|
||||
== Offline terrain data (hillshade)
|
||||
|
||||
The sidebar's "Terrain shading" toggle works online out of the box
|
||||
(public https://github.com/tilezen/joerd[Terrarium] elevation tiles,
|
||||
no API key). For it to work offline too, place a PMTiles archive of
|
||||
Terrarium-encoded `raster-dem` tiles at `data/terrain.pmtiles`
|
||||
(`GET /api/terrain-tiles` reports `available: true` once it's there,
|
||||
same pattern as offline map data above). It is not part of the
|
||||
repository — build one locally from a DEM source (e.g. Copernicus
|
||||
GLO-30 or SRTM) with a tool like
|
||||
https://github.com/mapbox/rio-rgbify[rio-rgbify] (GeoTIFF ->
|
||||
Terrarium-encoded PNG tiles in an `.mbtiles`), then
|
||||
`pmtiles convert out.mbtiles data/terrain.pmtiles` with
|
||||
https://github.com/protomaps/go-pmtiles[go-pmtiles], same as
|
||||
`data/offline.pmtiles`.
|
||||
|
||||
== GNSS position (gpsd)
|
||||
|
||||
The "GNSS" control needs https://gpsd.io[gpsd] running and pointed at
|
||||
|
||||
@@ -7,6 +7,12 @@ export const LAYERS_ICON = `
|
||||
<path d="M2 14l10 6 10-6"/>
|
||||
</svg>`;
|
||||
|
||||
export const HILLSHADE_ICON = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M2 18 9 7l3.5 5.5L15 9l7 9z"/>
|
||||
<path d="M2 18h20"/>
|
||||
</svg>`;
|
||||
|
||||
export const GPX_ICON = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="5" cy="6" r="2"/>
|
||||
|
||||
+75
-1
@@ -11,7 +11,7 @@ import { LiveTrackPanelControl } from "/js/livetrack.js";
|
||||
import { TrackProfilePanel } from "/js/track_profile.js";
|
||||
import { MeasureControl } from "/js/measure.js";
|
||||
import { LocateControl } from "/js/locate.js";
|
||||
import { LAYERS_ICON } from "/js/icons.js";
|
||||
import { LAYERS_ICON, HILLSHADE_ICON } from "/js/icons.js";
|
||||
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
|
||||
import HomeButton from "/vendor/npmap-home-button/home-button.esm.js";
|
||||
import OverviewMap from "/vendor/npmap-overview/overview.esm.js";
|
||||
@@ -305,6 +305,73 @@ class LayerToggleControl {
|
||||
}
|
||||
}
|
||||
|
||||
const HILLSHADE_ID = "hillshade-dem";
|
||||
// Public, unauthenticated, no API key — the same open (SRTM/ArcticDEM-derived)
|
||||
// elevation tiles OpenStreetMap's own hillshade layer and many other free
|
||||
// tools use. Only reachable online; while the offline basemap is active this
|
||||
// is skipped in favor of a local terrain.pmtiles if one's been generated
|
||||
// (see docs/development.adoc), same optional-local-file story as
|
||||
// offline.pmtiles itself.
|
||||
const ONLINE_TERRAIN_TILES = ["https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"];
|
||||
|
||||
// A simple on/off sidebar toggle, not a panel — there's nothing to choose,
|
||||
// just whether the shading shows.
|
||||
class HillshadeControl {
|
||||
constructor(map) {
|
||||
this._map = map;
|
||||
this._active = false;
|
||||
this._offlineAvailable = false;
|
||||
// setStyle() (switching Online/Offline) tears down all custom
|
||||
// sources/layers — put hillshade back if it was on.
|
||||
map.on("style.load", () => {
|
||||
if (this._active) this._addLayer();
|
||||
});
|
||||
}
|
||||
|
||||
mount(sidebar) {
|
||||
this._container = document.createElement("div");
|
||||
this._container.className = "sidebar-item";
|
||||
this._container.innerHTML = `<button type="button" class="sidebar-icon" title="Terrain shading" aria-label="Terrain shading" aria-pressed="false">${HILLSHADE_ICON}</button>`;
|
||||
this._button = this._container.querySelector(".sidebar-icon");
|
||||
this._button.addEventListener("click", () => this._toggle());
|
||||
sidebar.appendChild(this._container);
|
||||
}
|
||||
|
||||
setOfflineAvailable(available) {
|
||||
this._offlineAvailable = available;
|
||||
}
|
||||
|
||||
_toggle() {
|
||||
this._active = !this._active;
|
||||
this._button.classList.toggle("active", this._active);
|
||||
this._button.setAttribute("aria-pressed", String(this._active));
|
||||
if (this._active) this._addLayer();
|
||||
else this._removeLayer();
|
||||
}
|
||||
|
||||
_addLayer() {
|
||||
if (this._map.getSource(HILLSHADE_ID)) return;
|
||||
|
||||
const source = this._offlineAvailable
|
||||
? { type: "raster-dem", url: `pmtiles://${window.location.origin}/tiles/terrain.pmtiles`, encoding: "terrarium" }
|
||||
: {
|
||||
type: "raster-dem",
|
||||
tiles: ONLINE_TERRAIN_TILES,
|
||||
tileSize: 256,
|
||||
encoding: "terrarium",
|
||||
maxzoom: 15,
|
||||
attribution: "Terrain tiles: Mapzen, SRTM, ArcticDEM",
|
||||
};
|
||||
this._map.addSource(HILLSHADE_ID, source);
|
||||
this._map.addLayer({ id: HILLSHADE_ID, type: "hillshade", source: HILLSHADE_ID, paint: { "hillshade-exaggeration": 0.4 } });
|
||||
}
|
||||
|
||||
_removeLayer() {
|
||||
if (this._map.getLayer(HILLSHADE_ID)) this._map.removeLayer(HILLSHADE_ID);
|
||||
if (this._map.getSource(HILLSHADE_ID)) this._map.removeSource(HILLSHADE_ID);
|
||||
}
|
||||
}
|
||||
|
||||
// A small map-corner button (grouped with Home/zoom, see below) that's a
|
||||
// second entry point to the same panel LayerToggleControl already manages
|
||||
// in the page's own sidebar — matching the National Park Service's own
|
||||
@@ -454,6 +521,8 @@ profilePanel.mount();
|
||||
|
||||
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Explorer"></div>`);
|
||||
new GpxPanelControl(map, profilePanel).mount(sidebar);
|
||||
const hillshadeControl = new HillshadeControl(map);
|
||||
hillshadeControl.mount(sidebar);
|
||||
|
||||
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Offline hub"></div>`);
|
||||
layerToggle.mount(sidebar);
|
||||
@@ -501,6 +570,11 @@ fetch("/api/offline-tiles")
|
||||
// failing, not evidence offline tiles are actually unavailable.
|
||||
});
|
||||
|
||||
fetch("/api/terrain-tiles")
|
||||
.then((response) => response.json())
|
||||
.then(({ available }) => hillshadeControl.setOfflineAvailable(available))
|
||||
.catch(() => {});
|
||||
|
||||
// Alt+click anywhere shows every rendered feature at that point — base map
|
||||
// (OSM tags via OpenFreeMap/protomaps), GPX/FIT layers, radar, all of it —
|
||||
// with its raw properties, inspired by NPMap4's infoboxControl (a generic
|
||||
|
||||
Reference in new issue
Block a user