import './style.css'; import L from 'leaflet'; window.L = L; import * as turf from '@turf/turf'; import 'leaflet-control-geocoder'; import 'leaflet-control-geocoder/dist/Control.Geocoder.css'; import icon from 'leaflet/dist/images/marker-icon.png'; import iconRetina from 'leaflet/dist/images/marker-icon-2x.png'; import shadow from 'leaflet/dist/images/marker-shadow.png'; import proj4Module from 'proj4'; const proj4 = proj4Module.default || proj4Module; window.proj4 = proj4; proj4.defs("EPSG:25832", "+proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs"); const EPSG25832 = "EPSG:25832"; const EPSG4326 = "EPSG:4326"; // --- Expert Constants --- const MILITARY_RADARS = [ { name: "Avenwedde", lat: 51.936, lng: 8.421 }, { name: "Erndtebrück", lat: 50.992, lng: 8.246 }, { name: "Brakel (Auenhausen)", lat: 51.716, lng: 9.183 }, { name: "Marienbaum (Kalkar)", lat: 51.750, lng: 6.366 } ]; // --- Icons --- const DefaultIcon = L.icon({ iconUrl: icon, iconRetinaUrl: iconRetina, shadowUrl: shadow, iconSize: [25, 41], iconAnchor: [12, 41] }); L.Marker.prototype.options.icon = DefaultIcon; const turbineIcon = L.divIcon({ className: 'custom-turbine-icon', html: `
`, iconSize: [60, 85], iconAnchor: [30, 85] }); // --- Karten-Setup --- const map = L.map('map', { zoomControl: false }).setView([51.4332, 7.6616], 10); // Global export function for onclick handlers window.exportToPDF = async function () { const element = document.querySelector('.result-card'); if (!element) return; const opt = { margin: 10, filename: `Analysebericht_${new Date().toISOString().split('T')[0]}.pdf`, image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, useCORS: true, logging: false }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, pagebreak: { mode: ['avoid-all', 'css', 'legacy'] } }; // Add a class for PDF styling element.classList.add('pdf-export-mode'); try { await html2pdf().set(opt).from(element).save(); } catch (err) { console.error("PDF Export failed:", err); alert("PDF Export fehlgeschlagen. Bitte versuchen Sie es erneut."); } finally { element.classList.remove('pdf-export-mode'); } }; const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'); const dopLayer = L.tileLayer.wms('/api/wms/dop', { layers: 'nw_dop_rgb', format: 'image/png', transparent: false, attribution: 'Geobasis NRW' }); osmLayer.addTo(map); // WMS Overlays const overlays = { "Schutzgebiete (LINFOS)": L.tileLayer.wms('/api/wms/linfos', { layers: 'Naturschutzgebiete,Landschaftsschutzgebiet,FFH-Gebiete,Vogelschutzgebiete', format: 'image/png', transparent: true, opacity: 0.6 }), "Wasserschutzgebiete": L.tileLayer.wms('/api/wms/wsg', { layers: 'wsg_zone3,wsg_zone2,wsg_zone1', format: 'image/png', transparent: true, opacity: 0.5 }), "Überschwemmungsgebiete": L.tileLayer.wms('/api/wms/uesg', { layers: 'nw_uesg_t7', format: 'image/png', transparent: true, opacity: 0.5 }), "Straßennetz": L.tileLayer.wms('/api/wms/roads', { layers: 'wms_strassen_nrw', format: 'image/png', transparent: true, opacity: 0.8 }), "Hausumringe": L.tileLayer.wms('/api/wms/hu', { layers: 'nw_hu_gebaeude', format: 'image/png', transparent: true, opacity: 0.8 }), "Windenergieanlagen (Bestand)": L.tileLayer.wms('/api/wms/energy', { layers: 'ea_wind', format: 'image/png', transparent: true }), "Stromnetz (OpenInfraMap)": L.tileLayer('https://tiles.openinframap.org/power/{z}/{x}/{y}.png', { attribution: 'Map data © OpenStreetMap contributors, OpenInfraMap' }) }; const alkisLayer = L.tileLayer.wms('/api/wms/alkis', { layers: 'adv_alkis_flurstuecke,adv_alkis_flurstuecksnummern', format: 'image/png', transparent: true, minZoom: 16, version: '1.1.1', attribution: 'Geobasis NRW' }).addTo(map); // Vektor Overlays const planningLayer = L.geoJSON(null, { style: { color: '#0ea5e9', weight: 2, fillOpacity: 0.2 }, onEachFeature: (f, l) => l.bindPopup(`Regionalplan
${f.properties.GENNAME || 'Windenergiebereich'}`) }); const potentialLayer = L.geoJSON(null, { style: { color: '#10b981', weight: 2, fillOpacity: 0.2 }, onEachFeature: (f, l) => l.bindPopup(`LANUV Potenzial`) }); const userLayer = L.layerGroup().addTo(map); const distLayer = L.layerGroup().addTo(map); const residenceLayer = L.geoJSON(null, { style: { color: 'brown', weight: 1, fillOpacity: 0.3 } }).addTo(map); L.control.layers({ "OpenStreetMap": osmLayer, "Satellit (DOP)": dopLayer }, { ...overlays, "Regionalplan (Vektor)": planningLayer, "LANUV Potenzial (Vektor)": potentialLayer, "Luftfahrt (Flughäfen/VOR)": L.tileLayer.wms('/api/wms/energy', { layers: 'ea_luft_flughafe,ea_luft_vor', format: 'image/png', transparent: true }), "Gebäude (ALKIS)": residenceLayer, "Flurstücke & Nummern": alkisLayer }, { collapsed: true }).addTo(map); // Geocoder const geocoder = L.Control.geocoder({ defaultMarkGeocode: false, collapsed: false, placeholder: "Adresse suchen..." }).on('markgeocode', e => { const center = e.geocode.center; map.setView(center, 15); placeMarker(center); }); // Move geocoder to search-container const geocoderContainer = document.getElementById('search-container'); if (geocoderContainer) { geocoderContainer.appendChild(geocoder.onAdd(map)); } // Basemap Toggle Logic const btnOsm = document.getElementById('toggle-osm'); const btnDop = document.getElementById('toggle-dop'); if (btnOsm && btnDop) { btnOsm.onclick = () => { map.removeLayer(dopLayer); osmLayer.addTo(map); btnOsm.classList.add('active'); btnDop.classList.remove('active'); }; btnDop.onclick = () => { map.removeLayer(osmLayer); dopLayer.addTo(map); btnDop.classList.add('active'); btnOsm.classList.remove('active'); }; } // Daten vorladen fetch('/api/local/planning-data').then(r => r.json()).then(data => planningLayer.addData(data)).catch(console.error); fetch('/api/local/potential-data').then(r => r.json()).then(data => potentialLayer.addData(data)).catch(console.error); let userMarker = null; map.on('click', e => placeMarker(e.latlng)); function placeMarker(latlng) { if (userMarker) { userLayer.removeLayer(userMarker); } userMarker = L.marker(latlng, { draggable: true, icon: turbineIcon }).addTo(userLayer); userMarker.on('drag', ev => { updateDistanceLine(ev.latlng); }); userMarker.on('dragend', updateAnalysis); document.getElementById('check-site-btn').disabled = false; document.getElementById('intro-hint')?.classList.add('hidden'); const introMsg = document.getElementById('marker-hint-msg'); if (introMsg) introMsg.innerHTML = '✓ Standort gewählt'; updateAnalysis(); } /** * Lightweight update for the distance line during dragging */ function updateDistanceLine(latlng) { if (!residenceLayer || residenceLayer.getLayers().length === 0) return; const pt = turf.point([latlng.lng, latlng.lat]); let minDist = Infinity; let nearPt = null; residenceLayer.eachLayer(l => { const poly = l.feature; try { // Find distance to the closest point on the polygon boundary const line = turf.polygonToLine(poly); const nearest = turf.nearestPointOnLine(line, pt); const d = turf.distance(pt, nearest, { units: 'meters' }); if (d < minDist) { minDist = d; nearPt = nearest.geometry.coordinates; } } catch (err) { // Fallback for points/errors: use centroid try { const center = turf.centerOfMass(poly); const d = turf.distance(pt, center, { units: 'meters' }); if (d < minDist) { minDist = d; nearPt = center.geometry.coordinates; } } catch (e2) { } } }); distLayer.clearLayers(); if (nearPt && minDist <= 1000) { const roundedDist = Math.round(minDist); const statusColor = '#ef4444'; // Always red as requested L.polyline([latlng, [nearPt[1], nearPt[0]]], { color: statusColor, weight: 5, dashArray: '10, 15', opacity: 0.8 }).addTo(distLayer); userMarker.setTooltipContent(`
Nächstes Wohnhaus
${roundedDist}m
`); } else if (userMarker.isTooltipOpen && userMarker.getTooltip().getContent().includes('Wohnhaus')) { // Clear if too far userMarker.setTooltipContent(`
Kein Wohnhaus im Nahbereich (1000m)
`); } } async function updateAnalysis() { if (!userMarker) return; const pos = userMarker.getLatLng(); if (!pos || (pos.lat === 0 && pos.lng === 0)) return; // Avoid empty/default coords const pt = turf.point([pos.lng, pos.lat]); const bboxStr = `${pos.lng - 0.02},${pos.lat - 0.015},${pos.lng + 0.02},${pos.lat + 0.015}`; try { const res = await fetch(`/api/local/buildings?bbox=${bboxStr}&limit=1000`); if (res.ok) { const data = await res.json(); residenceLayer.clearLayers(); residenceLayer.addData(data); let minDist = Infinity; let nearPt = null; residenceLayer.eachLayer(l => { const poly = l.feature; try { const line = turf.polygonToLine(poly); const nearest = turf.nearestPointOnLine(line, pt); const d = turf.distance(pt, nearest, { units: 'meters' }); if (d < minDist) { minDist = d; nearPt = nearest.geometry.coordinates; } } catch (err) { try { const center = turf.centerOfMass(poly); const d = turf.distance(pt, center, { units: 'meters' }); if (d < minDist) { minDist = d; nearPt = center.geometry.coordinates; } } catch (e2) { } } }); distLayer.clearLayers(); const roundedDist = Math.round(minDist); userMarker.minDist = isFinite(minDist) ? minDist : 1001; if (nearPt && minDist <= 1000) { const statusColor = '#ef4444'; // Always red as requested L.polyline([pos, [nearPt[1], nearPt[0]]], { color: statusColor, weight: 5, dashArray: '10, 15', opacity: 0.8 }).addTo(distLayer); userMarker.bindTooltip(`
Nächstes Wohnhaus
${roundedDist} Meter
`, { permanent: true, direction: 'top', className: 'dist-tooltip-premium', offset: [0, -40] }).openTooltip(); residenceLayer.setStyle({ color: statusColor, weight: 2, fillOpacity: 0.4 }); } else { userMarker.unbindTooltip(); } } } catch (e) { console.error("Gebäude-Analyse fehlgeschlagen", e); userMarker.minDist = 1001; } } const CATEGORIES = [ { name: "Flächennutzung", isSpecial: "usage" }, { name: "Standort & Flurstück", isSpecial: "parcel" }, { name: "Abstand Wohnbebauung", isSpecial: "residential" }, { name: "Planungsrecht & Repowering", isSpecial: "planning" }, { name: "Naturschutz", items: [ { name: "Naturschutzgebiete", url: "/api/wms/linfos", layers: "Naturschutzgebiete", format: "application/geo+json" }, { name: "Landschaftsschutzgebiete", url: "/api/wms/linfos", layers: "Landschaftsschutzgebiet", format: "application/geo+json" }, { name: "FFH-Gebiete", url: "/api/wms/linfos", layers: "FFH-Gebiete", format: "application/geo+json" }, { name: "Vogelschutzgebiete", url: "/api/wms/linfos", layers: "Vogelschutzgebiete", format: "application/geo+json" }, { name: "Geschützte Biotope", url: "/api/wms/linfos", layers: "geschuetzteBiotope", format: "application/geo+json" }, { name: "Bereich zum Schutz der Natur", url: "/api/wms/linfos", layers: "GSN", format: "application/geo+json" } ] }, { name: "Wasserschutz & Hochwasser", items: [ { name: "Wasserschutzgebiete", url: "/api/wms/wsg", layers: "wsg_zone1,wsg_zone2", format: "application/geo+json" }, { name: "Überschwemmungsgebiete", url: "/api/wms/uesg", layers: "nw_uesg_t7", format: "application/geo+json" } ] }, { name: "Infrastruktur & Abstände", isSpecial: "infrastructure" }, { name: "Luftfahrt & Radar", isSpecial: "aviation" }, { name: "Netzanschluss", isSpecial: "grid_connection" } ]; /** * Expert Analysis Logic */ const queryOverpass = async (query, retries = 2) => { // Add a small initial delay to avoid hitting limits when multiple calls are made await new Promise(resolve => setTimeout(resolve, Math.random() * 500)); const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(query)}`; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); // 15s timeout const response = await fetch(url, { signal: controller.signal }); if (response.status === 429 && retries > 0) { clearTimeout(timeoutId); console.warn("Overpass Rate Limit hit, retrying in 2s..."); await new Promise(resolve => setTimeout(resolve, 2000)); return queryOverpass(query, retries - 1); } if (!response.ok) { clearTimeout(timeoutId); console.error("Overpass API Error:", response.status); return null; } const data = await response.json(); clearTimeout(timeoutId); return data; } catch (e) { if (e.name === 'AbortError') { console.error("Overpass Fetch timed out."); } else { console.error("Overpass Fetch failed:", e); } return null; } }; const queryWFS = async (proxy, type, pos, bboxSize = 0.001) => { // We use WFS 1.1.0 and LonLat order for better compatibility with NRW services const bbox = `${pos.lng - bboxSize},${pos.lat - bboxSize},${pos.lng + bboxSize},${pos.lat + bboxSize}`; const url = `/api/wfs/${proxy}?SERVICE=WFS&VERSION=1.1.0&REQUEST=GetFeature&TYPENAME=${type}&SRSNAME=EPSG:4326&BBOX=${bbox}&OUTPUTFORMAT=application/json`; try { const r = await fetch(url); if (!r.ok) return []; const d = await r.json(); return d.features || []; } catch (e) { return []; } }; const queryWFSGML = async (proxy, type, pos, bboxSize = 0.015) => { // For services that do not support JSON (like the NRW road network WFS) const bbox = `${pos.lng - bboxSize},${pos.lat - bboxSize},${pos.lng + bboxSize},${pos.lat + bboxSize}`; const url = `/api/wfs/${proxy}?SERVICE=WFS&VERSION=1.1.0&REQUEST=GetFeature&TYPENAME=${type}&SRSNAME=EPSG:4326&BBOX=${bbox}`; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); const r = await fetch(url, { signal: controller.signal }); if (!r.ok) { clearTimeout(timeoutId); return []; } const text = await r.text(); clearTimeout(timeoutId); const features = []; // Extract const regex = /]*>(.*?)<\/gml:posList>/gs; let match; while ((match = regex.exec(text)) !== null) { const coordsStr = match[1].trim().split(/\s+/); const coords = []; // EPSG:4326 in MapServer WFS 1.1.0 is generally Lat Lon for (let i = 0; i < coordsStr.length; i += 2) { const lat = parseFloat(coordsStr[i]); const lon = parseFloat(coordsStr[i + 1]); if (!isNaN(lat) && !isNaN(lon)) { coords.push([lon, lat]); // Turf uses Lon, Lat } } if (coords.length > 1) { // Add a dummy properties object to store the type name later features.push(turf.lineString(coords, { type: type })); } } return features; } catch (e) { console.error("WFS GML Error:", e); return []; } }; /** * Perform a WMS GetFeatureInfo request to check for features at a point */ const queryWMS = async (proxy, layers, pos) => { const utm = proj4(EPSG4326, EPSG25832, [pos.lng, pos.lat]); const size = 5; const bbox = `${utm[0] - size},${utm[1] - size},${utm[0] + size},${utm[1] + size}`; const infoFormat = encodeURIComponent('application/geo+json'); const url = `/api/wms/${proxy}?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetFeatureInfo&LAYERS=${layers}&QUERY_LAYERS=${layers}&BBOX=${bbox}&FEATURE_COUNT=1&HEIGHT=100&WIDTH=100&X=50&Y=50&SRS=EPSG:25832&INFO_FORMAT=${infoFormat}&STYLES=`; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout for WMS const r = await fetch(url, { signal: controller.signal }); if (!r.ok) { clearTimeout(timeoutId); throw new Error("WMS Status " + r.status); } const text = await r.text(); clearTimeout(timeoutId); // Handle HTML/XML responses (often returned when no features found or format not supported) if (text.trim().startsWith(' elements that contain actual alphanumeric data. const hasDataCells = /]*>.*?\w+.*?<\/td>/i.test(text); const isNoResult = lowerText.includes('keine information') || lowerText.includes('no features found') || lowerText.includes('empty') || lowerText.includes('keine objekte'); if (hasDataCells && !isNoResult) { return [{ properties: { info: "Feature found" } }]; } return []; } try { const d = JSON.parse(text); return d.features || d.results || []; } catch (jsonErr) { // If JSON fails, it might be plain text const lowerText = text.toLowerCase(); const isNoResult = lowerText.includes('no results') || lowerText.includes('keine objekte') || lowerText.includes('no features found') || text.trim() === ''; if (!isNoResult && text.trim().length > 0) { return [{ properties: { info: text.trim() } }]; } return []; } } catch (e) { console.error("WMS Error:", e); return []; } }; /** * Initialize a mini map in the report area */ const initReportMiniMap = (pos) => { const mapDom = document.getElementById('report-mini-map'); if (!mapDom) return; // Short timeout to ensure DOM is rendered setTimeout(() => { const miniMap = L.map('report-mini-map', { center: [pos.lat, pos.lng], zoom: 16, // Weiter rausgezoomt zoomControl: false, attributionControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false, boxZoom: false, touchZoom: false, keyboard: false }); // DOP Layer L.tileLayer.wms('/api/wms/dop', { layers: 'nw_dop_rgb', format: 'image/png', transparent: false, version: '1.3.0' }).addTo(miniMap); // ALKIS Layer (Parcels) L.tileLayer.wms('/api/wms/alkis', { layers: 'adv_alkis_flurstuecke', format: 'image/png', transparent: true, styles: 'Gelb', version: '1.3.0' }).addTo(miniMap); // Standort als einfacher Punkt (CircleMarker) L.circleMarker([pos.lat, pos.lng], { radius: 7, fillColor: "#ff4444", color: "#fff", weight: 2, opacity: 1, fillOpacity: 0.9 }).addTo(miniMap); }, 200); }; document.getElementById('check-site-btn').onclick = async () => { if (!userMarker) return; const pos = userMarker.getLatLng(); const resultsArea = document.getElementById('results-area'); const today = new Date().toLocaleDateString('de-DE'); // Header & Loading resultsArea.innerHTML = `

