From f8732b91d7423df7b9278e1a9e5107b77c2104c4 Mon Sep 17 00:00:00 2001 From: Johannes Baumeister Date: Sat, 4 Jul 2026 22:08:49 +0200 Subject: [PATCH] fix: JSON.parse of wea selector values (handle "); ensure mw is parsed correctly --- app.js | 624 +++++++++++++++++++-------------------------------------- 1 file changed, 203 insertions(+), 421 deletions(-) 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,