Pavel Zhigalov před 1 měsícem
rodič
revize
54c32c0a4e

+ 10 - 6
src/index.ts

@@ -7,14 +7,14 @@ import Deserializer from "./v1/io/deserialize/Deserializer.js";
 import DefaultJsonDeserializer from "./v1/io/deserialize/json/DefaultJsonDeserializer.js";
 import Serializer from "./v1/io/serialize/Serializer.js";
 import DefaultJsonSerializer from "./v1/io/serialize/json/DefaultJsonSerializer.js";
-import CoordinateAssignmentStep from "./v1/optimizer/siguiyama/CoordinateAssignmentStep.js";
-import CycleRemoveStep from "./v1/optimizer/siguiyama/CycleRemoveStep.js";
-import LayerAssignmentStep from "./v1/optimizer/siguiyama/LayerAssignmentStep.js";
-import NodeOrderingStep from "./v1/optimizer/siguiyama/NodeOrderingStep.js";
+import CoordinateAssignmentStep from "./v1/optimizer/steps/CoordinateAssignmentStep.js";
+import CycleRemoveStep from "./v1/optimizer/steps/CycleRemoveStep.js";
+import LayerAssignmentStep from "./v1/optimizer/steps/LayerAssignmentStep.js";
+import NodeOrderingStep from "./v1/optimizer/steps/NodeOrderingStep.js";
 import SiguiyamaAlgorithm from "./v1/optimizer/siguiyama/SiguiyamaAlgorithm.js";
 import { SiguiyamaContext } from "./v1/optimizer/siguiyama/SiguiyamaContext.js";
 
