|
|
@@ -1,10 +1,8 @@
|
|
|
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 Layering from "../../graph/layering/Layering.js";
|
|
|
-import Node from "../../graph/node/Node.js";
|
|
|
+import CoordinateAssignmentStepError from "../../errors/optimizer/CoordinateAssignmentStepError.js";
|
|
|
import NodeGrid from "../../graph/grid/NodeGrid.js";
|
|
|
+import Node from "../../graph/node/Node.js";
|
|
|
|
|
|
/**
|
|
|
* Шаг назначения координат вершинам
|
|
|
@@ -12,112 +10,34 @@ import NodeGrid from "../../graph/grid/NodeGrid.js";
|
|
|
* учитывая размеры вершин, вертикальный отступ между строками и горизонтальный отступ между слоями.
|
|
|
*/
|
|
|
export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaContext> {
|
|
|
- /**
|
|
|
- * Отступ между вершинами по вертикали внутри одной колонки.
|
|
|
- */
|
|
|
- private readonly _layerGap: 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");
|
|
|
-
|
|
|
+ public constructor() {
|
|
|
super(CoordinateAssignmentStep.name);
|
|
|
-
|
|
|
- this._layerGap = layerGap;
|
|
|
- this._padding = padding;
|
|
|
}
|
|
|
|
|
|
public run(context: SiguiyamaContext): void {
|
|
|
- const { layering } = context;
|
|
|
-
|
|
|
- if(!layering)
|
|
|
- throw new Error("Layering of graph was not found!");
|
|
|
-
|
|
|
- const grid = this.buildGrid(layering);
|
|
|
- this.assignCoordinatesByGrid(grid);
|
|
|
-
|
|
|
- context.grid = grid;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Назначение координат всем вершинам графа.
|
|
|
- *
|
|
|
- * Алгоритм:
|
|
|
- * - слои обходятся справа налево (`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));
|
|
|
+ const { grid } = context;
|
|
|
|
|
|
- node.setX(xOffset + width / 2 - node.getWidth() / 2).setY(yOffset + height / 2 - node.getHeight() / 2);
|
|
|
+ if(!grid)
|
|
|
+ throw new CoordinateAssignmentStepError("Grid is null or undefined!");
|
|
|
|
|
|
- yOffset += height + this._layerGap;
|
|
|
- }
|
|
|
-
|
|
|
- xOffset += width + this._padding;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private buildGrid(layering: Layering<Node, Edge<Node>>) : NodeGrid<Node> {
|
|
|
- const layers = layering.getLayers().toReversed();
|
|
|
- const grid = new NodeGrid(layers.length, Math.max(...layers.map((l) => l.getNodes().length)))
|
|
|
-
|
|
|
- for(let i = 0; i < layers.length; i++) {
|
|
|
- const layer = layers[i]!, nodes = layer.getNodes();
|
|
|
-
|
|
|
- for(let j = 0; j < nodes.length; j++) {
|
|
|
- grid.set(j, i, nodes[j]!);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return grid;
|
|
|
+ this.setCoordinatesByGrid(grid);
|
|
|
}
|
|
|
|
|
|
- private assignCoordinatesByGrid(grid: NodeGrid<Node>) : void {
|
|
|
- const gridSize = grid.getSize();
|
|
|
-
|
|
|
- let yOffset = 0.0;
|
|
|
- for(let r = 0; r < gridSize.height; r++) {
|
|
|
- const rowHeight = grid.getHeight(r, 0);
|
|
|
- let xOffset = 0.0;
|
|
|
+ private setCoordinatesByGrid(grid: NodeGrid<Node>) : void {
|
|
|
+ const { height: rowsCount, width: colsCount } = grid.getSize();
|
|
|
|
|
|
- for(let c = 0; c < gridSize.width; c++) {
|
|
|
- const cellSize = grid.getCellSize(r, c);
|
|
|
+ for(let r = 0; r < rowsCount; r++) {
|
|
|
+ for(let c = 0; c < colsCount; c++) {
|
|
|
const node = grid.get(r, c);
|
|
|
+ if(!node)
|
|
|
+ continue;
|
|
|
|
|
|
- if(node)
|
|
|
- node.setX(xOffset + cellSize.width / 2 - node.getWidth() / 2).setY(yOffset + cellSize.height / 2 - node.getHeight() / 2);
|
|
|
+ const cellCoordinates = grid.getCellCoordinates(r, c);
|
|
|
+ const cellSize = grid.getCellSize(r, c);
|
|
|
+ const nodeSize = node.getSize();
|
|
|
|
|
|
- xOffset += cellSize.width + this._padding;
|
|
|
+ node.setX(cellCoordinates.x + cellSize.width / 2 - nodeSize.width / 2).setY(cellCoordinates.y + cellSize.height / 2 - nodeSize.height / 2);
|
|
|
}
|
|
|
-
|
|
|
- yOffset += rowHeight + this._layerGap;
|
|
|
}
|
|
|
}
|
|
|
}
|