Files
diagrams-drawio/scripts/src/services/profiles/tube-map.ts
T

51 lines
4.4 KiB
TypeScript

import type { DiagramIRV2, DiagramPoint } from "../../model/diagram-ir.js";
import { validateDiagramIR } from "../../model/diagram-ir.js";
export interface TubeStation { id: string; label: string; provenance?: Record<string, unknown> }
export interface TubeLine { id: string; label: string; color: string; stations: readonly string[]; provenance?: Record<string, unknown> }
export interface TubeMapInput { title: string; stations: readonly TubeStation[]; lines: readonly TubeLine[]; provenance?: Record<string, unknown> }
export function createTubeMap(input: TubeMapInput): DiagramIRV2 {
if (!input || !Array.isArray(input.stations) || input.stations.length === 0) throw new Error("Tube map requires stations");
if (!Array.isArray(input.lines)) throw new Error("Tube map lines must be an array");
const stations: readonly TubeStation[] = input.stations;
const lines: readonly TubeLine[] = input.lines;
const stationIds = new Set(stations.map((station) => station.id));
if (stationIds.size !== stations.length) throw new Error("Tube station IDs must be unique");
for (const line of lines) {
if (line.stations.length < 2) throw new Error(`Tube line ${line.id} requires at least two stations`);
for (const id of line.stations) if (!stationIds.has(id)) throw new Error(`Tube line ${line.id} references unknown station ${id}`);
if (!/^#[0-9A-Fa-f]{6}$/.test(line.color)) throw new Error(`Tube line ${line.id} color must use strict #RRGGBB format`);
}
const memberships = new Map(stations.map((station) => [station.id, [] as string[]]));
lines.forEach((line) => line.stations.forEach((id) => memberships.get(id)!.push(line.id)));
memberships.forEach((lines) => lines.sort());
const firstLine = new Map<string, number>();
lines.forEach((line, lineIndex) => line.stations.forEach((id) => { if (!firstLine.has(id)) firstLine.set(id, lineIndex); }));
const centers = new Map<string, DiagramPoint>();
const nodes = stations.map((station, index) => {
const center = { x: 100 + index * 160, y: 100 + (firstLine.get(station.id) ?? lines.length) * 160 }; centers.set(station.id, center);
const stationLines = memberships.get(station.id)!;
return { id: station.id, label: station.label, kind: stationLines.length > 1 ? "tube-interchange" : "tube-station", geometry: { x: center.x - 20, y: center.y - 20, width: 40, height: 40 }, style: `ellipse;whiteSpace=wrap;html=1;strokeWidth=${stationLines.length > 1 ? 6 : 3}`, properties: { interchange: stationLines.length > 1, lines: stationLines }, provenance: station.provenance === undefined ? undefined : structuredClone(station.provenance) };
});
const edges = lines.flatMap((line, lineIndex) => line.stations.slice(0, -1).map((source, index) => {
const routeCorridorY = 40 + lineIndex * 20;
const target = line.stations[index + 1]; const a = centers.get(source)!; const b = centers.get(target)!;
const laneOffset = 30 + Math.floor(lineIndex / 2) * 10;
const approachX = (lineIndex % 2 === 0 ? 1 : -1) * laneOffset;
const sourceApproachY = a.y + Math.sign(routeCorridorY - a.y) * laneOffset;
const targetApproachY = b.y + Math.sign(routeCorridorY - b.y) * laneOffset;
const waypoints = source === target
? [{ x: a.x + 40, y: a.y }, { x: a.x + 40, y: a.y + 40 }, { x: a.x, y: a.y + 40 }]
: [
{ x: a.x + approachX, y: sourceApproachY },
{ x: a.x + approachX, y: routeCorridorY },
{ x: b.x + approachX, y: routeCorridorY },
{ x: b.x + approachX, y: targetApproachY },
];
return { id: `${line.id}-segment-${index + 1}`, source, target, kind: "tube-line", style: `edgeStyle=none;rounded=0;strokeColor=${line.color};strokeWidth=8;endArrow=none`, waypoints, properties: { lineId: line.id, lineLabel: line.label, segment: index + 1 }, provenance: line.provenance === undefined ? undefined : structuredClone(line.provenance) };
}));
const ir: DiagramIRV2 = { version: 2, title: input.title, pages: [{ id: "tube-map", title: input.title, nodes, edges, layout: { type: "manual", gridSize: 10 }, width: Math.max(320, stations.length * 160 + 40), height: Math.max(320, (lines.length + 1) * 160), properties: { profile: "tube-map", lines: lines.map((line) => ({ id: line.id, label: line.label, color: line.color })) } }], provenance: input.provenance === undefined ? undefined : structuredClone(input.provenance), properties: { profile: "tube-map" } };
return validateDiagramIR(ir) as DiagramIRV2;
}