feat(web): 重构线路图渲染引擎,引入网格化对齐与拓扑深度推导算法,完美还原真实地铁图的整齐排版
This commit is contained in:
@@ -16,8 +16,8 @@
|
|||||||
"mtimeMs": 1783602619602.658
|
"mtimeMs": 1783602619602.658
|
||||||
},
|
},
|
||||||
"ticket-order.js": {
|
"ticket-order.js": {
|
||||||
"size": 40982,
|
"size": 41045,
|
||||||
"mtimeMs": 1783823251900.007
|
"mtimeMs": 1783829841573.1077
|
||||||
},
|
},
|
||||||
"ticket-search.js": {
|
"ticket-search.js": {
|
||||||
"size": 17269,
|
"size": 17269,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"blog.css": 2,
|
"blog.css": 2,
|
||||||
"custom-dialog.js": 11,
|
"custom-dialog.js": 11,
|
||||||
"public-status.js": 14,
|
"public-status.js": 14,
|
||||||
"ticket-order.js": 28,
|
"ticket-order.js": 29,
|
||||||
"ticket-search.js": 12,
|
"ticket-search.js": 12,
|
||||||
"ticket-route.js": 3,
|
"ticket-route.js": 3,
|
||||||
"index.js": 5,
|
"index.js": 5,
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=11"></script>
|
||||||
<script src="/ticket-order.js?v=28"></script>
|
<script src="/ticket-order.js?v=29"></script>
|
||||||
<script src="/public-status.js?v=14"></script>
|
<script src="/public-status.js?v=14"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|||||||
+48
-41
@@ -265,55 +265,61 @@
|
|||||||
const startX = 140;
|
const startX = 140;
|
||||||
const baseY = 80;
|
const baseY = 80;
|
||||||
const lineGapY = 64;
|
const lineGapY = 64;
|
||||||
const minGapX = 68;
|
const gridScaleX = 96;
|
||||||
|
|
||||||
const occurrences = {};
|
// 1. Assign base Y to lines
|
||||||
|
const lineBaseY = {};
|
||||||
|
for (let i = 0; i < lineStops.length; i++) {
|
||||||
|
lineBaseY[lineStops[i].idx] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Assign Grid X to canonical stations (Topological / Depth based)
|
||||||
|
const stationGridX = {};
|
||||||
for (const li of lineStops) {
|
for (const li of lineStops) {
|
||||||
for (let i = 0; i < li.stops.length; i++) {
|
li.stops.forEach((code, idx) => {
|
||||||
const code = li.stops[i];
|
if (code === '-') return;
|
||||||
if (code === '-') continue;
|
|
||||||
const canonical = stationCanonicalByCode[code] || code;
|
const canonical = stationCanonicalByCode[code] || code;
|
||||||
if (!occurrences[canonical]) occurrences[canonical] = [];
|
if (stationGridX[canonical] == null) {
|
||||||
occurrences[canonical].push(i * minGapX);
|
stationGridX[canonical] = idx;
|
||||||
}
|
} else {
|
||||||
}
|
stationGridX[canonical] = Math.max(stationGridX[canonical], idx);
|
||||||
const stationDegreeByCanonical = {};
|
}
|
||||||
for (const canonical of Object.keys(lineIdxsByCanonical)) {
|
});
|
||||||
stationDegreeByCanonical[canonical] = lineIdxsByCanonical[canonical].size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stationXByCanonical = {};
|
// Relax Grid X to ensure connections always move forward (minimum 1 unit gap)
|
||||||
for (const canonical of Object.keys(occurrences)) {
|
let changed = true;
|
||||||
const arr = occurrences[canonical];
|
let passes = 0;
|
||||||
const avg = arr.reduce((a,b)=>a+b,0) / Math.max(1, arr.length);
|
while (changed && passes < 100) {
|
||||||
stationXByCanonical[canonical] = startX + Math.round(avg);
|
changed = false;
|
||||||
}
|
passes++;
|
||||||
|
|
||||||
const sortedCanonicals = Object.keys(stationXByCanonical).sort((a, b) => stationXByCanonical[a] - stationXByCanonical[b]);
|
|
||||||
for (let i = 1; i < sortedCanonicals.length; i++) {
|
|
||||||
const prev = sortedCanonicals[i - 1];
|
|
||||||
const cur = sortedCanonicals[i];
|
|
||||||
const minSpacing = (stationDegreeByCanonical[prev] >= 2 || stationDegreeByCanonical[cur] >= 2) ? 88 : 68;
|
|
||||||
if (stationXByCanonical[cur] < stationXByCanonical[prev] + minSpacing) {
|
|
||||||
stationXByCanonical[cur] = stationXByCanonical[prev] + minSpacing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let pass = 0; pass < 3; pass++) {
|
|
||||||
for (const li of lineStops) {
|
for (const li of lineStops) {
|
||||||
let prevX = startX - minGapX;
|
let prevCanonical = null;
|
||||||
for (const code of li.stops) {
|
for (const code of li.stops) {
|
||||||
if (code === '-') continue;
|
if (code === '-') {
|
||||||
|
prevCanonical = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const canonical = stationCanonicalByCode[code] || code;
|
const canonical = stationCanonicalByCode[code] || code;
|
||||||
const x = stationXByCanonical[canonical] ?? (prevX + minGapX);
|
if (prevCanonical) {
|
||||||
const spacing = stationDegreeByCanonical[canonical] >= 2 ? 88 : minGapX;
|
// Extra spacing if either is a transfer station to avoid crowding
|
||||||
const nx = Math.max(x, prevX + spacing);
|
const needExtra = (lineIdxsByCanonical[canonical]?.size >= 2 || lineIdxsByCanonical[prevCanonical]?.size >= 2) ? 1.5 : 1;
|
||||||
if (stationXByCanonical[canonical] == null || nx > stationXByCanonical[canonical]) stationXByCanonical[canonical] = nx;
|
if (stationGridX[canonical] < stationGridX[prevCanonical] + needExtra) {
|
||||||
prevX = stationXByCanonical[canonical];
|
stationGridX[canonical] = stationGridX[prevCanonical] + needExtra;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevCanonical = canonical;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stationXByCanonical = {};
|
||||||
|
for (const canonical of Object.keys(stationGridX)) {
|
||||||
|
stationXByCanonical[canonical] = startX + stationGridX[canonical] * gridScaleX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Assign Grid Y to canonical stations (Average of their lines)
|
||||||
const hubYByCanonical = {};
|
const hubYByCanonical = {};
|
||||||
for (const canonical of Object.keys(lineIdxsByCanonical)) {
|
for (const canonical of Object.keys(lineIdxsByCanonical)) {
|
||||||
const idxs = Array.from(lineIdxsByCanonical[canonical] || []).sort((a, b) => a - b);
|
const idxs = Array.from(lineIdxsByCanonical[canonical] || []).sort((a, b) => a - b);
|
||||||
@@ -475,10 +481,11 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allX = Object.values(stationXByCanonical);
|
const allCanonicals = Object.keys(stationXByCanonical);
|
||||||
const maxX = allX.length > 0 ? Math.max(...allX) : 0;
|
const minX = Math.min(...allCanonicals.map(c => stationXByCanonical[c] || 0));
|
||||||
const width = Math.max(800, maxX + 180);
|
const maxX = Math.max(...allCanonicals.map(c => stationXByCanonical[c] || 0));
|
||||||
const height = Math.max(260, baseY + currentLineIdx * lineGapY + 60);
|
const width = Math.max(560, maxX + 200);
|
||||||
|
const height = Math.max(260, baseY + currentLineIdx * lineGapY + 120);
|
||||||
mapContainer.innerHTML = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${svgContent}</svg>`;
|
mapContainer.innerHTML = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${svgContent}</svg>`;
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user