diff --git a/app.js b/app.js
index a1e434b..49464a0 100644
--- a/app.js
+++ b/app.js
@@ -371,6 +371,201 @@ document.addEventListener('DOMContentLoaded', async () => {
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);
+ });
+// We'll use negative angle for CCW to achieve CW rotation if expected.
+
+ const transform = (relCoords) => {
+ // 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?.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 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;
+ let matched = false;
+ for (const opt of editWeaSelector.options) {
+ if (opt.value) {
+ try {
+ const data = JSON.parse(opt.value);
+ if (data.hersteller === turbine.hersteller && data.anlagentyp === turbine.type && data.nabenhoehe === turbine.hh) {
+ editWeaSelector.value = opt.value;
+ matched = true;
+ break;
+ }
+ } catch(e) {}
+ }
+ }
+ if (!matched) editWeaSelector.value = "";
+ editRd.value = turbine.rd;
+ editNh.value = turbine.hh;
+ editFr.value = turbine.fr || 15;
+ editKsfAngle.value = turbine.ksfAngle || 0;
+ editKsfMirrored.checked = !!turbine.ksfMirrored;
+
+ // Show rotation handle
+ variantLayers[turbine.variant].addLayer(turbine.layers.rotationHandle);
+
+ editPanel.style.display = 'block';
+ updateEditPanelPosition();
+
+ document.getElementById('statusInfo').innerText = `Bearbeite WEA ${turbine.nr}`;
+ }
+
+ function closeEditPanel() {
+ if (activeTurbine) {
+ variantLayers[activeTurbine.variant].removeLayer(activeTurbine.layers.rotationHandle);
+ }
+ activeTurbine = null;
+ editPanel.style.display = 'none';
+ document.getElementById('statusInfo').innerText = "Bereit.";
+ }
+
+ // Reposition floating panel on map movement
+ state.map.on('move zoom', updateEditPanelPosition);
+
+ // Legend Logic
+ function updateLegend() {
+ if (!legendContent) return;
+
+ let html = '
Anlagen-Geometrien
';
+ if (state.turbines.length > 0) {
+ html += `
+ Rotorfläche
+ Techn. Abstand (Ellipse)
+ Techn. Abstand (Circle)
+ Auflastenradius
+ Fundament
+ Kranstellfläche (KSF)
+ `;
+ } else {
+ html += 'Keine Anlagen gesetzt
';
+ }
+
+ html += 'Sicherungsstand (ALKIS)
';
+ Object.keys(STATUS_MAP).forEach(status => {
+ const data = STATUS_MAP[status];
+ html += `
+
+
+
+
${status}
+
${data.desc}
+
+
+ `;
+ });
+
+ legendContent.innerHTML = html;
+ }
+
+ // Toggle Legend collapse
+ legendHeader.addEventListener('click', () => {
+ floatingLegend.classList.toggle('collapsed');
+ btnToggleLegend.innerHTML = floatingLegend.classList.contains('collapsed') ? '▼' : '▲';
+ });
+
state.map.on('overlayadd overlayremove', updateLegend);
// Also update on variant change
variantTabs.forEach(tab => {
@@ -386,11 +581,13 @@ document.addEventListener('DOMContentLoaded', async () => {
const fr = overrideData?.fr || Number.parseFloat(inputFoundation?.value) || 15;
let type = overrideData?.type || "Standard Typ";
let hersteller = overrideData?.hersteller || "Unbekannt";
+ let mw = overrideData?.mw || null;
if (!overrideData && weaTypeSelector && weaTypeSelector.value) {
try {
- const parsed = JSON.parse(weaTypeSelector.value);
+ const parsed = JSON.parse(weaTypeSelector.value.replace(/"/g, '"'));
type = parsed.anlagentyp;
hersteller = parsed.hersteller;
+ mw = parsed.nennleistung;
} catch(e) {}
}
const weaNr = overrideData?.nr || loadedNr || (state.turbines.length + 1).toString();
@@ -418,7 +615,7 @@ document.addEventListener('DOMContentLoaded', async () => {
id: `WEA_${Date.now()}`,
nr: weaNr,
variant: overrideData?.variant || state.activeVariant,
- hersteller, type, rd, hh, fr, latlng,
+ hersteller, type, mw, rd, hh, fr, latlng,
ksfAngle, ksfMirrored,
totalHeight: geoms.totalHeight,
layers: {
@@ -555,11 +752,13 @@ document.addEventListener('DOMContentLoaded', async () => {
const newNr = editNr.value;
let newManufacturer = activeTurbine.hersteller;
let newType = activeTurbine.type;
+ let newMw = activeTurbine.mw;
if (editWeaSelector && editWeaSelector.value) {
try {
- const parsed = JSON.parse(editWeaSelector.value);
+ const parsed = JSON.parse(editWeaSelector.value.replace(/"/g, '"'));
newManufacturer = parsed.hersteller;
newType = parsed.anlagentyp;
+ newMw = parsed.nennleistung;
} catch(e) {}
}
const newRd = Number.parseFloat(editRd.value);
@@ -575,6 +774,7 @@ document.addEventListener('DOMContentLoaded', async () => {
activeTurbine.nr = newNr;
activeTurbine.hersteller = newManufacturer;
activeTurbine.type = newType;
+ activeTurbine.mw = newMw;
activeTurbine.rd = newRd;
activeTurbine.hh = newNh;
activeTurbine.fr = newFr;
@@ -2335,424 +2535,6 @@ document.addEventListener('DOMContentLoaded', async () => {
// Start loading process
await loadWeaTypes();
- initializeProjects();
-
- // 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 (state.isLoading) {
- console.log("Auto-Save unterdrückt: App befindet sich noch in der Ladephase.");
- return;
- }
- if (!state.hasLoadedFromDB) {
- console.warn("Auto-Save unterdrückt: Datenbank wurde beim Start nicht erfolgreich geladen.");
- return;
- }
- if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
- autoSaveTimeout = setTimeout(() => {
- saveTurbinesToDB(false);
- }, 1500); // 1.5 Seconds debounce
- }
-
- async function saveTurbinesToDB(force = false) {
- if (state.isLoading && !force) {
- console.log("Speichern unterdrückt: Ladephase aktiv.");
- return;
- }
- if (!state.hasLoadedFromDB && !force) {
- console.warn("Speichern unterdrückt: Datenbank wurde beim Start nicht geladen.");
- return;
- }
-
- if (!state.hasLoadedFromDB && force) {
- const confirmSave = confirm("WARNUNG: Die Datenbank wurde beim Start nicht erfolgreich geladen. Wenn Sie jetzt speichern, überschreiben Sie eventuell vorhandene Standorte in der Datenbank. Möchten Sie trotzdem speichern?");
- if (!confirmSave) return;
- }
-
- const statusEl = document.getElementById('statusInfo');
- if (statusEl) statusEl.innerText = "Speicherung läuft...";
-
- const projekt_id = getProjektId();
- const turbineData = state.turbines.map(t => ({
- nr: t.nr,
- variant: t.variant,
- hersteller: t.hersteller,
- type: t.type,
- rd: t.rd,
- hh: t.hh,
- ksfAngle: t.ksfAngle,
- latlng: t.layers.marker.getLatLng()
- }));
-
- try {
- const response = await fetch('/api/wea', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ projekt_id, turbines: turbineData })
- });
-
- const result = await response.json();
- if (response.ok) {
- if (statusEl) statusEl.innerHTML = `${result.message}`;
- } else {
- throw new Error(result.error || "Fehler beim Speichern in DB");
- }
- } catch (err) {
- console.error(err);
- if (statusEl) statusEl.innerHTML = `Speicherfehler: ${err.message}`;
- }
- }
-
- const btnSaveDB = document.getElementById('btnSaveDB');
- if (btnSaveDB) {
- btnSaveDB.addEventListener('click', () => saveTurbinesToDB(true));
- }
-
- async function loadTurbinesFromDB() {
- const projekt_id = getProjektId();
- 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.`);
-
- // 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) {
- console.error("Error clearing existing layers:", e);
- }
- });
- state.turbines = [];
-
- if (dbTurbines && dbTurbines.length > 0) {
- 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: Number.parseFloat(t.rd) || 160,
- hh: Number.parseFloat(t.hh) || 165,
- ksfAngle: Number.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.");
- }
- state.hasLoadedFromDB = true;
- } catch (err) {
- console.error("GLOBALER FEHLER beim Laden aus der DB:", err);
- if (statusEl) statusEl.innerText = "Fehler beim Laden der Standorte.";
- state.hasLoadedFromDB = false;
- }
- }
-
- btnLoad.addEventListener('click', () => projectInput.click());
-
- projectInput.addEventListener('change', async (e) => {
- const file = e.target.files[0];
- if (!file) return;
-
- try {
- const text = await file.text();
- const data = JSON.parse(text);
-
- // 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)
- if (inputType) inputType.value = tData.type;
- if (inputRotor) inputRotor.value = tData.rd;
- if (inputHub) 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.");
- }
- });
-
- // 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.setLatLng(latlng);
- userAccuracyCircle.setLatLng(latlng);
- userAccuracyCircle.setRadius(accuracy);
- } else {
- 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);
- }
-
- document.getElementById('statusInfo').innerText = `Standort aktualisiert (Genauigkeit: ${accuracy.toFixed(0)}m)`;
- }, (err) => {
- console.error("Geolocation error code:", err.code, err.message);
- let msg = "Fehler bei der Standorterkennung.";
- if (err.code === 1) msg = "Standortzugriff verweigert. Bitte erlauben Sie den Zugriff in den Browser-Einstellungen.";
- else if (err.code === 2) msg = "Standort nicht verfügbar. Prüfen Sie, ob GPS/Ortungsdienste am Gerät aktiviert sind.";
- else if (err.code === 3) msg = "Zeitüberschreitung bei der Standorterkennung.";
-
- alert(msg);
- stopTracking();
- }, {
- enableHighAccuracy: true,
- timeout: 30000,
- maximumAge: 30000
- });
- }
-
- if (btnLocate) {
- btnLocate.onclick = () => {
- if (watchId === null) {
- startTracking();
- } else {
- stopTracking();
- }
- };
- }
-
- // Initialize Edit Mode Toggle
- const btnToggleEditMode = document.getElementById('btnToggleEditMode');
- const editModeIcon = document.getElementById('editModeIcon');
- const editModeText = document.getElementById('editModeText');
-
- function updateEditModeUI() {
- if (state.isEditMode) {
- editModeIcon.innerText = '🔓';
- editModeText.innerText = 'Bearbeitung aktiv';
- btnToggleEditMode.classList.replace('btn-secondary', 'btn-primary');
- btnPlaceTurbine.disabled = false;
- btnPlaceTurbine.style.opacity = '1';
- btnCreateAtUTM.disabled = false;
- btnCreateAtUTM.style.opacity = '1';
- if (btnManageOwners) {
- btnManageOwners.disabled = false;
- btnManageOwners.style.opacity = '1';
- }
- } else {
- editModeIcon.innerText = '🔒';
- editModeText.innerText = 'Bearbeitung gesperrt';
- btnToggleEditMode.classList.replace('btn-primary', 'btn-secondary');
- btnPlaceTurbine.disabled = true;
- btnPlaceTurbine.style.opacity = '0.5';
- btnCreateAtUTM.disabled = true;
- btnCreateAtUTM.style.opacity = '0.5';
- if (btnManageOwners) {
- btnManageOwners.disabled = true;
- btnManageOwners.style.opacity = '0.5';
- }
-
- closeEditPanel();
- if (ownerModal) ownerModal.style.display = 'none';
- if (placementMode) {
- placementMode = false;
- btnPlaceTurbine.classList.remove('active');
- state.map.getContainer().style.cursor = '';
- state.map.getContainer().classList.remove('placement-active');
- }
- }
-
- state.turbines.forEach(t => {
- if (state.isEditMode) {
- t.layers.marker.dragging.enable();
- t.layers.rotationHandle.dragging.enable();
- } else {
- t.layers.marker.dragging.disable();
- t.layers.rotationHandle.dragging.disable();
- }
- });
- }
-
- if (btnToggleEditMode) {
- btnToggleEditMode.addEventListener('click', () => {
- state.isEditMode = !state.isEditMode;
- updateEditModeUI();
- });
- // Set initial state (locked)
- updateEditModeUI();
- }
-
- updateLegend();
- console.log("WindPlaner initialisiert.");
- const btnExportCoordsPDF = document.getElementById('btnExportCoordsPDF');
- if (btnExportCoordsPDF) {
- btnExportCoordsPDF.addEventListener('click', async () => {
- const originalText = btnExportCoordsPDF.innerText;
- btnExportCoordsPDF.innerText = 'Exportiere...';
- btnExportCoordsPDF.disabled = true;
-
- try {
- // Finde aktive Variante
- const activeVariantNode = document.querySelector('.variant-tab.active');
- const activeVariant = activeVariantNode ? activeVariantNode.dataset.variant : 'variant1';
-
- // Filtere Anlagen, die zur aktuellen Variante gehören
- const activeTurbines = state.turbines.filter(t => t.variant === activeVariant);
-
- if (activeTurbines.length === 0) {
- alert('Keine Anlagen in der aktuellen Variante gefunden.');
- return;
- }
-
- // Sort by WEA Nr (numerically if possible)
- activeTurbines.sort((a, b) => {
- const numA = parseInt(a.nr) || 0;
- const numB = parseInt(b.nr) || 0;
- return numA - numB;
- });
-
- // Initialisiere jsPDF
- const { jsPDF } = window.jspdf;
- const doc = new jsPDF();
-
- // Titel und Projektname
- const projNameForFile = document.getElementById('projectSelector')?.options[document.getElementById('projectSelector')?.selectedIndex]?.text || 'Projekt unbekannt';
-
- doc.setFontSize(16);
- doc.text(`Koordinatenliste`, 14, 20);
-
- // Untertitel (Variante)
- doc.setFontSize(12);
- let variantName = activeVariant === 'variant1' ? 'Variante 1' : activeVariant === 'variant2' ? 'Variante 2' : 'Variante 3';
- doc.text(`Variante: ${variantName}`, 14, 30);
-
- // Daten vorbereiten
- const tableData = activeTurbines.map(t => {
- const latlng = t.layers.marker.getLatLng();
- const utmCoords = proj4(wgs84, utm32, [latlng.lng, latlng.lat]);
-
- // Nennleistung aus loaded types finden
- let nennleistung = '-';
- if (window.availableWeaTypes) {
- const matchedType = window.availableWeaTypes.find(w => w.hersteller === t.hersteller && w.anlagentyp === t.type);
- if (matchedType && matchedType.nennleistung) {
- nennleistung = matchedType.nennleistung;
- }
- }
-
- const anlagentyp = t.type === 'Standard Typ' ? '-' : (t.type || '-');
-
- return [
t.nr,
t.hersteller || '-',
anlagentyp,