Files
Hugo_MAP-NSU-Komplex/themes/op-dz/assets/js/map.js
T
maik 76d68b013a fix(karte): Detail-Minikarte auf Strassenzoom (17) statt Deutschland-Locator
fitGermanyIfNeeded ueberschrieb bei klein den Zoom-17-Fit aus apply() auf
ganz Deutschland. Jetzt nur noch bei leerem Ergebnis (!records) ganz DE zeigen;
die Detailseiten-Minikarte bleibt auf Strasse/Haus des Schauplatzes.
2026-08-20 18:38:08 +02:00

285 lines
12 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 pmtilesUrl = payload.pmtilesUrl || "";
var flavor = payload.flavor || "light";
var attribution = payload.attribution || "© OpenStreetMap-Mitwirkende";
// Statischer Deutschland-Ausschnitt (für Locator/leere Filterung ohne Marker)
var germanyBounds = L.latLngBounds([[47.2, 5.8], [55.1, 15.1]]);
var map = L.map(el, {
scrollWheelZoom: false,
minZoom: 5, maxZoom: 18,
attributionControl: true
});
map.attributionControl.setPrefix(false).addAttribution(attribution);
map.on("focus", function () { map.scrollWheelZoom.enable(); });
map.on("blur", function () { map.scrollWheelZoom.disable(); });
// Ausschnitt fest auf Deutschland begrenzen: kein Wegscrollen ins Ausland.
map.setMaxBounds(germanyBounds.pad(0.08));
// Basemap: selbst gehostete Vektor-Tiles (Protomaps/OSM, .pmtiles auf dem
// eigenen Server kein externes CDN). Der Browser rendert Strassen/Haeuser.
var tilesActive = false;
if (pmtilesUrl && typeof protomapsL !== "undefined" && protomapsL.leafletLayer) {
try {
protomapsL.leafletLayer({ url: pmtilesUrl, flavor: flavor, lang: "de" }).addTo(map);
tilesActive = true;
} catch (e) { tilesActive = false; }
}
// Grenz-GeoJSON (Bundeslaender) immer laden:
// - ohne Tiles: gefuellter Umriss als Ersatz-Hintergrund
// - IMMER: Invers-Maske, die alles ausserhalb Deutschlands abdeckt, damit
// nur Deutschland sichtbar ist (Nachbarlaender/Meer ausgeblendet)
if (geojsonUrl) {
fetch(geojsonUrl)
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (geo) {
if (!geo) { fitGermanyIfNeeded(); return; }
if (!tilesActive) {
L.geoJSON(geo, {
style: { color: "#1e2758", weight: 1, opacity: .85, fillColor: "#f4f1e6", fillOpacity: 1 },
interactive: false
}).addTo(map).bringToBack();
}
addGermanyMask(geo);
fitGermanyIfNeeded();
})
.catch(function () { fitGermanyIfNeeded(); });
}
function fitGermanyIfNeeded() {
// Nur bei leerem Ergebnis ganz Deutschland zeigen. Die Detail-Minikarte
// (klein) bleibt auf dem Strassenzoom aus apply() (maxZoom 17), damit man
// Strasse/Haus des Schauplatzes sieht.
if (!records.length) {
try { map.fitBounds(germanyBounds.pad(0.02)); } catch (e) {}
}
}
// Invers-Maske: weltgrosses Rechteck mit Deutschland als "Loechern".
// Fuellung (Papierfarbe) deckt alles ausserhalb der Landesgrenze ab.
// Even-Odd-Fuellregel: es MUESSEN alle Ringe rein (auch innere), sonst
// werden Enklaven wie Berlin/Bremen faelschlich ueberdeckt.
// Liegt im overlayPane ueber den Tiles, aber unter den Markern.
function addGermanyMask(geo) {
var world = [[-89, -180], [-89, 180], [89, 180], [89, -180]];
var holes = [];
function toLatLng(ring) {
return ring.map(function (c) { return [c[1], c[0]]; });
}
function collect(geom) {
if (!geom) return;
if (geom.type === "Polygon") {
geom.coordinates.forEach(function (ring) { holes.push(toLatLng(ring)); });
} else if (geom.type === "MultiPolygon") {
geom.coordinates.forEach(function (poly) {
poly.forEach(function (ring) { holes.push(toLatLng(ring)); });
});
}
}
(geo.features || []).forEach(function (f) { collect(f.geometry); });
if (geo.type === "Polygon" || geo.type === "MultiPolygon") collect(geo);
if (!holes.length) return;
L.polygon([world].concat(holes), {
stroke: false,
fill: true, fillColor: "#f6f6f1", fillOpacity: 1,
fillRule: "evenodd",
interactive: false
}).addTo(map);
}
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()) {
// Detail-Minikarte: nah an Strasse/Haus heran (Zoom 17)
if (klein) map.fitBounds(bounds.pad(0.15), { maxZoom: 17 });
else map.fitBounds(bounds.pad(0.15));
}
else if (allBounds) map.fitBounds(allBounds.pad(0.15), klein ? { maxZoom: 17 } : 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);
})();