重写服务器端SVG生成逻辑,将原铁路线路图生成功能替换为站点间票价矩阵生成。 优化前端换乘路径绘制:新增线路偏移与弯道处理,避免多条线路路径重叠。 修复站点标记的外层圆圈填充颜色,使其正确使用对应线路的配色。
224 lines
8.1 KiB
JavaScript
224 lines
8.1 KiB
JavaScript
const escapeHTML = (s) => String(s || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
|
||
function generate(stations, lines, fares, nameByCode, transfers) {
|
||
const enNameByCode = (() => {
|
||
try {
|
||
const map = {};
|
||
for (const s of (stations || [])) {
|
||
map[String(s.code || '')] = s.en_name || s.enName || '';
|
||
}
|
||
return map;
|
||
} catch (_) {
|
||
return {};
|
||
}
|
||
})();
|
||
|
||
const linesForStation = {};
|
||
(lines || []).forEach((li) => {
|
||
const id = li.id || '';
|
||
const name = li.name || li.cn_name || '';
|
||
const color = li.color || '#93a2b7';
|
||
const arr = (Array.isArray(li.stations) ? li.stations : (Array.isArray(li.stops) ? li.stops : [])).map((x) => String(x));
|
||
for (const code of arr) {
|
||
if (!linesForStation[code]) linesForStation[code] = [];
|
||
linesForStation[code].push({ id, name, color, stations: arr });
|
||
}
|
||
});
|
||
|
||
const groupsMap = {};
|
||
(stations || []).forEach((s) => {
|
||
const k = String(s.name || s.cn_name || s.station_name || s.en_name || '').trim();
|
||
if (!k) return;
|
||
if (!groupsMap[k]) groupsMap[k] = [];
|
||
groupsMap[k].push(String(s.code || ''));
|
||
});
|
||
|
||
const groupsForColors = Object.values(groupsMap).filter((arr) => arr.length > 1);
|
||
const colorsForRoute = (a, b) => {
|
||
const la = linesForStation[a] || [];
|
||
const lb = linesForStation[b] || [];
|
||
const same = la.find((x) => lb.some((y) => y.id === x.id));
|
||
if (same) return [same.color || '#93a2b7'];
|
||
|
||
for (const g of groupsForColors) {
|
||
const startLine = la.find((li) => (li.stations || []).some((code) => g.includes(code)));
|
||
const endLine = lb.find((li) => (li.stations || []).some((code) => g.includes(code)));
|
||
if (startLine && endLine) {
|
||
const c1 = startLine.color || '#93a2b7';
|
||
const c2 = endLine.color || '#93a2b7';
|
||
return (startLine.id === endLine.id) ? [c1] : [c1, c2];
|
||
}
|
||
}
|
||
return [];
|
||
};
|
||
|
||
const buildGraph = () => {
|
||
const adj = new Map();
|
||
const addNode = (c) => { if (!adj.has(c)) adj.set(c, new Map()); };
|
||
const addEdge = (u, v, w) => {
|
||
addNode(u);
|
||
addNode(v);
|
||
adj.get(u).set(v, w);
|
||
adj.get(v).set(u, w);
|
||
};
|
||
|
||
const validStationCodes = new Set((stations || []).map((s) => String(s.code || '')));
|
||
|
||
(fares || []).forEach((f) => {
|
||
const a = String(f.from || '');
|
||
const b = String(f.to || '');
|
||
if (!a || !b) return;
|
||
if (!validStationCodes.has(a) || !validStationCodes.has(b)) return;
|
||
|
||
const w = Number(f.cost_regular ?? f.cost) || 0;
|
||
addEdge(a, b, w);
|
||
});
|
||
|
||
if (Array.isArray(transfers)) {
|
||
transfers.forEach((p) => {
|
||
const u = String(p[0] || '');
|
||
const v = String(p[1] || '');
|
||
if (u && v && validStationCodes.has(u) && validStationCodes.has(v)) {
|
||
addEdge(u, v, 0);
|
||
}
|
||
});
|
||
}
|
||
|
||
(stations || []).forEach((s) => {
|
||
const c = String(s.code || '');
|
||
if (c) addNode(c);
|
||
});
|
||
return adj;
|
||
};
|
||
|
||
const adj = buildGraph();
|
||
const codes = Array.from(adj.keys()).sort((a, b) => a.localeCompare(b));
|
||
const neighborsOf = (u) => adj.get(u) || new Map();
|
||
|
||
const dijkstra = (src) => {
|
||
const dist = {};
|
||
const visited = new Set();
|
||
for (const n of adj.keys()) dist[n] = Infinity;
|
||
dist[src] = 0;
|
||
|
||
while (true) {
|
||
let u = null;
|
||
let best = Infinity;
|
||
for (const n of adj.keys()) {
|
||
if (!visited.has(n) && dist[n] < best) {
|
||
best = dist[n];
|
||
u = n;
|
||
}
|
||
}
|
||
if (u === null) break;
|
||
visited.add(u);
|
||
for (const [v, w] of neighborsOf(u)) {
|
||
const nd = dist[u] + Number(w || 0);
|
||
if (nd < dist[v]) dist[v] = nd;
|
||
}
|
||
}
|
||
|
||
return dist;
|
||
};
|
||
|
||
const distAll = {};
|
||
codes.forEach((c) => {
|
||
distAll[c] = dijkstra(c);
|
||
});
|
||
|
||
let maxFare = 1;
|
||
for (const r of codes) {
|
||
for (const c of codes) {
|
||
if (r === c) continue;
|
||
const v = Number(distAll[r][c] || 0);
|
||
if (v > maxFare) maxFare = v;
|
||
}
|
||
}
|
||
|
||
const margin = 8;
|
||
const cellW = 56;
|
||
const cellH = 40;
|
||
const estimateWidth = (text, size) => Math.ceil(String(text || '').length * (size <= 10 ? 7 : 10));
|
||
const maxCnW = Math.max(1, ...codes.map((c) => estimateWidth(nameByCode[c] || c, 14)));
|
||
const maxEnW = Math.max(1, ...codes.map((c) => estimateWidth(enNameByCode[c] || '', 10)));
|
||
const maxCodeW = Math.max(1, ...codes.map((c) => estimateWidth(c, 10)));
|
||
const leftW = Math.max(140, 24 + Math.max(maxCnW, maxEnW, maxCodeW));
|
||
const topH = 64;
|
||
const n = codes.length;
|
||
const width = margin + leftW + n * cellW + margin;
|
||
const height = margin + topH + n * cellH + margin;
|
||
|
||
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" style="background:#ffffff;font-family:Inter, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif">`;
|
||
|
||
for (let j = 0; j < n; j++) {
|
||
const code = codes[j];
|
||
const name = nameByCode[code] || code;
|
||
const en = enNameByCode[code] || '';
|
||
const x = margin + leftW + j * cellW + cellW / 2;
|
||
const y = margin + 20;
|
||
svg += `<text x="${x}" y="${y}" font-size="14" font-weight="900" fill="#000000" text-anchor="middle">${escapeHTML(name)}</text>`;
|
||
if (en) svg += `<text x="${x}" y="${y + 14}" font-size="10" fill="#000000" text-anchor="middle">${escapeHTML(en)}</text>`;
|
||
svg += `<text x="${x}" y="${y + 26}" font-size="10" fill="#000000" text-anchor="middle">${escapeHTML(code)}</text>`;
|
||
}
|
||
|
||
for (let i = 0; i < n; i++) {
|
||
const code = codes[i];
|
||
const name = nameByCode[code] || code;
|
||
const en = enNameByCode[code] || '';
|
||
const xText = margin + 6;
|
||
const baseY = margin + topH + i * cellH + 22;
|
||
svg += `<text x="${xText}" y="${baseY - 10}" font-size="14" font-weight="900" fill="#000000">${escapeHTML(name)}</text>`;
|
||
if (en) svg += `<text x="${xText}" y="${baseY + 4}" font-size="10" fill="#000000">${escapeHTML(en)}</text>`;
|
||
svg += `<text x="${xText}" y="${baseY + 16}" font-size="10" fill="#000000">${escapeHTML(code)}</text>`;
|
||
|
||
for (let j = 0; j < n; j++) {
|
||
const x0 = margin + leftW + j * cellW;
|
||
const y0 = margin + topH + i * cellH;
|
||
svg += `<rect x="${x0}" y="${y0}" width="${cellW}" height="${cellH}" fill="#ffffff" stroke="#e5e7eb"/>`;
|
||
if (i === j) {
|
||
svg += `<text x="${x0 + cellW / 2}" y="${y0 + 22}" font-size="12" fill="#6b7280" text-anchor="middle">-</text>`;
|
||
} else {
|
||
const distVal = distAll[code][codes[j]];
|
||
if (distVal === Infinity || !Number.isFinite(distVal)) {
|
||
svg += `<text x="${x0 + cellW / 2}" y="${y0 + 22}" font-size="14" font-weight="800" fill="#ef4444" text-anchor="middle">×</text>`;
|
||
} else {
|
||
const val = Math.round(Number(distVal || 0));
|
||
const routeColors = colorsForRoute(code, codes[j]);
|
||
|
||
if (routeColors.length > 0) {
|
||
const stripW = cellW / routeColors.length;
|
||
for (let k = 0; k < routeColors.length; k++) {
|
||
const c = routeColors[k] || '#93a2b7';
|
||
const stripX = x0 + k * stripW;
|
||
svg += `<rect x="${stripX}" y="${y0}" width="${stripW}" height="${cellH}" fill="${escapeHTML(c)}" fill-opacity="0.15" stroke="none"/>`;
|
||
}
|
||
} else {
|
||
svg += `<rect x="${x0}" y="${y0}" width="${cellW}" height="${cellH}" fill="#000000" fill-opacity="0.03" stroke="none"/>`;
|
||
}
|
||
|
||
svg += `<rect x="${x0}" y="${y0}" width="${cellW}" height="${cellH}" fill="none" stroke="#e5e7eb" stroke-width="0.5"/>`;
|
||
svg += `<text x="${x0 + cellW / 2}" y="${y0 + 22}" font-size="12" font-weight="800" fill="#000000" text-anchor="middle">${escapeHTML(String(val))}</text>`;
|
||
|
||
const dots = routeColors.slice(0, 4);
|
||
for (let k = 0; k < dots.length; k++) {
|
||
const cx = x0 + 8 + k * 10;
|
||
const cy = y0 + 10;
|
||
const dotColor = dots[k] || '#93a2b7';
|
||
svg += `<circle cx="${cx}" cy="${cy}" r="3.5" fill="${escapeHTML(dotColor)}" stroke="white" stroke-width="1"/>`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
svg += `</svg>`;
|
||
return svg;
|
||
}
|
||
|
||
module.exports = { generate };
|