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.
93 lines
2.6 KiB
JavaScript
93 lines
2.6 KiB
JavaScript
import {
|
|
Map as MaplibreMap,
|
|
NavigationControl,
|
|
addProtocol,
|
|
} from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
|
import { RadarControl } from "/js/radar.js";
|
|
|
|
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.
|
|
});
|