-class BPMNOptimizer {
+export class BPMNOptimizer {
 	private _optimizationAlgorithm?: SiguiyamaAlgorithm;
 
 	public setOptimizationAlgorithm(algorithm: SiguiyamaAlgorithm) : this {
@@ -37,7 +37,11 @@ function getSerializer() : Serializer<Graph<Node, Edge<Node>>, string> {
 	return new DefaultJsonSerializer();
 }
 
-const algorithm = new SiguiyamaAlgorithm().addStep(new CycleRemoveStep()).addStep(new LayerAssignmentStep()).addStep(new NodeOrderingStep()).addStep(new CoordinateAssignmentStep(100, 50));
+const algorithm = new SiguiyamaAlgorithm()
+	.addStep(new CycleRemoveStep())
+	.addStep(new LayerAssignmentStep())
+	.addStep(new NodeOrderingStep())
+	.addStep(new CoordinateAssignmentStep(100, 50));
 const optimizer = new BPMNOptimizer().setOptimizationAlgorithm(algorithm);
 
 const data = readFileSync("./data/graph.json").toString();

+ 8 - 0
src/v1/errors/optimizer/EdgeRoutingStepError.ts

@@ -0,0 +1,8 @@
+import BPMNError from "../BPMNError.js";
+
+export default class EdgeRoutingStepError extends BPMNError {
+	public constructor(message: string) {
+		super(message);
+		this.name = EdgeRoutingStepError.name;
+	}
+}

+ 63 - 123
src/v1/graph/Graph.ts

@@ -3,129 +3,90 @@ import Edge, { EdgeSchema } from "./edge/Edge.js";
 import Node, { NodeSchema } from "./node/Node.js";
 import GraphError from "../errors/graph/GraphError.js";
 
-type GraphCache<TNode extends Node> = Partial<{
-	isAcyclic: boolean,
-	adjacencyList: Map<TNode["id"], Array<TNode["id"]>>
-}>
-
 export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
-	private _cache: GraphCache<TNode> = {};
+	private readonly _adjacencyList: Map<TNode, Array<TNode>> = new Map();
 
-	private readonly _nodeMap: Map<TNode["id"], TNode>;
-	private readonly _edgeMap: Map<TEdge["id"], TEdge>;
+	private readonly _nodeMap: Map<string, TNode>;
+	private readonly _edgeMap: Map<string, TEdge>;
 
 	public constructor(nodes: TNode[], edges: TEdge[]) {
 		this._nodeMap = new Map();
-		this._edgeMap = new Map();
-		
-		for(const node of nodes)
-			this._nodeMap.set(node.id, node);
-
-		for(const edge of edges)
-			this._edgeMap.set(edge.id, edge);
-	}
-
-	private get adjacencyList(): Map<TNode["id"], Array<TNode["id"]>> {
-		const cache = this._cache.adjacencyList;
+		nodes.forEach((node) => this.addNode(node));
 
-		if(cache)
-			return cache;
+		this._edgeMap = new Map();
+		edges.forEach((edge) => this.addEdge(edge));
 
-		const adjacencyList = new Map<TNode["id"], Array<TNode["id"]>>();
+		for(const edge of edges) {
+			const from = edge.getFrom(), to = edge.getTo();
 
-		for(const [, edge] of this._edgeMap) {
-			const { from, to } = edge;
-			const adjacencyNodes = adjacencyList.get(from);
+			const adjacencyNodes = this._adjacencyList.get(from);
 			if(adjacencyNodes)
 				adjacencyNodes.push(to);
 			else
-				adjacencyList.set(from, [to]);
+				this._adjacencyList.set(from, [to]);
 		}
-
-		return this._cache.adjacencyList = adjacencyList;
 	}
 
-	public get isAcyclic() : boolean {
-		const cache = this._cache.isAcyclic;
-
-		if(cache !== undefined)
-			return cache;
-
-		const queue: TNode["id"][] = [];
-		const nodeToInDegree = new Map<TNode["id"], number>();
+	public isAcyclic() : boolean {
+		const queue: TNode[] = [];
+		const nodeToInDegree = new Map<TNode, number>();
 		const nodesCount = this._nodeMap.size;
 		let visitedNodesCount = 0;
 
-		for(const { id } of this.getNodes())
-			nodeToInDegree.set(id, this.getNodeInputs(id).length);
+		for(const node of this.getNodes())
+			nodeToInDegree.set(node, this.getNodeInputs(node).length);
 
 		for(const [nodeId, inDegree] of nodeToInDegree)
 			if(inDegree === 0)
 				queue.push(nodeId);
 
 		while(queue.length > 0) {
-			const nodeId = queue.pop()!;
+			const node = queue.pop()!;
 			visitedNodesCount++;
 
-			for(const { id: neighbourId } of this.getNodeOutputs(nodeId)) {
-				const inDegree = nodeToInDegree.get(neighbourId);
+			for(const output of this.getNodeOutputs(node)) {
+				const inDegree = nodeToInDegree.get(output);
 				const newInDegree = inDegree !== undefined ? inDegree - 1 : 0;
 
-				nodeToInDegree.set(neighbourId, newInDegree);
+				nodeToInDegree.set(output, newInDegree);
 
 				if(newInDegree === 0)
-					queue.push(neighbourId);
+					queue.push(output);
 			}
 		}
 
-		return this._cache.isAcyclic = visitedNodesCount !== nodesCount;
+		return visitedNodesCount !== nodesCount;
 	}
 
 	public getNodes() : TNode[] {
-		return Array.from(this._nodeMap.values());
-	}
-
-	public getNode(nodeId: TNode["id"]): TNode | null {
-		return this._nodeMap.get(nodeId) || null;
+		return [...this._nodeMap.values()];
 	}
 
 	public addNode(node: TNode) : this {
-		const { id } = node;
+		const id = node.getId();
 
 		if(this._nodeMap.has(id))
 			throw new GraphError(`Can't add a new node to graph: node with id ${id} already exists`);
 
 		this._nodeMap.set(id, node);
-		this.invalidateCache();
-
-		return this;
-	}
-
-	public updateNode(nodeId: TNode["id"], node: TNode) : this {
-		const { id } = node;
 
-		if(!this._nodeMap.has(nodeId))
-			throw new GraphError(`Can't update node: node with id ${nodeId} doesn't exist`);
-
-		if(nodeId != id)
-			throw new GraphError(`Cant' update node: node id cannot be changed`);
-
-		this._nodeMap.set(nodeId, node);
-		this.invalidateCache();
+		node.subscribe(() => this.onNodeChange(node));
 
 		return this;
 	}
 
-	public removeNode(nodeId: TNode["id"]) : this {
+	public removeNode(nodeId: string) : this {
 		if(!this._nodeMap.has(nodeId))
 			throw new GraphError(`Can't delete a node from graph: node with id ${nodeId} doesn't exists`);
 
-		for(const [edgeId, edge] of this._edgeMap)
-			if(edge.from == nodeId || edge.to == nodeId)
+		for(const [edgeId, edge] of this._edgeMap) {
+			const fromId = edge.getFrom().getId(), toId = edge.getTo().getId();
+
+			if(fromId == nodeId || toId == nodeId)
 				this._edgeMap.delete(edgeId);
+		}
 
 		this._nodeMap.delete(nodeId);
-		this.invalidateCache();
 
 		return this;
 	}
@@ -134,99 +95,78 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
 		return Array.from(this._edgeMap.values());
 	}
 
-	public getEdge(edgeId: TEdge["id"]): TEdge | null {
+	public getEdge(edgeId: string): TEdge | null {
 		return this._edgeMap.get(edgeId) || null;
 	}
 
 	public addEdge(edge: TEdge) : this {
-		const { id, from, to } = edge;
+		const id = edge.getId(), from = edge.getFrom(), to = edge.getTo();
 
 		if(this._edgeMap.has(id))
 			throw new GraphError(`Can't add a new edge to graph: edge with id ${id} already exists`);
 
-		if(!this._nodeMap.has(from) || !this._nodeMap.has(to))
-			throw new GraphError(`Can't add a new edge to graph: edge references non-existing nodes`);
+		if(!this._nodeMap.has(from.getId()) || !this._nodeMap.has(to.getId()))
+			throw new GraphError(`Can't add a new edge to graph: edge references to non-existing nodes`);
 
 		this._edgeMap.set(id, edge);
-		this.invalidateCache();
 
-		return this;
-	}
-
-	
-	public updateEdge(id: TEdge["id"], edge: TEdge) : this {
-		const { id: edgeId, from, to } = edge;
-	
-		if(!this._edgeMap.has(id))
-			throw new Error(`Can't update edge: edge with id "${id}" does not exist`);
-
-		if(id !== edgeId)
-			throw new Error(`Can't update edge: edge id cannot be changed`);
-
-		if(!this._nodeMap.has(from) || !this._nodeMap.has(to))
-			throw new Error(`Can't update edge: edge references non-existing nodes`);
-
-		this._edgeMap.set(edgeId, edge);
-		this.invalidateCache();
+		edge.subscribe(() => this.onEdgeChange(edge));
 
 		return this;
 	}
-	
-	public removeEdge(edgeId: TEdge["id"]): this {
+
+	public removeEdge(edgeId: string): this {
 		if(!this._edgeMap.has(edgeId))
 			return this;
 
 		this._edgeMap.delete(edgeId);
-		this.invalidateCache();
 
 		return this;
 	}
 
-	public getNodeInputs(nodeId: TNode["id"]) : TNode[] {
+	public getNodeInputs(node: TNode) : TNode[] {
 		const inputs: TNode[] = [];
 
-		for(const [id, outputs] of this.adjacencyList) {
-			if(!outputs.includes(nodeId))
-				continue;
-
-			const input = this._nodeMap.get(id);
-			if(!input)
+		for(const [input, outputs] of this._adjacencyList) {
+			if(!outputs.includes(node))
 				continue;
-
 			inputs.push(input);
 		}
 
 		return inputs;
 	}
 
-	public getNodeOutputs(nodeId: TNode["id"]) : TNode[] {
-		const outputIds = this.adjacencyList.get(nodeId);
-		if(!outputIds)
-			return [];
-
-		const outputs: TNode[] = [];
-
-		outputIds.forEach((id) => {
-			const node = this._nodeMap.get(id);
-			if(!node)
-				return;
+	public getNodeOutputs(node: TNode) : TNode[] {
+		return  this._adjacencyList.get(node) || [];
+	}
 
-			outputs.push(node);
-		})
+	public isSourceNode(node: TNode) : boolean {
+		return this.getNodeInputs(node).length === 0;
+	}
 
-		return outputs;
+	public isSinkNode(node: TNode) : boolean {
+		return this.getNodeOutputs(node).length === 0;
 	}
 
-	public isSourceNode(nodeId: TNode["id"]) : boolean {
-		return this.getNodeInputs(nodeId).length === 0;
+	protected onNodeChange(node: TNode) : void {
 	}
 
-	public isSinkNode(nodeId: TNode["id"]) : boolean {
-		return this.getNodeOutputs(nodeId).length === 0;
+	protected onEdgeChange(edge: TEdge) : void {
+		this.buildAdjacencyList();
 	}
 
-	private invalidateCache(): void {
-		this._cache = {};
+	private buildAdjacencyList() : void {
+		this._adjacencyList.clear();
+
+		for(const [, edge] of this._edgeMap) {
+			const from = edge.getFrom(), to = edge.getTo();
+
+			const adjacencyNodes = this._adjacencyList.get(from);
+			if(adjacencyNodes)
+				adjacencyNodes.push(to);
+			else
+				this._adjacencyList.set(from, [to]);
+		}
 	}
 }
 

+ 60 - 11
src/v1/graph/edge/Edge.ts

@@ -1,20 +1,69 @@
-import { object, string } from "valibot";
-import Node, { NodeSchema } from "../node/Node.js";
+import {array, object, optional, string} from "valibot";
+import Node, {NodeSchema} from "../node/Node.js";
+import Observable from "../../observable/Observable.js";
 
-export default class Edge<TNode extends Node> {
-	public readonly id: string;
-	public readonly from: TNode["id"];
-	public readonly to: TNode["id"];
+/**
+ * Стандартное ребро графа
+ */
+export default class Edge<TNode extends Node> extends Observable {
+	private readonly _id: string;
+	private _from: TNode;
+	private _to: TNode;
+	private _waypoints: Array<{ x: number, y: number }> = [];
 	
-	public constructor(from: TNode["id"], to: TNode["id"], id?: string) {
-		this.from = from;
-		this.to = to;
-		this.id = id || crypto.randomUUID();
+	public constructor(from: TNode, to: TNode, waypoints?: Array<{ x: number, y: number }>, id?: string) {
+		super();
+        
+		this._id = id || crypto.randomUUID();
+		this._from = from;
+		this._to = to;
+		this._waypoints = waypoints || [];
+	}
+
+	/**
+     * Уникальный идентификатор ребра
+     */
+	public getId() : string {
+		return this._id;
+	}
+
+	/**
+     * Вершина, из которой исходит ребро
+     */
+	public getFrom() : TNode {
+		return this._from;
+	}
+
+	public setFrom(from: TNode) : this {
+		return this._from = from, this.notify(), this;
+	}
+
+	/**
+     * Вершина, в которое ребро входит (целевая вершина)
+     */
+	public getTo() : TNode {
+		return this._to;
+	}
+
+	public setTo(to: TNode) : this {
+		return this._to = to, this.notify(), this;
+	}
+
+	/**
+     * Промежуточные точки в двухмерном пространстве, через которые проходит ребро
+     */
+	public getWaypoints() : Array<{ x: number, y: number }> {
+		return this._waypoints;
+	}
+
+	public setWaypoints(waypoints: Array<{ x: number, y: number }>) : this {
+		return this._waypoints = waypoints, this.notify(), this;
 	}
 }
 
 export const EdgeSchema = object({
 	id: string(),
 	from: NodeSchema.entries.id,
-	to: NodeSchema.entries.id
+	to: NodeSchema.entries.id,
+	waypoints: optional(array(object({ x: NodeSchema.entries.x, y: NodeSchema.entries.y })))
 })

+ 7 - 7
src/v1/graph/layering/Layer.ts

@@ -2,21 +2,21 @@ import Node from "../node/Node.js";
 
 export default class Layer<TNode extends Node> {
 	public readonly index: number;
-	private readonly _nodes: TNode["id"][];
+	private readonly _nodes: TNode[];
 
 	public constructor(index: number) {
 		this.index = index;
 		this._nodes = [];
 	}
 
-	public addNodes(nodes: TNode["id"][]) : this {
+	public addNodes(...nodes: TNode[]) : this {
 		this._nodes.push(...nodes);
 		return this;
 	}
 
-	public removeNodes(nodes: TNode["id"][]) : this {
-		for(const id of nodes) {
-			const index = this._nodes.indexOf(id);
+	public removeNodes(...nodes: TNode[]) : this {
+		for(const node of nodes) {
+			const index = this._nodes.findIndex((n) => n.getId() === node.getId());
 			if(index !== -1)
 				this._nodes.splice(index, 1);
 		}
@@ -24,11 +24,11 @@ export default class Layer<TNode extends Node> {
 		return this;
 	}
 
-	public get nodes(): TNode["id"][] {
+	public get nodes(): TNode[] {
 		return [...this._nodes];
 	}
 
-	public set nodes(nodes: TNode["id"][]) {
+	public set nodes(nodes: TNode[]) {
 		this._nodes.length = 0;
 		this._nodes.push(...nodes);
 	}

+ 15 - 36
src/v1/graph/layering/Layering.ts

@@ -14,7 +14,7 @@ export default class Layering<TNode extends Node, TEdge extends Edge<TNode>> {
 		if(!this._layers.has(layerIndex))
 			this._layers.set(layerIndex, new Layer(layerIndex));
 
-		this._layers.get(layerIndex)!.addNodes([node.id]);
+		this._layers.get(layerIndex)!.addNodes(node);
 
 		return this;
 	}
@@ -23,30 +23,30 @@ export default class Layering<TNode extends Node, TEdge extends Edge<TNode>> {
 		return Array.from(this._layers.values()).sort((a, b) => a.index - b.index);
 	}
 
-	public getLayerOf(nodeId: TNode["id"]) : number | null {
+	public getLayerOf(node: TNode) : number | null {
 		for(const [layerIndex, layer] of this._layers)
-			if(layer.nodes.some((id) => id == nodeId))
+			if(layer.nodes.some((n) => n.getId() === node.getId()))
 				return layerIndex;
 
 		return null;
 	}
 
-	public setLayerOf(nodeId: TNode["id"], newLayerIndex: number) : this {
-		const oldIndex = this.getLayerOf(nodeId);
+	public setLayerOf(node: TNode, newLayerIndex: number) : this {
+		const oldIndex = this.getLayerOf(node);
 		if(oldIndex !== null) {
 			const oldLayer = this._layers.get(oldIndex);
-			oldLayer?.removeNodes([nodeId])
+			oldLayer?.removeNodes(node);
 		}
 
 		if(!this._layers.has(newLayerIndex))
 			this._layers.set(newLayerIndex, new Layer(newLayerIndex));
-		this._layers.get(newLayerIndex)!.addNodes([nodeId]);
+		this._layers.get(newLayerIndex)!.addNodes(node);
 
 		return this;
 	}
 
-	public getPositionOf(nodeId: TNode["id"]) : number | null {
-		const layerIndex = this.getLayerOf(nodeId);
+	public getPositionOf(node: TNode) : number | null {
+		const layerIndex = this.getLayerOf(node);
 		if(layerIndex === null)
 			return null;
 
@@ -54,50 +54,29 @@ export default class Layering<TNode extends Node, TEdge extends Edge<TNode>> {
 		if(!layer)
 			return null;
 
-		const position = layer.nodes.indexOf(nodeId);
+		const position = layer.nodes.indexOf(node);
 		return position !== -1 ? position : null;
 	}
 
 	public isEdgeTight(edge: TEdge) : boolean {
-		const { from, to } = edge;
-		const fromLayerIndex = this.getLayerOf(from), toLayerIndex = this.getLayerOf(to);
+		const fromLayerIndex = this.getLayerOf(edge.getFrom()), toLayerIndex = this.getLayerOf(edge.getTo());
 
 		if(fromLayerIndex == null || toLayerIndex == null)
-			throw new LayeringError(`Node from edge ${edge.id} is not assigned to any layer`);
+			throw new LayeringError(`Node from edge ${edge.getId()} is not assigned to any layer`);
 
 		return Math.abs(fromLayerIndex - toLayerIndex) === 1;
 	}
 
 	public getEdgeSpan(edge: TEdge) : number {
-		const fromLayer = this.getLayerOf(edge.from);
-		const toLayer = this.getLayerOf(edge.to);
+		const fromLayer = this.getLayerOf(edge.getFrom());
+		const toLayer = this.getLayerOf(edge.getTo());
 
 		if(fromLayer === null || toLayer === null)
-			throw new LayeringError(`Node from edge ${edge.id} is not assigned to any layer`);
+			throw new LayeringError(`Node from edge ${edge.getId()} is not assigned to any layer`);
 
 		return Math.abs(fromLayer - toLayer);
 	}
 
-	public normalize() : this {
-		const minLayerIndex = Math.min(...Array.from(this._layers.keys()));
-		if(minLayerIndex === 0) 
-			return this;
-
-		const newLayers = new Map<number, Layer<TNode>>();
-		for(const [index, layer] of this._layers) {
-			const newLayerIndex = index - minLayerIndex;
-			const newLayer = new Layer<TNode>(newLayerIndex);
-			newLayer.addNodes(layer.nodes);
-			newLayers.set(newLayerIndex, newLayer);
-		}
-
-		this._layers.clear();
-		newLayers.forEach((layer, index) => this._layers.set(index, layer));
-	
-		return this;
-	}
-
-
 	public get height() : number {
 		return this._layers.size;
 	}

+ 81 - 13
src/v1/graph/node/Node.ts

@@ -1,20 +1,88 @@
 import { number, object, string } from "valibot";
+import Observable from "../../observable/Observable.js";
 
-export default class Node {
-	public readonly id: string;
-	public x: number;
-	public y: number;
-	public width: number;
-	public height: number;
-	public type: string;
+/**
+ * Стандратная вершина графа
+ */
+export default class Node extends Observable {
+	private readonly _id: string;
+	private _x: number;
+	private _y: number;
+	private _width: number;
+	private _height: number;
+	private _type: string;
 
 	public constructor(x: number, y: number, width: number, height: number, type: string, id?: string) {
-		this.id = id || crypto.randomUUID();
-		this.x = x;
-		this.y = y;
-		this.width = width;
-		this.height = height;
-		this.type = type;
+		super();
+        
+		this._id = id || crypto.randomUUID();
+		this._x = x;
+		this._y = y;
+		this._width = width;
+		this._height = height;
+		this._type = type;
+	}
+
+	/**
+     * Уникальный идентификатор вершины
+     */
+	public getId() : string {
+		return this._id;
+	}
+
+	/**
+     * X-координата вершины в двухмерном пространстве
+     */
+	public getX() : number {
+		return this._x;
+	}
+
+	public setX(x: number) : this {
+		return this._x = x, this.notify(), this;
+	}
+
+	/**
+     * Y-кооридината веришны в двухмерном пространстве
+     */
+	public getY() : number {
+		return this._y;
+	}
+
+	public setY(y: number) : this {
+		return this._y = y, this.notify(), this;
+	}
+
+	/**
+     * "Ширина" вершины
+     */
+	public getWidth() : number {
+		return this._width;
+	}
+
+	public setWidth(width: number) : this {
+		return this._width = width, this.notify(), this;
+	}
+
+	/**
+     * "Длина" вершины
+     */
+	public getHeight() : number {
+		return this._height;
+	}
+
+	public setHeight(height: number) : this {
+		return this._height = height, this.notify(), this;
+	}
+
+	/**
+     * Тип вершины. Может быть любым строковым значением
+     */
+	public getType() : string {
+		return this._type;
+	}
+
+	public setType(type: string) : this {
+		return this._type = type, this.notify(), this;
 	}
 }
 

+ 0 - 35
src/v1/io/deserialize/dzwf/DZWFDeserializer.ts

@@ -1,35 +0,0 @@
-import { InferOutput, parse } from "valibot";
-import DeserializerError from "../../../errors/DeserializerError.js";
-import Edge from "../../../graph/edge/Edge.js";
-import Graph, { GraphSchema } from "../../../graph/Graph.js";
-import Node from "../../../graph/node/Node.js";
-import { JsonDeserializer } from "../json/JsonDeserializer.js";
-import { DZWFData } from "../../dzwf/DZWFData.js";
-
-export default class DZWFDeserializer implements JsonDeserializer<{ graph: Graph<Node, Edge<Node>>, dzwfData: DZWFData & unknown }> {
-	deserialize(input: string) : { graph: Graph<Node, Edge<Node>>, dzwfData: DZWFData & unknown }  {
-		const parsed = JSON.parse(input) as DZWFData;
-
-		if(!parsed)
-			throw new DeserializerError("Result of parsing graph structure not found");
-
-		let graph: InferOutput<typeof GraphSchema>;
-		try {
-			const nodes: InferOutput<typeof GraphSchema>["nodes"] = parsed.elements.map((element) => {
-				const { diagram: { position: { x, y } }, type, id } = element;
-				return { x, y, type, id, width: 0, height: 0 }
-			})
-			const edges: InferOutput<typeof GraphSchema>["edges"] = parsed.links.map((link) => {
-				const { sourceId, targetId, id, diagram } = link;
-				return { id, from: sourceId, to: targetId, waypoints: diagram?.vertices ?? [] };
-			})
-
-			graph = parse(GraphSchema, { nodes, edges });
-		// eslint-disable-next-line @typescript-eslint/no-explicit-any
-		} catch (error: any) {
-			throw new DeserializerError(error.message)
-		}
-
-		return { graph: new Graph(graph.nodes, graph.edges), dzwfData: parsed };
-	}
-}

+ 15 - 1
src/v1/io/deserialize/json/DefaultJsonDeserializer.ts

@@ -17,6 +17,20 @@ export default class DefaultJsonDeserializer implements JsonDeserializer<Graph<N
 			throw new DeserializerError(e.message);
 		}
 
-		return new Graph(graph.nodes, graph.edges);
+		const nodes = graph.nodes.map((node) => new Node(node.x, node.y, node.width, node.height, node.type, node.id));
+
+		const edges = graph.edges.map((edge) => {
+			const { from, to, waypoints } = edge;
+
+			const fromNode = nodes.find((node) => node.getId() === from);
+			const toNode = nodes.find((node) => node.getId() === to);
+
+			if(!fromNode || !toNode)
+				throw new DeserializerError("Can't find node deserialized graph!");
+
+			return new Edge<Node>(fromNode, toNode, waypoints, edge.id);
+		})
+
+		return new Graph(nodes, edges);
 	}
 }

+ 31 - 0
src/v1/observable/Observable.ts

@@ -0,0 +1,31 @@
+/**
+ * Слушатель событий, происходящих внутри класса или объекта
+ */
+type Listener = () => void;
+
+/**
+ * Базовый класс, реализующий паттерн "наблюдатель" для всех дочерних классов
+ */
+export default class Observable {
+	private readonly _listeners: Set<Listener> = new Set();
+
+	/**
+     * Добавление слушателя событий
+     * @param listener Функция, которая будет отрабатывать при возникновении события
+     */
+	public subscribe(listener: Listener) : Listener {
+		this._listeners.add(listener);
+
+		return () => {
+			this._listeners.delete(listener);
+		}
+	}
+
+	/**
+     * Вызов фукнций-обработчиков, добавленных в качестве слушателей
+     * @protected
+     */
+	protected notify() : void {
+		this._listeners.forEach((listener) => listener.call(this));
+	}
+}

+ 2 - 0
src/v1/optimizer/AlgorithmStep.ts

@@ -8,10 +8,12 @@ export default abstract class AlgorithmStep<C extends AlgorithmContext> {
 	}
 
 	public onBeforeRun() : void {
+		console.log(this._id + ' before');
 	}
 
 	public abstract run(context: C) : void;
 	
 	public onAfterRun() : void {
+		console.log(this._id + ' after');
 	}
 }

+ 4 - 2
src/v1/optimizer/siguiyama/SiguiyamaContext.ts

@@ -2,10 +2,12 @@ import Edge from "../../graph/edge/Edge.js"
 import Layering from "../../graph/layering/Layering.js";
 import Node from "../../graph/node/Node.js"
 import { AlgorithmContext } from "../AlgorithmContext.js"
+import Grid from "./grid/Grid.js";
 
-export type FeedbackSet = Edge<Node>["id"][];
+export type FeedbackSet = Edge<Node>[];
 
 export type SiguiyamaContext = AlgorithmContext & Partial<{
 	feedbackSet: FeedbackSet,
-	layering: Layering<Node, Edge<Node>>;
+	layering: Layering<Node, Edge<Node>>,
+	grid: Grid<Node>,
 }>

+ 15 - 4
src/v1/optimizer/siguiyama/grid/Grid.ts

@@ -1,7 +1,7 @@
 import Node from "../../../graph/node/Node.js";
 
 export default class Grid<TNode extends Node> {
-	private readonly _cells: Map<string, TNode["id"]> = new Map();
+	private readonly _cells: Map<string, TNode> = new Map();
 	private _rows: number = 0;
 	private _cols: number = 0;
 
@@ -9,16 +9,27 @@ export default class Grid<TNode extends Node> {
 		return `${row}:${col}`;
 	}
 
-	public set(row: number, col: number, nodeId: TNode["id"]): void {
-		this._cells.set(Grid.key(row, col), nodeId);
+	public set(row: number, col: number, node: TNode): void {
+		this._cells.set(Grid.key(row, col), node);
 		this._rows = Math.max(this._rows, row + 1);
 		this._cols = Math.max(this._cols, col + 1);
 	}
 
-	public get(row: number, col: number): TNode["id"] | null {
+	public get(row: number, col: number): TNode | null {
 		return this._cells.get(Grid.key(row, col)) ?? null;
 	}
 
+	public getCoordinates(nodeId: TNode): { row: number, col: number } | null {
+		for(const [key, id] of this._cells) {
+			if(id === nodeId) {
+				const [row, col] = key.split(":").map(Number);
+				return { row: row!, col: col! };
+			}
+		}
+
+		return null;
+	}
+
 	public get rows(): number { 
 		return this._rows; 
 	}

+ 23 - 45
src/v1/optimizer/siguiyama/CoordinateAssignmentStep.ts → src/v1/optimizer/steps/CoordinateAssignmentStep.ts

@@ -3,8 +3,8 @@ 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 "./grid/Grid.js";
-import { FeedbackSet, SiguiyamaContext } from "./SiguiyamaContext.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;
@@ -37,17 +37,15 @@ export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaCon
 		const edges = graph.getEdges();
 
 		for(const edge of edges) {
-			if(!feedbackSet.includes(edge.id))
+			if(!feedbackSet.includes(edge))
 				continue;
 
-			const from = edge.to, to = edge.from;
-			graph.updateEdge(edge.id, { ...edge, from, to });
+			const from = edge.getTo(), to = edge.getFrom();
+			edge.setFrom(to).setTo(from);
 		}
 	}
 
-	private buildGrid(
-		layering: Layering<Node, Edge<Node>>
-	): Grid<Node> {
+	private buildGrid(layering: Layering<Node, Edge<Node>>): Grid<Node> {
 		const grid = new Grid<Node>();
 		const layers = [...layering.getLayers()].reverse(); 
 
@@ -61,79 +59,59 @@ export default class CoordinateAssignmentStep extends AlgorithmStep<SiguiyamaCon
 		return grid;
 	}
 
-	private assignCoordinatesFromGrid(
-		graph: Graph<Node, Edge<Node>>,
-		grid: Grid<Node>
-	): void {
+	private assignCoordinatesFromGrid(graph: Graph<Node, Edge<Node>>, grid: Grid<Node>): void {
 		const PADDING = 60;
 		const H_GAP = 80;  
 		const V_GAP = 50; 
 
-		const colWidths = this.computeColWidths(graph, grid);
-		const rowHeights = this.computeRowHeights(graph, grid);
+		const colWidths = this.computeColWidths(grid);
+		const rowHeights = this.computeRowHeights(grid);
 
 		const colX = this.computeOffsets(colWidths, H_GAP, PADDING);
 		const rowY = this.computeOffsets(rowHeights, V_GAP, PADDING);
 
 		for(let col = 0; col < grid.cols; col++) {
 			for(let row = 0; row < grid.rows; row++) {
-				const nodeId = grid.get(row, col);
-				if(!nodeId) continue;
+				const node = grid.get(row, col);
+				if(!node)
+					continue;
 
-				const node = graph.getNode(nodeId);
-				if(!node) continue;
-
-				node.x = colX[col]! + (colWidths[col]! - node.width) / 2;
-				node.y = rowY[row]! + (rowHeights[row]! - node.height) / 2;
+				node.setX(colX[col]! + (colWidths[col]! - node.getWidth()) / 2);
+				node.setY(rowY[row]! + (rowHeights[row]! - node.getHeight()) / 2);
 			}
 		}
 	}
 
-	private computeColWidths(
-		graph: Graph<Node, Edge<Node>>,
-		grid: Grid<Node>
-	): number[] {
+	private computeColWidths(grid: Grid<Node>): number[] {
 		const widths: number[] = new Array(grid.cols).fill(0);
 
 		for(let col = 0; col < grid.cols; col++)
 			for(let row = 0; row < grid.rows; row++) {
-				const nodeId = grid.get(row, col);
-				if(!nodeId) continue;
+				const node = grid.get(row, col);
+				if(!node)
+					continue;
 
-				const node = graph.getNode(nodeId);
-				if(!node) continue;
-
-				widths[col] = Math.max(widths[col]!, node.width);
+				widths[col] = Math.max(widths[col]!, node.getWidth());
 			}
 
 		return widths;
 	}
 
-	private computeRowHeights(
-		graph: Graph<Node, Edge<Node>>,
-		grid: Grid<Node>
-	): number[] {
+	private computeRowHeights(grid: Grid<Node>): number[] {
 		const heights: number[] = new Array(grid.rows).fill(0);
 
 		for(let row = 0; row < grid.rows; row++)
 			for(let col = 0; col < grid.cols; col++) {
-				const nodeId = grid.get(row, col);
-				if(!nodeId) continue;
-
-				const node = graph.getNode(nodeId);
+				const node = grid.get(row, col);
 				if(!node) continue;
 
-				heights[row] = Math.max(heights[row]!, node.height);
+				heights[row] = Math.max(heights[row]!, node.getHeight());
 			}
 
 		return heights;
 	}
 
-	private computeOffsets(
-		sizes: number[],
-		gap: number,
-		padding: number
-	): number[] {
+	private computeOffsets(sizes: number[], gap: number, padding: number): number[] {
 		const offsets: number[] = [padding];
 
 		for(let i = 1; i < sizes.length; i++)

+ 12 - 14
src/v1/optimizer/siguiyama/CycleRemoveStep.ts → src/v1/optimizer/steps/CycleRemoveStep.ts

@@ -3,10 +3,10 @@ import Edge from "../../graph/edge/Edge.js";
 import Graph from "../../graph/Graph.js";
 import Node from "../../graph/node/Node.js";
 import AlgorithmStep from "../AlgorithmStep.js";
-import { SiguiyamaContext } from "./SiguiyamaContext.js";
+import {SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
 
 /**
- * Greedy Cycle Removal Algoirthm
+ * Greedy Cycle Removal Algorithm
  */
 export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
 	public constructor() {
@@ -18,9 +18,7 @@ export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
 		if(!graph)
 			throw new CycleRemoveStepError("Graph not found!");
 
-		const feedbackSet = this.removeCycles(graph);
-
-		context.feedbackSet = feedbackSet;
+		context.feedbackSet = this.removeCycles(graph);
 	}
 
 	protected removeCycles(graph: Graph<Node, Edge<Node>>) : NonNullable<SiguiyamaContext["feedbackSet"]> {
@@ -30,13 +28,13 @@ export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
 		const feedbackSet: NonNullable<SiguiyamaContext["feedbackSet"]> = [];
 
 		for(const node of nodes) {
-			const isSource = graph.isSourceNode(node.id);
+			const isSource = graph.isSourceNode(node);
 			if(isSource) {
 				l.push(node);
 				continue;
 			}
 
-			const isSink = graph.isSinkNode(node.id);
+			const isSink = graph.isSinkNode(node);
 			if(isSink) {
 				r.unshift(node);
 				continue;
@@ -56,13 +54,13 @@ export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
 		const sortedNodes = [...l, ...r.reverse()];
 
 		for(const edge of edges) {
-			const { from, to } = edge;
-			const fromIndex = sortedNodes.findIndex((node) => node.id === from);
-			const toIndex = sortedNodes.findIndex((node) => node.id === to);
+			const from = edge.getFrom(), to = edge.getTo();
+			const fromIndex = sortedNodes.findIndex((node) => node.getId() === from.getId());
+			const toIndex = sortedNodes.findIndex((node) => node.getId() === to.getId());
 
 			if(fromIndex > toIndex) {
-				graph.updateEdge(edge.id, { ...edge, from: edge.to, to: edge.from });
-				feedbackSet.push(edge.id);
+				edge.setFrom(to).setTo(from);
+				feedbackSet.push(edge);
 			}
 		}
 
@@ -70,8 +68,8 @@ export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
 	}
 
 	private calculateScore(graph: Graph<Node, Edge<Node>>, node: Node) : number {
-		const outDegree = graph.getNodeOutputs(node.id).length;
-		const inDegree = graph.getNodeInputs(node.id).length;
+		const outDegree = graph.getNodeOutputs(node).length;
+		const inDegree = graph.getNodeInputs(node).length;
 
 		return outDegree - inDegree;
 	}

+ 16 - 0
src/v1/optimizer/steps/EdgeRoutingStep.ts

@@ -0,0 +1,16 @@
+import EdgeRoutingStepError from "../../errors/optimizer/EdgeRoutingStepError.js";
+import AlgorithmStep from "../AlgorithmStep.js";
+import {SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
+
+export default class EdgeRoutingStep extends AlgorithmStep<SiguiyamaContext> {
+	public run(context: SiguiyamaContext): void {
+		const { graph, layering, grid } = context;
+
+		if(!graph)
+			throw new EdgeRoutingStepError("Source graph was not found!");
+		if(!layering)
+			throw new EdgeRoutingStepError("Layering of graph was not found!");
+		if(!grid)
+			throw new EdgeRoutingStepError("Grid was not found!");
+	}
+}

+ 19 - 17
src/v1/optimizer/siguiyama/LayerAssignmentStep.ts → src/v1/optimizer/steps/LayerAssignmentStep.ts

@@ -5,7 +5,7 @@ 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 { FeedbackSet, SiguiyamaContext } from "./SiguiyamaContext.js";
+import { FeedbackSet, SiguiyamaContext } from "../siguiyama/SiguiyamaContext.js";
 
 /**
  * Network simplex algorithm
@@ -33,8 +33,8 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 	private longestPathAlgorithm(dag: Graph<Node, Edge<Node>>) : NonNullable<SiguiyamaContext["layering"]> {
 		const nodes = dag.getNodes();
 
-		const alreadyLayeredNodeIds = new Set<Node["id"]>();
-		const underCurrentLayerNodeIds = new Set<Node["id"]>();
+		const alreadyLayeredNodeIds = new Set<Node>();
+		const underCurrentLayerNodeIds = new Set<Node>();
 		const layering = new Layering<Node, Edge<Node>>();
 
 		let currentLayerIndex = 0;
@@ -43,17 +43,17 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 			let isNodeSelected = false;
 
 			for(const node of nodes) {
-				if(alreadyLayeredNodeIds.has(node.id))
+				if(alreadyLayeredNodeIds.has(node))
 					continue;
 
-				const successors = dag.getNodeOutputs(node.id);
-				const isAllSuccessorsUnder = successors.every((node) => underCurrentLayerNodeIds.has(node.id));
+				const successors = dag.getNodeOutputs(node);
+				const isAllSuccessorsUnder = successors.every((node) => underCurrentLayerNodeIds.has(node));
 
 				if(!isAllSuccessorsUnder)
 					continue;
 
 				layering.assign(node, currentLayerIndex);
-				alreadyLayeredNodeIds.add(node.id);
+				alreadyLayeredNodeIds.add(node);
 				isNodeSelected = true;
 			}
 
@@ -84,38 +84,40 @@ export default class LayerAssignmentStep extends AlgorithmStep<SiguiyamaContext>
 		if(span <= 1)
 			return;
 
-		const edgeReversedIndex = feedbackSet.findIndex((e) => e == edge.id);
+		const edgeId = edge.getId(), edgeFrom = edge.getFrom(), edgeTo = edge.getTo();
+
+		const edgeReversedIndex = feedbackSet.findIndex((e) => e.getId() == edgeId);
 		const isReversed = edgeReversedIndex !== -1;
 
 		if(isReversed)
 			feedbackSet.splice(edgeReversedIndex, 1);
 
-		graph.removeEdge(edge.id);
+		graph.removeEdge(edge.getId());
 
-		const fromLayer = layering.getLayerOf(edge.from)!;
-		let previousNodeId = edge.from;
+		const fromLayer = layering.getLayerOf(edgeFrom)!;
+		let previousNode = edgeFrom;
 
 		for(let i = 1; i < span; i++) {
-			const dummyId = `dummy-${edge.id}-${i}`;
+			const dummyId = `dummy-${edgeId}-${i}`;
 			const node = new DummyNode(0, 0, dummyId);
 			const dummyLayerIndex = fromLayer - i;
 
 			graph.addNode(node);
 			layering.assign(node, dummyLayerIndex);
 
-			const newEdge = new Edge(previousNodeId, node.id, `${edge.id}_segment_${i}`);
+			const newEdge = new Edge(previousNode, node, [], `${edgeId}_segment_${i}`);
 
 			graph.addEdge(newEdge);
 
 			if(isReversed)
-				feedbackSet.push(newEdge.id);
+				feedbackSet.push(newEdge);
 
-			previousNodeId = node.id;
+			previousNode = node;
 		}
 
-		const lastEdge = new Edge(previousNodeId, edge.to, `${edge.id}_segment_${span}`);
+		const lastEdge = new Edge(previousNode, edgeTo, [], `${edgeId}_segment_${span}`);
 		graph.addEdge(lastEdge);
 		if(isReversed)
-			feedbackSet.push(lastEdge.id);
+			feedbackSet.push(lastEdge);
 	}
 }

+ 21 - 25
src/v1/optimizer/siguiyama/NodeOrderingStep.ts → src/v1/optimizer/steps/NodeOrderingStep.ts

@@ -4,12 +4,14 @@ 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 { SiguiyamaContext } from "./SiguiyamaContext.js";
+import {SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
 
 export default class NodeOrderingStep extends AlgorithmStep<SiguiyamaContext> {
+	private static readonly DefaultIterationsCount = 24;
+
 	private readonly _iterationsCount: number;
 
-	public constructor(iterationsCount: number = 24) {
+	public constructor(iterationsCount: number = NodeOrderingStep.DefaultIterationsCount) {
 		super(NodeOrderingStep.name);
 		this._iterationsCount = iterationsCount;
 	}
@@ -46,8 +48,7 @@ export default class NodeOrderingStep extends AlgorithmStep<SiguiyamaContext> {
 
 		for(let i = 0; i < layers.length; i++) {
 			const currLayer = layers[i]!;
-			const newNodesOrder = this.applyBarycenter(graph, layering, currLayer.nodes, "down");
-			currLayer.nodes = newNodesOrder;
+			currLayer.nodes = this.applyBarycenter(graph, layering, currLayer.nodes, "down");
 		}
 	}
 
@@ -56,32 +57,27 @@ export default class NodeOrderingStep extends AlgorithmStep<SiguiyamaContext> {
 
 		for(let i = layers.length - 1; i > 0; i--) {
 			const currLayer = layers[i]!;
-			const newNodesOrder = this.applyBarycenter(graph, layering, currLayer.nodes, "up");
-			currLayer.nodes = newNodesOrder;
+			currLayer.nodes = this.applyBarycenter(graph, layering, currLayer.nodes, "up");
 		}
 	}
 
-	private applyBarycenter(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, nodeIds: Node["id"][], direction: "up" | "down") : Node["id"][] {
-		const barycenterValues = nodeIds.map((nodeId) => {
-			const node = graph.getNode(nodeId);
-			if(!node)
-				return { nodeId, value: Infinity };
-
-			const neighbors = direction === "down" ? graph.getNodeInputs(nodeId) : graph.getNodeOutputs(nodeId);
+	private applyBarycenter(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, nodeIds: Node[], direction: "up" | "down") : Node[] {
+		const barycenterValues = nodeIds.map((node) => {
+			const neighbors = direction === "down" ? graph.getNodeInputs(node) : graph.getNodeOutputs(node);
 			if(neighbors.length === 0) {
-				const currentPosition = layering.getPositionOf(nodeId);
-				return { nodeId, value: currentPosition ?? Infinity };
+				const currentPosition = layering.getPositionOf(node);
+				return { node, value: currentPosition ?? Infinity };
 			}
 
 			const posistionSum = neighbors.reduce((sum, neighbor) => {
-				const position = layering.getPositionOf(neighbor.id);
+				const position = layering.getPositionOf(neighbor);
 				return sum + (position ?? 0);
 			}, 0);
 
-			return { nodeId, value: posistionSum / neighbors.length };
+			return { node, value: posistionSum / neighbors.length };
 		});
 
-		return barycenterValues.sort((a, b) => a.value - b.value).map((entry) => entry.nodeId);
+		return barycenterValues.sort((a, b) => a.value - b.value).map((entry) => entry.node);
 	}
 
 	private countTotalCrossings(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>) : number {
@@ -96,9 +92,9 @@ export default class NodeOrderingStep extends AlgorithmStep<SiguiyamaContext> {
 	}
 
 	private countCrossingsBetweenLayers(graph: Graph<Node, Edge<Node>>, layering: Layering<Node, Edge<Node>>, upperLayerIndex: number, lowerLayerIndex: number) : number {
-		const edges = graph.getEdges().filter(({ from, to }) => {
-			const fromLayer = layering.getLayerOf(from);
-			const toLayer = layering.getLayerOf(to);
+		const edges = graph.getEdges().filter((edge) => {
+			const fromLayer = layering.getLayerOf(edge.getFrom());
+			const toLayer = layering.getLayerOf(edge.getTo());
 			return fromLayer === upperLayerIndex && toLayer === lowerLayerIndex;
 		});
 		
@@ -106,10 +102,10 @@ export default class NodeOrderingStep extends AlgorithmStep<SiguiyamaContext> {
 
 		for(let i = 0; i < edges.length; i++) {
 			for(let j = i + 1; j < edges.length; j++) {
-				const posU = layering.getPositionOf(edges[i]!.from) ?? 0;
-				const posV = layering.getPositionOf(edges[i]!.to) ?? 0;
-				const posS = layering.getPositionOf(edges[j]!.from) ?? 0;
-				const posT = layering.getPositionOf(edges[j]!.to) ?? 0;
+				const posU = layering.getPositionOf(edges[i]!.getFrom()) ?? 0;
+				const posV = layering.getPositionOf(edges[i]!.getTo()) ?? 0;
+				const posS = layering.getPositionOf(edges[j]!.getFrom()) ?? 0;
+				const posT = layering.getPositionOf(edges[j]!.getTo()) ?? 0;
 
 				if((posU < posS && posV > posT) || (posU > posS && posV < posT))
 					crossings++;