317 lines
10 KiB
JavaScript
317 lines
10 KiB
JavaScript
|
|
const DataService = require('./data');
|
||
|
|
|
||
|
|
const buildRouteGraph = (stations, lines, fares, cfg) => {
|
||
|
|
const valid = new Set((stations || []).map(s => String(s?.code || '').trim()).filter(Boolean));
|
||
|
|
const graph = new Map();
|
||
|
|
const segKey = (a, b) => [String(a || '').trim(), String(b || '').trim()].sort().join('|');
|
||
|
|
const ensureNode = (code) => {
|
||
|
|
if (!graph.has(code)) graph.set(code, new Map());
|
||
|
|
};
|
||
|
|
const addEdge = (u, v, edge) => {
|
||
|
|
if (!u || !v || u === v) return;
|
||
|
|
if (!valid.has(u) || !valid.has(v)) return;
|
||
|
|
ensureNode(u);
|
||
|
|
ensureNode(v);
|
||
|
|
const prevUV = graph.get(u).get(v);
|
||
|
|
const prevVU = graph.get(v).get(u);
|
||
|
|
const next = (() => {
|
||
|
|
if (!prevUV) return edge;
|
||
|
|
if ((edge?.paid_segments ?? 0) !== (prevUV?.paid_segments ?? 0)) {
|
||
|
|
return (edge.paid_segments < prevUV.paid_segments) ? edge : prevUV;
|
||
|
|
}
|
||
|
|
if ((edge?.transfer_count ?? 0) !== (prevUV?.transfer_count ?? 0)) {
|
||
|
|
return (edge.transfer_count < prevUV.transfer_count) ? edge : prevUV;
|
||
|
|
}
|
||
|
|
const prevFare = (prevUV?.regular ?? 0) + (prevUV?.express ?? 0);
|
||
|
|
const nextFare = (edge?.regular ?? 0) + (edge?.express ?? 0);
|
||
|
|
return nextFare < prevFare ? edge : prevUV;
|
||
|
|
})();
|
||
|
|
graph.get(u).set(v, next);
|
||
|
|
graph.get(v).set(u, prevVU && prevVU !== prevUV ? prevVU : next);
|
||
|
|
};
|
||
|
|
|
||
|
|
for (const code of valid) ensureNode(code);
|
||
|
|
|
||
|
|
const fareBySegment = new Map();
|
||
|
|
for (const f of (fares || [])) {
|
||
|
|
const a = String(f?.from || '').trim();
|
||
|
|
const b = String(f?.to || '').trim();
|
||
|
|
if (!a || !b) continue;
|
||
|
|
fareBySegment.set(segKey(a, b), {
|
||
|
|
regular: Number(f?.cost_regular ?? f?.cost ?? 0) || 0,
|
||
|
|
express: Number(f?.cost_express ?? f?.cost ?? (f?.cost_regular ?? f?.cost ?? 0)) || 0
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
let segmentEdgeCount = 0;
|
||
|
|
for (const line of (lines || [])) {
|
||
|
|
const stops = Array.isArray(line?.stations) ? line.stations : (Array.isArray(line?.stops) ? line.stops : []);
|
||
|
|
for (let i = 0; i < stops.length - 1; i++) {
|
||
|
|
const a = String(stops[i] || '').trim();
|
||
|
|
const b = String(stops[i + 1] || '').trim();
|
||
|
|
if (!a || !b || a === b) continue;
|
||
|
|
const fare = fareBySegment.get(segKey(a, b));
|
||
|
|
if (!fare) continue;
|
||
|
|
addEdge(a, b, {
|
||
|
|
type: 'segment',
|
||
|
|
regular: fare.regular,
|
||
|
|
express: fare.express,
|
||
|
|
paid_segments: 1,
|
||
|
|
transfer_count: 0
|
||
|
|
});
|
||
|
|
segmentEdgeCount += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (segmentEdgeCount === 0) {
|
||
|
|
for (const [key, fare] of fareBySegment.entries()) {
|
||
|
|
const [a, b] = key.split('|');
|
||
|
|
addEdge(a, b, {
|
||
|
|
type: 'segment',
|
||
|
|
regular: fare.regular,
|
||
|
|
express: fare.express,
|
||
|
|
paid_segments: 1,
|
||
|
|
transfer_count: 0
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const addTransferEdge = (u, v) => {
|
||
|
|
addEdge(u, v, {
|
||
|
|
type: 'transfer',
|
||
|
|
regular: 0,
|
||
|
|
express: 0,
|
||
|
|
paid_segments: 0,
|
||
|
|
transfer_count: 1
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const transfers = Array.isArray(cfg?.transfers) ? cfg.transfers : [];
|
||
|
|
for (const p of transfers) {
|
||
|
|
const u = String(p?.[0] || '').trim();
|
||
|
|
const v = String(p?.[1] || '').trim();
|
||
|
|
if (!u || !v) continue;
|
||
|
|
addTransferEdge(u, v);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const s of (stations || [])) {
|
||
|
|
if (!s?.transfer_enabled) continue;
|
||
|
|
const from = String(s?.code || '').trim();
|
||
|
|
if (!from) continue;
|
||
|
|
const tos = Array.isArray(s.transfer_to) ? s.transfer_to : [];
|
||
|
|
for (const t of tos) {
|
||
|
|
const to = String((typeof t === 'string') ? t : (t?.code || t?.station || t?.id || t?.[0] || '')).trim();
|
||
|
|
if (!to) continue;
|
||
|
|
addTransferEdge(from, to);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return graph;
|
||
|
|
};
|
||
|
|
|
||
|
|
const makeRouteCost = (paidSegments = Infinity, transfers = Infinity, hops = Infinity) => ({
|
||
|
|
paidSegments,
|
||
|
|
transfers,
|
||
|
|
hops
|
||
|
|
});
|
||
|
|
|
||
|
|
const compareRouteCost = (a, b) => {
|
||
|
|
if ((a?.paidSegments ?? Infinity) !== (b?.paidSegments ?? Infinity)) {
|
||
|
|
return (a?.paidSegments ?? Infinity) - (b?.paidSegments ?? Infinity);
|
||
|
|
}
|
||
|
|
if ((a?.transfers ?? Infinity) !== (b?.transfers ?? Infinity)) {
|
||
|
|
return (a?.transfers ?? Infinity) - (b?.transfers ?? Infinity);
|
||
|
|
}
|
||
|
|
return (a?.hops ?? Infinity) - (b?.hops ?? Infinity);
|
||
|
|
};
|
||
|
|
|
||
|
|
const addRouteCost = (base, edge) => makeRouteCost(
|
||
|
|
(base?.paidSegments ?? Infinity) + (edge?.paid_segments ?? 0),
|
||
|
|
(base?.transfers ?? Infinity) + (edge?.transfer_count ?? 0),
|
||
|
|
(base?.hops ?? Infinity) + 1
|
||
|
|
);
|
||
|
|
|
||
|
|
const findRoutePath = (graph, src, dst) => {
|
||
|
|
const dist = new Map();
|
||
|
|
const prev = new Map();
|
||
|
|
const visited = new Set();
|
||
|
|
for (const n of graph.keys()) dist.set(n, makeRouteCost());
|
||
|
|
if (!graph.has(src)) return { dist, path: null, prev };
|
||
|
|
dist.set(src, makeRouteCost(0, 0, 0));
|
||
|
|
|
||
|
|
while (true) {
|
||
|
|
let u = null;
|
||
|
|
let best = makeRouteCost();
|
||
|
|
for (const n of graph.keys()) {
|
||
|
|
if (visited.has(n)) continue;
|
||
|
|
const d = dist.get(n);
|
||
|
|
if (u == null || compareRouteCost(d, best) < 0) {
|
||
|
|
best = d;
|
||
|
|
u = n;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (u == null || !Number.isFinite(best.paidSegments)) break;
|
||
|
|
if (u === dst) break;
|
||
|
|
visited.add(u);
|
||
|
|
for (const [v, edge] of graph.get(u).entries()) {
|
||
|
|
const nd = addRouteCost(best, edge);
|
||
|
|
if (compareRouteCost(nd, dist.get(v)) < 0) {
|
||
|
|
dist.set(v, nd);
|
||
|
|
prev.set(v, u);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const dstCost = dist.get(dst);
|
||
|
|
if (!dstCost || !Number.isFinite(dstCost.paidSegments)) return { dist, path: null, prev };
|
||
|
|
const path = [];
|
||
|
|
let cur = dst;
|
||
|
|
while (cur != null) {
|
||
|
|
path.push(cur);
|
||
|
|
if (cur === src) break;
|
||
|
|
cur = prev.get(cur);
|
||
|
|
}
|
||
|
|
if (path[path.length - 1] !== src) return { dist, path: null, prev };
|
||
|
|
path.reverse();
|
||
|
|
return { dist, path, prev };
|
||
|
|
};
|
||
|
|
|
||
|
|
const accumulateFareAlongPath = (graph, path) => {
|
||
|
|
if (!Array.isArray(path) || path.length < 2) return { regular: 0, express: 0 };
|
||
|
|
let regular = 0;
|
||
|
|
let express = 0;
|
||
|
|
for (let i = 0; i < path.length - 1; i++) {
|
||
|
|
const edge = graph.get(path[i])?.get(path[i + 1]);
|
||
|
|
if (!edge) return null;
|
||
|
|
if (edge.type === 'transfer') continue;
|
||
|
|
regular += Number(edge.regular ?? 0) || 0;
|
||
|
|
express += Number(edge.express ?? 0) || 0;
|
||
|
|
}
|
||
|
|
return { regular, express };
|
||
|
|
};
|
||
|
|
|
||
|
|
const computeTransfersAlongPath = (graph, path) => {
|
||
|
|
if (!Array.isArray(path) || path.length < 2) return [];
|
||
|
|
const out = new Set();
|
||
|
|
for (let i = 0; i < path.length - 1; i++) {
|
||
|
|
const a = path[i];
|
||
|
|
const b = path[i + 1];
|
||
|
|
const edge = graph.get(a)?.get(b);
|
||
|
|
if (edge?.type === 'transfer') {
|
||
|
|
out.add(a);
|
||
|
|
out.add(b);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return Array.from(out);
|
||
|
|
};
|
||
|
|
|
||
|
|
const LogicService = {
|
||
|
|
// Compute ticket price based on fares and lines
|
||
|
|
computePrice: (fromCode, toCode, trainType) => {
|
||
|
|
try {
|
||
|
|
const cfg = DataService.getConfig();
|
||
|
|
const baseBoth = LogicService.computeFareBoth(fromCode, toCode);
|
||
|
|
const baseReg = Number(baseBoth?.regular ?? 0) || 0;
|
||
|
|
const baseExp = Number(baseBoth?.express ?? 0) || 0;
|
||
|
|
const discount = Number(cfg?.promotion?.discount ?? 1);
|
||
|
|
const base = (trainType === 'Express') ? baseExp : baseReg;
|
||
|
|
return Math.floor(base * (discount > 0 ? discount : 1));
|
||
|
|
} catch (_) { return 0; }
|
||
|
|
},
|
||
|
|
|
||
|
|
computeFareBoth: (fromCode, toCode) => {
|
||
|
|
try {
|
||
|
|
const src = String(fromCode || '').trim();
|
||
|
|
const dst = String(toCode || '').trim();
|
||
|
|
if (!src || !dst) return null;
|
||
|
|
if (src === dst) {
|
||
|
|
return {
|
||
|
|
regular: 0,
|
||
|
|
express: 0,
|
||
|
|
regular_path: [src],
|
||
|
|
express_path: [src],
|
||
|
|
regular_transfers: [],
|
||
|
|
express_transfers: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const cfg = DataService.getConfig();
|
||
|
|
const stations = DataService.getStations();
|
||
|
|
const lines = DataService.getLines();
|
||
|
|
const fares = DataService.getFares();
|
||
|
|
const graph = buildRouteGraph(stations, lines, fares, cfg);
|
||
|
|
const result = findRoutePath(graph, src, dst);
|
||
|
|
if (!Array.isArray(result.path)) return null;
|
||
|
|
const totals = accumulateFareAlongPath(graph, result.path);
|
||
|
|
if (!totals) return null;
|
||
|
|
const transfers = computeTransfersAlongPath(graph, result.path);
|
||
|
|
|
||
|
|
return {
|
||
|
|
regular: Math.round(Number(totals.regular ?? 0) || 0),
|
||
|
|
express: Math.round(Number(totals.express ?? 0) || 0),
|
||
|
|
regular_path: result.path,
|
||
|
|
express_path: result.path,
|
||
|
|
regular_transfers: transfers,
|
||
|
|
express_transfers: transfers,
|
||
|
|
};
|
||
|
|
} catch (_) { return null; }
|
||
|
|
},
|
||
|
|
|
||
|
|
accumulateLineFare: (fromCode, toCode, fares, lines) => {
|
||
|
|
if (!Array.isArray(lines)) return null;
|
||
|
|
const line = lines.find(l => {
|
||
|
|
const stations = Array.isArray(l.stations) ? l.stations : (Array.isArray(l.stops) ? l.stops : []);
|
||
|
|
return stations.includes(fromCode) && stations.includes(toCode);
|
||
|
|
});
|
||
|
|
if (!line) return null;
|
||
|
|
const arr = Array.isArray(line.stations) ? line.stations : (Array.isArray(line.stops) ? line.stops : []);
|
||
|
|
const i = arr.indexOf(fromCode);
|
||
|
|
const j = arr.indexOf(toCode);
|
||
|
|
if (i === -1 || j === -1) return null;
|
||
|
|
const start = Math.min(i, j);
|
||
|
|
const end = Math.max(i, j);
|
||
|
|
let regular = 0, express = 0;
|
||
|
|
for (let k = start; k < end; k++) {
|
||
|
|
const a = arr[k], b = arr[k + 1];
|
||
|
|
const f = fares.find(x => (x.from === a && x.to === b) || (x.from === b && x.to === a));
|
||
|
|
if (!f) return null;
|
||
|
|
const vr = Number((f.cost_regular ?? f.cost ?? 0)) || 0;
|
||
|
|
const ve = Number((f.cost_express ?? f.cost ?? 0)) || 0;
|
||
|
|
regular += vr;
|
||
|
|
express += ve;
|
||
|
|
}
|
||
|
|
return { regular, express };
|
||
|
|
},
|
||
|
|
|
||
|
|
genVoucherCode: () => {
|
||
|
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789';
|
||
|
|
const pick = (n) => Array.from({ length: n }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
||
|
|
let code = pick(5);
|
||
|
|
const idx = DataService.getOrderIndex();
|
||
|
|
while (idx[code]) { code = pick(5); }
|
||
|
|
return code;
|
||
|
|
},
|
||
|
|
|
||
|
|
// Helper for SVG generation
|
||
|
|
buildStationNameMap: () => {
|
||
|
|
try {
|
||
|
|
const arr = DataService.getStations();
|
||
|
|
const map = {};
|
||
|
|
for (const s of arr) {
|
||
|
|
map[s.code] = s.name || s.cn_name || s.station_name || s.code;
|
||
|
|
}
|
||
|
|
return map;
|
||
|
|
} catch (_) { return {}; }
|
||
|
|
},
|
||
|
|
|
||
|
|
generateLightFareMapSVG: () => {
|
||
|
|
const stations = DataService.getStations();
|
||
|
|
const lines = DataService.getLines();
|
||
|
|
const fares = DataService.getFares();
|
||
|
|
const nameByCode = LogicService.buildStationNameMap();
|
||
|
|
return require('./svg-generator').generate(stations, lines, fares, nameByCode);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
module.exports = LogicService;
|