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:
1 parent
dce5fd87da
commit
a8d58abaca
6 files changed
+255
-170
No files matched your search
+41
-30
@@ -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 © <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">▶</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);
|
||||
|
||||
Reference in new issue
Block a user