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 = ``; // Draw lines (lines || []).forEach(li => { const arr = (Array.isArray(li.stations) ? li.stations : (Array.isArray(li.stops) ? li.stops : [])).map(x => String(x)); const color = li.color || '#93a2b7'; if (arr.length < 2) return; let d = ''; for (let i = 0; i < arr.length; i++) { const n = nodeMap.get(arr[i]); if (n) { d += (i === 0 ? 'M' : 'L') + ` ${n.x} ${n.y}`; } } if (d) { svg += ``; } }); // Draw edges (fares and transfers) const drawnEdges = new Set(); for (const [u, edges] of adj.entries()) { for (const [v, w] of edges.entries()) { if (u < v && nodeMap.has(u) && nodeMap.has(v)) { const nu = nodeMap.get(u), nv = nodeMap.get(v); const isTransfer = transferSet.has(`${u}|${v}`); if (isTransfer) { svg += ``; } if (w > 0) { const cx = (nu.x + nv.x) / 2; const cy = (nu.y + nv.y) / 2; svg += ``; svg += `${w}`; } } } } // Draw nodes nodes.forEach(n => { svg += ``; svg += ``; svg += `${escapeHTML(n.name)}`; svg += `${escapeHTML(n.code)}`; svg += ``; }); svg += ``; return svg; } module.exports = { generate };