Browse Source

edge routing step

icestormikk 1 month ago
parent
commit
4f2a0b5228

+ 1 - 1
src/v1/graph/edge/DummyEdge.ts

@@ -2,7 +2,7 @@ import Edge from "./Edge.js";
 import Node from "../node/Node.js";
 
 export default class DummyEdge<TNode extends Node> extends Edge<TNode> {
-	public constructor(from: TNode, to: TNode, waypoints: { x: number; y: number }[], id?: string) {
+	public constructor(from: TNode, to: TNode, waypoints?: { x: number; y: number }[], id?: string) {
 		super(from, to, waypoints, id);
 	}
 }

+ 1 - 8
src/v1/graph/node/DummyNode.ts

@@ -4,14 +4,7 @@ import Node from "./Node.js";
  * Мнимая вершина графа, имеющая нулевые размеры
  */
 export default class DummyNode extends Node {
-	private readonly _originalEdgeId: string;
-
-	public constructor(x: number, y: number, originalEdgeId: string, id?: string) {
+	public constructor(x: number, y: number, id?: string) {
 		super(x, y, 0, 0, "dummy-node", id);
-		this._originalEdgeId = originalEdgeId;
-	}
-
-	public getOriginalEdgeId() : string {
-		return this._originalEdgeId;
 	}
 }

+ 2 - 2
src/v1/io/deserialize/json/DzwfJsonDeserializer.ts

@@ -10,7 +10,7 @@ export default class DzwfJsonDeserializer implements JsonDeserializer<{ dzwfData
 		const json = JSON.parse(input) as DzwfData;
 
 		const nodes = json.elements.map(({ type, diagram, id }) => {
-			return new Node(diagram.position.x, diagram.position.y, 100, 100, type, id);
+			return new Node(diagram.position.x, diagram.position.y, diagram.size.width, diagram.size.height, type, id);
 		})
 
 		const edges = json.links.map(({ sourceId, targetId, id }) => {
@@ -25,4 +25,4 @@ export default class DzwfJsonDeserializer implements JsonDeserializer<{ dzwfData
 
 		return { dzwfData: json, graph: new Graph(nodes, edges) };
 	}
-}
+}

+ 74 - 3
src/v1/io/serialize/json/DzwfJsonSerializer.ts

@@ -3,7 +3,7 @@ import Node from "../../../graph/node/Node.js";
 import Edge from "../../../graph/edge/Edge.js";
 import Graph from "../../../graph/Graph.js";
 import SerializerError from "../../../errors/SerializerError.js";
-import {DzwfData} from "../../dzwf/DzwfData.js";
+import {DzwfData, DzwfLink} from "../../dzwf/DzwfData.js";
 
 export default class DzwfJsonSerializer implements JsonSerializer<{ dzwfData: DzwfData, graph: Graph<Node, Edge<Node>> }> {
 	serialize(data: { dzwfData: DzwfData, graph: Graph<Node, Edge<Node>> }): string {
@@ -18,6 +18,77 @@ export default class DzwfJsonSerializer implements JsonSerializer<{ dzwfData: Dz
 			element.diagram.position.y = node.getY();
 		}
 
-		return JSON.stringify(dzwfData);
+		dzwfData.links = this.restoreLinks(graph.getEdges());
+
+		return JSON.stringify(dzwfData, null, 4);
+	}
+
+	private restoreLinks(edges: Edge<Node>[]): DzwfLink[] {
+		const outgoing = new Map<string, Edge<Node>[]>();
+		const links: DzwfLink[] = [];
+
+		for(const edge of edges) {
+			const fromId = edge.getFrom().getId();
+			const bucket = outgoing.get(fromId);
+			if(bucket)
+				bucket.push(edge);
+			else
+				outgoing.set(fromId, [edge]);
+		}
+
+		for(const edge of edges) {
+			const from = edge.getFrom();
+			if(this.isDummyNode(from))
+				continue;
+
+			const waypoints: { x: number, y: number }[] = [];
+			const visitedDummyIds = new Set<string>();
+			let target = edge.getTo();
+
+			while(this.isDummyNode(target)) {
+				if(visitedDummyIds.has(target.getId()))
+					throw new SerializerError(`Cycle was detected while restoring edge ${edge.getId()}`);
+				visitedDummyIds.add(target.getId());
+
+				waypoints.push(this.toCenter(target));
+
+				const nextEdges = outgoing.get(target.getId()) || [];
+				if(nextEdges.length !== 1)
+					throw new SerializerError(
+						`Can't restore edge ${edge.getId()}: dummy node ${target.getId()} must have exactly one outgoing edge, got ${nextEdges.length}`
+					);
+
+				target = nextEdges[0]!.getTo();
+			}
+
+			links.push({
+				id: this.resolveLinkId(edge),
+				sourceId: from.getId(),
+				targetId: target.getId(),
+				diagram: { vertices: waypoints }
+			});
+		}
+
+		return links;
+	}
+
+	private isDummyNode(node: Node): boolean {
+		return node.getType() === "dummy-node";
+	}
+
+	private toCenter(node: Node): { x: number, y: number } {
+		return {
+			x: node.getX() + node.getWidth() / 2,
+			y: node.getY() + node.getHeight() / 2
+		};
+	}
+
+	private resolveLinkId(edge: Edge<Node>): string {
+		const firstTarget = edge.getTo();
+		if(!this.isDummyNode(firstTarget))
+			return edge.getId();
+
+		const candidate = firstTarget as Node & Partial<{ getOriginalEdgeId: () => string }>;
+		return typeof candidate.getOriginalEdgeId === "function" ? candidate.getOriginalEdgeId() : edge.getId();
 	}
-}
+}

+ 3 - 0
src/v1/optimizer/siguiyama/SiguiyamaContext.ts

@@ -8,10 +8,13 @@ import { AlgorithmContext } from "../AlgorithmContext.js"
  */
 export type FeedbackSet = Edge<Node>[];
 
+export type EdgeSubdivisions<TNode extends Node, TEdge extends Edge<TNode>> = Map<TEdge, { left: TEdge, right: TEdge }>
+
 /**
  * Контекст, передаваемый между шагами алгоритма Сигуямы. Содержит всю необходимую информацию о графе, его разбиении на слои, сетке и рёбрах обратной связи, которая может быть использована и модифицирована на каждом этапе оптимизации.
  */
 export type SiguiyamaContext = AlgorithmContext & Partial<{
 	feedbackSet: FeedbackSet,
+	edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>>
 	layering: Layering<Node, Edge<Node>>,
 }>

+ 47 - 28
src/v1/optimizer/steps/EdgeRoutingStep.ts

@@ -1,10 +1,12 @@
 import AlgorithmStep from "../AlgorithmStep.js";
-import {SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
+import {EdgeSubdivisions, SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
 import EdgeRoutingStepError from "../../errors/optimizer/EdgeRoutingStepError.js";
 import Node from "../../graph/node/Node.js";
 import Edge from "../../graph/edge/Edge.js";
 import DummyNode from "../../graph/node/DummyNode.js";
 import DummyEdge from "../../graph/edge/DummyEdge.js";
+import Graph from "../../graph/Graph.js";
+import Layering from "../../graph/layering/Layering.js";
 
 export default class EdgeRoutingStep extends AlgorithmStep<SiguiyamaContext> {
 	public constructor() {
@@ -12,60 +14,77 @@ export default class EdgeRoutingStep extends AlgorithmStep<SiguiyamaContext> {
 	}
 
 	public run(context: SiguiyamaContext) : void {
-		const { layering, graph } = context;
+		const { layering, graph, edgeSubdivisions } = context;
 
 		if(!layering)
 			throw new EdgeRoutingStepError("Layering is null or undefined");
 		if(!graph)
 			throw new EdgeRoutingStepError("Graph is null or undefined");
+		if(!edgeSubdivisions)
+			throw new EdgeRoutingStepError("Edge Subdivisions are null or undefined");
 
-		this.routeEdges(graph.getEdges(), layering, graph);
+		this.routeEdges(graph, layering, edgeSubdivisions);
 	}
 
-	private routeEdges(edges: Edge<Node>[], layering: NonNullable<SiguiyamaContext["layering"]>, graph: NonNullable<SiguiyamaContext["graph"]>): void {
-		const layers = layering.getLayers();
+	private routeEdges(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>>): void {
+		const edges = graph.getEdges();
 
 		for(const edge of edges) {
 			const from = edge.getFrom();
 			const to = edge.getTo();
+			const fromNodeIndex = this.getNodeIndex(layering, from);
+			const toNodeIndex = this.getNodeIndex(layering, to);
 
-			const fromLayerIndex = layering.getNodeLayerIndex(from);
-			const toLayerIndex = layering.getNodeLayerIndex(to);
-
-			if(fromLayerIndex < 0 || toLayerIndex < 0)
+			if(fromNodeIndex === -1 || toNodeIndex === -1 || fromNodeIndex === toNodeIndex)
 				continue;
 
-			const fromRowIndex = layers[fromLayerIndex]?.getNodes().findIndex(node => node === from) ?? -1;
-			const toRowIndex = layers[toLayerIndex]?.getNodes().findIndex(node => node === to) ?? -1;
-
-			if(fromRowIndex < 0 || toRowIndex < 0 || fromRowIndex === toRowIndex)
-				continue;
-
-			const rightLayer = layers[fromLayerIndex - 1];
-			if(!rightLayer)
+			const fromLayerIndex = layering.getNodeLayerIndex(from);
+			if(fromLayerIndex === -1)
 				continue;
 
-			const rightLayerNodes = rightLayer.getNodes();
-			if(rightLayerNodes.length === 0)
+			const targetRowIndex = fromNodeIndex > toNodeIndex ? fromNodeIndex - 1 : fromNodeIndex + 1;
+			const neighborRowCenter = this.getRowCenter(layering, targetRowIndex);
+			if(neighborRowCenter === null)
 				continue;
 
-			const anchorNode = rightLayerNodes[0]!;
+			const fromCenterY = from.getY() + from.getHeight() / 2;
 			const routedNode = new DummyNode(
-				anchorNode.getX() + anchorNode.getWidth() / 2,
-				from.getY() + from.getHeight() / 2,
-				edge.getId()
+				from.getX() + from.getWidth() / 2,
+				(fromCenterY + neighborRowCenter) / 2
 			);
 
-			layering.addToLayer(fromLayerIndex - 1, routedNode);
+			layering.addToLayer(fromLayerIndex, routedNode);
 			graph.addNode(routedNode);
 
-			const firstSegment = new DummyEdge(from, routedNode, []);
-			const secondSegment = new DummyEdge(routedNode, to, []);
+			const left = new DummyEdge(from, routedNode);
+			const right = new DummyEdge(routedNode, to);
 
-			graph.addEdge(firstSegment);
-			graph.addEdge(secondSegment);
+			graph.addEdge(left);
+			graph.addEdge(right);
 			graph.removeEdge(edge.getId());
+
+			edgeSubdivisions.set(edge, { left, right });
 		}
+	}
+
+	private getRowCenter(layering: Layering<Node, Edge<Node>>, rowIndex: number): number | null {
+		if(rowIndex < 0)
+			return null;
+
+		for(const layer of layering.getLayers()) {
+			const node = layer.getNodes().at(rowIndex);
+			if(node)
+				return node.getY() + node.getHeight() / 2;
+		}
+
+		return null;
+	}
+
+	private getNodeIndex(layering: Layering<Node, Edge<Node>>, node: Node) : number {
+		const layerIndex = layering.getNodeLayerIndex(node);
+		if(layerIndex === -1)
+			return -1;
 
+		return layering.getLayers()[layerIndex]!.getNodes().indexOf(node);
 	}
 }

+ 26 - 18
src/v1/optimizer/steps/LayerAssignmentStep.ts

@@ -4,7 +4,7 @@ import Graph from "../../graph/Graph.js";
 import Layering from "../../graph/layering/Layering.js";
 import Node from "../../graph/node/Node.js";
 import AlgorithmStep from "../AlgorithmStep.js";
-import {FeedbackSet, SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
+import {EdgeSubdivisions, FeedbackSet, SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
 import DummyNode from "../../graph/node/DummyNode.js";
 import DummyEdge from "../../graph/edge/DummyEdge.js";
 
@@ -37,10 +37,12 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 			throw new LayerAssignmentStepError("Graph is acyclic, can not assign layers to an acyclic graph!");
 
 		const layering = this.longestPathAlgorithm(graph);
+		const edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>> = new Map();
 
-		this.divideLongEdges(graph, layering, feedbackSet);
+		this.divideLongEdges(graph, layering, feedbackSet, edgeSubdivisions);
 
 		context.layering = layering;
+		context.edgeSubdivisions = edgeSubdivisions;
 	}
 
 	/**
@@ -89,8 +91,9 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
      * @param graph Граф, в который добавляются dummy-вершины и сегменты рёбер.
      * @param layering Текущее разбиение графа на слои, используемое для вычисления span и вставки dummy-вершин.
      * @param feedbackSet Набор рёбер обратной связи
-	 */
-	private divideLongEdges(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, feedbackSet: FeedbackSet) : void {
+     * @param edgeSubdivisions Информация о разделении рёбер в графе
+     */
+	private divideLongEdges(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, feedbackSet: FeedbackSet, edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>>) : void {
 		const edges = graph.getEdges();
 
 		for(const edge of edges) {
@@ -100,28 +103,33 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 
 			const from = edge.getFrom(), to = edge.getTo();
 			const fromLayer = layering.getNodeLayerIndex(from);
-			const isReversed = feedbackSet.includes(edge);
+			let currentEdge: Edge<Node> = edge;
+
+			graph.removeEdge(edge.getId());
 
-			let currentNode = from;
 			for(let i = 1; i < span; i++) {
-				const dummyNode = new DummyNode(0, 0, edge.getId());
+				const dummyNode = new DummyNode(0, 0);
 				layering.addToLayer(fromLayer - i, dummyNode);
 				graph.addNode(dummyNode);
 
-				const dummyEdge = new DummyEdge(currentNode, dummyNode, [], "dummy-" + crypto.randomUUID());
-				graph.addEdge(dummyEdge);
-				if(isReversed)
-					feedbackSet.push(dummyEdge);
+				const left = new DummyEdge(currentEdge.getFrom(), dummyNode);
+				const right = new DummyEdge(dummyNode, currentEdge.getTo());
 
-				currentNode = dummyNode;
-			}
+				edgeSubdivisions.set(currentEdge, { left, right });
 
-			const finalEdge = new DummyEdge(currentNode, to, [], "dummy-" + crypto.randomUUID());
-			graph.addEdge(finalEdge);
-			if(isReversed)
-				feedbackSet.push(finalEdge);
+				graph.addEdge(left).addEdge(right);
 
-			graph.removeEdge(edge.getId());
+				const feedbackIndex = feedbackSet.indexOf(currentEdge);
+				if(feedbackIndex >= 0) {
+					feedbackSet.splice(feedbackIndex, 1);
+					feedbackSet.push(left, right);
+				}
+
+				if(currentEdge !== edge)
+					graph.removeEdge(currentEdge.getId());
+
+				currentEdge = right;
+			}
 		}
 	}
 }