Enabled MultiLine segments for route variants
Deploy TrassenPlaner / deploy (push) Waiting to run Details

This commit is contained in:
Johannes Baumeister 2026-04-15 22:02:23 +02:00
parent 0cf7110f67
commit 60eadc4d38
1 changed files with 182 additions and 201 deletions

View File

@ -1888,44 +1888,52 @@
} }
}); });
// Helper function to flatten coordinates function getFlattenedCoords(raw) {
function getFlattenedCoords(rawRoutes) { if (!raw) return [];
let latLngs = []; // If it's a flat array of LatLngs
const flattenLL = (raw) => { if (raw.length > 0 && (raw[0].lat !== undefined || (Array.isArray(raw[0]) && typeof raw[0][0] === 'number'))) {
if (!raw) return; return raw.map(ll => L.latLng(ll));
if (typeof raw === 'object' && raw.lat !== undefined && !isNaN(raw.lat)) {
latLngs.push(L.latLng(raw));
} else if (Array.isArray(raw) && raw.length === 2 && typeof raw[0] === 'number') {
latLngs.push(L.latLng(raw));
} else if (Array.isArray(raw)) {
raw.forEach(item => flattenLL(item));
} }
}; // If it's nested
flattenLL(rawRoutes); const flattened = [];
return latLngs.map(ll => { raw.forEach(section => {
try { return L.latLng(ll); } catch (e) { return null; } if (Array.isArray(section)) {
}).filter(ll => ll && !isNaN(ll.lat) && !isNaN(ll.lng)); section.forEach(ll => flattened.push(L.latLng(ll)));
}
});
return flattened;
}
function getNestedCoords(raw) {
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)));
}
// If it's flat, wrap it
if (raw.length > 0) return [raw.map(ll => L.latLng(ll))];
return [];
} }
function renderSegmentLabels(variant, coordsInput = null, tempPt = null) { function renderSegmentLabels(variant, coordsInput = null, tempPt = null) {
const vLabel = labelLayers[variant.id]; const vLabel = labelLayers[variant.id];
if (!vLabel) return; if (!vLabel) return;
if (!variant.visible) { vLabel.clearLayers(); return; }
// Use passed coordinates or fall back to state const nested = coordsInput ? [coordsInput] : getNestedCoords(variant.routes);
let latLngs = coordsInput ? [...coordsInput] : getFlattenedCoords(variant.routes);
if (tempPt) latLngs.push(L.latLng(tempPt));
if (!variant.visible || latLngs.length < 2) {
vLabel.clearLayers();
return;
}
const currentMarkers = vLabel.getLayers(); const currentMarkers = vLabel.getLayers();
let markerIdx = 0; let markerIdx = 0;
for (let i = 0; i < latLngs.length - 1; i++) { nested.forEach((latLngs, sectionIdx) => {
const p1 = latLngs[i]; const drawLatLngs = [...latLngs];
const p2 = latLngs[i + 1]; // 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 (drawLatLngs.length < 2) return;
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 dist = map.distance(p1, p2);
const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2); const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2);
const labelText = `${dist.toFixed(0)}m`; const labelText = `${dist.toFixed(0)}m`;
@ -1934,26 +1942,14 @@
const m = currentMarkers[markerIdx]; const m = currentMarkers[markerIdx];
m.setLatLng(mid); m.setLatLng(mid);
const el = m.getElement(); const el = m.getElement();
let contentSpan = null;
if (el) { if (el) {
contentSpan = el.querySelector('.segment-label-content'); const span = el.querySelector('.segment-label-content');
} if (span) span.textContent = labelText;
if (contentSpan) {
contentSpan.textContent = labelText;
} else {
// Backup if element not yet rendered or span missing
m.setIcon(L.divIcon({
className: 'segment-label',
html: `<span class="segment-label-content">${labelText}</span>`,
iconSize: [46, 20],
iconAnchor: [23, 10]
}));
} }
} else { } else {
L.marker(mid, { L.marker(mid, {
interactive: false, interactive: false,
pane: 'labelPane', // Explicitly set pane pane: 'labelPane',
icon: L.divIcon({ icon: L.divIcon({
className: 'segment-label', className: 'segment-label',
html: `<span class="segment-label-content">${labelText}</span>`, html: `<span class="segment-label-content">${labelText}</span>`,
@ -1964,8 +1960,8 @@
} }
markerIdx++; markerIdx++;
} }
});
// Remove excess markers
while (markerIdx < currentMarkers.length) { while (markerIdx < currentMarkers.length) {
vLabel.removeLayer(currentMarkers[markerIdx++]); vLabel.removeLayer(currentMarkers[markerIdx++]);
} }
@ -1979,26 +1975,18 @@
const vDrill = drillingLayers[variant.id]; const vDrill = drillingLayers[variant.id];
if (vDrill) vDrill.clearLayers(); if (vDrill) vDrill.clearLayers();
const latLngs = getFlattenedCoords(variant.routes); const nested = getNestedCoords(variant.routes);
if (latLngs.length < 2) {
variant.stats.total = 0; variant.stats.total = 0;
variant.stats.drilling = 0; variant.stats.drilling = 0;
variant.stats.muffen = 0; variant.stats.muffen = 0;
return; variant.stats.hasTooLongDrilling = false;
} variant.drillingSegments = [];
if (nested.length === 0) return;
// Sync labels for final state
renderSegmentLabels(variant); renderSegmentLabels(variant);
// Pre-filter obstacles once
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
variant.stats.total = turf.length(line, { units: 'meters' });
const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]);
// --- Bohrungs-Logik (Optimized Version) ---
const drillingRanges = [];
// Only re-filter if cache is empty or data changed
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 => {
@ -2006,53 +1994,44 @@
return keywords.some(k => type.includes(k)); return keywords.some(k => type.includes(k));
}); });
} }
const currentObstacles = cachedObstacles || []; const currentObstacles = cachedObstacles || [];
nested.forEach(latLngs => {
if (latLngs.length < 2) return;
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) { if (currentObstacles.length > 0) {
const lineBbox = turf.bbox(line); const lineBbox = turf.bbox(line);
currentObstacles.forEach(obs => { currentObstacles.forEach(obs => {
try { try {
const obsBbox = turf.bbox(obs); const obsBbox = turf.bbox(obs);
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;
lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return;
if (turf.booleanIntersects(line, obs)) { if (turf.booleanIntersects(line, obs)) {
// Robust Intersection Strategy:
// 1. Find all intersection points
const intersect = turf.lineIntersect(line, obs); const intersect = turf.lineIntersect(line, obs);
let distances = [0, variant.stats.total]; let distances = [0, sectionLength];
intersect.features.forEach(f => { intersect.features.forEach(f => {
const d = turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }); distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }));
distances.push(d);
}); });
// 2. Sort unique distances to create segments
distances = [...new Set(distances)].sort((a, b) => a - b); distances = [...new Set(distances)].sort((a, b) => a - b);
// 3. Test the midpoint of each segment
for (let i = 0; i < distances.length - 1; i++) { for (let i = 0; i < distances.length - 1; i++) {
const dStart = distances[i]; const dStart = distances[i];
const dEnd = distances[i + 1]; const dEnd = distances[i + 1];
const midDist = (dStart + dEnd) / 2; const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' });
const midPt = turf.along(line, midDist / 1000, { units: 'kilometers' });
if (turf.booleanPointInPolygon(midPt, obs)) { if (turf.booleanPointInPolygon(midPt, obs)) {
drillingRanges.push([ drillingRanges.push([Math.max(0, dStart - 20), Math.min(sectionLength, dEnd + 20)]);
Math.max(0, dStart - 20),
Math.min(variant.stats.total, dEnd + 20)
]);
} }
} }
} }
} catch (e) { /* ignore error */ } } catch (e) {}
}); });
} }
// Merge
let mergedRanges = []; let mergedRanges = [];
if (drillingRanges.length > 0) { if (drillingRanges.length > 0) {
drillingRanges.sort((a, b) => a[0] - b[0]); drillingRanges.sort((a, b) => a[0] - b[0]);
@ -2064,31 +2043,24 @@
mergedRanges.push(cur); mergedRanges.push(cur);
} }
variant.stats.drilling = mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0); variant.stats.drilling += mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0);
variant.stats.open = Math.max(0, variant.stats.total - variant.stats.drilling); variant.stats.muffen += mergedRanges.length * 2;
variant.stats.muffen = mergedRanges.length * 2;
variant.stats.hasTooLongDrilling = false;
// Reconstruct geometry mergedRanges.forEach(range => {
variant.drillingSegments = mergedRanges.map(range => {
try {
const lengthM = range[1] - range[0]; const lengthM = range[1] - range[0];
if (lengthM > 180) variant.stats.hasTooLongDrilling = true; if (lengthM > 180) variant.stats.hasTooLongDrilling = true;
const s = turf.along(line, range[0] / 1000, { units: 'kilometers' }); const s = turf.along(line, range[0] / 1000, { units: 'kilometers' });
const e = turf.along(line, range[1] / 1000, { units: 'kilometers' }); const e = turf.along(line, range[1] / 1000, { units: 'kilometers' });
const sCoord = [s.geometry.coordinates[1], s.geometry.coordinates[0]]; variant.drillingSegments.push({
const eCoord = [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]]],
return { length: lengthM,
path: [sCoord, eCoord], muffen: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]]
muffen: [sCoord, eCoord], });
length: lengthM });
}; });
} catch (e) { return null; }
}).filter(s => s !== null); variant.stats.open = Math.max(0, variant.stats.total - variant.stats.drilling);
if (variant.visible) {
// Drilling & Muffen
if (vDrill) { if (vDrill) {
const drillingOpacity = variant.active ? 1 : 0.3; const drillingOpacity = variant.active ? 1 : 0.3;
variant.drillingSegments.forEach(seg => { variant.drillingSegments.forEach(seg => {
@ -2096,7 +2068,6 @@
color: '#000000', color: '#000000',
weight: 8, weight: 8,
opacity: drillingOpacity, opacity: drillingOpacity,
dashArray: '5, 10',
interactive: false, interactive: false,
pane: 'drillingPane' pane: 'drillingPane'
}).addTo(vDrill); }).addTo(vDrill);
@ -2130,7 +2101,6 @@
}).addTo(vDrill); }).addTo(vDrill);
}); });
}); });
}
// ONLY update the sidebar UI/list if this is the active variant // ONLY update the sidebar UI/list if this is the active variant
if (variant.active) { if (variant.active) {
@ -2151,11 +2121,14 @@
} }
try { try {
const latLngs = getFlattenedCoords(variant.routes); const nested = getNestedCoords(variant.routes);
if (latLngs.length < 2) return; if (nested.length === 0) return;
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat])); const lines = nested.filter(s => s.length >= 2).map(s => turf.lineString(s.map(ll => [ll.lng, ll.lat])));
const lineBbox = turf.bbox(line); 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 {
@ -2163,7 +2136,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(line, f); return turf.booleanIntersects(multiLine, f);
} catch (e) { return false; } } catch (e) { return false; }
}); });
@ -2216,6 +2189,13 @@
} }
// --- Variant Management UI --- // --- Variant Management UI ---
window.startNewPath = (id) => {
const layer = routeLayers[id];
if (layer && layer.editor) {
layer.editor.newPath();
}
};
function renderVariants() { function renderVariants() {
const container = document.getElementById('variant-controls'); const container = document.getElementById('variant-controls');
if (!container) return; if (!container) return;
@ -2284,6 +2264,7 @@
<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>