[verified] feat: add specialized diagram profiles
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user