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)
This commit is contained in:
maik
2026-08-19 15:58:05 +02:00
parent bfcb2ce6c0
commit fb4310666c
6 changed files with 112 additions and 27 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/
+6 -3
View File
@@ -22,9 +22,12 @@ removePathAccents = true # ä/ö/ü → a/o/u in URLs
email = "info@offener-prozess.de" email = "info@offener-prozess.de"
[params.map] [params.map]
# Keine externen Kacheln/CDN Basemap ist die lokale Bundesländer-GeoJSON # Kein externes CDN: Basemap = selbst gehostete Vektor-Tiles (Protomaps/OSM)
# unter static/geo/. Attribution nur als Text (kein Request). # als .pmtiles auf dem eigenen Server unter /tiles/, gerendert im Browser via
attribution = "Geodaten: © GeoBasis-DE / BKG (Bundesländergrenzen)" # 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
+71 -21
View File
@@ -21,41 +21,91 @@
var defaultColor = payload.defaultColor || "#1e2758"; var defaultColor = payload.defaultColor || "#1e2758";
var klein = !!payload.klein; var klein = !!payload.klein;
var geojsonUrl = payload.geojsonUrl || ""; var geojsonUrl = payload.geojsonUrl || "";
var attribution = payload.attribution || "Geodaten: © GeoBasis-DE / BKG"; var pmtilesUrl = payload.pmtilesUrl || "";
var germanyBounds = null; 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, { var map = L.map(el, {
scrollWheelZoom: false, scrollWheelZoom: false,
minZoom: 5, maxZoom: 12, minZoom: 5, maxZoom: 18,
attributionControl: true attributionControl: true
}); });
map.attributionControl.setPrefix(false).addAttribution(attribution); 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(); });
// Lokale Deutschland-Karte (Bundesländer) als Vektor-Hintergrund laden. // Ausschnitt fest auf Deutschland begrenzen: kein Wegscrollen ins Ausland.
// Same-Origin-Fetch aus dem Repo kein externer Request. 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) { if (geojsonUrl) {
fetch(geojsonUrl) fetch(geojsonUrl)
.then(function (r) { return r.ok ? r.json() : null; }) .then(function (r) { return r.ok ? r.json() : null; })
.then(function (geo) { .then(function (geo) {
if (!geo) return; if (!geo) { fitGermanyIfNeeded(); return; }
var land = L.geoJSON(geo, { if (!tilesActive) {
style: { L.geoJSON(geo, {
color: "#1e2758", weight: 1, opacity: .85, style: { color: "#1e2758", weight: 1, opacity: .85, fillColor: "#f4f1e6", fillOpacity: 1 },
fillColor: "#f4f1e6", fillOpacity: 1
},
interactive: false interactive: false
}).addTo(map); }).addTo(map).bringToBack();
land.bringToBack(); }
germanyBounds = land.getBounds(); addGermanyMask(geo);
// Detail-Minikarte = Verortung: ganz Deutschland mit dem einen Pin zeigen. fitGermanyIfNeeded();
// Grosse Karte: nur wenn (noch) keine Marker den Ausschnitt bestimmen. })
.catch(function () { fitGermanyIfNeeded(); });
}
function fitGermanyIfNeeded() {
if (klein || !records.length) { if (klein || !records.length) {
try { map.fitBounds(germanyBounds.pad(0.02)); } catch (e) {} try { map.fitBounds(germanyBounds.pad(0.02)); } catch (e) {}
} }
}) }
.catch(function () {});
// 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";
@@ -125,11 +175,11 @@
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()) { if (bounds.isValid()) {
// Auf der Detail-Minikarte nicht ins Leere zoomen: Umkreis statt Punkt // Detail-Minikarte: nah an Strasse/Haus heran (Zoom 17)
if (klein) map.fitBounds(bounds.pad(0.15), { maxZoom: 8 }); if (klein) map.fitBounds(bounds.pad(0.15), { maxZoom: 17 });
else map.fitBounds(bounds.pad(0.15)); else map.fitBounds(bounds.pad(0.15));
} }
else if (allBounds) map.fitBounds(allBounds.pad(0.15), klein ? { maxZoom: 8 } : undefined); else if (allBounds) map.fitBounds(allBounds.pad(0.15), klein ? { maxZoom: 17 } : undefined);
else if (germanyBounds) map.fitBounds(germanyBounds.pad(0.02)); else if (germanyBounds) map.fitBounds(germanyBounds.pad(0.02));
} }
+3 -1
View File
@@ -36,8 +36,10 @@
"colors" $colorMap "colors" $colorMap
"defaultColor" $default "defaultColor" $default
"klein" ($klein | default false) "klein" ($klein | default false)
"pmtilesUrl" (site.Params.map.pmtilesURL | default ("tiles/germany.pmtiles" | relURL))
"flavor" (site.Params.map.flavor | default "light")
"geojsonUrl" ("geo/deutschland-bundeslaender.geojson" | relURL) "geojsonUrl" ("geo/deutschland-bundeslaender.geojson" | relURL)
"attribution" (site.Params.map.attribution | default "Geodaten: © GeoBasis-DE / BKG (Bundesländergrenzen)") "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