Files
windwiderstand/src/lib/components/WindkarteMap.svelte
T
Peter Meier c66899fa9b
Deploy / verify (push) Successful in 48s
Deploy / deploy (push) Successful in 59s
feat(windkarte): slugPrefix enforcement, name→gebiets_nr, name field removed
- RustyCMS: SchemaDefinition.slug_prefix + auto-prepend in create_entry
- wind_area/wind_map schemas: slugPrefix config, name field removed
- wind_area entries: name→gebiets_nr (faktische Gebietsnummer, GeoJSON-Join-Key)
- WindkarteMap/WindAreaPanel: Join + Anzeige auf gebiets_nr

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 09:35:28 +02:00

152 lines
5.0 KiB
Svelte

<script lang="ts">
import "leaflet/dist/leaflet.css";
import { onMount, onDestroy } from "svelte";
import type { WindArea } from "$lib/windkarte";
let {
areas,
geojsonUrl,
onselect,
}: {
areas: WindArea[];
geojsonUrl: string;
onselect: (area: WindArea | null) => void;
} = $props();
let mapEl: HTMLDivElement;
let map: import("leaflet").Map | null = null;
let bufferLayer: import("leaflet").LayerGroup | null = null;
let selectedSlug = $state<string | null>(null);
let geojsonLayer: import("leaflet").GeoJSON | null = null;
const COLORS: Record<string, { fill: string; stroke: string }> = {
rechtskraeftig: { fill: "#2d7a45", stroke: "#1a4d28" },
entwurf_2: { fill: "#b08a52", stroke: "#6e5530" },
entwurf_3: { fill: "#436e85", stroke: "#264150" },
};
function style(status: string, selected: boolean) {
const c = COLORS[status] ?? COLORS.rechtskraeftig;
return {
color: c.stroke,
fillColor: c.fill,
weight: selected ? 2.5 : 1.5,
fillOpacity: selected ? 0.45 : 0.25,
opacity: 1,
};
}
async function drawBuffer(L: typeof import("leaflet"), feature: GeoJSON.Feature) {
const { buffer } = await import("@turf/buffer");
if (bufferLayer) bufferLayer.clearLayers();
else bufferLayer = L.layerGroup().addTo(map!);
const distances = [
{ m: 200, label: "200 m", color: "#e35651", dash: "4 4" },
{ m: 600, label: "600 m", color: "#b08a52", dash: "6 3" },
{ m: 1000, label: "1000 m", color: "#436e85", dash: "8 4" },
];
for (const { m, color, dash } of distances) {
const buffered = buffer(feature, m / 1000, { units: "kilometers" });
if (!buffered) continue;
L.geoJSON(buffered, {
style: { color, fillColor: color, fillOpacity: 0.05, weight: 1.5, dashArray: dash },
}).addTo(bufferLayer!);
}
}
onMount(async () => {
const L = await import("leaflet");
// Fix Leaflet default icon path issue with Vite bundling
const iconUrl = new URL("leaflet/dist/images/marker-icon.png", import.meta.url).href;
const icon2xUrl = new URL("leaflet/dist/images/marker-icon-2x.png", import.meta.url).href;
const shadowUrl = new URL("leaflet/dist/images/marker-shadow.png", import.meta.url).href;
L.Icon.Default.mergeOptions({ iconUrl, iconRetinaUrl: icon2xUrl, shadowUrl });
map = L.map(mapEl, { zoomControl: true }).setView([50.62, 10.42], 9);
L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/attributions">CARTO</a> | Geodaten: © GDI-Th, dl-de/by-2-0',
subdomains: "abcd",
maxZoom: 16,
}).addTo(map);
const byNr = Object.fromEntries(areas.map((a) => [a.gebiets_nr, a]));
try {
const geoRes = await fetch(geojsonUrl);
const geo: GeoJSON.FeatureCollection = await geoRes.json();
geojsonLayer = L.geoJSON(geo, {
style: (f) => {
const s = f?.properties?.status ?? "rechtskraeftig";
return style(s, false);
},
onEachFeature: (feature, layer) => {
const cmsArea = byNr[feature.properties?.name] ?? null;
if (cmsArea) feature.properties.cms = cmsArea;
layer.on("click", async () => {
const name = feature.properties?.name;
const prev = selectedSlug;
selectedSlug = name ?? null;
// Re-style all layers
geojsonLayer?.eachLayer((l) => {
const f = (l as import("leaflet").Path & { feature?: GeoJSON.Feature }).feature;
if (!f) return;
const s = f.properties?.status ?? "rechtskraeftig";
const sel = f.properties?.name === selectedSlug;
(l as import("leaflet").Path).setStyle(style(s, sel));
});
if (cmsArea) {
onselect(cmsArea);
await drawBuffer(L, feature);
} else {
onselect(null);
bufferLayer?.clearLayers();
}
// Zoom to feature if first click or switching areas
if (prev !== name) {
map!.fitBounds((layer as import("leaflet").Polygon).getBounds(), { padding: [60, 60] });
}
});
// Tooltip with name
const label = feature.properties?.bezeichnung ?? feature.properties?.name ?? "";
layer.bindTooltip(label, { sticky: true, className: "windkarte-tooltip" });
},
}).addTo(map);
} catch (e) {
console.error("GeoJSON laden fehlgeschlagen:", e);
}
});
onDestroy(() => {
map?.remove();
map = null;
});
</script>
<div bind:this={mapEl} class="h-full w-full"></div>
<style>
:global(.windkarte-tooltip) {
background: white;
border: 1px solid #d5d8d6;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0,0,0,0.15);
color: #333735;
font-size: 12px;
font-weight: 500;
padding: 4px 8px;
}
:global(.windkarte-tooltip::before) {
display: none;
}
</style>