Compare commits

..
2 Commits
Author SHA1 Message Date
maik fb4310666c feat(karte): nur Deutschland anzeigen (Invers-Maske) + lokale pmtiles-Basemap verdrahten
- map.js: Invers-Maske aus Bundeslaender-GeoJSON blendet Nachbarlaender/Meer aus
  (Even-Odd, alle Ringe -> Enklaven Berlin/Bremen bleiben sichtbar)
- map.js: Ausschnitt via setMaxBounds fest auf Deutschland begrenzt
- map.js: Grenz-GeoJSON immer laden; gefuellter Umriss als Fallback ohne Tiles
- scripts.html/karte.html/hugo.toml: protomaps-leaflet + pmtiles-Basemap
- .gitignore: /static/tiles/ (6,7-GB-pmtiles liegt ausserhalb des Repos)
2026-08-19 15:58:05 +02:00
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
9 changed files with 158 additions and 20 deletions
+5
View File
@@ -9,3 +9,8 @@ Thumbs.db
# Persönliche Notizen nie pushen # Persönliche Notizen nie pushen
/Todo.md /Todo.md
# Große Vektor-Basemap (6,7 GB) außerhalb des Repos, lokal per Symlink
# nach static/tiles/ eingehängt (ln -s /home/maik/op-pmtiles/germany.pmtiles).
# Nie ins Git, auf dem Server separat unter /tiles/ bereitstellen.
/static/tiles/
+18 -6
View File
@@ -2,8 +2,9 @@
Hugo-Neubau von [map.offener-prozess.de](https://map.offener-prozess.de/) Hugo-Neubau von [map.offener-prozess.de](https://map.offener-prozess.de/)
(Ablösung des alten WordPress/Listable-Setups). Interaktive Karte der (Ablösung des alten WordPress/Listable-Setups). Interaktive Karte der
Schauplätze des NSU-Komplexes mit **Leaflet + OpenStreetMap** (statt der alten Schauplätze des NSU-Komplexes mit **Leaflet** und einer **lokalen Deutschland-Karte**
Google-Maps-/Mapbox-Einbindung), im Layout ans Original angelehnt (Split-View: (Bundesländer-GeoJSON im Repo, keine externen Kacheln/CDN DSGVO-freundlich),
im Layout ans Original angelehnt (Split-View:
Kartenliste links, Karte rechts), umgesetzt mit dem Theme Kartenliste links, Karte rechts), umgesetzt mit dem Theme
[`themes/op-dz/`](themes/op-dz/) (UIkit 3, Schrift *ABC Favorit*, alle Assets [`themes/op-dz/`](themes/op-dz/) (UIkit 3, Schrift *ABC Favorit*, alle Assets
lokal/DSGVO-konform). lokal/DSGVO-konform).
@@ -71,11 +72,22 @@ Der Punkt erscheint automatisch auf allen Karten (Start, Kategorie) und bekommt
eine Detailseite mit Mini-Karte + „Anfahrt per OpenStreetMap". Ein Bild eine Detailseite mit Mini-Karte + „Anfahrt per OpenStreetMap". Ein Bild
`cover.jpg` im selben Ordner wird als Card-/Kopfbild genutzt. `cover.jpg` im selben Ordner wird als Card-/Kopfbild genutzt.
## Karte / Kacheln ## Karte (komplett lokal, keine externen CDN)
Standard: OpenStreetMap. Eigener Tile-Server (OSM-Usage-Policy!) in `hugo.toml` Die Basemap ist **keine** externe Kachel (kein OpenStreetMap/CDN, DSGVO-freundlich),
unter `[params.map]` (`tileURL`, `attribution`). 118 Marker werden per sondern eine lokale **Bundesländer-GeoJSON** unter
Leaflet.markercluster gebündelt; Farbe = Kategorie (`data/kategorien.yaml`). [`static/geo/deutschland-bundeslaender.geojson`](static/geo/deutschland-bundeslaender.geojson)
(amtliche BKG-Geometrie, vereinfacht, ~40 KB). `map.js` lädt sie same-origin per
`fetch` und rendert sie vektoriell mit Leaflet (Bundesländer-Grenzen blau, Land
cremefarben). 118 Marker werden per Leaflet.markercluster gebündelt; Farbe =
Kategorie (`data/kategorien.yaml`). Attribution steht als **Text** in `hugo.toml`
unter `[params.map]` (kein Request).
Tradeoff: keine Straßen-Detailtiefe beim Reinzoomen die Karte ist eine
Übersichts-/Verortungskarte. Die Detailseiten zeigen den Ort als Pin auf der
ganzen Deutschland-Silhouette (Locator). Neue amtliche Geometrie:
[isellsoap/deutschlandGeoJSON](https://github.com/isellsoap/deutschlandGeoJSON)
(`2_bundeslaender/3_mittel.geo.json` für mehr Detail).
## Externe Quellen ## Externe Quellen
+6 -2
View File
@@ -22,8 +22,12 @@ removePathAccents = true # ä/ö/ü → a/o/u in URLs
email = "info@offener-prozess.de" email = "info@offener-prozess.de"
[params.map] [params.map]
tileURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" # Kein externes CDN: Basemap = selbst gehostete Vektor-Tiles (Protomaps/OSM)
attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>-Mitwirkende' # als .pmtiles auf dem eigenen Server unter /tiles/, gerendert im Browser via
# protomaps-leaflet. Fallback ohne pmtiles = lokaler Umriss (static/geo/).
# pmtilesURL leer lassen → Default /tiles/germany.pmtiles (relativ zur baseURL).
flavor = "light"
attribution = "© OpenStreetMap-Mitwirkende"
# ============================================================================ # ============================================================================
# SPRACHEN # SPRACHEN
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -474,9 +474,11 @@ a:hover { color: var(--op-blau-dunkel); }
width: 100%; width: 100%;
height: 560px; height: 560px;
border: 2px solid var(--op-blau); border: 2px solid var(--op-blau);
background: var(--op-papier); background: #d9e1ea; /* neutrale Fläche „ausserhalb DE" (kein Tile-Grau) */
z-index: 0; /* unter der Sticky-Navbar halten */ z-index: 0; /* unter der Sticky-Navbar halten */
} }
/* Leaflet-eigener Container-Hintergrund (ohne Tiles) an den Hausstil angleichen */
.op-map .leaflet-container { background: #d9e1ea; }
.op-map--klein { height: 320px; } .op-map--klein { height: 320px; }
/* Leaflet-Popup im Hausstil: Bild oben, gelber Body (wie im Original) */ /* Leaflet-Popup im Hausstil: Bild oben, gelber Body (wie im Original) */
+95 -9
View File
@@ -1,6 +1,7 @@
/* ========================================================================= /* =========================================================================
OP·Karte Leaflet + OpenStreetMap OP·Karte Leaflet mit LOKALER Deutschland-Karte (keine externen Tiles/CDN)
- farbige Kreis-Marker je Kategorie, Clustering (markercluster) - 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 - Kategorie-Filter (Checkboxen) + Textsuche, synchron mit der Card-Liste
- Klick auf Card öffnet den zugehörigen Marker - Klick auf Card öffnet den zugehörigen Marker
Daten kommen aus <script type="application/json" id="op-map-data">. Daten kommen aus <script type="application/json" id="op-map-data">.
@@ -18,15 +19,95 @@
var places = payload.places || []; var places = payload.places || [];
var colors = payload.colors || {}; var colors = payload.colors || {};
var defaultColor = payload.defaultColor || "#1e2758"; var defaultColor = payload.defaultColor || "#1e2758";
var tileUrl = payload.tileUrl || "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; var klein = !!payload.klein;
var attribution = payload.attribution || var geojsonUrl = payload.geojsonUrl || "";
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>-Mitwirkende'; 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 }); var map = L.map(el, {
L.tileLayer(tileUrl, { maxZoom: 19, attribution: attribution }).addTo(map); scrollWheelZoom: false,
minZoom: 5, maxZoom: 18,
attributionControl: true
});
map.attributionControl.setPrefix(false).addAttribution(attribution);
map.on("focus", function () { map.scrollWheelZoom.enable(); }); map.on("focus", function () { map.scrollWheelZoom.enable(); });
map.on("blur", function () { map.scrollWheelZoom.disable(); }); 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() {
if (klein || !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 useCluster = places.length > 1 && typeof L.markerClusterGroup === "function";
var group = useCluster var group = useCluster
? L.markerClusterGroup({ maxClusterRadius: 45, showCoverageOnHover: false }) ? L.markerClusterGroup({ maxClusterRadius: 45, showCoverageOnHover: false })
@@ -93,8 +174,13 @@
}); });
var countCards = cards.filter(function (c) { return !c.classList.contains("is--hidden"); }).length; var countCards = cards.filter(function (c) { return !c.classList.contains("is--hidden"); }).length;
if (countEl) countEl.textContent = countCards; if (countEl) countEl.textContent = countCards;
if (bounds.isValid()) map.fitBounds(bounds.pad(0.15)); if (bounds.isValid()) {
else if (allBounds) map.fitBounds(allBounds.pad(0.15)); // 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 --- // --- DOM-Hooks ---
+5 -2
View File
@@ -35,8 +35,11 @@
"places" $places "places" $places
"colors" $colorMap "colors" $colorMap
"defaultColor" $default "defaultColor" $default
"tileUrl" (site.Params.map.tileURL | default "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png") "klein" ($klein | default false)
"attribution" (site.Params.map.attribution | default `&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>-Mitwirkende`) "pmtilesUrl" (site.Params.map.pmtilesURL | default ("tiles/germany.pmtiles" | relURL))
"flavor" (site.Params.map.flavor | default "light")
"geojsonUrl" ("geo/deutschland-bundeslaender.geojson" | relURL)
"attribution" (site.Params.map.attribution | default "© OpenStreetMap-Mitwirkende")
}} }}
<div id="op-map" class="op-map{{ if $klein }} op-map--klein{{ end }}" <div id="op-map" class="op-map{{ if $klein }} op-map--klein{{ end }}"
@@ -7,6 +7,8 @@
{{ if or .IsHome (eq .Type "schauplaetze") (eq .Type "kategorien") (eq .Type "erinnerungsorte") }} {{ if or .IsHome (eq .Type "schauplaetze") (eq .Type "kategorien") (eq .Type "erinnerungsorte") }}
<script src="{{ "vendor/leaflet/leaflet.js" | relURL }}"></script> <script src="{{ "vendor/leaflet/leaflet.js" | relURL }}"></script>
<script src="{{ "vendor/markercluster/leaflet.markercluster.js" | relURL }}"></script> <script src="{{ "vendor/markercluster/leaflet.markercluster.js" | relURL }}"></script>
{{/* Selbst gehostete Vektor-Basemap (Protomaps/OSM, .pmtiles) kein externes CDN */}}
<script src="{{ "vendor/protomaps/protomaps-leaflet.js" | relURL }}"></script>
{{ $map := resources.Get "js/map.js" }} {{ $map := resources.Get "js/map.js" }}
{{ if hugo.IsProduction }}{{ $map = $map | minify | fingerprint }}{{ end }} {{ if hugo.IsProduction }}{{ $map = $map | minify | fingerprint }}{{ end }}
<script src="{{ $map.RelPermalink }}"{{ with $map.Data.Integrity }} integrity="{{ . }}"{{ end }}></script> <script src="{{ $map.RelPermalink }}"{{ with $map.Data.Integrity }} integrity="{{ . }}"{{ end }}></script>
File diff suppressed because one or more lines are too long