diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 6812170..6d62101 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -31,7 +31,7 @@ jobs: echo "DB_USER=${{ secrets.USER }}" >> .env echo "DB_PASSWORD='${{ secrets.PASSWORD }}'" >> .env echo "DB_NAME=${{ secrets.NAME }}" >> .env - echo "DB_SCHEMA=bw_scheddebrock" >> .env + echo "DB_SCHEMA=wind_projekt_bwscheddebrock" >> .env docker compose up -d --build --force-recreate docker image prune -f diff --git a/docker-compose.yml b/docker-compose.yml index 216dd7b..8bb6747 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: - DB_USER=authentik - DB_PASSWORD=WX1t1cgP1qK09 - DB_NAME=authentik - - DB_SCHEMA=bw_scheddebrock + - DB_SCHEMA=wind_projekt_bwscheddebrock networks: - proxy labels: diff --git a/index.html b/index.html index d54459b..d018046 100644 --- a/index.html +++ b/index.html @@ -6,15 +6,15 @@ TrassenPlaner Pro - - - - - - - - - + + + + + + + + + @@ -837,8 +837,12 @@ doc.setFontSize(9); const footerText = "Dieses Dokument wurde vom Auftragnehmer erstellt und aus dem Planungstool automatisch generiert."; - const latLngs = getFlattenedCoords(v.routes); - const lineStr = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat])); + const nested = getNestedCoords(v.routes); + if (nested.length === 0) return; + const lines = nested.filter(s => s.length >= 2).map(s => turf.lineString(s.map(ll => [ll.lng, ll.lat]))); + if (lines.length === 0) return; + + const lineStr = lines.length === 1 ? lines[0] : turf.multiLineString(lines.map(l => l.geometry.coordinates)); const lineBbox = turf.bbox(lineStr); const tableData = []; @@ -1128,21 +1132,24 @@ const variant = state.variants.find(v => routeLayers[v.id] === layer); if (variant) { - const liveCoords = getFlattenedCoords(layer.getLatLngs()); const tempPt = (e.type === 'editable:drawing:move') ? e.latlng : null; - const isContinuous = ['editable:vertex:drag', 'editable:drawing:move', 'editable:drag', 'editable:editing'].includes(e.type); - // Always make sure variant.routes is up to date during dragging to allow calculateStats and labels to function properly + // Single-path update if (isContinuous) { - renderSegmentLabels(variant, liveCoords, tempPt); + try { + variant.routes = layer.getLatLngs(); + renderSegmentLabels(variant, variant.routes, tempPt); + calculateStats(variant); + updateVariantStatsUI(variant); + } catch (err) { console.warn("Live render error:", err); } } else { - variant.routes = liveCoords; + variant.routes = layer.getLatLngs(); calculateStats(variant); updateVariantStatsUI(variant); renderVariants(); if (variant.active) updateRequiredPlots(variant); - const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(routeLayers[_activeV.id] || _activeV); + saveVariantToDB(variant); } } } catch (err) { @@ -1710,6 +1717,7 @@ weight: 6, opacity: 1, editable: true, + middleMarkers: true, pane: 'trassenPane', lineOptions: { color: '#cca300', weight: 6 }, vertexOptions: { color: '#cca300', radius: 5 }, @@ -1741,15 +1749,16 @@ // Z-Index: Active variant to front if (v.active) { - layer.bringToFront(); + if (layer.bringToFront) layer.bringToFront(); if (drillingLayers[v.id]) drillingLayers[v.id].bringToFront(); if (labelLayers[v.id]) labelLayers[v.id].bringToFront(); } - // Sync geometry if not actively being dragged/edited - if (!v.active) { + // Direct sync for single LineString + const isDrawing = v.active && map.editTools && map.editTools.drawing(); + if (!isDrawing) { try { - layer.setLatLngs(v.routes); + layer.setLatLngs(v.routes || []); } catch (e) { console.warn("Geometrie-Sync Warnung:", e); } @@ -1764,41 +1773,27 @@ // Labels aktualisieren (Name in der Mitte der Linie auf weißem Hintergrund) labelLayer.clearLayers(); - if (v.routes && v.routes.length > 1) { - // Find geometric middle of the polyline - const points = layer.getLatLngs(); - let totalDist = 0; - const distances = []; - for (let i = 0; i < points.length - 1; i++) { - const d = map.distance(points[i], points[i+1]); - distances.push(d); - totalDist += d; - } + const latLngs = (v.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(ll => !!ll && typeof ll.lat === 'number'); + if (latLngs.length >= 2) { + try { + const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat])); + const totalKm = turf.length(line, { units: 'kilometers' }); + const midPoint = turf.along(line, totalKm / 2, { units: 'kilometers' }); + const midPos = L.latLng(midPoint.geometry.coordinates[1], midPoint.geometry.coordinates[0]); - let currentDist = 0; - let midPos = points[0]; - for (let i = 0; i < distances.length; i++) { - if (currentDist + distances[i] >= totalDist / 2) { - // Midpoint is on this segment - const ratio = (totalDist / 2 - currentDist) / distances[i]; - const lat = points[i].lat + (points[i+1].lat - points[i].lat) * ratio; - const lng = points[i].lng + (points[i+1].lng - points[i].lng) * ratio; - midPos = L.latLng(lat, lng); - break; - } - currentDist += distances[i]; + L.marker(midPos, { + pane: 'labelPane', + icon: L.divIcon({ + className: 'variant-map-label', + html: `
${v.name}
`, + iconSize: [0, 0], + iconAnchor: [0, 0] + }), + interactive: false + }).addTo(labelLayer); + } catch (err) { + console.warn("Label positioning failed:", err); } - - L.marker(midPos, { - pane: 'labelPane', - icon: L.divIcon({ - className: 'variant-map-label', - html: `
${v.name}
`, - iconSize: [0, 0], - iconAnchor: [0, 0] - }), - interactive: false - }).addTo(labelLayer); } // Sync edit state @@ -1853,6 +1848,22 @@ renderVariants(); }); + function isClickOnUI(e) { + if (!e || !e.originalEvent) return false; + const sidebar = document.getElementById('sidebar'); + const rightPanel = document.getElementById('right-panel'); + const target = e.originalEvent.target; + const isMarkerLabel = target.closest && (target.closest('.variant-map-label') || target.closest('.segment-label')); + return ((sidebar && sidebar.contains(target)) || (rightPanel && rightPanel.contains(target))) && !isMarkerLabel; + } + + map.on('click', (e) => { + if (isClickOnUI(e)) return; + if (state.isMeasuring) { + addMeasurementPoint(e.latlng); + } + }); + function updateToolCursors() { const container = map.getContainer(); if (state.isDrawing) container.style.cursor = 'crosshair'; @@ -1863,59 +1874,48 @@ document.getElementById('btn-measure').className = state.isMeasuring ? 'btn btn-primary' : 'btn btn-outline'; } - map.on('click', (e) => { - if (state.isMeasuring) { - addMeasurementPoint(e.latlng); - } - }); - // Capture changes (drawing, dragging, inserting, deleting) map.on('editable:drawing:clicked editable:drawing:move editable:drawing:end editable:created editable:vertex:drag editable:vertex:dragend editable:vertex:deleted editable:vertex:inserted', (e) => { const activeV = state.variants.find(v => v.active); if (!activeV) return; - // Enforce color on everything newly created or edited + // Enforce color if (e.layer && e.layer.setStyle) { e.layer.setStyle({ color: '#cca300', weight: 6 }); } - // If a new layer was created (via startPolyline or startNewPath), merge it into our existing layer - if (e.type === 'editable:created' && activeV && routeLayers[activeV.id] && e.layer !== routeLayers[activeV.id]) { - const current = getNestedCoords(routeLayers[activeV.id].getLatLngs()); - const addition = e.layer.getLatLngs(); - const merged = [...current, addition]; - - routeLayers[activeV.id].setLatLngs(merged); - activeV.routes = merged; - - map.removeLayer(e.layer); // Clean up temp layer - routeLayers[activeV.id].enableEdit(); - } else if (routeLayers[activeV.id] && e.layer === routeLayers[activeV.id]) { + // Sync logic for single line + if (activeV && routeLayers[activeV.id] && e.layer === routeLayers[activeV.id]) { activeV.routes = e.layer.getLatLngs(); } - // Live updates for labels, drillings and stats calculateStats(activeV); updateVariantStatsUI(activeV); - if (e.type.includes('end') || e.type === 'editable:created') { + updateVariantStatsUI(activeV); + if (e.type.includes('end') || e.type === 'editable:created' || e.type.includes('dragend')) { renderVariants(); - const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV); + const _activeV = state.variants.find(v => v.active); + if (_activeV) saveVariantToDB(_activeV); } }); function getFlattenedCoords(raw) { if (!raw) return []; - // If it's a flat array of LatLngs - if (raw.length > 0 && (raw[0].lat !== undefined || (Array.isArray(raw[0]) && typeof raw[0][0] === 'number'))) { + // If it's a flat array of LatLngs already + if (raw.length > 0 && raw[0] && (raw[0].lat !== undefined || (Array.isArray(raw[0]) && typeof raw[0][0] === 'number'))) { return raw.map(ll => L.latLng(ll)); } - // If it's nested + // If it's nested [[LatLng...], [LatLng...]] const flattened = []; - raw.forEach(section => { - if (Array.isArray(section)) { - section.forEach(ll => flattened.push(L.latLng(ll))); - } - }); + if (Array.isArray(raw)) { + raw.forEach(section => { + if (Array.isArray(section)) { + section.forEach(ll => { + if (ll) flattened.push(L.latLng(ll)); + }); + } + }); + } return flattened; } @@ -1923,10 +1923,20 @@ if (!raw) return []; // Check if it's already nested if (raw.length > 0 && Array.isArray(raw[0]) && raw[0].length > 0 && (raw[0][0].lat !== undefined || Array.isArray(raw[0][0]))) { - return raw.map(section => section.map(ll => L.latLng(ll))); + return raw.map(section => { + if (!Array.isArray(section)) return []; + return section.map(ll => { + try { return L.latLng(ll); } catch(e) { return null; } + }).filter(ll => !!ll && typeof ll.lat === 'number'); + }).filter(s => s.length > 0); } // If it's flat, wrap it - if (raw.length > 0) return [raw.map(ll => L.latLng(ll))]; + if (raw.length > 0) { + const flat = raw.map(ll => { + try { return L.latLng(ll); } catch(e) { return null; } + }).filter(ll => !!ll && typeof ll.lat === 'number'); + return flat.length > 0 ? [flat] : []; + } return []; } @@ -1935,47 +1945,49 @@ if (!vLabel) return; if (!variant.visible) { vLabel.clearLayers(); return; } - const nested = coordsInput ? [coordsInput] : getNestedCoords(variant.routes); + const latLngs = (coordsInput || variant.routes || []).map(ll => { + try { return L.latLng(ll); } catch(e) { return null; } + }).filter(p => !!p && typeof p.lat === 'number'); + + if (tempPt) latLngs.push(L.latLng(tempPt)); + const currentMarkers = vLabel.getLayers(); let markerIdx = 0; - nested.forEach((latLngs, sectionIdx) => { - const drawLatLngs = [...latLngs]; - // Only add temp point to the very LAST section if we are actively drawing - if (tempPt && sectionIdx === nested.length - 1) drawLatLngs.push(L.latLng(tempPt)); + if (latLngs.length < 2) { + vLabel.clearLayers(); + return; + } - if (drawLatLngs.length < 2) return; + for (let i = 0; i < latLngs.length - 1; i++) { + const p1 = latLngs[i]; + const p2 = latLngs[i + 1]; + const dist = map.distance(p1, p2); + const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2); + const labelText = `${dist.toFixed(0)}m`; - for (let i = 0; i < drawLatLngs.length - 1; i++) { - const p1 = drawLatLngs[i]; - const p2 = drawLatLngs[i + 1]; - const dist = map.distance(p1, p2); - const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2); - const labelText = `${dist.toFixed(0)}m`; - - if (markerIdx < currentMarkers.length) { - const m = currentMarkers[markerIdx]; - m.setLatLng(mid); - const el = m.getElement(); - if (el) { - const span = el.querySelector('.segment-label-content'); - if (span) span.textContent = labelText; - } - } else { - L.marker(mid, { - interactive: false, - pane: 'labelPane', - icon: L.divIcon({ - className: 'segment-label', - html: `${labelText}`, - iconSize: [46, 20], - iconAnchor: [23, 10] - }) - }).addTo(vLabel); + if (markerIdx < currentMarkers.length) { + const m = currentMarkers[markerIdx]; + m.setLatLng(mid); + const el = m.getElement(); + if (el) { + const span = el.querySelector('.segment-label-content'); + if (span) span.textContent = labelText; } - markerIdx++; + } else { + L.marker(mid, { + interactive: false, + pane: 'labelPane', + icon: L.divIcon({ + className: 'segment-label', + html: `${labelText}`, + iconSize: [46, 20], + iconAnchor: [23, 10] + }) + }).addTo(vLabel); } - }); + markerIdx++; + } while (markerIdx < currentMarkers.length) { vLabel.removeLayer(currentMarkers[markerIdx++]); @@ -1990,18 +2002,24 @@ const vDrill = drillingLayers[variant.id]; if (vDrill) vDrill.clearLayers(); - const nested = getNestedCoords(variant.routes); + const latLngs = (variant.routes || []).map(ll => { + try { return L.latLng(ll); } catch(e) { return null; } + }).filter(p => !!p && typeof p.lat === 'number'); + variant.stats.total = 0; variant.stats.drilling = 0; variant.stats.muffen = 0; variant.stats.hasTooLongDrilling = false; variant.drillingSegments = []; - if (nested.length === 0) return; + if (latLngs.length < 2) { + renderSegmentLabels(variant); + return; + } renderSegmentLabels(variant); - // Pre-filter obstacles once + // Pre-filter obstacles if (!cachedObstacles && state.usage.features) { const keywords = ['bahn', 'gewässer', 'wasser', 'straße', 'verkehr', 'gehölz', 'baufläche', 'wald', 'forst', 'hecke', 'weg', 'pfad', 'graben', 'bach', 'fluss']; cachedObstacles = state.usage.features.filter(f => { @@ -2011,66 +2029,61 @@ } const currentObstacles = cachedObstacles || []; - nested.forEach(latLngs => { - if (latLngs.length < 2) return; + const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat])); + variant.stats.total = turf.length(line, { units: 'kilometers' }) * 1000; + + const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]); + const drillingRanges = []; - const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat])); - const sectionLength = turf.length(line, { units: 'meters' }); - variant.stats.total += sectionLength; - - const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]); - const drillingRanges = []; - - if (currentObstacles.length > 0) { - const lineBbox = turf.bbox(line); - currentObstacles.forEach(obs => { - try { - const obsBbox = turf.bbox(obs); - if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return; - if (turf.booleanIntersects(line, obs)) { - const intersect = turf.lineIntersect(line, obs); - let distances = [0, sectionLength]; - intersect.features.forEach(f => { - distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' })); - }); - distances = [...new Set(distances)].sort((a, b) => a - b); - for (let i = 0; i < distances.length - 1; i++) { - const dStart = distances[i]; - const dEnd = distances[i + 1]; - const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' }); - if (turf.booleanPointInPolygon(midPt, obs)) { - drillingRanges.push([Math.max(0, dStart - 20), Math.min(sectionLength, dEnd + 20)]); - } + if (currentObstacles.length > 0) { + const lineBbox = turf.bbox(line); + currentObstacles.forEach(obs => { + try { + const obsBbox = turf.bbox(obs); + if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return; + if (turf.booleanIntersects(line, obs)) { + const intersect = turf.lineIntersect(line, obs); + let distances = [0, variant.stats.total]; + intersect.features.forEach(f => { + distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' })); + }); + distances = [...new Set(distances)].sort((a, b) => a - b); + for (let i = 0; i < distances.length - 1; i++) { + const dStart = distances[i]; + const dEnd = distances[i + 1]; + const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' }); + if (turf.booleanPointInPolygon(midPt, obs)) { + drillingRanges.push([Math.max(0, dStart - 20), Math.min(variant.stats.total, dEnd + 20)]); } } - } catch (e) {} - }); + } + } catch (e) {} + }); + } + + let mergedRanges = []; + if (drillingRanges.length > 0) { + drillingRanges.sort((a, b) => a[0] - b[0]); + let cur = drillingRanges[0]; + for (let i = 1; i < drillingRanges.length; i++) { + if (drillingRanges[i][0] <= cur[1]) cur[1] = Math.max(cur[1], drillingRanges[i][1]); + else { mergedRanges.push(cur); cur = drillingRanges[i]; } } + mergedRanges.push(cur); + } - let mergedRanges = []; - if (drillingRanges.length > 0) { - drillingRanges.sort((a, b) => a[0] - b[0]); - let cur = drillingRanges[0]; - for (let i = 1; i < drillingRanges.length; i++) { - if (drillingRanges[i][0] <= cur[1]) cur[1] = Math.max(cur[1], drillingRanges[i][1]); - else { mergedRanges.push(cur); cur = drillingRanges[i]; } - } - mergedRanges.push(cur); - } + variant.stats.drilling += mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0); + variant.stats.muffen += mergedRanges.length * 2; - variant.stats.drilling += mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0); - variant.stats.muffen += mergedRanges.length * 2; - - mergedRanges.forEach(range => { - const lengthM = range[1] - range[0]; - if (lengthM > 180) variant.stats.hasTooLongDrilling = true; - const s = turf.along(line, range[0] / 1000, { units: 'kilometers' }); - const e = turf.along(line, range[1] / 1000, { units: 'kilometers' }); - variant.drillingSegments.push({ - path: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]], - length: lengthM, - muffen: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]] - }); + mergedRanges.forEach(range => { + const lengthM = range[1] - range[0]; + if (lengthM > 180) variant.stats.hasTooLongDrilling = true; + const s = turf.along(line, range[0] / 1000, { units: 'kilometers' }); + const e = turf.along(line, range[1] / 1000, { units: 'kilometers' }); + variant.drillingSegments.push({ + path: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]], + length: lengthM, + muffen: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]] }); }); @@ -2129,21 +2142,17 @@ if (!container) return; container.innerHTML = '

Benötigte Flurstücke

'; - const latLngs = getFlattenedCoords(variant ? variant.routes : []); - if (!variant || latLngs.length < 2 || !state.owners || !state.owners.features || state.owners.features.length === 0) { + const latLngs = (variant.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(p => !!p && typeof p.lat === 'number'); + const hasData = latLngs.length >= 2; + + if (!variant || !hasData || !state.owners || !state.owners.features || state.owners.features.length === 0) { container.innerHTML += '

Keine Daten oder Trasse vorhanden

'; return; } try { - const nested = getNestedCoords(variant.routes); - if (nested.length === 0) return; - - const lines = nested.filter(s => s.length >= 2).map(s => turf.lineString(s.map(ll => [ll.lng, ll.lat]))); - if (lines.length === 0) return; - - const multiLine = lines.length === 1 ? lines[0] : turf.multiLineString(lines.map(l => l.geometry.coordinates)); - const lineBbox = turf.bbox(multiLine); + const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat])); + const lineBbox = turf.bbox(line); const intersectingPlots = state.owners.features.filter(f => { try { @@ -2151,7 +2160,7 @@ if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return false; - return turf.booleanIntersects(multiLine, f); + return turf.booleanIntersects(line, f); } catch (e) { return false; } }); @@ -2206,6 +2215,10 @@ // --- Variant Management UI --- window.startNewPath = (id) => { if (map.editTools) { + const v = state.variants.find(v => v.id == id); + if (v && !v.visible) { + toggleVariantVisibility(v.id, true); + } state.isDrawing = true; state.isMeasuring = false; updateToolCursors(); @@ -2281,7 +2294,6 @@ - @@ -2344,7 +2356,7 @@ if (newName && newName.trim()) { v.name = newName.trim(); renderVariants(); - if (activeV) saveVariantToDB(activeV); + if (v) saveVariantToDB(v); } }; @@ -2519,6 +2531,7 @@ // Global Map Click for generous vertex insertion ("Click anywhere near the line") const handleVertexInsertion = (e) => { + if (isClickOnUI(e)) return; if (state.isMeasuring) return; // VERY IMPORTANT: Don't interfere if we are actively drawing a path if (map.editTools && map.editTools.drawing()) return; @@ -2529,24 +2542,14 @@ const layer = routeLayers[variant.id]; if (!layer) return; - // If the variant is empty, we don't return here anymore, - // but we only attempt insertion if we have at least one segment (2 points). - const latlngs = getFlattenedCoords(layer.getLatLngs()); - - // If drawing and clicking far away, we let startPolyline/continueForward handle it - if (latlngs.length < 2) return; - - if (!layer.editor) { - layer.enableEdit(); - } - + const allPoints = (variant.routes || []).map(ll => L.latLng(ll)); let minIdx = -1; let minDist = Infinity; const pt = map.latLngToLayerPoint(e.latlng); - for (let i = 0; i < latlngs.length - 1; i++) { - const p1 = map.latLngToLayerPoint(latlngs[i]); - const p2 = map.latLngToLayerPoint(latlngs[i + 1]); + for (let i = 0; i < allPoints.length - 1; i++) { + const p1 = map.latLngToLayerPoint(allPoints[i]); + const p2 = map.latLngToLayerPoint(allPoints[i + 1]); const dist = L.LineUtil.pointToSegmentDistance(pt, p1, p2); if (dist < minDist) { minDist = dist; @@ -2554,29 +2557,27 @@ } } - // CRITICAL FIX: Check if we are clicking directly on or very near an existing vertex - // If we are closer than 10 pixels to any vertex, we assume the user wants - // to move the point, not insert a new one. - const isNearVertex = latlngs.some(ll => map.latLngToLayerPoint(ll).distanceTo(pt) < 12); + const isNearVertex = allPoints.some(ll => map.latLngToLayerPoint(ll).distanceTo(pt) < 12); if (isNearVertex) return; - // High tolerance hit-testing: 35 pixels (approx. the width of a finger/mouse inaccuracy) + // High tolerance hit-testing: 35 pixels const tolerance = state.isDrawing ? 15 : 35; if (minIdx !== -1 && minDist < tolerance) { - latlngs.splice(minIdx + 1, 0, e.latlng); - layer.setLatLngs(latlngs); + allPoints.splice(minIdx + 1, 0, e.latlng); + variant.routes = allPoints; + layer.setLatLngs(allPoints); - if (layer.editor && layer.editor.reset) { - layer.editor.reset(); + // FORCE REFRESH of editor markers + if (layer.editor) { + layer.disableEdit(); + layer.enableEdit(); } - // Force synchronization - variant.routes = getFlattenedCoords(layer.getLatLngs()); calculateStats(variant); updateVariantStatsUI(variant); renderVariants(); - const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV); + saveVariantToDB(variant); } }; @@ -2691,46 +2692,37 @@ } async function saveVariantToDB(variantDef) { + if (!variantDef) return; + const indicator = document.getElementById('db-status-indicator'); + if (indicator) indicator.classList.add('active'); + try { - // Identify the true state variant regardless of what parameter was passed - const v = variantDef.id ? variantDef : state.variants.find(vx => routeLayers[vx.id] === variantDef); + // Ensure we have a valid variant object and points + const v = variantDef.id ? variantDef : state.variants.find(vx => vx.id === variantDef.id); if (!v) { - console.warn("Save skipped: Could not map to a valid variant."); - return; - } - - const name = v.name || "Neue Trasse"; - let rawRoutes = v.routes || []; - - // Fallback attempt if routes is somehow empty but layer exists - if (rawRoutes.length === 0 && routeLayers[v.id] && typeof routeLayers[v.id].getLatLngs === 'function') { - rawRoutes = routeLayers[v.id].getLatLngs(); + if (indicator) indicator.classList.remove('active'); + return; } - if (!rawRoutes || rawRoutes.length === 0) { - console.warn("Save skipped: No route data available."); + // Simple flat coordinates from LineString + const latLngs = (v.routes || []).map(ll => { + try { return L.latLng(ll); } catch(e) { return null; } + }).filter(p => !!p && typeof p.lat === 'number'); + + if (latLngs.length < 2) { + if (indicator) indicator.classList.remove('active'); return; } - // Leaflet-Punkte in GeoJSON-Format umwandeln [lng, lat] - let geoJsonCoords = []; - const isMulti = routeLayers[v.id] && Array.isArray(rawRoutes[0]); - - if (isMulti || Array.isArray(rawRoutes[0])) { - geoJsonCoords = rawRoutes.map(line => line.map(p => [p.lng, p.lat])); - } else { - geoJsonCoords = rawRoutes.map(p => [p.lng, p.lat]); - } - - const geomType = (isMulti || Array.isArray(rawRoutes[0])) ? "MultiLineString" : "LineString"; - - const payload = { id: v.id, geometry: { - type: geoJsonCoords.length ? geomType : "LineString", - coordinates: geoJsonCoords + const payload = { + id: v.id, + geometry: { + type: "LineString", + coordinates: latLngs.map(p => [p.lng, p.lat]) }, properties: { - name: name, - Variante: name.replace('Variante ', '') + name: v.name, + Variante: v.name.replace('Variante ', '') } }; @@ -2748,113 +2740,98 @@ throw new Error('Server-Fehler: ' + errorText); } + const result = await response.json(); + if (result.success && result.id) { + v.id = result.id; // Sync the database ID back to the state + } console.log("Speichern in Datenbank erfolgreich!"); } catch (err) { console.error("DB Save failed:", err.message); - // alert("Fehler beim Speichern: " + err.message); // Commented to prevent spam during map moves + } finally { + if (indicator) indicator.classList.remove('active'); } } async function loadFromDatabase() { - if (isLoading) return; - isLoading = true; + const indicator = document.getElementById('db-status-indicator'); + if (indicator) indicator.classList.add('active'); + console.log("[V3-Sync] Starte Daten-Abruf vom Server..."); + + const sanitize = (gj) => { + if (!gj || !gj.features) return { type: "FeatureCollection", features: [] }; + return gj; + }; + + // 1. Varianten (JETZT ALS ERSTES) try { - console.log("Starte Daten-Abruf vom Server..."); - - const isVal = (c) => Array.isArray(c) && c.length >= 2 && !isNaN(c[0]) && !isNaN(c[1]) && c[0] !== null && c[1] !== null; - const sanitize = (gj) => { - if (!gj || !gj.features) return { type: "FeatureCollection", features: [] }; - return { - ...gj, - features: gj.features.filter(f => { - if (!f || !f.geometry || !f.geometry.coordinates) return false; - // Recursive coordinate check for simple structures - if (f.geometry.type === 'Point') return isVal(f.geometry.coordinates); - return true; // Polygons are harder to check deeply, but usually safer - }) - }; - }; + const res = await fetch(`${API_BASE}/variants`); + if (res.ok) { + const rawData = await res.json(); + let features = Array.isArray(rawData) ? rawData.map(item => ({ + type: "Feature", + id: item.id, + geometry: { + type: "LineString", + coordinates: (Array.isArray(item.routes[0]) ? item.routes[0] : item.routes) + .map(p => [(p.lng || p[0]), (p.lat || p[1])]) + }, + properties: { Variante: item.name ? item.name.replace('Variante ', '') : 'A', name: item.name } + })) : (rawData.features || []); - // 1. Eigentümer - try { - const res = await fetch(`${API_BASE}/owners`); - if (res.ok) { - let gj = sanitize(await res.json()); - if (gj.features.length > 0) { - // Check for UTM transform - const c = gj.features[0].geometry.coordinates; - const test = Array.isArray(c[0]) ? (Array.isArray(c[0][0]) ? c[0][0][0] : c[0][0]) : c[0]; - if (Math.abs(test) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326"); + console.log(`[V3-Sync] ${features.length} Trassen geladen.`); + + features.forEach(f => { + const p = f.properties || {}; + const varName = p.Variante ? `Variante ${p.Variante}` : (p.name || "Variante A"); + const slot = state.variants.find(lv => lv.name === varName); + if (slot && f.geometry && f.geometry.coordinates) { + slot.id = f.id || p.id || slot.id; + slot.routes = f.geometry.coordinates.map(c => ({ lat: c[1], lng: c[0] })); + console.log(`[V3-Sync] ${slot.name} erfolgreich gemappt.`); } - state.owners = gj; - updateOwnerLayer(); - if (layers.owners.getBounds().isValid()) map.fitBounds(layers.owners.getBounds()); - } - } catch (e) { console.error("Owners load error:", e); } + }); - // 2. Varianten - try { - const res = await fetch(`${API_BASE}/variants`); - if (res.ok) { - const v = await res.json(); - v.forEach(sv => { - const l = state.variants.find(lv => lv.id === sv.id); - if (l) { l.routes = sv.routes || []; l.name = sv.name || l.name; } - }); - updateRouteLayers(); - } - } catch (e) { console.error("Variants load error:", e); } + state.variants.forEach(v => { calculateStats(v); updateVariantStatsUI(v); }); + const varA = state.variants.find(v => v.name === "Variante A"); + if (varA) setActiveVariant(varA.id); + renderVariants(); + updateRouteLayers(); + } + } catch (e) { console.error("[V3-Sync] Variants Error:", e); } - // 3. Nutzungen - try { - const res = await fetch(`${API_BASE}/usage`); - if (res.ok) { - let gj = sanitize(await res.json()); - // Transform usage if UTM - if (gj.features.length > 0) { - const c = gj.features[0].geometry.coordinates; - const test = Array.isArray(c[0]) ? (Array.isArray(c[0][0]) ? c[0][0][0] : c[0][0]) : c[0]; - if (Math.abs(test) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326"); - } - state.usage = gj; - updateUsageLayer(); + // 2. Eigentümer (SCHWERER LAYER) + try { + const res = await fetch(`${API_BASE}/owners`); + if (res.ok) { + let gj = sanitize(await res.json()); + if (gj.features.length > 0) { + const c = gj.features[0].geometry.coordinates; + if (Math.abs(Array.isArray(c[0]) ? c[0][0] : c[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326"); } - } catch (e) { console.error("Usage load error:", e); } + state.owners = gj; + updateOwnerLayer(); + } + } catch (e) { console.error("[V3-Sync] Owners Error:", e); } - // 4. WEA - try { - const res = await fetch(`${API_BASE}/wea`); - if (res.ok) { - let gj = sanitize(await res.json()); - if (gj.features.length > 0) { - if (Math.abs(gj.features[0].geometry.coordinates[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326"); - } - state.wea = gj; - updateWEALayer(); + // 3. Nutzungen + try { + const res = await fetch(`${API_BASE}/usage`); + if (res.ok) { + let gj = sanitize(await res.json()); + if (gj.features.length > 0) { + const c = gj.features[0].geometry.coordinates; + if (Math.abs(Array.isArray(c[0]) ? c[0][0] : c[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326"); } - } catch (e) { console.error("WEA load error:", e); } + updateUsageLayer(gj); + } + } catch (e) { console.error("[V3-Sync] Usage Error:", e); } - // 5. Infrastruktur - try { - const res = await fetch(`${API_BASE}/infrastructure`); - if (res.ok) { - let gj = sanitize(await res.json()); - if (gj.features.length > 0) { - if (Math.abs(gj.features[0].geometry.coordinates[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326"); - } - state.infrastructure = gj; - updateInfrastructureLayer(); - } - } catch (e) { console.error("Infra load error:", e); } - - console.log("Daten-Ladevorgang abgeschlossen."); - } catch (err) { - console.error("Kritischer Fehler beim Laden der Datenbank:", err); - } finally { - isLoading = false; - renderVariants(); - } + console.log("[V3-Sync] Daten-Ladevorgang abgeschlossen."); + if (indicator) indicator.classList.remove('active'); + isLoading = false; + renderVariants(); } + let isSaving = false; async function saveToFolder() { if (isSaving || !state.directoryHandle) return; @@ -3117,6 +3094,26 @@ input.click(); }); + window.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + if (map.editTools) { + if (map.editTools.drawing()) { + map.editTools.stopDrawing(); + } + state.isDrawing = false; + state.isMeasuring = false; + updateToolCursors(); + renderVariants(); + + const activeV = state.variants.find(v => v.active); + if (activeV && routeLayers[activeV.id]) { + routeLayers[activeV.id].enableEdit(); + saveVariantToDB(activeV); + } + } + } + }); + window.addEventListener('load', async () => { try { initApp(); diff --git a/server.js b/server.js index bd4a500..85cfec8 100644 --- a/server.js +++ b/server.js @@ -28,32 +28,33 @@ app.use(express.static(__dirname)); // Helper to run the schema isolation command async function setSchema(client) { - await client.query(`SET search_path TO ${process.env.DB_SCHEMA}, public;`); + const schema = process.env.DB_SCHEMA || 'wind_projekt_bwscheddebrock'; + await client.query(`SET search_path TO "${schema}", public;`); } -// Routes +// Database Initial Setup / Migration Logic Removed +// (Now treating existing tables as the read-only 'Source of Truth') + +// --- API Routes --- app.get('/api/health', (req, res) => { - res.json({ status: 'OK', time: new Date().toISOString() }); + res.json({ status: 'OK', schema: process.env.DB_SCHEMA, time: new Date().toISOString() }); }); // 1. Get Owner Data app.get('/api/owners', async (req, res) => { - console.log("Anfrage erhalten: /api/owners"); const client = await pool.connect(); try { await setSchema(client); - // 1. Log actual count in DB - const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Eigentuemerdaten"'); - console.log(`Datenbank-Check: ${countRes.rows[0].count} Eigentümer in bw_scheddebrock."Eigentuemerdaten" gefunden.`); + const countRes = await client.query('SELECT count(*) FROM "Eigentuemerdaten"'); + console.log(`Datenbank-Check: ${countRes.rows[0].count} Einträge in "Eigentuemerdaten".`); const query = ` SELECT *, ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry - FROM bw_scheddebrock."Eigentuemerdaten" + FROM "Eigentuemerdaten" `; const result = await client.query(query); - console.log(`Abfrage erfolgreich: ${result.rowCount} Zeilen für Frontend geladen.`); const geojson = { type: "FeatureCollection", @@ -62,7 +63,7 @@ app.get('/api/owners', async (req, res) => { const geomObj = typeof geometry === 'string' ? JSON.parse(geometry) : geometry; return { type: "Feature", - id: row.id || row.id_0, + id: row.id, geometry: geomObj, properties: properties }; @@ -82,18 +83,13 @@ app.get('/api/usage', async (req, res) => { const client = await pool.connect(); try { await setSchema(client); - // 1. Log actual count in DB - const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Nutzung"'); - console.log(`Datenbank-Check: ${countRes.rows[0].count} Nutzungs-Flächen in bw_scheddebrock."Nutzung" gefunden.`); - const query = ` SELECT *, ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry - FROM bw_scheddebrock."Nutzung" + FROM "Nutzung" `; const result = await client.query(query); - console.log(`Abfrage erfolgreich: ${result.rowCount} Zeilen für Frontend geladen.`); const geojson = { type: "FeatureCollection", @@ -116,7 +112,7 @@ app.get('/api/usage', async (req, res) => { } }); -// 2.5 Get WEA Data (Windturbinen) +// 2.5 Get WEA Data app.get('/api/wea', async (req, res) => { const client = await pool.connect(); try { @@ -127,10 +123,9 @@ app.get('/api/wea', async (req, res) => { SELECT *, ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry - FROM bw_scheddebrock."WEA" + FROM "WEA" `); } catch (dbErr) { - console.log("WEA Tabelle fehlt oder nicht abrufbar."); result = { rows: [], rowCount: 0 }; } @@ -155,7 +150,7 @@ app.get('/api/wea', async (req, res) => { } }); -// 2.7 Get Infrastructure Data (UW, etc) +// 2.7 Get Infrastructure Data app.get('/api/infrastructure', async (req, res) => { const client = await pool.connect(); try { @@ -166,10 +161,9 @@ app.get('/api/infrastructure', async (req, res) => { SELECT *, ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry - FROM bw_scheddebrock."Infrastruktur" + FROM "Infrastruktur" `); } catch (dbErr) { - console.log("Infrastruktur Tabelle fehlt oder nicht abrufbar."); result = { rows: [], rowCount: 0 }; } @@ -199,41 +193,32 @@ app.get('/api/variants', async (req, res) => { const client = await pool.connect(); try { await setSchema(client); - - // Log count - const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Kabeltrasse"'); - console.log(`Datenbank-Check: ${countRes.rows[0].count} Kabeltrassen in bw_scheddebrock."Kabeltrasse" gefunden.`); - const query = ` SELECT - id_0 as id, name, "Variante", + id, name, "Variante", ST_AsGeoJSON(ST_Transform(ST_SetSRID(geom, 25832), 4326)) as geometry - FROM bw_scheddebrock."Kabeltrasse" - ORDER BY id_0 DESC + FROM "Kabeltrasse" + ORDER BY id DESC `; const result = await client.query(query); - console.log(`Abfrage erfolgreich: ${result.rowCount} Trassen geladen.`); - const variants = result.rows.map(row => { - let routes = []; - const geomObj = typeof row.geometry === 'string' ? JSON.parse(row.geometry) : row.geometry; - if (geomObj && geomObj.coordinates) { - if (geomObj.type === 'MultiLineString') { - routes = geomObj.coordinates.map(line => line.map(c => ({ lat: c[1], lng: c[0] }))); - } else if (geomObj.type === 'LineString') { - routes = geomObj.coordinates.map(c => ({ lat: c[1], lng: c[0] })); - } - } - return { - id: row.id, - name: row.name || row.Variante || 'Trasse', - active: false, - visible: true, - stats: { total: 0, drilling: 0, open: 0, muffen: 0 }, - routes: routes - }; - }); - res.json(variants); + const featureCollection = { + type: "FeatureCollection", + features: result.rows.map(row => { + const geomObj = JSON.parse(row.geometry || 'null'); + return { + type: "Feature", + id: row.id, + geometry: geomObj, + properties: { + id: row.id, + name: row.name || (row.Variante ? `Variante ${row.Variante}` : 'Neue Trasse'), + Variante: row.Variante + } + }; + }) + }; + res.json(featureCollection); } catch (err) { console.error(err); res.status(500).json({ error: 'Failed to fetch variants' }); @@ -244,52 +229,36 @@ app.get('/api/variants', async (req, res) => { // 4. Save/Update Variant app.post('/api/variants', async (req, res) => { - console.log("Empfangene Daten (Payload):", JSON.stringify(req.body).substring(0, 200) + "..."); const { geometry, properties } = req.body; const client = await pool.connect(); try { await setSchema(client); - console.log("Starte PostgreSQL-Query..."); - - // Clean UPSERT by Database ID. - const routeId = req.body.id; const routeName = properties.name || 'Neue Trasse'; - const variante = properties.Variante || properties.name || 'A'; + const variante = properties.Variante || (properties.name ? properties.name.replace('Variante ', '') : 'A'); const geoJsonStr = JSON.stringify(geometry); + + const upsertQuery = ` + INSERT INTO "Kabeltrasse" (geom, name, "Variante") + VALUES ( + ST_MakeValid(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 25832)), + $2, + $3 + ) + ON CONFLICT ("Variante") + DO UPDATE SET + geom = EXCLUDED.geom, + name = EXCLUDED.name + RETURNING id + `; - // Check if the passed ID is a valid database ID (small int) vs a frontend dummy Date.now() timestamp - // Or try to match it initially just in case it's a known database row. - const existing = await client.query('SELECT id_0 FROM bw_scheddebrock."Kabeltrasse" WHERE id_0 = $1', [Number(routeId) || 0]); + const upsertRes = await client.query(upsertQuery, [geoJsonStr, routeName, variante]); + const finalId = upsertRes.rows[0].id; - let result; - if (existing.rowCount > 0) { - // EXACT match found physically in DB, do an UPDATE - const updateQuery = ` - UPDATE bw_scheddebrock."Kabeltrasse" - SET geom = ST_MakeValid(ST_SetSRID(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 25832), 25832)), - name = $2, - "Variante" = $3 - WHERE id_0 = $4 - RETURNING id_0 as id; - `; - result = await client.query(updateQuery, [geoJsonStr, routeName, variante, existing.rows[0].id_0]); - console.log(`PostgreSQL-Ergebnis: Zeile ${existing.rows[0].id_0} aktualisiert (Variante: ${routeName})!`); - } else { - // INSERT new - const insertQuery = ` - INSERT INTO bw_scheddebrock."Kabeltrasse" (geom, name, "Variante") - VALUES (ST_MakeValid(ST_SetSRID(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 25832), 25832)), $2, $3) - RETURNING id_0 as id; - `; - result = await client.query(insertQuery, [geoJsonStr, routeName, variante]); - console.log(`PostgreSQL-Ergebnis: Zeile eingefügt! ID: ${result.rows[0].id}`); - } - - res.json({ success: true, id: result.rows[0].id }); + res.json({ success: true, id: finalId }); } catch (err) { - console.error("KRITISCHER SQL-FEHLER:", err.message); - res.status(500).json({ error: 'Failed to save variant: ' + err.message }); + console.error("SQL-FEHLER (SAVE):", err.message); + res.status(500).json({ error: 'Failed to save variant' }); } finally { client.release(); } @@ -303,18 +272,18 @@ app.patch('/api/owners/:id', async (req, res) => { try { await setSchema(client); const query = ` - UPDATE bw_scheddebrock."Eigentuemerdaten" + UPDATE "Eigentuemerdaten" SET status = $1, notiz = $2 WHERE id = $3 RETURNING id; `; const result = await client.query(query, [status, notiz, id]); if (result.rowCount === 0) { - return res.status(404).json({ error: 'Owner not found in database' }); + return res.status(404).json({ error: 'Owner not found' }); } res.json({ success: true, id: result.rows[0].id }); } catch (err) { - console.error("Owner Update Error:", err); + console.error(err); res.status(500).json({ error: 'Failed to update owner' }); } finally { client.release();