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_PASSWORD='${{ secrets.PASSWORD }}'" >> .env
|
||||
echo "DB_NAME=${{ secrets.NAME }}" >> .env
|
||||
echo "DB_SCHEMA=bw_scheddebrock" >> .env
|
||||
echo "DB_SCHEMA=wind_projekt_bwscheddebrock" >> .env
|
||||
|
||||
docker compose up -d --build --force-recreate
|
||||
docker image prune -f
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ services:
|
|||
- DB_USER=authentik
|
||||
- DB_PASSWORD=WX1t1cgP1qK09
|
||||
- DB_NAME=authentik
|
||||
- DB_SCHEMA=bw_scheddebrock
|
||||
- DB_SCHEMA=wind_projekt_bwscheddebrock
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
|
|
|
|||
671
index.html
671
index.html
|
|
@ -6,15 +6,15 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TrassenPlaner Pro</title>
|
||||
|
||||
<!-- Libraries (CDN via jsDelivr) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/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://cdn.jsdelivr.net/npm/@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://cdn.jsdelivr.net/npm/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://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/proj4@2.11.0/dist/proj4.js"></script>
|
||||
<!-- Libraries (CDN via unpkg) -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
|
||||
<script src="https://unpkg.com/shpjs@latest/dist/shp.js"></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-editable@1.2.0/src/Leaflet.Editable.js"></script>
|
||||
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.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/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>
|
||||
|
|
@ -837,8 +837,12 @@
|
|||
doc.setFontSize(9);
|
||||
const footerText = "Dieses Dokument wurde vom Auftragnehmer erstellt und aus dem Planungstool automatisch generiert.";
|
||||
|
||||
const latLngs = getFlattenedCoords(v.routes);
|
||||
const lineStr = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
|
||||
const nested = getNestedCoords(v.routes);
|
||||
if (nested.length === 0) return;
|
||||
const lines = nested.filter(s => s.length >= 2).map(s => turf.lineString(s.map(ll => [ll.lng, ll.lat])));
|
||||
if (lines.length === 0) return;
|
||||
|
||||
const lineStr = lines.length === 1 ? lines[0] : turf.multiLineString(lines.map(l => l.geometry.coordinates));
|
||||
const lineBbox = turf.bbox(lineStr);
|
||||
|
||||
const tableData = [];
|
||||
|
|
@ -1128,21 +1132,24 @@
|
|||
|
||||
const variant = state.variants.find(v => routeLayers[v.id] === layer);
|
||||
if (variant) {
|
||||
const liveCoords = getFlattenedCoords(layer.getLatLngs());
|
||||
const tempPt = (e.type === 'editable:drawing:move') ? e.latlng : null;
|
||||
|
||||
const isContinuous = ['editable:vertex:drag', 'editable:drawing:move', 'editable:drag', 'editable:editing'].includes(e.type);
|
||||
|
||||
// Always make sure variant.routes is up to date during dragging to allow calculateStats and labels to function properly
|
||||
// Single-path update
|
||||
if (isContinuous) {
|
||||
renderSegmentLabels(variant, liveCoords, tempPt);
|
||||
try {
|
||||
variant.routes = layer.getLatLngs();
|
||||
renderSegmentLabels(variant, variant.routes, tempPt);
|
||||
calculateStats(variant);
|
||||
updateVariantStatsUI(variant);
|
||||
} catch (err) { console.warn("Live render error:", err); }
|
||||
} else {
|
||||
variant.routes = liveCoords;
|
||||
variant.routes = layer.getLatLngs();
|
||||
calculateStats(variant);
|
||||
updateVariantStatsUI(variant);
|
||||
renderVariants();
|
||||
if (variant.active) updateRequiredPlots(variant);
|
||||
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(routeLayers[_activeV.id] || _activeV);
|
||||
saveVariantToDB(variant);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -1710,6 +1717,7 @@
|
|||
weight: 6,
|
||||
opacity: 1,
|
||||
editable: true,
|
||||
middleMarkers: true,
|
||||
pane: 'trassenPane',
|
||||
lineOptions: { color: '#cca300', weight: 6 },
|
||||
vertexOptions: { color: '#cca300', radius: 5 },
|
||||
|
|
@ -1741,15 +1749,16 @@
|
|||
|
||||
// Z-Index: Active variant to front
|
||||
if (v.active) {
|
||||
layer.bringToFront();
|
||||
if (layer.bringToFront) layer.bringToFront();
|
||||
if (drillingLayers[v.id]) drillingLayers[v.id].bringToFront();
|
||||
if (labelLayers[v.id]) labelLayers[v.id].bringToFront();
|
||||
}
|
||||
|
||||
// Sync geometry if not actively being dragged/edited
|
||||
if (!v.active) {
|
||||
// Direct sync for single LineString
|
||||
const isDrawing = v.active && map.editTools && map.editTools.drawing();
|
||||
if (!isDrawing) {
|
||||
try {
|
||||
layer.setLatLngs(v.routes);
|
||||
layer.setLatLngs(v.routes || []);
|
||||
} catch (e) {
|
||||
console.warn("Geometrie-Sync Warnung:", e);
|
||||
}
|
||||
|
|
@ -1764,41 +1773,27 @@
|
|||
|
||||
// Labels aktualisieren (Name in der Mitte der Linie auf weißem Hintergrund)
|
||||
labelLayer.clearLayers();
|
||||
if (v.routes && v.routes.length > 1) {
|
||||
// Find geometric middle of the polyline
|
||||
const points = layer.getLatLngs();
|
||||
let totalDist = 0;
|
||||
const distances = [];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const d = map.distance(points[i], points[i+1]);
|
||||
distances.push(d);
|
||||
totalDist += d;
|
||||
}
|
||||
const latLngs = (v.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(ll => !!ll && typeof ll.lat === 'number');
|
||||
if (latLngs.length >= 2) {
|
||||
try {
|
||||
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
|
||||
const totalKm = turf.length(line, { units: 'kilometers' });
|
||||
const midPoint = turf.along(line, totalKm / 2, { units: 'kilometers' });
|
||||
const midPos = L.latLng(midPoint.geometry.coordinates[1], midPoint.geometry.coordinates[0]);
|
||||
|
||||
let currentDist = 0;
|
||||
let midPos = points[0];
|
||||
for (let i = 0; i < distances.length; i++) {
|
||||
if (currentDist + distances[i] >= totalDist / 2) {
|
||||
// Midpoint is on this segment
|
||||
const ratio = (totalDist / 2 - currentDist) / distances[i];
|
||||
const lat = points[i].lat + (points[i+1].lat - points[i].lat) * ratio;
|
||||
const lng = points[i].lng + (points[i+1].lng - points[i].lng) * ratio;
|
||||
midPos = L.latLng(lat, lng);
|
||||
break;
|
||||
}
|
||||
currentDist += distances[i];
|
||||
L.marker(midPos, {
|
||||
pane: 'labelPane',
|
||||
icon: L.divIcon({
|
||||
className: 'variant-map-label',
|
||||
html: `<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);
|
||||
} catch (err) {
|
||||
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
|
||||
|
|
@ -1853,6 +1848,22 @@
|
|||
renderVariants();
|
||||
});
|
||||
|
||||
function isClickOnUI(e) {
|
||||
if (!e || !e.originalEvent) return false;
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const rightPanel = document.getElementById('right-panel');
|
||||
const target = e.originalEvent.target;
|
||||
const isMarkerLabel = target.closest && (target.closest('.variant-map-label') || target.closest('.segment-label'));
|
||||
return ((sidebar && sidebar.contains(target)) || (rightPanel && rightPanel.contains(target))) && !isMarkerLabel;
|
||||
}
|
||||
|
||||
map.on('click', (e) => {
|
||||
if (isClickOnUI(e)) return;
|
||||
if (state.isMeasuring) {
|
||||
addMeasurementPoint(e.latlng);
|
||||
}
|
||||
});
|
||||
|
||||
function updateToolCursors() {
|
||||
const container = map.getContainer();
|
||||
if (state.isDrawing) container.style.cursor = 'crosshair';
|
||||
|
|
@ -1863,59 +1874,48 @@
|
|||
document.getElementById('btn-measure').className = state.isMeasuring ? 'btn btn-primary' : 'btn btn-outline';
|
||||
}
|
||||
|
||||
map.on('click', (e) => {
|
||||
if (state.isMeasuring) {
|
||||
addMeasurementPoint(e.latlng);
|
||||
}
|
||||
});
|
||||
|
||||
// Capture changes (drawing, dragging, inserting, deleting)
|
||||
map.on('editable:drawing:clicked editable:drawing:move editable:drawing:end editable:created editable:vertex:drag editable:vertex:dragend editable:vertex:deleted editable:vertex:inserted', (e) => {
|
||||
const activeV = state.variants.find(v => v.active);
|
||||
if (!activeV) return;
|
||||
|
||||
// Enforce color on everything newly created or edited
|
||||
// Enforce color
|
||||
if (e.layer && e.layer.setStyle) {
|
||||
e.layer.setStyle({ color: '#cca300', weight: 6 });
|
||||
}
|
||||
|
||||
// If a new layer was created (via startPolyline or startNewPath), merge it into our existing layer
|
||||
if (e.type === 'editable:created' && activeV && routeLayers[activeV.id] && e.layer !== routeLayers[activeV.id]) {
|
||||
const current = getNestedCoords(routeLayers[activeV.id].getLatLngs());
|
||||
const addition = e.layer.getLatLngs();
|
||||
const merged = [...current, addition];
|
||||
|
||||
routeLayers[activeV.id].setLatLngs(merged);
|
||||
activeV.routes = merged;
|
||||
|
||||
map.removeLayer(e.layer); // Clean up temp layer
|
||||
routeLayers[activeV.id].enableEdit();
|
||||
} else if (routeLayers[activeV.id] && e.layer === routeLayers[activeV.id]) {
|
||||
// Sync logic for single line
|
||||
if (activeV && routeLayers[activeV.id] && e.layer === routeLayers[activeV.id]) {
|
||||
activeV.routes = e.layer.getLatLngs();
|
||||
}
|
||||
|
||||
// Live updates for labels, drillings and stats
|
||||
calculateStats(activeV);
|
||||
updateVariantStatsUI(activeV);
|
||||
if (e.type.includes('end') || e.type === 'editable:created') {
|
||||
updateVariantStatsUI(activeV);
|
||||
if (e.type.includes('end') || e.type === 'editable:created' || e.type.includes('dragend')) {
|
||||
renderVariants();
|
||||
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV);
|
||||
const _activeV = state.variants.find(v => v.active);
|
||||
if (_activeV) saveVariantToDB(_activeV);
|
||||
}
|
||||
});
|
||||
|
||||
function getFlattenedCoords(raw) {
|
||||
if (!raw) return [];
|
||||
// If it's a flat array of LatLngs
|
||||
if (raw.length > 0 && (raw[0].lat !== undefined || (Array.isArray(raw[0]) && typeof raw[0][0] === 'number'))) {
|
||||
// If it's a flat array of LatLngs already
|
||||
if (raw.length > 0 && raw[0] && (raw[0].lat !== undefined || (Array.isArray(raw[0]) && typeof raw[0][0] === 'number'))) {
|
||||
return raw.map(ll => L.latLng(ll));
|
||||
}
|
||||
// If it's nested
|
||||
// If it's nested [[LatLng...], [LatLng...]]
|
||||
const flattened = [];
|
||||
raw.forEach(section => {
|
||||
if (Array.isArray(section)) {
|
||||
section.forEach(ll => flattened.push(L.latLng(ll)));
|
||||
}
|
||||
});
|
||||
if (Array.isArray(raw)) {
|
||||
raw.forEach(section => {
|
||||
if (Array.isArray(section)) {
|
||||
section.forEach(ll => {
|
||||
if (ll) flattened.push(L.latLng(ll));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return flattened;
|
||||
}
|
||||
|
||||
|
|
@ -1923,10 +1923,20 @@
|
|||
if (!raw) return [];
|
||||
// Check if it's already nested
|
||||
if (raw.length > 0 && Array.isArray(raw[0]) && raw[0].length > 0 && (raw[0][0].lat !== undefined || Array.isArray(raw[0][0]))) {
|
||||
return raw.map(section => section.map(ll => L.latLng(ll)));
|
||||
return raw.map(section => {
|
||||
if (!Array.isArray(section)) return [];
|
||||
return section.map(ll => {
|
||||
try { return L.latLng(ll); } catch(e) { return null; }
|
||||
}).filter(ll => !!ll && typeof ll.lat === 'number');
|
||||
}).filter(s => s.length > 0);
|
||||
}
|
||||
// If it's flat, wrap it
|
||||
if (raw.length > 0) return [raw.map(ll => L.latLng(ll))];
|
||||
if (raw.length > 0) {
|
||||
const flat = raw.map(ll => {
|
||||
try { return L.latLng(ll); } catch(e) { return null; }
|
||||
}).filter(ll => !!ll && typeof ll.lat === 'number');
|
||||
return flat.length > 0 ? [flat] : [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -1935,47 +1945,49 @@
|
|||
if (!vLabel) return;
|
||||
if (!variant.visible) { vLabel.clearLayers(); return; }
|
||||
|
||||
const nested = coordsInput ? [coordsInput] : getNestedCoords(variant.routes);
|
||||
const latLngs = (coordsInput || variant.routes || []).map(ll => {
|
||||
try { return L.latLng(ll); } catch(e) { return null; }
|
||||
}).filter(p => !!p && typeof p.lat === 'number');
|
||||
|
||||
if (tempPt) latLngs.push(L.latLng(tempPt));
|
||||
|
||||
const currentMarkers = vLabel.getLayers();
|
||||
let markerIdx = 0;
|
||||
|
||||
nested.forEach((latLngs, sectionIdx) => {
|
||||
const drawLatLngs = [...latLngs];
|
||||
// Only add temp point to the very LAST section if we are actively drawing
|
||||
if (tempPt && sectionIdx === nested.length - 1) drawLatLngs.push(L.latLng(tempPt));
|
||||
if (latLngs.length < 2) {
|
||||
vLabel.clearLayers();
|
||||
return;
|
||||
}
|
||||
|
||||
if (drawLatLngs.length < 2) return;
|
||||
for (let i = 0; i < latLngs.length - 1; i++) {
|
||||
const p1 = latLngs[i];
|
||||
const p2 = latLngs[i + 1];
|
||||
const dist = map.distance(p1, p2);
|
||||
const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2);
|
||||
const labelText = `${dist.toFixed(0)}m`;
|
||||
|
||||
for (let i = 0; i < drawLatLngs.length - 1; i++) {
|
||||
const p1 = drawLatLngs[i];
|
||||
const p2 = drawLatLngs[i + 1];
|
||||
const dist = map.distance(p1, p2);
|
||||
const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2);
|
||||
const labelText = `${dist.toFixed(0)}m`;
|
||||
|
||||
if (markerIdx < currentMarkers.length) {
|
||||
const m = currentMarkers[markerIdx];
|
||||
m.setLatLng(mid);
|
||||
const el = m.getElement();
|
||||
if (el) {
|
||||
const span = el.querySelector('.segment-label-content');
|
||||
if (span) span.textContent = labelText;
|
||||
}
|
||||
} else {
|
||||
L.marker(mid, {
|
||||
interactive: false,
|
||||
pane: 'labelPane',
|
||||
icon: L.divIcon({
|
||||
className: 'segment-label',
|
||||
html: `<span class="segment-label-content">${labelText}</span>`,
|
||||
iconSize: [46, 20],
|
||||
iconAnchor: [23, 10]
|
||||
})
|
||||
}).addTo(vLabel);
|
||||
if (markerIdx < currentMarkers.length) {
|
||||
const m = currentMarkers[markerIdx];
|
||||
m.setLatLng(mid);
|
||||
const el = m.getElement();
|
||||
if (el) {
|
||||
const span = el.querySelector('.segment-label-content');
|
||||
if (span) span.textContent = labelText;
|
||||
}
|
||||
markerIdx++;
|
||||
} else {
|
||||
L.marker(mid, {
|
||||
interactive: false,
|
||||
pane: 'labelPane',
|
||||
icon: L.divIcon({
|
||||
className: 'segment-label',
|
||||
html: `<span class="segment-label-content">${labelText}</span>`,
|
||||
iconSize: [46, 20],
|
||||
iconAnchor: [23, 10]
|
||||
})
|
||||
}).addTo(vLabel);
|
||||
}
|
||||
});
|
||||
markerIdx++;
|
||||
}
|
||||
|
||||
while (markerIdx < currentMarkers.length) {
|
||||
vLabel.removeLayer(currentMarkers[markerIdx++]);
|
||||
|
|
@ -1990,18 +2002,24 @@
|
|||
const vDrill = drillingLayers[variant.id];
|
||||
if (vDrill) vDrill.clearLayers();
|
||||
|
||||
const nested = getNestedCoords(variant.routes);
|
||||
const latLngs = (variant.routes || []).map(ll => {
|
||||
try { return L.latLng(ll); } catch(e) { return null; }
|
||||
}).filter(p => !!p && typeof p.lat === 'number');
|
||||
|
||||
variant.stats.total = 0;
|
||||
variant.stats.drilling = 0;
|
||||
variant.stats.muffen = 0;
|
||||
variant.stats.hasTooLongDrilling = false;
|
||||
variant.drillingSegments = [];
|
||||
|
||||
if (nested.length === 0) return;
|
||||
if (latLngs.length < 2) {
|
||||
renderSegmentLabels(variant);
|
||||
return;
|
||||
}
|
||||
|
||||
renderSegmentLabels(variant);
|
||||
|
||||
// Pre-filter obstacles once
|
||||
// Pre-filter obstacles
|
||||
if (!cachedObstacles && state.usage.features) {
|
||||
const keywords = ['bahn', 'gewässer', 'wasser', 'straße', 'verkehr', 'gehölz', 'baufläche', 'wald', 'forst', 'hecke', 'weg', 'pfad', 'graben', 'bach', 'fluss'];
|
||||
cachedObstacles = state.usage.features.filter(f => {
|
||||
|
|
@ -2011,66 +2029,61 @@
|
|||
}
|
||||
const currentObstacles = cachedObstacles || [];
|
||||
|
||||
nested.forEach(latLngs => {
|
||||
if (latLngs.length < 2) return;
|
||||
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
|
||||
variant.stats.total = turf.length(line, { units: 'kilometers' }) * 1000;
|
||||
|
||||
const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]);
|
||||
const drillingRanges = [];
|
||||
|
||||
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
|
||||
const sectionLength = turf.length(line, { units: 'meters' });
|
||||
variant.stats.total += sectionLength;
|
||||
|
||||
const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]);
|
||||
const drillingRanges = [];
|
||||
|
||||
if (currentObstacles.length > 0) {
|
||||
const lineBbox = turf.bbox(line);
|
||||
currentObstacles.forEach(obs => {
|
||||
try {
|
||||
const obsBbox = turf.bbox(obs);
|
||||
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return;
|
||||
if (turf.booleanIntersects(line, obs)) {
|
||||
const intersect = turf.lineIntersect(line, obs);
|
||||
let distances = [0, sectionLength];
|
||||
intersect.features.forEach(f => {
|
||||
distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }));
|
||||
});
|
||||
distances = [...new Set(distances)].sort((a, b) => a - b);
|
||||
for (let i = 0; i < distances.length - 1; i++) {
|
||||
const dStart = distances[i];
|
||||
const dEnd = distances[i + 1];
|
||||
const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' });
|
||||
if (turf.booleanPointInPolygon(midPt, obs)) {
|
||||
drillingRanges.push([Math.max(0, dStart - 20), Math.min(sectionLength, dEnd + 20)]);
|
||||
}
|
||||
if (currentObstacles.length > 0) {
|
||||
const lineBbox = turf.bbox(line);
|
||||
currentObstacles.forEach(obs => {
|
||||
try {
|
||||
const obsBbox = turf.bbox(obs);
|
||||
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return;
|
||||
if (turf.booleanIntersects(line, obs)) {
|
||||
const intersect = turf.lineIntersect(line, obs);
|
||||
let distances = [0, variant.stats.total];
|
||||
intersect.features.forEach(f => {
|
||||
distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }));
|
||||
});
|
||||
distances = [...new Set(distances)].sort((a, b) => a - b);
|
||||
for (let i = 0; i < distances.length - 1; i++) {
|
||||
const dStart = distances[i];
|
||||
const dEnd = distances[i + 1];
|
||||
const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' });
|
||||
if (turf.booleanPointInPolygon(midPt, obs)) {
|
||||
drillingRanges.push([Math.max(0, dStart - 20), Math.min(variant.stats.total, dEnd + 20)]);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
let mergedRanges = [];
|
||||
if (drillingRanges.length > 0) {
|
||||
drillingRanges.sort((a, b) => a[0] - b[0]);
|
||||
let cur = drillingRanges[0];
|
||||
for (let i = 1; i < drillingRanges.length; i++) {
|
||||
if (drillingRanges[i][0] <= cur[1]) cur[1] = Math.max(cur[1], drillingRanges[i][1]);
|
||||
else { mergedRanges.push(cur); cur = drillingRanges[i]; }
|
||||
}
|
||||
mergedRanges.push(cur);
|
||||
}
|
||||
|
||||
let mergedRanges = [];
|
||||
if (drillingRanges.length > 0) {
|
||||
drillingRanges.sort((a, b) => a[0] - b[0]);
|
||||
let cur = drillingRanges[0];
|
||||
for (let i = 1; i < drillingRanges.length; i++) {
|
||||
if (drillingRanges[i][0] <= cur[1]) cur[1] = Math.max(cur[1], drillingRanges[i][1]);
|
||||
else { mergedRanges.push(cur); cur = drillingRanges[i]; }
|
||||
}
|
||||
mergedRanges.push(cur);
|
||||
}
|
||||
variant.stats.drilling += mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0);
|
||||
variant.stats.muffen += mergedRanges.length * 2;
|
||||
|
||||
variant.stats.drilling += mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0);
|
||||
variant.stats.muffen += mergedRanges.length * 2;
|
||||
|
||||
mergedRanges.forEach(range => {
|
||||
const lengthM = range[1] - range[0];
|
||||
if (lengthM > 180) variant.stats.hasTooLongDrilling = true;
|
||||
const s = turf.along(line, range[0] / 1000, { units: 'kilometers' });
|
||||
const e = turf.along(line, range[1] / 1000, { units: 'kilometers' });
|
||||
variant.drillingSegments.push({
|
||||
path: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]],
|
||||
length: lengthM,
|
||||
muffen: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]]
|
||||
});
|
||||
mergedRanges.forEach(range => {
|
||||
const lengthM = range[1] - range[0];
|
||||
if (lengthM > 180) variant.stats.hasTooLongDrilling = true;
|
||||
const s = turf.along(line, range[0] / 1000, { units: 'kilometers' });
|
||||
const e = turf.along(line, range[1] / 1000, { units: 'kilometers' });
|
||||
variant.drillingSegments.push({
|
||||
path: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]],
|
||||
length: lengthM,
|
||||
muffen: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]]
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -2129,21 +2142,17 @@
|
|||
if (!container) return;
|
||||
container.innerHTML = '<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 : []);
|
||||
if (!variant || latLngs.length < 2 || !state.owners || !state.owners.features || state.owners.features.length === 0) {
|
||||
const latLngs = (variant.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(p => !!p && typeof p.lat === 'number');
|
||||
const hasData = latLngs.length >= 2;
|
||||
|
||||
if (!variant || !hasData || !state.owners || !state.owners.features || state.owners.features.length === 0) {
|
||||
container.innerHTML += '<p style="font-size: 11px; color: #94a3b8; text-align: center; margin-top: 20px;">Keine Daten oder Trasse vorhanden</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const nested = getNestedCoords(variant.routes);
|
||||
if (nested.length === 0) return;
|
||||
|
||||
const lines = nested.filter(s => s.length >= 2).map(s => turf.lineString(s.map(ll => [ll.lng, ll.lat])));
|
||||
if (lines.length === 0) return;
|
||||
|
||||
const multiLine = lines.length === 1 ? lines[0] : turf.multiLineString(lines.map(l => l.geometry.coordinates));
|
||||
const lineBbox = turf.bbox(multiLine);
|
||||
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
|
||||
const lineBbox = turf.bbox(line);
|
||||
|
||||
const intersectingPlots = state.owners.features.filter(f => {
|
||||
try {
|
||||
|
|
@ -2151,7 +2160,7 @@
|
|||
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] ||
|
||||
lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return false;
|
||||
|
||||
return turf.booleanIntersects(multiLine, f);
|
||||
return turf.booleanIntersects(line, f);
|
||||
} catch (e) { return false; }
|
||||
});
|
||||
|
||||
|
|
@ -2206,6 +2215,10 @@
|
|||
// --- Variant Management UI ---
|
||||
window.startNewPath = (id) => {
|
||||
if (map.editTools) {
|
||||
const v = state.variants.find(v => v.id == id);
|
||||
if (v && !v.visible) {
|
||||
toggleVariantVisibility(v.id, true);
|
||||
}
|
||||
state.isDrawing = true;
|
||||
state.isMeasuring = false;
|
||||
updateToolCursors();
|
||||
|
|
@ -2281,7 +2294,6 @@
|
|||
<input type="checkbox" ${v.visible ? 'checked' : ''} onclick="toggleVariantVisibility(${v.id}, this.checked)">
|
||||
<i data-lucide="eye" style="width: 14px;"></i>
|
||||
</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="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>
|
||||
|
|
@ -2344,7 +2356,7 @@
|
|||
if (newName && newName.trim()) {
|
||||
v.name = newName.trim();
|
||||
renderVariants();
|
||||
if (activeV) saveVariantToDB(activeV);
|
||||
if (v) saveVariantToDB(v);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2519,6 +2531,7 @@
|
|||
|
||||
// Global Map Click for generous vertex insertion ("Click anywhere near the line")
|
||||
const handleVertexInsertion = (e) => {
|
||||
if (isClickOnUI(e)) return;
|
||||
if (state.isMeasuring) return;
|
||||
// VERY IMPORTANT: Don't interfere if we are actively drawing a path
|
||||
if (map.editTools && map.editTools.drawing()) return;
|
||||
|
|
@ -2529,24 +2542,14 @@
|
|||
const layer = routeLayers[variant.id];
|
||||
if (!layer) return;
|
||||
|
||||
// If the variant is empty, we don't return here anymore,
|
||||
// but we only attempt insertion if we have at least one segment (2 points).
|
||||
const latlngs = getFlattenedCoords(layer.getLatLngs());
|
||||
|
||||
// If drawing and clicking far away, we let startPolyline/continueForward handle it
|
||||
if (latlngs.length < 2) return;
|
||||
|
||||
if (!layer.editor) {
|
||||
layer.enableEdit();
|
||||
}
|
||||
|
||||
const allPoints = (variant.routes || []).map(ll => L.latLng(ll));
|
||||
let minIdx = -1;
|
||||
let minDist = Infinity;
|
||||
const pt = map.latLngToLayerPoint(e.latlng);
|
||||
|
||||
for (let i = 0; i < latlngs.length - 1; i++) {
|
||||
const p1 = map.latLngToLayerPoint(latlngs[i]);
|
||||
const p2 = map.latLngToLayerPoint(latlngs[i + 1]);
|
||||
for (let i = 0; i < allPoints.length - 1; i++) {
|
||||
const p1 = map.latLngToLayerPoint(allPoints[i]);
|
||||
const p2 = map.latLngToLayerPoint(allPoints[i + 1]);
|
||||
const dist = L.LineUtil.pointToSegmentDistance(pt, p1, p2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
|
|
@ -2554,29 +2557,27 @@
|
|||
}
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Check if we are clicking directly on or very near an existing vertex
|
||||
// If we are closer than 10 pixels to any vertex, we assume the user wants
|
||||
// to move the point, not insert a new one.
|
||||
const isNearVertex = latlngs.some(ll => map.latLngToLayerPoint(ll).distanceTo(pt) < 12);
|
||||
const isNearVertex = allPoints.some(ll => map.latLngToLayerPoint(ll).distanceTo(pt) < 12);
|
||||
if (isNearVertex) return;
|
||||
|
||||
// High tolerance hit-testing: 35 pixels (approx. the width of a finger/mouse inaccuracy)
|
||||
// High tolerance hit-testing: 35 pixels
|
||||
const tolerance = state.isDrawing ? 15 : 35;
|
||||
|
||||
if (minIdx !== -1 && minDist < tolerance) {
|
||||
latlngs.splice(minIdx + 1, 0, e.latlng);
|
||||
layer.setLatLngs(latlngs);
|
||||
allPoints.splice(minIdx + 1, 0, e.latlng);
|
||||
variant.routes = allPoints;
|
||||
layer.setLatLngs(allPoints);
|
||||
|
||||
if (layer.editor && layer.editor.reset) {
|
||||
layer.editor.reset();
|
||||
// FORCE REFRESH of editor markers
|
||||
if (layer.editor) {
|
||||
layer.disableEdit();
|
||||
layer.enableEdit();
|
||||
}
|
||||
|
||||
// Force synchronization
|
||||
variant.routes = getFlattenedCoords(layer.getLatLngs());
|
||||
calculateStats(variant);
|
||||
updateVariantStatsUI(variant);
|
||||
renderVariants();
|
||||
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV);
|
||||
saveVariantToDB(variant);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2691,46 +2692,37 @@
|
|||
}
|
||||
|
||||
async function saveVariantToDB(variantDef) {
|
||||
if (!variantDef) return;
|
||||
const indicator = document.getElementById('db-status-indicator');
|
||||
if (indicator) indicator.classList.add('active');
|
||||
|
||||
try {
|
||||
// Identify the true state variant regardless of what parameter was passed
|
||||
const v = variantDef.id ? variantDef : state.variants.find(vx => routeLayers[vx.id] === variantDef);
|
||||
// Ensure we have a valid variant object and points
|
||||
const v = variantDef.id ? variantDef : state.variants.find(vx => vx.id === variantDef.id);
|
||||
if (!v) {
|
||||
console.warn("Save skipped: Could not map to a valid variant.");
|
||||
return;
|
||||
}
|
||||
|
||||
const name = v.name || "Neue Trasse";
|
||||
let rawRoutes = v.routes || [];
|
||||
|
||||
// Fallback attempt if routes is somehow empty but layer exists
|
||||
if (rawRoutes.length === 0 && routeLayers[v.id] && typeof routeLayers[v.id].getLatLngs === 'function') {
|
||||
rawRoutes = routeLayers[v.id].getLatLngs();
|
||||
if (indicator) indicator.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rawRoutes || rawRoutes.length === 0) {
|
||||
console.warn("Save skipped: No route data available.");
|
||||
// Simple flat coordinates from LineString
|
||||
const latLngs = (v.routes || []).map(ll => {
|
||||
try { return L.latLng(ll); } catch(e) { return null; }
|
||||
}).filter(p => !!p && typeof p.lat === 'number');
|
||||
|
||||
if (latLngs.length < 2) {
|
||||
if (indicator) indicator.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
|
||||
// Leaflet-Punkte in GeoJSON-Format umwandeln [lng, lat]
|
||||
let geoJsonCoords = [];
|
||||
const isMulti = routeLayers[v.id] && Array.isArray(rawRoutes[0]);
|
||||
|
||||
if (isMulti || Array.isArray(rawRoutes[0])) {
|
||||
geoJsonCoords = rawRoutes.map(line => line.map(p => [p.lng, p.lat]));
|
||||
} else {
|
||||
geoJsonCoords = rawRoutes.map(p => [p.lng, p.lat]);
|
||||
}
|
||||
|
||||
const geomType = (isMulti || Array.isArray(rawRoutes[0])) ? "MultiLineString" : "LineString";
|
||||
|
||||
const payload = { id: v.id, geometry: {
|
||||
type: geoJsonCoords.length ? geomType : "LineString",
|
||||
coordinates: geoJsonCoords
|
||||
const payload = {
|
||||
id: v.id,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: latLngs.map(p => [p.lng, p.lat])
|
||||
},
|
||||
properties: {
|
||||
name: name,
|
||||
Variante: name.replace('Variante ', '')
|
||||
name: v.name,
|
||||
Variante: v.name.replace('Variante ', '')
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2748,113 +2740,98 @@
|
|||
throw new Error('Server-Fehler: ' + errorText);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.id) {
|
||||
v.id = result.id; // Sync the database ID back to the state
|
||||
}
|
||||
console.log("Speichern in Datenbank erfolgreich!");
|
||||
} catch (err) {
|
||||
console.error("DB Save failed:", err.message);
|
||||
// alert("Fehler beim Speichern: " + err.message); // Commented to prevent spam during map moves
|
||||
} finally {
|
||||
if (indicator) indicator.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFromDatabase() {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
const indicator = document.getElementById('db-status-indicator');
|
||||
if (indicator) indicator.classList.add('active');
|
||||
console.log("[V3-Sync] Starte Daten-Abruf vom Server...");
|
||||
|
||||
const sanitize = (gj) => {
|
||||
if (!gj || !gj.features) return { type: "FeatureCollection", features: [] };
|
||||
return gj;
|
||||
};
|
||||
|
||||
// 1. Varianten (JETZT ALS ERSTES)
|
||||
try {
|
||||
console.log("Starte Daten-Abruf vom Server...");
|
||||
|
||||
const isVal = (c) => Array.isArray(c) && c.length >= 2 && !isNaN(c[0]) && !isNaN(c[1]) && c[0] !== null && c[1] !== null;
|
||||
const sanitize = (gj) => {
|
||||
if (!gj || !gj.features) return { type: "FeatureCollection", features: [] };
|
||||
return {
|
||||
...gj,
|
||||
features: gj.features.filter(f => {
|
||||
if (!f || !f.geometry || !f.geometry.coordinates) return false;
|
||||
// Recursive coordinate check for simple structures
|
||||
if (f.geometry.type === 'Point') return isVal(f.geometry.coordinates);
|
||||
return true; // Polygons are harder to check deeply, but usually safer
|
||||
})
|
||||
};
|
||||
};
|
||||
const res = await fetch(`${API_BASE}/variants`);
|
||||
if (res.ok) {
|
||||
const rawData = await res.json();
|
||||
let features = Array.isArray(rawData) ? rawData.map(item => ({
|
||||
type: "Feature",
|
||||
id: item.id,
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: (Array.isArray(item.routes[0]) ? item.routes[0] : item.routes)
|
||||
.map(p => [(p.lng || p[0]), (p.lat || p[1])])
|
||||
},
|
||||
properties: { Variante: item.name ? item.name.replace('Variante ', '') : 'A', name: item.name }
|
||||
})) : (rawData.features || []);
|
||||
|
||||
// 1. Eigentümer
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/owners`);
|
||||
if (res.ok) {
|
||||
let gj = sanitize(await res.json());
|
||||
if (gj.features.length > 0) {
|
||||
// Check for UTM transform
|
||||
const c = gj.features[0].geometry.coordinates;
|
||||
const test = Array.isArray(c[0]) ? (Array.isArray(c[0][0]) ? c[0][0][0] : c[0][0]) : c[0];
|
||||
if (Math.abs(test) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
|
||||
console.log(`[V3-Sync] ${features.length} Trassen geladen.`);
|
||||
|
||||
features.forEach(f => {
|
||||
const p = f.properties || {};
|
||||
const varName = p.Variante ? `Variante ${p.Variante}` : (p.name || "Variante A");
|
||||
const slot = state.variants.find(lv => lv.name === varName);
|
||||
if (slot && f.geometry && f.geometry.coordinates) {
|
||||
slot.id = f.id || p.id || slot.id;
|
||||
slot.routes = f.geometry.coordinates.map(c => ({ lat: c[1], lng: c[0] }));
|
||||
console.log(`[V3-Sync] ${slot.name} erfolgreich gemappt.`);
|
||||
}
|
||||
state.owners = gj;
|
||||
updateOwnerLayer();
|
||||
if (layers.owners.getBounds().isValid()) map.fitBounds(layers.owners.getBounds());
|
||||
}
|
||||
} catch (e) { console.error("Owners load error:", e); }
|
||||
});
|
||||
|
||||
// 2. Varianten
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/variants`);
|
||||
if (res.ok) {
|
||||
const v = await res.json();
|
||||
v.forEach(sv => {
|
||||
const l = state.variants.find(lv => lv.id === sv.id);
|
||||
if (l) { l.routes = sv.routes || []; l.name = sv.name || l.name; }
|
||||
});
|
||||
updateRouteLayers();
|
||||
}
|
||||
} catch (e) { console.error("Variants load error:", e); }
|
||||
state.variants.forEach(v => { calculateStats(v); updateVariantStatsUI(v); });
|
||||
const varA = state.variants.find(v => v.name === "Variante A");
|
||||
if (varA) setActiveVariant(varA.id);
|
||||
renderVariants();
|
||||
updateRouteLayers();
|
||||
}
|
||||
} catch (e) { console.error("[V3-Sync] Variants Error:", e); }
|
||||
|
||||
// 3. Nutzungen
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/usage`);
|
||||
if (res.ok) {
|
||||
let gj = sanitize(await res.json());
|
||||
// Transform usage if UTM
|
||||
if (gj.features.length > 0) {
|
||||
const c = gj.features[0].geometry.coordinates;
|
||||
const test = Array.isArray(c[0]) ? (Array.isArray(c[0][0]) ? c[0][0][0] : c[0][0]) : c[0];
|
||||
if (Math.abs(test) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
|
||||
}
|
||||
state.usage = gj;
|
||||
updateUsageLayer();
|
||||
// 2. Eigentümer (SCHWERER LAYER)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/owners`);
|
||||
if (res.ok) {
|
||||
let gj = sanitize(await res.json());
|
||||
if (gj.features.length > 0) {
|
||||
const c = gj.features[0].geometry.coordinates;
|
||||
if (Math.abs(Array.isArray(c[0]) ? c[0][0] : c[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
|
||||
}
|
||||
} catch (e) { console.error("Usage load error:", e); }
|
||||
state.owners = gj;
|
||||
updateOwnerLayer();
|
||||
}
|
||||
} catch (e) { console.error("[V3-Sync] Owners Error:", e); }
|
||||
|
||||
// 4. WEA
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/wea`);
|
||||
if (res.ok) {
|
||||
let gj = sanitize(await res.json());
|
||||
if (gj.features.length > 0) {
|
||||
if (Math.abs(gj.features[0].geometry.coordinates[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
|
||||
}
|
||||
state.wea = gj;
|
||||
updateWEALayer();
|
||||
// 3. Nutzungen
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/usage`);
|
||||
if (res.ok) {
|
||||
let gj = sanitize(await res.json());
|
||||
if (gj.features.length > 0) {
|
||||
const c = gj.features[0].geometry.coordinates;
|
||||
if (Math.abs(Array.isArray(c[0]) ? c[0][0] : c[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
|
||||
}
|
||||
} catch (e) { console.error("WEA load error:", e); }
|
||||
updateUsageLayer(gj);
|
||||
}
|
||||
} catch (e) { console.error("[V3-Sync] Usage Error:", e); }
|
||||
|
||||
// 5. Infrastruktur
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/infrastructure`);
|
||||
if (res.ok) {
|
||||
let gj = sanitize(await res.json());
|
||||
if (gj.features.length > 0) {
|
||||
if (Math.abs(gj.features[0].geometry.coordinates[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
|
||||
}
|
||||
state.infrastructure = gj;
|
||||
updateInfrastructureLayer();
|
||||
}
|
||||
} catch (e) { console.error("Infra load error:", e); }
|
||||
|
||||
console.log("Daten-Ladevorgang abgeschlossen.");
|
||||
} catch (err) {
|
||||
console.error("Kritischer Fehler beim Laden der Datenbank:", err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
renderVariants();
|
||||
}
|
||||
console.log("[V3-Sync] Daten-Ladevorgang abgeschlossen.");
|
||||
if (indicator) indicator.classList.remove('active');
|
||||
isLoading = false;
|
||||
renderVariants();
|
||||
}
|
||||
|
||||
let isSaving = false;
|
||||
async function saveToFolder() {
|
||||
if (isSaving || !state.directoryHandle) return;
|
||||
|
|
@ -3117,6 +3094,26 @@
|
|||
input.click();
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (map.editTools) {
|
||||
if (map.editTools.drawing()) {
|
||||
map.editTools.stopDrawing();
|
||||
}
|
||||
state.isDrawing = false;
|
||||
state.isMeasuring = false;
|
||||
updateToolCursors();
|
||||
renderVariants();
|
||||
|
||||
const activeV = state.variants.find(v => v.active);
|
||||
if (activeV && routeLayers[activeV.id]) {
|
||||
routeLayers[activeV.id].enableEdit();
|
||||
saveVariantToDB(activeV);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('load', async () => {
|
||||
try {
|
||||
initApp();
|
||||
|
|
|
|||
149
server.js
149
server.js
|
|
@ -28,32 +28,33 @@ app.use(express.static(__dirname));
|
|||
|
||||
// Helper to run the schema isolation command
|
||||
async function setSchema(client) {
|
||||
await client.query(`SET search_path TO ${process.env.DB_SCHEMA}, public;`);
|
||||
const schema = process.env.DB_SCHEMA || 'wind_projekt_bwscheddebrock';
|
||||
await client.query(`SET search_path TO "${schema}", public;`);
|
||||
}
|
||||
|
||||
// Routes
|
||||
// Database Initial Setup / Migration Logic Removed
|
||||
// (Now treating existing tables as the read-only 'Source of Truth')
|
||||
|
||||
// --- API Routes ---
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'OK', time: new Date().toISOString() });
|
||||
res.json({ status: 'OK', schema: process.env.DB_SCHEMA, time: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// 1. Get Owner Data
|
||||
app.get('/api/owners', async (req, res) => {
|
||||
console.log("Anfrage erhalten: /api/owners");
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await setSchema(client);
|
||||
// 1. Log actual count in DB
|
||||
const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Eigentuemerdaten"');
|
||||
console.log(`Datenbank-Check: ${countRes.rows[0].count} Eigentümer in bw_scheddebrock."Eigentuemerdaten" gefunden.`);
|
||||
const countRes = await client.query('SELECT count(*) FROM "Eigentuemerdaten"');
|
||||
console.log(`Datenbank-Check: ${countRes.rows[0].count} Einträge in "Eigentuemerdaten".`);
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
*,
|
||||
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
|
||||
FROM bw_scheddebrock."Eigentuemerdaten"
|
||||
FROM "Eigentuemerdaten"
|
||||
`;
|
||||
const result = await client.query(query);
|
||||
console.log(`Abfrage erfolgreich: ${result.rowCount} Zeilen für Frontend geladen.`);
|
||||
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
|
|
@ -62,7 +63,7 @@ app.get('/api/owners', async (req, res) => {
|
|||
const geomObj = typeof geometry === 'string' ? JSON.parse(geometry) : geometry;
|
||||
return {
|
||||
type: "Feature",
|
||||
id: row.id || row.id_0,
|
||||
id: row.id,
|
||||
geometry: geomObj,
|
||||
properties: properties
|
||||
};
|
||||
|
|
@ -82,18 +83,13 @@ app.get('/api/usage', async (req, res) => {
|
|||
const client = await pool.connect();
|
||||
try {
|
||||
await setSchema(client);
|
||||
// 1. Log actual count in DB
|
||||
const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Nutzung"');
|
||||
console.log(`Datenbank-Check: ${countRes.rows[0].count} Nutzungs-Flächen in bw_scheddebrock."Nutzung" gefunden.`);
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
*,
|
||||
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
|
||||
FROM bw_scheddebrock."Nutzung"
|
||||
FROM "Nutzung"
|
||||
`;
|
||||
const result = await client.query(query);
|
||||
console.log(`Abfrage erfolgreich: ${result.rowCount} Zeilen für Frontend geladen.`);
|
||||
|
||||
const geojson = {
|
||||
type: "FeatureCollection",
|
||||
|
|
@ -116,7 +112,7 @@ app.get('/api/usage', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// 2.5 Get WEA Data (Windturbinen)
|
||||
// 2.5 Get WEA Data
|
||||
app.get('/api/wea', async (req, res) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
|
|
@ -127,10 +123,9 @@ app.get('/api/wea', async (req, res) => {
|
|||
SELECT
|
||||
*,
|
||||
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
|
||||
FROM bw_scheddebrock."WEA"
|
||||
FROM "WEA"
|
||||
`);
|
||||
} catch (dbErr) {
|
||||
console.log("WEA Tabelle fehlt oder nicht abrufbar.");
|
||||
result = { rows: [], rowCount: 0 };
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +150,7 @@ app.get('/api/wea', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// 2.7 Get Infrastructure Data (UW, etc)
|
||||
// 2.7 Get Infrastructure Data
|
||||
app.get('/api/infrastructure', async (req, res) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
|
|
@ -166,10 +161,9 @@ app.get('/api/infrastructure', async (req, res) => {
|
|||
SELECT
|
||||
*,
|
||||
ST_AsGeoJSON(ST_Transform(geom, 4326)) as geometry
|
||||
FROM bw_scheddebrock."Infrastruktur"
|
||||
FROM "Infrastruktur"
|
||||
`);
|
||||
} catch (dbErr) {
|
||||
console.log("Infrastruktur Tabelle fehlt oder nicht abrufbar.");
|
||||
result = { rows: [], rowCount: 0 };
|
||||
}
|
||||
|
||||
|
|
@ -199,41 +193,32 @@ app.get('/api/variants', async (req, res) => {
|
|||
const client = await pool.connect();
|
||||
try {
|
||||
await setSchema(client);
|
||||
|
||||
// Log count
|
||||
const countRes = await client.query('SELECT count(*) FROM bw_scheddebrock."Kabeltrasse"');
|
||||
console.log(`Datenbank-Check: ${countRes.rows[0].count} Kabeltrassen in bw_scheddebrock."Kabeltrasse" gefunden.`);
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
id_0 as id, name, "Variante",
|
||||
id, name, "Variante",
|
||||
ST_AsGeoJSON(ST_Transform(ST_SetSRID(geom, 25832), 4326)) as geometry
|
||||
FROM bw_scheddebrock."Kabeltrasse"
|
||||
ORDER BY id_0 DESC
|
||||
FROM "Kabeltrasse"
|
||||
ORDER BY id DESC
|
||||
`;
|
||||
const result = await client.query(query);
|
||||
console.log(`Abfrage erfolgreich: ${result.rowCount} Trassen geladen.`);
|
||||
|
||||
const variants = result.rows.map(row => {
|
||||
let routes = [];
|
||||
const geomObj = typeof row.geometry === 'string' ? JSON.parse(row.geometry) : row.geometry;
|
||||
if (geomObj && geomObj.coordinates) {
|
||||
if (geomObj.type === 'MultiLineString') {
|
||||
routes = geomObj.coordinates.map(line => line.map(c => ({ lat: c[1], lng: c[0] })));
|
||||
} else if (geomObj.type === 'LineString') {
|
||||
routes = geomObj.coordinates.map(c => ({ lat: c[1], lng: c[0] }));
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name || row.Variante || 'Trasse',
|
||||
active: false,
|
||||
visible: true,
|
||||
stats: { total: 0, drilling: 0, open: 0, muffen: 0 },
|
||||
routes: routes
|
||||
};
|
||||
});
|
||||
res.json(variants);
|
||||
const featureCollection = {
|
||||
type: "FeatureCollection",
|
||||
features: result.rows.map(row => {
|
||||
const geomObj = JSON.parse(row.geometry || 'null');
|
||||
return {
|
||||
type: "Feature",
|
||||
id: row.id,
|
||||
geometry: geomObj,
|
||||
properties: {
|
||||
id: row.id,
|
||||
name: row.name || (row.Variante ? `Variante ${row.Variante}` : 'Neue Trasse'),
|
||||
Variante: row.Variante
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
res.json(featureCollection);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to fetch variants' });
|
||||
|
|
@ -244,52 +229,36 @@ app.get('/api/variants', async (req, res) => {
|
|||
|
||||
// 4. Save/Update Variant
|
||||
app.post('/api/variants', async (req, res) => {
|
||||
console.log("Empfangene Daten (Payload):", JSON.stringify(req.body).substring(0, 200) + "...");
|
||||
const { geometry, properties } = req.body;
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await setSchema(client);
|
||||
|
||||
console.log("Starte PostgreSQL-Query...");
|
||||
|
||||
// Clean UPSERT by Database ID.
|
||||
const routeId = req.body.id;
|
||||
const routeName = properties.name || 'Neue Trasse';
|
||||
const variante = properties.Variante || properties.name || 'A';
|
||||
const variante = properties.Variante || (properties.name ? properties.name.replace('Variante ', '') : 'A');
|
||||
const geoJsonStr = JSON.stringify(geometry);
|
||||
|
||||
const upsertQuery = `
|
||||
INSERT INTO "Kabeltrasse" (geom, name, "Variante")
|
||||
VALUES (
|
||||
ST_MakeValid(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 25832)),
|
||||
$2,
|
||||
$3
|
||||
)
|
||||
ON CONFLICT ("Variante")
|
||||
DO UPDATE SET
|
||||
geom = EXCLUDED.geom,
|
||||
name = EXCLUDED.name
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
// Check if the passed ID is a valid database ID (small int) vs a frontend dummy Date.now() timestamp
|
||||
// Or try to match it initially just in case it's a known database row.
|
||||
const existing = await client.query('SELECT id_0 FROM bw_scheddebrock."Kabeltrasse" WHERE id_0 = $1', [Number(routeId) || 0]);
|
||||
const upsertRes = await client.query(upsertQuery, [geoJsonStr, routeName, variante]);
|
||||
const finalId = upsertRes.rows[0].id;
|
||||
|
||||
let result;
|
||||
if (existing.rowCount > 0) {
|
||||
// EXACT match found physically in DB, do an UPDATE
|
||||
const updateQuery = `
|
||||
UPDATE bw_scheddebrock."Kabeltrasse"
|
||||
SET geom = ST_MakeValid(ST_SetSRID(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 25832), 25832)),
|
||||
name = $2,
|
||||
"Variante" = $3
|
||||
WHERE id_0 = $4
|
||||
RETURNING id_0 as id;
|
||||
`;
|
||||
result = await client.query(updateQuery, [geoJsonStr, routeName, variante, existing.rows[0].id_0]);
|
||||
console.log(`PostgreSQL-Ergebnis: Zeile ${existing.rows[0].id_0} aktualisiert (Variante: ${routeName})!`);
|
||||
} else {
|
||||
// INSERT new
|
||||
const insertQuery = `
|
||||
INSERT INTO bw_scheddebrock."Kabeltrasse" (geom, name, "Variante")
|
||||
VALUES (ST_MakeValid(ST_SetSRID(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 25832), 25832)), $2, $3)
|
||||
RETURNING id_0 as id;
|
||||
`;
|
||||
result = await client.query(insertQuery, [geoJsonStr, routeName, variante]);
|
||||
console.log(`PostgreSQL-Ergebnis: Zeile eingefügt! ID: ${result.rows[0].id}`);
|
||||
}
|
||||
|
||||
res.json({ success: true, id: result.rows[0].id });
|
||||
res.json({ success: true, id: finalId });
|
||||
} catch (err) {
|
||||
console.error("KRITISCHER SQL-FEHLER:", err.message);
|
||||
res.status(500).json({ error: 'Failed to save variant: ' + err.message });
|
||||
console.error("SQL-FEHLER (SAVE):", err.message);
|
||||
res.status(500).json({ error: 'Failed to save variant' });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
|
@ -303,18 +272,18 @@ app.patch('/api/owners/:id', async (req, res) => {
|
|||
try {
|
||||
await setSchema(client);
|
||||
const query = `
|
||||
UPDATE bw_scheddebrock."Eigentuemerdaten"
|
||||
UPDATE "Eigentuemerdaten"
|
||||
SET status = $1, notiz = $2
|
||||
WHERE id = $3
|
||||
RETURNING id;
|
||||
`;
|
||||
const result = await client.query(query, [status, notiz, id]);
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ error: 'Owner not found in database' });
|
||||
return res.status(404).json({ error: 'Owner not found' });
|
||||
}
|
||||
res.json({ success: true, id: result.rows[0].id });
|
||||
} catch (err) {
|
||||
console.error("Owner Update Error:", err);
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Failed to update owner' });
|
||||
} finally {
|
||||
client.release();
|
||||
|
|
|
|||
Loading…
Reference in New Issue