/** * WindPlaner - Core Logic */ document.addEventListener('DOMContentLoaded', async () => { const state = { map: null, config: null, turbines: [], activeVariant: 'A', bakedData: {}, // Cache for standalone persistence ownerMapping: { firstName: 'vorname', lastName: 'nachname' }, // Default for ALKIS and modern Shapefiles ownerStatuses: {}, // { "name vorname": { status: "...", notiz: "..." } } showAuxiliary: true }; const STATUS_MAP = { 'Ablehnung': { color: '#ff0000', desc: 'Der Eigentümer lehnt das Vorhaben strikt ab.' }, 'Erwartet Negativ': { color: '#ffa500', desc: 'Erste Signale oder Tendenzen deuten auf eine Ablehnung hin.' }, 'Unentschlossen': { color: '#ffff00', desc: 'Rückmeldung ist noch offen oder der Eigentümer zögert.' }, 'Unbekannt': { color: '#cccccc', desc: 'Bisher kein Kontakt erfolgt; Status ist völlig offen.' }, 'Erwartet Positiv': { color: '#90ee90', desc: 'Eine grundsätzliche Bereitschaft zur Zustimmung wird erwartet.' }, 'Zusage (mündlich)': { color: '#008000', desc: 'Klare mündliche Zustimmung liegt vor, der schriftliche Vertrag ist noch offen.' }, 'Vertraglich gesichert': { color: '#006400', desc: 'Der Vertrag liegt unterschrieben vor.' }, 'In der Projektgesellschaft': { color: '#ff00ff', desc: 'Grundstückseigentümer ist in der Projektgesellschaft.' }, 'Fremdplanung': { color: '#c71585', desc: 'Anderes Vorhaben (WEA), keine Kooperation.' }, 'Kooperationspartner': { color: '#ffffff', desc: 'Anderes Vorhaben mit dem kooperiert wird.' } }; // Mapping for old database values const LEGACY_STATUS_MAP = { 'declined': 'Ablehnung', 'negative': 'Ablehnung', 'external': 'Fremdplanung', 'fremdplanung': 'Fremdplanung', 'positive': 'Erwartet Positiv', 'undecided': 'Unentschlossen', 'gbr': 'In der Projektgesellschaft', 'gesichert': 'Vertraglich gesichert' }; // Removed fetch for config to prevent CORS errors on file:// protocol console.log("Konfiguration geladen."); // Initialize Map state.map = L.map('map', { center: [51.5, 7.5], // Center NRW roughly zoom: 13, zoomControl: false }); // Add Zoom Control to the right L.control.zoom({ position: 'topright' }).addTo(state.map); const updateZoomClass = () => { const zoom = state.map.getZoom(); const container = state.map.getContainer(); container.className = container.className.replace(/\bzoom-\d+\b/g, ''); container.classList.add(`zoom-${zoom}`); }; state.map.on('zoomend', updateZoomClass); updateZoomClass(); // Standard Tile Layer (requires Internet, but we provide it as default) // In a real offline scenario, this would be a local MBTiles layer or similar. L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19 }).addTo(state.map); // Layer Groups for Variants const variantLayers = { 'A': L.layerGroup().addTo(state.map), 'B': L.layerGroup(), 'C': L.layerGroup() }; // UI Elements const variantTabs = document.querySelectorAll('.variant-tab'); const btnPlaceTurbine = document.getElementById('btnPlaceTurbine'); const btnMeasureDist = document.getElementById('btnMeasureDist'); const btnMeasureArea = document.getElementById('btnMeasureArea'); const editPanel = document.getElementById('editPanel'); const btnCloseEdit = document.getElementById('btnCloseEdit'); const btnSaveEdit = document.getElementById('btnSaveEdit'); const btnDeleteWEA = document.getElementById('btnDeleteWEA'); const editNr = document.getElementById('edit-wea-nr'); const editManufacturer = document.getElementById('edit-wea-manufacturer'); const editType = document.getElementById('edit-wea-type'); const editRd = document.getElementById('edit-wea-rd'); const editNh = document.getElementById('edit-wea-nh'); const editFr = document.getElementById('edit-wea-fr'); const editKsfAngle = document.getElementById('edit-wea-ksf-angle'); const editKsfMirrored = document.getElementById('edit-wea-ksf-mirrored'); // Legend Elements const floatingLegend = document.getElementById('floatingLegend'); const legendContent = document.getElementById('legendContent'); const btnToggleLegend = document.getElementById('btnToggleLegend'); const legendHeader = document.getElementById('legendHeader'); const inputRotor = document.getElementById('rotorDiameter'); const inputHub = document.getElementById('hubHeight'); const inputFoundation = document.getElementById('foundationRadius'); const inputManufacturer = document.getElementById('turbineManufacturer'); const inputType = document.getElementById('turbineType'); let placementMode = false; let measureMode = null; // 'dist' or 'area' let activeTurbine = null; // Proj4 Definition for UTM32 (EPSG:25832) const utm32 = "+proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs"; const wgs84 = "+proj=longlat +datum=WGS84 +no_defs"; // Calculation Functions function calculateGeometries(latlng, rotorDiameter, hubHeight, foundationRadius, ksfAngle = 0, ksfMirrored = false, hersteller = 'Enercon') { // Ensure valid numbers for Turf const rd = parseFloat(rotorDiameter) || 160; const hh = parseFloat(hubHeight) || 165; const fr = parseFloat(foundationRadius) || 15; const totalHeight = hh + (rd / 2); const point = turf.point([latlng.lng, latlng.lat]); const steps = 128; // Increased resolution for smoother circles/ellipses // Convert to UTM32 FOR calculation and display const utmCoords = proj4(wgs84, utm32, [latlng.lng, latlng.lat]); const centerE = utmCoords[0]; const centerN = utmCoords[1]; // 1. Swept Area (Rotorüberstreichfläche) const sweptArea = turf.circle(point, (rd / 2) / 1000, { units: 'kilometers', steps: steps }); // 2. Technical Distance 1 (Ellipse 2.5 x 4.0 RD) const techDist = turf.ellipse(point, (rd * 4.0) / 1000, (rd * 2.5) / 1000, { units: 'kilometers', angle: 135, steps: steps }); // 2b. Technical Distance 2 (2.0 RD Circle) const techDistSmall = turf.circle(point, (rd * 2.0) / 1000, { units: 'kilometers', steps: steps }); // 3. Auflastenradius (0.3 x Gesamthöhe) const loadRadius = turf.circle(point, (totalHeight * 0.3) / 1000, { units: 'kilometers', steps: steps }); // 3b. Fundament (configurable radius) const foundation = turf.circle(point, (fr) / 1000, { units: 'kilometers', steps: steps }); // Helper for KSF Geometries const spg = ksfMirrored ? -1 : 1; // Turf uses CCW for positive angles. Most tools use CW. // We'll use negative angle for CCW to achieve CW rotation if expected. const angle = -ksfAngle; const transform = (relCoords) => { const rad = (-ksfAngle * Math.PI) / 180; // Negative for CW rotation if turf math is used, or just math // Manual Cartesian Rotation (Clockwise) // x' = x * cos(a) + y * sin(a) // y' = -x * sin(a) + y * cos(a) const a = (ksfAngle * Math.PI) / 180; const cosA = Math.cos(a); const sinA = Math.sin(a); const finalCoords = relCoords.map(c => { const x = c[0] * spg; const y = c[1]; // Rotation CW const xRot = x * cosA + y * sinA; const yRot = -x * sinA + y * cosA; const utmPoint = [centerE + xRot, centerN + yRot]; return proj4(utm32, wgs84, utmPoint); }); return turf.polygon([finalCoords]); }; let blfCoords, ksfCoords, mfParts; if (hersteller === 'Nordex') { // Nordex Geometries (Based on technical drawing: Tower is not centered in KSF width) // Foundation: R=5.5 // KSF: 59.65m (L) x 36.50m (B). // Width Offset: Tower center is approx. 12.1m from one edge and 24.4m from the other (Total 36.5m) ksfCoords = [[-12.1, -5.5], [24.4, -5.5], [24.4, -65.15], [-12.1, -65.15], [-12.1, -5.5]]; // AMF: 230.00m (L) x 15.00m (B). Centered on tower axis. const amf = [[-7.5, -65.15], [7.5, -65.15], [7.5, -295.15], [-7.5, -295.15], [-7.5, -65.15]]; // NVM (Nabenvor-Montagefläche): 26,00m (L) x 10,50m (B). Attached to the wider side of KSF. const nvm = [[24.4, -5.5], [34.9, -5.5], [34.9, -31.5], [24.4, -31.5], [24.4, -5.5]]; mfParts = [amf, nvm]; // BLF (Blattlagerfläche): 90,00m (L) x 15,00m (B). Next to AMF on the wider side. blfCoords = [[7.5, -65.15], [22.5, -65.15], [22.5, -155.15], [7.5, -155.15], [7.5, -65.15]]; } else { // Enercon / Vestas / GE (Standard) blfCoords = [[-41, 9], [-61, 9], [-61, -81], [-41, -81], [-41, 9]]; ksfCoords = [[-8, 0], [-36, 0], [-36, -50], [-8, -50], [-8, 0]]; mfParts = [ [[-36, 0], [-36, 18], [-8, 18], [-8, 0], [-36, 0]], [[12, -62], [-22, -62], [-22, -72], [12, -72], [12, -62]], [[12, 0], [-8, 0], [-8, -50], [-36, -50], [-36, -72], [-22, -72], [-22, -62], [12, -62], [12, 0]], [[-41, 18], [-47, 18], [-47, 9], [-41, 9], [-41, 18]], [[-41, -81], [-47, -81], [-47, -96], [-41, -96], [-41, -81]], [[-36, 18], [-41, 18], [-41, -72], [-36, -72], [-36, 18]] ]; } const blf = transform(blfCoords); const ksf = transform(ksfCoords); const mf = turf.featureCollection(mfParts.map(part => transform(part))); return { sweptArea, techDist, techDistSmall, loadRadius, foundation, blf, ksf, mf, totalHeight, utmCoords }; } function updateLabel(turbine, geoms) { const labelText = `
WEA ${turbine.nr}
E: ${geoms.utmCoords[0].toFixed(0)} | N: ${geoms.utmCoords[1].toFixed(0)}
NH: ${turbine.hh}m | RD: ${turbine.rd}m
GH: ${geoms.totalHeight.toFixed(1)}m
`; turbine.layers.marker.bindTooltip(labelText, { permanent: true, direction: 'right', offset: [20, -10], className: 'wea-label' }).openTooltip(); } function updateEditPanelPosition() { if (!activeTurbine || editPanel.style.display === 'none') return; const centerPx = state.map.latLngToContainerPoint(activeTurbine.latlng); const sidebarWidth = 260; const panelWidth = 210; // Position it clearly to the LEFT of the turbine center // Right edge of panel should be 120px to the left of the center let left = (centerPx.x + sidebarWidth) - panelWidth - 120; let top = centerPx.y - 120; // Screen boundaries if (left < sidebarWidth + 10) left = sidebarWidth + 10; if (top < 10) top = 10; if (left + panelWidth > window.innerWidth - 10) left = window.innerWidth - panelWidth - 10; editPanel.style.left = `${left}px`; editPanel.style.top = `${top}px`; } function openEditPanel(turbine) { if (activeTurbine && activeTurbine !== turbine) { variantLayers[activeTurbine.variant].removeLayer(activeTurbine.layers.rotationHandle); } activeTurbine = turbine; editNr.value = turbine.nr; editManufacturer.value = turbine.hersteller || 'Enercon'; editType.value = turbine.type; editRd.value = turbine.rd; editNh.value = turbine.hh; editFr.value = turbine.fr || 15; editKsfAngle.value = turbine.ksfAngle || 0; editKsfMirrored.checked = !!turbine.ksfMirrored; // Show rotation handle variantLayers[turbine.variant].addLayer(turbine.layers.rotationHandle); editPanel.style.display = 'block'; updateEditPanelPosition(); document.getElementById('statusInfo').innerText = `Bearbeite WEA ${turbine.nr}`; } function closeEditPanel() { if (activeTurbine) { variantLayers[activeTurbine.variant].removeLayer(activeTurbine.layers.rotationHandle); } activeTurbine = null; editPanel.style.display = 'none'; document.getElementById('statusInfo').innerText = "Bereit."; } // Reposition floating panel on map movement state.map.on('move zoom', updateEditPanelPosition); // Legend Logic function updateLegend() { if (!legendContent) return; let html = '
Anlagen-Geometrien
'; if (state.turbines.length > 0) { html += `
Rotorfläche
Techn. Abstand (Ellipse)
Techn. Abstand (Circle)
Auflastenradius
Fundament
Kranstellfläche (KSF)
`; } else { html += '
Keine Anlagen gesetzt
'; } html += '
Sicherungsstand (ALKIS)
'; Object.keys(STATUS_MAP).forEach(status => { const data = STATUS_MAP[status]; html += `
${status}
${data.desc}
`; }); legendContent.innerHTML = html; } // Toggle Legend collapse legendHeader.addEventListener('click', () => { floatingLegend.classList.toggle('collapsed'); btnToggleLegend.innerHTML = floatingLegend.classList.contains('collapsed') ? '▼' : '▲'; }); state.map.on('overlayadd overlayremove', updateLegend); // Also update on variant change variantTabs.forEach(tab => { tab.addEventListener('click', () => { // ... existing variant logic (already there, but we need to trigger updateLegend) setTimeout(updateLegend, 100); }); }); function createTurbine(latlng, loadedNr = null, overrideData = null) { const rd = overrideData?.rd || parseFloat(inputRotor.value) || 160; const hh = overrideData?.hh || parseFloat(inputHub.value) || 165; const fr = overrideData?.fr || parseFloat(inputFoundation.value) || 15; const type = overrideData?.type || inputType.value || "Standard Typ"; const hersteller = overrideData?.hersteller || inputManufacturer.value || "Enercon"; const weaNr = overrideData?.nr || loadedNr || (state.turbines.length + 1).toString(); const ksfAngle = overrideData?.ksfAngle || 0; const ksfMirrored = overrideData?.ksfMirrored || false; const geoms = calculateGeometries(latlng, rd, hh, fr, ksfAngle, ksfMirrored, hersteller); const turbineIcon = L.divIcon({ className: 'turbine-icon-container', html: ` `, iconSize: [32, 32], iconAnchor: [16, 16] }); const turbine = { id: `WEA_${Date.now()}`, nr: weaNr, variant: overrideData?.variant || state.activeVariant, hersteller, type, rd, hh, fr, latlng, ksfAngle, ksfMirrored, totalHeight: geoms.totalHeight, layers: { marker: L.marker(latlng, { draggable: true, icon: turbineIcon }), sweptArea: L.geoJSON(geoms.sweptArea, { style: { color: '#00c8ff', weight: 1, dashArray: '4, 4', fillOpacity: 0.1 } }), techDist: L.geoJSON(geoms.techDist, { style: { color: '#ffcc00', weight: 2, dashArray: '5, 5', fillOpacity: 0 } }), techDistSmall: L.geoJSON(geoms.techDistSmall, { style: { color: '#ffcc00', weight: 1.5, dashArray: '2, 4', fillOpacity: 0 } }), loadRadius: L.geoJSON(geoms.loadRadius, { style: { color: '#ff4444', weight: 1, fillOpacity: 0.05 } }), foundation: L.geoJSON(geoms.foundation, { style: { color: '#3498db', weight: 1, fillOpacity: 0.3 } }), ksf: L.geoJSON(geoms.ksf, { style: { color: '#e74c3c', weight: 1.5, fillOpacity: 0.4 } }), blf: L.geoJSON(geoms.blf, { style: { color: '#9b59b6', weight: 1, dashArray: '3, 3', fillOpacity: 0.2 } }), mf: L.geoJSON(geoms.mf, { style: { color: '#95a5a6', weight: 1, dashArray: '2, 2', fillOpacity: 0.15 } }), rotationHandle: L.marker(latlng, { draggable: true, icon: L.divIcon({ className: 'rotation-handle', html: `
`, iconSize: [24, 24], iconAnchor: [12, 12] }) }) } }; // Function to position the rotation handle based on current center and angle const updateRotationHandlePos = (turbine) => { const angleRad = (turbine.ksfAngle * Math.PI) / 180; const dist = (turbine.rd * 0.4) / 1000; // Place it within or near the rotor radius for easy access (scaled for km) // In UTM for better precision const utmCenter = proj4(wgs84, utm32, [turbine.latlng.lng, turbine.latlng.lat]); // Negative for CW rotation if ksfAngle follows that convention const handleUtm = [ utmCenter[0] - (turbine.rd * 0.5 + 10) * Math.sin(angleRad), utmCenter[1] - (turbine.rd * 0.5 + 10) * Math.cos(angleRad) ]; const handleWgs = proj4(utm32, wgs84, handleUtm); turbine.layers.rotationHandle.setLatLng([handleWgs[1], handleWgs[0]]); }; updateRotationHandlePos(turbine); Object.entries(turbine.layers).forEach(([name, layer]) => { if (name !== 'rotationHandle') { if (name === 'marker' || state.showAuxiliary) { variantLayers[turbine.variant].addLayer(layer); } } }); updateLabel(turbine, geoms); // Click to Edit turbine.layers.marker.on('click', () => openEditPanel(turbine)); // Drag Update turbine.layers.marker.on('drag', (e) => { const newPos = e.target.getLatLng(); turbine.latlng = newPos; const newGeoms = calculateGeometries(newPos, turbine.rd, turbine.hh, turbine.fr, turbine.ksfAngle, turbine.ksfMirrored, turbine.hersteller); turbine.layers.sweptArea.clearLayers().addData(newGeoms.sweptArea); turbine.layers.techDist.clearLayers().addData(newGeoms.techDist); turbine.layers.techDistSmall.clearLayers().addData(newGeoms.techDistSmall); turbine.layers.loadRadius.clearLayers().addData(newGeoms.loadRadius); turbine.layers.foundation.clearLayers().addData(newGeoms.foundation); turbine.layers.ksf.clearLayers().addData(newGeoms.ksf); turbine.layers.blf.clearLayers().addData(newGeoms.blf); turbine.layers.mf.clearLayers().addData(newGeoms.mf); updateRotationHandlePos(turbine); updateLabel(turbine, newGeoms); updateProximityLines(); updateLegend(); // Show symbols in legend triggerAutoSave(); if (activeTurbine && activeTurbine.id === turbine.id) { // Keep fields updated editNr.value = turbine.nr; updateEditPanelPosition(); // Sync floating panel } }); // Rotation Handle drag logic turbine.layers.rotationHandle.on('drag', (e) => { const handlePos = e.target.getLatLng(); const centerUtm = proj4(wgs84, utm32, [turbine.latlng.lng, turbine.latlng.lat]); const handleUtm = proj4(wgs84, utm32, [handlePos.lng, handlePos.lat]); // Calculate angle: Math.atan2(dx, dy). Note: y is North (up), x is East (right) // We want 0 deg to be South (down), following the KSF pattern if needed. // Moving handle CW (to West/-X) should increase angle. const dx = handleUtm[0] - centerUtm[0]; const dy = centerUtm[1] - handleUtm[1]; // Handle is South of center -> positive dy let angle = Math.atan2(-dx, dy) * (180 / Math.PI); if (angle < 0) angle += 360; turbine.ksfAngle = angle; if (activeTurbine && activeTurbine.id === turbine.id) { editKsfAngle.value = angle.toFixed(1); } const newGeoms = calculateGeometries(turbine.latlng, turbine.rd, turbine.hh, turbine.fr, turbine.ksfAngle, turbine.ksfMirrored, turbine.hersteller); turbine.layers.ksf.clearLayers().addData(newGeoms.ksf); turbine.layers.blf.clearLayers().addData(newGeoms.blf); turbine.layers.mf.clearLayers().addData(newGeoms.mf); updateRotationHandlePos(turbine); }); turbine.layers.rotationHandle.on('dragend', triggerAutoSave); state.turbines.push(turbine); updateProximityLines(); triggerAutoSave(); if (!loadedNr) openEditPanel(turbine); } // Panel Event Listeners btnCloseEdit.onclick = closeEditPanel; btnSaveEdit.onclick = () => { if (!activeTurbine) return; const newNr = editNr.value; const newManufacturer = editManufacturer.value; const newType = editType.value; const newRd = parseFloat(editRd.value); const newNh = parseFloat(editNh.value); const newFr = parseFloat(editFr.value) || 15; const newAngle = parseFloat(editKsfAngle.value) || 0; const newMirrored = editKsfMirrored.checked; if (isNaN(newRd) || isNaN(newNh) || isNaN(newFr)) { alert("Bitte RD, NH und Fundament korrekt angeben."); return; } activeTurbine.nr = newNr; activeTurbine.hersteller = newManufacturer; activeTurbine.type = newType; activeTurbine.rd = newRd; activeTurbine.hh = newNh; activeTurbine.fr = newFr; activeTurbine.ksfAngle = newAngle; activeTurbine.ksfMirrored = newMirrored; const geoms = calculateGeometries(activeTurbine.layers.marker.getLatLng(), newRd, newNh, newFr, newAngle, newMirrored, newManufacturer); activeTurbine.totalHeight = geoms.totalHeight; activeTurbine.layers.sweptArea.clearLayers().addData(geoms.sweptArea); activeTurbine.layers.techDist.clearLayers().addData(geoms.techDist); activeTurbine.layers.techDistSmall.clearLayers().addData(geoms.techDistSmall); activeTurbine.layers.loadRadius.clearLayers().addData(geoms.loadRadius); activeTurbine.layers.ksf.clearLayers().addData(geoms.ksf); activeTurbine.layers.blf.clearLayers().addData(geoms.blf); activeTurbine.layers.mf.clearLayers().addData(geoms.mf); updateLabel(activeTurbine, geoms); updateProximityLines(); triggerAutoSave(); document.getElementById('statusInfo').innerText = `WEA ${newNr} gespeichert.`; }; btnDeleteWEA.onclick = () => { if (!activeTurbine) return; if (confirm(`WEA ${activeTurbine.nr} wirklich löschen?`)) { Object.values(activeTurbine.layers).forEach(l => variantLayers[activeTurbine.variant].removeLayer(l)); state.turbines = state.turbines.filter(t => t.id !== activeTurbine.id); updateProximityLines(); triggerAutoSave(); closeEditPanel(); } }; // Toggle Placement Mode btnPlaceTurbine.addEventListener('click', () => { placementMode = !placementMode; btnPlaceTurbine.classList.toggle('active', placementMode); state.map.getContainer().style.cursor = placementMode ? 'crosshair' : ''; state.map.getContainer().classList.toggle('placement-active', placementMode); }); state.map.on('click', (e) => { if (placementMode) { createTurbine(e.latlng); placementMode = false; btnPlaceTurbine.classList.remove('active'); state.map.getContainer().style.cursor = ''; state.map.getContainer().classList.remove('placement-active'); } }); // Variant Switching Logic variantTabs.forEach(tab => { tab.addEventListener('click', () => { if (state.activeVariant === tab.dataset.variant) return; variantTabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); const newVariant = tab.dataset.variant; state.map.removeLayer(variantLayers[state.activeVariant]); state.activeVariant = newVariant; variantLayers[state.activeVariant].addTo(state.map); document.getElementById('statusInfo').innerText = `Variante ${newVariant} aktiv.`; }); }); // Hilfsgeometrien Toggle const checkShowAux = document.getElementById('checkShowAux'); if (checkShowAux) { checkShowAux.onchange = () => { state.showAuxiliary = checkShowAux.checked; state.turbines.forEach(t => { Object.entries(t.layers).forEach(([name, layer]) => { if (name !== 'marker' && name !== 'rotationHandle') { if (state.showAuxiliary) { variantLayers[t.variant].addLayer(layer); } else { variantLayers[t.variant].removeLayer(layer); } } }); }); }; } // Simplified Measurement Tool let measureLayer = null; let measurePoints = []; let mouseMarker = L.circleMarker([0, 0], { radius: 0, opacity: 0 }).addTo(state.map); // UTM Creation Logic const btnCreateAtUTM = document.getElementById('btnCreateAtUTM'); const inputUtmE = document.getElementById('utm-e'); const inputUtmN = document.getElementById('utm-n'); btnCreateAtUTM.addEventListener('click', () => { const e = parseFloat(inputUtmE.value); const n = parseFloat(inputUtmN.value); if (isNaN(e) || isNaN(n)) { alert("Bitte geben Sie gültige UTM-Koordinaten (Rechtswert und Hochwert) ein."); return; } try { // Convert UTM32 (EPSG:25832) to WGS84 const coords = proj4(utm32, wgs84, [e, n]); const latlng = L.latLng(coords[1], coords[0]); createTurbine(latlng); state.map.setView(latlng, 15); // Success visual feedback btnCreateAtUTM.style.background = '#2ecc71'; setTimeout(() => btnCreateAtUTM.style.background = '', 1000); } catch (err) { console.error("UTM conversion error:", err); alert("Fehler bei der Koordinatenumrechnung. Bitte prüfen Sie die Werte."); } }); function stopMeasurement() { measureMode = null; measurePoints = []; if (measureLayer) state.map.removeLayer(measureLayer); measureLayer = null; mouseMarker.unbindTooltip(); btnMeasureDist.classList.remove('active'); btnMeasureArea.classList.remove('active'); state.map.getContainer().style.cursor = ''; } function startMeasurement(mode) { // Clear previous results from Hilfs-Geometrien as requested overlays["Hilfs-Geometrien"].clearLayers(); stopMeasurement(); measureMode = mode; if (mode === 'dist') { btnMeasureDist.classList.add('active'); measureLayer = L.polyline([], { color: 'var(--primary-color)', weight: 3, dashArray: '5, 5' }).addTo(state.map); } else { btnMeasureArea.classList.add('active'); measureLayer = L.polygon([], { color: 'var(--primary-color)', weight: 3, fillOpacity: 0.2, dashArray: '5, 5' }).addTo(state.map); } state.map.getContainer().style.cursor = 'crosshair'; document.getElementById('statusInfo').innerText = "Klicke zum Starten. Doppelklick zum Beenden."; } btnMeasureDist.onclick = () => measureMode === 'dist' ? stopMeasurement() : startMeasurement('dist'); btnMeasureArea.onclick = () => measureMode === 'area' ? stopMeasurement() : startMeasurement('area'); state.map.on('mousemove', (e) => { if (!measureMode || measurePoints.length === 0) return; const tempPoints = [...measurePoints, e.latlng]; measureLayer.setLatLngs(tempPoints); // Calculate intermediate result const geojson = measureLayer.toGeoJSON(); let val = ""; if (measureMode === 'dist') { const len = turf.length(geojson, { units: 'kilometers' }); val = len < 1 ? `${(len * 1000).toFixed(0)}m` : `${len.toFixed(2)}km`; } else if (measurePoints.length > 2) { const area = turf.area(geojson); val = area < 10000 ? `${area.toFixed(0)}m²` : `${(area / 10000).toFixed(2)}ha`; } if (val) { mouseMarker.setLatLng(e.latlng); mouseMarker.bindTooltip(val, { permanent: true, direction: 'top', className: 'measure-tooltip' }).openTooltip(); } }); state.map.on('click', (e) => { if (placementMode) { createTurbine(e.latlng); placementMode = false; btnPlaceTurbine.classList.remove('active'); state.map.getContainer().style.cursor = ''; return; } if (measureMode) { measurePoints.push(e.latlng); measureLayer.setLatLngs(measurePoints); // After first click, change style to solid measureLayer.setStyle({ dashArray: null }); return; } // Close panel if clicking empty map if (e.originalEvent && (e.originalEvent.target.id === 'map' || e.originalEvent.target.classList.contains('leaflet-container'))) { closeEditPanel(); } }); state.map.on('dblclick', (e) => { if (measureMode) { if (measurePoints.length < 2) { stopMeasurement(); return; } const geojson = measureLayer.toGeoJSON(); let result = ""; if (measureMode === 'dist') { const len = turf.length(geojson, { units: 'kilometers' }); result = len < 1 ? `${(len * 1000).toFixed(1)} m` : `${len.toFixed(3)} km`; } else { const area = turf.area(geojson); result = area < 10000 ? `${area.toFixed(1)} m²` : `${(area / 10000).toFixed(2)} ha`; } // Keep the layer const finalLayer = measureLayer; finalLayer.setStyle({ color: '#ffcc00', weight: 4 }); const popupDiv = document.createElement('div'); popupDiv.innerHTML = `
Messung:
${result}
`; popupDiv.querySelector('.btn-del-measure').onclick = () => { overlays["Hilfs-Geometrien"].removeLayer(finalLayer); }; finalLayer.bindPopup(popupDiv).openPopup(e.latlng); measureLayer = null; overlays["Hilfs-Geometrien"].addLayer(finalLayer); stopMeasurement(); } }); // Variant Switching Logic variantTabs.forEach(tab => { tab.addEventListener('click', () => { if (state.activeVariant === tab.dataset.variant) return; variantTabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); state.map.removeLayer(variantLayers[state.activeVariant]); state.activeVariant = tab.dataset.variant; variantLayers[state.activeVariant].addTo(state.map); updateProximityLines(); closeEditPanel(); }); }); // Base Layers & Overlays const baseLayers = { "Straßenkarte": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OSM' }), "Luftbild": L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri' }).addTo(state.map) }; const overlays = { "Variante A": variantLayers['A'], "Variante B": variantLayers['B'], "Variante C": variantLayers['C'], "Hilfs-Geometrien": L.featureGroup().addTo(state.map), "Abstände (Gleiches Layout)": L.featureGroup().addTo(state.map) }; const proximityLinesLayer = overlays["Abstände (Gleiches Layout)"]; function updateProximityLines() { proximityLinesLayer.clearLayers(); // Only show lines if the current variant layer is visible if (!state.map.hasLayer(variantLayers[state.activeVariant])) return; // Filter turbines for current variant const currentVariantTurbines = state.turbines.filter(t => t.variant === state.activeVariant); if (currentVariantTurbines.length === 0) return; // 1. Turbine-to-Turbine Proximity (600m) const groups = {}; currentVariantTurbines.forEach(t => { const key = `${t.type}_${t.rd}_${t.hh}`; if (!groups[key]) groups[key] = []; groups[key].push(t); }); for (const key in groups) { const group = groups[key]; if (group.length < 2) continue; for (let i = 0; i < group.length; i++) { for (let j = i + 1; j < group.length; j++) { const t1 = group[i]; const t2 = group[j]; const p1 = turf.point([t1.latlng.lng, t1.latlng.lat]); const p2 = turf.point([t2.latlng.lng, t2.latlng.lat]); const distKm = turf.distance(p1, p2, { units: 'kilometers' }); const distM = distKm * 1000; if (distM < 600) { const line = L.polyline([t1.latlng, t2.latlng], { color: '#ff8800', weight: 2, dashArray: '10, 10', opacity: 0.7 }).addTo(proximityLinesLayer); const mid = L.latLng((t1.latlng.lat + t2.latlng.lat) / 2, (t1.latlng.lng + t2.latlng.lng) / 2); line.bindTooltip(`${distM.toFixed(1)} m`, { permanent: true, direction: 'center', className: 'proximity-tooltip' }).openTooltip(mid); } } } } // 2. Feature Proximity (Houses 700m, Lines 300m) Object.keys(state.bakedData).forEach(layerName => { const layer = overlays[layerName]; const isVisible = layer && state.map.hasLayer(layer); if (!isVisible) return; const nameLow = layerName.toLowerCase(); let threshold = 0; let type = ''; let color = '#9400d3'; // Default violet if (nameLow.includes('wohnbebauung') || nameLow.includes('wohngebäude')) { threshold = 700; type = 'house'; color = '#ff4444'; } else if (nameLow.includes('freileitung')) { threshold = 140; type = 'overhead'; color = '#ccaa00'; } else if (nameLow.includes('leitung')) { threshold = 300; type = 'line'; color = '#9400d3'; } if (threshold === 0) return; const layerData = state.bakedData[layerName].data; currentVariantTurbines.forEach(t => { const GH = Number(t.hh) + (Number(t.rd) / 2); const tPoint = turf.point([t.latlng.lng, t.latlng.lat]); turf.featureEach(layerData, (feature) => { let closestPoint; try { if (feature.geometry.type === 'Point') { closestPoint = turf.point(feature.geometry.coordinates); } else if (feature.geometry.type === 'LineString' || feature.geometry.type === 'MultiLineString') { closestPoint = turf.nearestPointOnLine(feature, tPoint); } else if (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon') { // Find closest point on polygon boundary const boundary = turf.polygonToLine(feature); closestPoint = turf.nearestPointOnLine(boundary, tPoint); } else { return; } const distKm = turf.distance(tPoint, closestPoint, { units: 'kilometers' }); const distM = distKm * 1000; if (distM < threshold) { const ratio = (distM / GH).toFixed(1); const targetLatLng = L.latLng(closestPoint.geometry.coordinates[1], closestPoint.geometry.coordinates[0]); const line = L.polyline([t.latlng, targetLatLng], { color: color, weight: 2, dashArray: '5, 8', opacity: 0.8 }).addTo(proximityLinesLayer); const mid = L.latLng((t.latlng.lat + targetLatLng.lat) / 2, (t.latlng.lng + targetLatLng.lng) / 2); const label = type === 'house' ? `${distM.toFixed(1)} m (${ratio} H)` : `${distM.toFixed(1)} m`; line.bindTooltip(label, { permanent: true, direction: 'center', className: type === 'house' ? 'proximity-tooltip-red' : type === 'overhead' ? 'proximity-tooltip-yellow' : 'proximity-tooltip-violet' }).openTooltip(mid); } } catch (err) { console.warn("Distance calc error", err); } }); }); }); } const layerControl = L.control.layers(baseLayers, overlays, { collapsed: false }).addTo(state.map); // Update proximity lines when layers are toggled in legend state.map.on('overlayadd overlayremove', () => { updateProximityLines(); }); // Filtered loadLocalLayer logic... async function loadLocalLayer(path, label, color) { try { const response = await fetch(path).catch(() => null); if (!response || !response.ok) return; const data = await response.json(); const layer = L.geoJSON(data, { style: { color: color, weight: 1.5, fillOpacity: 0.2 }, onEachFeature: (feature, layer) => { if (feature.properties) { let popup = "Attribute:
"; for (let key in feature.properties) popup += `${key}: ${feature.properties[key]}
`; layer.bindPopup(popup); } } }); overlays[label] = layer; state.map.addLayer(layer); layerControl.addOverlay(layer, label); if (label.toLowerCase().includes('eigentümer') || label.toLowerCase().includes('flurstücke')) { layer.bringToBack(); } } catch (e) { } } function getDynamicStyle(layerName) { const name = layerName.toLowerCase(); if (name.includes('wohnbebauung')) { return { color: '#ff4444', weight: 1.5, fillOpacity: 0.6, isResidential: true }; } if (name.includes('freileitung')) { return { color: '#ccaa00', weight: 3, fillOpacity: 0 }; } if (name.includes('leitung')) { return { color: '#9400d3', weight: 2.5, fillOpacity: 0 }; } if (name.includes('eigentümer')) { return { color: '#000000', weight: 2, fillOpacity: 0, fillColor: 'transparent' }; } if (name.includes('windeignungsgebiet') || name.includes('konzentrationszone') || name.includes('plangebiet')) { return { color: '#ff0000', weight: 3, fillOpacity: 0, fillColor: 'transparent' }; } if (name.includes('standort') || (name.includes('wea') && !name.includes('zone'))) { return { isTurbine: true }; } return null; // Return null if no smart match } async function processShapefileBuffers(shpBuffer, dbfBuffer, layerName, manualStyle = null) { const statusEl = document.getElementById('statusInfo'); try { statusEl.innerText = `Verarbeite ${layerName}...`; const geojson = await shp.combine([shp.parseShp(shpBuffer), shp.parseDbf(dbfBuffer)]); let count = 0; geojson.features.forEach(f => { if (!f.geometry) return; const reproject = (coords) => { if (typeof coords[0] === 'number') { count++; return proj4(utm32, wgs84, coords); } return coords.map(reproject); }; f.geometry.coordinates = reproject(f.geometry.coordinates); }); // Apply Smart Styling (Smart Match takes precedence over manual config) const smartStyle = getDynamicStyle(layerName); const style = smartStyle || manualStyle || { color: '#00c8ff', weight: 1.5, fillOpacity: 0.2 }; // Store in cache for bundling state.bakedData[layerName] = { data: geojson, style: style }; // Automated Turbine Creation from Points if (style.isTurbine) { geojson.features.forEach(f => { if (f.geometry && f.geometry.type === 'Point') { const latlng = L.latLng(f.geometry.coordinates[1], f.geometry.coordinates[0]); const props = f.properties || {}; createTurbine(latlng, null, { nr: props.WEA_Nr || props.Nr || props.ID, type: props.Typ || props.Type, rd: parseFloat(props.RD || props.Rotor), hh: parseFloat(props.NH || props.HubHeight || props.HH) }); } }); statusEl.innerText = `${layerName}: WEAs automatisch erstellt.`; return; // Don't add as a regular GeoJSON layer if we created real WEAs } const layer = L.geoJSON(geojson, { style: (feature) => { let fillColor = style.fillColor || style.color; // Auto-detect ALKIS columns if not set if (layerName.toLowerCase().includes('eigentümer') && !state.ownerMapping) { const sampleProps = feature.properties; const hasGNA = 'GNA' in sampleProps || 'gna' in sampleProps; const hasVNA = 'VNA' in sampleProps || 'vna' in sampleProps; if (hasGNA && hasVNA) { state.ownerMapping = { firstName: 'VNA' in sampleProps ? 'VNA' : 'vna', lastName: 'GNA' in sampleProps ? 'GNA' : 'gna' }; console.log("ALKIS-Spalten automatisch erkannt:", state.ownerMapping); } } // Owner-Status-Coloring if (layerName.toLowerCase().includes('eigentümer') && state.ownerMapping) { const props = feature.properties; const getProp = (key) => props[key] || props[key.toLowerCase()] || props[key.toUpperCase()] || ''; const firstName = getProp(state.ownerMapping.firstName); const lastName = getProp(state.ownerMapping.lastName); const ownerName = `${firstName} ${lastName}`.trim().toLowerCase(); const stored = state.ownerStatuses[ownerName]; const status = (typeof stored === 'string' ? stored : (stored?.status || "")).toLowerCase(); if (status === 'gbr' || status === 'gesichert') fillColor = '#2ecc71'; if (status === 'external' || status === 'fremdplanung') fillColor = '#e74c3c'; if (status === 'declined' || status === 'ablehnend' || status === 'negative') fillColor = '#e74c3c'; if (status === 'undecided' || status === 'unentschlossen') fillColor = '#95a5a6'; if (status === 'positive' || status === 'positiv') fillColor = '#5efd9c'; if (status === 'in verhandlung') fillColor = '#f1c40f'; return { color: '#000', weight: 1, fillOpacity: 0.7, fillColor: fillColor }; } return { color: style.color, weight: style.weight, fillOpacity: style.fillOpacity, fillColor: fillColor }; }, pointToLayer: (feature, latlng) => { if (style.isResidential) { return L.marker(latlng, { icon: L.divIcon({ className: 'residential-icon', html: '🏠', iconSize: [16, 16], iconAnchor: [8, 8] }) }); } return L.circleMarker(latlng, { radius: 4, fillColor: style.color, color: "#fff", weight: 1, opacity: 1, fillOpacity: 0.8 }); }, onEachFeature: (feature, layer) => { if (feature.properties) { let popup = `${layerName}

`; for (let key in feature.properties) { const val = feature.properties[key]; if (val !== null && val !== undefined) popup += `${key}: ${val}
`; } layer.bindPopup(popup); } } }); overlays[layerName] = layer; state.map.addLayer(layer); layerControl.addOverlay(layer, layerName); if (layerName.toLowerCase().includes('eigentümer') || layerName.toLowerCase().includes('flurstücke')) { layer.bringToBack(); } // Auto-Zoom to Planning Area if (layerName.toLowerCase().includes('windeignungsgebiet')) { state.map.fitBounds(layer.getBounds()); } statusEl.innerText = `${layerName} erfolgreich geladen.`; } catch (e) { console.error(`Fehler beim Verarbeiten von ${layerName}:`, e); statusEl.innerHTML = `Fehler bei ${layerName}: ${e.message}`; } } async function loadShapefileLayer(layerDef) { const statusEl = document.getElementById('statusInfo'); try { statusEl.innerText = `Lade Shapefile: ${layerDef.name}...`; const encodedFile = encodeURIComponent(layerDef.file); const shpResp = await fetch(`Shapefile/${encodedFile}.shp`); const dbfResp = await fetch(`Shapefile/${encodedFile}.dbf`); if (!shpResp.ok || !dbfResp.ok) throw new Error("Fetch fehlgeschlagen"); const shpBuffer = await shpResp.arrayBuffer(); const dbfBuffer = await dbfResp.arrayBuffer(); await processShapefileBuffers(shpBuffer, dbfBuffer, layerDef.name, layerDef); } catch (e) { if (window.location.protocol !== 'file:') console.error(`Fehler bei ${layerDef.name}:`, e); } } async function initDynamicLayers() { const statusEl = document.getElementById('statusInfo'); const isLocalFile = window.location.protocol === 'file:'; try { // Priority 1: Use window.BAKED_DATA if available (Standalone Mode) if (window.BAKED_DATA) { // Support both old format (direct layers) and new format (with mapping/statuses) let bakedLayers = window.BAKED_DATA; if (window.BAKED_DATA.layers) { bakedLayers = window.BAKED_DATA.layers; state.ownerMapping = window.BAKED_DATA.ownerMapping || null; state.ownerStatuses = window.BAKED_DATA.ownerStatuses || {}; } for (const name in bakedLayers) { const entry = bakedLayers[name]; const smartStyle = getDynamicStyle(name); const style = smartStyle || entry.style || { color: '#00c8ff', weight: 1.5, fillOpacity: 0.2 }; // Automated Turbine Creation from BAKED_DATA if (style.isTurbine) { entry.data.features.forEach(f => { if (f.geometry && f.geometry.type === 'Point') { const latlng = L.latLng(f.geometry.coordinates[1], f.geometry.coordinates[0]); const props = f.properties || {}; createTurbine(latlng, null, { nr: props.WEA_Nr || props.Nr || props.ID, type: props.Typ || props.Type, rd: parseFloat(props.RD || props.Rotor), hh: parseFloat(props.NH || props.HubHeight || props.HH) }); } }); state.bakedData[name] = entry; continue; } const layer = L.geoJSON(entry.data, { style: (feature) => { let fillColor = style.fillColor || style.color; if (name.toLowerCase().includes('eigentümer') && state.ownerMapping) { const props = feature.properties; const firstName = props[state.ownerMapping.firstName] || ''; const lastName = props[state.ownerMapping.lastName] || ''; const ownerName = `${firstName} ${lastName}`.trim(); const status = state.ownerStatuses[ownerName]; if (status === 'gbr') fillColor = '#2ecc71'; if (status === 'external') fillColor = '#e74c3c'; if (status === 'declined') fillColor = '#f1c40f'; if (status === 'positive') fillColor = '#5efd9c'; if (status === 'undecided') fillColor = '#95a5a6'; return { color: '#000', weight: 1, fillOpacity: 0.7, fillColor: fillColor }; } return { color: style.color, weight: style.weight, fillOpacity: style.fillOpacity, fillColor: fillColor }; }, pointToLayer: (feature, latlng) => { if (style.isResidential) { return L.marker(latlng, { icon: L.divIcon({ className: 'residential-icon', html: '🏠', iconSize: [16, 16], iconAnchor: [8, 8] }) }); } return L.circleMarker(latlng, { radius: 4, fillColor: style.color, color: "#fff", weight: 1, opacity: 1, fillOpacity: 0.8 }); }, onEachFeature: (feature, layer) => { if (feature.properties) { let popup = `${name}

`; for (let key in feature.properties) { const val = feature.properties[key]; if (val !== null && val !== undefined) popup += `${key}: ${val}
`; } layer.bindPopup(popup); } } }); overlays[name] = layer; state.map.addLayer(layer); layerControl.addOverlay(layer, name); state.bakedData[name] = entry; if (name.toLowerCase().includes('eigentümer') || name.toLowerCase().includes('flurstücke')) { layer.bringToBack(); } // Auto-Zoom to Planning Area (Standalone Mode) if (name.toLowerCase().includes('windeignungsgebiet')) { state.map.fitBounds(layer.getBounds()); } } statusEl.innerText = "Stand-Alone Daten geladen."; // NICHT return – wir laden den ALKIS-DB-Layer immer, // damit Status-Farben aus der Datenbank angezeigt werden. } // Priority 2: Use window.LAYER_CONFIG (Script-based config) // Nur laden wenn BAKED_DATA nicht vorhanden war let layers = !window.BAKED_DATA ? window.LAYER_CONFIG : null; // Fallback: Fetch layers.json (if server is running) if (!layers) { const resp = await fetch('config/layers.json').catch(() => null); if (resp && resp.ok) layers = await resp.json(); } if (!layers) { if (isLocalFile && !window.BAKED_DATA) { statusEl.innerHTML = `Manuelles Laden: Ziehen Sie Shapefiles auf die Karte.`; } // Nicht return – wir versuchen trotzdem den ALKIS-Layer zu laden } if (layers) { for (const l of layers) { // Lokalen Eigentümer-Layer umbenennen, um Verwechslung mit DB zu vermeiden if (l.name.toLowerCase() === 'eigentümer') { l.name = 'Eigentümer (Lokal)'; } if (l.file.toLowerCase().endsWith('.geojson')) { await loadLocalLayer(`data/${l.file}`, l.name, l.color); } else { await loadShapefileLayer(l); } } } // ALKIS aus Datenbank IMMER laden console.log("Lade ALKIS-Layer aus Datenbank..."); const alkisResp = await fetch('/api/layers/alkis').catch(err => { console.error("Netzwerkfehler beim Laden des ALKIS-Layers:", err); return null; }); if (alkisResp && alkisResp.ok) { const data = await alkisResp.json(); console.log(`ALKIS API: ${data.features ? data.features.length : 0} Features erhalten.`); await processALKISData(data, "Eigentümer (ALKIS DB)"); // KRITISCH: Lokalen Eigentümer-Shapefile-Layer entfernen, da der DB-Layer // die gleichen Geometrien hat + Status-Farben. Der lokale Layer hat // fillOpacity: 0 und verdeckt sonst die Farben des DB-Layers. const localOwnerKeys = Object.keys(overlays).filter(k => k.toLowerCase().includes('eigentümer') && k !== 'Eigentümer (ALKIS DB)' ); localOwnerKeys.forEach(key => { console.log(`Entferne lokalen Layer "${key}" (wird durch ALKIS DB ersetzt).`); if (state.map.hasLayer(overlays[key])) { state.map.removeLayer(overlays[key]); } layerControl.removeLayer(overlays[key]); delete overlays[key]; }); } else { const errorText = alkisResp ? await alkisResp.text() : "Server nicht erreichbar"; console.warn("ALKIS-Layer konnte nicht geladen werden:", errorText); document.getElementById('statusInfo').innerHTML += ` | ALKIS-Layer Fehler`; } statusEl.innerText = "Layer geladen (ALKIS DB integriert)."; } catch (e) { if (!isLocalFile) console.error("Layer-Init fehlgeschlagen:", e); } } async function processALKISData(geojson, layerName) { console.log(`Verarbeite ALKIS-Daten für Layer: ${layerName}. Features: ${geojson.features ? geojson.features.length : 0}`); const layer = L.geoJSON(geojson, { style: (feature) => { const props = feature.properties; const firstName = (props.vorname || props.VNA || '').trim(); const lastName = (props.nachname || props.GNA || '').trim(); // Normalisierung des Namens für den Abgleich const normalize = (s) => (s || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '').trim(); const ownerKey = normalize(firstName + lastName); // Suche in den geladenen Status-Einträgen let rawStatus = ''; let foundInState = false; // Wir suchen im state.ownerStatuses for (let key in state.ownerStatuses) { if (normalize(key) === ownerKey) { rawStatus = state.ownerStatuses[key].status; foundInState = true; break; } } if (!rawStatus) rawStatus = props.status || ''; // Translate legacy values safely const status = (rawStatus && LEGACY_STATUS_MAP[rawStatus.toLowerCase()]) ? LEGACY_STATUS_MAP[rawStatus.toLowerCase()] : (rawStatus || 'none'); let fillColor = 'transparent'; let opacity = 0.1; if (status && STATUS_MAP[status]) { fillColor = STATUS_MAP[status].color; opacity = 0.8; // High visibility } return { color: '#000', weight: 1.5, // Stronger border fillOpacity: opacity, fillColor: fillColor }; }, onEachFeature: (feature, layer) => { if (feature.properties) { const props = feature.properties; const firstName = (props.vorname || props.VNA || '').trim(); const lastName = (props.nachname || props.GNA || '').trim(); const normalize = (s) => (s || '').toString().toLowerCase().replace(/[^a-z0-9]/g, '').trim(); const ownerKey = normalize(firstName + lastName); let rawStatus = props.status || 'Kein Status'; let notiz = props.notiz || ''; for (let key in state.ownerStatuses) { if (normalize(key) === ownerKey) { rawStatus = state.ownerStatuses[key].status; notiz = state.ownerStatuses[key].notiz; break; } } const status = (rawStatus && LEGACY_STATUS_MAP[rawStatus.toLowerCase()]) ? LEGACY_STATUS_MAP[rawStatus.toLowerCase()] : (rawStatus || 'Kein Status'); let popup = `${layerName}

`; popup += `Eigentümer: ${firstName} ${lastName}
`; popup += `Status: ${status}
`; if (notiz) popup += `Notiz: ${notiz}
`; popup += `
`; for (let key in props) { if (['VNA', 'GNA', 'vorname', 'nachname', 'status', 'notiz', 'id', 'FLN', 'ZAE', 'NEN', 'FSK'].includes(key)) continue; const val = props[key]; if (val !== null && val !== undefined) popup += `${key}: ${val}
`; } layer.bindPopup(popup); // NEU: Tooltip-Label für die Karte (wird per CSS gesteuert erst bei Zoom eingeblendet) const flur = props.FLN || '-'; const fst = props.ZAE ? (props.NEN ? `${props.ZAE}/${props.NEN}` : props.ZAE) : '-'; const labelContent = `
${lastName}, ${firstName}
Flur ${flur}, Flst. ${fst}
`; layer.bindTooltip(labelContent, { permanent: true, direction: 'center', className: 'alkis-label', offset: [0, 0] }); } } }); overlays[layerName] = layer; state.map.addLayer(layer); layerControl.addOverlay(layer, layerName); layer.bringToFront(); // Ensure it's on top of local shapefiles } // Manual Import & Bundling const btnManualImport = document.getElementById('btnManualImport'); const manualShpInput = document.getElementById('manualShpInput'); const btnExportBundle = document.getElementById('btnExportBundle'); if (btnExportBundle) { btnExportBundle.onclick = () => { if (Object.keys(state.bakedData).length === 0) { alert("Keine Daten zum Exportieren vorhanden! Bitte laden Sie erst Shapefiles."); return; } // Include Parcel statuses and mapping in the export const exportData = { layers: state.bakedData, ownerMapping: state.ownerMapping, ownerStatuses: state.ownerStatuses }; const content = `window.BAKED_DATA = ${JSON.stringify(exportData)};`; const blob = new Blob([content], { type: 'application/javascript' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'baked_layers.js'; a.click(); URL.revokeObjectURL(url); alert("Bundle erstellt! Bitte speichern Sie 'baked_layers.js' im Ordner /data des Projekts."); }; } // Owner Management UI & Logic const btnManageOwners = document.getElementById('btnManageOwners'); const ownerModal = document.getElementById('ownerModal'); const btnCloseOwnerModal = document.getElementById('btnCloseOwnerModal'); const ownerMappingSection = document.getElementById('ownerMappingSection'); const ownerListSection = document.getElementById('ownerListSection'); const selectFirstName = document.getElementById('selectFirstName'); const selectLastName = document.getElementById('selectLastName'); const btnConfirmMapping = document.getElementById('btnConfirmMapping'); const ownerTableBody = document.querySelector('#ownerTable tbody'); const ownerSearch = document.getElementById('ownerSearch'); btnManageOwners.onclick = () => { const ownerLayer = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); if (!ownerLayer) { alert("Kein Eigentümer-Layer gefunden! Bitte laden Sie erst die Eigentümer-Daten."); return; } ownerModal.style.display = 'flex'; // Versuche Auto-Mapping falls noch nicht geschehen if (!state.ownerMapping) { const layer = overlays[ownerLayer]; const allKeys = new Set(); layer.eachLayer(l => { if (l.feature && l.feature.properties) { Object.keys(l.feature.properties).forEach(k => allKeys.add(k)); } }); const sortedKeys = Array.from(allKeys); const vnaMatch = sortedKeys.find(k => k.toUpperCase() === 'VNA' || k.toUpperCase() === 'VORNAME'); const gnaMatch = sortedKeys.find(k => k.toUpperCase() === 'GNA' || k.toUpperCase() === 'NBA' || k.toUpperCase() === 'NACHNAME' || k.toUpperCase() === 'NAME'); if (vnaMatch && gnaMatch) { state.ownerMapping = { firstName: vnaMatch, lastName: gnaMatch }; console.log("Auto-Mapping erfolgreich:", state.ownerMapping); } else if (gnaMatch) { // If only last name/name is found, use it state.ownerMapping = { firstName: '', lastName: gnaMatch }; console.log("Partial Auto-Mapping (Last Name only):", state.ownerMapping); } } if (!state.ownerMapping) { showMappingStage(overlays[ownerLayer]); } else { showOwnerListStage(overlays[ownerLayer]); } }; btnCloseOwnerModal.onclick = () => ownerModal.style.display = 'none'; function showMappingStage(layer) { ownerMappingSection.style.display = 'flex'; ownerListSection.style.display = 'none'; // Extract properties from all available features to ensure we get all keys const allKeys = new Set(); layer.eachLayer(l => { if (l.feature && l.feature.properties) { Object.keys(l.feature.properties).forEach(k => allKeys.add(k)); } }); const sortedKeys = Array.from(allKeys).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); if (sortedKeys.length > 0) { const optionsHtml = sortedKeys.map(k => ``).join(''); selectFirstName.innerHTML = optionsHtml; selectLastName.innerHTML = optionsHtml; // Smart Defaults const vnaMatch = sortedKeys.find(k => ['vorname', 'vna', 'first'].includes(k.toLowerCase())); const gnaMatch = sortedKeys.find(k => ['nachname', 'gna', 'nba', 'name', 'last'].includes(k.toLowerCase())); if (vnaMatch) selectFirstName.value = vnaMatch; else { const vMatch = sortedKeys.find(k => k.toLowerCase().startsWith('v')); if (vMatch) selectFirstName.value = vMatch; } if (gnaMatch) selectLastName.value = gnaMatch; else { const nMatch = sortedKeys.find(k => k.toLowerCase().startsWith('n')); if (nMatch) selectLastName.value = nMatch; } } else { selectFirstName.innerHTML = ''; selectLastName.innerHTML = ''; } } btnConfirmMapping.onclick = () => { state.ownerMapping = { firstName: selectFirstName.value, lastName: selectLastName.value }; const ownerLayer = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); showOwnerListStage(overlays[ownerLayer]); }; function showOwnerListStage(layer) { ownerMappingSection.style.display = 'none'; ownerListSection.style.display = 'flex'; updateOwnerTable(layer); } function updateOwnerTable(layer) { const owners = {}; layer.eachLayer(l => { const p = l.feature.properties; const first = p[state.ownerMapping.firstName] || ''; const last = p[state.ownerMapping.lastName] || ''; const fullName = `${first} ${last}`.trim() || "Unbekannt"; // Adress-Felder extrahieren (STR, HSN, PLZ, ORP) const str = p.STR || ''; const hsn = p.HSN || ''; const plz = p.PLZ || ''; const ort = p.ORP || ''; const address = `${str} ${hsn}, ${plz} ${ort}`.trim().replace(/^,/, '').trim(); if (!owners[fullName]) { owners[fullName] = { count: 0, first, last, address: address || 'Keine Adresse' }; } owners[fullName].count++; }); renderOwnerRows(owners); ownerSearch.oninput = () => { const query = ownerSearch.value.toLowerCase(); const filtered = {}; for (let name in owners) { const o = owners[name]; if (name.toLowerCase().includes(query) || (o.address && o.address.toLowerCase().includes(query))) { filtered[name] = o; } } renderOwnerRows(filtered); }; } function renderOwnerRows(owners) { ownerTableBody.innerHTML = ''; Object.keys(owners).sort().forEach(name => { const data = owners[name]; const stored = state.ownerStatuses[name.toLowerCase()] || { status: 'none', notiz: '' }; const status = typeof stored === 'object' ? (stored.status || 'none') : (stored || 'none'); const notiz = typeof stored === 'object' ? (stored.notiz || '') : ''; const row = document.createElement('tr'); // If last name is missing but first name has content, show first name in bold const displayLast = data.last || data.first || 'Unbekannt'; const displayFirst = data.last ? data.first : ''; row.innerHTML = ` ${displayLast} ${displayFirst} ${data.address} ${data.count} Flurstücke `; ownerTableBody.appendChild(row); }); // Add event listeners to dropdowns document.querySelectorAll('.status-select').forEach(sel => { sel.onchange = async (e) => { const name = e.target.dataset.owner; const status = e.target.value; const notizInput = document.querySelector(`.notiz-input[data-owner="${name}"]`); const notiz = notizInput ? notizInput.value : ""; state.ownerStatuses[name.toLowerCase()] = { status, notiz }; // Sync with DB const data = owners[name]; if (data) { await secureOwner(data.first, data.last, e.target, status, notiz); } refreshOwnerLayerStyle(); updateLegend(); // Refresh legend too }; }); // Auto-Save für Notiz-Feld bei Verlassen (Blur) document.querySelectorAll('.notiz-input').forEach(input => { input.onblur = async (e) => { const name = e.target.dataset.owner; const notiz = e.target.value; const sel = document.querySelector(`.status-select[data-owner="${name}"]`); const status = sel ? sel.value : 'none'; state.ownerStatuses[name.toLowerCase()] = { status, notiz }; const data = owners[name]; if (data) { await secureOwner(data.first, data.last, e.target, status, notiz); } refreshOwnerLayerStyle(); }; input.onkeydown = (e) => { if (e.key === 'Enter') e.target.blur(); }; }); } async function secureOwner(vorname, nachname, element, status = 'none', notiz = '') { const isSelect = element.tagName === 'SELECT'; const isInput = element.tagName === 'INPUT'; // Visual feedback if (isSelect || isInput) { element.style.borderColor = '#2ecc71'; setTimeout(() => element.style.borderColor = '', 1500); } const projekt_id = "BWSamern-Ohne"; try { const response = await fetch('/api/sicherung', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vorname, nachname, projekt_id, status, notiz }) }); const result = await response.json(); if (response.ok) { const fullName = `${vorname || ''} ${nachname || ''}`.trim(); if (fullName) { state.ownerStatuses[fullName.toLowerCase()] = { status, notiz }; } refreshOwnerLayerStyle(); } else { throw new Error(result.error || "Fehler"); } } catch (err) { console.error(err); element.style.borderColor = '#e74c3c'; } } async function handleFileSelection(fileList) { const files = Array.from(fileList); const shpFiles = files.filter(f => f.name.toLowerCase().endsWith('.shp')); const dbfFiles = files.filter(f => f.name.toLowerCase().endsWith('.dbf')); if (shpFiles.length === 0) { document.getElementById('statusInfo').innerHTML = `Keine .shp Dateien gefunden.`; return; } for (const shpFile of shpFiles) { const baseName = shpFile.name.slice(0, -4); const dbfFile = dbfFiles.find(f => f.name.slice(0, -4).toLowerCase() === baseName.toLowerCase()); if (dbfFile) { const shpBuffer = await shpFile.arrayBuffer(); const dbfBuffer = await dbfFile.arrayBuffer(); await processShapefileBuffers(shpBuffer, dbfBuffer, baseName); } else { alert(`DBF-Datei für ${baseName} fehlt! Bitte wählen Sie .shp und .dbf gleichzeitig aus.`); } } } if (btnManualImport && manualShpInput) { btnManualImport.onclick = () => manualShpInput.click(); manualShpInput.onchange = (e) => handleFileSelection(e.target.files); } // Drag & Drop Support const mapContainer = state.map.getContainer(); mapContainer.addEventListener('dragover', (e) => { e.preventDefault(); mapContainer.style.outline = '4px dashed var(--primary-color)'; mapContainer.style.outlineOffset = '-10px'; }); mapContainer.addEventListener('dragleave', () => { mapContainer.style.outline = ''; }); mapContainer.addEventListener('drop', (e) => { e.preventDefault(); mapContainer.style.outline = ''; if (e.dataTransfer.files.length > 0) { handleFileSelection(e.dataTransfer.files); } }); async function loadOwnerStatusesFromDB() { const projekt_id = "BWSamern-Ohne"; console.log("Lade Eigentümer-Status aus Datenbank..."); try { const response = await fetch(`/api/sicherung/${projekt_id}`); if (response.ok) { const entries = await response.json(); entries.forEach(s => { const first = s.vorname || ''; const last = s.nachname || ''; const fullName = `${first} ${last}`.trim().toLowerCase(); if (fullName) { state.ownerStatuses[fullName] = { status: s.status, notiz: s.notiz }; } }); console.log(`${entries.length} Eigentümer-Status geladen.`); refreshOwnerLayerStyle(); } else { console.warn("API Fehler beim Laden der Status:", response.status); } } catch (err) { console.error("Netzwerkfehler beim Laden der Status:", err); } } // Hilfsfunktion zum Neu-Stylen des Eigentümer-Layers function refreshOwnerLayerStyle() { // Bevorzuge den ALKIS DB Layer const alkisKey = Object.keys(overlays).find(k => k === 'Eigentümer (ALKIS DB)'); const ownerLayerName = alkisKey || Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); if (ownerLayerName && overlays[ownerLayerName]) { const layer = overlays[ownerLayerName]; if (typeof layer.options.style === 'function') { layer.setStyle(layer.options.style); } console.log(`refreshOwnerLayerStyle: Layer "${ownerLayerName}" aktualisiert.`); } } // Initialize Dynamic Layers and then load data initDynamicLayers().then(async () => { // Erst Status laden await loadOwnerStatusesFromDB(); // Dann WEAs laden await loadTurbinesFromDB(); // Automatisches Mapping für den Eigentümer-Layer prüfen const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); if (ownerLayerName && overlays[ownerLayerName]) { const layer = overlays[ownerLayerName]; const layers = layer.getLayers(); if (layers.length > 0) { const firstFeature = layers[0].feature; if (firstFeature && firstFeature.properties) { const props = firstFeature.properties; const vna = Object.keys(props).find(k => k.toUpperCase() === 'VNA'); const gna = Object.keys(props).find(k => k.toUpperCase() === 'GNA' || k.toUpperCase() === 'NBA'); if (vna && gna) { state.ownerMapping = { firstName: vna, lastName: gna }; refreshOwnerLayerStyle(); } } } } }); // Project Persistence const btnSave = document.getElementById('btnSaveProject'); const btnLoad = document.getElementById('btnLoadProject'); const projectInput = document.getElementById('projectInput'); btnSave.addEventListener('click', () => { const projectData = { version: "1.0", timestamp: new Date().toISOString(), config: state.config, turbines: state.turbines.map(t => ({ id: t.id, nr: t.nr, variant: t.variant, type: t.type, rd: t.rd, hh: t.hh, latlng: t.layers.marker.getLatLng() })), ownerMapping: state.ownerMapping, ownerStatuses: state.ownerStatuses }; const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `WindPlan_Projekt_${new Date().toLocaleDateString()}.json`; a.click(); URL.revokeObjectURL(url); document.getElementById('statusInfo').innerText = "Projekt lokal exportiert."; }); // DB Persistence let autoSaveTimeout = null; function triggerAutoSave() { if (autoSaveTimeout) clearTimeout(autoSaveTimeout); autoSaveTimeout = setTimeout(() => { saveTurbinesToDB(); }, 1500); // 1.5 Seconds debounce } async function saveTurbinesToDB() { const statusEl = document.getElementById('statusInfo'); if (statusEl) statusEl.innerText = "Automatische Speicherung..."; const projekt_id = "BWSamern-Ohne"; const turbineData = state.turbines.map(t => ({ nr: t.nr, variant: t.variant, hersteller: t.hersteller, type: t.type, rd: t.rd, hh: t.hh, ksfAngle: t.ksfAngle, latlng: t.layers.marker.getLatLng() })); try { const response = await fetch('/api/wea', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projekt_id, turbines: turbineData }) }); const result = await response.json(); if (response.ok) { if (statusEl) statusEl.innerHTML = `${result.message}`; } else { throw new Error(result.error || "Fehler beim Speichern in DB"); } } catch (err) { console.error(err); if (statusEl) statusEl.innerHTML = `Auto-Save Fehler: ${err.message}`; } } const btnSaveDB = document.getElementById('btnSaveDB'); if (btnSaveDB) { btnSaveDB.addEventListener('click', saveTurbinesToDB); } async function loadTurbinesFromDB() { const projekt_id = "BWSamern-Ohne"; const statusEl = document.getElementById('statusInfo'); console.log(`Lade WEAs aus Datenbank für Projekt: ${projekt_id}...`); try { const response = await fetch(`/api/wea/${projekt_id}`); if (response.ok) { const dbTurbines = await response.json(); console.log(`Datenbank-Response: ${dbTurbines.length} WEAs erhalten.`); if (dbTurbines.length > 0) { // Clear existing turbines state.turbines.forEach(t => { Object.values(t.layers).forEach(l => variantLayers[t.variant].removeLayer(l)); }); state.turbines = []; dbTurbines.forEach(t => { const latlng = L.latLng(t.lat, t.lng); createTurbine(latlng, null, { nr: t.nr, hersteller: t.hersteller, type: t.type, rd: t.rd, hh: t.hh, ksfAngle: t.ksfangle ?? t.ksfAngle ?? 0, variant: t.variant }); }); statusEl.innerText = `${dbTurbines.length} WEAs aus Datenbank geladen.`; } else { console.log("Datenbank ist leer für dieses Projekt."); } } else { console.error("Fehler beim API-Aufruf:", response.status); } } catch (err) { console.error("Netzwerkfehler beim Laden aus der DB:", err); } } btnLoad.addEventListener('click', () => projectInput.click()); projectInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const data = JSON.parse(event.target.result); // Clear existing turbines state.turbines.forEach(t => { Object.values(t.layers).forEach(l => { variantLayers[t.variant].removeLayer(l); }); }); state.turbines = []; // Restore Owner Data state.ownerMapping = data.ownerMapping || null; state.ownerStatuses = data.ownerStatuses || {}; // Reconstruct from data data.turbines.forEach(tData => { // Set current UI state momentarily for creating turbine correctly // (Easier than refactoring createTurbine now) inputType.value = tData.type; inputRotor.value = tData.rd; inputHub.value = tData.hh; const oldVariant = state.activeVariant; state.activeVariant = tData.variant; createTurbine(tData.latlng, tData.nr); state.activeVariant = oldVariant; }); updateProximityLines(); updateLegend(); triggerAutoSave(); document.getElementById('statusInfo').innerText = "Projekt geladen."; } catch (err) { console.error("Fehler beim Laden:", err); alert("Ungültige Projektdatei."); } }; reader.readAsText(file); }); updateLegend(); console.log("WindPlaner initialisiert."); document.getElementById('statusInfo').innerText = "System bereit. Karte geladen."; });