Finalizing DB structure mapping and removing migrations
Deploy TrassenPlaner / deploy (push) Waiting to run Details

This commit is contained in:
Johannes Baumeister 2026-04-19 22:27:17 +02:00
parent 6d152ad3c9
commit 86f2e4c8ff
4 changed files with 395 additions and 429 deletions

View File

@ -31,7 +31,7 @@ jobs:
echo "DB_USER=${{ secrets.USER }}" >> .env echo "DB_USER=${{ secrets.USER }}" >> .env
echo "DB_PASSWORD='${{ secrets.PASSWORD }}'" >> .env echo "DB_PASSWORD='${{ secrets.PASSWORD }}'" >> .env
echo "DB_NAME=${{ secrets.NAME }}" >> .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 compose up -d --build --force-recreate
docker image prune -f docker image prune -f

View File

@ -11,7 +11,7 @@ services:
- DB_USER=authentik - DB_USER=authentik
- DB_PASSWORD=WX1t1cgP1qK09 - DB_PASSWORD=WX1t1cgP1qK09
- DB_NAME=authentik - DB_NAME=authentik
- DB_SCHEMA=bw_scheddebrock - DB_SCHEMA=wind_projekt_bwscheddebrock
networks: networks:
- proxy - proxy
labels: labels:

View File

