const fs = require('fs');
const escapeHTML = (s) => String(s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
function generate(stations, lines, fares, nameByCode, transfers) {
const enNameByCode = {};
stations.forEach(s => { enNameByCode[String(s.code || '')] = s.en_name || s.enName || ''; });
// 1. Build Graph
const validStationCodes = new Set(stations.map(s => String(s.code || '')));
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); };
stations.forEach(s => { const c = String(s.code || ''); if (c) addNode(c); });
const edgeCosts = new Map();
(fares || []).forEach(f => {
const a = String(f.from || ''), b = String(f.to || '');
if (a && b && validStationCodes.has(a) && validStationCodes.has(b)) {
const w = Number(f.cost_regular ?? f.cost) || 0;
addEdge(a, b, w);
edgeCosts.set(`${a}|${b}`, w);
edgeCosts.set(`${b}|${a}`, w);
}
});
const transferSet = new Set();
if (Array.isArray(transfers)) {
transfers.forEach(p => {
const u = String(p[0] || ''), v = String(p[1] || '');
if (u && v && validStationCodes.has(u) && validStationCodes.has(v)) {
addEdge(u, v, 0);
transferSet.add(`${u}|${v}`);
transferSet.add(`${v}|${u}`);
}
});
}
// 2. Force Directed Layout
const nodes = Array.from(validStationCodes).map(code => ({
code,
x: Math.random() * 800,
y: Math.random() * 600,
vx: 0, vy: 0,
name: nameByCode[code] || code
}));
const nodeMap = new Map(nodes.map(n => [n.code, n]));
// Add line connections to edges for layout purposes
const layoutEdges = [];
(lines || []).forEach(li => {
const arr = (Array.isArray(li.stations) ? li.stations : (Array.isArray(li.stops) ? li.stops : [])).map(x => String(x));
for (let i = 0; i < arr.length - 1; i++) {
if (nodeMap.has(arr[i]) && nodeMap.has(arr[i+1])) {
layoutEdges.push({ source: nodeMap.get(arr[i]), target: nodeMap.get(arr[i+1]), type: 'line' });
}
}
});
for (const [u, edges] of adj.entries()) {
for (const [v, w] of edges.entries()) {
if (u < v && nodeMap.has(u) && nodeMap.has(v)) {
layoutEdges.push({ source: nodeMap.get(u), target: nodeMap.get(v), type: 'fare' });
}
}
}
const iterations = 300;
const k = Math.sqrt((1200 * 800) / nodes.length) || 100;
for (let i = 0; i < iterations; i++) {
const t = 1 - i / iterations;
// Repulsion
for (let a = 0; a < nodes.length; a++) {
for (let b = a + 1; b < nodes.length; b++) {
const dx = nodes[a].x - nodes[b].x;
const dy = nodes[a].y - nodes[b].y;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) { dist = 0.01; }
if (dist < k * 2) {
const force = (k * k) / dist;
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
nodes[a].vx += fx; nodes[a].vy += fy;
nodes[b].vx -= fx; nodes[b].vy -= fy;
}
}
}
// Attraction
for (const e of layoutEdges) {
const dx = e.target.x - e.source.x;
const dy = e.target.y - e.source.y;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) dist = 0.01;
const idealDist = e.type === 'line' ? k * 0.8 : k * 1.5;
const force = (dist * dist) / idealDist;
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
e.source.vx += fx; e.source.vy += fy;
e.target.vx -= fx; e.target.vy -= fy;
}
// Gravity
for (const n of nodes) {
const dx = 600 - n.x;
const dy = 400 - n.y;
n.vx += dx * 0.05;
n.vy += dy * 0.05;
}
for (const n of nodes) {
n.x += n.vx * t * 0.1;
n.y += n.vy * t * 0.1;
n.vx *= 0.8;
n.vy *= 0.8;
}
}
// Normalize coordinates
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
nodes.forEach(n => {
minX = Math.min(minX, n.x); minY = Math.min(minY, n.y);
maxX = Math.max(maxX, n.x); maxY = Math.max(maxY, n.y);
});
const margin = 100;
nodes.forEach(n => {
n.x = n.x - minX + margin;
n.y = n.y - minY + margin;
});
const width = maxX - minX + margin * 2;
const height = maxY - minY + margin * 2;
let svg = ``;
return svg;
}
module.exports = { generate };