Add a debug page for comparing offline base-map themes side by side
CI / check (push) Successful in 19s
CI / deploy (push) Successful in 13s

Renders the bare protomaps-themes-base themes next to the app's actual
offline/online styles (each real app instance, embedded), with pan/zoom
kept in sync via the existing URL-hash mechanism, to make it easy to see
where the hand-tuned offline palette diverges from the online one.
This commit is contained in:
randy committed 2026-09-11 16:58:32 +02:00
1 parent 9223410b40
commit 0874e8f8ed
2 files changed
+122

No files matched your search

+11
View File
@@ -23,6 +23,17 @@ explicit cache headers, so an edit to `public/` (or `views/`) can
appear not to have taken effect on the next reload. Force a real
reload (e.g. Ctrl/Cmd+Shift+R) if that happens.
`/theme_preview.html` is a standalone debug page (not linked from the
app itself) for comparing offline base-map themes side by side: a bare
`protomaps-themes-base` theme, an iframe of the real app's offline
style, and one of its online style, with pan/zoom kept in sync across
all three via the same URL-hash mechanism `map.js` already uses for
shareable links. It's a plain static file, so nothing gates it to
development — it's reachable on a production instance too, same as any
other file under `public/`. Harmless if it stays that way (it shows
nothing `/` doesn't already), but worth knowing if that's ever not the
case.
== Offline map data
To enable the offline style, place a PMTiles file at
+111
View File
@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Offline theme preview</title>
<link rel="stylesheet" href="/vendor/maplibre-gl/maplibre-gl.css">
<style>
body { margin: 0; font-family: sans-serif; background: #222; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; }
.cell { position: relative; height: 90vh; }
.cell .map, .cell iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.cell .label {
position: absolute; top: 4px; left: 4px; z-index: 1;
background: rgba(0,0,0,.6); color: #fff; padding: 2px 8px;
font-size: 13px; border-radius: 3px;
}
</style>
</head>
<body>
<div class="grid">
<div class="cell">
<div class="label">light (bare base theme)</div>
<div class="map" id="light-map"></div>
</div>
<div class="cell">
<div class="label">offline (current app)</div>
<iframe id="offline-frame" src="/"></iframe>
</div>
<div class="cell">
<div class="label">online (current app)</div>
<iframe id="online-frame" src="/"></iframe>
</div>
</div>
<script src="/vendor/pmtiles/pmtiles.js"></script>
<script src="/vendor/protomaps-themes-base/protomaps-themes-base.js"></script>
<script type="module">
import { Map as MaplibreMap, addProtocol } from "/vendor/maplibre-gl/maplibre-gl.mjs";
const protocol = new pmtiles.Protocol();
addProtocol("pmtiles", protocol.tile);
const CENTER = [17.1077, 48.1486];
const ZOOM = 13;
const pmtilesUrl = `${window.location.origin}/tiles/offline.pmtiles`;
const lightMap = new MaplibreMap({
container: "light-map",
center: CENTER,
zoom: ZOOM,
style: {
version: 8,
glyphs: `${window.location.origin}/vendor/fonts/{fontstack}/{range}.pbf`,
sources: {
offline: { type: "vector", url: `pmtiles://${pmtilesUrl}` },
},
layers: protomaps_themes_base.layers("offline", protomaps_themes_base.namedTheme("light"), { lang: "en" }),
},
});
// "offline" frame needs nothing: the app already defaults to its offline
// style whenever /api/offline-tiles reports data present (see map.js).
// "online" frame: click its own real Online button, same button a user
// would click — reuses the app's actual ONLINE_STYLE (patched Liberty)
// instead of re-fetching/re-patching a second copy here.
const offlineFrame = document.getElementById("offline-frame");
const onlineFrame = document.getElementById("online-frame");
const clickOnline = () => {
const button = onlineFrame.contentDocument?.querySelector('.basemap-panel__option[data-style="online"]');
if (button) button.click();
};
onlineFrame.addEventListener("load", () => {
clickOnline();
setTimeout(clickOnline, 1000); // retry once in case map.js was still mid-init
});
// Keep all three views looking at the same spot. The two iframes are the
// real app (separate documents, own module scope — nothing to reach into
// directly), but map.js already syncs its map to the URL hash (`hash:
// "map"`, format #map=zoom/lat/lon) for shareable links. Reusing that: poll
// each source's position, and whichever moved gets pushed to the other
// two — for a frame, by writing its location.hash, which fires its own
// hashchange and lets its Hash control jumpTo for us.
const HASH_RE = /map=([\d.]+)\/(-?[\d.]+)\/(-?[\d.]+)/;
function frameHash(frame) {
const match = HASH_RE.exec(frame.contentWindow?.location.hash ?? "");
return match ? { zoom: +match[1], lat: +match[2], lon: +match[3] } : null;
}
function lightPos() {
const c = lightMap.getCenter();
return { zoom: lightMap.getZoom(), lat: c.lat, lon: c.lng };
}
function samePos(a, b) {
return Math.abs(a.zoom - b.zoom) < 0.005 && Math.abs(a.lat - b.lat) < 1e-4 && Math.abs(a.lon - b.lon) < 1e-4;
}
let current = { zoom: ZOOM, lat: CENTER[1], lon: CENTER[0] };
setInterval(() => {
const sources = { light: lightPos(), offline: frameHash(offlineFrame), online: frameHash(onlineFrame) };
const leader = Object.keys(sources).find((name) => sources[name] && !samePos(sources[name], current));
if (!leader) return;
current = sources[leader];
if (leader !== "light") lightMap.jumpTo({ center: [current.lon, current.lat], zoom: current.zoom });
if (leader !== "offline" && offlineFrame.contentWindow) {
offlineFrame.contentWindow.location.hash = `map=${current.zoom}/${current.lat}/${current.lon}`;
}
if (leader !== "online" && onlineFrame.contentWindow) {
onlineFrame.contentWindow.location.hash = `map=${current.zoom}/${current.lat}/${current.lon}`;
}
}, 150);
</script>
</body>
</html>