Bitte haben Sie einen Moment Geduld...

Abfrage der Geodaten läuft...

Analyse läuft...

`; resultsArea.classList.add('results-visible'); // UTM / Coords Helper const utm = proj4(EPSG4326, EPSG25832, [pos.lng, pos.lat]); // Category Data Fetching const fetchParcel = async () => { try { // Primär: Neuer lokaler ALKIS-Handler (GML-Parsing im Backend) const r = await fetch(`/api/local/alkis-info?lat=${pos.lat}&lng=${pos.lng}`); if (r.ok) { const data = await r.json(); if (data.found) { const p = data.properties; return `
Prüfdatum: ${today}
Koordinaten (WGS84): ${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}
Koordinaten (UTM32): ${Math.round(utm[0])}, ${Math.round(utm[1])}
Kreis / Gemeinde: ${p.kreis || '-'} / ${p.gemeinde || '-'}
Gemarkung: ${p.gemarkung || '-'}
Flur / Flurstück: ${p.flur || '-'} / ${p.flstnrzae || '-'}${p.flstnrnen ? '/' + p.flstnrnen : ''}
Fläche (amtl.): ${p.flaeche ? Math.round(parseFloat(p.flaeche)).toLocaleString('de-DE') + ' m²' : '-'}
Lage: ${p.lagebeztxt || '-'}
`; } } // Sekundär: Fallback auf ATKIS-DLM (Topographie) falls ALKIS fehlschlägt const atkis = await queryWFS('atkis', 'adv:AX_Gemarkung', pos, 0.0001); if (atkis && atkis.length > 0) { const p = atkis[0].properties; return `
Prüfdatum: ${today}
WGS84: ${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}
Gemeinde: ${p.gemeindename || '-'}
Gemarkung: ${p.gemarkungsname || '-'}
Nur ATKIS-Topographiedaten verfügbar
`; } } catch (e) { console.error("Parcel Fetch Error:", e); } return `
Prüfdatum: ${today}
WGS84: ${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}
Amtliche Flurstücksdaten (ALKIS) zur Zeit nicht abrufbar
`; }; // Parallel processing for basic checks const [parcelHtml, planRes, potRes] = await Promise.all([ fetchParcel(), fetch(`/api/local/check-planning?lat=${pos.lat}&lng=${pos.lng}`).then(r => r.json()), fetch(`/api/local/check-potential?lat=${pos.lat}&lng=${pos.lng}`).then(r => r.json()) ]); const dist = userMarker.minDist || 2500; let distStatus = "In der Regel ✔"; let distColor = "text-emerald-500"; if (dist < 400) { distStatus = "Keine moderne Windenergieanlage möglich."; distColor = "text-red-500 font-bold"; } else if (dist < 500) { distStatus = "Abstand macht moderne Anlagen in der Regel nicht möglich. Eine detaillierte Prüfung ist notwendig."; distColor = "text-red-500 font-bold"; } else if (dist <= 1000) { distStatus = "HINWEIS: Wohnbebauung im Nahbereich (< 1000m)"; distColor = "text-red-500 font-bold"; } else { distStatus = "In der Regel ✔ (> 1000m)"; distColor = "text-emerald-500"; } let reportHtml = `
enwelo

