feat(web): 撤销静态SVG图片渲染,回退到自动线条生成模式,并保留断点支持
This commit is contained in:
@@ -16,8 +16,8 @@
|
||||
"mtimeMs": 1783602619602.658
|
||||
},
|
||||
"ticket-order.js": {
|
||||
"size": 25105,
|
||||
"mtimeMs": 1783775559758.2654
|
||||
"size": 32589,
|
||||
"mtimeMs": 1783779020571.2231
|
||||
},
|
||||
"ticket-search.js": {
|
||||
"size": 17269,
|
||||
@@ -28,8 +28,8 @@
|
||||
"mtimeMs": 1783666241687.1482
|
||||
},
|
||||
"index.js": {
|
||||
"size": 84094,
|
||||
"mtimeMs": 1783777527829.4636
|
||||
"size": 82754,
|
||||
"mtimeMs": 1783779774480.386
|
||||
},
|
||||
"ic-card-admin.js": {
|
||||
"size": 19095,
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"blog.css": 2,
|
||||
"custom-dialog.js": 11,
|
||||
"public-status.js": 14,
|
||||
"ticket-order.js": 17,
|
||||
"ticket-order.js": 18,
|
||||
"ticket-search.js": 12,
|
||||
"ticket-route.js": 3,
|
||||
"index.js": 4,
|
||||
"index.js": 5,
|
||||
"ic-card-admin.js": 3,
|
||||
"ic-card-order.js": 3,
|
||||
"ic-card-detail.js": 2,
|
||||
|
||||
+2
-60
@@ -816,65 +816,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="assetsRouteMapUrl" style="border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: #0b0b0f; padding: 16px;">
|
||||
|
||||
<div class="flex mb-4" style="gap: 10px; align-items: center; background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px;">
|
||||
<label class="switch-label">
|
||||
<input type="checkbox" v-model="mapBindingMode">
|
||||
<span class="slider"></span>
|
||||
<span class="label-text">开启坐标绑定模式</span>
|
||||
</label>
|
||||
<select v-if="mapBindingMode" v-model="mapBindingStation" style="width: 240px;">
|
||||
<option value="">-- 选择要绑定的站点 --</option>
|
||||
<optgroup v-for="g in unboundStationsGrouped" :key="g.name" :label="g.name + (g.isTransfer ? ' (换乘站)' : '') + (g.isBound ? ' - 已绑定' : '')">
|
||||
<option v-for="s in g.stations" :key="s.code" :value="s.code">
|
||||
{{ s.name }} ({{ s.code }})
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<div v-if="mapBindingMode" class="text-muted" style="font-size: 0.9em;">
|
||||
在左侧选择站点后,点击下方图片中的位置即可绑定。同名换乘站会自动同步坐标绑定。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: relative; display: inline-block; width: 100%;" :style="{ cursor: mapBindingMode ? 'crosshair' : 'default' }" @click="onMapImageClick">
|
||||
<img :src="assetsRouteMapUrl" alt="线路图" style="display:block; width: 100%; height: auto;" ref="mapImageRef">
|
||||
|
||||
<!-- 坐标标记点 -->
|
||||
<template v-for="s in stations" :key="'marker-'+s.code">
|
||||
<div v-if="s.svg_x != null && s.svg_y != null"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: s.svg_x + '%',
|
||||
top: s.svg_y + '%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: s.code === mapBindingStation ? '16px' : '10px',
|
||||
height: s.code === mapBindingStation ? '16px' : '10px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: s.code === mapBindingStation ? 'var(--primary)' : 'rgba(255,50,50,0.7)',
|
||||
border: '2px solid white',
|
||||
boxShadow: '0 0 4px rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
transition: 'all 0.2s ease'
|
||||
}">
|
||||
</div>
|
||||
<div v-if="mapBindingMode && s.svg_x != null && s.svg_y != null"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: s.svg_x + '%',
|
||||
top: `calc(${s.svg_y}% + 12px)`,
|
||||
transform: 'translateX(-50%)',
|
||||
color: 'white',
|
||||
background: 'rgba(0,0,0,0.7)',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '10px',
|
||||
pointerEvents: 'none',
|
||||
whiteSpace: 'nowrap'
|
||||
}">
|
||||
{{ s.name }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<img :src="assetsRouteMapUrl" alt="线路图" style="display:block; width: 100%; height: auto;" ref="mapImageRef">
|
||||
</div>
|
||||
<div v-else class="loading">未上传线路图</div>
|
||||
</div>
|
||||
@@ -978,7 +920,7 @@
|
||||
</main>
|
||||
</div>
|
||||
<script src="/custom-dialog.js?v=11"></script>
|
||||
<script src="index.js?v=4"></script>
|
||||
<script src="index.js?v=5"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const isDomain = location.hostname.includes('fse-media.group');
|
||||
|
||||
@@ -1139,80 +1139,6 @@ createApp({
|
||||
const zoomFareMapOut = () => { fareMapScale.value = Math.max(0.3, Math.round((fareMapScale.value - 0.1) * 100) / 100); };
|
||||
const zoomFareMapReset = () => { fareMapScale.value = 1; };
|
||||
|
||||
const mapBindingMode = ref(false);
|
||||
const mapBindingStation = ref('');
|
||||
const mapImageRef = ref(null);
|
||||
|
||||
const unboundStationsGrouped = computed(() => {
|
||||
const groups = {};
|
||||
stations.value.forEach(s => {
|
||||
const name = String(s.name || s.cn_name || '').trim();
|
||||
if (!groups[name]) groups[name] = [];
|
||||
groups[name].push(s);
|
||||
});
|
||||
const out = [];
|
||||
for (const [name, list] of Object.entries(groups)) {
|
||||
out.push({
|
||||
name,
|
||||
isTransfer: list.length > 1,
|
||||
stations: list,
|
||||
isBound: list.every(s => s.svg_x != null && s.svg_y != null)
|
||||
});
|
||||
}
|
||||
return out.sort((a, b) => {
|
||||
if (a.isBound !== b.isBound) return a.isBound ? 1 : -1;
|
||||
if (a.isTransfer !== b.isTransfer) return a.isTransfer ? -1 : 1;
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
const onMapImageClick = async (e) => {
|
||||
if (!mapBindingMode.value || !mapBindingStation.value) return;
|
||||
const img = mapImageRef.value;
|
||||
if (!img) return;
|
||||
|
||||
const rect = img.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const percentX = (x / rect.width) * 100;
|
||||
const percentY = (y / rect.height) * 100;
|
||||
|
||||
const station = stations.value.find(s => s.code === mapBindingStation.value);
|
||||
if (!station) return;
|
||||
|
||||
const pctX = Number(percentX.toFixed(2));
|
||||
const pctY = Number(percentY.toFixed(2));
|
||||
|
||||
const targetName = String(station.name || station.cn_name || '').trim().toLowerCase();
|
||||
|
||||
// Auto-bind all stations with the same name (transfers)
|
||||
const stationsToUpdate = stations.value.filter(s => {
|
||||
if (s.code === mapBindingStation.value) return true;
|
||||
const sName = String(s.name || s.cn_name || '').trim().toLowerCase();
|
||||
return targetName && sName === targetName;
|
||||
});
|
||||
|
||||
try {
|
||||
for (const s of stationsToUpdate) {
|
||||
s.svg_x = pctX;
|
||||
s.svg_y = pctY;
|
||||
await requestJson(`/api/stations/${encodeURIComponent(s.code)}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(s)
|
||||
}, { expectOk: true });
|
||||
}
|
||||
|
||||
// Find the next station without coordinates and select it
|
||||
const nextStation = stations.value.find(s => s.svg_x == null && s.svg_y == null);
|
||||
if (nextStation) {
|
||||
mapBindingStation.value = nextStation.code;
|
||||
}
|
||||
} catch (err) {
|
||||
alert('保存坐标失败:' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Management Actions ---
|
||||
|
||||
const createStation = async () => {
|
||||
@@ -1845,7 +1771,6 @@ createApp({
|
||||
showAddLine, showAddStation, newLine, newStation, fareMapSvg, ticketSearch,
|
||||
assetsManifest, assetsFarePreview, assetsRouteMapUrl, assetsFareTableUrl,
|
||||
uploadRouteMap, uploadFareTable, deleteRouteMap, deleteFareTable,
|
||||
mapBindingMode, mapBindingStation, mapImageRef, onMapImageClick,
|
||||
|
||||
// Management
|
||||
selectedLine, fareMode, stationEditMode, fareSelection, showFareModal, currentFare, availableStations,
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/custom-dialog.js?v=11"></script>
|
||||
<script src="/ticket-order.js?v=17"></script>
|
||||
<script src="/ticket-order.js?v=18"></script>
|
||||
<script src="/public-status.js?v=14"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
+198
-50
@@ -90,10 +90,9 @@
|
||||
async function renderSystemMap() {
|
||||
mapContainer.innerHTML = '<div class="loading">加载线路数据...</div>';
|
||||
try {
|
||||
const [linesData, stationsData, manifestData] = await Promise.all([
|
||||
const [linesData, stationsData] = await Promise.all([
|
||||
fetch('/api/public/lines').then(r=>r.json()),
|
||||
fetch('/api/public/stations').then(r=>r.json()),
|
||||
fetch('/api/assets/manifest').then(r=>r.json()).catch(() => ({ ok: true }))
|
||||
fetch('/api/public/stations').then(r=>r.json())
|
||||
]);
|
||||
|
||||
window.cachedStationsData = stationsData;
|
||||
@@ -117,66 +116,217 @@
|
||||
}
|
||||
|
||||
const lineStops = [];
|
||||
const lineColors = [];
|
||||
const linesByStation = new Map();
|
||||
|
||||
// NEW logic for breaking lines with '-'
|
||||
let currentLineIdx = 0;
|
||||
for (let i = 0; i < linesData.length; i++) {
|
||||
const line = linesData[i] || {};
|
||||
const color = line.color || line.颜色 || '#93a2b7';
|
||||
const stopsRaw = Array.isArray(line.stops) ? line.stops : (Array.isArray(line.站点列表) ? line.站点列表 : []);
|
||||
const stops = stopsRaw.map(c => String(c || '').trim()).filter(Boolean);
|
||||
if (stops.length === 0) continue;
|
||||
lineStops.push({ idx: lineStops.length, stops });
|
||||
|
||||
let currentSegment = [];
|
||||
let addedAny = false;
|
||||
for (const c of stops) {
|
||||
if (c === '-') continue;
|
||||
const arr = linesByStation.get(c) || [];
|
||||
arr.push(lineStops.length - 1);
|
||||
linesByStation.set(c, arr);
|
||||
if (c === '-') {
|
||||
if (currentSegment.length > 0) {
|
||||
lineStops.push({ idx: currentLineIdx, name: line.name || line.线路名称 || '', color, stops: currentSegment });
|
||||
lineColors.push(color);
|
||||
for (const stopCode of currentSegment) {
|
||||
const arr = linesByStation.get(stopCode) || [];
|
||||
arr.push(currentLineIdx);
|
||||
linesByStation.set(stopCode, arr);
|
||||
}
|
||||
currentSegment = [];
|
||||
addedAny = true;
|
||||
}
|
||||
} else {
|
||||
currentSegment.push(c);
|
||||
}
|
||||
}
|
||||
if (currentSegment.length > 0) {
|
||||
lineStops.push({ idx: currentLineIdx, name: line.name || line.线路名称 || '', color, stops: currentSegment });
|
||||
lineColors.push(color);
|
||||
for (const stopCode of currentSegment) {
|
||||
const arr = linesByStation.get(stopCode) || [];
|
||||
arr.push(currentLineIdx);
|
||||
linesByStation.set(stopCode, arr);
|
||||
}
|
||||
addedAny = true;
|
||||
}
|
||||
if (addedAny) {
|
||||
currentLineIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
if (lineStops.length === 0) {
|
||||
mapContainer.innerHTML = '<div class="empty-state jr-empty-state"><p>暂无可显示的线路数据。</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [c, arr] of linesByStation.entries()) {
|
||||
const uniq = Array.from(new Set(arr));
|
||||
if (uniq.length >= 2) stationTransfer.add(c);
|
||||
}
|
||||
|
||||
// Build SVG
|
||||
const legendW = 210;
|
||||
const legendX = 16;
|
||||
const legendSwatchW = 22;
|
||||
const legendTextX = legendX + legendSwatchW + 10;
|
||||
const startX = legendW + 90;
|
||||
const baseY = 44;
|
||||
const lineGapY = 92;
|
||||
const minGapX = 78;
|
||||
|
||||
// Build Interactive Map Over Static SVG
|
||||
if (!manifestData || !manifestData.routeMap) {
|
||||
mapContainer.innerHTML = '<div class="empty-state jr-empty-state"><p>暂未上传静态线路图,请在控制台进行配置。</p></div>';
|
||||
return;
|
||||
const occurrences = {};
|
||||
for (const li of lineStops) {
|
||||
for (let i = 0; i < li.stops.length; i++) {
|
||||
const code = li.stops[i];
|
||||
const canonical = stationCanonicalByCode[code] || code;
|
||||
if (!occurrences[canonical]) occurrences[canonical] = [];
|
||||
occurrences[canonical].push(i * minGapX);
|
||||
}
|
||||
}
|
||||
stationXByCanonical = {};
|
||||
for (const canonical of Object.keys(occurrences)) {
|
||||
const arr = occurrences[canonical];
|
||||
const avg = arr.reduce((a,b)=>a+b,0) / Math.max(1, arr.length);
|
||||
stationXByCanonical[canonical] = startX + Math.round(avg);
|
||||
}
|
||||
|
||||
const routeMapUrl = `/assets/${manifestData.routeMap}?v=${manifestData.updatedAt || Date.now()}`;
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
for (const li of lineStops) {
|
||||
let prevX = startX - minGapX;
|
||||
for (const code of li.stops) {
|
||||
const canonical = stationCanonicalByCode[code] || code;
|
||||
const x = stationXByCanonical[canonical] ?? (prevX + minGapX);
|
||||
const nx = Math.max(x, prevX + minGapX);
|
||||
if (stationXByCanonical[canonical] == null || nx > stationXByCanonical[canonical]) stationXByCanonical[canonical] = nx;
|
||||
prevX = stationXByCanonical[canonical];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const primaryLineByStation = {};
|
||||
for (const [c, arr] of linesByStation.entries()) {
|
||||
const uniq = Array.from(new Set(arr)).sort((a,b)=>a-b);
|
||||
if (uniq.length === 0) continue;
|
||||
primaryLineByStation[c] = uniq[0];
|
||||
}
|
||||
|
||||
stationYByCanonical = {};
|
||||
const ysByCanonical = {};
|
||||
for (const c of Object.keys(primaryLineByStation)) {
|
||||
const li = primaryLineByStation[c];
|
||||
if (li == null) continue;
|
||||
const canonical = stationCanonicalByCode[c] || c;
|
||||
if (!ysByCanonical[canonical]) ysByCanonical[canonical] = [];
|
||||
ysByCanonical[canonical].push(baseY + li * lineGapY);
|
||||
}
|
||||
|
||||
const canonicalY = {};
|
||||
for (const canonical of Object.keys(ysByCanonical)) {
|
||||
const ys = ysByCanonical[canonical];
|
||||
canonicalY[canonical] = ys.reduce((a,b) => a+b, 0) / ys.length;
|
||||
}
|
||||
|
||||
for (const c of Object.keys(primaryLineByStation)) {
|
||||
const canonical = stationCanonicalByCode[c] || c;
|
||||
stationYByCanonical[c] = canonicalY[canonical];
|
||||
}
|
||||
|
||||
const allStations = Object.keys(primaryLineByStation);
|
||||
if (allStations.length === 0) {
|
||||
mapContainer.innerHTML = '<div class="empty-state jr-empty-state"><p>线路数据为空,请稍后再试。</p></div>';
|
||||
return;
|
||||
}
|
||||
const labelShownForCanonical = new Set();
|
||||
const transferColorsByStation = {};
|
||||
const primaryColorByStation = {};
|
||||
for (const c of allStations) {
|
||||
const lineIdxs = Array.from(new Set(linesByStation.get(c) || [])).sort((a,b)=>a-b);
|
||||
const colors = lineIdxs.map(i => lineColors[i]).filter(Boolean);
|
||||
transferColorsByStation[c] = Array.from(new Set(colors));
|
||||
primaryColorByStation[c] = colors[0] || '#93a2b7';
|
||||
}
|
||||
|
||||
let svgContent = '';
|
||||
|
||||
for (const s of stationsData) {
|
||||
const code = String(s.code || s.编号 || '').trim();
|
||||
if (!code || s.svg_x == null || s.svg_y == null) continue;
|
||||
const drawnLegends = new Set();
|
||||
|
||||
for (const li of lineStops) {
|
||||
const yLine = baseY + li.idx * lineGapY;
|
||||
|
||||
const x = s.svg_x;
|
||||
const y = s.svg_y;
|
||||
const name = stationNameByCode[code] || code;
|
||||
const isTransfer = stationTransfer.has(code);
|
||||
|
||||
// We render interactive dots using percentage coordinates over the SVG
|
||||
if (!drawnLegends.has(li.idx)) {
|
||||
svgContent += `<rect x="${legendX}" y="${yLine-10}" width="${legendSwatchW}" height="10" rx="5" fill="${li.color}" />`;
|
||||
svgContent += `<text x="${legendTextX}" y="${yLine-1}" fill="#e5e7eb" font-size="16" font-weight="800">${li.name}</text>`;
|
||||
drawnLegends.add(li.idx);
|
||||
}
|
||||
|
||||
if (li.stops.length >= 2) {
|
||||
const pts = li.stops.map(code => {
|
||||
const canonical = stationCanonicalByCode[code] || code;
|
||||
return {
|
||||
x: stationXByCanonical[canonical] || 0,
|
||||
y: stationYByCanonical[code] ?? yLine
|
||||
};
|
||||
});
|
||||
|
||||
let d = `M ${pts[0].x} ${pts[0].y}`;
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
d += ` L ${pts[i].x} ${pts[i].y}`;
|
||||
}
|
||||
svgContent += `<path d="${d}" fill="none" stroke="${li.color}" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" />`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const canonical of Object.keys(stationCodesByCanonical)) {
|
||||
const codes = (stationCodesByCanonical[canonical] || []).filter(code => Number.isFinite(stationYByCanonical[code]));
|
||||
if (codes.length < 2) continue;
|
||||
const x = stationXByCanonical[canonical];
|
||||
const y = stationYByCanonical[codes[0]];
|
||||
const labelY = y + 34;
|
||||
svgContent += `<circle cx="${x}" cy="${y}" r="7" fill="#ffffff" stroke="#111827" stroke-width="3" />`;
|
||||
svgContent += `<rect x="${x - 42}" y="${labelY - 16}" width="84" height="24" rx="12" fill="rgba(241,245,249,0.92)" stroke="#cbd5e1" stroke-width="1.5" />`;
|
||||
svgContent += `<text x="${x}" y="${labelY}" text-anchor="middle" fill="#334155" font-size="13" font-weight="800">${getStationDisplayName(canonical)}</text>`;
|
||||
labelShownForCanonical.add(canonical);
|
||||
}
|
||||
|
||||
for (const c of allStations) {
|
||||
const canonical = stationCanonicalByCode[c] || c;
|
||||
const x = stationXByCanonical[canonical];
|
||||
const yNode = stationYByCanonical[c] ?? baseY;
|
||||
const name = stationNameByCode[c] || c;
|
||||
const isTransfer = stationTransfer.has(c);
|
||||
const cls = isTransfer ? 'map-station transfer' : 'map-station';
|
||||
const ringSvg = '';
|
||||
const primaryColor = primaryColorByStation[c] || '#93a2b7';
|
||||
const outer = isTransfer
|
||||
? `<circle cx="${x}" cy="${yNode}" r="13" fill="#ffffff" stroke="${primaryColor}" stroke-width="5" />`
|
||||
: `<circle cx="${x}" cy="${yNode}" r="12" fill="#ffffff" stroke="${primaryColor}" stroke-width="5" />`;
|
||||
const transferFill = isTransfer ? `<circle cx="${x}" cy="${yNode}" r="7" fill="#ffffff" stroke="#111827" stroke-width="3" />` : '';
|
||||
const coreFill = isTransfer ? '#ffffff' : '#ffffff';
|
||||
const showLabel = !isTransfer || !labelShownForCanonical.has(canonical);
|
||||
const textSvg = showLabel
|
||||
? `<text x="${x}" y="${yNode+28}" text-anchor="middle" fill="#e5e7eb" font-size="14" font-weight="700">${name}</text>`
|
||||
: '';
|
||||
svgContent += `
|
||||
<div class="map-station-overlay map-station" data-code="${code}"
|
||||
onclick="handleStationClick('${code}')"
|
||||
style="position: absolute; left: ${x}%; top: ${y}%; transform: translate(-50%, -50%); cursor: pointer;">
|
||||
<div class="station-hotspot" title="${name} (${code})"></div>
|
||||
</div>
|
||||
<g class="${cls}" data-code="${c}" onclick="handleStationClick('${c}')" style="cursor:pointer">
|
||||
${ringSvg}
|
||||
${outer}
|
||||
${transferFill}
|
||||
<circle class="node-core" cx="${x}" cy="${yNode}" r="8" fill="${coreFill}" stroke="#111827" stroke-width="3" />
|
||||
${textSvg}
|
||||
</g>
|
||||
`;
|
||||
}
|
||||
|
||||
mapContainer.innerHTML = `
|
||||
<div class="static-map-scroll-container" style="width: 100%; max-width: 100%; overflow: auto;">
|
||||
<div class="static-map-wrapper" style="position: relative; width: 100%; min-width: 800px;">
|
||||
<img src="${routeMapUrl}" alt="线路图" style="display: block; width: 100%; height: auto;" />
|
||||
<svg class="route-svg-overlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;"></svg>
|
||||
<div class="static-map-overlays" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
|
||||
${svgContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const width = Math.max(560, Math.max(...allStations.map(c => stationXByCanonical[stationCanonicalByCode[c] || c] || 0)) + 120);
|
||||
const height = Math.max(260, baseY + currentLineIdx * lineGapY + 60);
|
||||
mapContainer.innerHTML = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${svgContent}</svg>`;
|
||||
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
@@ -311,13 +461,15 @@
|
||||
function getStationPoint(code) {
|
||||
const normalized = String(code || '').trim();
|
||||
if (!normalized) return null;
|
||||
const el = document.querySelector(`.map-station-overlay[data-code="${normalized}"]`);
|
||||
if (!el) return null;
|
||||
return { x: el.offsetLeft, y: el.offsetTop };
|
||||
const canonical = stationCanonicalByCode[normalized] || normalized;
|
||||
const x = stationXByCanonical[canonical];
|
||||
const y = stationYByCanonical[normalized];
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function renderRouteOverlay() {
|
||||
const svgEl = mapContainer.querySelector('.route-svg-overlay');
|
||||
const svgEl = mapContainer.querySelector('svg');
|
||||
if (!svgEl) return;
|
||||
|
||||
const existing = svgEl.querySelector('.route-overlay-group');
|
||||
@@ -328,12 +480,6 @@
|
||||
const points = currentRoute.map(getStationPoint).filter(Boolean);
|
||||
if (points.length < 2) return;
|
||||
|
||||
// Update SVG dimensions to match the image
|
||||
const wrapper = mapContainer.querySelector('.static-map-wrapper');
|
||||
if (wrapper) {
|
||||
svgEl.setAttribute('viewBox', `0 0 ${wrapper.offsetWidth} ${wrapper.offsetHeight}`);
|
||||
}
|
||||
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
const group = document.createElementNS(ns, 'g');
|
||||
group.setAttribute('class', 'route-overlay-group');
|
||||
@@ -395,7 +541,9 @@
|
||||
group.appendChild(arrowG);
|
||||
}
|
||||
|
||||
svgEl.appendChild(group);
|
||||
const firstStation = svgEl.querySelector('.map-station');
|
||||
if (firstStation) svgEl.insertBefore(group, firstStation);
|
||||
else svgEl.appendChild(group);
|
||||
}
|
||||
|
||||
function updateSelectionUI(skipPreview = false) {
|
||||
|
||||
Reference in New Issue
Block a user