1142 lines
55 KiB
JavaScript
1142 lines
55 KiB
JavaScript
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 © <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');
|
||
}
|
||
});
|
||
}
|