|
|
@@ -1,212 +1,91 @@
|
|
|
-import AlgorithmStep from "../AlgorithmStep.js";
|
|
|
-import { SiguiyamaContext } from "./SiguiyamaContext.js";
|
|
|
-import Edge from "../../graph/Edge.js";
|
|
|
+import Edge from "../../graph/edge/Edge.js";
|
|
|
import Graph from "../../graph/Graph.js";
|
|
|
-import Layering from "../../graph/layering/Layering.js";
|
|
|
-import Node from "../../graph/Node.js";
|
|
|
import Layer from "../../graph/layering/Layer.js";
|
|
|
+import Node from "../../graph/node/Node.js";
|
|
|
+import AlgorithmStep from "../AlgorithmStep.js";
|
|
|
+import { FeedbackSet, SiguiyamaContext } from "./SiguiyamaContext.js";
|
|
|
|
|
|
export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaContext> {
|
|
|
- private readonly _iterationsCount: number;
|
|
|
- private readonly _verticalSpacing: number;
|
|
|
- private readonly _horizontalSpacing: number;
|
|
|
+ private readonly _layerGap: number;
|
|
|
+ private readonly _nodeGap: number;
|
|
|
|
|
|
- public constructor(iterationsCount: number = 6, verticalSpacing: number = 50, horizontalSpacing: number = 50) {
|
|
|
+ public constructor(layerGap: number, nodeGap: number) {
|
|
|
super(CoordinateAssignmentStep.name);
|
|
|
- this._iterationsCount = iterationsCount;
|
|
|
- this._verticalSpacing = verticalSpacing;
|
|
|
- this._horizontalSpacing = horizontalSpacing;
|
|
|
+ this._layerGap = layerGap;
|
|
|
+ this._nodeGap = nodeGap;
|
|
|
}
|
|
|
|
|
|
public run(context: SiguiyamaContext): void {
|
|
|
- const { graph, layering } = context;
|
|
|
+ const { graph, layering, feedbackSet } = context;
|
|
|
|
|
|
if(!graph)
|
|
|
throw new Error("Source graph was not found!");
|
|
|
if(!layering)
|
|
|
throw new Error("Layering of graph was not found!");
|
|
|
+ if(!feedbackSet)
|
|
|
+ throw new Error("Feedback set was not found!");
|
|
|
|
|
|
- // Step 1: Assign Y coordinates
|
|
|
- this.assignYCoordinates(graph, layering);
|
|
|
+ this.reverseEdgesInFeedback(graph, feedbackSet);
|
|
|
|
|
|
- // Step 2: Initial X placement
|
|
|
- this.assignInitialXCoordinates(graph, layering);
|
|
|
-
|
|
|
- // Steps 3-5: Iterative alignment with constraint resolution
|
|
|
- for(let i = 0; i < this._iterationsCount; i++) {
|
|
|
- this.topDownPass(graph, layering);
|
|
|
- this.bottomUpPass(graph, layering);
|
|
|
- }
|
|
|
+ const reversedLayers = layering.getLayers().reverse();
|
|
|
|
|
|
- // Step 7: Optional centering
|
|
|
- this.centerLayout(graph);
|
|
|
+ this.assignXCoordinates(graph, reversedLayers);
|
|
|
+ this.assignYCoordinates(graph, reversedLayers);
|
|
|
}
|
|
|
|
|
|
- private assignYCoordinates(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
|
|
|
- const layers = layering.getLayers();
|
|
|
- const layerHeights = new Map<number, number>();
|
|
|
- const layerOffsets = new Map<number, number>();
|
|
|
-
|
|
|
- // Compute max height per layer
|
|
|
- for(const layer of layers) {
|
|
|
- let maxHeight = 0;
|
|
|
- for(const nodeId of layer.nodes) {
|
|
|
- const node = graph.getNode(nodeId);
|
|
|
- if(node && node.height > maxHeight)
|
|
|
- maxHeight = node.height;
|
|
|
- }
|
|
|
- layerHeights.set(layer.index, maxHeight);
|
|
|
- }
|
|
|
+ private reverseEdgesInFeedback(graph: Graph<Node,Edge<Node>>, feedbackSet: FeedbackSet) : void {
|
|
|
+ const edges = graph.getEdges();
|
|
|
|
|
|
- // Compute vertical offsets
|
|
|
- let currentOffset = 0;
|
|
|
- for(const layer of layers) {
|
|
|
- layerOffsets.set(layer.index, currentOffset);
|
|
|
- const height = layerHeights.get(layer.index) || 0;
|
|
|
- currentOffset += height + this._verticalSpacing;
|
|
|
- }
|
|
|
+ for(const edge of edges) {
|
|
|
+ if(!feedbackSet.includes(edge.id))
|
|
|
+ continue;
|
|
|
|
|
|
- // Assign Y coordinates
|
|
|
- for(const layer of layers) {
|
|
|
- const offset = layerOffsets.get(layer.index) || 0;
|
|
|
- for(const nodeId of layer.nodes) {
|
|
|
- const node = graph.getNode(nodeId);
|
|
|
- if(node) {
|
|
|
- node.y = offset + node.height / 2;
|
|
|
- }
|
|
|
- }
|
|
|
+ const from = edge.to, to = edge.from;
|
|
|
+ graph.updateEdge(edge.id, { ...edge, from, to });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private assignInitialXCoordinates(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
|
|
|
- const layers = layering.getLayers();
|
|
|
+ /**
|
|
|
+ * Назначение X-координаты для всех вершин
|
|
|
+ * @param graph Граф, содержащий информацию о вершинах и рёбрах
|
|
|
+ * @param layers Массив из слоев, по которым распределены вершины
|
|
|
+ */
|
|
|
+ private assignXCoordinates(graph: Graph<Node, Edge<Node>>, layers: Layer<Node>[]) : void {
|
|
|
+ let currentX = 0;
|
|
|
|
|
|
for(const layer of layers) {
|
|
|
- let x = 0;
|
|
|
- for(const nodeId of layer.nodes) {
|
|
|
- const node = graph.getNode(nodeId);
|
|
|
- if(node) {
|
|
|
- node.x = x + node.width / 2;
|
|
|
- x += node.width + this._horizontalSpacing;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private topDownPass(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
|
|
|
- const layers = layering.getLayers();
|
|
|
- const targetXMap = new Map<string, number>();
|
|
|
-
|
|
|
- // Compute target positions based on incoming neighbors
|
|
|
- for(let i = 1; i < layers.length; i++) {
|
|
|
- const layer = layers[i]!;
|
|
|
+ const layerWidth = Math.max(...layer.nodes.map((nodeId) => graph.getNode(nodeId)?.width ?? 0));
|
|
|
+
|
|
|
for(const nodeId of layer.nodes) {
|
|
|
const node = graph.getNode(nodeId);
|
|
|
- if(!node) continue;
|
|
|
+ if(!node)
|
|
|
+ continue;
|
|
|
|
|
|
- const incomingNodes = graph.getNodeInputs(nodeId);
|
|
|
- if(incomingNodes.length > 0) {
|
|
|
- const avgX = incomingNodes.reduce((sum, neighbor) => sum + neighbor.x, 0) / incomingNodes.length;
|
|
|
- targetXMap.set(nodeId, avgX);
|
|
|
- }
|
|
|
+ node.x = currentX;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // Apply positions with constraints
|
|
|
- for(let i = 1; i < layers.length; i++) {
|
|
|
- this.applyPositionsWithConstraints(graph, layers[i]!, targetXMap);
|
|
|
+ currentX += layerWidth + this._layerGap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private bottomUpPass(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
|
|
|
- const layers = layering.getLayers();
|
|
|
- const targetXMap = new Map<string, number>();
|
|
|
-
|
|
|
- // Compute target positions based on outgoing neighbors
|
|
|
- for(let i = layers.length - 2; i >= 0; i--) {
|
|
|
- const layer = layers[i]!;
|
|
|
+ /**
|
|
|
+ * Назначение Y-координаты для всех вершин
|
|
|
+ * @param graph Граф, содержащий информацию о вершинах и рёбрах
|
|
|
+ * @param layering Укладка графа
|
|
|
+ * @param layers Массив из слоев, по которым распределены вершины
|
|
|
+ */
|
|
|
+ private assignYCoordinates(graph: Graph<Node, Edge<Node>>, layers: Layer<Node>[]) : void {
|
|
|
+ for(const layer of layers) {
|
|
|
+ let currentY = 0.0;
|
|
|
+
|
|
|
for(const nodeId of layer.nodes) {
|
|
|
const node = graph.getNode(nodeId);
|
|
|
- if(!node) continue;
|
|
|
+ if(!node)
|
|
|
+ continue;
|
|
|
|
|
|
- const outgoingNodes = graph.getNodeOutputs(nodeId);
|
|
|
- if(outgoingNodes.length > 0) {
|
|
|
- const avgX = outgoingNodes.reduce((sum, neighbor) => sum + neighbor.x, 0) / outgoingNodes.length;
|
|
|
- targetXMap.set(nodeId, avgX);
|
|
|
- }
|
|
|
+ node.y = currentY;
|
|
|
+ currentY += node.height + this._nodeGap;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // Apply positions with constraints
|
|
|
- for(let i = layers.length - 2; i >= 0; i--) {
|
|
|
- this.applyPositionsWithConstraints(graph, layers[i]!, targetXMap);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private applyPositionsWithConstraints(graph: Graph<Node, Edge<Node>>, layer: Layer<Node>, targetXMap: Map<string, number>): void {
|
|
|
- const nodeIds = layer.nodes;
|
|
|
-
|
|
|
- // Step 5.1: Apply target X positions
|
|
|
- for(const nodeId of nodeIds) {
|
|
|
- if(targetXMap.has(nodeId)) {
|
|
|
- const node = graph.getNode(nodeId);
|
|
|
- if(node) {
|
|
|
- node.x = targetXMap.get(nodeId)!;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Step 5.2: Resolve overlaps (left to right)
|
|
|
- for(let i = 0; i < nodeIds.length - 1; i++) {
|
|
|
- const leftNode = graph.getNode(nodeIds[i]!);
|
|
|
- const rightNode = graph.getNode(nodeIds[i + 1]!);
|
|
|
-
|
|
|
- if(!leftNode || !rightNode) continue;
|
|
|
-
|
|
|
- const minDistance = leftNode.width / 2 + this._horizontalSpacing + rightNode.width / 2;
|
|
|
-
|
|
|
- if(rightNode.x < leftNode.x + minDistance) {
|
|
|
- rightNode.x = leftNode.x + minDistance;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Step 5.3: Stabilize (right to left)
|
|
|
- for(let i = nodeIds.length - 1; i > 0; i--) {
|
|
|
- const leftNode = graph.getNode(nodeIds[i - 1]!);
|
|
|
- const rightNode = graph.getNode(nodeIds[i]!);
|
|
|
-
|
|
|
- if(!leftNode || !rightNode) continue;
|
|
|
-
|
|
|
- const minDistance = leftNode.width / 2 + this._horizontalSpacing + rightNode.width / 2;
|
|
|
-
|
|
|
- if(leftNode.x > rightNode.x - minDistance) {
|
|
|
- leftNode.x = rightNode.x - minDistance;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private centerLayout(graph: Graph<Node, Edge<Node>>): void {
|
|
|
- const nodes = graph.getNodes();
|
|
|
-
|
|
|
- if(nodes.length === 0) return;
|
|
|
-
|
|
|
- // Find bounding box
|
|
|
- let minX = Infinity;
|
|
|
- let maxX = -Infinity;
|
|
|
-
|
|
|
- for(const node of nodes) {
|
|
|
- const left = node.x - node.width / 2;
|
|
|
- const right = node.x + node.width / 2;
|
|
|
- minX = Math.min(minX, left);
|
|
|
- maxX = Math.max(maxX, right);
|
|
|
- }
|
|
|
-
|
|
|
- // Calculate center offset
|
|
|
- const center = (minX + maxX) / 2;
|
|
|
-
|
|
|
- // Center all nodes
|
|
|
- for(const node of nodes) {
|
|
|
- node.x -= center;
|
|
|
- }
|
|
|
}
|
|
|
}
|