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
+73 -23
View File
@@ -21,41 +21,91 @@
var defaultColor = payload.defaultColor || "#1e2758";
var klein = !!payload.klein;
var geojsonUrl = payload.geojsonUrl || "";
var attribution = payload.attribution || "Geodaten: © GeoBasis-DE / BKG";
var germanyBounds = null;
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: 12,
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(); });
// Lokale Deutschland-Karte (Bundesländer) als Vektor-Hintergrund laden.
// Same-Origin-Fetch aus dem Repo kein externer Request.
// 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) return;
var land = L.geoJSON(geo, {
style: {
color: "#1e2758", weight: 1, opacity: .85,
fillColor: "#f4f1e6", fillOpacity: 1
},
interactive: false
}).addTo(map);
land.bringToBack();
germanyBounds = land.getBounds();
// Detail-Minikarte = Verortung: ganz Deutschland mit dem einen Pin zeigen.
// Grosse Karte: nur wenn (noch) keine Marker den Ausschnitt bestimmen.
if (klein || !records.length) {
try { map.fitBounds(germanyBounds.pad(0.02)); } catch (e) {}
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 () {});
.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";
@@ -125,11 +175,11 @@
var countCards = cards.filter(function (c) { return !c.classList.contains("is--hidden"); }).length;
if (countEl) countEl.textContent = countCards;
if (bounds.isValid()) {
// Auf der Detail-Minikarte nicht ins Leere zoomen: Umkreis statt Punkt
if (klein) map.fitBounds(bounds.pad(0.15), { maxZoom: 8 });
// 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: 8 } : undefined);
else if (allBounds) map.fitBounds(allBounds.pad(0.15), klein ? { maxZoom: 17 } : undefined);
else if (germanyBounds) map.fitBounds(germanyBounds.pad(0.02));
}