Enabled MultiLine segments for route variants
Deploy TrassenPlaner / deploy (push) Waiting to run
Details
Deploy TrassenPlaner / deploy (push) Waiting to run
Details
This commit is contained in:
parent
0cf7110f67
commit
60eadc4d38
383
index.html
383
index.html
|
|
@ -1888,84 +1888,80 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Helper function to flatten coordinates
|
||||
function getFlattenedCoords(rawRoutes) {
|
||||
let latLngs = [];
|
||||
const flattenLL = (raw) => {
|
||||
if (!raw) return;
|
||||
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));
|
||||
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'))) {
|
||||
return raw.map(ll => L.latLng(ll));
|
||||
}
|
||||
// If it's nested
|
||||
const flattened = [];
|
||||
raw.forEach(section => {
|
||||
if (Array.isArray(section)) {
|
||||
section.forEach(ll => flattened.push(L.latLng(ll)));
|
||||
}
|
||||
};
|
||||
flattenLL(rawRoutes);
|
||||
return latLngs.map(ll => {
|
||||
try { return L.latLng(ll); } catch (e) { return null; }
|
||||
}).filter(ll => ll && !isNaN(ll.lat) && !isNaN(ll.lng));
|
||||
});
|
||||
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) {
|
||||
const vLabel = labelLayers[variant.id];
|
||||
if (!vLabel) return;
|
||||
if (!variant.visible) { vLabel.clearLayers(); return; }
|
||||
|
||||
// Use passed coordinates or fall back to state
|
||||
let latLngs = coordsInput ? [...coordsInput] : getFlattenedCoords(variant.routes);
|
||||
if (tempPt) latLngs.push(L.latLng(tempPt));
|
||||
|
||||
if (!variant.visible || latLngs.length < 2) {
|
||||
vLabel.clearLayers();
|
||||
return;
|
||||
}
|
||||
|
||||
const nested = coordsInput ? [coordsInput] : getNestedCoords(variant.routes);
|
||||
const currentMarkers = vLabel.getLayers();
|
||||
let markerIdx = 0;
|
||||
|
||||
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`;
|
||||
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 (markerIdx < currentMarkers.length) {
|
||||
const m = currentMarkers[markerIdx];
|
||||
m.setLatLng(mid);
|
||||
const el = m.getElement();
|
||||
let contentSpan = null;
|
||||
if (el) {
|
||||
contentSpan = el.querySelector('.segment-label-content');
|
||||
}
|
||||
if (drawLatLngs.length < 2) return;
|
||||
|
||||
if (contentSpan) {
|
||||
contentSpan.textContent = labelText;
|
||||
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 {
|
||||
// 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]
|
||||
}));
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
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++;
|
||||
}
|
||||
markerIdx++;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove excess markers
|
||||
while (markerIdx < currentMarkers.length) {
|
||||
vLabel.removeLayer(currentMarkers[markerIdx++]);
|
||||
}
|
||||
|
|
@ -1979,26 +1975,18 @@
|
|||
const vDrill = drillingLayers[variant.id];
|
||||
if (vDrill) vDrill.clearLayers();
|
||||
|
||||
const latLngs = getFlattenedCoords(variant.routes);
|
||||
if (latLngs.length < 2) {
|
||||
variant.stats.total = 0;
|
||||
variant.stats.drilling = 0;
|
||||
variant.stats.muffen = 0;
|
||||
return;
|
||||
}
|
||||
const nested = getNestedCoords(variant.routes);
|
||||
variant.stats.total = 0;
|
||||
variant.stats.drilling = 0;
|
||||
variant.stats.muffen = 0;
|
||||
variant.stats.hasTooLongDrilling = false;
|
||||
variant.drillingSegments = [];
|
||||
|
||||
if (nested.length === 0) return;
|
||||
|
||||
// Sync labels for final state
|
||||
renderSegmentLabels(variant);
|
||||
|
||||
|
||||
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
|
||||
// Pre-filter obstacles once
|
||||
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 => {
|
||||
|
|
@ -2006,132 +1994,114 @@
|
|||
return keywords.some(k => type.includes(k));
|
||||
});
|
||||
}
|
||||
|
||||
const currentObstacles = cachedObstacles || [];
|
||||
|
||||
if (currentObstacles.length > 0) {
|
||||
const lineBbox = turf.bbox(line);
|
||||
nested.forEach(latLngs => {
|
||||
if (latLngs.length < 2) return;
|
||||
|
||||
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;
|
||||
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 (turf.booleanIntersects(line, obs)) {
|
||||
// Robust Intersection Strategy:
|
||||
// 1. Find all intersection points
|
||||
const intersect = turf.lineIntersect(line, obs);
|
||||
let distances = [0, variant.stats.total];
|
||||
|
||||
intersect.features.forEach(f => {
|
||||
const d = 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);
|
||||
|
||||
// 3. Test the midpoint of each segment
|
||||
for (let i = 0; i < distances.length - 1; i++) {
|
||||
const dStart = distances[i];
|
||||
const dEnd = distances[i + 1];
|
||||
const midDist = (dStart + dEnd) / 2;
|
||||
|
||||
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)
|
||||
]);
|
||||
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)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
});
|
||||
} 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);
|
||||
}
|
||||
|
||||
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
|
||||
if (variant.active) {
|
||||
updateRequiredPlots(variant);
|
||||
|
|
@ -2151,11 +2121,14 @@
|
|||
}
|
||||
|
||||
try {
|
||||
const latLngs = getFlattenedCoords(variant.routes);
|
||||
if (latLngs.length < 2) return;
|
||||
const nested = getNestedCoords(variant.routes);
|
||||
if (nested.length === 0) return;
|
||||
|
||||
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
|
||||
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 => {
|
||||
try {
|
||||
|
|
@ -2163,7 +2136,7 @@
|
|||
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] ||
|
||||
lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return false;
|
||||
|
||||
return turf.booleanIntersects(line, f);
|
||||
return turf.booleanIntersects(multiLine, f);
|
||||
} catch (e) { return false; }
|
||||
});
|
||||
|
||||
|
|
@ -2216,6 +2189,13 @@
|
|||
}
|
||||
|
||||
// --- Variant Management UI ---
|
||||
window.startNewPath = (id) => {
|
||||
const layer = routeLayers[id];
|
||||
if (layer && layer.editor) {
|
||||
layer.editor.newPath();
|
||||
}
|
||||
};
|
||||
|
||||
function renderVariants() {
|
||||
const container = document.getElementById('variant-controls');
|
||||
if (!container) return;
|
||||
|
|
@ -2284,6 +2264,7 @@
|
|||
<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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue