erster brauchbarer stand.

This commit is contained in:
maik
2026-08-18 15:32:41 +02:00
commit 553247cdfb
460 changed files with 13751 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
/* =========================================================================
OP·Karte Leaflet + OpenStreetMap
- farbige Kreis-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 tileUrl = payload.tileUrl || "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
var attribution = payload.attribution ||
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>-Mitwirkende';
var map = L.map(el, { scrollWheelZoom: false });
L.tileLayer(tileUrl, { maxZoom: 19, attribution: attribution }).addTo(map);
map.on("focus", function () { map.scrollWheelZoom.enable(); });
map.on("blur", function () { map.scrollWheelZoom.disable(); });
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()) map.fitBounds(bounds.pad(0.15));
else if (allBounds) map.fitBounds(allBounds.pad(0.15));
}
// --- 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);
})();