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
@@ -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