2172 lines
94 KiB
JavaScript
2172 lines
94 KiB
JavaScript
/**
|
||
* 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') {
|
||
console.log(`Berechne Geometrien für ${hersteller}, RD=${rotorDiameter}, HH=${hubHeight}`);
|
||
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 isNordex = hersteller && hersteller.toLowerCase() === 'nordex';
|
||
const spg = (ksfMirrored !== isNordex) ? -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 && hersteller.toLowerCase() === 'nordex') {
|
||
// Nordex Geometries (Based on new QGIS schema provided by user)
|
||
// KSF (Red): Complex polygon
|
||
ksfCoords = [
|
||
[18, -44.72], [18, 14.91], [-6.31, 14.91], [-18.38, 0],
|
||
[-18.38, -33.19], [-7, -44.72], [18, -44.72]
|
||
];
|
||
|
||
// AMF (Green Area): 180m long starting at KSF edge, 15m wide
|
||
const amf = [
|
||
[18, -44.72], [18, -224.72], [3, -224.72], [3, -44.72], [18, -44.72]
|
||
];
|
||
mfParts = [amf];
|
||
|
||
// BLF/Road (Blue Area): Complex unified shape
|
||
blfCoords = [
|
||
[18, -224.72], [18, 40], [49.5, 40], [49.5, -100],
|
||
[24, -100], [24, -224.72], [18, -224.72]
|
||
];
|
||
} 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]]
|
||
];
|
||
}
|
||
|
||
try {
|
||
const blf = transform(blfCoords);
|
||
const ksf = transform(ksfCoords);
|
||
|
||
// Create the feature collection of polygons
|
||
const mf = {
|
||
type: "FeatureCollection",
|
||
features: mfParts.map(part => transform(part))
|
||
};
|
||
|
||
return { sweptArea, techDist, techDistSmall, loadRadius, foundation, blf, ksf, mf, totalHeight, utmCoords };
|
||
} catch (err) {
|
||
console.error("Fehler in calculateGeometries:", err);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
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');
|
||
state.map.removeLayer(variantLayers[state.activeVariant]);
|
||
state.activeVariant = tab.dataset.variant;
|
||
variantLayers[state.activeVariant].addTo(state.map);
|
||
updateProximityLines();
|
||
closeEditPanel();
|
||
document.getElementById('statusInfo').innerText = `Variante ${state.activeVariant} aktiv.`;
|
||
});
|
||
});
|
||
|
||
// Sidebar Toggle Logic
|
||
const btnToggleSidebar = document.getElementById('btnToggleSidebar');
|
||
const sidebar = document.querySelector('.sidebar');
|
||
if (btnToggleSidebar && sidebar) {
|
||
btnToggleSidebar.onclick = () => {
|
||
sidebar.classList.toggle('collapsed');
|
||
// Give the transition time to finish before invalidating map size
|
||
setTimeout(() => {
|
||
state.map.invalidateSize();
|
||
}, 300);
|
||
};
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
};
|
||
}
|
||
|
||
// Eigentümerzustimmung Toggle
|
||
const checkShowOwners = document.getElementById('checkShowOwners');
|
||
if (checkShowOwners) {
|
||
checkShowOwners.onchange = () => {
|
||
const ownerLayer = overlays["Eigentümer (ALKIS DB)"];
|
||
if (ownerLayer) {
|
||
if (checkShowOwners.checked) {
|
||
state.map.addLayer(ownerLayer);
|
||
} else {
|
||
state.map.removeLayer(ownerLayer);
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// 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: true }).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;
|
||
|
||
// Only add to map if checkbox is checked
|
||
const checkShowOwners = document.getElementById('checkShowOwners');
|
||
if (!checkShowOwners || checkShowOwners.checked) {
|
||
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');
|
||
const ownerStatusFilter = document.getElementById('ownerStatusFilter');
|
||
|
||
// Populate Status Filter Options
|
||
if (ownerStatusFilter) {
|
||
ownerStatusFilter.innerHTML = `
|
||
<option value="">Alle Status</option>
|
||
<option value="HAS_STATUS">Nur mit Status</option>
|
||
${Object.keys(STATUS_MAP).map(s => `<option value="${s}">${s}</option>`).join('')}
|
||
`;
|
||
}
|
||
|
||
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);
|
||
|
||
const runFilter = () => {
|
||
const query = ownerSearch.value.toLowerCase();
|
||
const statusFilter = ownerStatusFilter.value;
|
||
const filtered = {};
|
||
|
||
for (let name in owners) {
|
||
const o = owners[name];
|
||
const stored = state.ownerStatuses[name.toLowerCase()] || { status: 'none' };
|
||
const currentStatus = typeof stored === 'object' ? (stored.status || 'none') : (stored || 'none');
|
||
|
||
const matchesQuery = name.toLowerCase().includes(query) || (o.address && o.address.toLowerCase().includes(query));
|
||
let matchesStatus = !statusFilter || currentStatus === statusFilter;
|
||
|
||
if (statusFilter === 'HAS_STATUS') {
|
||
matchesStatus = currentStatus !== 'none' && currentStatus !== '';
|
||
}
|
||
|
||
if (matchesQuery && matchesStatus) {
|
||
filtered[name] = o;
|
||
}
|
||
}
|
||
renderOwnerRows(filtered);
|
||
};
|
||
|
||
ownerSearch.oninput = runFilter;
|
||
ownerStatusFilter.onchange = runFilter;
|
||
}
|
||
|
||
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) {
|
||
throw new Error(`HTTP-Fehler! Status: ${response.status}`);
|
||
}
|
||
const dbTurbines = await response.json();
|
||
console.log(`Datenbank-Response: ${dbTurbines.length} WEAs erhalten.`);
|
||
|
||
if (dbTurbines.length > 0) {
|
||
// Clear existing turbines safely
|
||
state.turbines.forEach(t => {
|
||
try {
|
||
if (t.layers && t.variant && variantLayers[t.variant]) {
|
||
Object.values(t.layers).forEach(l => variantLayers[t.variant].removeLayer(l));
|
||
}
|
||
} catch (e) {}
|
||
});
|
||
state.turbines = [];
|
||
|
||
dbTurbines.forEach(t => {
|
||
try {
|
||
const latlng = L.latLng(t.lat, t.lng);
|
||
const variant = t.variant || 'A';
|
||
|
||
if (!variantLayers[variant]) {
|
||
console.warn(`Ungültige Variante: ${variant}`);
|
||
return;
|
||
}
|
||
|
||
createTurbine(latlng, null, {
|
||
nr: t.nr,
|
||
hersteller: t.hersteller,
|
||
type: t.type,
|
||
rd: parseFloat(t.rd) || 160,
|
||
hh: parseFloat(t.hh) || 165,
|
||
ksfAngle: parseFloat(t.ksfangle ?? t.ksfAngle ?? 0),
|
||
variant: variant
|
||
});
|
||
} catch (e) {
|
||
console.error(`Fehler bei WEA ${t.nr}:`, e);
|
||
}
|
||
});
|
||
statusEl.innerText = `${dbTurbines.length} WEAs aus Datenbank geladen.`;
|
||
} else {
|
||
console.log("Datenbank ist leer.");
|
||
}
|
||
} catch (err) {
|
||
console.error("GLOBALER FEHLER beim Laden aus der DB:", err);
|
||
if (statusEl) statusEl.innerText = "Fehler beim Laden der Standorte.";
|
||
}
|
||
}
|
||
|
||
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);
|
||
});
|
||
|
||
// Geolocation Logic
|
||
let watchId = null;
|
||
let userMarker = null;
|
||
let userAccuracyCircle = null;
|
||
const btnLocate = document.getElementById('btnLocate');
|
||
|
||
function stopTracking() {
|
||
if (watchId !== null) {
|
||
navigator.geolocation.clearWatch(watchId);
|
||
watchId = null;
|
||
}
|
||
if (userMarker) state.map.removeLayer(userMarker);
|
||
if (userAccuracyCircle) state.map.removeLayer(userAccuracyCircle);
|
||
userMarker = null;
|
||
userAccuracyCircle = null;
|
||
btnLocate.classList.remove('active');
|
||
document.getElementById('statusInfo').innerText = "Standortverfolgung beendet.";
|
||
}
|
||
|
||
function startTracking() {
|
||
if (!("geolocation" in navigator)) {
|
||
alert("Geolocation wird von Ihrem Browser nicht unterstützt.");
|
||
return;
|
||
}
|
||
|
||
btnLocate.classList.add('active');
|
||
document.getElementById('statusInfo').innerText = "Suche Standort...";
|
||
|
||
watchId = navigator.geolocation.watchPosition((position) => {
|
||
const { latitude, longitude, accuracy } = position.coords;
|
||
const latlng = L.latLng(latitude, longitude);
|
||
|
||
if (!userMarker) {
|
||
userMarker = L.marker(latlng, {
|
||
icon: L.divIcon({
|
||
className: 'user-location-marker pulse',
|
||
iconSize: [16, 16],
|
||
iconAnchor: [8, 8]
|
||
})
|
||
}).addTo(state.map);
|
||
|
||
userAccuracyCircle = L.circle(latlng, {
|
||
radius: accuracy,
|
||
className: 'user-location-accuracy'
|
||
}).addTo(state.map);
|
||
|
||
// Center on first found position
|
||
state.map.setView(latlng, 16);
|
||
} else {
|
||
userMarker.setLatLng(latlng);
|
||
userAccuracyCircle.setLatLng(latlng);
|
||
userAccuracyCircle.setRadius(accuracy);
|
||
}
|
||
|
||
document.getElementById('statusInfo').innerText = `Standort aktualisiert (Genauigkeit: ${accuracy.toFixed(0)}m)`;
|
||
}, (err) => {
|
||
console.error("Geolocation error:", err);
|
||
let msg = "Fehler bei der Standorterkennung.";
|
||
if (err.code === 1) msg = "Standortzugriff verweigert.";
|
||
else if (err.code === 2) msg = "Standort nicht verfügbar.";
|
||
else if (err.code === 3) msg = "Zeitüberschreitung.";
|
||
|
||
alert(msg);
|
||
stopTracking();
|
||
}, {
|
||
enableHighAccuracy: true,
|
||
timeout: 10000,
|
||
maximumAge: 0
|
||
});
|
||
}
|
||
|
||
if (btnLocate) {
|
||
btnLocate.onclick = () => {
|
||
if (watchId === null) {
|
||
startTracking();
|
||
} else {
|
||
stopTracking();
|
||
}
|
||
};
|
||
}
|
||
|
||
updateLegend();
|
||
console.log("WindPlaner initialisiert.");
|
||
document.getElementById('statusInfo').innerText = "System bereit. Karte geladen.";
|
||
});
|