1762 lines
76 KiB
JavaScript
1762 lines
76 KiB
JavaScript
/**
|
|
* WindPlaner - Core Logic
|
|
*/
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// Basic App State
|
|
const state = {
|
|
map: null,
|
|
config: null,
|
|
turbines: [],
|
|
activeVariant: 'A',
|
|
bakedData: {}, // Cache for standalone persistence
|
|
ownerMapping: null, // { firstName: '', lastName: '' }
|
|
ownerStatuses: {}, // { "Name Vorname": "status" }
|
|
showAuxiliary: true
|
|
};
|
|
|
|
// 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 = `
|
|
<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;
|
|
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;
|
|
|
|
// Items to always show if any turbine exists
|
|
let html = '';
|
|
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>
|
|
`;
|
|
}
|
|
|
|
// Search for active external layers
|
|
Object.keys(overlays).forEach(name => {
|
|
const layer = overlays[name];
|
|
if (state.map.hasLayer(layer)) {
|
|
// Determine color (heuristic or from layer style)
|
|
let color = '#ccc';
|
|
if (name.includes('Eigentümer')) color = '#2ecc71';
|
|
if (name.includes('Hilfs')) color = '#ffcc00';
|
|
|
|
html += `<div class="legend-item"><span class="color-box" style="background: ${color}; opacity: 0.8;"></span> ${name}</div>`;
|
|
}
|
|
});
|
|
|
|
legendContent.innerHTML = html || '<div style="font-size: 0.75rem; color: var(--text-dim); text-align: center;">Keine aktiven Layer</div>';
|
|
}
|
|
|
|
// 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: `
|
|
<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,
|
|
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.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 = `
|
|
<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 © 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;
|
|
|
|
// Owner-Status-Coloring
|
|
if (layerName.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().toLowerCase();
|
|
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>${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.";
|
|
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 = `<b style="color: #ff8800;">Manuelles Laden:</b> 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);
|
|
}
|
|
}
|
|
statusEl.innerText = "Alle konfigurierten Layer geladen.";
|
|
} catch (e) {
|
|
if (!isLocalFile) console.error("Layer-Init fehlgeschlagen:", e);
|
|
}
|
|
}
|
|
|
|
// 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 => `<option value="${k}">${k}</option>`).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 = '<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";
|
|
|
|
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 status = state.ownerStatuses[name] || 'none';
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td><b>${name}</b></td>
|
|
<td>${data.count} Flurstücke</td>
|
|
<td>
|
|
<select class="status-select" data-owner="${name}">
|
|
<option value="none" ${status === 'none' ? 'selected' : ''}>Kein Status</option>
|
|
<option value="gbr" ${status === 'gbr' ? 'selected' : ''}>Mitglied der GbR</option>
|
|
<option value="external" ${status === 'external' ? 'selected' : ''}>Fremdplanung</option>
|
|
<option value="declined" ${status === 'declined' ? 'selected' : ''}>Ablehnend</option>
|
|
<option value="positive" ${status === 'positive' ? 'selected' : ''}>Positiv</option>
|
|
<option value="undecided" ${status === 'undecided' ? 'selected' : ''}>Unentschlossen</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<button class="btn-secure" data-first="${data.first}" data-last="${data.last}" style="padding: 4px 8px; font-size: 0.75rem; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer;">Sichern</button>
|
|
</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; // original name for display
|
|
const status = e.target.value;
|
|
state.ownerStatuses[name.toLowerCase()] = status;
|
|
|
|
// Sync with DB
|
|
const data = owners[name];
|
|
if (data) {
|
|
await secureOwner(data.first, data.last, e.target, status);
|
|
}
|
|
|
|
// Refresh map style
|
|
const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer'));
|
|
if (ownerLayerName) {
|
|
overlays[ownerLayerName].setStyle(overlays[ownerLayerName].options.style);
|
|
}
|
|
};
|
|
});
|
|
|
|
// 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;
|
|
await secureOwner(first, last, e.target);
|
|
};
|
|
});
|
|
}
|
|
|
|
async function secureOwner(vorname, nachname, element, status = 'Gesichert') {
|
|
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 })
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (response.ok) {
|
|
if (isButton) {
|
|
element.style.background = '#2ecc71';
|
|
element.innerText = "✓ Gesichert";
|
|
}
|
|
console.log(result.message);
|
|
|
|
const fullName = `${vorname || ''} ${nachname || ''}`.trim();
|
|
if (fullName) {
|
|
state.ownerStatuses[fullName.toLowerCase()] = status;
|
|
}
|
|
|
|
const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer'));
|
|
if (ownerLayerName) {
|
|
overlays[ownerLayerName].setStyle(overlays[ownerLayerName].options.style);
|
|
}
|
|
|
|
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 = `<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 statuses = await response.json();
|
|
statuses.forEach(s => {
|
|
const first = s.vorname || '';
|
|
const last = s.nachname || '';
|
|
const fullName = `${first} ${last}`.trim().toLowerCase();
|
|
if (fullName) {
|
|
state.ownerStatuses[fullName] = s.status;
|
|
}
|
|
});
|
|
console.log(`${statuses.length} Eigentümer-Status geladen.`);
|
|
|
|
// Refresh map style if owner layer exists
|
|
const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer'));
|
|
if (ownerLayerName && overlays[ownerLayerName]) {
|
|
overlays[ownerLayerName].setStyle(overlays[ownerLayerName].options.style);
|
|
}
|
|
} 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 = `<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,
|
|
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.";
|
|
});
|