- static/geo/deutschland-bundeslaender.geojson (BKG, vereinfacht, ~40KB) - map.js: Tile-Layer entfernt, Bundeslaender-GeoJSON per same-origin fetch als Vektor-Hintergrund; Detail-Minikarte als Deutschland-Locator - hugo.toml: OSM tileURL entfernt, Attribution nur noch als Text - CSS: neutraler Kartenhintergrund statt Tile-Grau - Ergebnis: 0 externe Requests (per Playwright-Netzwerk-Audit verifiziert)
232 lines
9.5 KiB
JavaScript
232 lines
9.5 KiB
JavaScript
/* =========================================================================
|
||
OP·Karte – Leaflet mit LOKALER Deutschland-Karte (keine externen Tiles/CDN)
|
||
- Basemap = Bundesländer-GeoJSON aus dem Repo (static/geo/…), vektoriell
|
||
- farbige Pin-Marker je Kategorie, Clustering (markercluster)
|
||
- Kategorie-Filter (Checkboxen) + Textsuche, synchron mit der Card-Liste
|
||
- Klick auf Card öffnet den zugehörigen Marker
|
||
Daten kommen aus <script type="application/json" id="op-map-data">.
|
||
========================================================================= */
|
||
(function () {
|
||
"use strict";
|
||
|
||
function init() {
|
||
var el = document.getElementById("op-map");
|
||
if (!el || typeof L === "undefined") return;
|
||
var dataNode = document.getElementById("op-map-data");
|
||
if (!dataNode) return;
|
||
|
||
var payload = JSON.parse(dataNode.textContent || "{}");
|
||
var places = payload.places || [];
|
||
var colors = payload.colors || {};
|
||
var defaultColor = payload.defaultColor || "#1e2758";
|
||
var klein = !!payload.klein;
|
||
var geojsonUrl = payload.geojsonUrl || "";
|
||
var attribution = payload.attribution || "Geodaten: © GeoBasis-DE / BKG";
|
||
var germanyBounds = null;
|
||
|
||
var map = L.map(el, {
|
||
scrollWheelZoom: false,
|
||
minZoom: 5, maxZoom: 12,
|
||
attributionControl: true
|
||
});
|
||
map.attributionControl.setPrefix(false).addAttribution(attribution);
|
||
map.on("focus", function () { map.scrollWheelZoom.enable(); });
|
||
map.on("blur", function () { map.scrollWheelZoom.disable(); });
|
||
|
||
// Lokale Deutschland-Karte (Bundesländer) als Vektor-Hintergrund laden.
|
||
// Same-Origin-Fetch aus dem Repo – kein externer Request.
|
||
if (geojsonUrl) {
|
||
fetch(geojsonUrl)
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (geo) {
|
||
if (!geo) return;
|
||
var land = L.geoJSON(geo, {
|
||
style: {
|
||
color: "#1e2758", weight: 1, opacity: .85,
|
||
fillColor: "#f4f1e6", fillOpacity: 1
|
||
},
|
||
interactive: false
|
||
}).addTo(map);
|
||
land.bringToBack();
|
||
germanyBounds = land.getBounds();
|
||
// Detail-Minikarte = Verortung: ganz Deutschland mit dem einen Pin zeigen.
|
||
// Grosse Karte: nur wenn (noch) keine Marker den Ausschnitt bestimmen.
|
||
if (klein || !records.length) {
|
||
try { map.fitBounds(germanyBounds.pad(0.02)); } catch (e) {}
|
||
}
|
||
})
|
||
.catch(function () {});
|
||
}
|
||
|
||
var useCluster = places.length > 1 && typeof L.markerClusterGroup === "function";
|
||
var group = useCluster
|
||
? L.markerClusterGroup({ maxClusterRadius: 45, showCoverageOnHover: false })
|
||
: L.featureGroup();
|
||
map.addLayer(group);
|
||
|
||
var byUrl = {};
|
||
var records = [];
|
||
|
||
places.forEach(function (p) {
|
||
if (typeof p.lat !== "number" || typeof p.lng !== "number") return;
|
||
var cat = (p.categories && p.categories[0]) || "";
|
||
var color = colors[cat] || defaultColor;
|
||
var marker = L.marker([p.lat, p.lng], {
|
||
icon: pinIcon(color),
|
||
riseOnHover: true,
|
||
keyboard: true,
|
||
title: p.title, // nativer Browser-Tooltip / a11y
|
||
alt: "Schauplatz: " + p.title
|
||
});
|
||
// Popup ohne autoPan, damit die Karte beim Hover nicht springt
|
||
marker.bindPopup(popupHtml(p, cat, color), { autoPan: false, closeButton: true, minWidth: 250, maxWidth: 250 });
|
||
// Beim Drüberfahren (Desktop) öffnet sich die Kurzinfo; Tap/Klick ebenso
|
||
marker.on("mouseover", function () { marker.openPopup(); });
|
||
var rec = { p: p, marker: marker, cats: p.categories || [], text: (p.title + " " + (p.subtitle || "") + " " + (p.address || "")).toLowerCase() };
|
||
records.push(rec);
|
||
if (p.url) byUrl[p.url] = rec;
|
||
});
|
||
|
||
var allBounds = records.length ? L.latLngBounds(records.map(function (r) { return r.marker.getLatLng(); })) : null;
|
||
|
||
// --- Filter-Status ---
|
||
var activeCats = null; // null = alle
|
||
var query = "";
|
||
|
||
function visible(rec) {
|
||
if (query && rec.text.indexOf(query) === -1) return false;
|
||
if (activeCats) {
|
||
var ok = rec.cats.some(function (c) { return activeCats[c]; });
|
||
if (!ok) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function apply() {
|
||
group.clearLayers();
|
||
var shown = 0, bounds = L.latLngBounds([]);
|
||
records.forEach(function (rec) {
|
||
var v = visible(rec);
|
||
if (v) { group.addLayer(rec.marker); shown++; bounds.extend(rec.marker.getLatLng()); }
|
||
});
|
||
// Cards synchronisieren
|
||
cards.forEach(function (card) {
|
||
var url = card.getAttribute("data-url");
|
||
var rec = url && byUrl[url];
|
||
var show;
|
||
if (rec) show = visible(rec);
|
||
else {
|
||
// Cards ohne Koordinaten: nur Textsuche greift
|
||
var t = (card.getAttribute("data-text") || "").toLowerCase();
|
||
show = !query || t.indexOf(query) !== -1;
|
||
}
|
||
card.classList.toggle("is--hidden", !show);
|
||
});
|
||
var countCards = cards.filter(function (c) { return !c.classList.contains("is--hidden"); }).length;
|
||
if (countEl) countEl.textContent = countCards;
|
||
if (bounds.isValid()) {
|
||
// Auf der Detail-Minikarte nicht ins Leere zoomen: Umkreis statt Punkt
|
||
if (klein) map.fitBounds(bounds.pad(0.15), { maxZoom: 8 });
|
||
else map.fitBounds(bounds.pad(0.15));
|
||
}
|
||
else if (allBounds) map.fitBounds(allBounds.pad(0.15), klein ? { maxZoom: 8 } : undefined);
|
||
else if (germanyBounds) map.fitBounds(germanyBounds.pad(0.02));
|
||
}
|
||
|
||
// --- DOM-Hooks ---
|
||
var cards = Array.prototype.slice.call(document.querySelectorAll("[data-op-card]"));
|
||
var countEl = document.querySelector("[data-op-count]");
|
||
var searchEl = document.querySelector("[data-op-search]");
|
||
var filterEl = document.querySelector("[data-op-map-filter]");
|
||
|
||
if (filterEl) {
|
||
var stateEl = document.querySelector("[data-op-filter-state]");
|
||
var wordAll = (stateEl && stateEl.getAttribute("data-word-all")) || "alle";
|
||
var wordOf = (stateEl && stateEl.getAttribute("data-word-of")) || "von";
|
||
var updateState = function () {
|
||
if (!stateEl) return;
|
||
var boxes = filterEl.querySelectorAll("input[type=checkbox]");
|
||
var total = boxes.length, on = 0;
|
||
boxes.forEach(function (cb) { if (cb.checked) on++; });
|
||
stateEl.textContent = on === total ? "· " + wordAll : "· " + on + " " + wordOf + " " + total;
|
||
};
|
||
var updateCats = function () {
|
||
var boxes = filterEl.querySelectorAll("input[type=checkbox]");
|
||
activeCats = {};
|
||
var anyUnchecked = false;
|
||
boxes.forEach(function (cb) { activeCats[cb.value] = cb.checked; if (!cb.checked) anyUnchecked = true; });
|
||
if (!anyUnchecked) activeCats = null; // alle aktiv → kein Filter
|
||
updateState();
|
||
apply();
|
||
};
|
||
filterEl.addEventListener("change", updateCats);
|
||
updateState(); // Anfangszustand im eingeklappten Filter anzeigen
|
||
|
||
// Schnellauswahl: alle auswählen / abwählen
|
||
var setAll = function (state) {
|
||
filterEl.querySelectorAll("input[type=checkbox]").forEach(function (cb) { cb.checked = state; });
|
||
updateCats();
|
||
};
|
||
var btnAll = document.querySelector("[data-op-filter-all]");
|
||
var btnNone = document.querySelector("[data-op-filter-none]");
|
||
if (btnAll) btnAll.addEventListener("click", function () { setAll(true); });
|
||
if (btnNone) btnNone.addEventListener("click", function () { setAll(false); });
|
||
}
|
||
if (searchEl) {
|
||
var deb;
|
||
searchEl.addEventListener("input", function () {
|
||
clearTimeout(deb);
|
||
deb = setTimeout(function () { query = searchEl.value.trim().toLowerCase(); apply(); }, 150);
|
||
});
|
||
}
|
||
|
||
// Cards sind normale Links → Klick führt direkt zur Detailseite.
|
||
// (Die Verortung passiert über die Marker/Popups auf der Karte.)
|
||
|
||
// Vorbelegung aus URL (?q=…), z.B. von der Kopf-Suche
|
||
try {
|
||
var q0 = new URLSearchParams(location.search).get("q");
|
||
if (q0) { query = q0.trim().toLowerCase(); if (searchEl) searchEl.value = q0; }
|
||
} catch (e) {}
|
||
|
||
// Erstanzeige (apply() befüllt die Layer-Gruppe und setzt den Ausschnitt)
|
||
if (!records.length) map.setView([50.9, 11.6], 6);
|
||
apply();
|
||
}
|
||
|
||
function pinIcon(color) {
|
||
var svg =
|
||
'<svg class="op-pin" width="30" height="42" viewBox="0 0 30 42" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
|
||
'<path d="M15 41 C15 41 3 24 3 14 A12 12 0 1 1 27 14 C27 24 15 41 15 41 Z" fill="' + color + '" stroke="#ffffff" stroke-width="2.5"/>' +
|
||
'<circle cx="15" cy="14" r="5" fill="#ffffff"/>' +
|
||
'</svg>';
|
||
return L.divIcon({
|
||
html: svg,
|
||
className: "op-pinwrap",
|
||
iconSize: [30, 42],
|
||
iconAnchor: [15, 41],
|
||
popupAnchor: [0, -36]
|
||
});
|
||
}
|
||
|
||
function popupHtml(p, cat, color) {
|
||
var tag = p.url ? "a" : "div";
|
||
var href = p.url ? ' href="' + p.url + '"' : "";
|
||
var h = "<" + tag + ' class="op-popup"' + href + ">";
|
||
if (p.img) h += '<span class="op-popup__img" style="background-image:url(\'' + p.img + '\')"></span>';
|
||
h += '<span class="op-popup__body">';
|
||
h += '<span class="op-popup__title">' + esc(p.title) + "</span>";
|
||
var meta = p.address || p.subtitle || "";
|
||
if (meta) h += '<span class="op-popup__addr">' + esc(meta) + "</span>";
|
||
h += "</span></" + tag + ">";
|
||
return h;
|
||
}
|
||
function esc(s) {
|
||
return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<")
|
||
.replace(/>/g, ">").replace(/"/g, """);
|
||
}
|
||
|
||
if (document.readyState !== "loading") init();
|
||
else document.addEventListener("DOMContentLoaded", init);
|
||
})();
|