Analyse-Ergebnis

Wichtiger Hinweis:

Dies ist eine automatisierte Vorab-Prüfung. Die Ergebnisse können Fehler enthalten und ersetzen keine professionelle Flächenprüfung.

Empfehlung: Unabhängig vom Ergebnis empfehlen wir Rücksprache mit einem Mitarbeiter der ENWELO zu halten:
`; // --- NEW: Combined Infrastructure Query to avoid 429 Too Many Requests --- // Optimized Radiuses and timeout to avoid 504 Gateway Timeout // Radiuses reduced significantly! const combinedInfraQuery = `[out:json][timeout:15];( way["power"~"line|cable"]["voltage"~"110000|220000|380000"](around:2500, ${pos.lat}, ${pos.lng}); node["aeroway"~"aerodrome|navaid"](around:15000, ${pos.lat}, ${pos.lng}); way["aeroway"="aerodrome"](around:15000, ${pos.lat}, ${pos.lng}); node["power"~"substation|transformer"](around:5000, ${pos.lat}, ${pos.lng}); way["power"~"substation|transformer"](around:5000, ${pos.lat}, ${pos.lng}); );out body geom;`; console.log("[InfraCheck] Fetching combined infrastructure data..."); document.getElementById('loader-status').innerText = "Abfrage Infrastruktur (OpenStreetMap)..."; const infraData = await queryOverpass(combinedInfraQuery); const infraElements = infraData?.elements || []; document.getElementById('loader-status').innerText = "Abfrage Straßennetz (WFS NRW)..."; // WFS Road fetching (parallel) // About 0.015 degrees is roughly 1-1.5km const [bab, bstr, lstr, kstr] = await Promise.all([ queryWFSGML('nrw_roads', 'ms:Bundesautobahnen', pos, 0.015), queryWFSGML('nrw_roads', 'ms:Bundesstrassen', pos, 0.015), queryWFSGML('nrw_roads', 'ms:Landesstrassen', pos, 0.015), queryWFSGML('nrw_roads', 'ms:Kreisstrassen', pos, 0.015), ]); const wfsRoads = [...bab, ...bstr, ...lstr, ...kstr]; document.getElementById('loader-status').innerText = "Erstelle den Bericht..."; for (const cat of CATEGORIES) { reportHtml += `

