import * as React from 'react'; import { DiagramEngine } from '@projectstorm/react-diagrams-core'; import { PathFindingLinkModel } from './PathFindingLinkModel'; import { PathFindingLinkWidget } from './PathFindingLinkWidget'; import * as _ from 'lodash'; import * as Path from 'paths-js/path'; import { DefaultLinkFactory } from '@projectstorm/react-diagrams-defaults'; import { AbstractDisplacementState, AbstractFactory, Action, FactoryBank, InputType, ListenerHandle } from '@projectstorm/react-canvas-core'; export class PathFindingLinkFactory extends DefaultLinkFactory { ROUTING_SCALING_FACTOR: number = 5; // calculated only when smart routing is active canvasMatrix: number[][] = []; routingMatrix: number[][] = []; // used when at least one element has negative coordinates hAdjustmentFactor: number = 0; vAdjustmentFactor: number = 0; static NAME = 'pathfinding'; listener: ListenerHandle; constructor() { super(PathFindingLinkFactory.NAME); } setDiagramEngine(engine: DiagramEngine): void { super.setDiagramEngine(engine); // listen for drag changes engine.getStateMachine().registerListener({ stateChanged: (event) => { if (event.newState instanceof AbstractDisplacementState) { const deRegister = engine.getActionEventBus().registerAction( new Action({ type: InputType.MOUSE_UP, fire: () => { this.calculateRoutingMatrix(); engine.repaintCanvas(); deRegister(); } }) ); } } }); this.listener = engine.registerListener({ canvasReady: () => { _.defer(() => { this.calculateRoutingMatrix(); engine.repaintCanvas(); }); } }); } setFactoryBank(bank: FactoryBank): void { super.setFactoryBank(bank); if (!bank && this.listener) { this.listener.deregister(); } } generateReactWidget(event): JSX.Element { return ; } generateModel(event): PathFindingLinkModel { return new PathFindingLinkModel(); } /** * A representation of the canvas in the following format: * * +-----------------+ * | 0 0 0 0 0 0 0 0 | * | 0 0 0 0 0 0 0 0 | * | 0 0 0 0 0 0 0 0 | * | 0 0 0 0 0 0 0 0 | * | 0 0 0 0 0 0 0 0 | * +-----------------+ * * In which all walkable points are marked by zeros. * It uses @link{#ROUTING_SCALING_FACTOR} to reduce the matrix dimensions and improve performance. */ getCanvasMatrix(): number[][] { if (this.canvasMatrix.length === 0) { this.calculateCanvasMatrix(); } return this.canvasMatrix; } calculateCanvasMatrix() { const { width: canvasWidth, hAdjustmentFactor, height: canvasHeight, vAdjustmentFactor } = this.calculateMatrixDimensions(); this.hAdjustmentFactor = hAdjustmentFactor; this.vAdjustmentFactor = vAdjustmentFactor; const matrixWidth = Math.ceil(canvasWidth / this.ROUTING_SCALING_FACTOR); const matrixHeight = Math.ceil(canvasHeight / this.ROUTING_SCALING_FACTOR); this.canvasMatrix = _.range(0, matrixHeight).map(() => { return new Array(matrixWidth).fill(0); }); } /** * A representation of the canvas in the following format: * * +-----------------+ * | 0 0 1 1 0 0 0 0 | * | 0 0 1 1 0 0 1 1 | * | 0 0 0 0 0 0 1 1 | * | 1 1 0 0 0 0 0 0 | * | 1 1 0 0 0 0 0 0 | * +-----------------+ * * In which all points blocked by a node (and its ports) are * marked as 1; points were there is nothing (ie, free) receive 0. */ getRoutingMatrix(): number[][] { if (this.routingMatrix.length === 0) { this.calculateRoutingMatrix(); } return this.routingMatrix; } calculateRoutingMatrix(): void { const matrix = _.cloneDeep(this.getCanvasMatrix()); // nodes need to be marked as blocked points this.markNodes(matrix); // same thing for ports this.markPorts(matrix); this.routingMatrix = matrix; } /** * The routing matrix does not have negative indexes, but elements could be negatively positioned. * We use the functions below to translate back and forth between these coordinates, relying on the * calculated values of hAdjustmentFactor and vAdjustmentFactor. */ translateRoutingX(x: number, reverse: boolean = false) { return x + this.hAdjustmentFactor * (reverse ? -1 : 1); } translateRoutingY(y: number, reverse: boolean = false) { return y + this.vAdjustmentFactor * (reverse ? -1 : 1); } /** * Despite being a long method, we simply iterate over all three collections (nodes, ports and points) * to find the highest X and Y dimensions, so we can build the matrix large enough to contain all elements. */ calculateMatrixDimensions = (): { width: number; hAdjustmentFactor: number; height: number; vAdjustmentFactor: number; } => { const allNodesCoords = _.values(this.engine.getModel().getNodes()).map((item) => ({ x: item.getX(), width: item.width, y: item.getY(), height: item.height })); const allLinks = _.values(this.engine.getModel().getLinks()); const allPortsCoords = _.flatMap(allLinks.map((link) => [link.getSourcePort(), link.getTargetPort()])) .filter((port) => port !== null) .map((item) => ({ x: item.getX(), width: item.width, y: item.getY(), height: item.height })); const allPointsCoords = _.flatMap(allLinks.map((link) => link.getPoints())).map((item) => ({ // points don't have width/height, so let's just use 0 x: item.getX(), width: 0, y: item.getY(), height: 0 })); const sumProps = (object, props) => _.reduce(props, (acc, prop) => acc + _.get(object, prop, 0), 0); const canvas = this.engine.getCanvas() as HTMLDivElement; const concatedCoords = _.concat(allNodesCoords, allPortsCoords, allPointsCoords); const minX = Math.floor(Math.min(_.get(_.minBy(concatedCoords, 'x'), 'x', 0), 0) / this.ROUTING_SCALING_FACTOR) * this.ROUTING_SCALING_FACTOR; const maxXElement = _.maxBy(concatedCoords, (item) => sumProps(item, ['x', 'width'])); const maxX = Math.max(sumProps(maxXElement, ['x', 'width']), canvas.offsetWidth); const minYCoords = _.minBy(concatedCoords, 'y'); const minY = Math.floor(Math.min(_.get(minYCoords, 'y', 0), 0) / this.ROUTING_SCALING_FACTOR) * this.ROUTING_SCALING_FACTOR; const maxYElement = _.maxBy(concatedCoords, (item) => sumProps(item, ['y', 'height'])); const maxY = Math.max(sumProps(maxYElement, ['y', 'height']), canvas.offsetHeight); return { width: Math.ceil(Math.abs(minX) + maxX), hAdjustmentFactor: Math.abs(minX) / this.ROUTING_SCALING_FACTOR + 1, height: Math.ceil(Math.abs(minY) + maxY), vAdjustmentFactor: Math.abs(minY) / this.ROUTING_SCALING_FACTOR + 1 }; }; /** * Updates (by reference) where nodes will be drawn on the matrix passed in. */ markNodes = (matrix: number[][]): void => { _.values(this.engine.getModel().getNodes()).forEach((node) => { const startX = Math.floor(node.getX() / this.ROUTING_SCALING_FACTOR); const endX = Math.ceil((node.getX() + node.width) / this.ROUTING_SCALING_FACTOR); const startY = Math.floor(node.getY() / this.ROUTING_SCALING_FACTOR); const endY = Math.ceil((node.getY() + node.height) / this.ROUTING_SCALING_FACTOR); for (let x = startX - 1; x <= endX + 1; x++) { for (let y = startY - 1; y < endY + 1; y++) { this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y)); } } }); }; /** * Updates (by reference) where ports will be drawn on the matrix passed in. */ markPorts = (matrix: number[][]): void => { const allElements = _.flatMap( _.values(this.engine.getModel().getLinks()).map((link) => [].concat(link.getSourcePort(), link.getTargetPort())) ); allElements .filter((port) => port !== null) .forEach((port) => { const startX = Math.floor(port.x / this.ROUTING_SCALING_FACTOR); const endX = Math.ceil((port.x + port.width) / this.ROUTING_SCALING_FACTOR); const startY = Math.floor(port.y / this.ROUTING_SCALING_FACTOR); const endY = Math.ceil((port.y + port.height) / this.ROUTING_SCALING_FACTOR); for (let x = startX - 1; x <= endX + 1; x++) { for (let y = startY - 1; y < endY + 1; y++) { this.markMatrixPoint(matrix, this.translateRoutingX(x), this.translateRoutingY(y)); } } }); }; markMatrixPoint = (matrix: number[][], x: number, y: number) => { if (matrix[y] !== undefined && matrix[y][x] !== undefined) { matrix[y][x] = 1; } }; generateDynamicPath(pathCoords: number[][]) { let path = Path(); path = path.moveto(pathCoords[0][0] * this.ROUTING_SCALING_FACTOR, pathCoords[0][1] * this.ROUTING_SCALING_FACTOR); pathCoords.slice(1).forEach((coords) => { path = path.lineto(coords[0] * this.ROUTING_SCALING_FACTOR, coords[1] * this.ROUTING_SCALING_FACTOR); }); return path.print(); } }