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})

+
+ + + GeoJSON + + + + KML + +
+
{/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 @@ - - - +