feat(windkarte): Leaflet-Block für Windvorranggebiete SWT
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:
@@ -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>
|
||||
Reference in New Issue
Block a user