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"; /** * Шаг назначения координат вершинам * Размещает вершины в двумерной плоскости по колонкам (слоям) и строкам (позициям в слое), * учитывая размеры вершин, вертикальный отступ между строками и горизонтальный отступ между слоями. */ export default class CoordinateAssignmentStep extends AlgorithmStep { /** * Отступ между вершинами по вертикали внутри одной колонки. */ 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"); 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!"); this.assignCoordinates(layering); } /** * Назначение координат всем вершинам графа. * * Алгоритм: * - слои обходятся справа налево (`toReversed()`), * - для каждого слоя вычисляется ширина колонки как максимальная ширина вершины в слое, * - для каждой позиции `i` внутри слоя вычисляется высота строки как максимум по всем слоям в этой позиции `i`, * - вершина размещается по центру своей ячейки. * @param layering Слоистая укладка графа. */ private assignCoordinates(layering: Layering>) : 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; } } }