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,84 +1888,80 @@
} }
}); });
// 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)); // If it's nested
} else if (Array.isArray(raw) && raw.length === 2 && typeof raw[0] === 'number') { const flattened = [];
latLngs.push(L.latLng(raw)); raw.forEach(section => {
} else if (Array.isArray(raw)) { if (Array.isArray(section)) {
raw.forEach(item => flattenLL(item)); section.forEach(ll => flattened.push(L.latLng(ll)));
} }
}; });
flattenLL(rawRoutes); return flattened;
return latLngs.map(ll => { }
try { return L.latLng(ll); } catch (e) { return null; }
}).filter(ll => ll && !isNaN(ll.lat) && !isNaN(ll.lng)); 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
const dist = map.distance(p1, p2); if (tempPt && sectionIdx === nested.length - 1) drawLatLngs.push(L.latLng(tempPt));
const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2);
const labelText = `${dist.toFixed(0)}m`;
if (markerIdx < currentMarkers.length) { if (drawLatLngs.length < 2) return;
const m = currentMarkers[markerIdx];
m.setLatLng(mid);
const el = m.getElement();
let contentSpan = null;
if (el) {
contentSpan = el.querySelector('.segment-label-content');
}
if (contentSpan) { for (let i = 0; i < drawLatLngs.length - 1; i++) {
contentSpan.textContent = labelText; 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 { } else {
// Backup if element not yet rendered or span missing L.marker(mid, {
m.setIcon(L.divIcon({ interactive: false,
className: 'segment-label', pane: 'labelPane',
html: `<span class="segment-label-content">${labelText}</span>`, icon: L.divIcon({
iconSize: [46, 20], className: 'segment-label',
iconAnchor: [23, 10] html: `<span class="segment-label-content">${labelText}</span>`,
})); iconSize: [46, 20],
iconAnchor: [23, 10]
})
}).addTo(vLabel);
} }
} else { markerIdx++;
L.marker(mid, {
interactive: false,
pane: 'labelPane', // Explicitly set pane
icon: L.divIcon({
className: 'segment-label',
html: `<span class="segment-label-content">${labelText}</span>`,
iconSize: [46, 20],
iconAnchor: [23, 10]
})
}).addTo(vLabel);
} }
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; variant.stats.hasTooLongDrilling = false;
return; 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,132 +1994,114 @@
return keywords.some(k => type.includes(k)); return keywords.some(k => type.includes(k));
}); });
} }
const currentObstacles = cachedObstacles || []; const currentObstacles = cachedObstacles || [];
if (currentObstacles.length > 0) { nested.forEach(latLngs => {
const lineBbox = turf.bbox(line); if (latLngs.length < 2) return;
currentObstacles.forEach(obs => { const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
try { const sectionLength = turf.length(line, { units: 'meters' });
const obsBbox = turf.bbox(obs); variant.stats.total += sectionLength;
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] ||
lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return; const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]);
const drillingRanges = [];
if (turf.booleanIntersects(line, obs)) { if (currentObstacles.length > 0) {
// Robust Intersection Strategy: const lineBbox = turf.bbox(line);
// 1. Find all intersection points currentObstacles.forEach(obs => {
const intersect = turf.lineIntersect(line, obs); try {
let distances = [0, variant.stats.total]; const obsBbox = turf.bbox(obs);
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return;
intersect.features.forEach(f => { if (turf.booleanIntersects(line, obs)) {
const d = turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }); const intersect = turf.lineIntersect(line, obs);
distances.push(d); let distances = [0, sectionLength];
}); intersect.features.forEach(f => {
distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }));
// 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);
for (let i = 0; i < distances.length - 1; i++) {
// 3. Test the midpoint of each segment const dStart = distances[i];
for (let i = 0; i < distances.length - 1; i++) { const dEnd = distances[i + 1];
const dStart = distances[i]; const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' });
const dEnd = distances[i + 1]; if (turf.booleanPointInPolygon(midPt, obs)) {
const midDist = (dStart + dEnd) / 2; drillingRanges.push([Math.max(0, dStart - 20), Math.min(sectionLength, dEnd + 20)]);
}
const midPt = turf.along(line, midDist / 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) { /* ignore error */ }
});
}
// Merge
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.open = Math.max(0, variant.stats.total - variant.stats.drilling);
variant.stats.muffen = mergedRanges.length * 2;
variant.stats.hasTooLongDrilling = false;
// Reconstruct geometry
variant.drillingSegments = mergedRanges.map(range => {
try {
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' });
const sCoord = [s.geometry.coordinates[1], s.geometry.coordinates[0]];
const eCoord = [e.geometry.coordinates[1], e.geometry.coordinates[0]];
return {
path: [sCoord, eCoord],
muffen: [sCoord, eCoord],
length: lengthM
};
} catch (e) { return null; }
}).filter(s => s !== null);
if (variant.visible) {
// Drilling & Muffen
if (vDrill) {
const drillingOpacity = variant.active ? 1 : 0.3;
variant.drillingSegments.forEach(seg => {
L.polyline(seg.path, {
color: '#000000',
weight: 8,
opacity: drillingOpacity,
dashArray: '5, 10',
interactive: false,
pane: 'drillingPane'
}).addTo(vDrill);
// Individual drilling label (number only)
const midLat = (seg.path[0][0] + seg.path[1][0]) / 2;
const midLng = (seg.path[0][1] + seg.path[1][1]) / 2;
L.marker([midLat, midLng], {
interactive: false,
pane: 'labelPane',
icon: L.divIcon({
className: 'drilling-segment-label',
html: `<div style="background: #000000; color: white; padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: bold; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.2); opacity: ${drillingOpacity};">${seg.length.toFixed(0)}m</div>`,
iconSize: [50, 20],
iconAnchor: [25, 25]
})
}).addTo(vDrill);
// Add Muffen markers
seg.muffen.forEach(mpos => {
L.circleMarker(mpos, {
radius: 5,
color: '#000000',
weight: 2,
fillOpacity: drillingOpacity,
opacity: drillingOpacity,
fillColor: '#ffffff',
pane: 'drillingPane',
interactive: false
}).addTo(vDrill);
});
}); });
} }
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;
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]]]
});
});
});
variant.stats.open = Math.max(0, variant.stats.total - variant.stats.drilling);
if (vDrill) {
const drillingOpacity = variant.active ? 1 : 0.3;
variant.drillingSegments.forEach(seg => {
L.polyline(seg.path, {
color: '#000000',
weight: 8,
opacity: drillingOpacity,
interactive: false,
pane: 'drillingPane'
}).addTo(vDrill);
// Individual drilling label (number only)
const midLat = (seg.path[0][0] + seg.path[1][0]) / 2;
const midLng = (seg.path[0][1] + seg.path[1][1]) / 2;
L.marker([midLat, midLng], {
interactive: false,
pane: 'labelPane',
icon: L.divIcon({
className: 'drilling-segment-label',
html: `<div style="background: #000000; color: white; padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: bold; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.2); opacity: ${drillingOpacity};">${seg.length.toFixed(0)}m</div>`,
iconSize: [50, 20],
iconAnchor: [25, 25]
})
}).addTo(vDrill);
// Add Muffen markers
seg.muffen.forEach(mpos => {
L.circleMarker(mpos, {
radius: 5,
color: '#000000',
weight: 2,
fillOpacity: drillingOpacity,
opacity: drillingOpacity,
fillColor: '#ffffff',
pane: 'drillingPane',
interactive: false
}).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) {
updateRequiredPlots(variant); updateRequiredPlots(variant);
@ -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>