Move Radar and Terrain shading into the Overlays panel

Both used to be their own separate sidebar icons; neither has
anything to manage beyond on/off, same as Wikipedia already got its
own Overlays entry for. Terrain shading is a plain checkbox item now.
Radar keeps its scrub/forecast controls, but behind an explicit
expand (clicking its name), separate from the checkbox — checking it
on shouldn't force the details open, and expanding to scrub/check the
forecast shouldn't also flip the layer on.

Both controls now build their own <li> (RadarControl.mount(li),
HillshadeControl.mount(li) — the latter moved into overlays.js
entirely, it has no reason to live in map.js once it's not building
its own sidebar icon) rather than a whole sidebar-item + flyout the
way they used to.

Also gave each Overlays item a leading icon: Wikipedia and Terrain
shading reuse existing NPS pictograms (WIKI_ICON, and WILDERNESS_ICON
newly exported from icons.js's forest entry — NPS has no dedicated
mountain/terrain glyph, wilderness-22.svg is the closest honest
match). Radar's is hand-drawn (a rain cloud) since NPS's set has
nothing weather-related to reuse.
This commit is contained in:
randy committed 2026-09-10 21:23:51 +02:00
1 parent dce5fd87da
commit a8d58abaca
6 files changed
+255 -170

No files matched your search

+2 -100
View File
@@ -4,7 +4,6 @@ import {
Popup,
addProtocol,
} from "/vendor/maplibre-gl/maplibre-gl.mjs";
import { RadarControl } from "/js/radar.js";
import { GpxPanelControl } from "/js/gpx.js";
import { OverlaysControl } from "/js/overlays.js";
import { GnssControl } from "/js/gnss.js";
@@ -14,7 +13,7 @@ import { MeasureControl } from "/js/measure.js";
import { NotesControl } from "/js/notes.js";
import { PluginsControl } from "/js/plugins.js";
import { LocateControl } from "/js/locate.js";
import { LAYERS_ICON, HILLSHADE_ICON } from "/js/icons.js";
import { LAYERS_ICON } from "/js/icons.js";
import { formatCoords, formatSunInfo } from "/js/sun.js";
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
import { isLoggedIn } from "/js/auth.js";
@@ -310,100 +309,6 @@ class LayerToggleControl {
}
}
const HILLSHADE_ID = "hillshade-dem";
const COLOR_RELIEF_ID = "elevation-tint";
// Hypsometric tint reusing this app's own palette (wood_a's green at
// valley floor, the road casing brown through the mid-slopes) rather than
// a generic rainbow ramp, so it reads as part of the same map rather than
// a bolted-on overlay. Stops picked for Central European terrain — the
// Tatras top out around 2600m — not a general-purpose world ramp.
const ELEVATION_COLOR_RAMP = [
"interpolate",
["linear"],
["raster-value"],
0, "#c3e8ab",
400, "#dcd08f",
900, "#c9a66b",
1600, "#a89a86",
2600, "#f9f6ef",
];
// 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);
// Elevation tint under, hillshade on top: the tint sets the base color
// per elevation band, hillshade multiplies slope-shading over it — the
// combined "characteristic relief look" the TODO asked for, not two
// independent layers.
this._map.addLayer({
id: COLOR_RELIEF_ID,
type: "color-relief",
source: HILLSHADE_ID,
paint: { "color-relief-color": ELEVATION_COLOR_RAMP, "color-relief-opacity": 0.7 },
});
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.getLayer(COLOR_RELIEF_ID)) this._map.removeLayer(COLOR_RELIEF_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
@@ -591,8 +496,6 @@ sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title=
new GpxPanelControl(map, profilePanel).mount(sidebar);
const overlaysControl = new OverlaysControl(map);
overlaysControl.mount(sidebar);
const hillshadeControl = new HillshadeControl(map);
hillshadeControl.mount(sidebar);
// Running a plugin needs a login (POST /api/plugins/:name/run) — same
// "hide the write-only affordance" story as NotesControl above.
if (isLoggedIn()) new PluginsControl(map).mount(sidebar);
@@ -600,7 +503,6 @@ if (isLoggedIn()) new PluginsControl(map).mount(sidebar);
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Offline hub"></div>`);
layerToggle.mount(sidebar);
layerToggle.setActive("online");
new RadarControl(map).mount(sidebar);
sidebar.insertAdjacentHTML("beforeend", `<div class="sidebar-group-label" title="Navigation"></div>`);
gnssControl.mount(sidebar);
@@ -645,7 +547,7 @@ fetch("/api/offline-tiles")
fetch("/api/terrain-tiles")
.then((response) => response.json())
.then(({ available }) => hillshadeControl.setOfflineAvailable(available))
.then(({ available }) => overlaysControl.setHillshadeOfflineAvailable(available))
.catch(() => {});
fetch("/api/kiwix")