|
|
@@ -1,107 +1,82 @@
|
|
|
+import AlgorithmStep from "../AlgorithmStep.js";
|
|
|
+import { SiguiyamaContext } from "../siguiyama/SiguiyamaContext.js";
|
|
|
+import EdgeRoutingStepError from "../../errors/optimizer/EdgeRoutingStepError.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/Node.js";
|
|
|
-import AlgorithmStep from "../AlgorithmStep.js";
|
|
|
-import Grid from "../siguiyama/grid/Grid.js";
|
|
|
-import { FeedbackSet, SiguiyamaContext } from "../siguiyama/SiguiyamaContext.js";
|
|
|
|
|
|
+/**
|
|
|
+ * Шаг назначения координат вершинам
|
|
|
+ * Размещает вершины в двумерной плоскости по колонкам (слоям) и строкам (позициям в слое),
|
|
|
+ * учитывая размеры вершин, вертикальный отступ между строками и горизонтальный отступ между слоями.
|
|
|
+ */
|
|
|
export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaContext> {
|
|
|
+ /**
|
|
|
+ * Отступ между вершинами по вертикали внутри одной колонки.
|
|
|
+ */
|
|
|
private readonly _layerGap: number;
|
|
|
- private readonly _nodeGap: number;
|
|
|
|
|
|
- public constructor(layerGap: number, nodeGap: number) {
|
|
|
+ /**
|
|
|
+ * Отступ между колонками (слоями) по горизонтали.
|
|
|
+ */
|
|
|
+ private readonly _padding: number;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param layerGap Вертикальный отступ между вершинами в колонке.
|
|
|
+ * @param padding Горизонтальный отступ между слоями.
|
|
|
+ * @throws {EdgeRoutingStepError} Если передан отрицательный `layerGap` или `padding`.
|
|
|
+ */
|
|
|
+ public constructor(layerGap: number = 100, padding: number = 60) {
|
|
|
+ if(layerGap < 0)
|
|
|
+ throw new EdgeRoutingStepError("Layer Gap must be greater than 0");
|
|
|
+ if(padding < 0)
|
|
|
+ throw new EdgeRoutingStepError("Padding must be greater than 0");
|
|
|
+
|
|
|
super(CoordinateAssignmentStep.name);
|
|
|
+
|
|
|
this._layerGap = layerGap;
|
|
|
- this._nodeGap = nodeGap;
|
|
|
+ this._padding = padding;
|
|
|
}
|
|
|
|
|
|
public run(context: SiguiyamaContext): void {
|
|
|
- const { graph, layering, feedbackSet } = context;
|
|
|
+ const { layering } = 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!");
|
|
|
|
|
|
- // const grid = this.buildGrid(layering);
|
|
|
- // context.grid = grid;
|
|
|
- //
|
|
|
- // this.assignCoordinatesFromGrid(graph, grid);
|
|
|
+ this.assignCoordinates(layering);
|
|
|
}
|
|
|
|
|
|
- // private buildGrid(layering: Layering<Node, Edge<Node>>): Grid<Node> {
|
|
|
- // const grid = new Grid<Node>();
|
|
|
- // const layers = layering.getLayers();
|
|
|
- //
|
|
|
- // for(let col = layers.length - 1; col >= 0; col--) {
|
|
|
- // const layer = layers[col]!;
|
|
|
- //
|
|
|
- // for(let row = 0; row < layer.nodes.length; row++)
|
|
|
- // grid.set(row, col, layer.nodes[row]!);
|
|
|
- // }
|
|
|
- //
|
|
|
- // return grid;
|
|
|
- // }
|
|
|
- //
|
|
|
- // private assignCoordinatesFromGrid(graph: Graph<Node, Edge<Node>>, grid: Grid<Node>): void {
|
|
|
- // const PADDING = 60;
|
|
|
- //
|
|
|
- // const colWidths = this.computeColWidths(grid);
|
|
|
- // const rowHeights = this.computeRowHeights(grid);
|
|
|
- //
|
|
|
- // const colX = this.computeOffsets(colWidths, this._nodeGap, PADDING);
|
|
|
- // const rowY = this.computeOffsets(rowHeights, this._layerGap, PADDING);
|
|
|
- //
|
|
|
- // for(let col = 0; col < grid.getCols(); col++) {
|
|
|
- // for(let row = 0; row < grid.getRows(); row++) {
|
|
|
- // const node = grid.get(row, col);
|
|
|
- // if(!node)
|
|
|
- // continue;
|
|
|
- //
|
|
|
- // node.setX(colX[col]! + (colWidths[col]! - node.getWidth()) / 2);
|
|
|
- // node.setY(rowY[row]! + (rowHeights[row]! - node.getHeight()) / 2);
|
|
|
- // }
|
|
|
- // }
|
|
|
- // }
|
|
|
- //
|
|
|
- // private computeColWidths(grid: Grid<Node>): number[] {
|
|
|
- // const widths: number[] = new Array(grid.getCols()).fill(0);
|
|
|
- //
|
|
|
- // for(let col = 0; col < grid.getCols(); col++)
|
|
|
- // for(let row = 0; row < grid.getRows(); row++) {
|
|
|
- // const node = grid.get(row, col);
|
|
|
- // if(!node)
|
|
|
- // continue;
|
|
|
- //
|
|
|
- // widths[col] = Math.max(widths[col]!, node.getWidth());
|
|
|
- // }
|
|
|
- //
|
|
|
- // return widths;
|
|
|
- // }
|
|
|
- //
|
|
|
- // private computeRowHeights(grid: Grid<Node>): number[] {
|
|
|
- // const heights: number[] = new Array(grid.getRows()).fill(0);
|
|
|
- //
|
|
|
- // for(let row = 0; row < grid.getRows(); row++)
|
|
|
- // for(let col = 0; col < grid.getCols(); col++) {
|
|
|
- // const node = grid.get(row, col);
|
|
|
- // if(!node) continue;
|
|
|
- //
|
|
|
- // heights[row] = Math.max(heights[row]!, node.getHeight());
|
|
|
- // }
|
|
|
- //
|
|
|
- // return heights;
|
|
|
- // }
|
|
|
- //
|
|
|
- // private computeOffsets(sizes: number[], gap: number, padding: number): number[] {
|
|
|
- // const offsets: number[] = [padding];
|
|
|
- //
|
|
|
- // for(let i = 1; i < sizes.length; i++)
|
|
|
- // offsets.push(offsets[i - 1]! + sizes[i - 1]! + gap);
|
|
|
- //
|
|
|
- // return offsets;
|
|
|
- // }
|
|
|
-}
|
|
|
+ /**
|
|
|
+ * Назначение координат всем вершинам графа.
|
|
|
+ *
|
|
|
+ * Алгоритм:
|
|
|
+ * - слои обходятся справа налево (`toReversed()`),
|
|
|
+ * - для каждого слоя вычисляется ширина колонки как максимальная ширина вершины в слое,
|
|
|
+ * - для каждой позиции `i` внутри слоя вычисляется высота строки как максимум по всем слоям в этой позиции `i`,
|
|
|
+ * - вершина размещается по центру своей ячейки.
|
|
|
+ * @param layering Слоистая укладка графа.
|
|
|
+ */
|
|
|
+ private assignCoordinates(layering: Layering<Node, Edge<Node>>) : void {
|
|
|
+ const layers = layering.getLayers().toReversed();
|
|
|
+
|
|
|
+ let xOffset = 0.0;
|
|
|
+ for(const layer of layers) {
|
|
|
+ const nodes = layer.getNodes();
|
|
|
+
|
|
|
+ const width = Math.max(...nodes.map(node => node.getWidth()));
|
|
|
+
|
|
|
+ let yOffset = 0.0;
|
|
|
+ for(let i = 0; i < nodes.length; i++) {
|
|
|
+ const node = nodes[i]!;
|
|
|
+ const height = Math.max(...layers.map((l) => l.getNodes().at(i)?.getHeight() ?? 0));
|
|
|
+
|
|
|
+ node.setX(xOffset + width / 2 - node.getWidth() / 2).setY(yOffset + height / 2 - node.getHeight() / 2);
|
|
|
+
|
|
|
+ yOffset += height + this._layerGap;
|
|
|
+ }
|
|
|
+
|
|
|
+ xOffset += width + this._padding;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|