Link POI popups to offline Wikipedia via kiwix-serve
CI / check (push) Successful in 40s
CI / deploy (push) Successful in 12s

lib/maptool/kiwix.rb: a small reachability check for an external
kiwix-serve instance. GET /api/kiwix reports it (same optional-
integration pattern as offline-tiles/terrain-tiles); the frontend
adds a "Nearby on Wikipedia" link to a point's popup pointing straight
at kiwix-serve's own geo search (GET /search?latitude=&longitude=
&distance=, confirmed from libkiwix's actual source rather than
guessed) — linked to directly, not proxied/embedded, since it already
has a full search UI of its own.

docker-compose.yaml: new `kiwix` service (ghcr.io/kiwix/kiwix-serve)
on the default network, KIWIX_URL wired to reach it by service name.
The ZIM itself isn't part of the repo (large, self-contained archive)
— data/kiwix/ gitignored same as the *.pmtiles files, setup steps in
docs/development.adoc.

No kiwix-serve/ZIM available here to test end-to-end — verified the
app side against a stand-in local HTTP server (GET /api/kiwix flips
available correctly) and confirmed the generated popup link matches
the real geo-search query shape. docker-compose.yaml validated with
`docker-compose config`. Full test suite: 118 runs, 0 failures.
This commit is contained in:
randy committed 2026-09-09 13:44:37 +02:00
1 parent 7cd3171998
commit 24ac5931f8
13 files changed
+150 -9

No files matched your search

+21 -4
View File
@@ -702,7 +702,7 @@ export class GpxPanelControl {
const editable = feature.properties.category === "Note" && !!feature.properties.id && isLoggedIn();
const popup = new Popup({ closeButton: true, maxWidth: "260px" })
.setLngLat(event.lngLat)
.setHTML(popupHtml(feature.properties, { editable }))
.setHTML(popupHtml(feature.properties, { editable, lngLat: event.lngLat }))
.addTo(this._map);
if (editable) this._wireNoteActions(popup, path, feature);
});
@@ -754,7 +754,7 @@ export class GpxPanelControl {
const bodyInput = content.querySelector(".gpx-popup__edit-body");
titleInput.focus();
content.querySelector(".gpx-popup__edit-cancel").addEventListener("click", () => {
content.innerHTML = popupHtml(feature.properties, { editable: true });
content.innerHTML = popupHtml(feature.properties, { editable: true, lngLat: popup.getLngLat() });
this._wireNoteActions(popup, path, feature);
});
content.querySelector(".gpx-popup__edit-save").addEventListener("click", async () => {
@@ -1077,13 +1077,26 @@ function gpxFileUrl(path) {
return `/api/gpx/file/${path.split("/").map(encodeURIComponent).join("/")}`;
}
// Set once from map.js after its one-time GET /api/kiwix check (same
// fetch-once-at-load story as the offline-tiles/terrain-tiles checks) —
// module-level rather than threaded through every popupHtml call, since
// it's the same answer for the whole page's lifetime.
let kiwixConfig = { available: false, url: "" };
export function setKiwixConfig(config) {
kiwixConfig = config;
}
// properties come straight from an uploaded GPX file — untrusted input —
// so every value is escaped before landing in the popup's HTML.
// editable (only ever true for a "Note" feature the viewer is logged in
// for — see _registerPointInteractivity) adds Edit/Delete buttons; the
// click handling for them lives in _wireNoteActions, not here, since this
// only builds markup, it has no map/store access of its own.
function popupHtml({ name, description, time, category, photos, icon }, { editable = false } = {}) {
// only builds markup, it has no map/store access of its own. lngLat (the
// popup's own anchor point) drives the "Nearby on Wikipedia" link — kiwix-
// serve's own geo search, linked to directly rather than proxied/embedded
// (it already has a full search UI of its own, see docs/development.adoc).
function popupHtml({ name, description, time, category, photos, icon }, { editable = false, lngLat = null } = {}) {
const parts = [];
if (name) {
const glyph = icon && POI_ICONS[icon] ? `<span class="gpx-popup__header-icon">${POI_ICONS[icon]}</span>` : "";
@@ -1104,6 +1117,10 @@ function popupHtml({ name, description, time, category, photos, icon }, { editab
parts.push(`<p class="gpx-popup__time">${escapeHtml(parsed.toLocaleString())}</p>`);
}
}
if (kiwixConfig.available && lngLat) {
const wikiUrl = `${kiwixConfig.url}/search?pattern=&latitude=${lngLat.lat}&longitude=${lngLat.lng}&distance=2000`;
parts.push(`<p class="gpx-popup__wiki"><a href="${escapeHtml(wikiUrl)}" target="_blank" rel="noopener noreferrer">Nearby on Wikipedia →</a></p>`);
}
if (editable) {
parts.push(
`<div class="gpx-popup__note-actions"><button type="button" class="gpx-popup__edit">Edit</button><button type="button" class="gpx-popup__delete">Delete</button></div>`,