bwsamern-ohne_standortplaner/app.js

2012 lines
89 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/**
* 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 measurePoints = [];
let measureLayer = null;
let mouseMarker = L.marker([0, 0], {
icon: L.divIcon({ className: 'measure-mouse-marker', iconSize: [0, 0] }),
interactive: false
}).addTo(state.map);
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: Narrow side (12.1m) is towards the road, wider side (24.4m) is away.
ksfCoords = [[-24.4, -5.5], [12.1, -5.5], [12.1, -65.15], [-24.4, -65.15], [-24.4, -5.5]];
// AMF: 180.00m (L) x 15.00m (B). Centered on tower axis.
const amf = [[-7.5, -65.15], [7.5, -65.15], [7.5, -245.15], [-7.5, -245.15], [-7.5, -65.15]];
// NVM (Nabenvor-Montagefläche): 26,00m (L) x 10,50m (B).
// Positioned on the RIGHT side (the 12.1m road side), starting at 18.25m from center.
const nvm = [[18.25, -5.5], [28.75, -5.5], [28.75, -31.5], [18.25, -31.5], [18.25, -5.5]];
// CRANE BOOM PAD (Triangular auxiliary area): 120.00m (L).
// Narrows from 18m to 7.5m width on the wider side (Left).
const cbp = [[-18.0, -65.15], [-7.5, -65.15], [-7.5, -185.15], [-18.0, -65.15]];
mfParts = [amf, nvm, cbp];
// BLF (Blattlagerfläche): 90,00m (L) x 15,00m (B).
// Positioned on the RIGHT side, starting at 18.25m from center.
blfCoords = [[18.25, -65.15], [33.25, -65.15], [33.25, -155.15], [18.25, -155.15], [18.25, -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 = `
<div style="text-align: left; font-size: 11px; line-height: 1.3;">
<b>WEA ${turbine.nr}</b><br>
E: ${geoms.utmCoords[0].toFixed(0)} | N: ${geoms.utmCoords[1].toFixed(0)}<br>
NH: ${turbine.hh}m | RD: ${turbine.rd}m<br>
GH: ${geoms.totalHeight.toFixed(1)}m
</div>
`;
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 = '<div class="legend-section-title">Anlagen-Geometrien</div>';
if (state.turbines.length > 0) {
html += `
<div class="legend-item"><span class="color-box" style="background: #00c8ff;"></span> Rotorfläche</div>
<div class="legend-item"><span class="color-box" style="border: 2px dashed #ffcc00; background: transparent;"></span> Techn. Abstand (Ellipse)</div>
<div class="legend-item"><span class="color-box" style="border: 1.5px dotted #ffcc00; background: transparent;"></span> Techn. Abstand (Circle)</div>
<div class="legend-item"><span class="color-box" style="background: rgba(255, 68, 68, 0.2); border: 1px solid #ff4444;"></span> Auflastenradius</div>
<div class="legend-item"><span class="color-box" style="background: rgba(52, 152, 219, 0.3); border: 1px solid #3498db;"></span> Fundament</div>
<div class="legend-item"><span class="color-box" style="background: #e74c3c; opacity: 0.6;"></span> Kranstellfläche (KSF)</div>
`;
} else {
html += '<div style="font-size: 0.7rem; opacity: 0.6; padding-left: 20px;">Keine Anlagen gesetzt</div>';
}
html += '<div class="legend-section-title" style="margin-top: 15px;">Sicherungsstand (ALKIS)</div>';
Object.keys(STATUS_MAP).forEach(status => {
const data = STATUS_MAP[status];
html += `
<div class="legend-item-status">
<span class="status-dot" style="background: ${data.color};"></span>
<div class="status-text-container">
<div class="status-label">${status}</div>
<div class="status-desc">${data.desc}</div>
</div>
</div>
`;
});
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: `
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 22L12.5 22L12.2 12L11.8 12L11.5 22Z" fill="#00c8ff" />
<circle cx="12" cy="12" r="1.5" fill="#00c8ff" stroke="white" stroke-width="0.5"/>
<path d="M12 12L12 3" stroke="white" stroke-width="1.8" stroke-linecap="round" />
<path d="M12 12L19.7942 16.5" stroke="white" stroke-width="1.8" stroke-linecap="round" />
<path d="M12 12L4.20577 16.5" stroke="white" stroke-width="1.8" stroke-linecap="round" />
</svg>
`,
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: `
<div style="background: #e74c3c; width: 24px; height: 24px; border-radius: 50%; border: 2px solid white; box-shadow: 0 0 5px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center;">
<svg viewBox="0 0 24 24" width="16" height="16" fill="white">
<path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0 0 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 0 0 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/>
</svg>
</div>`,
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);
}
}
});
});
};
}
// 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)}` : `${(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)}` : `${(area / 10000).toFixed(2)} ha`;
}
// Keep the layer
const finalLayer = measureLayer;
finalLayer.setStyle({ color: '#ffcc00', weight: 4 });
const popupDiv = document.createElement('div');
popupDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; gap: 12px;">
<div style="font-size: 0.85rem; line-height: 1.2;"><b>Messung:</b><br>${result}</div>
<button class="btn-del-measure" style="background: #ff4444; color: white; border: none; border-radius: 4px; padding: 3px 7px; cursor: pointer; font-weight: bold; font-size: 0.9rem;">✕</button>
</div>
`;
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 &copy; 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 = "<b>Attribute:</b><br>";
for (let key in feature.properties) popup += `${key}: ${feature.properties[key]}<br>`;
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 = `<b>${layerName}</b><br><hr style="margin: 5px 0; border: 0; border-top: 1px solid #444;">`;
for (let key in feature.properties) {
const val = feature.properties[key];
if (val !== null && val !== undefined) popup += `<b>${key}:</b> ${val}<br>`;
}
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 = `<span style="color: #ff4444;">Fehler bei ${layerName}: ${e.message}</span>`;
}
}
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 = `<b>${name}</b><br><hr style="margin: 5px 0; border: 0; border-top: 1px solid #444;">`;
for (let key in feature.properties) {
const val = feature.properties[key];
if (val !== null && val !== undefined) popup += `<b>${key}:</b> ${val}<br>`;
}
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 = `<b style="color: #ff8800;">Manuelles Laden:</b> 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 += ` | <span style="color: #ff8800;">ALKIS-Layer Fehler</span>`;
}
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 = `<b>${layerName}</b><br><hr style="margin: 5px 0; border: 0; border-top: 1px solid #444;">`;
popup += `<b>Eigentümer:</b> ${firstName} ${lastName}<br>`;
popup += `<b>Status:</b> ${status}<br>`;
if (notiz) popup += `<b>Notiz:</b> ${notiz}<br>`;
popup += `<hr style="margin: 5px 0; border: 0; border-top: 1px solid #444;">`;
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 += `<b>${key}:</b> ${val}<br>`;
}
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 = `
<div class="alkis-label-inner">
<span class="owner-name">${lastName}, ${firstName}</span><br>
<span class="parcel-info">Flur ${flur}, Flst. ${fst}</span>
</div>
`;
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 => `<option value="${k}">${k}</option>`).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 = '<option value="">Keine Spalten gefunden</option>';
selectLastName.innerHTML = '<option value="">Keine Spalten gefunden</option>';
}
}
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 = `
<td><b>${displayLast}</b></td>
<td>${displayFirst}</td>
<td style="font-size: 0.75rem; opacity: 0.8;">${data.address}</td>
<td>${data.count} Flurstücke</td>
<td>
<select class="status-select" data-owner="${name}">
<option value="none" ${status === 'none' ? 'selected' : ''}>Kein Status</option>
${Object.keys(STATUS_MAP).map(s => `<option value="${s}" ${status === s ? 'selected' : ''}>${s}</option>`).join('')}
</select>
</td>
<td>
<input type="text" class="notiz-input" data-owner="${name}" value="${notiz}" placeholder="Notiz..." style="width: 100%; font-size: 0.75rem; padding: 4px; border: 1px solid var(--border-color); border-radius: 4px; background: transparent; color: white;">
</td>
`;
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 = `<span style="color: #ff8800;">Keine .shp Dateien gefunden.</span>`;
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 = `<span style="color: #2ecc71;">${result.message}</span>`;
} else {
throw new Error(result.error || "Fehler beim Speichern in DB");
}
} catch (err) {
console.error(err);
if (statusEl) statusEl.innerHTML = `<span style="color: #ff4444;">Auto-Save Fehler: ${err.message}</span>`;
}
}
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.";
});