diff --git a/public/css/style.css b/public/css/style.css
index 8abd8e1..cf420b3 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -373,21 +373,6 @@ html, body {
color: #666;
}
-.gpx-panel__wiki {
- display: flex;
- align-items: center;
- gap: 6px;
- margin: 0 12px 8px;
- padding-bottom: 8px;
- border-bottom: 1px solid #e5e5e5;
- font-size: 13px;
- cursor: pointer;
-}
-
-.gpx-panel__wiki[hidden] {
- display: none;
-}
-
.gpx-tree {
/* flex:1 (not just overflow-y) so the tree fills the rest of the panel
even when short/empty — otherwise the blank space below it wouldn't
@@ -1189,6 +1174,27 @@ html, body {
box-shadow: 0 0 0 2px rgba(42, 157, 143, 0.4);
}
+.overlays-panel__list {
+ flex: 1;
+ overflow-y: auto;
+ list-style: none;
+ margin: 0;
+ padding: 8px 12px;
+}
+
+.overlays-panel__item label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 4px;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.overlays-panel__item label:hover {
+ background: #f0f0f0;
+}
+
.livetrack-panel__list {
flex: 1;
overflow-y: auto;
diff --git a/public/js/gpx.js b/public/js/gpx.js
index 4346fc2..e0d3938 100644
--- a/public/js/gpx.js
+++ b/public/js/gpx.js
@@ -11,7 +11,6 @@ import {
MAP_MARKER_ICON,
POI_ICONS,
PARK4NIGHT_TAGS,
- WIKI_ICON,
} from "/js/icons.js";
import { notifySidePanelOpen, onlyOneSidePanelOpen, onGpxStoreChanged, notifyGpxStoreChanged } from "/js/panels.js";
import { formatActivityType } from "/js/track_profile.js";
@@ -28,13 +27,6 @@ const POI_ICON_SIZE = 0.3; // 48 * 0.3 = ~14px, sized to sit inside the marker b
const MAP_MARKER_SIZE = 0.55;
const BADGE_IMAGE_PREFIX = "badge-"; // one registered image per distinct badge color actually seen, see _registerBadgeColors
-// The "Wikipedia" layer (a live GET /api/kiwix/nearby refetch on every
-// pan/zoom, not a file in the tree — see _enableWikiLayer) — one fixed
-// icon/color for every point on it, unlike a POI file's per-category ones.
-const WIKI_SOURCE_ID = "wiki-points";
-const WIKI_ICON_ID = "wiki-icon";
-const WIKI_BADGE_COLOR = "#795548";
-
// Custom drag payload MIME type for moving an already-uploaded file between
// folders, as opposed to an OS file drag (type "Files") for uploading a new
// one — both land on the same drop targets, told apart by this.
@@ -54,9 +46,6 @@ export class GpxPanelControl {
this._linesByPath = new Map(); // path -> its lines/polygons FeatureCollection, same reason (a polygon/line note also carries a category)
this._hiddenCategories = new Set(); // category names toggled off in the legend
this._openDownloadMenu = null; // the one gpx-tree__download-menu currently open, if any
- this._wikiEnabled = false; // the "Wikipedia" layer's checkbox 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
// setStyle() (switching the Online/Offline base map) tears down all
// custom sources/layers/images — re-register the category glyphs before
@@ -68,7 +57,6 @@ export class GpxPanelControl {
map.on("style.load", () => {
this._iconsReady = this._registerCategoryGlyphs();
this._visible.forEach((path) => this._addLayers(path));
- if (this._wikiEnabled) this._addWikiLayers().then(() => this._refreshWikiLayer());
});
// notes.js writes directly into the same GPX store (a hand-drawn note
@@ -126,17 +114,6 @@ export class GpxPanelControl {
);
}
- // Same registration as _registerCategoryGlyphs above, for the one fixed
- // glyph the "Wikipedia" layer uses — kept separate from POI_ICONS since
- // that set is keyed by OSM/p4n2gpx category and doubles as notes.js's
- // icon-picker option list, neither of which applies to this layer.
- async _registerWikiIcon() {
- if (this._map.hasImage(WIKI_ICON_ID)) return;
- const whiteSvg = WIKI_ICON.replace(/currentColor/g, "#fff");
- const imageData = await svgToImageData(whiteSvg, POI_ICON_IMAGE_SIZE);
- if (!this._map.hasImage(WIKI_ICON_ID)) this._map.addImage(WIKI_ICON_ID, imageData);
- }
-
mount(sidebar) {
this._container = document.createElement("div");
this._container.className = "sidebar-item";
@@ -148,12 +125,6 @@ export class GpxPanelControl {
this._treeContainer = this._panel.querySelector(".gpx-tree");
this._legendContainer = this._panel.querySelector(".gpx-legend");
this._uploadStatus = this._panel.querySelector(".gpx-upload__status");
- wikiToggleLabel = this._panel.querySelector(".gpx-panel__wiki");
- wikiToggleLabel.hidden = !kiwixConfig.available;
- wikiToggleLabel.querySelector(".gpx-panel__wiki-toggle").addEventListener("change", (event) => {
- if (event.target.checked) this._enableWikiLayer();
- else this._disableWikiLayer();
- });
this._panel.querySelector('[data-action="new-folder"]').innerHTML = NEW_FOLDER_ICON;
this._panel.querySelector('[data-action="upload"]').innerHTML = UPLOAD_ICON;
if (!isLoggedIn()) {
@@ -953,110 +924,6 @@ export class GpxPanelControl {
}
}
- // The "Wikipedia" layer's checkbox toggle (see mount()) — not a file in
- // _visible/the tree, so it gets its own small add/remove pair rather than
- // going through _addLayers/_removeLayers, which are built around a
- // GpxStore path. Refetches on every pan/zoom instead of once for a fixed
- // area (contrast the import plugins' "Run for this area" button): cheap
- // to do since GET /api/kiwix/nearby only ever talks to kiwix-serve's own
- // local ZIM, none of the plugins' shared/rate-limited API concerns apply.
- async _enableWikiLayer() {
- this._wikiEnabled = true;
- await this._addWikiLayers();
- this._wikiMoveHandler = () => this._refreshWikiLayer();
- this._map.on("moveend", this._wikiMoveHandler);
- this._refreshWikiLayer();
- }
-
- _disableWikiLayer() {
- this._wikiEnabled = false;
- if (this._wikiMoveHandler) {
- this._map.off("moveend", this._wikiMoveHandler);
- this._wikiMoveHandler = null;
- }
- this._removeWikiLayers();
- }
-
- async _addWikiLayers() {
- if (this._map.getSource(WIKI_SOURCE_ID)) return;
- await Promise.all([this._registerWikiIcon(), this._registerBadgeColors([WIKI_BADGE_COLOR])]);
- if (this._map.getSource(WIKI_SOURCE_ID)) return; // style.load raced this while awaiting
-
- this._map.addSource(WIKI_SOURCE_ID, { type: "geojson", data: { type: "FeatureCollection", features: [] } });
- this._map.addLayer({
- id: `${WIKI_SOURCE_ID}-badge`,
- type: "symbol",
- source: WIKI_SOURCE_ID,
- layout: {
- "icon-image": badgeImageId(WIKI_BADGE_COLOR),
- "icon-size": MAP_MARKER_SIZE,
- "icon-allow-overlap": true,
- "icon-ignore-placement": true,
- },
- });
- this._map.addLayer({
- id: `${WIKI_SOURCE_ID}-icon`,
- type: "symbol",
- source: WIKI_SOURCE_ID,
- layout: { "icon-image": WIKI_ICON_ID, "icon-size": POI_ICON_SIZE, "icon-allow-overlap": true, "icon-ignore-placement": true },
- });
- this._registerWikiInteractivity(`${WIKI_SOURCE_ID}-badge`);
- }
-
- _removeWikiLayers() {
- [`${WIKI_SOURCE_ID}-badge`, `${WIKI_SOURCE_ID}-icon`].forEach((layerId) => {
- if (this._map.getLayer(layerId)) this._map.removeLayer(layerId);
- });
- if (this._map.getSource(WIKI_SOURCE_ID)) this._map.removeSource(WIKI_SOURCE_ID);
- }
-
- // Not _registerPointInteractivity: that one's built around a GpxStore
- // path for Note edit/delete, neither of which a wiki article has —
- // simpler to keep this layer's own small click/hover pair separate than
- // to bend the shared one around a case it was never meant to cover.
- _registerWikiInteractivity(layerId) {
- if (this._interactiveLayers.has(layerId)) return;
- this._interactiveLayers.add(layerId);
-
- this._map.on("click", layerId, (event) => {
- const feature = event.features[0];
- if (!feature) return;
- new Popup({ closeButton: true, maxWidth: "260px" }).setLngLat(event.lngLat).setHTML(wikiPopupHtml(feature.properties)).addTo(this._map);
- });
- this._map.on("mouseenter", layerId, () => {
- this._map.getCanvas().style.cursor = "pointer";
- });
- this._map.on("mouseleave", layerId, () => {
- this._map.getCanvas().style.cursor = "";
- });
- }
-
- // seq guards against an out-of-order response — a fast second pan's
- // fetch resolving before a slower first one's — overwriting newer data
- // with stale data; not a full abort-in-flight setup, just the ordering
- // guarantee this actually needs.
- async _refreshWikiLayer() {
- const seq = ++this._wikiRequestSeq;
- const bounds = this._map.getBounds();
- const params = new URLSearchParams({
- south: bounds.getSouth(),
- west: bounds.getWest(),
- north: bounds.getNorth(),
- east: bounds.getEast(),
- });
-
- let geojson;
- try {
- geojson = await (await fetch(`/api/kiwix/nearby?${params}`)).json();
- } catch {
- return;
- }
- if (seq !== this._wikiRequestSeq) return;
-
- const source = this._map.getSource(WIKI_SOURCE_ID);
- if (source) source.setData(geojson);
- }
-
async _uploadFiles(fileList, folder) {
// .zip: Garmin's bulk/FIT export format — the server expands it into
// its .gpx/.fit/.geojson entries, so it's accepted here even though the
@@ -1193,7 +1060,11 @@ function uploadFile(formData, onUploadComplete) {
// fixed square size (the source SVGs have no explicit width/height, just a
// viewBox, so one is injected — otherwise an falls back to the CSS
// default replaced-element size instead of the viewBox's aspect).
-async function svgToImageData(svgMarkup, size) {
+// Exported: overlays.js's own live layers (Wikipedia, more to come per its
+// own doc comment) need the same SVG-to-map-image/badge-id/escaping
+// helpers this file already has, and reimplementing them there would just
+// be the same three functions typed twice.
+export async function svgToImageData(svgMarkup, size) {
// xmlns is required here even though the other hand-drawn icons in this
// app get away without it — those are inlined via innerHTML, where the
// HTML parser assigns the SVG namespace from context, but a standalone
@@ -1216,7 +1087,7 @@ async function svgToImageData(svgMarkup, size) {
}
}
-function badgeImageId(color) {
+export function badgeImageId(color) {
return `${BADGE_IMAGE_PREFIX}${color}`;
}
@@ -1232,21 +1103,6 @@ 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: "" };
-// Set once GpxPanelControl#mount runs — updated here too since map.js's
-// GET /api/kiwix check resolves after mount() already ran, so the panel
-// markup exists but its availability isn't known yet at that point.
-let wikiToggleLabel = null;
-
-export function setKiwixConfig(config) {
- kiwixConfig = config;
- if (wikiToggleLabel) wikiToggleLabel.hidden = !config.available;
-}
-
// 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
@@ -1282,16 +1138,6 @@ function popupHtml({ name, description, time, category, photos, icon }, { editab
return parts.length > 0 ? parts.join("") : "No details";
}
-// properties come from GET /api/kiwix/nearby — kiwix-serve's own ZIM
-// content, not an uploaded file, but still untrusted-by-convention like
-// every other popup builder here (escaped all the same). link already
-// carries the /kiwix prefix this app proxies (see lib/maptool/kiwix.rb).
-function wikiPopupHtml({ name, link }) {
- const parts = [`