wind_tool_standortpruefung/main.js

1142 lines
55 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: `<div style="display:flex; flex-direction:column; align-items:center;">
<svg width="60" height="85" viewBox="0 0 60 85">
<line x1="30" y1="85" x2="30" y2="40" stroke="#333" stroke-width="3"/>
<g class="turbine-rotor">
<circle cx="30" cy="40" r="2" fill="#333"/>
<path d="M30 40 L30 15 L28 15 L28 40 Z" fill="#666" transform="rotate(0 30 40)"/>
<path d="M30 40 L30 15 L28 15 L28 40 Z" fill="#666" transform="rotate(120 30 40)"/>
<path d="M30 40 L30 15 L28 15 L28 40 Z" fill="#666" transform="rotate(240 30 40)"/>
</g>
</svg>
</div>`,
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 &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="https://openinframap.org">OpenInfraMap</a>'
})
};
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(`<b>Regionalplan</b><br>${f.properties.GENNAME || 'Windenergiebereich'}`)
});
const potentialLayer = L.geoJSON(null, {
style: { color: '#10b981', weight: 2, fillOpacity: 0.2 },
onEachFeature: (f, l) => l.bindPopup(`<b>LANUV Potenzial</b>`)
});
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 = '<span class="text-emerald-600 font-bold">✓ Standort gewählt</span>';
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(`<div style="text-align:center"><b>Nächstes Wohnhaus</b><br/><span style="font-size:1.1rem">${roundedDist}m</span></div>`);
} else if (userMarker.isTooltipOpen && userMarker.getTooltip().getContent().includes('Wohnhaus')) {
// Clear if too far
userMarker.setTooltipContent(`<div style="text-align:center">Kein Wohnhaus im Nahbereich (1000m)</div>`);
}
}
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(`<div style="text-align:center"><b>Nächstes Wohnhaus</b><br/><span style="font-size:1.1rem">${roundedDist} Meter</span></div>`, {
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 <gml:posList>
const regex = /<gml:posList[^>]*>(.*?)<\/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('<?xml') || text.trim().toLowerCase().startsWith('<html') || text.trim().toLowerCase().startsWith('<!doctype html')) {
const lowerText = text.toLowerCase();
// Robust check: Does the HTML contain actual data or just empty tables/placeholders?
// Many NRW servers return a table with headers even if empty.
// We look for <td> elements that contain actual alphanumeric data.
const hasDataCells = /<td[^>]*>.*?\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 = `
<div class="glass-panel">
<h4 style="color:#ef4444; margin-bottom:0.5rem; font-size:0.8rem;">Bitte haben Sie einen Moment Geduld...</h4>
<div class="loader-container">
<div class="loader"></div>
<div id="loader-status" style="font-size:0.75rem; color:#64748b; margin-top:0.5rem;">Abfrage der Geodaten läuft...</div>
</div>
<h3>Analyse läuft...</h3>
</div>`;
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 `
<div class="result-item"><strong>Prüfdatum:</strong> <span>${today}</span></div>
<div class="result-item"><strong>Koordinaten (WGS84):</strong> <span>${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}</span></div>
<div class="result-item"><strong>Koordinaten (UTM32):</strong> <span>${Math.round(utm[0])}, ${Math.round(utm[1])}</span></div>
<div style="background: rgba(0,0,0,0.03); border: 1px solid #e2e8f0; border-radius: 6px; padding: 0.75rem; margin: 1rem 0; line-height: 1.25;">
<div class="result-item" style="font-size: 0.85rem; margin-bottom: 0.25rem;"><strong>Kreis / Gemeinde:</strong> <span>${p.kreis || '-'} / ${p.gemeinde || '-'}</span></div>
<div class="result-item" style="font-size: 0.85rem; margin-bottom: 0.25rem;"><strong>Gemarkung:</strong> <span>${p.gemarkung || '-'}</span></div>
<div class="result-item" style="font-size: 0.85rem; margin-bottom: 0.25rem;"><strong>Flur / Flurstück:</strong> <span>${p.flur || '-'} / ${p.flstnrzae || '-'}${p.flstnrnen ? '/' + p.flstnrnen : ''}</span></div>
<div class="result-item" style="font-size: 0.85rem; margin-bottom: 0.25rem;"><strong>Fläche (amtl.):</strong> <span>${p.flaeche ? Math.round(parseFloat(p.flaeche)).toLocaleString('de-DE') + ' m²' : '-'}</span></div>
<div class="result-item" style="font-size: 0.85rem; margin-bottom: 0;"><strong>Lage:</strong> <span>${p.lagebeztxt || '-'}</span></div>
</div>
<div id="report-mini-map" style="height:250px; margin-top:1rem; border-radius:8px; border:2px solid #e2e8f0; position: relative; z-index: 10;"></div>
`;
}
}
// 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 `
<div class="result-item"><strong>Prüfdatum:</strong> <span>${today}</span></div>
<div class="result-item"><strong>WGS84:</strong> <span>${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}</span></div>
<div style="background: rgba(0,0,0,0.03); border: 1px solid #e2e8f0; border-radius: 6px; padding: 0.75rem; margin: 1rem 0; line-height: 1.25;">
<div class="result-item" style="font-size: 0.85rem; margin-bottom: 0.25rem;"><strong>Gemeinde:</strong> <span>${p.gemeindename || '-'}</span></div>
<div class="result-item" style="font-size: 0.85rem; margin-bottom: 0.25rem;"><strong>Gemarkung:</strong> <span>${p.gemarkungsname || '-'}</span></div>
<div class="result-item" style="font-size: 0.85rem; margin-bottom: 0;"><span class="text-amber-500">Nur ATKIS-Topographiedaten verfügbar</span></div>
</div>
<div id="report-mini-map" style="height:250px; margin-top:1rem; border-radius:8px; border:2px solid #e2e8f0; position: relative; z-index: 10;"></div>
`;
}
} catch (e) {
console.error("Parcel Fetch Error:", e);
}
return `
<div class="result-item"><strong>Prüfdatum:</strong> <span>${today}</span></div>
<div class="result-item"><strong>WGS84:</strong> <span>${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}</span></div>
<div class="result-item"><span class="text-amber-500">Amtliche Flurstücksdaten (ALKIS) zur Zeit nicht abrufbar</span></div>
<div id="report-mini-map" style="height:250px; margin-top:1rem; border-radius:8px; border:2px solid #e2e8f0; position: relative; z-index: 10;"></div>`;
};
// 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 = `
<div class="result-card glass-panel">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:1.5rem; border-bottom: 2px solid var(--primary-color); padding-bottom: 1rem;">
<div style="display:flex; flex-direction:column; gap:0.5rem;">
<img src="/Logos/20201202-ENWELO-Logo-4c_ohne_Claim.png" style="height:40px; width:fit-content;" alt="enwelo">
<h2 style="margin:0; font-size:1.4rem; color:var(--primary-dark);">Analyse-Ergebnis</h2>
</div>
<div style="display:flex; gap:0.5rem; align-items:center;">
<button class="export-pdf-btn" onclick="exportToPDF()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
PDF
</button>
<button class="close-btn" onclick="document.getElementById('results-area').classList.remove('results-visible')">×</button>
</div>
</div>
<div class="report-note">
<span class="report-note-title">Wichtiger Hinweis:</span>
<p style="margin: 0 0 0.5rem 0;">Dies ist eine automatisierte Vorab-Prüfung. Die Ergebnisse können Fehler enthalten und ersetzen keine professionelle Flächenprüfung.</p>
<div style="font-size: 0.8rem;">
<strong>Empfehlung:</strong> Unabhängig vom Ergebnis empfehlen wir Rücksprache mit einem Mitarbeiter der ENWELO zu halten:
<div style="margin-top: 0.25rem; display: flex; flex-wrap: wrap; gap: 1rem;">
<span>📧 <a href="mailto:windplanung@enwelo.de" style="color: inherit;">windplanung@enwelo.de</a></span>
<span>📞 <a href="tel:02551709090" style="color: inherit;">02551 70 90 90</a></span>
</div>
</div>
</div>
`;
// --- 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 += `<section class="category-block">
<h3 class="category-title">${cat.name}</h3>`;
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 += `<div class="result-item"><strong>Status:</strong> <span class="${usageColor}">${usageStatus}</span></div>`;
reportHtml += `<div class="result-item"><strong>Nutzart:</strong> <span>${nutzart}</span></div>`;
reportHtml += `<div class="result-item"><strong>Bezeichnung:</strong> <span>${bez}</span></div>`;
} else if (cat.isSpecial === "parcel") {
reportHtml += parcelHtml;
} else if (cat.isSpecial === "residential") {
reportHtml += `
<div class="result-item"><strong>Abstand (Ist):</strong> <span>${Math.round(dist)} Meter</span></div>
<div class="result-item"><strong>Max. Anlagenhöhe (H/2):</strong> <span style="font-weight:700;">${Math.floor(dist / 2)} Meter</span></div>
<div class="report-note">
<span class="report-note-title">Status Wohnbebauung:</span>
${distStatus}
</div>
`;
} else if (cat.isSpecial === "planning") {
// Wind turbines for Repowering Check
let repoweringInfo = `<div class="result-item"><strong>Repowering-Potenzial:</strong> <span class="text-slate-400">Keine Bestandsanlage < 600 Meter</span></div>`;
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 = `<div class="result-item"><strong>Repowering-Potenzial:</strong> <span class="text-emerald-500 font-bold">Möglich (${candidates.length} Anlagen)</span></div>
<div class="result-item"><strong>Nächs. Bestandsanlage:</strong> <span>${dist.toFixed(0)} Meter (Inbetriebnahme: ${best.ibjahr})</span></div>`;
}
}
} catch (e) { console.error("Repowering check fail", e); }
let municipalityNote = "";
if (!planRes.found && !repoweringPossible) {
municipalityNote = `<div class="report-note"><span class="report-note-title">Wichtiger Hinweis Planung:</span> Positivplanung durch die Gemeinde notwendig</div>`;
}
reportHtml += `
<div class="result-item"><strong>Regionalplan:</strong> <span class="${planRes.found ? 'text-emerald-500' : 'text-amber-500'}">${planRes.found ? 'Passend' : 'Prüfung notwendig: Kein Windenergiebereich'}</span></div>
${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 = `<span class="text-amber-500 font-bold">Prüfung notwendig: ${roadInfo}</span>`;
} else {
roadDisplay = `<span class="text-emerald-500">✔ (${roadInfo})</span>`;
}
} else {
roadDisplay = `<span class="text-emerald-500">✔ (> 1000 Meter)</span>`;
}
reportHtml += `<div class="result-item"><strong>Straßennetz:</strong> ${roadDisplay}</div>`;
// 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 += `<div class="result-item"><strong>Freileitungen (>= 110kV):</strong> <span class="${powerColor}">${powerStatus}</span></div>`;
// 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 = `<div class="result-item" style="font-size:0.85rem; opacity:0.8;">Inbetriebnahme: ${jahr}</div>`;
} 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 += `<div class="result-item"><strong>Windenergieanlagen (600 Meter):</strong> <span class="${turbineColor}">${turbineStatus}</span></div>`;
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 += `<div class="result-item"><strong>Militärradar ${r.name}:</strong> <span class="text-amber-500">Prüfung notwendig (${d.toFixed(1)} Kilometer)</span></div>`;
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 += `<div class="result-item"><strong>Drehfunkfeuer (VOR):</strong> <span class="${nearestVor ? 'text-amber-500' : 'text-emerald-500'}">${vorStatus}</span></div>`;
reportHtml += `<div class="result-item"><strong>Flugplatz-Nahbereich:</strong> <span class="${nearestAir ? 'text-amber-500' : 'text-emerald-500'}">${airStatus}</span></div>`;
} 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 += `<div class="result-item"><strong>Netzanschluss (15 Kilometer):</strong> <span class="${gridColor}">${gridStatus}</span></div>`;
reportHtml += `<div class="report-note">
<strong>Hinweis Netzanschluss:</strong> Die Daten der Netzanschlüsse sind nicht vollständig. Eine verbindliche Netzanschlussanfrage ist zwingend erforderlich.
</div>`;
} 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 += `<div class="result-item"><strong>Naturschutzgebiet:</strong> <span class="${nsg.length > 0 ? 'text-red-500' : 'text-emerald-500'}">${nsg.length > 0 ? 'Tabu / Standort im Naturschutzgebiet' : '✔'}</span></div>`;
reportHtml += `<div class="result-item"><strong>FFH-Gebiet:</strong> <span class="${ffh.length > 0 ? 'text-red-500' : 'text-emerald-500'}">${ffh.length > 0 ? 'Tabu / Standort im FFH-Gebiet' : '✔'}</span></div>`;
reportHtml += `<div class="result-item"><strong>Vogelschutzgebiet:</strong> <span class="${vsg.length > 0 ? 'text-red-500' : 'text-emerald-500'}">${vsg.length > 0 ? 'Tabu / Standort im Vogelschutzgebiet' : '✔'}</span></div>`;
reportHtml += `<div class="result-item"><strong>Gesetzl. gesch. Biotope:</strong> <span class="${biotop.length > 0 ? 'text-amber-500' : 'text-emerald-500'}">${biotop.length > 0 ? 'Prüfung notwendig / Standort im Biotop' : '✔'}</span></div>`;
reportHtml += `<div class="result-item"><strong>Landschaftsschutzgebiet:</strong> <span class="${lsg.length > 0 ? 'text-amber-500' : 'text-emerald-500'}">${lsg.length > 0 ? 'Prüfung notwendig / Standort im Landschaftsschutzgebiet' : '✔'}</span></div>`;
reportHtml += `<div class="result-item"><strong>Bereich zum Schutz der Natur:</strong> <span class="${gsn_res.length > 0 ? 'text-amber-500' : 'text-emerald-500'}">${gsn_res.length > 0 ? 'Prüfung notwendig' : '✔'}</span></div>`;
// 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 += `<div class="result-item"><strong>${item.name}:</strong> <span class="${r.length > 0 ? 'text-amber-500' : 'text-emerald-500'}">${r.length > 0 ? 'Prüfung notwendig' : '✔'}</span></div>`;
}
}
reportHtml += `<div class="report-note">
<strong>Artenschutz:</strong> Diese automatisierte Abfrage ersetzt keine detaillierte <strong>Artenschutzrechtliche Prüfung (ffh-VP / ASP)</strong>.
</div>`;
} 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 += `<div class="result-item"><strong>Wasserschutzgebiet:</strong> <span class="${zones.length > 0 ? 'text-amber-500' : 'text-emerald-500'}">${wsgLabel}</span></div>`;
reportHtml += `<div class="result-item"><strong>Überschwemmungsgebiet:</strong> <span class="${uesg.length > 0 ? 'text-amber-500' : 'text-emerald-500'}">${uesg.length > 0 ? 'Prüfung notwendig' : '✔'}</span></div>`;
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 += `<div class="result-item"><strong>${item.name}:</strong> <span class="${r.length > 0 ? 'text-amber-500' : 'text-emerald-500'}">${r.length > 0 ? 'Prüfung notwendig' : '✔'}</span></div>`;
}
}
} 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 `<div class="result-item"><strong>${item.name}:</strong> <span class="${r.length > 0 ? 'text-red-500' : 'text-emerald-500'}">${r.length > 0 ? 'Konflikt' : '✔'}</span></div>`;
}));
reportHtml += catResults.join('');
}
reportHtml += `</section>`;
}
// --- NEUE KATEGORIE: LANUV POTENZIAL (Ganz am Ende) ---
reportHtml += `
<section class="category-block" style="border-top: 1px solid var(--primary-pale); margin-top: 2rem; padding-top: 1.5rem;">
<h3 class="category-title" style="color: var(--primary-color);">Informations-Zusatz: LANUV-Potenzial</h3>
<div class="report-note">
<strong>Hintergrund:</strong> Potentialstudie des Landesamtes (LANUV). Diese Daten geben einen Hinweis über die generelle Eignung einer Fläche.
</div>
<div class="result-item"><strong>LANUV-Potenzial:</strong> <span class="${potRes.found ? 'text-emerald-500 font-bold' : 'text-slate-400'}">${potRes.found ? 'JA (Fläche vorhanden) ✔' : 'NEIN (Keine Fläche)'}</span></div>
${potRes.found && potRes.properties ? `<div class="result-item" style="font-size:0.8rem; margin-top:0.3rem; opacity:0.8;">Details: ${JSON.stringify(potRes.properties)}</div>` : ''}
</section>
`;
reportHtml += `
<div style="margin-top:2rem; padding:1rem; border-top:1px solid var(--primary-pale); font-size:0.75rem; color:var(--accent-color); font-style:italic;">
* Alle Angaben ohne Gewähr. Erstellt am ${new Date().toLocaleString('de-DE')}.
</div>
</div>`;
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');
}
});
}