Browse Source

grid big update

Pavel Zhigalov 2 weeks ago
parent
commit
6ac01bb19b

+ 11 - 0
src/errors/optimizer/CoordinateAssignmentStepError.ts

@@ -0,0 +1,11 @@
+import OptimizerError from "./OptimizerError.js";
+
+/**
+ * Ошибка, возникшая в процессе работы класса {@link CoordinateAssignmentStep}
+ */
+export default class CoordinateAssignmentStepError extends OptimizerError {
+	public constructor(message: string) {
+		super(message);
+		this.name = CoordinateAssignmentStepError.name;
+	}
+}

+ 11 - 0
src/errors/optimizer/CreateGridStepError.ts

@@ -0,0 +1,11 @@
+import OptimizerError from "./OptimizerError.js";
+
+/**
+ * Ошибка, возникшая во время работы класса {@link CreateGridStep}
+ */
+export default class CreateGridStepError extends OptimizerError {
+	public constructor(message: string) {
+		super(message);
+		this.name = CreateGridStepError.name;
+	}
+}

+ 1 - 1
src/graph/grid/Grid.ts

@@ -1,7 +1,7 @@
 import GridError from "../../errors/graph/layering/grid/GridError.js";
 
 /**
- * Стека для укладка графа на двумерной плоскости
+ * Сетка для укладка графа на двумерной плоскости
  */
 export class Grid<TElement> {
 	/**

+ 79 - 0
src/graph/grid/NodeGrid.ts

@@ -2,7 +2,34 @@ import {Grid} from "./Grid.js";
 import Node from "../node/Node.js";
 import DummyNode from "../node/DummyNode.js";
 
+/**
+ * Сетка для укладка графа на двумерной плоскости, каждая ячейка которой может содержать объекта типа {@link TNode}
+ * @template TNode Объекта класса {@link Node} или его подкласса
+ */
 export default class NodeGrid<TNode extends Node> extends Grid<TNode> {
+	/**
+     * Горизонтальные отступы между ячейками в сетке
+     * @private
+     */
+	private readonly _horizontalGap: number;
+	/**
+     * Вертикальные отступы между ячейками в сетке
+     * @private
+     */
+	private readonly _verticalGap: number;
+
+	public constructor(width: number, height: number, horizontalGap: number = 0, verticalGap: number = 0) {
+		super(width, height);
+		this._horizontalGap = horizontalGap;
+		this._verticalGap = verticalGap;
+	}
+
+	/**
+     * Получение высоты ячейки в сетке. Вычисляется как максимальная высота среди всех ячеек в строке
+     * @param row Индекс строки ячейки
+     * @param col Индекс столбца ячейки
+     * @return Высота ячейки в пикселях
+     */
 	public getHeight(row: number, col: number) : number {
 		this.validate(row, col);
 
@@ -17,6 +44,12 @@ export default class NodeGrid<TNode extends Node> extends Grid<TNode> {
 		return height;
 	}
 
+	/**
+     * Получение ширины ячейки в сетке. Вычисляется как максимальная ширина среди всех ячеек в столбце
+     * @param row Индекс строки ячейки
+     * @param col Индекс столбца ячейки
+     * @return Ширина ячейки в пикселях
+     */
 	public getWidth(row: number, col: number) : number {
 		this.validate(row, col);
 
@@ -31,10 +64,56 @@ export default class NodeGrid<TNode extends Node> extends Grid<TNode> {
 		return width;
 	}
 
+	/**
+     * Получение размеров ячейки (ширина, высота) в двумерной сетке
+     * @param row Индекс строки ячейки
+     * @param col Индекс столбца ячейки
+     * @return Объект с информацией я размерах ячейки в сетке
+     */
 	public getCellSize(row: number, col: number) : { width: number, height: number } {
 		return { width: this.getWidth(row, col), height: this.getHeight(row, col) };
 	}
 
+	/**
+     * Получение X-координаты ячейки в двумерной сетке
+     * @param col Индекс столбца ячейки
+     * @return X-координата ячейки
+     * @private
+     */
+	private getCellX(col: number) : number {
+		let x = 0.0;
+
+		for(let i = 1; i <= col; i++)
+			x += this.getWidth(0, i - 1) + this._verticalGap;
+
+		return x;
+	}
+
+	/**
+     * Получение Y-координаты ячейки в двумерной сетке
+     * @param row Индекс строки ячейки
+     * @return Y-координата ячейки
+     * @private
+     */
+	private getCellY(row: number) : number {
+		let y = 0.0;
+
+		for(let i = 1; i <= row; i++)
+			y += this.getHeight(i - 1, 0) + this._horizontalGap;
+
+		return y;
+	}
+
+	/**
+     * Получение координат ячейки в двумерной сетке
+     * @param row Индекс строки ячейки
+     * @param col Индекс столбца ячейки
+     * @return Координаты ячейки в двумерной сетке
+     */
+	public getCellCoordinates(row: number, col: number) : { x: number, y: number } {
+		return { x: this.getCellX(col), y: this.getCellY(row) };
+	}
+
 	public toString() : string {
 		let result = "";
 

+ 26 - 2
src/graph/layering/Layer.ts

@@ -1,5 +1,8 @@
 import Node from "../node/Node.js";
 
+/**
+ * Слой в слоистой укладке графа
+ */
 export default class Layer<TNode extends Node> {
 	private readonly _nodes: TNode[];
 
@@ -7,17 +10,38 @@ export default class Layer<TNode extends Node> {
 		this._nodes = [];
 	}
 
+	/**
+     * Список всех вершин, входящих в состав слоя
+     */
 	public getNodes(): TNode[] {
 		return [...this._nodes];
 	}
 
+	/**
+     * Обновление списка вершин, входящих в слой
+     * @param nodes
+     */
 	public setNodes(nodes: TNode[]) : this {
 		this._nodes.length = 0;
 		this._nodes.push(...nodes);
 		return this;
 	}
 
-	public addNode(node: TNode) : this {
-		return this._nodes.push(node), this;
+	/**
+     * Добавление новой вершины в слой
+     * @param node Вершина, которую необходимо добавить в слой
+     * @param index Индекс, по которому необходимо вставить вершину. Если не задано, то новая вершина вставляется в конец слоя
+     */
+	public addNode(node: TNode, index?: number) : this {
+		return index !== undefined ? this._nodes.splice(index, 0, node) : this._nodes.push(node), this;
+	}
+
+	/**
+     * Порядковый номер вершины в слое
+     * @param node Вершина, для которой нужно вычислить порядковый номер
+     * @return Порядковый номер вершины, начиная с 0; `-1`, если такой вершины нет в слое
+     */
+	public getNodeIndex(node: TNode) : number {
+		return this._nodes.findIndex((n) => n.getId() === node.getId());
 	}
 }

+ 6 - 6
src/graph/layering/Layering.ts

@@ -14,21 +14,21 @@ export default class Layering<TNode extends Node, TEdge extends Edge<TNode>> {
 	}
 
 	/**
-	 * Добавление вершины в слой.
-	 * Если слой, в который добавляется вершина, не существует, то он создаётся
+	 * Добавление вершины в слой. Если слой, в который добавляется вершина, не существует, то он создаётся
 	 * @param layerIndex Номер слоя, начиная с 0, в который добавляется веришна
 	 * @param node Вершина, которую необходимо добавить
+     * @param index Индекс, по которому необходимо вставить вершину. Если не задано, то новая вершина вставляется в конец слоя
 	 */
-	public addToLayer(layerIndex: number, node: TNode) : this {
+	public addToLayer(layerIndex: number, node: TNode, index?: number) : this {
 		if(layerIndex < 0)
 			throw new LayeringError("Layer index can not be negative");
 
 		const layer = this._layers[layerIndex];
 
 		if(!layer)
-			this._layers[layerIndex] = new Layer<TNode>().addNode(node);
+			this._layers[layerIndex] = new Layer<TNode>().addNode(node, index);
 		else
-			layer.addNode(node);
+			layer.addNode(node, index);
 
 		return this;
 	}
@@ -52,7 +52,7 @@ export default class Layering<TNode extends Node, TEdge extends Edge<TNode>> {
 	 * @returns Индекс слоя или `-1`, если вершина отсутствует во всех слоях.
 	 */
 	public getNodeLayerIndex(node: TNode) : number {
-		return this._layers.findIndex((layer) => layer.getNodes().includes(node));
+		return this._layers.findIndex((layer) => layer.getNodeIndex(node) !== -1);
 	}
 
 	/**

+ 7 - 0
src/graph/node/Node.ts

@@ -84,6 +84,13 @@ export default class Node extends Observable {
 	public setType(type: string) : this {
 		return this._type = type, this.notify(), this;
 	}
+
+	/**
+     * Размеры вершины (ширина, высота)
+     */
+	public getSize() : { width: number, height: number } {
+		return { width: this.getWidth(), height: this.getHeight() };
+	}
 }
 
 export const NodeSchema = object({

+ 18 - 10
src/index.ts

@@ -4,18 +4,22 @@ import Node from "./graph/node/Node.js";
 import Edge from "./graph/edge/Edge.js";
 import BPMNError from "./errors/BPMNError.js";
 import DzwfJsonDeserializer from "./io/deserialize/json/DzwfJsonDeserializer.js";
-import {readFileSync} from "node:fs";
+import {readFileSync, writeFileSync} from "node:fs";
 import CycleRemoveStep from "./optimizer/steps/CycleRemoveStep.js";
 import LayerAssignmentStep from "./optimizer/steps/LayerAssignmentStep.js";
 import NodeOrderingStep from "./optimizer/steps/NodeOrderingStep.js";
 import CoordinateAssignmentStep from "./optimizer/steps/CoordinateAssignmentStep.js";
+import EdgeRoutingStep from "./optimizer/steps/EdgeRoutingStep.js";
+import CleanupStep from "./optimizer/steps/CleanupStep.js";
+import DzwfJsonSerializer from "./io/serialize/json/DzwfJsonSerializer.js";
+import CreateGridStep from "./optimizer/steps/CreateGridStep.js";
 
 export class BPMNOptimizer {
-	private _optimizationAlgorithm?: SiguiyamaAlgorithm;
+	private _algorithm?: SiguiyamaAlgorithm;
 	private _graph?: Graph<Node, Edge<Node>>;
 
-	public setOptimizationAlgorithm(algorithm: SiguiyamaAlgorithm) : this {
-		this._optimizationAlgorithm = algorithm;
+	public setAlgorithm(algorithm: SiguiyamaAlgorithm) : this {
+		this._algorithm = algorithm;
 		return this;
 	}
 
@@ -25,17 +29,21 @@ export class BPMNOptimizer {
 	}
 
 	public run() : void {
-		if(!this._optimizationAlgorithm)
+		if(!this._algorithm)
 			throw new BPMNError("Optimization algorithm not found, call \"setOptimizationAlgorithm\" method first!");
 		if(!this._graph)
 			throw new BPMNError("Graph graph not found, call \"setGraph\" method first!");
 
-		this._optimizationAlgorithm.run(this._graph);
+		this._algorithm.run(this._graph);
 	}
 }
 
 const deserializer = new DzwfJsonDeserializer();
-const data = deserializer.deserialize(readFileSync("./data/bpmn-graph.json").toString());
-const algorithm = new SiguiyamaAlgorithm().addStep(new CycleRemoveStep()).addStep(new LayerAssignmentStep()).addStep(new NodeOrderingStep()).addStep(new CoordinateAssignmentStep());
-const optimizer = new BPMNOptimizer().setOptimizationAlgorithm(algorithm).setGraph(data.graph);
-optimizer.run();
+const data = deserializer.deserialize(readFileSync("./data/newest.dzwf").toString());
+const algorithm = new SiguiyamaAlgorithm().addStep(new CycleRemoveStep()).addStep(new LayerAssignmentStep()).addStep(new NodeOrderingStep()).addStep(new EdgeRoutingStep()).addStep(new CreateGridStep()).addStep(new CoordinateAssignmentStep()).addStep(new CleanupStep());
+const optimizer = new BPMNOptimizer().setAlgorithm(algorithm).setGraph(data.graph);
+optimizer.run();
+
+
+const serializer = new DzwfJsonSerializer();
+writeFileSync("./data/result.dzwf", serializer.serialize(data))

+ 17 - 97
src/optimizer/steps/CoordinateAssignmentStep.ts

@@ -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;
 		}
 	}
 }

+ 63 - 0
src/optimizer/steps/CreateGridStep.ts

@@ -0,0 +1,63 @@
+import AlgorithmStep from "../AlgorithmStep.js";
+import {SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
+import NodeGrid from "../../graph/grid/NodeGrid.js";
+import Node from "../../graph/node/Node.js";
+import Layering from "../../graph/layering/Layering.js";
+import Edge from "../../graph/edge/Edge.js";
+import CreateGridStepError from "../../errors/optimizer/CreateGridStepError.js";
+
+/**
+ * Создание двумерной сетки для слоистой укладки графа
+ */
+export default class CreateGridStep extends AlgorithmStep<SiguiyamaContext> {
+	/**
+     * Отступ между вершинами по вертикали внутри одной колонки.
+     */
+	private readonly _horizontalGap: number;
+
+	/**
+     * Отступ между колонками (слоями) по горизонтали.
+     */
+	private readonly _verticalGap: number;
+    
+	/**
+     * @param horizontalGap Вертикальный отступ между вершинами в колонке.
+     * @param verticalGap Горизонтальный отступ между слоями.
+     * @throws {CreateGridStepError} Если передан отрицательный `layerGap` или `padding`.
+     */
+	public constructor(horizontalGap: number = 100, verticalGap: number = 60) {
+		if(horizontalGap < 0)
+			throw new CreateGridStepError("Horizontal gap must be >= 0");
+		if(verticalGap < 0)
+			throw new CreateGridStepError("Padding must be >= 0");
+
+		super(CreateGridStep.name);
+
+		this._horizontalGap = horizontalGap;
+		this._verticalGap = verticalGap;
+	}
+
+	public run(context: SiguiyamaContext): void {
+		const { layering } = context;
+
+		if(!layering)
+			throw new CreateGridStepError("Layering of graph was not found!");
+
+		context.grid = this.createGrid(layering);
+	}
+
+	private createGrid(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)), this._horizontalGap, this._verticalGap)
+
+		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;
+	}
+}

+ 18 - 16
src/optimizer/steps/EdgeRoutingStep.ts

@@ -4,9 +4,9 @@ import EdgeRoutingStepError from "../../errors/optimizer/EdgeRoutingStepError.js
 import Graph from "../../graph/Graph.js";
 import Node from "../../graph/node/Node.js";
 import Edge from "../../graph/edge/Edge.js";
-import NodeGrid from "../../graph/grid/NodeGrid.js";
 import DummyNode from "../../graph/node/DummyNode.js";
 import DummyEdge from "../../graph/edge/DummyEdge.js";
+import Layering from "../../graph/layering/Layering.js";
 
 export default class EdgeRoutingStep extends AlgorithmStep<SiguiyamaContext> {
 	public constructor() {
@@ -14,43 +14,45 @@ export default class EdgeRoutingStep extends AlgorithmStep<SiguiyamaContext> {
 	}
 
 	public run(context: SiguiyamaContext) : void {
-		const { graph, grid, edgeSubdivisions } = context;
+		const { graph, layering, edgeSubdivisions } = context;
 
 		if(!graph)
 			throw new EdgeRoutingStepError("Graph is null or undefined");
-		if(!grid)
-			throw new EdgeRoutingStepError("Grid is null or undefined");
+		if(!layering)
+			throw new EdgeRoutingStepError("Layering is null or undefined");
 		if(!edgeSubdivisions)
 			throw new EdgeRoutingStepError("Edge subdivisions information is null or undefined");
 
-		this.manhattanEdgeRouting(graph, grid, edgeSubdivisions);
+		this.manhattanEdgeRouting(graph, layering, edgeSubdivisions);
 	}
 
-	private manhattanEdgeRouting(graph: Graph<Node, Edge<Node>>, grid: NodeGrid<Node>, edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>>) : void {
+	private manhattanEdgeRouting(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, edgeSubdivisions: EdgeSubdivisions<Node, Edge<Node>>) : void {
 		const edges = graph.getEdges();
 
 		for(const edge of edges) {
 			const from = edge.getFrom(), to = edge.getTo();
-			const fromPosition = grid.getPositionOf(from), toPosition = grid.getPositionOf(to);
+			const fromRow = this.getNodeRow(from, layering), toRow = this.getNodeRow(to, layering);
 
-			if(!fromPosition || !toPosition || fromPosition.row === toPosition.row)
+			if(fromRow === -1 || toRow === -1 || fromRow === toRow)
 				continue;
 
-			const fromRow = fromPosition.row, fromColumn = fromPosition.column;
-			const toRow = toPosition.row, toColumn = toPosition.column;
-
-			const targetCellPosition: { row: number, column: number } = toRow > fromRow ? { row: toRow, column: fromColumn } : { row: fromRow, column: toColumn };
-
-			const cellSize = grid.getCellSize(targetCellPosition.row, targetCellPosition.column);
-			const dummyNode = new DummyNode(cellSize.width / 2,cellSize.height / 2);
+			const dummyNode = new DummyNode(0, 0);
 			graph.addNode(dummyNode);
-			grid.set(targetCellPosition.row, targetCellPosition.column, dummyNode);
 
 			const left = new DummyEdge(from, dummyNode), right = new DummyEdge(dummyNode, to);
 			graph.addEdge(left).addEdge(right).removeEdge(edge.getId());
 
 			edgeSubdivisions.set(edge, { left, right });
 
+			layering.addToLayer(layering.getNodeLayerIndex(toRow > fromRow ? from : to), dummyNode, toRow);
 		}
 	}
+
+	private getNodeRow(node: Node, layering: Layering<Node, Edge<Node>>) : number {
+		const layerIndex = layering.getNodeLayerIndex(node);
+		if(layerIndex === -1)
+			return -1;
+
+		return layering.getLayers()[layerIndex]!.getNodeIndex(node);
+	}
 }