@ -6,15 +6,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TrassenPlaner Pro</title> <title>TrassenPlaner Pro</title>
<!-- Libraries (CDN via jsDelivr) --> <!-- Libraries (CDN via unpkg) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6.5.0/turf.min.js"></script> <script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/shpjs@latest/dist/shp.js"></script> <script src="https://unpkg.com/shpjs@latest/dist/shp.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@latest/dist/umd/lucide.js"></script> <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet-editable@1.2.0/src/Leaflet.Editable.js"></script> <script src="https://unpkg.com/leaflet-editable@1.2.0/src/Leaflet.Editable.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script> <script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/proj4@2.11.0/dist/proj4.js"></script> <script src="https://unpkg.com/proj4@2.11.0/dist/proj4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
@ -837,8 +837,12 @@
doc.setFontSize(9); doc.setFontSize(9);
const footerText = "Dieses Dokument wurde vom Auftragnehmer erstellt und aus dem Planungstool automatisch generiert."; const footerText = "Dieses Dokument wurde vom Auftragnehmer erstellt und aus dem Planungstool automatisch generiert.";
const latLngs = getFlattenedCoords(v.routes); const nested = getNestedCoords(v.routes);
const lineStr = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat])); 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 lineBbox = turf.bbox(lineStr);
const tableData = []; const tableData = [];
@ -1128,21 +1132,24 @@
const variant = state.variants.find(v => routeLayers[v.id] === layer); const variant = state.variants.find(v => routeLayers[v.id] === layer);
if (variant) { if (variant) {
const liveCoords = getFlattenedCoords(layer.getLatLngs());
const tempPt = (e.type === 'editable:drawing:move') ? e.latlng : null; 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); 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) { 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 { } else {
variant.routes = liveCoords; variant.routes = layer.getLatLngs();
calculateStats(variant); calculateStats(variant);
updateVariantStatsUI(variant); updateVariantStatsUI(variant);
renderVariants(); renderVariants();
if (variant.active) updateRequiredPlots(variant); if (variant.active) updateRequiredPlots(variant);
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(routeLayers[_activeV.id] || _activeV); saveVariantToDB(variant);
} }
} }
} catch (err) { } catch (err) {
@ -1710,6 +1717,7 @@
weight: 6, weight: 6,
opacity: 1, opacity: 1,
editable: true, editable: true,
middleMarkers: true,
pane: 'trassenPane', pane: 'trassenPane',
lineOptions: { color: '#cca300', weight: 6 }, lineOptions: { color: '#cca300', weight: 6 },
vertexOptions: { color: '#cca300', radius: 5 }, vertexOptions: { color: '#cca300', radius: 5 },
@ -1741,15 +1749,16 @@
// Z-Index: Active variant to front // Z-Index: Active variant to front
if (v.active) { if (v.active) {
layer.bringToFront(); if (layer.bringToFront) layer.bringToFront();
if (drillingLayers[v.id]) drillingLayers[v.id].bringToFront(); if (drillingLayers[v.id]) drillingLayers[v.id].bringToFront();
if (labelLayers[v.id]) labelLayers[v.id].bringToFront(); if (labelLayers[v.id]) labelLayers[v.id].bringToFront();
} }
// Sync geometry if not actively being dragged/edited // Direct sync for single LineString
if (!v.active) { const isDrawing = v.active && map.editTools && map.editTools.drawing();
if (!isDrawing) {
try { try {
layer.setLatLngs(v.routes); layer.setLatLngs(v.routes || []);
} catch (e) { } catch (e) {
console.warn("Geometrie-Sync Warnung:", e); console.warn("Geometrie-Sync Warnung:", e);
} }
@ -1764,41 +1773,27 @@
// Labels aktualisieren (Name in der Mitte der Linie auf weißem Hintergrund) // Labels aktualisieren (Name in der Mitte der Linie auf weißem Hintergrund)
labelLayer.clearLayers(); labelLayer.clearLayers();
if (v.routes && v.routes.length > 1) { const latLngs = (v.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(ll => !!ll && typeof ll.lat === 'number');
// Find geometric middle of the polyline if (latLngs.length >= 2) {
const points = layer.getLatLngs(); try {
let totalDist = 0; const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
const distances = []; const totalKm = turf.length(line, { units: 'kilometers' });
for (let i = 0; i < points.length - 1; i++) { const midPoint = turf.along(line, totalKm / 2, { units: 'kilometers' });
const d = map.distance(points[i], points[i+1]); const midPos = L.latLng(midPoint.geometry.coordinates[1], midPoint.geometry.coordinates[0]);
distances.push(d);
totalDist += d;
}
let currentDist = 0; L.marker(midPos, {
let midPos = points[0]; pane: 'labelPane',
for (let i = 0; i < distances.length; i++) { icon: L.divIcon({
if (currentDist + distances[i] >= totalDist / 2) { className: 'variant-map-label',
// Midpoint is on this segment html: `<div style="background: white; border: 1px solid #cca300; color: #444; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px; box-shadow: 0 1px 4px rgba(0,0,0,0.2); white-space: nowrap; opacity: ${v.active ? 1 : 0.4}; transition: opacity 0.3s;">${v.name}</div>`,
const ratio = (totalDist / 2 - currentDist) / distances[i]; iconSize: [0, 0],
const lat = points[i].lat + (points[i+1].lat - points[i].lat) * ratio; iconAnchor: [0, 0]
const lng = points[i].lng + (points[i+1].lng - points[i].lng) * ratio; }),
midPos = L.latLng(lat, lng); interactive: false
break; }).addTo(labelLayer);
} } catch (err) {
currentDist += distances[i]; console.warn("Label positioning failed:", err);
} }
L.marker(midPos, {
pane: 'labelPane',
icon: L.divIcon({
className: 'variant-map-label',
html: `<div style="background: white; border: 1px solid #cca300; color: #444; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px; box-shadow: 0 1px 4px rgba(0,0,0,0.2); white-space: nowrap; opacity: ${v.active ? 1 : 0.4}; transition: opacity 0.3s;">${v.name}</div>`,
iconSize: [0, 0],
iconAnchor: [0, 0]
}),
interactive: false
}).addTo(labelLayer);
} }
// Sync edit state // Sync edit state
@ -1853,6 +1848,22 @@
renderVariants(); 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() { function updateToolCursors() {
const container = map.getContainer(); const container = map.getContainer();
if (state.isDrawing) container.style.cursor = 'crosshair'; if (state.isDrawing) container.style.cursor = 'crosshair';
@ -1863,59 +1874,48 @@
document.getElementById('btn-measure').className = state.isMeasuring ? 'btn btn-primary' : 'btn btn-outline'; 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) // 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) => { 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); const activeV = state.variants.find(v => v.active);
if (!activeV) return; if (!activeV) return;
// Enforce color on everything newly created or edited // Enforce color
if (e.layer && e.layer.setStyle) { if (e.layer && e.layer.setStyle) {
e.layer.setStyle({ color: '#cca300', weight: 6 }); e.layer.setStyle({ color: '#cca300', weight: 6 });
} }
// If a new layer was created (via startPolyline or startNewPath), merge it into our existing layer // Sync logic for single line
if (e.type === 'editable:created' && activeV && routeLayers[activeV.id] && e.layer !== routeLayers[activeV.id]) { if (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]) {
activeV.routes = e.layer.getLatLngs(); activeV.routes = e.layer.getLatLngs();
} }
// Live updates for labels, drillings and stats
calculateStats(activeV); calculateStats(activeV);
updateVariantStatsUI(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(); 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) { function getFlattenedCoords(raw) {
if (!raw) return []; if (!raw) return [];
// If it's a flat array of LatLngs // If it's a flat array of LatLngs already
if (raw.length > 0 && (raw[0].lat !== undefined || (Array.isArray(raw[0]) && typeof raw[0][0] === 'number'))) { 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)); return raw.map(ll => L.latLng(ll));
} }
// If it's nested // If it's nested [[LatLng...], [LatLng...]]
const flattened = []; const flattened = [];
raw.forEach(section => { if (Array.isArray(raw)) {
if (Array.isArray(section)) { raw.forEach(section => {
section.forEach(ll => flattened.push(L.latLng(ll))); if (Array.isArray(section)) {
} section.forEach(ll => {
}); if (ll) flattened.push(L.latLng(ll));
});
}
});
}
return flattened; return flattened;
} }
@ -1923,10 +1923,20 @@
if (!raw) return []; if (!raw) return [];
// Check if it's already nested // 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]))) { 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 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 []; return [];
} }
@ -1935,47 +1945,49 @@
if (!vLabel) return; if (!vLabel) return;
if (!variant.visible) { vLabel.clearLayers(); 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(); const currentMarkers = vLabel.getLayers();
let markerIdx = 0; let markerIdx = 0;
nested.forEach((latLngs, sectionIdx) => { if (latLngs.length < 2) {
const drawLatLngs = [...latLngs]; vLabel.clearLayers();
// Only add temp point to the very LAST section if we are actively drawing return;
if (tempPt && sectionIdx === nested.length - 1) drawLatLngs.push(L.latLng(tempPt)); }
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++) { if (markerIdx < currentMarkers.length) {
const p1 = drawLatLngs[i]; const m = currentMarkers[markerIdx];
const p2 = drawLatLngs[i + 1]; m.setLatLng(mid);
const dist = map.distance(p1, p2); const el = m.getElement();
const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2); if (el) {
const labelText = `${dist.toFixed(0)}m`; const span = el.querySelector('.segment-label-content');
if (span) span.textContent = labelText;
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: `<span class="segment-label-content">${labelText}</span>`,
iconSize: [46, 20],
iconAnchor: [23, 10]
})
}).addTo(vLabel);
} }
markerIdx++; } else {
L.marker(mid, {
interactive: false,
pane: 'labelPane',
icon: L.divIcon({
className: 'segment-label',
html: `<span class="segment-label-content">${labelText}</span>`,
iconSize: [46, 20],
iconAnchor: [23, 10]
})
}).addTo(vLabel);
} }
}); markerIdx++;
}
while (markerIdx < currentMarkers.length) { while (markerIdx < currentMarkers.length) {
vLabel.removeLayer(currentMarkers[markerIdx++]); vLabel.removeLayer(currentMarkers[markerIdx++]);
@ -1990,18 +2002,24 @@
const vDrill = drillingLayers[variant.id]; const vDrill = drillingLayers[variant.id];
if (vDrill) vDrill.clearLayers(); 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.total = 0;
variant.stats.drilling = 0; variant.stats.drilling = 0;
variant.stats.muffen = 0; variant.stats.muffen = 0;
variant.stats.hasTooLongDrilling = false; variant.stats.hasTooLongDrilling = false;
variant.drillingSegments = []; variant.drillingSegments = [];
if (nested.length === 0) return; if (latLngs.length < 2) {
renderSegmentLabels(variant);
return;
}
renderSegmentLabels(variant); renderSegmentLabels(variant);
// Pre-filter obstacles once // Pre-filter obstacles
if (!cachedObstacles && state.usage.features) { 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']; 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 => { cachedObstacles = state.usage.features.filter(f => {
@ -2011,66 +2029,61 @@
} }
const currentObstacles = cachedObstacles || []; const currentObstacles = cachedObstacles || [];
nested.forEach(latLngs => { const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
if (latLngs.length < 2) return; 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])); if (currentObstacles.length > 0) {
const sectionLength = turf.length(line, { units: 'meters' }); const lineBbox = turf.bbox(line);
variant.stats.total += sectionLength; currentObstacles.forEach(obs => {
try {
const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]); const obsBbox = turf.bbox(obs);
const drillingRanges = []; if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return;
if (turf.booleanIntersects(line, obs)) {
if (currentObstacles.length > 0) { const intersect = turf.lineIntersect(line, obs);
const lineBbox = turf.bbox(line); let distances = [0, variant.stats.total];
currentObstacles.forEach(obs => { intersect.features.forEach(f => {
try { distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }));
const obsBbox = turf.bbox(obs); });
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return; distances = [...new Set(distances)].sort((a, b) => a - b);
if (turf.booleanIntersects(line, obs)) { for (let i = 0; i < distances.length - 1; i++) {
const intersect = turf.lineIntersect(line, obs); const dStart = distances[i];
let distances = [0, sectionLength]; const dEnd = distances[i + 1];
intersect.features.forEach(f => { const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' });
distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' })); if (turf.booleanPointInPolygon(midPt, obs)) {
}); drillingRanges.push([Math.max(0, dStart - 20), Math.min(variant.stats.total, dEnd + 20)]);
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)]);
}
} }
} }
} 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 = []; variant.stats.drilling += mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0);
if (drillingRanges.length > 0) { variant.stats.muffen += mergedRanges.length * 2;
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); mergedRanges.forEach(range => {
variant.stats.muffen += mergedRanges.length * 2; const lengthM = range[1] - range[0];
if (lengthM > 180) variant.stats.hasTooLongDrilling = true;
mergedRanges.forEach(range => { const s = turf.along(line, range[0] / 1000, { units: 'kilometers' });
const lengthM = range[1] - range[0]; const e = turf.along(line, range[1] / 1000, { units: 'kilometers' });
if (lengthM > 180) variant.stats.hasTooLongDrilling = true; variant.drillingSegments.push({
const s = turf.along(line, range[0] / 1000, { units: 'kilometers' }); path: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]],
const e = turf.along(line, range[1] / 1000, { units: 'kilometers' }); length: lengthM,
variant.drillingSegments.push({ muffen: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]]
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; if (!container) return;
container.innerHTML = '<h4 style="font-size: 13px; margin-bottom: 10px; color: #64748b; display: flex; align-items: center; gap: 8px;"><i data-lucide="layers" style="width: 14px;"></i> Benötigte Flurstücke</h4>'; container.innerHTML = '<h4 style="font-size: 13px; margin-bottom: 10px; color: #64748b; display: flex; align-items: center; gap: 8px;"><i data-lucide="layers" style="width: 14px;"></i> Benötigte Flurstücke</h4>';
const latLngs = getFlattenedCoords(variant ? variant.routes : []); const latLngs = (variant.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(p => !!p && typeof p.lat === 'number');
if (!variant || latLngs.length < 2 || !state.owners || !state.owners.features || state.owners.features.length === 0) { const hasData = latLngs.length >= 2;
if (!variant || !hasData || !state.owners || !state.owners.features || state.owners.features.length === 0) {
container.innerHTML += '<p style="font-size: 11px; color: #94a3b8; text-align: center; margin-top: 20px;">Keine Daten oder Trasse vorhanden</p>'; container.innerHTML += '<p style="font-size: 11px; color: #94a3b8; text-align: center; margin-top: 20px;">Keine Daten oder Trasse vorhanden</p>';
return; return;
} }
try { try {
const nested = getNestedCoords(variant.routes); const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
if (nested.length === 0) return; const lineBbox = turf.bbox(line);
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 intersectingPlots = state.owners.features.filter(f => { const intersectingPlots = state.owners.features.filter(f => {
try { try {
@ -2151,7 +2160,7 @@
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] ||
lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return false; lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return false;
return turf.booleanIntersects(multiLine, f); return turf.booleanIntersects(line, f);
} catch (e) { return false; } } catch (e) { return false; }
}); });
@ -2206,6 +2215,10 @@
// --- Variant Management UI --- // --- Variant Management UI ---
window.startNewPath = (id) => { window.startNewPath = (id) => {
if (map.editTools) { if (map.editTools) {
const v = state.variants.find(v => v.id == id);
if (v && !v.visible) {
toggleVariantVisibility(v.id, true);
}
state.isDrawing = true; state.isDrawing = true;
state.isMeasuring = false; state.isMeasuring = false;
updateToolCursors(); updateToolCursors();
@ -2281,7 +2294,6 @@
<input type="checkbox" ${v.visible ? 'checked' : ''} onclick="toggleVariantVisibility(${v.id}, this.checked)"> <input type="checkbox" ${v.visible ? 'checked' : ''} onclick="toggleVariantVisibility(${v.id}, this.checked)">
<i data-lucide="eye" style="width: 14px;"></i> <i data-lucide="eye" style="width: 14px;"></i>
</label> </label>
<i data-lucide="plus-circle" class="action-icon" title="Weiteren Abschnitt hinzufügen" style="cursor: pointer; width: 15px; color: #10b981;" onclick="startNewPath(${v.id})"></i>
<i data-lucide="table" class="action-icon" title="Tabelle herunterladen" style="cursor: pointer; width: 15px; color: var(--corporate-teal);" onclick="downloadVariantTable(${v.id})"></i> <i data-lucide="table" class="action-icon" title="Tabelle herunterladen" style="cursor: pointer; width: 15px; color: var(--corporate-teal);" onclick="downloadVariantTable(${v.id})"></i>
<i data-lucide="copy" class="action-icon" title="Duplizieren" style="cursor: pointer; width: 15px; color: #64748b;" onclick="duplicateVariant(${v.id})"></i> <i data-lucide="copy" class="action-icon" title="Duplizieren" style="cursor: pointer; width: 15px; color: #64748b;" onclick="duplicateVariant(${v.id})"></i>
<i data-lucide="trash-2" class="action-icon" title="Inhalt löschen" style="cursor: pointer; width: 15px; color: var(--danger);" onclick="clearVariant(${v.id})"></i> <i data-lucide="trash-2" class="action-icon" title="Inhalt löschen" style="cursor: pointer; width: 15px; color: var(--danger);" onclick="clearVariant(${v.id})"></i>
@ -2344,7 +2356,7 @@
if (newName && newName.trim()) { if (newName && newName.trim()) {
v.name = newName.trim(); v.name = newName.trim();
renderVariants(); 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") // Global Map Click for generous vertex insertion ("Click anywhere near the line")
const handleVertexInsertion = (e) => { const handleVertexInsertion = (e) => {
if (isClickOnUI(e)) return;
if (state.isMeasuring) return; if (state.isMeasuring) return;
// VERY IMPORTANT: Don't interfere if we are actively drawing a path // VERY IMPORTANT: Don't interfere if we are actively drawing a path
if (map.editTools && map.editTools.drawing()) return; if (map.editTools && map.editTools.drawing()) return;
@ -2529,24 +2542,14 @@
const layer = routeLayers[variant.id]; const layer = routeLayers[variant.id];
if (!layer) return; if (!layer) return;
// If the variant is empty, we don't return here anymore, const allPoints = (variant.routes || []).map(ll => L.latLng(ll));
// 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();
}
let minIdx = -1; let minIdx = -1;
let minDist = Infinity; let minDist = Infinity;
const pt = map.latLngToLayerPoint(e.latlng); const pt = map.latLngToLayerPoint(e.latlng);
for (let i = 0; i < latlngs.length - 1; i++) { for (let i = 0; i < allPoints.length - 1; i++) {
const p1 = map.latLngToLayerPoint(latlngs[i]); const p1 = map.latLngToLayerPoint(allPoints[i]);
const p2 = map.latLngToLayerPoint(latlngs[i + 1]); const p2 = map.latLngToLayerPoint(allPoints[i + 1]);
const dist = L.LineUtil.pointToSegmentDistance(pt, p1, p2); const dist = L.LineUtil.pointToSegmentDistance(pt, p1, p2);
if (dist < minDist) { if (dist < minDist) {
minDist = dist; minDist = dist;
@ -2554,29 +2557,27 @@
} }
} }
// CRITICAL FIX: Check if we are clicking directly on or very near an existing vertex const isNearVertex = allPoints.some(ll => map.latLngToLayerPoint(ll).distanceTo(pt) < 12);
// 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);
if (isNearVertex) return; 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; const tolerance = state.isDrawing ? 15 : 35;
if (minIdx !== -1 && minDist < tolerance) { if (minIdx !== -1 && minDist < tolerance) {
latlngs.splice(minIdx + 1, 0, e.latlng); allPoints.splice(minIdx + 1, 0, e.latlng);
layer.setLatLngs(latlngs); variant.routes = allPoints;
layer.setLatLngs(allPoints);
if (layer.editor && layer.editor.reset) { // FORCE REFRESH of editor markers
layer.editor.reset(); if (layer.editor) {
layer.disableEdit();
layer.enableEdit();
} }
// Force synchronization
variant.routes = getFlattenedCoords(layer.getLatLngs());
calculateStats(variant); calculateStats(variant);
updateVariantStatsUI(variant); updateVariantStatsUI(variant);
renderVariants(); renderVariants();
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV); saveVariantToDB(variant);
} }
}; };
@ -2691,46 +2692,37 @@
} }
async function saveVariantToDB(variantDef) { async function saveVariantToDB(variantDef) {
if (!variantDef) return;
const indicator = document.getElementById('db-status-indicator');
if (indicator) indicator.classList.add('active');
try { try {
// Identify the true state variant regardless of what parameter was passed // Ensure we have a valid variant object and points
const v = variantDef.id ? variantDef : state.variants.find(vx => routeLayers[vx.id] === variantDef); const v = variantDef.id ? variantDef : state.variants.find(vx => vx.id === variantDef.id);
if (!v) { if (!v) {
console.warn("Save skipped: Could not map to a valid variant."); if (indicator) indicator.classList.remove('active');
return; 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 (!rawRoutes || rawRoutes.length === 0) { // Simple flat coordinates from LineString
console.warn("Save skipped: No route data available."); 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; return;
} }
// Leaflet-Punkte in GeoJSON-Format umwandeln [lng, lat] const payload = {
let geoJsonCoords = []; id: v.id,
const isMulti = routeLayers[v.id] && Array.isArray(rawRoutes[0]); geometry: {
type: "LineString",
if (isMulti || Array.isArray(rawRoutes[0])) { coordinates: latLngs.map(p => [p.lng, p.lat])
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
}, },
properties: { properties: {
name: name, name: v.name,
Variante: name.replace('Variante ', '') Variante: v.name.replace('Variante ', '')
} }
}; };
@ -2748,113 +2740,98 @@
throw new Error('Server-Fehler: ' + errorText); 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!"); console.log("Speichern in Datenbank erfolgreich!");
} catch (err) { } catch (err) {
console.error("DB Save failed:", err.message); 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() { async function loadFromDatabase() {
if (isLoading) return; const indicator = document.getElementById('db-status-indicator');
isLoading = true; 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 { try {
console.log("Starte Daten-Abruf vom Server..."); const res = await fetch(`${API_BASE}/variants`);
if (res.ok) {
const isVal = (c) => Array.isArray(c) && c.length >= 2 && !isNaN(c[0]) && !isNaN(c[1]) && c[0] !== null && c[1] !== null; const rawData = await res.json();
const sanitize = (gj) => { let features = Array.isArray(rawData) ? rawData.map(item => ({
if (!gj || !gj.features) return { type: "FeatureCollection", features: [] }; type: "Feature",
return { id: item.id,
...gj, geometry: {
features: gj.features.filter(f => { type: "LineString",
if (!f || !f.geometry || !f.geometry.coordinates) return false; coordinates: (Array.isArray(item.routes[0]) ? item.routes[0] : item.routes)
// Recursive coordinate check for simple structures .map(p => [(p.lng || p[0]), (p.lat || p[1])])
if (f.geometry.type === 'Point') return isVal(f.geometry.coordinates); },
return true; // Polygons are harder to check deeply, but usually safer properties: { Variante: item.name ? item.name.replace('Variante ', '') : 'A', name: item.name }
}) })) : (rawData.features || []);
};
};
// 1. Eigentümer console.log(`[V3-Sync] ${features.length} Trassen geladen.`);
try {
const res = await fetch(`${API_BASE}/owners`); features.forEach(f => {
if (res.ok) { const p = f.properties || {};
let gj = sanitize(await res.json()); const varName = p.Variante ? `Variante ${p.Variante}` : (p.name || "Variante A");
if (gj.features.length > 0) { const slot = state.variants.find(lv => lv.name === varName);
// Check for UTM transform if (slot && f.geometry && f.geometry.coordinates) {
const c = gj.features[0].geometry.coordinates; slot.id = f.id || p.id || slot.id;
const test = Array.isArray(c[0]) ? (Array.isArray(c[0][0]) ? c[0][0][0] : c[0][0]) : c[0]; slot.routes = f.geometry.coordinates.map(c => ({ lat: c[1], lng: c[0] }));
if (Math.abs(test) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326"); 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 state.variants.forEach(v => { calculateStats(v); updateVariantStatsUI(v); });
try { const varA = state.variants.find(v => v.name === "Variante A");
const res = await fetch(`${API_BASE}/variants`); if (varA) setActiveVariant(varA.id);
if (res.ok) { renderVariants();
const v = await res.json(); updateRouteLayers();
v.forEach(sv => { }
const l = state.variants.find(lv => lv.id === sv.id); } catch (e) { console.error("[V3-Sync] Variants Error:", e); }
if (l) { l.routes = sv.routes || []; l.name = sv.name || l.name; }
});
updateRouteLayers();
}
} catch (e) { console.error("Variants load error:", e); }
// 3. Nutzungen // 2. Eigentümer (SCHWERER LAYER)
try { try {
const res = await fetch(`${API_BASE}/usage`); const res = await fetch(`${API_BASE}/owners`);
if (res.ok) { if (res.ok) {
let gj = sanitize(await res.json()); let gj = sanitize(await res.json());
// Transform usage if UTM if (gj.features.length > 0) {
if (gj.features.length > 0) { const c = gj.features[0].geometry.coordinates;
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");
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();
} }
} catch (e) { console.error("Usage load error:", e); } state.owners = gj;
updateOwnerLayer();
}
} catch (e) { console.error("[V3-Sync] Owners Error:", e); }
// 4. WEA // 3. Nutzungen
try { try {
const res = await fetch(`${API_BASE}/wea`); const res = await fetch(`${API_BASE}/usage`);
if (res.ok) { if (res.ok) {
let gj = sanitize(await res.json()); let gj = sanitize(await res.json());
if (gj.features.length > 0) { if (gj.features.length > 0) {
if (Math.abs(gj.features[0].geometry.coordinates[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326"); 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");
state.wea = gj;
updateWEALayer();
} }
} catch (e) { console.error("WEA load error:", e); } updateUsageLayer(gj);
}
} catch (e) { console.error("[V3-Sync] Usage Error:", e); }
// 5. Infrastruktur console.log("[V3-Sync] Daten-Ladevorgang abgeschlossen.");
try { if (indicator) indicator.classList.remove('active');
const res = await fetch(`${API_BASE}/infrastructure`); isLoading = false;
if (res.ok) { renderVariants();
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();
}
} }
let isSaving = false; let isSaving = false;
async function saveToFolder() { async function saveToFolder() {
if (isSaving || !state.directoryHandle) return; if (isSaving || !state.directoryHandle) return;
@ -3117,6 +3094,26 @@
input.click(); 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 () => { window.addEventListener('load', async () => {
try { try {
initApp(); initApp();

149
server.js
View File

@ -28,32 +28,33 @@ app.use(express.static(__dirname));
// Helper to run the schema isolation command // Helper to run the schema isolation command
async function setSchema(client) { 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) => { 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 // 1. Get Owner Data
app.get('/api/owners', async (req, res) => { app.get('/api/owners', async (req, res) => {
console.log("Anfrage erhalten: /api/owners");
const client = await pool.connect(); const client = await pool.connect();
try { try {
await setSchema(client); await setSchema(client);
// 1. Log actual count in DB const countRes = await client.query('SELECT count(*) FROM "Eigentuemerdaten"');
const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Eigentuemerdaten"'); console.log(`Datenbank-Check: ${countRes.rows[0].count} Einträge in "Eigentuemerdaten".`);
console.log(`Datenbank-Check: ${countRes.rows[0].count} Eigentümer in bw_scheddebrock."Eigentuemerdaten" gefunden.`);
const query = ` const query = `
SELECT SELECT
*, *,
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
FROM bw_scheddebrock."Eigentuemerdaten" FROM "Eigentuemerdaten"
`; `;
const result = await client.query(query); const result = await client.query(query);
console.log(`Abfrage erfolgreich: ${result.rowCount} Zeilen für Frontend geladen.`);
const geojson = { const geojson = {
type: "FeatureCollection", type: "FeatureCollection",
@ -62,7 +63,7 @@ app.get('/api/owners', async (req, res) => {
const geomObj = typeof geometry === 'string' ? JSON.parse(geometry) : geometry; const geomObj = typeof geometry === 'string' ? JSON.parse(geometry) : geometry;
return { return {
type: "Feature", type: "Feature",
id: row.id || row.id_0, id: row.id,
geometry: geomObj, geometry: geomObj,
properties: properties properties: properties
}; };
@ -82,18 +83,13 @@ app.get('/api/usage', async (req, res) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
await setSchema(client); 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 = ` const query = `
SELECT SELECT
*, *,
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
FROM bw_scheddebrock."Nutzung" FROM "Nutzung"
`; `;
const result = await client.query(query); const result = await client.query(query);
console.log(`Abfrage erfolgreich: ${result.rowCount} Zeilen für Frontend geladen.`);
const geojson = { const geojson = {
type: "FeatureCollection", 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) => { app.get('/api/wea', async (req, res) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
@ -127,10 +123,9 @@ app.get('/api/wea', async (req, res) => {
SELECT SELECT
*, *,
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
FROM bw_scheddebrock."WEA" FROM "WEA"
`); `);
} catch (dbErr) { } catch (dbErr) {
console.log("WEA Tabelle fehlt oder nicht abrufbar.");
result = { rows: [], rowCount: 0 }; 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) => { app.get('/api/infrastructure', async (req, res) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
@ -166,10 +161,9 @@ app.get('/api/infrastructure', async (req, res) => {
SELECT SELECT
*, *,
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
FROM bw_scheddebrock."Infrastruktur" FROM "Infrastruktur"
`); `);
} catch (dbErr) { } catch (dbErr) {
console.log("Infrastruktur Tabelle fehlt oder nicht abrufbar.");
result = { rows: [], rowCount: 0 }; result = { rows: [], rowCount: 0 };
} }
@ -199,41 +193,32 @@ app.get('/api/variants', async (req, res) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
await setSchema(client); 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 = ` const query = `
SELECT SELECT
id_0 as id, name, "Variante", id, name, "Variante",
ST_AsGeoJSON(ST_Transform(ST_SetSRID(geom, 25832), 4326)) as geometry ST_AsGeoJSON(ST_Transform(ST_SetSRID(geom, 25832), 4326)) as geometry
FROM bw_scheddebrock."Kabeltrasse" FROM "Kabeltrasse"
ORDER BY id_0 DESC ORDER BY id DESC
`; `;
const result = await client.query(query); const result = await client.query(query);
console.log(`Abfrage erfolgreich: ${result.rowCount} Trassen geladen.`);
const variants = result.rows.map(row => { const featureCollection = {
let routes = []; type: "FeatureCollection",
const geomObj = typeof row.geometry === 'string' ? JSON.parse(row.geometry) : row.geometry; features: result.rows.map(row => {
if (geomObj && geomObj.coordinates) { const geomObj = JSON.parse(row.geometry || 'null');
if (geomObj.type === 'MultiLineString') { return {
routes = geomObj.coordinates.map(line => line.map(c => ({ lat: c[1], lng: c[0] }))); type: "Feature",
} else if (geomObj.type === 'LineString') { id: row.id,
routes = geomObj.coordinates.map(c => ({ lat: c[1], lng: c[0] })); geometry: geomObj,
} properties: {
} id: row.id,
return { name: row.name || (row.Variante ? `Variante ${row.Variante}` : 'Neue Trasse'),
id: row.id, Variante: row.Variante
name: row.name || row.Variante || 'Trasse', }
active: false, };
visible: true, })
stats: { total: 0, drilling: 0, open: 0, muffen: 0 }, };
routes: routes res.json(featureCollection);
};
});
res.json(variants);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(500).json({ error: 'Failed to fetch variants' }); res.status(500).json({ error: 'Failed to fetch variants' });
@ -244,52 +229,36 @@ app.get('/api/variants', async (req, res) => {
// 4. Save/Update Variant // 4. Save/Update Variant
app.post('/api/variants', async (req, res) => { 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 { geometry, properties } = req.body;
const client = await pool.connect(); const client = await pool.connect();
try { try {
await setSchema(client); 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 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 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 const upsertRes = await client.query(upsertQuery, [geoJsonStr, routeName, variante]);
// Or try to match it initially just in case it's a known database row. const finalId = upsertRes.rows[0].id;
const existing = await client.query('SELECT id_0 FROM bw_scheddebrock."Kabeltrasse" WHERE id_0 = $1', [Number(routeId) || 0]);
let result; res.json({ success: true, id: finalId });
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 });
} catch (err) { } catch (err) {
console.error("KRITISCHER SQL-FEHLER:", err.message); console.error("SQL-FEHLER (SAVE):", err.message);
res.status(500).json({ error: 'Failed to save variant: ' + err.message }); res.status(500).json({ error: 'Failed to save variant' });
} finally { } finally {
client.release(); client.release();
} }
@ -303,18 +272,18 @@ app.patch('/api/owners/:id', async (req, res) => {
try { try {
await setSchema(client); await setSchema(client);
const query = ` const query = `
UPDATE bw_scheddebrock."Eigentuemerdaten" UPDATE "Eigentuemerdaten"
SET status = $1, notiz = $2 SET status = $1, notiz = $2
WHERE id = $3 WHERE id = $3
RETURNING id; RETURNING id;
`; `;
const result = await client.query(query, [status, notiz, id]); const result = await client.query(query, [status, notiz, id]);
if (result.rowCount === 0) { 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 }); res.json({ success: true, id: result.rows[0].id });
} catch (err) { } catch (err) {
console.error("Owner Update Error:", err); console.error(err);
res.status(500).json({ error: 'Failed to update owner' }); res.status(500).json({ error: 'Failed to update owner' });
} finally { } finally {
client.release(); client.release();