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(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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue