Switch to vector maps (MapLibre GL JS + PMTiles) and add RainViewer radar

Replace Leaflet/raster tiles with MapLibre GL JS: an online OpenFreeMap
style and an offline style built from a local PMTiles archive via
pmtiles.js and protomaps-themes-base (no glyphs/labels offline by
design, see docs/architecture.adoc). Frontend libraries are vendored
locally rather than loaded from a CDN so the app keeps working with no
internet access.

Also add an animated RainViewer precipitation radar overlay (toggle,
play/pause, scrub slider), matching the default time window of the
weather-radar-card Home Assistant integration.
This commit is contained in:
randy committed 2026-08-25 01:18:36 +02:00
1 parent 7329497e95
commit 5afb130c45
19 files changed
+1378 -14

No files matched your search

+91 -5
View File
@@ -1,6 +1,92 @@
const map = L.map("map").setView([48.1486, 17.1077], 13);
import {
Map as MaplibreMap,
NavigationControl,
addProtocol,
} from "/vendor/maplibre-gl/maplibre-gl.mjs";
import { RadarControl } from "/js/radar.js";
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "© OpenStreetMap contributors",
}).addTo(map);
const CENTER = [17.1077, 48.1486];
const ZOOM = 13;
const ONLINE_STYLE = "https://tiles.openfreemap.org/styles/liberty";
const protocol = new pmtiles.Protocol();
addProtocol("pmtiles", protocol.tile);
// No label layers here (see docs/architecture.adoc): rendering text needs a
// glyphs server, which the offline style deliberately does not depend on.
function offlineStyle() {
const theme = protomaps_themes_base.namedTheme("light");
const pmtilesUrl = `${window.location.origin}/tiles/offline.pmtiles`;
return {
version: 8,
sources: {
offline: {
type: "vector",
url: `pmtiles://${pmtilesUrl}`,
attribution: "© OpenStreetMap contributors",
},
},
layers: protomaps_themes_base.layers("offline", theme),
};
}
class LayerToggleControl {
constructor(onSelect) {
this._onSelect = onSelect;
}
onAdd() {
this._container = document.createElement("div");
this._container.className = "maplibregl-ctrl maplibregl-ctrl-group layer-toggle";
this._container.innerHTML = `
<button type="button" data-style="online">Online</button>
<button type="button" data-style="offline">Offline</button>
`;
this._container.addEventListener("click", (event) => {
const button = event.target.closest("button[data-style]");
if (button) this._onSelect(button.dataset.style);
});
return this._container;
}
onRemove() {
this._container.remove();
}
setActive(name) {
this._container.querySelectorAll("button").forEach((button) => {
button.classList.toggle("active", button.dataset.style === name);
});
}
}
const map = new MaplibreMap({
container: "map",
style: ONLINE_STYLE,
center: CENTER,
zoom: ZOOM,
});
map.addControl(new NavigationControl(), "top-right");
map.addControl(new RadarControl(map), "top-right");
const layerToggle = new LayerToggleControl(applyStyle);
map.addControl(layerToggle, "top-left");
layerToggle.setActive("online");
function applyStyle(name) {
layerToggle.setActive(name);
map.setStyle(name === "offline" ? offlineStyle() : ONLINE_STYLE);
}
// If a local PMTiles file is present, prefer it as the default style — its
// presence signals the app is meant to be used offline here.
fetch("/api/offline-tiles")
.then((response) => response.json())
.then(({ available }) => {
if (available) applyStyle("offline");
})
.catch(() => {
// Backend check failed; keep the online style.
});