diff --git a/src/app.html b/src/app.html
index 4705f0d..425c4ac 100644
--- a/src/app.html
+++ b/src/app.html
@@ -3,7 +3,6 @@
-
%sveltekit.head%
diff --git a/src/lib/components/WindAreaPanel.svelte b/src/lib/components/WindAreaPanel.svelte
index bdf82dc..21ef41f 100644
--- a/src/lib/components/WindAreaPanel.svelte
+++ b/src/lib/components/WindAreaPanel.svelte
@@ -45,6 +45,19 @@
if (!c) return null;
return `https://www.openstreetmap.org/?mlat=${c[0].toFixed(5)}&mlon=${c[1].toFixed(5)}#map=14/${c[0].toFixed(5)}/${c[1].toFixed(5)}`;
}
+
+ const exportInfo = $derived(
+ area
+ ? [
+ `Gebiet ${area.gebiets_nr}${area.bezeichnung ? ` – ${area.bezeichnung}` : ''}`,
+ area.flaeche_ha != null ? `Fläche: ${area.flaeche_ha.toFixed(1)} ha` : null,
+ area.anlagen_geplant != null ? `Geplante Anlagen: ${area.anlagen_geplant}` : null,
+ area.hoehe_max_m != null ? `Max. Nabenhöhe: ${area.hoehe_max_m} m` : null,
+ ]
+ .filter(Boolean)
+ .join('\n')
+ : ''
+ );
{#if area}
@@ -155,6 +168,31 @@
{/if}
+
+
+
+
Geodaten-Export ({area.gebiets_nr})
+
+
{/if}
diff --git a/src/lib/components/WindkarteMap.svelte b/src/lib/components/WindkarteMap.svelte
index 64cbb06..5265903 100644
--- a/src/lib/components/WindkarteMap.svelte
+++ b/src/lib/components/WindkarteMap.svelte
@@ -257,8 +257,22 @@
});
// Hover tooltip
- const hoverLabel = area.bezeichnung ?? p.name ?? "";
- layer.bindTooltip(hoverLabel, { sticky: true, className: "windkarte-tooltip" });
+ const nrLabel = area.gebiets_nr ?? p.name ?? "";
+ const title = area.bezeichnung
+ ? `${nrLabel} – ${area.bezeichnung}`
+ : nrLabel;
+ const lines: string[] = [];
+ if (area.flaeche_ha != null) lines.push(`Fläche: ${area.flaeche_ha.toFixed(1)} ha`);
+ if (area.anlagen_geplant != null) lines.push(`Geplante Anlagen: ${area.anlagen_geplant}`);
+ if (area.hoehe_max_m != null) lines.push(`Max. Nabenhöhe: ${area.hoehe_max_m} m`);
+ if (area.gemeinden && area.gemeinden.length > 0) {
+ lines.push(`Gemeinden: ${area.gemeinden.slice(0, 3).join(', ')}${area.gemeinden.length > 3 ? '…' : ''}`);
+ }
+ const esc = (s: string) => s.replace(/&/g, '&').replace(//g, '>');
+ const tooltipHtml =
+ `${esc(title)}
` +
+ (lines.length ? `${lines.map(esc).join('
')}
` : '');
+ layer.bindTooltip(tooltipHtml, { sticky: true, className: "windkarte-tooltip" });
// Permanent label via divIcon marker at polygon center
const nr = area.gebiets_nr ?? p.name;
diff --git a/src/lib/components/blocks/WindkarteBlock.svelte b/src/lib/components/blocks/WindkarteBlock.svelte
index d23dfb3..9f13ef4 100644
--- a/src/lib/components/blocks/WindkarteBlock.svelte
+++ b/src/lib/components/blocks/WindkarteBlock.svelte
@@ -76,7 +76,7 @@ let areas = $state([]);
);
areas = results.filter((r): r is WindArea => r !== null);
} else {
- const res = await fetch(`${CMS_BASE}/api/content/wind_area?limit=100`, {
+ const res = await fetch(`${CMS_BASE}/api/content/wind_area?_limit=100`, {
signal: AbortSignal.timeout(6_000),
});
if (res.ok) {
diff --git a/src/lib/iconify-mdi-subset.generated.json b/src/lib/iconify-mdi-subset.generated.json
index 476dc6a..17b1d24 100644
--- a/src/lib/iconify-mdi-subset.generated.json
+++ b/src/lib/iconify-mdi-subset.generated.json
@@ -72,6 +72,9 @@
"map-outline": {
"body": ""
},
+ "download": {
+ "body": ""
+ },
"map-marker-outline": {
"body": ""
},
@@ -81,9 +84,6 @@
"file-download": {
"body": ""
},
- "download": {
- "body": ""
- },
"google": {
"body": ""
},
diff --git a/src/lib/wind-area-export.ts b/src/lib/wind-area-export.ts
new file mode 100644
index 0000000..034e4eb
--- /dev/null
+++ b/src/lib/wind-area-export.ts
@@ -0,0 +1,119 @@
+import { env } from '$env/dynamic/public';
+import type { WindArea } from '$lib/windkarte';
+
+const CMS_BASE = () =>
+ (env.PUBLIC_CMS_URL || import.meta.env.PUBLIC_CMS_URL || 'http://localhost:3000').replace(/\/$/, '');
+
+export async function fetchAllWindAreas(): Promise {
+ const url = `${CMS_BASE()}/api/content/wind_area?_limit=500`;
+ const res = await fetch(url, { signal: AbortSignal.timeout(8_000) });
+ if (!res.ok) throw new Error(`CMS ${res.status}`);
+ const data = await res.json();
+ return (data.items ?? []) as WindArea[];
+}
+
+export function filterByGebietsNr(areas: WindArea[], id: string | null): WindArea[] {
+ if (!id) return areas;
+ return areas.filter((a) => a.gebiets_nr === id);
+}
+
+export function toGeoJSON(areas: WindArea[]): GeoJSON.FeatureCollection {
+ return {
+ type: 'FeatureCollection',
+ features: areas
+ .filter((a) => a.geometry)
+ .map((a) => ({
+ type: 'Feature' as const,
+ properties: {
+ id: a.gebiets_nr,
+ bezeichnung: a.bezeichnung ?? null,
+ status: a.status ?? 'rechtskraeftig',
+ flaeche_ha: a.flaeche_ha ?? null,
+ gemeinden: a.gemeinden ?? [],
+ anlagen_geplant: a.anlagen_geplant ?? null,
+ hoehe_max_m: a.hoehe_max_m ?? null,
+ investor: a.investor ?? null,
+ },
+ geometry: a.geometry!,
+ })),
+ };
+}
+
+// KML colors: AABBGGRR (alpha, blue, green, red)
+const KML_STYLES: Record = {
+ rechtskraeftig: { fill: '7f457a2d', line: 'ff284d1a' },
+ entwurf_2: { fill: '7f528ab0', line: 'ff305568' },
+ entwurf_2_voraussichtlich: { fill: '7f2a75d4', line: 'ff184a8a' },
+ entwurf_3: { fill: '7f85706e', line: 'ff504126' },
+};
+
+function coordsToKml(ring: number[][]): string {
+ return ring.map(([lng, lat]) => `${lng},${lat},0`).join(' ');
+}
+
+function geometryToKml(geom: GeoJSON.Geometry): string {
+ if (geom.type === 'Polygon') {
+ const [outer, ...holes] = geom.coordinates;
+ return `
+ ${coordsToKml(outer)}
+ ${holes.map((h) => `${coordsToKml(h)}`).join('')}
+ `;
+ }
+ if (geom.type === 'MultiPolygon') {
+ return `${geom.coordinates
+ .map(([outer, ...holes]) => `
+ ${coordsToKml(outer)}
+ ${holes.map((h) => `${coordsToKml(h)}`).join('')}
+ `)
+ .join('')}`;
+ }
+ return '';
+}
+
+export function toKML(areas: WindArea[]): string {
+ const styleIds = Object.keys(KML_STYLES);
+ const styleXml = styleIds
+ .map((id) => {
+ const { fill, line } = KML_STYLES[id];
+ return ``;
+ })
+ .join('\n ');
+
+ const placemarks = areas
+ .filter((a) => a.geometry)
+ .map((a) => {
+ const status = a.status ?? 'rechtskraeftig';
+ const desc = [
+ a.flaeche_ha != null ? `Fläche: ${a.flaeche_ha.toFixed(1)} ha` : null,
+ a.gemeinden?.length ? `Gemeinden: ${a.gemeinden.join(', ')}` : null,
+ `Status: ${status}`,
+ a.anlagen_geplant != null ? `Geplante Anlagen: ${a.anlagen_geplant}` : null,
+ ]
+ .filter(Boolean)
+ .join('
');
+
+ return `
+ ${esc(a.gebiets_nr)}${a.bezeichnung ? ` – ${esc(a.bezeichnung)}` : ''}
+ ${esc(desc)}
+ #${styleIds.includes(status) ? status : 'rechtskraeftig'}
+ ${geometryToKml(a.geometry!)}
+ `;
+ })
+ .join('\n ');
+
+ return `
+
+
+ Windvorranggebiete Thüringen
+ ${styleXml}
+ ${placemarks}
+
+`;
+}
+
+function esc(s: string): string {
+ return s.replace(/&/g, '&').replace(//g, '>');
+}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index dd7e573..fc75db9 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -106,6 +106,11 @@
const breadcrumbItems = $derived(pageData?.breadcrumbItems ?? []);
+ const faviconUrl = $derived(data.logoUrl ?? '/favicon.svg');
+ const faviconType = $derived(
+ faviconUrl.split('?')[0].toLowerCase().endsWith('.svg') ? 'image/svg+xml' : 'image/png'
+ );
+
const robotsContent = $derived.by(() => {
const base = (pageData?.robots ?? 'index, follow').trim();
if (/noindex/i.test(base)) return base;
@@ -195,7 +200,7 @@
{#if isEmbed}
-
+
{:else}
@@ -210,13 +215,10 @@
fetchpriority="high"
/>
{/if}
-
+
+
-
-
-
-
{seoTitle}
{#if seoDescription}
diff --git a/src/routes/api/vorranggebiete.geojson/+server.ts b/src/routes/api/vorranggebiete.geojson/+server.ts
new file mode 100644
index 0000000..689cac4
--- /dev/null
+++ b/src/routes/api/vorranggebiete.geojson/+server.ts
@@ -0,0 +1,17 @@
+import type { RequestHandler } from '@sveltejs/kit';
+import { fetchAllWindAreas, filterByGebietsNr, toGeoJSON } from '$lib/wind-area-export';
+
+export const GET: RequestHandler = async ({ url }) => {
+ const id = url.searchParams.get('id');
+ const areas = filterByGebietsNr(await fetchAllWindAreas(), id);
+ const body = JSON.stringify(toGeoJSON(areas), null, 2);
+ const filename = id ? `vorranggebiet-${id}.geojson` : 'vorranggebiete.geojson';
+
+ return new Response(body, {
+ headers: {
+ 'Content-Type': 'application/geo+json; charset=utf-8',
+ 'Content-Disposition': `attachment; filename="${filename}"`,
+ 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
+ },
+ });
+};
diff --git a/src/routes/api/vorranggebiete.kml/+server.ts b/src/routes/api/vorranggebiete.kml/+server.ts
new file mode 100644
index 0000000..9de2f9b
--- /dev/null
+++ b/src/routes/api/vorranggebiete.kml/+server.ts
@@ -0,0 +1,17 @@
+import type { RequestHandler } from '@sveltejs/kit';
+import { fetchAllWindAreas, filterByGebietsNr, toKML } from '$lib/wind-area-export';
+
+export const GET: RequestHandler = async ({ url }) => {
+ const id = url.searchParams.get('id');
+ const areas = filterByGebietsNr(await fetchAllWindAreas(), id);
+ const body = toKML(areas);
+ const filename = id ? `vorranggebiet-${id}.kml` : 'vorranggebiete.kml';
+
+ return new Response(body, {
+ headers: {
+ 'Content-Type': 'application/vnd.google-earth.kml+xml; charset=utf-8',
+ 'Content-Disposition': `attachment; filename="${filename}"`,
+ 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
+ },
+ });
+};
diff --git a/static/favicon.ico b/static/favicon.ico
deleted file mode 100644
index 7f48a94..0000000
Binary files a/static/favicon.ico and /dev/null differ
diff --git a/static/favicons/android-icon-144x144.png b/static/favicons/android-icon-144x144.png
deleted file mode 100644
index e8c8b34..0000000
Binary files a/static/favicons/android-icon-144x144.png and /dev/null differ
diff --git a/static/favicons/android-icon-192x192.png b/static/favicons/android-icon-192x192.png
deleted file mode 100644
index 5a8f829..0000000
Binary files a/static/favicons/android-icon-192x192.png and /dev/null differ
diff --git a/static/favicons/android-icon-36x36.png b/static/favicons/android-icon-36x36.png
deleted file mode 100644
index d43bc5d..0000000
Binary files a/static/favicons/android-icon-36x36.png and /dev/null differ
diff --git a/static/favicons/android-icon-48x48.png b/static/favicons/android-icon-48x48.png
deleted file mode 100644
index 09c3614..0000000
Binary files a/static/favicons/android-icon-48x48.png and /dev/null differ
diff --git a/static/favicons/android-icon-72x72.png b/static/favicons/android-icon-72x72.png
deleted file mode 100644
index 160cc4b..0000000
Binary files a/static/favicons/android-icon-72x72.png and /dev/null differ
diff --git a/static/favicons/android-icon-96x96.png b/static/favicons/android-icon-96x96.png
deleted file mode 100644
index f68e007..0000000
Binary files a/static/favicons/android-icon-96x96.png and /dev/null differ
diff --git a/static/favicons/apple-icon-114x114.png b/static/favicons/apple-icon-114x114.png
deleted file mode 100644
index a5b46d8..0000000
Binary files a/static/favicons/apple-icon-114x114.png and /dev/null differ
diff --git a/static/favicons/apple-icon-120x120.png b/static/favicons/apple-icon-120x120.png
deleted file mode 100644
index c3e30c2..0000000
Binary files a/static/favicons/apple-icon-120x120.png and /dev/null differ
diff --git a/static/favicons/apple-icon-144x144.png b/static/favicons/apple-icon-144x144.png
deleted file mode 100644
index fb74aa1..0000000
Binary files a/static/favicons/apple-icon-144x144.png and /dev/null differ
diff --git a/static/favicons/apple-icon-152x152.png b/static/favicons/apple-icon-152x152.png
deleted file mode 100644
index 7d09c1d..0000000
Binary files a/static/favicons/apple-icon-152x152.png and /dev/null differ
diff --git a/static/favicons/apple-icon-180x180.png b/static/favicons/apple-icon-180x180.png
deleted file mode 100644
index 5738ab6..0000000
Binary files a/static/favicons/apple-icon-180x180.png and /dev/null differ
diff --git a/static/favicons/apple-icon-57x57.png b/static/favicons/apple-icon-57x57.png
deleted file mode 100644
index 7bab301..0000000
Binary files a/static/favicons/apple-icon-57x57.png and /dev/null differ
diff --git a/static/favicons/apple-icon-60x60.png b/static/favicons/apple-icon-60x60.png
deleted file mode 100644
index 0372483..0000000
Binary files a/static/favicons/apple-icon-60x60.png and /dev/null differ
diff --git a/static/favicons/apple-icon-72x72.png b/static/favicons/apple-icon-72x72.png
deleted file mode 100644
index 24270d2..0000000
Binary files a/static/favicons/apple-icon-72x72.png and /dev/null differ
diff --git a/static/favicons/apple-icon-76x76.png b/static/favicons/apple-icon-76x76.png
deleted file mode 100644
index b5e9092..0000000
Binary files a/static/favicons/apple-icon-76x76.png and /dev/null differ
diff --git a/static/favicons/apple-icon-precomposed.png b/static/favicons/apple-icon-precomposed.png
deleted file mode 100644
index ff96e2b..0000000
Binary files a/static/favicons/apple-icon-precomposed.png and /dev/null differ
diff --git a/static/favicons/apple-icon.png b/static/favicons/apple-icon.png
deleted file mode 100644
index ff96e2b..0000000
Binary files a/static/favicons/apple-icon.png and /dev/null differ
diff --git a/static/favicons/browserconfig.xml b/static/favicons/browserconfig.xml
deleted file mode 100644
index 890faf3..0000000
--- a/static/favicons/browserconfig.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
- #ffffff
-
-
-
diff --git a/static/favicons/favicon-16x16.png b/static/favicons/favicon-16x16.png
deleted file mode 100644
index 515bd88..0000000
Binary files a/static/favicons/favicon-16x16.png and /dev/null differ
diff --git a/static/favicons/favicon-32x32.png b/static/favicons/favicon-32x32.png
deleted file mode 100644
index 0aecd45..0000000
Binary files a/static/favicons/favicon-32x32.png and /dev/null differ
diff --git a/static/favicons/favicon-96x96.png b/static/favicons/favicon-96x96.png
deleted file mode 100644
index 4af9923..0000000
Binary files a/static/favicons/favicon-96x96.png and /dev/null differ
diff --git a/static/favicons/favicon.ico b/static/favicons/favicon.ico
deleted file mode 100644
index 07e927c..0000000
Binary files a/static/favicons/favicon.ico and /dev/null differ
diff --git a/static/favicons/manifest.json b/static/favicons/manifest.json
deleted file mode 100644
index 60bed5f..0000000
--- a/static/favicons/manifest.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "name": "Windwiderstand",
- "short_name": "Windwiderstand",
- "start_url": "/",
- "display": "browser",
- "background_color": "#ffffff",
- "theme_color": "#ffffff",
- "icons": [
- {
- "src": "/favicons/android-icon-36x36.png",
- "sizes": "36x36",
- "type": "image/png",
- "density": "0.75"
- },
- {
- "src": "/favicons/android-icon-48x48.png",
- "sizes": "48x48",
- "type": "image/png",
- "density": "1.0"
- },
- {
- "src": "/favicons/android-icon-72x72.png",
- "sizes": "72x72",
- "type": "image/png",
- "density": "1.5"
- },
- {
- "src": "/favicons/android-icon-96x96.png",
- "sizes": "96x96",
- "type": "image/png",
- "density": "2.0"
- },
- {
- "src": "/favicons/android-icon-144x144.png",
- "sizes": "144x144",
- "type": "image/png",
- "density": "3.0"
- },
- {
- "src": "/favicons/android-icon-192x192.png",
- "sizes": "192x192",
- "type": "image/png",
- "density": "4.0"
- }
- ]
-}
diff --git a/static/favicons/ms-icon-144x144.png b/static/favicons/ms-icon-144x144.png
deleted file mode 100644
index fb74aa1..0000000
Binary files a/static/favicons/ms-icon-144x144.png and /dev/null differ
diff --git a/static/favicons/ms-icon-150x150.png b/static/favicons/ms-icon-150x150.png
deleted file mode 100644
index 4900f79..0000000
Binary files a/static/favicons/ms-icon-150x150.png and /dev/null differ
diff --git a/static/favicons/ms-icon-310x310.png b/static/favicons/ms-icon-310x310.png
deleted file mode 100644
index 0f811f1..0000000
Binary files a/static/favicons/ms-icon-310x310.png and /dev/null differ
diff --git a/static/favicons/ms-icon-70x70.png b/static/favicons/ms-icon-70x70.png
deleted file mode 100644
index 45bcb06..0000000
Binary files a/static/favicons/ms-icon-70x70.png and /dev/null differ
diff --git a/static/safari-pinned-tab.svg b/static/safari-pinned-tab.svg
index 3229aa1..cf9f762 100644
--- a/static/safari-pinned-tab.svg
+++ b/static/safari-pinned-tab.svg
@@ -1,10 +1 @@
-
+