|
|
@@ -1,21 +1,19 @@
|
|
|
-import AlgorithmStep from "../AlgorithmStep.js";
|
|
|
-import { SiguiyamaContext } from "./SiguiyamaContext.js";
|
|
|
import Edge from "../../graph/Edge.js";
|
|
|
import Graph from "../../graph/Graph.js";
|
|
|
+import Layer from "../../graph/layering/Layer.js";
|
|
|
import Layering from "../../graph/layering/Layering.js";
|
|
|
import Node from "../../graph/Node.js";
|
|
|
-import Layer from "../../graph/layering/Layer.js";
|
|
|
+import AlgorithmStep from "../AlgorithmStep.js";
|
|
|
+import { 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 {
|
|
|
@@ -26,187 +24,146 @@ export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaCon
|
|
|
if(!layering)
|
|
|
throw new Error("Layering of graph was not found!");
|
|
|
|
|
|
- // Step 1: Assign Y coordinates
|
|
|
- this.assignYCoordinates(graph, layering);
|
|
|
-
|
|
|
- // Step 2: Initial X placement
|
|
|
- this.assignInitialXCoordinates(graph, layering);
|
|
|
+ const reversedLayers = layering.getLayers().reverse();
|
|
|
|
|
|
- // Steps 3-5: Iterative alignment with constraint resolution
|
|
|
- for(let i = 0; i < this._iterationsCount; i++) {
|
|
|
- this.topDownPass(graph, layering);
|
|
|
- this.bottomUpPass(graph, layering);
|
|
|
- }
|
|
|
-
|
|
|
- // Step 7: Optional centering
|
|
|
- this.centerLayout(graph);
|
|
|
+ console.log(graph.getNodes());
|
|
|
+ this.assignXCoordinates(graph, reversedLayers);
|
|
|
+ this.assignYCoordinates(graph, layering, reversedLayers);
|
|
|
+ console.log(graph.getNodes());
|
|
|
}
|
|
|
|
|
|
- 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>();
|
|
|
+ /**
|
|
|
+ * Назначение X-координаты для всех вершин
|
|
|
+ * @param graph Граф, содержащий информацию о вершинах и рёбрах
|
|
|
+ * @param layers Массив из слоев, по которым распределены вершины
|
|
|
+ */
|
|
|
+ private assignXCoordinates(graph: Graph<Node, Edge<Node>>, layers: Layer<Node>[]) : void {
|
|
|
+ let currentX = 0;
|
|
|
|
|
|
- // Compute max height per layer
|
|
|
for(const layer of layers) {
|
|
|
- let maxHeight = 0;
|
|
|
+ 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 && node.height > maxHeight)
|
|
|
- maxHeight = node.height;
|
|
|
- }
|
|
|
- layerHeights.set(layer.index, maxHeight);
|
|
|
- }
|
|
|
-
|
|
|
- // 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;
|
|
|
- }
|
|
|
+ if(!node)
|
|
|
+ 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;
|
|
|
- }
|
|
|
+ node.x = currentX + (layerWidth - node.width) / 2;
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private assignInitialXCoordinates(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
|
|
|
- const layers = layering.getLayers();
|
|
|
|
|
|
- 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;
|
|
|
- }
|
|
|
- }
|
|
|
+ currentX += layerWidth + this._layerGap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private topDownPass(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>): void {
|
|
|
- const layers = layering.getLayers();
|
|
|
- const targetXMap = new Map<string, number>();
|
|
|
+ /**
|
|
|
+ * Назначение Y-координаты для всех вершин
|
|
|
+ * @param graph Граф, содержащий информацию о вершинах и рёбрах
|
|
|
+ * @param layering Укладка графа
|
|
|
+ * @param layers Массив из слоев, по которым распределены вершины
|
|
|
+ */
|
|
|
+ private assignYCoordinates(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, layers: Layer<Node>[]) : void {
|
|
|
+ if(layers.length === 0)
|
|
|
+ return;
|
|
|
|
|
|
- // Compute target positions based on incoming neighbors
|
|
|
- for(let i = 1; i < layers.length; i++) {
|
|
|
- const layer = layers[i]!;
|
|
|
- for(const nodeId of layer.nodes) {
|
|
|
- const node = graph.getNode(nodeId);
|
|
|
- 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);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ this.distributeLayerEvenly(graph, layers[0]!);
|
|
|
|
|
|
- // Apply positions with constraints
|
|
|
for(let i = 1; i < layers.length; i++) {
|
|
|
- this.applyPositionsWithConstraints(graph, layers[i]!, targetXMap);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- 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]!;
|
|
|
- for(const nodeId of layer.nodes) {
|
|
|
- const node = graph.getNode(nodeId);
|
|
|
- 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);
|
|
|
- }
|
|
|
- }
|
|
|
+ this.assignYByBarycenter(graph, layer);
|
|
|
+ this.resolveNodeOverlaps(graph, layer);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // Apply positions with constraints
|
|
|
- for(let i = layers.length - 2; i >= 0; i--) {
|
|
|
- this.applyPositionsWithConstraints(graph, layers[i]!, targetXMap);
|
|
|
+ /**
|
|
|
+ * Равномерное распределение вершин внутри слоя
|
|
|
+ * @param graph Граф, содержащий информацию о вершинах и рёбрах
|
|
|
+ * @param layer Слой, вершины которого нужно распределить равномерно
|
|
|
+ */
|
|
|
+ private distributeLayerEvenly(graph: Graph<Node, Edge<Node>>, layer: Layer<Node>) : void {
|
|
|
+ let currentY = 0.0;
|
|
|
+
|
|
|
+ for(const nodeId of layer.nodes) {
|
|
|
+ const node = graph.getNode(nodeId);
|
|
|
+ if(!node)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ node.y = currentY;
|
|
|
+ currentY += node.height + this._nodeGap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- 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)!;
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Назначение Y-координаты для вершин внутрия слоя с выравниванием по barycenter соседних вершин
|
|
|
+ * @param graph Граф, содержащий информацию о вершинах и рёбрах
|
|
|
+ * @param layer Слой, вершины которого нужно распределить и выровнять
|
|
|
+ */
|
|
|
+ private assignYByBarycenter(graph: Graph<Node, Edge<Node>>, layer: Layer<Node>) : void {
|
|
|
+ const assignments: { nodeId: Node["id"], y: number }[] = [];
|
|
|
+
|
|
|
+ for(const nodeId of layer.nodes) {
|
|
|
+ const node = graph.getNode(nodeId);
|
|
|
+ if(!node)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ const predecessors = graph.getNodeInputs(nodeId);
|
|
|
+ if(predecessors.length === 0) {
|
|
|
+ assignments.push({ nodeId, y: node.y ?? 0 });
|
|
|
+ continue;
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- // 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;
|
|
|
- }
|
|
|
+ const averageY = predecessors.reduce((sum, pred) => sum + (pred.y ?? 0) + (pred.height ?? 0) / 2, 0) / predecessors.length;
|
|
|
+ assignments.push({ nodeId, y: averageY - node.height / 2});
|
|
|
}
|
|
|
|
|
|
- // 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]!);
|
|
|
+ assignments.sort((a, b) => a.y - b.y);
|
|
|
|
|
|
- if(!leftNode || !rightNode) continue;
|
|
|
+ for(const { nodeId, y } of assignments) {
|
|
|
+ const node = graph.getNode(nodeId);
|
|
|
+ if(!node)
|
|
|
+ continue;
|
|
|
|
|
|
- const minDistance = leftNode.width / 2 + this._horizontalSpacing + rightNode.width / 2;
|
|
|
+ node.y = y;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if(leftNode.x > rightNode.x - minDistance) {
|
|
|
- leftNode.x = rightNode.x - minDistance;
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Разрешение конфликтов, связанных с перекрытием вершин друг другом
|
|
|
+ * @param graph Граф, содержащий информацию о вершинах и рёбрах
|
|
|
+ * @param layer Слой, в котором необходимо разрешить конфликты перекрытия вершин
|
|
|
+ */
|
|
|
+ private resolveNodeOverlaps(graph: Graph<Node, Edge<Node>>, layer: Layer<Node>) : void {
|
|
|
+ const nodes = layer.nodes.map((nodeId) => graph.getNode(nodeId)).filter((node) => node !== null).sort((a, b) => (a.y ?? 0) - (b.y ?? 0));
|
|
|
+
|
|
|
+ for(let i = 1; i < nodes.length; i++) {
|
|
|
+ const previous = nodes[i - 1], current = nodes[i];
|
|
|
+ const minY = (previous?.y ?? 0) + (previous?.height ?? 0) + this._nodeGap;
|
|
|
+
|
|
|
+ if(current && (current.y ?? 0) < minY)
|
|
|
+ current.y = minY;
|
|
|
}
|
|
|
+
|
|
|
+ this.centerNodesVertically(nodes);
|
|
|
}
|
|
|
|
|
|
- private centerLayout(graph: Graph<Node, Edge<Node>>): void {
|
|
|
- const nodes = graph.getNodes();
|
|
|
|
|
|
- if(nodes.length === 0) return;
|
|
|
+ /**
|
|
|
+ * Центрирование вершин в слое по вертикали
|
|
|
+ * @param nodes Вершины, которые надо отцентровать
|
|
|
+ */
|
|
|
+ private centerNodesVertically(nodes: Node[]) : void {
|
|
|
+ if(!nodes || nodes.length == 0)
|
|
|
+ return;
|
|
|
|
|
|
- // Find bounding box
|
|
|
- let minX = Infinity;
|
|
|
- let maxX = -Infinity;
|
|
|
+ const totalHeight = nodes.reduce((sum, node) => sum + (node.height ?? 0), 0) + this._nodeGap * (nodes.length - 1);
|
|
|
|
|
|
- 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);
|
|
|
- }
|
|
|
+ const firstY = nodes[0]?.y ?? 0;
|
|
|
+ const offset = firstY - totalHeight / 2;
|
|
|
|
|
|
- // Calculate center offset
|
|
|
- const center = (minX + maxX) / 2;
|
|
|
+ const eps = 1.0;
|
|
|
+ if(Math.abs(offset) < eps)
|
|
|
+ return;
|
|
|
|
|
|
- // Center all nodes
|
|
|
- for(const node of nodes) {
|
|
|
- node.x -= center;
|
|
|
- }
|
|
|
+ for(const node of nodes)
|
|
|
+ node.y = (node.y ?? 0) - offset;
|
|
|
}
|
|
|
}
|