/** * WindPlaner - Core Logic */ document.addEventListener('DOMContentLoaded', async () => { const state = { map: null, config: null, turbines: [], activeVariant: 'A', bakedData: {}, // Cache for standalone persistence ownerMapping: { firstName: 'VNA', lastName: 'GNA' }, // Default for ALKIS 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.' } }; // 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); // 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 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 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) { // 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]); }; // 4. Blattlagerfläche (BLF) // Coords for "Nein" case (@spg=1): x is -41 const blfCoords = [[-41, 9], [-61, 9], [-61, -81], [-41, -81], [-41, 9]]; const blf = transform(blfCoords); // 5. Kranstellfläche (KSF) const ksfCoords = [[-8, 0], [-36, 0], [-36, -50], [-8, -50], [-8, 0]]; const ksf = transform(ksfCoords); // 6. Montagefläche (MF) const 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 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; 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 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); 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, 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.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.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 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.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); 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; // 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."; return; } // Priority 2: Use window.LAYER_CONFIG (Script-based config) let layers = window.LAYER_CONFIG; // 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) { statusEl.innerHTML = `Manuelles Laden: Ziehen Sie Shapefiles auf die Karte.`; } return; } for (const l of layers) { if (l.file.toLowerCase().endsWith('.geojson')) { await loadLocalLayer(`data/${l.file}`, l.name, l.color); } else { await loadShapefileLayer(l); } } // NEU: ALKIS aus Datenbank laden, falls kein Eigentümer-Layer da ist const hasOwnerLayer = Object.keys(overlays).some(k => k.toLowerCase().includes('eigentümer')); if (!hasOwnerLayer) { console.log("Kein lokaler Eigentümer-Layer. Lade ALKIS aus Datenbank..."); const resp = await fetch('/api/layers/alkis').catch(() => null); if (resp && resp.ok) { const data = await resp.json(); await processALKISData(data, "Eigentümer (ALKIS DB)"); } } statusEl.innerText = "Alle konfigurierten Layer geladen."; } catch (e) { if (!isLocalFile) console.error("Layer-Init fehlgeschlagen:", e); } } async function processALKISData(geojson, layerName) { const layer = L.geoJSON(geojson, { style: (feature) => { const props = feature.properties; const firstName = (props.VNA || '').trim(); const lastName = (props.GNA || '').trim(); const ownerName = `${firstName} ${lastName}`.trim().toLowerCase(); const stored = state.ownerStatuses[ownerName]; const status = typeof stored === 'object' ? (stored.status || '') : (stored || props.status || ''); let fillColor = 'transparent'; let opacity = 0.1; if (STATUS_MAP[status]) { fillColor = STATUS_MAP[status].color; opacity = 0.7; } else if (status === 'none' || status === '') { fillColor = 'transparent'; opacity = 0.1; } return { color: '#000', weight: 1, fillOpacity: opacity, fillColor: fillColor }; }, onEachFeature: (feature, layer) => { if (feature.properties) { const status = feature.properties.status || 'Kein Status'; const notiz = feature.properties.notiz || ''; let popup = `${layerName}

`; popup += `Eigentümer: ${feature.properties.VNA} ${feature.properties.GNA}
`; popup += `Status: ${status}
`; if (notiz) popup += `Notiz: ${notiz}
`; popup += `
`; for (let key in feature.properties) { if (['VNA', 'GNA', 'status', 'notiz', 'id'].includes(key)) continue; 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); layer.bringToBack(); } // 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'); const gnaMatch = sortedKeys.find(k => k.toUpperCase() === 'GNA' || k.toUpperCase() === 'NBA'); if (vnaMatch && gnaMatch) { state.ownerMapping = { firstName: vnaMatch, lastName: gnaMatch }; console.log("Auto-Mapping erfolgreich:", 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: Prefer VNA for First Name and NBA (or GNA/Name) for Last Name const vnaMatch = sortedKeys.find(k => k.toLowerCase() === 'vna'); const nbaMatch = sortedKeys.find(k => k.toLowerCase() === 'nba'); const gnaMatch = sortedKeys.find(k => k.toLowerCase() === 'gna'); if (vnaMatch) selectFirstName.value = vnaMatch; else { const vMatch = sortedKeys.find(k => k.toLowerCase().startsWith('v')); if (vMatch) selectFirstName.value = vMatch; } if (nbaMatch) selectLastName.value = nbaMatch; else 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"; if (!owners[fullName]) { owners[fullName] = { count: 0, first, last }; } owners[fullName].count++; }); renderOwnerRows(owners); ownerSearch.oninput = () => { const query = ownerSearch.value.toLowerCase(); const filtered = {}; for (let name in owners) { if (name.toLowerCase().includes(query)) filtered[name] = owners[name]; } 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 === 'string' ? stored : (stored.status || 'none'); const notiz = typeof stored === 'string' ? '' : (stored.notiz || ''); const row = document.createElement('tr'); row.innerHTML = ` ${name} ${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(); }; }); // NEU: 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); } }; // Enter-Taste triggert ebenfalls Speichern input.onkeydown = (e) => { if (e.key === 'Enter') e.target.blur(); }; }); // Add event listeners to secure buttons document.querySelectorAll('.btn-secure').forEach(btn => { btn.onclick = async (e) => { const first = e.target.dataset.first; const last = e.target.dataset.last; const name = e.target.dataset.owner; const sel = document.querySelector(`.status-select[data-owner="${name}"]`); const status = sel ? sel.value : 'Gesichert'; const notizInput = document.querySelector(`.notiz-input[data-owner="${name}"]`); const notiz = notizInput ? notizInput.value : ""; await secureOwner(first, last, e.target, status, notiz); }; }); } async function secureOwner(vorname, nachname, element, status = 'Gesichert', notiz = '') { const isButton = element.tagName === 'BUTTON'; const originalText = isButton ? element.innerText : ""; if (isButton) { element.innerText = "Wait..."; element.disabled = true; } 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) { if (isButton) { element.style.background = '#2ecc71'; element.innerText = "✓ Gespeichert"; setTimeout(() => { element.style.background = ''; element.innerText = "Speichern"; element.disabled = false; }, 2000); } console.log(result.message); const fullName = `${vorname || ''} ${nachname || ''}`.trim(); if (fullName) { state.ownerStatuses[fullName.toLowerCase()] = { status, notiz }; } refreshOwnerLayerStyle(); const select = document.querySelector(`.status-select[data-owner="${fullName}"]`); if (select) select.value = status; } else { throw new Error(result.error || "Fehler"); } } catch (err) { console.error(err); if (isButton) { element.style.background = '#e74c3c'; element.innerText = "Error"; setTimeout(() => { element.style.background = ''; element.innerText = originalText; element.disabled = false; }, 2000); } } } 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() { const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); if (ownerLayerName && overlays[ownerLayerName]) { overlays[ownerLayerName].setStyle(overlays[ownerLayerName].options.style); } } initDynamicLayers().then(async () => { // Erst Status laden, dann WEAs await loadOwnerStatusesFromDB(); // Nach dem Laden der Status: Prüfen ob wir den Layer automatisch mappen können const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); if (ownerLayerName && overlays[ownerLayerName]) { const layer = overlays[ownerLayerName]; const firstFeature = layer.getLayers()[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(); // Jetzt werden die Farben sichtbar! } } } loadTurbinesFromDB(); }); // 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, 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, 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); } } // Load from DB after layers are initialized initDynamicLayers().then(() => { loadTurbinesFromDB(); }); 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."; });