Files
Hugo_MAP-NSU-Komplex/themes/op-dz/assets/js/map.js
T
maik bfcb2ce6c0 feat(karte): lokale Deutschland-Basemap statt OpenStreetMap-Kacheln (keine externen CDN)
- 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)
2026-08-19 13:16:16 +02:00

232 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =========================================================================
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, "&amp;").replace(/</g, "&lt;")
.replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
if (document.readyState !== "loading") init();
else document.addEventListener("DOMContentLoaded", init);
})();