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

+41 -30
View File
@@ -1,4 +1,4 @@
import { RADAR_ICON } from "/js/icons.js";
import { RADAR_CLOUD_ICON } from "/js/icons.js";
const RAINVIEWER_API = "https://api.rainviewer.com/public/weather-maps.json";
@@ -39,10 +39,10 @@ const OPACITY = 0.7;
const ATTRIBUTION =
'Weather data &copy; <a href="https://www.rainviewer.com" target="_blank">RainViewer</a>';
// A sidebar icon that fetches RainViewer's recent precipitation frames on
// demand and animates them as a raster overlay, with play/pause and a scrub
// slider in a flyout. No API key needed — see docs/architecture.adoc for
// RainViewer's usage terms.
// An Overlays panel item (see overlays.js) that fetches RainViewer's recent
// precipitation frames on demand and animates them as a raster overlay,
// with play/pause and a scrub slider revealed by expanding the item. No API
// key needed — see docs/architecture.adoc for RainViewer's usage terms.
export class RadarControl {
constructor(map) {
this._map = map;
@@ -59,12 +59,19 @@ export class RadarControl {
});
}
mount(sidebar) {
this._container = document.createElement("div");
this._container.className = "sidebar-item";
this._container.innerHTML = `
<button type="button" class="sidebar-icon" title="Radar" aria-label="Radar">${RADAR_ICON}</button>
<div class="sidebar-flyout" hidden>
// li: the <li class="overlays-panel__item"> this item owns (see
// overlays.js) — built here rather than in the static markup since this
// control is the only thing that knows its own internal structure
// (playback/forecast), same as before when it built its own sidebar
// flyout.
mount(li) {
li.innerHTML = `
<div class="overlays-panel__row">
<input type="checkbox" class="overlays-panel__toggle">
<span class="overlays-panel__icon">${RADAR_CLOUD_ICON}</span>
<button type="button" class="overlays-panel__name">Radar</button>
</div>
<div class="overlays-panel__details" hidden>
<div class="radar-control__playback">
<button type="button" class="radar-control__play" aria-label="Play">&#9654;</button>
<input type="range" class="radar-control__scrub" min="0" max="0" step="1" value="0">
@@ -74,27 +81,34 @@ export class RadarControl {
</div>
`;
this._icon = this._container.querySelector(".sidebar-icon");
this._flyout = this._container.querySelector(".sidebar-flyout");
this._playback = this._container.querySelector(".radar-control__playback");
this._playButton = this._container.querySelector(".radar-control__play");
this._scrub = this._container.querySelector(".radar-control__scrub");
this._time = this._container.querySelector(".radar-control__time");
this._forecastEl = this._container.querySelector(".radar-control__forecast");
this._checkbox = li.querySelector(".overlays-panel__toggle");
this._nameButton = li.querySelector(".overlays-panel__name");
this._details = li.querySelector(".overlays-panel__details");
this._playback = li.querySelector(".radar-control__playback");
this._playButton = li.querySelector(".radar-control__play");
this._scrub = li.querySelector(".radar-control__scrub");
this._time = li.querySelector(".radar-control__time");
this._forecastEl = li.querySelector(".radar-control__forecast");
this._icon.addEventListener("click", () => this._setEnabled(!this._enabled));
this._checkbox.addEventListener("change", (event) => this._setEnabled(event.target.checked));
// Separate from the checkbox on purpose — expanding to see the scrub/
// forecast shouldn't also flip the layer on, and turning the layer on
// shouldn't force the details open (a viewer who never cares to scrub
// history/see the forecast keeps a one-line item either way).
this._nameButton.addEventListener("click", () => {
const expanding = this._details.hidden;
this._details.hidden = !expanding;
this._nameButton.setAttribute("aria-expanded", String(expanding));
});
this._playButton.addEventListener("click", () => this._togglePlayback());
this._scrub.addEventListener("input", () => {
this._stopAnimation();
this._showFrame(Number(this._scrub.value));
});
sidebar.appendChild(this._container);
}
async _setEnabled(enabled) {
this._enabled = enabled;
this._icon.classList.toggle("active", enabled);
if (!enabled) {
this._stopAnimation();
this._removeLayers();
@@ -104,15 +118,15 @@ export class RadarControl {
return;
}
this._icon.disabled = true;
this._checkbox.disabled = true;
// Independent fetches: RainViewer being unreachable shouldn't hide a
// forecast that Open-Meteo still answered, or vice versa — especially
// since only the forecast has a cached fallback to fall back to.
const [framesResult, forecastResult] = await Promise.allSettled([fetchFrames(), this._loadForecast()]);
this._icon.disabled = false;
this._checkbox.disabled = false;
// The icon may have been clicked again (disabling) while the fetches
// were in flight.
// The checkbox may have been unchecked again (disabling) while the
// fetches were in flight.
if (!this._enabled) return;
const frames = framesResult.status === "fulfilled" ? framesResult.value : [];
@@ -123,7 +137,7 @@ export class RadarControl {
// own failure.
if (frames.length === 0 && !forecast) {
this._enabled = false;
this._icon.classList.remove("active");
this._checkbox.checked = false;
} else if (frames.length > 0) {
this._frames = frames;
this._frameIndex = frames.length - 1;
@@ -252,9 +266,6 @@ export class RadarControl {
}
_updateFrameUI() {
// Shown whenever there's radar frames, a forecast, or both — either
// one on its own is still worth showing the flyout for.
this._flyout.hidden = !this._enabled;
this._playback.hidden = this._frames.length === 0;
this._scrub.max = String(Math.max(0, this._frames.length - 1));
this._scrub.value = String(this._frameIndex);