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

+142 -15
View File
@@ -1,7 +1,8 @@
import { Popup } from "/vendor/maplibre-gl/maplibre-gl.mjs";
import { OVERLAYS_ICON, WIKI_ICON, MAP_MARKER_ICON } from "/js/icons.js";
import { OVERLAYS_ICON, WIKI_ICON, WILDERNESS_ICON, MAP_MARKER_ICON } from "/js/icons.js";
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
import { svgToImageData, badgeImageId, escapeHtml } from "/js/gpx.js";
import { RadarControl } from "/js/radar.js";
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
@@ -11,16 +12,115 @@ const WIKI_SOURCE_ID = "wiki-points";
const WIKI_ICON_ID = "wiki-icon";
const WIKI_BADGE_COLOR = "#795548";
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 plain on/off Overlays item — there's nothing to choose, just whether
// the shading shows, unlike Radar's expandable scrub/forecast.
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(li) {
li.innerHTML = `
<label>
<input type="checkbox" class="overlays-panel__toggle">
<span class="overlays-panel__icon">${WILDERNESS_ICON}</span>
<span>Terrain shading</span>
</label>
`;
this._checkbox = li.querySelector(".overlays-panel__toggle");
this._checkbox.addEventListener("change", (event) => this._toggle(event.target.checked));
}
setOfflineAvailable(available) {
this._offlineAvailable = available;
}
_toggle(active) {
this._active = active;
if (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 sidebar side panel (same shell as LiveTrackPanelControl/#livetrack-panel)
// listing live map layers that aren't a base map, a Tracks & Points file, or
// a fixed on/off toggle (Hillshade) — each one refetched from its own
// source as the map moves, not something to add/remove/reorder the way
// Tracks & Points' file tree is, so this is a plain checklist. Wikipedia is
// the first entry; more can join the same #overlays-panel list later
// without a redesign.
// listing live map layers that aren't a base map or a Tracks & Points file —
// each one refetched/toggled live rather than something to add/remove/
// reorder, so this is a plain checklist, its items built here (like
// PluginsControl's own list) rather than as static markup. Radar and
// Terrain shading used to be their own separate sidebar icons; both moved
// here since neither has anything to "manage," just on/off (Radar's own
// scrub/forecast controls live behind expanding its item, not a factor in
// where it's mounted).
export class OverlaysControl {
constructor(map) {
this._map = map;
this._hillshade = new HillshadeControl(map);
this._radar = new RadarControl(map);
this._wikiEnabled = false; // the checkbox's own state
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
@@ -28,7 +128,8 @@ export class OverlaysControl {
// setStyle() (switching the Online/Offline base map) tears down all
// custom sources/layers/images — put the wiki layer back afterwards,
// same as GpxPanelControl does for visible GPX files.
// same as GpxPanelControl does for visible GPX files. Hillshade/Radar
// handle their own the same way, from their own constructors above.
map.on("style.load", () => {
if (this._wikiEnabled) this._addWikiLayers().then(() => this._refreshWikiLayer());
});
@@ -45,11 +146,17 @@ export class OverlaysControl {
this._panel
.querySelector(".basemap-panel__close")
.addEventListener("click", () => this._togglePanel(false));
this._wikiToggle = this._panel.querySelector(".overlays-panel__wiki-toggle");
this._wikiToggle.addEventListener("change", (event) => {
if (event.target.checked) this._enableWikiLayer();
else this._disableWikiLayer();
});
const list = this._panel.querySelector(".overlays-panel__list");
list.innerHTML = `
<li class="overlays-panel__item"></li>
<li class="overlays-panel__item"></li>
<li class="overlays-panel__item overlays-panel__item--expandable"></li>
`;
const [hillshadeItem, wikiItem, radarItem] = list.children;
this._hillshade.mount(hillshadeItem);
this._mountWiki(wikiItem);
this._radar.mount(radarItem);
onlyOneSidePanelOpen(this._panel, () => this._icon.classList.remove("active"));
@@ -63,13 +170,33 @@ export class OverlaysControl {
if (open) notifySidePanelOpen(this._panel);
}
setHillshadeOfflineAvailable(available) {
this._hillshade.setOfflineAvailable(available);
}
_mountWiki(li) {
li.innerHTML = `
<label>
<input type="checkbox" class="overlays-panel__toggle">
<span class="overlays-panel__icon">${WIKI_ICON}</span>
<span>Wikipedia</span>
</label>
`;
this._wikiItem = li;
this._wikiToggle = li.querySelector(".overlays-panel__toggle");
this._wikiToggle.addEventListener("change", (event) => {
if (event.target.checked) this._enableWikiLayer();
else this._disableWikiLayer();
});
}
// Called once map.js's one-time GET /api/kiwix check resolves — disables
// (rather than hides) the checkbox rather than the whole panel/icon,
// since a future second overlay could still be usable with no kiwix-serve
// since the other overlays are still usable with no kiwix-serve
// configured at all.
setWikiAvailable(available) {
this._wikiToggle.disabled = !available;
if (!available) this._wikiToggle.closest(".overlays-panel__item").title = "No kiwix-serve configured";
if (!available) this._wikiItem.title = "No kiwix-serve configured";
}
async _enableWikiLayer() {