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.
285 lines
12 KiB
JavaScript
285 lines
12 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 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, "&").replace(/</g, "<")
|
||
.replace(/>/g, ">").replace(/"/g, """);
|
||
}
|
||
|
||
if (document.readyState !== "loading") init();
|
||
else document.addEventListener("DOMContentLoaded", init);
|
||
})();
|