devexpress-diagram
Version:
DevExpress Diagram Control
581 lines (561 loc) • 30.8 kB
text/typescript
import { LayoutBuilder } from "./BaseBuilder";
import { Graph, FastGraph, IEdge } from "../Graph";
import { NodeLayout, EdgeLayout, NodeInfo } from "../NodeLayout";
import { ConnectionMode, Edge } from "../Structures";
import { ItemKey } from "../../Model/DiagramItem";
import { HashSet, IHashCodeOwner, KeySet, KeyNumberMap } from "../../ListUtils";
import { SearchUtils } from "@devexpress/utils/lib/utils/search";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { LogicalDirectionKind, DataLayoutOrientation, LayoutSettings } from "../LayoutSettings";
import { GraphLayout } from "../GraphLayout";
import { ConnectorPosition } from "../../Model/Connectors/Connector";
import { IKeyOwner } from "../../Interfaces";
import { CycleRemover } from "../Utility/CycleRemover";
type OrderInfo = {[layer: number]: NodeOnLayer[]};
type Medians = { [nodeKey: string]: EdgeOnLayer };
type AbsoluteOffsetInfo = {[coord: number]: {leftOffset: number, width: number}};
export class SugiyamaLayoutBuilder extends LayoutBuilder<LayoutSettings> {
build(): GraphLayout {
let offset = 0;
const layout = new GraphLayout();
const nodeOrderer = new SugiyamaNodesOrderer();
this.graph.getConnectedComponents()
.forEach(component => {
const acyclicGraphInfo = CycleRemover.removeCycles(component);
const layers = SugiyamaLayerDistributor.getLayers(acyclicGraphInfo.graph);
const orderedGraph = nodeOrderer.orderNodes(acyclicGraphInfo.graph, layers);
const removedEdges = Object.keys(acyclicGraphInfo.removedEdges).map(ek => component.getEdge(ek));
const coordinatedGraph = nodeOrderer.assignAbsCoordinates(orderedGraph);
const componentLayout = this.createInfoGraphLayout(coordinatedGraph, acyclicGraphInfo.reversedEdges, removedEdges);
layout.extend(this.setComponentOffset(componentLayout, offset));
offset += this.getComponentOffset(componentLayout);
});
return layout;
}
createInfoGraphLayout(coordinatedGraph: FastGraph<NodeOnLayer, EdgeOnLayer>, reversedEdges: KeySet, removedEdges: Edge[]): GraphLayout {
let currentPosition: Point = new Point(0, 0);
const items = coordinatedGraph.items;
const sortedLayers = new HashSet(items.map(n => n.layer).sort((a, b) => a - b));
const absOffsetInfo = this.getAbsOffsetInfo(coordinatedGraph.items);
const positions: {[nodeKey: string]: Point} = {};
let totalDepth = 0;
let leftEdge = Number.MAX_SAFE_INTEGER || Number.MAX_VALUE;
let rightEdge = Number.MIN_SAFE_INTEGER || Number.MAX_VALUE;
for(let i = 0; i < sortedLayers.length; i++) {
const layer = sortedLayers.item(i);
let maxDepthLayer = 0;
items
.filter(n => n.layer === layer)
.sort((a, b) => a.position - b.position)
.forEach(n => {
const depthNodeSize = this.getDepthNodeSize(n);
const directionOffset = this.chooseDirectionValue(0, depthNodeSize);
const absPosition = this.getAbsPosition(n.position, this.getBreadthNodeSize(n), absOffsetInfo);
currentPosition = this.setBreadth(currentPosition, absPosition);
const nodePosition = this.setDepthOffset(currentPosition, -directionOffset);
positions[n.key] = nodePosition;
if(n.isDummy) return;
const breadth = this.settings.orientation === DataLayoutOrientation.Horizontal ? nodePosition.y : nodePosition.x;
leftEdge = Math.min(leftEdge, breadth);
rightEdge = Math.max(rightEdge, breadth + this.getBreadthNodeSize(n));
maxDepthLayer = Math.max(maxDepthLayer, this.getDepthNodeSize(n));
});
totalDepth += maxDepthLayer;
currentPosition = this.setBreadth(currentPosition, 0);
currentPosition = this.setDepthOffset(currentPosition, this.getDirectionValue(maxDepthLayer + this.settings.layerSpacing));
}
totalDepth += (sortedLayers.length - 1) * this.settings.layerSpacing;
const layout = new GraphLayout();
this.createNodesLayout(coordinatedGraph, layout, leftEdge, totalDepth, positions);
this.createEdgesLayout(coordinatedGraph, layout, reversedEdges, removedEdges);
return layout;
}
createNodesLayout(infoGraph: FastGraph<NodeOnLayer, EdgeOnLayer>, layout: GraphLayout, leftEdge: number, totalDepth: number, positions: {[nodeKey: string]: Point}): void {
const offset = this.settings.orientation === DataLayoutOrientation.Vertical ?
new Point(-leftEdge, this.chooseDirectionValue(0, totalDepth)) :
new Point(this.chooseDirectionValue(0, totalDepth), -leftEdge);
infoGraph.items.forEach(n => {
if(!n.isDummy) {
const node = this.graph.getNode(n.key);
layout.addNode(new NodeLayout(node, positions[n.key].clone().offset(offset.x, offset.y)));
}
});
}
createEdgesLayout(infoGraph: FastGraph<NodeOnLayer, EdgeOnLayer>, layout: GraphLayout, reversedEdges: KeySet, removedEdges: Edge[]): void {
const DIRECT = this.getDirectEdgeLayout();
const TOP_TO_BOTTOM = this.getDiffLevelEdgeLayout(true);
const BOTTOM_TO_TOP = this.getDiffLevelEdgeLayout(false);
const TOP_TO_TOP = this.getSameLevelEdgeLayout(true);
const BOTTOM_TO_BOTTOM = this.getSameLevelEdgeLayout(false);
const occupied: {[key_point: string]: ConnectorPosition} = {};
infoGraph.edges
.filter(e => !e.isDummy)
.concat(removedEdges.map(e => new EdgeOnLayer(e.key, false, e.from, e.to)))
.sort((a, b) => {
return (infoGraph.getNode(a.originFrom).layer - infoGraph.getNode(b.originFrom).layer) ||
(infoGraph.getNode(a.to).layer - infoGraph.getNode(b.to).layer);
})
.forEach(e => {
const isReversed = reversedEdges[e.key];
const from = infoGraph.getNode(isReversed ? e.to : e.originFrom);
const to = infoGraph.getNode(isReversed ? e.originFrom : e.to);
if(to.layer - from.layer === 1)
layout.addEdge(new EdgeLayout(e.key, DIRECT.from, DIRECT.to));
else {
const candidates: {from: number, to: number}[] = [];
if(to.position - from.position >= 1) {
candidates.push(TOP_TO_BOTTOM);
candidates.push({ from: DIRECT.from, to: TOP_TO_BOTTOM.to });
candidates.push({ from: TOP_TO_BOTTOM.from, to: DIRECT.to });
}
else if(to.position - from.position <= -1) {
candidates.push(BOTTOM_TO_TOP);
candidates.push({ from: DIRECT.from, to: BOTTOM_TO_TOP.to });
candidates.push({ from: BOTTOM_TO_TOP.from, to: DIRECT.to });
}
else {
const oneliner = from.position === to.position && to.position === 0 ? [TOP_TO_TOP, BOTTOM_TO_BOTTOM] : [BOTTOM_TO_BOTTOM, TOP_TO_TOP];
oneliner.forEach(c => candidates.push(c));
oneliner.forEach(c => {
candidates.push({ from: c.from, to: DIRECT.to });
candidates.push({ from: DIRECT.from, to: c.to });
});
}
candidates.push(DIRECT);
for(let i = 0, candidate: {from: number, to: number}; candidate = candidates[i]; i++) {
const fromKey = from.key + "_" + candidate.from;
const toKey = to.key + "_" + candidate.to;
if(occupied[fromKey] !== ConnectorPosition.End && occupied[toKey] !== ConnectorPosition.Begin) {
layout.addEdge(new EdgeLayout(e.key, candidate.from, candidate.to));
occupied[fromKey] = ConnectorPosition.Begin;
occupied[toKey] = ConnectorPosition.End;
break;
}
}
}
});
}
private getDirectEdgeLayout(): { from: number, to: number } {
if(this.settings.orientation === DataLayoutOrientation.Horizontal)
return this.settings.direction === LogicalDirectionKind.Forward ? { from: 1, to: 3 } : { from: 3, to: 1 };
return this.settings.direction === LogicalDirectionKind.Forward ? { from: 2, to: 0 } : { from: 0, to: 2 };
}
private getDiffLevelEdgeLayout(topToBottom: boolean): { from: number, to: number } {
if(this.settings.orientation === DataLayoutOrientation.Horizontal)
return topToBottom ? { from: 2, to: 0 } : { from: 0, to: 2 };
return topToBottom ? { from: 3, to: 1 } : { from: 1, to: 3 };
}
private getSameLevelEdgeLayout(topToBottom: boolean): { from: number, to: number } {
if(this.settings.orientation === DataLayoutOrientation.Horizontal)
return topToBottom ? { from: 0, to: 0 } : { from: 2, to: 2 };
return topToBottom ? { from: 3, to: 3 } : { from: 1, to: 1 };
}
getAbsOffsetInfo(nodesInfos: NodeOnLayer[]): AbsoluteOffsetInfo {
const absOffsetMatrix: {[coord: number]: number} = {};
const addCell = (n: NodeOnLayer, intAbsCoord: number) => {
if(absOffsetMatrix[intAbsCoord] === undefined)
absOffsetMatrix[intAbsCoord] = this.getBreadthNodeSize(n);
absOffsetMatrix[intAbsCoord] = Math.max(absOffsetMatrix[intAbsCoord], this.getBreadthNodeSize(n));
};
nodesInfos.forEach(n => {
const intAbsCoord = trunc(n.position);
addCell(n, intAbsCoord);
if(absOffsetMatrix[intAbsCoord] % 1 !== 0)
addCell(n, intAbsCoord + 1);
});
const absOffsetInfo: AbsoluteOffsetInfo = {};
let leftOffset = 0;
Object.keys(absOffsetMatrix).sort((a, b) => parseFloat(a) - parseFloat(b)).forEach(coord => {
absOffsetInfo[coord] = { leftOffset: leftOffset, width: absOffsetMatrix[coord] };
leftOffset += absOffsetMatrix[coord] + this.settings.columnSpacing;
});
return absOffsetInfo;
}
setBreadth(position: Point, breadthPosition: number): Point {
if(this.settings.orientation === DataLayoutOrientation.Vertical)
return new Point(breadthPosition, position.y);
return new Point(position.x, breadthPosition);
}
setDepthOffset(position: Point, offset: number): Point {
if(this.settings.orientation === DataLayoutOrientation.Horizontal)
return new Point(position.x + offset, position.y);
return new Point(position.x, position.y + offset);
}
getAbsPosition(absCoordinate: number, itemSize: number, absoluteOffsetInfo: AbsoluteOffsetInfo): number {
const intAbsCoord = trunc(absCoordinate);
const absLeftOffset = absoluteOffsetInfo[intAbsCoord].leftOffset;
const cellWidth = absoluteOffsetInfo[intAbsCoord].width;
if(absCoordinate % 1 === 0)
return absLeftOffset + (cellWidth - itemSize) / 2;
return absLeftOffset + cellWidth - (itemSize - this.settings.columnSpacing) / 2;
}
getBreadthNodeSize(node: NodeOnLayer): number {
return node.isDummy ? 0 : this.getBreadthNodeSizeCore(this.graph.getNode(node.key));
}
getDepthNodeSize(node: NodeOnLayer): number {
return node.isDummy ? 0 : this.getDepthNodeSizeCore(this.graph.getNode(node.key));
}
}
export class SugiyamaLayerDistributor {
static getLayers(acyclicGraph: Graph<NodeInfo>): KeyNumberMap {
const feasibleTree = this.getFeasibleTree(acyclicGraph);
return this.calcNodesLayers(feasibleTree);
}
private static getFeasibleTree(graph: Graph<NodeInfo>): Graph<NodeInfo> {
const layers = this.initLayerAssignment(graph);
return graph.getSpanningGraph(graph.nodes[0], ConnectionMode.OutgoingAndIncoming, (e) => layers[e.to] - layers[e.from]);
}
private static initLayerAssignment(graph: Graph<NodeInfo>): KeyNumberMap {
const layers: KeyNumberMap = {};
let currentLayer = 0;
const actualAssignedNodes: KeySet = {};
let assigningNodes: ItemKey[] = graph.nodes.filter(n => !graph.getAdjacentEdges(n, ConnectionMode.Incoming).length);
while(assigningNodes.length) {
assigningNodes.forEach(n => {
layers[n] = currentLayer;
actualAssignedNodes[n] = true;
});
Object.keys(actualAssignedNodes).forEach(n => {
if(graph.getAdjacentEdges(n, ConnectionMode.Outgoing).filter(e => layers[e.to] === undefined).length === 0)
delete actualAssignedNodes[n];
});
const assigningNodesSet: {[nodeKey: string]: boolean} = {};
Object.keys(actualAssignedNodes).forEach(n => {
graph.getAdjacentEdges(n, ConnectionMode.Outgoing)
.map(e => e.to)
.filter(n => layers[n] === undefined && graph.getAdjacentEdges(n, ConnectionMode.Incoming).reduce((acc, e) => acc && layers[e.from] !== undefined, true))
.forEach(n => assigningNodesSet[n] = true);
});
assigningNodes = Object.keys(assigningNodesSet);
currentLayer++;
}
return layers;
}
private static calcNodesLayers(graph: Graph<NodeInfo>): KeyNumberMap {
const layers: KeyNumberMap = {};
let minLayer = Number.MAX_SAFE_INTEGER || Number.MAX_VALUE;
let currentLevel = 0;
const iterator = graph.createIterator(ConnectionMode.OutgoingAndIncoming);
iterator.visitEachEdgeOnce = false;
iterator.onNode = (n) => {
layers[n.key] = currentLevel;
minLayer = Math.min(minLayer, currentLevel);
};
iterator.skipNode = (n) => layers[n.key] !== undefined;
iterator.skipEdge = (e) => layers[e.from] !== undefined && layers[e.to] !== undefined;
iterator.onEdge = (e, out) => {
if(out)
currentLevel = layers[e.from] + 1;
else
currentLevel = layers[e.to] - 1;
};
iterator.iterate(graph.nodes[0]);
for(const key in layers) {
if(!Object.prototype.hasOwnProperty.call(layers, key)) continue;
layers[key] -= minLayer;
}
return layers;
}
}
export class SugiyamaNodesOrderer {
private idCounter = -10000;
orderNodes(graph: Graph<NodeInfo>, layers: KeyNumberMap): FastGraph<NodeOnLayer, EdgeOnLayer> {
const maxIteration = 14;
let currentIteration = 1;
const graphInfo = this.initGraphInfo(graph, layers);
const nodeInfos = graphInfo.items;
let orderInfo = this.initOrder(nodeInfos);
let bestNodesPositions = this.getNodeToPositionMap(nodeInfos);
let bestCrossCount = this.getCrossCount(orderInfo, graphInfo);
let isParentToChildren = true;
while(currentIteration < maxIteration && bestCrossCount !== 0) {
orderInfo = this.getNodesOrder(orderInfo, graphInfo, isParentToChildren);
const crossCount = this.getCrossCount(orderInfo, graphInfo);
if(crossCount < bestCrossCount) {
bestNodesPositions = this.getNodeToPositionMap(graphInfo.items);
bestCrossCount = crossCount;
}
isParentToChildren = !isParentToChildren;
currentIteration++;
}
graphInfo.items.forEach(n => n.position = bestNodesPositions[n.key]);
return graphInfo;
}
private getNodesOrder(current: OrderInfo, graph: FastGraph<NodeOnLayer>, isParentToChildren: boolean): OrderInfo {
const order: OrderInfo = {};
for(const layer in current) {
if(!Object.prototype.hasOwnProperty.call(current, layer)) continue;
const nodePositions: KeyNumberMap = {};
const nodeKeys: string[] = [];
current[layer].forEach(ni => {
const adjacentNodesPositions = (isParentToChildren ? graph.getChildren(ni.key) : graph.getParents(ni.key))
.map(nk => graph.getNode(nk).position);
nodeKeys.push(ni.key);
nodePositions[ni.key] = this.getNodePosition(adjacentNodesPositions);
});
order[layer] = this.sortNodes(nodeKeys, nodePositions, graph);
}
return order;
}
private sortNodes(nodeKeys: string[], nodePositions: KeyNumberMap, graph: FastGraph<NodeOnLayer>): NodeOnLayer[] {
return nodeKeys
.sort((a, b) => nodePositions[a] - nodePositions[b])
.map((nk, index) => {
const node = graph.getNode(nk);
node.position = index;
return node;
});
}
private getNodePosition(adjacentNodesPositions: number[]): number {
adjacentNodesPositions = adjacentNodesPositions.sort((a, b) => a - b);
if(!adjacentNodesPositions.length)
return 0;
const medianIndex = Math.floor(adjacentNodesPositions.length / 2);
if(adjacentNodesPositions.length === 2 || adjacentNodesPositions.length % 2 === 1)
return adjacentNodesPositions[medianIndex];
const leftMedianPosition = adjacentNodesPositions[medianIndex - 1] - adjacentNodesPositions[0];
const rightMedianPosition = adjacentNodesPositions[adjacentNodesPositions.length - 1] - adjacentNodesPositions[medianIndex];
return Math.floor(
(adjacentNodesPositions[medianIndex - 1] * rightMedianPosition + adjacentNodesPositions[medianIndex] * leftMedianPosition) /
(leftMedianPosition + rightMedianPosition)
);
}
private initOrder(nodeInfos: NodeOnLayer[]): OrderInfo {
const result: OrderInfo = {};
nodeInfos.forEach(ni => (result[ni.layer] || (result[ni.layer] = [])).push(ni));
return result;
}
private getCrossCount(orderInfo: OrderInfo, graph: FastGraph<NodeOnLayer>): number {
let count = 0;
for(const layer in orderInfo) {
if(!Object.prototype.hasOwnProperty.call(orderInfo, layer)) continue;
let viewedAdjacentNodesPositions: number[] = [];
orderInfo[layer].forEach(n => {
const positions = graph.getChildren(n.key).map(c => graph.getNode(c).position);
positions.forEach(p => {
count += viewedAdjacentNodesPositions.filter(vp => p < vp).length;
});
viewedAdjacentNodesPositions = viewedAdjacentNodesPositions.concat(positions);
});
}
return count;
}
private initGraphInfo(graph: Graph<NodeInfo>, layers: KeyNumberMap): FastGraph<NodeOnLayer, EdgeOnLayer> {
const countNodesOnLayer: {[layer: number]: number} = {};
const nodesInfoMap: {[nodeKey: string]: NodeOnLayer} = {};
const nodeInfos: NodeOnLayer[] = [];
const edgeInfos: EdgeOnLayer[] = [];
graph.nodes.forEach(n => {
const layer = layers[n];
if(countNodesOnLayer[layer] === undefined)
countNodesOnLayer[layer] = 0;
const info = new NodeOnLayer(n, false, layer, countNodesOnLayer[layer]++);
nodesInfoMap[n] = info;
nodeInfos.push(info);
});
graph.edges.forEach(e => {
const span = layers[e.to] - layers[e.from];
if(span > 1) {
let prevNodeInfo = nodesInfoMap[e.from];
for(let delta = 1; delta < span; delta++) {
const dNodeInfo = new NodeOnLayer(this.createDummyID(), true, layers[e.from] + delta, countNodesOnLayer[layers[e.from] + delta]++);
edgeInfos.push(new EdgeOnLayer(this.createDummyID(), true, prevNodeInfo.key, dNodeInfo.key));
nodeInfos.push(dNodeInfo);
prevNodeInfo = dNodeInfo;
}
edgeInfos.push(new EdgeOnLayer(e.key, false, prevNodeInfo.key, nodesInfoMap[e.to].key, nodesInfoMap[e.from].key));
}
else
edgeInfos.push(new EdgeOnLayer(e.key, false, nodesInfoMap[e.from].key, nodesInfoMap[e.to].key));
});
return new FastGraph(nodeInfos, edgeInfos);
}
private createDummyID(): string {
return "dummy_" + --this.idCounter;
}
private getNodeToPositionMap(nodeInfos: NodeOnLayer[]): KeyNumberMap {
return nodeInfos.reduce((acc, ni) => {
acc[ni.key] = ni.position;
return acc;
}, {});
}
assignAbsCoordinates(graph: FastGraph<NodeOnLayer, EdgeOnLayer>): FastGraph<NodeOnLayer, EdgeOnLayer> {
const absCoordinates = this.getAbsCoodinate(graph);
return new FastGraph(
graph.items.map(n => new NodeOnLayer(n.key, n.isDummy, n.layer, absCoordinates[n.key])),
graph.edges.slice(0)
);
}
private getAbsCoodinate(graph: FastGraph<NodeOnLayer, EdgeOnLayer>): KeyNumberMap {
const orderInfo: OrderInfo = graph.items.reduce<OrderInfo>((acc, n) => {
acc[n.layer] = acc[n.layer] || [];
const pos = SearchUtils.binaryIndexOf(acc[n.layer], ni => ni.position - n.position);
acc[n.layer].splice(pos < 0 ? ~pos : pos, 0, n);
return acc;
}, {});
const medianPositions =
[MedianAlignmentMode.TopLeft, MedianAlignmentMode.TopRight, MedianAlignmentMode.BottomLeft, MedianAlignmentMode.BottomRight]
.map(alignment => this.getPositionByMedian(graph, alignment, orderInfo));
const nodeToPosition: KeyNumberMap = {};
graph.items.forEach(n => {
const posList: number[] = medianPositions.map(positions => positions[n.key]).sort((a, b) => a - b);
nodeToPosition[n.key] = (posList[1] + posList[2]) / 2;
});
return nodeToPosition;
}
private getPositionByMedian(graph: FastGraph<NodeOnLayer, EdgeOnLayer>, alignment: MedianAlignmentMode, orderInfo: OrderInfo): KeyNumberMap {
const nodeInfos = graph.items;
const positions = this.getNodeToPositionMap(nodeInfos);
let medians = this.getMedians(graph, nodeInfos, alignment);
medians = this.resolveMedianConflicts(graph, orderInfo, medians, alignment);
this.getSortedBlocks(graph, nodeInfos, medians, alignment)
.forEach(block => {
const maxPos = block.reduce((acc, n) => positions[n.key] > acc ? positions[n.key] : acc, -2);
block.forEach(n => {
const delta = maxPos - positions[n.key];
if(delta > 0)
orderInfo[n.layer]
.filter(ln => ln.position > n.position)
.forEach(ln => positions[ln.key] += delta);
positions[n.key] = maxPos;
});
});
return positions;
}
private getSortedBlocks(graph: FastGraph<NodeOnLayer>, nodeInfos: NodeOnLayer[], medians: Medians, alignment: MedianAlignmentMode): NodeOnLayer[][] {
const blocks: NodeOnLayer[][] = [];
const isBottom = alignment === MedianAlignmentMode.BottomLeft || alignment === MedianAlignmentMode.BottomRight;
const allNodesInfo = new HashSet<NodeOnLayer>(nodeInfos.slice(0).sort((a, b) => isBottom ? (a.layer - b.layer) : (b.layer - a.layer)), n => n.key);
const knownNodes = new HashSet();
while(allNodesInfo.length) {
const firstNode = allNodesInfo.item(0);
const block = this.getBlock(graph, firstNode, medians, alignment).filter(n => knownNodes.tryPush(n.key));
blocks.push(block);
block.forEach(n => allNodesInfo.remove(n));
}
blocks.sort((x, y) => {
const xMinNodeInfo = x.reduce((min, n) => n.position < min.position ? n : min, x[0]);
const yOnMinXLayer = y.filter(n => n.layer === xMinNodeInfo.layer)[0];
if(yOnMinXLayer)
return xMinNodeInfo.position > yOnMinXLayer.position ? 1 : -1;
const yMinNodeInfo = y.reduce((min, n) => n.position < min.position ? n : min, y[0]);
const xOnMinYLayer = x.filter(n => n.layer === yMinNodeInfo.layer)[0];
if(xOnMinYLayer)
return xOnMinYLayer.position > yMinNodeInfo.position ? 1 : -1;
return xMinNodeInfo.layer > yMinNodeInfo.layer ? 1 : -1;
});
return blocks;
}
private getBlock(graph: FastGraph<NodeOnLayer>, root: NodeOnLayer, medians: Medians, alignment: MedianAlignmentMode): NodeOnLayer[] {
const block: NodeOnLayer[] = [];
let median: EdgeOnLayer = null;
do {
if(median)
root = alignment === MedianAlignmentMode.TopLeft || alignment === MedianAlignmentMode.TopRight ? graph.getNode(median.from) : graph.getNode(median.to);
block.push(root);
median = medians[root.key];
} while(median);
return block;
}
private resolveMedianConflicts(graph: FastGraph<NodeOnLayer>, layers: OrderInfo, medians: Medians, alignment: MedianAlignmentMode): Medians {
const filteredMedians: Medians = {};
for(const layer in layers) {
if(!Object.prototype.hasOwnProperty.call(layers, layer)) continue;
let minPos: number;
let maxPos: number;
let nodeInfos = layers[layer];
if(alignment === MedianAlignmentMode.TopRight || alignment === MedianAlignmentMode.BottomRight)
nodeInfos = nodeInfos.slice(0).sort((a, b) => b.position - a.position);
nodeInfos.forEach(n => {
const median = medians[n.key];
if(!median)
filteredMedians[n.key] = null;
else {
const medianItemKey = alignment === MedianAlignmentMode.TopLeft || alignment === MedianAlignmentMode.TopRight ? median.from : median.to;
const medianPosition = graph.getNode(medianItemKey).position;
if(this.checkMedianConfict(minPos, maxPos, medianPosition, alignment))
filteredMedians[n.key] = null;
else {
minPos = minPos === undefined ? medianPosition : Math.min(minPos, medianPosition);
maxPos = maxPos === undefined ? medianPosition : Math.max(maxPos, medianPosition);
filteredMedians[n.key] = median;
}
}
});
}
return filteredMedians;
}
private checkMedianConfict(min: number, max: number, medianPosition: number, alignment: MedianAlignmentMode): boolean {
if(min === undefined || max === undefined)
return false;
if(alignment === MedianAlignmentMode.TopLeft || alignment === MedianAlignmentMode.BottomLeft)
return max >= medianPosition;
return min <= medianPosition;
}
private getMedians(graph: FastGraph<NodeOnLayer, EdgeOnLayer>, nodeInfos: NodeOnLayer[], alignment: MedianAlignmentMode): Medians {
const medians: {[nodeKey: string]: EdgeOnLayer} = {};
nodeInfos.forEach(n => {
const actualAdjacentEdges = this.getActualAdjacentEdges(graph, n, alignment);
const medianPosition = this.getMedianPosition(actualAdjacentEdges.length, alignment);
medians[n.key] = actualAdjacentEdges[medianPosition];
});
return medians;
}
getMedianPosition(length: number, alignment: MedianAlignmentMode): number {
if(length === 0)
return -1;
if(length % 2 !== 0)
return Math.floor(length / 2);
if(alignment === MedianAlignmentMode.TopLeft || alignment === MedianAlignmentMode.BottomLeft)
return Math.floor(length / 2) - 1;
if(alignment === MedianAlignmentMode.TopRight || alignment === MedianAlignmentMode.BottomRight)
return Math.floor(length / 2);
throw new Error("Invalid Operation");
}
getActualAdjacentEdges(graph: FastGraph<NodeOnLayer, EdgeOnLayer>, node: NodeOnLayer, alignment: MedianAlignmentMode): EdgeOnLayer[] {
if(alignment === MedianAlignmentMode.TopLeft || alignment === MedianAlignmentMode.TopRight)
return graph.getAdjacentEdges(node.key, ConnectionMode.Incoming).sort((a, b) => graph.getNode(a.from).position - graph.getNode(b.from).position);
return graph.getAdjacentEdges(node.key, ConnectionMode.Outgoing).sort((a, b) => graph.getNode(a.to).position - graph.getNode(b.to).position);
}
}
export class NodeOnLayer implements IHashCodeOwner, IKeyOwner {
constructor(
public key: ItemKey,
public isDummy: boolean,
public layer: number,
public position: number
) {}
getHashCode(): string {
return this.key.toString();
}
}
export class EdgeOnLayer implements IEdge, IHashCodeOwner {
getHashCode(): string {
return this.from + "-" + this.to;
}
private _originFrom: ItemKey;
constructor(
public key: ItemKey,
public isDummy: boolean,
public from: ItemKey,
public to: ItemKey,
originFrom?: ItemKey
) {
this._originFrom = originFrom;
}
get originFrom(): ItemKey {
return this._originFrom !== undefined ? this._originFrom : this.from;
}
}
enum MedianAlignmentMode {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
function trunc(val: number) {
if(Math.trunc)
return Math.trunc(val);
if(!isFinite(val)) return val;
return (val - val % 1) || (val < 0 ? -0 : val === 0 ? val : 0);
}