fix: JSON.parse of wea selector values (handle "); ensure mw is parsed correctly
Deploy Standortplaner / deploy (push) Successful in 17s Details

This commit is contained in:
Johannes Baumeister 2026-07-04 22:08:49 +02:00
parent 3ce303a00f
commit f8732b91d7
1 changed files with 203 additions and 421 deletions

624
app.js
View File

@ -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 = '<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 => {
@ -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(/&quot;/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(/&quot;/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 = `<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;">Speicherfehler: ${err.message}</span>`;
}
}
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,