${cat.name}

`; if (cat.isSpecial === "usage") { let usageStatus = "Keine Daten"; let usageColor = "text-slate-400"; let nutzart = "Unbekannt"; let bez = "Unbekannt"; try { const r = await fetch(`/api/local/alkis-usage?lat=${pos.lat}&lng=${pos.lng}`); if (r.ok) { const data = await r.json(); if (data.found) { nutzart = data.nutzart || "Unbekannt"; bez = data.bez || "Unbekannt"; const bezLower = bez.toLowerCase(); const nutzartLower = nutzart.toLowerCase(); // Heuristik für geeignete Flächen const isAcker = bezLower.includes("acker") || nutzartLower.includes("acker"); const isGruenland = bezLower.includes("grünland") || bezLower.includes("wiese") || bezLower.includes("weide") || nutzartLower.includes("grünland"); const isLandwirtschaft = bezLower.includes("landwirtschaft") || nutzartLower.includes("landwirtschaft"); if (isAcker || isGruenland || isLandwirtschaft) { usageStatus = "✔ (" + (bez !== "Unbekannt" ? bez : nutzart) + ")"; usageColor = "text-emerald-500"; } else { usageStatus = "Prüfung notwendig"; usageColor = "text-amber-500 font-bold"; } } } } catch (e) { console.error("ALKIS usage error:", e); } reportHtml += `
Status: ${usageStatus}
`; reportHtml += `
Nutzart: ${nutzart}
`; reportHtml += `
Bezeichnung: ${bez}
`; } else if (cat.isSpecial === "parcel") { reportHtml += parcelHtml; } else if (cat.isSpecial === "residential") { reportHtml += `
Abstand (Ist): ${Math.round(dist)} Meter
Max. Anlagenhöhe (H/2): ${Math.floor(dist / 2)} Meter
Status Wohnbebauung: ${distStatus}
`; } else if (cat.isSpecial === "planning") { // Wind turbines for Repowering Check let repoweringInfo = `
Repowering-Potenzial: Keine Bestandsanlage < 600 Meter
`; let repoweringPossible = false; try { const turbinesRes = await fetch(`/api/local/wind-turbines?lat=${pos.lat}&lng=${pos.lng}`).then(r => r.json()); if (turbinesRes && turbinesRes.features && turbinesRes.features.length > 0) { const ptNode = turf.point([pos.lng, pos.lat]); const candidates = turbinesRes.features.filter(f => { const d = turf.distance(ptNode, f, { units: 'meters' }); // Repowering: 15 Jahre alt bis 2031 const ibYear = parseInt(f.properties.ibjahr || 0); const isOldEnough = (ibYear > 0 && (ibYear + 15 <= 2031)); return d <= 600 && isOldEnough; }); if (candidates.length > 0) { repoweringPossible = true; candidates.sort((a, b) => turf.distance(ptNode, a) - turf.distance(ptNode, b)); const best = candidates[0].properties; const dist = turf.distance(ptNode, candidates[0], { units: 'meters' }); repoweringInfo = `
Repowering-Potenzial: Möglich (${candidates.length} Anlagen)
Nächs. Bestandsanlage: ${dist.toFixed(0)} Meter (Inbetriebnahme: ${best.ibjahr})
`; } } } catch (e) { console.error("Repowering check fail", e); } let municipalityNote = ""; if (!planRes.found && !repoweringPossible) { municipalityNote = `
Wichtiger Hinweis Planung: Positivplanung durch die Gemeinde notwendig
`; } reportHtml += `
Regionalplan: ${planRes.found ? 'Passend' : 'Prüfung notwendig: Kein Windenergiebereich'}
${repoweringInfo} ${municipalityNote} `; } else if (cat.isSpecial === "infrastructure") { // Use WFS pre-fetched data for roads let nearestRoad = null; let minDistRoad = 999999; const point = turf.point([pos.lng, pos.lat]); wfsRoads.forEach(lineFeature => { const d = turf.pointToLineDistance(point, lineFeature, { units: 'meters' }); if (d < minDistRoad) { minDistRoad = d; nearestRoad = lineFeature; } }); let roadDisplay = "Keine klassifizierte Straße im Nahbereich"; if (nearestRoad) { const typeMap = { 'ms:Bundesautobahnen': 'Autobahn', 'ms:Bundesstrassen': 'Bundesstraße', 'ms:Landesstrassen': 'Landesstraße', 'ms:Kreisstrassen': 'Kreisstraße' }; const typeStr = typeMap[nearestRoad.properties.type] || "Straße"; const roadInfo = `${typeStr} in ${minDistRoad.toFixed(1)} Meter`; if (minDistRoad <= 200) { roadDisplay = `Prüfung notwendig: ${roadInfo}`; } else { roadDisplay = `✔ (${roadInfo})`; } } else { roadDisplay = `✔ (> 1000 Meter)`; } reportHtml += `
Straßennetz: ${roadDisplay}
`; // Power Line Distance (>= 110kV) - using pre-fetched data const powerElements = infraElements.filter(el => (el.tags?.power === 'line' || el.tags?.power === 'cable') && el.tags?.voltage); let nearestLine = null; let minDistPower = 999999; powerElements.forEach(el => { if (el.geometry) { const line = turf.lineString(el.geometry.map(g => [g.lon, g.lat])); const d = turf.pointToLineDistance(point, line, { units: 'meters' }); if (d < minDistPower) { minDistPower = d; nearestLine = el; } } }); let powerStatus = "✔ (> 2 Kilometer)"; let powerColor = "text-emerald-500"; if (nearestLine) { powerStatus = `Abstand: ${minDistPower.toFixed(0)} Meter`; powerColor = minDistPower < 100 ? "text-amber-500" : "text-emerald-500"; } reportHtml += `
Freileitungen (>= 110kV): ${powerStatus}
`; // Existing Wind Turbines (Infrastruktur Check - 600 Meter) let turbineStatus = "✔ (> 600 Meter)"; let turbineColor = "text-emerald-500"; let closestInfo = ""; try { const r = await fetch(`/api/local/wind-turbines?lat=${pos.lat}&lng=${pos.lng}`); if (!r.ok) { const errText = await r.text(); console.error("[WindCheck] API Error:", r.status, errText); turbineStatus = `Fehler (${r.status})`; } else { const data = await r.json(); console.log(`[WindCheck] API returned ${data.features?.length || 0} WGS84 turbines for ${pos.lat},${pos.lng}`); if (data && data.features && data.features.length > 0) { const ptNode = turf.point([pos.lng, pos.lat]); let minDist = 999999; let nearest = null; data.features.forEach(f => { const d = turf.distance(ptNode, f, { units: 'meters' }); if (d < minDist) { minDist = d; nearest = f; } }); console.log(`[WindCheck] Nearest turbine at ${minDist.toFixed(1)}m`); if (minDist <= 600) { const jahr = nearest.properties.ibjahr || "Unbekannt"; turbineStatus = `Prüfung notwendig: Anlage in ${minDist.toFixed(0)} Meter`; turbineColor = "text-amber-500"; closestInfo = `
Inbetriebnahme: ${jahr}
`; } else if (minDist < 2000) { turbineStatus = `✔ (Nächste in ${minDist.toFixed(0)} Meter)`; } } } } catch (e) { console.error("Turbine check fail", e); turbineStatus = "Check-Fehler: " + e.message; } reportHtml += `
Windenergieanlagen (600 Meter): ${turbineStatus}
`; if (closestInfo) reportHtml += closestInfo; } else if (cat.isSpecial === "aviation") { let radarFound = false; MILITARY_RADARS.forEach(r => { const d = turf.distance(turf.point([pos.lng, pos.lat]), turf.point([r.lng, r.lat]), { units: 'kilometers' }); if (d < 15) { reportHtml += `
Militärradar ${r.name}: Prüfung notwendig (${d.toFixed(1)} Kilometer)
`; radarFound = true; } }); const aviationElements = infraElements.filter(el => el.tags?.aeroway); let nearestVor = null; let minDistVor = 15; let nearestAir = null; let minDistAir = 25; const point = turf.point([pos.lng, pos.lat]); aviationElements.forEach(el => { let elLon, elLat; if (el.type === 'node') { elLon = el.lon; elLat = el.lat; } else if (el.center) { elLon = el.center.lon; elLat = el.center.lat; } else if (el.geometry && el.geometry.length > 0) { elLon = el.geometry[0].lon; elLat = el.geometry[0].lat; } if (elLon !== undefined && elLat !== undefined) { const elPt = turf.point([elLon, elLat]); const d = turf.distance(point, elPt, { units: 'kilometers' }); if (el.tags.aeroway === 'navaid' || el.tags.navaid) { if (d < minDistVor) { minDistVor = d; nearestVor = el; } } else if (el.tags.aeroway === 'aerodrome' || el.tags.aeroway === 'airport') { if (d < minDistAir) { minDistAir = d; nearestAir = el; } } } }); const vorStatus = nearestVor ? `Prüfung notwendig: ${nearestVor.tags.name || 'VOR'} in ${minDistVor.toFixed(1)} Kilometer` : `✔ (> 15 Kilometer)`; const airStatus = nearestAir ? `Prüfung notwendig: ${nearestAir.tags.name || 'Flugplatz'} in ${minDistAir.toFixed(1)} Kilometer` : `✔ (> 25 Kilometer)`; reportHtml += `
Drehfunkfeuer (VOR): ${vorStatus}
`; reportHtml += `
Flugplatz-Nahbereich: ${airStatus}
`; } else if (cat.isSpecial === "grid_connection") { const gridElements = infraElements.filter(el => el.tags?.power === 'substation' || el.tags?.power === 'transformer'); let nearestSub = null; let minDistGrid = 15; // km const point = turf.point([pos.lng, pos.lat]); gridElements.forEach(el => { let elLon, elLat; if (el.type === 'node') { elLon = el.lon; elLat = el.lat; } else if (el.center) { elLon = el.center.lon; elLat = el.center.lat; } else if (el.geometry && el.geometry.length > 0) { elLon = el.geometry[0].lon; elLat = el.geometry[0].lat; } if (elLon !== undefined && elLat !== undefined) { const elPt = turf.point([elLon, elLat]); const d = turf.distance(point, elPt, { units: 'kilometers' }); if (d < minDistGrid) { minDistGrid = d; nearestSub = el; } } }); let gridStatus = "Kein Anschluss (> 15 Kilometer)"; let gridColor = "text-amber-500"; if (nearestSub) { const type = nearestSub.tags.power === 'substation' ? 'Umspannwerk' : 'Transformator'; const name = nearestSub.tags.name ? ` (${nearestSub.tags.name})` : ""; gridStatus = `${type}${name} in ${minDistGrid.toFixed(1)} Kilometer`; gridColor = "text-emerald-500"; } reportHtml += `
Netzanschluss (15 Kilometer): ${gridStatus}
`; reportHtml += `
Hinweis Netzanschluss: Die Daten der Netzanschlüsse sind nicht vollständig. Eine verbindliche Netzanschlussanfrage ist zwingend erforderlich.
`; } else if (cat.name.includes("Naturschutz")) { // Gezielte Abfrage für alle relevanten Schutzgebiete const nsg = await queryWMS('linfos', 'Naturschutzgebiete', pos); const ffh = await queryWMS('linfos', 'FFH-Gebiete', pos); const vsg = await queryWMS('linfos', 'Vogelschutzgebiete', pos); const biotop = await queryWMS('linfos', 'geschuetzteBiotope', pos); const lsg = await queryWMS('linfos', 'Landschaftsschutzgebiet', pos); const gsn_res = await queryWMS('linfos', 'GSN', pos); // Gebiete für den Schutz der Natur (queryfähig) const nature_found = nsg.length > 0 || ffh.length > 0 || vsg.length > 0 || biotop.length > 0 || lsg.length > 0 || gsn_res.length > 0; reportHtml += `
Naturschutzgebiet: ${nsg.length > 0 ? 'Tabu / Standort im Naturschutzgebiet' : '✔'}
`; reportHtml += `
FFH-Gebiet: ${ffh.length > 0 ? 'Tabu / Standort im FFH-Gebiet' : '✔'}
`; reportHtml += `
Vogelschutzgebiet: ${vsg.length > 0 ? 'Tabu / Standort im Vogelschutzgebiet' : '✔'}
`; reportHtml += `
Gesetzl. gesch. Biotope: ${biotop.length > 0 ? 'Prüfung notwendig / Standort im Biotop' : '✔'}
`; reportHtml += `
Landschaftsschutzgebiet: ${lsg.length > 0 ? 'Prüfung notwendig / Standort im Landschaftsschutzgebiet' : '✔'}
`; reportHtml += `
Bereich zum Schutz der Natur: ${gsn_res.length > 0 ? 'Prüfung notwendig' : '✔'}
`; // Fallback für weitere Layer aus der Konfiguration if (cat.items) { const checked = ['Naturschutzgebiete', 'FFH-Gebiete', 'Vogelschutzgebiete', 'geschuetzteBiotope', 'Landschaftsschutzgebiet', 'GSN', 'BSN']; for (const item of cat.items.filter(i => !checked.includes(i.layers))) { const r = await queryWMS('linfos', item.layers, pos); reportHtml += `
${item.name}: ${r.length > 0 ? 'Prüfung notwendig' : '✔'}
`; } } reportHtml += `
Artenschutz: Diese automatisierte Abfrage ersetzt keine detaillierte Artenschutzrechtliche Prüfung (ffh-VP / ASP).
`; } else if (cat.name.includes("Wasserschutz")) { // Flood zones & Water protection const uesg = await queryWMS('uesg', '6', pos); // Festgesetzte ÜSG (Layer 6) const zones = await queryWMS('wsg', '4', pos); // Trinkwasser festgesetzt (Layer 4) let wsgLabel = "✔"; if (zones.length > 0) { const p = zones[0].properties || {}; const zone = p.schutzzone || p.zone || p.ZONENNAME || p.ZONE || p.name || ""; wsgLabel = `Prüfung notwendig: ${zone}`.trim(); if (wsgLabel === "Prüfung notwendig:") wsgLabel = "Prüfung notwendig / Standort im Wasserschutzgebiet"; } reportHtml += `
Wasserschutzgebiet: ${wsgLabel}
`; reportHtml += `
Überschwemmungsgebiet: ${uesg.length > 0 ? 'Prüfung notwendig' : '✔'}
`; if (cat.items) { // Check if any other items in this category are NOT covered by the above for (const item of cat.items.filter(i => !i.layers.includes('wsg_zone1') && !i.name.includes('Überschwemmungsgebiete'))) { const r = await queryWMS(item.url.split('/').pop(), item.layers, pos); reportHtml += `
${item.name}: ${r.length > 0 ? 'Prüfung notwendig' : '✔'}
`; } } } else { const catResults = await Promise.all(cat.items.map(async item => { const proxy = item.url.split('/').pop(); const r = await queryWFS(proxy, item.layers, pos, 0.001); return `
${item.name}: ${r.length > 0 ? 'Konflikt' : '✔'}
`; })); reportHtml += catResults.join(''); } reportHtml += `
`; } // --- NEUE KATEGORIE: LANUV POTENZIAL (Ganz am Ende) --- reportHtml += `

Informations-Zusatz: LANUV-Potenzial

Hintergrund: Potentialstudie des Landesamtes (LANUV). Diese Daten geben einen Hinweis über die generelle Eignung einer Fläche.
LANUV-Potenzial: ${potRes.found ? 'JA (Fläche vorhanden) ✔' : 'NEIN (Keine Fläche)'}
${potRes.found && potRes.properties ? `
Details: ${JSON.stringify(potRes.properties)}
` : ''}
`; reportHtml += `
* Alle Angaben ohne Gewähr. Erstellt am ${new Date().toLocaleString('de-DE')}.
`; resultsArea.innerHTML = reportHtml; // Initialize mini map after content is injected initReportMiniMap(pos); }; // --- Backend Health Check --- async function checkBackendStatus() { const statusEl = document.getElementById('backend-status'); const statusText = statusEl?.querySelector('.status-text'); if (!statusEl) return; try { const res = await fetch('/api/local/buildings?bbox=0,0,0,0'); // Simple ping if (res.ok) { statusEl.classList.remove('offline'); statusEl.classList.add('online'); if (statusText) statusText.textContent = "Geodaten-Server online"; } else { throw new Error(); } } catch (e) { statusEl.classList.remove('online'); statusEl.classList.add('offline'); if (statusText) statusText.textContent = "Geodaten-Server offline"; } } // Initial check and regular interval checkBackendStatus(); setInterval(checkBackendStatus, 10000); // --- Impressum Modal Logic --- const impressumModal = document.getElementById('impressum-modal'); const openImpressumBtn = document.getElementById('open-impressum'); const closeImpressumBtn = document.querySelector('.close-modal-btn'); if (openImpressumBtn && impressumModal) { openImpressumBtn.addEventListener('click', () => { impressumModal.classList.remove('hidden'); }); } if (closeImpressumBtn && impressumModal) { closeImpressumBtn.addEventListener('click', () => { impressumModal.classList.add('hidden'); }); // Close on click outside impressumModal.addEventListener('click', (e) => { if (e.target === impressumModal) { impressumModal.classList.add('hidden'); } }); }