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:
randy committed 2026-09-09 09:27:17 +02:00
1 parent f49e44800a
commit 77a4c96b1c
5 files changed
+115 -4

No files matched your search

+75 -1
View File
@@ -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