@projectstorm/react-diagrams-routing
Version:
This package adds dagre integration for laying out nodes and links
206 lines • 9.44 kB
JavaScript
import { PointModel } from '@projectstorm/react-diagrams-core';
import * as dagre from 'dagre';
import _every from 'lodash/every';
import _findIndex from 'lodash/findIndex';
import _forEach from 'lodash/forEach';
import _map from 'lodash/map';
import _range from 'lodash/range';
import _sortBy from 'lodash/sortBy';
import { Point } from '@projectstorm/geometry';
export class DagreEngine {
constructor(options = {}) {
this.options = options;
}
redistribute(model) {
// Create a new directed graph
var g = new dagre.graphlib.Graph({
multigraph: true,
compound: true
});
g.setGraph(this.options.graph || {});
g.setDefaultEdgeLabel(function () {
return {};
});
// set nodes
_forEach(model.getNodes(), (node) => {
g.setNode(node.getID(), { width: node.width, height: node.height });
});
_forEach(model.getLinks(), (link) => {
// set edges
if (link.getSourcePort() && link.getTargetPort()) {
g.setEdge({
v: link.getSourcePort().getNode().getID(),
w: link.getTargetPort().getNode().getID(),
name: link.getID()
});
}
});
// layout the graph
dagre.layout(g);
g.nodes().forEach((v) => {
const node = g.node(v);
model.getNode(v).setPosition(node.x - node.width / 2, node.y - node.height / 2);
});
// also include links?
if (this.options.includeLinks) {
g.edges().forEach((e) => {
const edge = g.edge(e);
const link = model.getLink(e.name);
const points = [link.getFirstPoint()];
for (let i = 1; i < edge.points.length - 1; i++) {
points.push(new PointModel({ link: link, position: new Point(edge.points[i].x, edge.points[i].y) }));
}
link.setPoints(points.concat(link.getLastPoint()));
});
}
}
/**
* TODO cleanup this method into smaller methods
*/
refreshLinks(diagram) {
const { nodeMargin } = this.options;
const nodes = diagram.getNodes();
const links = diagram.getLinks();
let maxChunkRowIndex = -1;
// build the chunk matrix
const chunks = {}; // true: occupied, false: blank
const NodeXColumnIndexDictionary = {};
let verticalLines = [];
_forEach(nodes, (node) => {
// find vertical lines. vertical lines go through maximum number of nodes located under each other.
const nodeColumnCenter = node.getX() + node.width / 2;
if (_every(verticalLines, (vLine) => {
return Math.abs(nodeColumnCenter - vLine) > nodeMargin;
})) {
verticalLines.push(nodeColumnCenter);
}
});
// sort chunk columns
verticalLines = verticalLines.sort((a, b) => a - b);
_forEach(verticalLines, (line, index) => {
chunks[index] = {};
chunks[index + 0.5] = {};
});
// set occupied chunks
_forEach(nodes, (node) => {
const nodeColumnCenter = node.getX() + node.width / 2;
const startChunkIndex = Math.floor(node.getY() / nodeMargin);
const endChunkIndex = Math.floor((node.getY() + node.height) / nodeMargin);
// find max ChunkRowIndex
if (endChunkIndex > maxChunkRowIndex)
maxChunkRowIndex = endChunkIndex;
const nodeColumnIndex = _findIndex(verticalLines, (vLine) => {
return Math.abs(nodeColumnCenter - vLine) <= nodeMargin;
});
_forEach(_range(startChunkIndex, endChunkIndex + 1), (chunkIndex) => {
chunks[nodeColumnIndex][chunkIndex] = true;
});
NodeXColumnIndexDictionary[node.getX()] = nodeColumnIndex;
});
// sort links based on their distances
const edges = _map(links, (link) => {
if (link.getSourcePort() && link.getTargetPort()) {
const source = link.getSourcePort().getNode();
const target = link.getTargetPort().getNode();
const sourceIndex = NodeXColumnIndexDictionary[source.getX()];
const targetIndex = NodeXColumnIndexDictionary[target.getX()];
return sourceIndex > targetIndex
? {
link,
sourceIndex,
sourceY: source.getY() + source.height / 2,
source,
targetIndex,
targetY: target.getY() + source.height / 2,
target
}
: {
link,
sourceIndex: targetIndex,
sourceY: target.getY() + target.height / 2,
source: target,
targetIndex: sourceIndex,
targetY: source.getY() + source.height / 2,
target: source
};
}
});
const sortedEdges = _sortBy(edges, (link) => {
return Math.abs(link.targetIndex - link.sourceIndex);
});
// set link points
if (this.options.includeLinks) {
_forEach(sortedEdges, (edge) => {
const link = diagram.getLink(edge.link.getID());
// re-draw
if (Math.abs(edge.sourceIndex - edge.targetIndex) > 1) {
// get the length of link in column
const columns = _range(edge.sourceIndex - 1, edge.targetIndex);
const chunkIndex = Math.floor(edge.sourceY / nodeMargin);
const targetChunkIndex = Math.floor(edge.targetY / nodeMargin);
// check upper paths
let northCost = 1;
let aboveRowIndex = chunkIndex;
for (; aboveRowIndex >= 0; aboveRowIndex--, northCost++) {
if (_every(columns, (columnIndex) => {
return !(chunks[columnIndex][aboveRowIndex] ||
chunks[columnIndex + 0.5][aboveRowIndex] ||
chunks[columnIndex - 0.5][aboveRowIndex]);
})) {
break;
}
}
// check lower paths
let southCost = 0;
let belowRowIndex = chunkIndex;
for (; belowRowIndex <= maxChunkRowIndex; belowRowIndex++, southCost++) {
if (_every(columns, (columnIndex) => {
return !(chunks[columnIndex][belowRowIndex] ||
chunks[columnIndex + 0.5][belowRowIndex] ||
chunks[columnIndex - 0.5][belowRowIndex]);
})) {
break;
}
}
// pick the cheapest path
const pathRowIndex = southCost + (belowRowIndex - targetChunkIndex) < northCost + (targetChunkIndex - aboveRowIndex)
? belowRowIndex + 1
: aboveRowIndex - 1;
// Finally update the link points
const points = [link.getFirstPoint()];
points.push(new PointModel({
link: link,
position: new Point((verticalLines[columns[0]] + verticalLines[columns[0] + 1]) / 2, (pathRowIndex + 0.5) * nodeMargin)
}));
_forEach(columns, (column) => {
points.push(new PointModel({
link: link,
position: new Point(verticalLines[column], (pathRowIndex + 0.5) * nodeMargin)
}));
points.push(new PointModel({
link: link,
position: new Point((verticalLines[column] + verticalLines[column - 1]) / 2, (pathRowIndex + 0.5) * nodeMargin)
}));
chunks[column][pathRowIndex] = true;
chunks[column][pathRowIndex + 1] = true;
chunks[column + 0.5][pathRowIndex] = true;
chunks[column + 0.5][pathRowIndex + 1] = true;
});
link.setPoints(points.concat(link.getLastPoint()));
}
else {
// refresh
link.setPoints([link.getFirstPoint(), link.getLastPoint()]);
const columnIndex = (edge.sourceIndex + edge.targetIndex) / 2;
if (!chunks[columnIndex]) {
chunks[columnIndex] = {};
}
const rowIndex = Math.floor((edge.sourceY + edge.targetY) / 2 / nodeMargin);
chunks[columnIndex][rowIndex] = true;
chunks[columnIndex][rowIndex + 1] = true;
}
});
}
}
}
//# sourceMappingURL=DagreEngine.js.map