feat(windkarte): Leaflet-Block für Windvorranggebiete SWT
Deploy / verify (push) Successful in 58s
Deploy / deploy (push) Successful in 1m18s

Neuer CMS-Block `wind_map` — platzierbar auf jeder Seite per Row-Content.
Fetcht wind_area-Einträge + GeoJSON-Asset client-side, rendert Leaflet-Karte
(CartoDB Light) mit farbcodierten Polygonen nach Planungsstatus. Klick auf
Gebiet öffnet Slide-in-Panel mit Kenndaten, Gemeinden, Stellungnahme-Link
und Puffer-Ringen (200/600/1000 m via Turf). Erweiterbar für 2. Entwurf 2026.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-13 09:20:32 +02:00
parent cdc2a79950
commit 39d7d70c8b
9 changed files with 570 additions and 5 deletions
@@ -0,0 +1,95 @@
<script lang="ts">
import { onMount } from "svelte";
import { browser } from "$app/environment";
import { env } from "$env/dynamic/public";
import type { WindMapBlockData } from "$lib/block-types";
import type { WindArea } from "$lib/windkarte";
import WindAreaPanel from "$lib/components/WindAreaPanel.svelte";
let { block }: { block: WindMapBlockData } = $props();
const CMS_BASE = (
env.PUBLIC_CMS_URL || "http://localhost:3000"
).replace(/\/$/, "");
const GEOJSON_URL = `${CMS_BASE}/api/assets/geodaten/windvorranggebiete_swt_2012.geojson`;
let areas = $state<WindArea[]>([]);
let selectedArea = $state<WindArea | null>(null);
let mapReady = $state(false);
// Dynamic import: WindkarteMap needs window/document
const MapComponent = browser
? import("$lib/components/WindkarteMap.svelte").then((m) => m.default)
: null;
onMount(async () => {
try {
const res = await fetch(`${CMS_BASE}/api/content/wind_area?limit=100`, {
signal: AbortSignal.timeout(6_000),
});
if (res.ok) {
const data = await res.json();
areas = data.items ?? [];
}
} catch {
// Map renders without CMS metadata — GeoJSON tooltips still work
}
mapReady = true;
});
</script>
<div class="wind-map-block">
{#if block.title || block.subtitle}
<div class="mb-4">
{#if block.title}
<h2 class="text-xl font-semibold text-stein-800">{block.title}</h2>
{/if}
{#if block.subtitle}
<p class="mt-0.5 text-sm text-stein-500">{block.subtitle}</p>
{/if}
</div>
{/if}
<div
class="relative overflow-hidden rounded-xl border border-stein-200 shadow-sm"
style="height: 520px;"
>
{#if browser && MapComponent && mapReady}
{#await MapComponent then Map}
<Map
{areas}
geojsonUrl={GEOJSON_URL}
onselect={(area) => { selectedArea = area; }}
/>
{/await}
{:else}
<div class="flex h-full items-center justify-center bg-stein-50 text-stein-400 text-sm">
Karte wird geladen …
</div>
{/if}
<!-- Sliding panel -->
<div
class="pointer-events-none absolute inset-y-0 right-0 z-[500] w-72 transition-transform duration-300 ease-in-out {selectedArea ? 'translate-x-0' : 'translate-x-full'}"
>
<WindAreaPanel
area={selectedArea}
onclose={() => { selectedArea = null; }}
/>
</div>
</div>
<!-- Legend -->
<div class="mt-2 flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-stein-400">
<span class="flex items-center gap-1.5">
<span class="inline-block size-2.5 rounded-sm" style="background:#2d7a45; opacity:.7"></span>
Rechtskräftig (2012)
</span>
<span class="flex items-center gap-1.5">
<span class="inline-block size-2.5 rounded-sm" style="background:#b08a52; opacity:.7"></span>
2. Entwurf 2026
</span>
<span class="ml-auto">Geodaten: © GDI-Th, dl-de/by-2-0</span>
</div>
</div>