Finalizing DB structure mapping and removing migrations
Deploy TrassenPlaner / deploy (push) Waiting to run
Details
Deploy TrassenPlaner / deploy (push) Waiting to run
Details
This commit is contained in:
parent
6d152ad3c9
commit
86f2e4c8ff
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
671
index.html
671
index.html
|
|
@ -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
149
server.js
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue