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
@@ -1214,6 +1214,47 @@ html, body {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.overlays-panel__icon {
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.overlays-panel__icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overlays-panel__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.overlays-panel__name {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.overlays-panel__name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.overlays-panel__details {
|
||||
margin: 0 4px 6px 26px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.livetrack-panel__list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
+20
-13
@@ -17,12 +17,6 @@ export const OVERLAYS_ICON = `
|
||||
<circle cx="15" cy="12" r="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 NOTE_PIN_ICON = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 21s7-7.58 7-12a7 7 0 0 0-14 0c0 4.42 7 12 7 12z"/>
|
||||
@@ -36,11 +30,6 @@ export const GPX_ICON = `
|
||||
<path d="M6.5 7.5C10 11 8 13 12 13s2 5 6.5 5"/>
|
||||
</svg>`;
|
||||
|
||||
export const RADAR_ICON = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M7 18a4 4 0 1 1 .7-7.94A5 5 0 0 1 17 9a4 4 0 0 1 0 8H7z"/>
|
||||
</svg>`;
|
||||
|
||||
export const GNSS_ICON = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="5.5"/>
|
||||
@@ -151,6 +140,25 @@ export const MAP_MARKER_ICON = `
|
||||
// tinted at render time, see gpx.js's _registerCategoryGlyphs) so only the
|
||||
// alpha shape matters, not the actual `currentColor` value.
|
||||
|
||||
// src/wilderness-22.svg — exported (unlike BICYCLE_TRAIL_ICON below, this
|
||||
// one's also used outside this file) as the Overlays panel's Terrain
|
||||
// shading glyph (overlays.js): the closest honest match in NPS's set for
|
||||
// "mountains/terrain" — it has no weather or elevation-specific pictogram.
|
||||
export const WILDERNESS_ICON = `
|
||||
<svg viewBox="0 0 22 22" fill="currentColor"><polygon points="18,18 13,15 17,15 13,11 16,11 12,7 15,7 12,4 14,4 11,0 8,4 10,4 7,7 10,7 6,11 9,11 5,15 9,15 4,18 10,18 10,22 12,22 12,18 "/></svg>`;
|
||||
|
||||
// Hand-drawn, not NPS-sourced — that set has no weather/precipitation
|
||||
// pictogram to reuse for the Overlays panel's Radar item, unlike Wikipedia
|
||||
// (WIKI_ICON) and Terrain shading (WILDERNESS_ICON) above.
|
||||
export const RADAR_CLOUD_ICON = `
|
||||
<svg viewBox="0 0 22 22" fill="currentColor">
|
||||
<path d="M17,10.1c-0.4-3-2.9-5.3-6-5.3c-2.5,0-4.7,1.5-5.6,3.8C3.4,8.9,1,11.1,1,13.9C1,16.7,3.3,19,6.1,19h10.3
|
||||
c2.5,0,4.6-2,4.6-4.5C21,12.2,19.3,10.4,17,10.1z"/>
|
||||
<rect x="6" y="20" width="1.6" height="2"/>
|
||||
<rect x="10.2" y="20" width="1.6" height="2"/>
|
||||
<rect x="14.4" y="20" width="1.6" height="2"/>
|
||||
</svg>`;
|
||||
|
||||
// src/bicycle-trail-22.svg — pulled out to its own constant since it's also
|
||||
// used unrasterized, inline, as PARK4NIGHT_TAGS.vtt below.
|
||||
const BICYCLE_TRAIL_ICON = `
|
||||
@@ -213,8 +221,7 @@ export const POI_ICONS = {
|
||||
</svg>`,
|
||||
|
||||
// src/wilderness-22.svg
|
||||
forest: `
|
||||
<svg viewBox="0 0 22 22" fill="currentColor"><polygon points="18,18 13,15 17,15 13,11 16,11 12,7 15,7 12,4 14,4 11,0 8,4 10,4 7,7 10,7 6,11 9,11 5,15 9,15 4,18 10,18 10,22 12,22 12,18 "/></svg>`,
|
||||
forest: WILDERNESS_ICON,
|
||||
|
||||
// src/bicycle-trail-22.svg
|
||||
sport_cycling: BICYCLE_TRAIL_ICON,
|
||||
|
||||
+2
-100
@@ -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")
|
||||
|
||||
+142
-15
@@ -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() {
|
||||
|
||||
+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