UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

581 lines (561 loc) 30.8 kB
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); }