UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

154 lines (145 loc) 8.78 kB
import { GraphLayout } from "../GraphLayout"; import { Tree } from "../Tree"; import { NodeLayout, EdgeLayout, NodeInfo } from "../NodeLayout"; import { Range } from "../../Utils"; import { Point } from "@devexpress/utils/lib/geometry/point"; import { LayoutBuilder } from "./BaseBuilder"; import { ConnectionMode, Edge } from "../Structures"; import { ConnectionPointSide } from "../../Model/DiagramItem"; import { TreeLayoutSettings, Alignment, DataLayoutOrientation, LogicalDirectionKind } from "../LayoutSettings"; import { Graph } from "../Graph"; export class TreeLayoutBuilder extends LayoutBuilder<TreeLayoutSettings> { private nodeToLevel: {[key: string]: number} = {}; private levelDepthSize: {[level: number]: number} = {}; build(): GraphLayout { const layout = new GraphLayout(); let offset = 0; this.graph.getConnectedComponents().forEach(c => { const tree = Tree.createSpanningTree(c); const componentLayout = this.processTree(tree); let subOffset = this.getComponentOffset(componentLayout); while(componentLayout.nodeKeys.length < c.nodes.length) { const subGraph = new Graph<NodeInfo>( c.nodes.filter(n => !componentLayout.nodeToLayout[n]).map(n => c.getNode(n)), c.edges.filter(e => !componentLayout.edgeToPosition[e.key]) ); const subTree = Tree.createSpanningTree(subGraph); const subComponentLayout = this.processTree(subTree); componentLayout.extend(this.setComponentOffset(subComponentLayout, subOffset)); subOffset += this.getComponentOffset(subComponentLayout); } layout.extend(this.setComponentOffset(componentLayout, offset)); offset += this.getComponentOffset(componentLayout); }); return layout; } protected preProcessTree(tree: Tree<NodeInfo>, parents: NodeInfo[], level: number) { parents = parents.filter(p => (this.nodeToLevel[p.key] === undefined ? (this.nodeToLevel[p.key] = level) : -1) >= 0); if(parents.length) { const maxDepthSize = this.getMaxDepthSize(parents); this.levelDepthSize[level] = maxDepthSize; this.preProcessTree(tree, [].concat(...parents.map(p => tree.getChildren(p))), level + 1); } } protected loadNodes(tree: Tree<NodeInfo>, layout: GraphLayout, parent?: NodeLayout): NodeLayout[] { if(!parent) return [layout.addNode(new NodeLayout(tree.root, Point.zero()))]; return tree.getChildren(parent.info).map(child => { return !layout.hasNode(child.key) ? layout.addNode(new NodeLayout(child, Point.zero())) : undefined; }).filter(nl => nl); } protected processTree(tree: Tree<NodeInfo>): GraphLayout { const layout = new GraphLayout(); this.preProcessTree(tree, [tree.root], 0); this.processLevel(tree, layout, 0, new Range(0), 0); if(this.settings.direction === LogicalDirectionKind.Backward) { const levelDepths = Object.keys(this.levelDepthSize).map(l => this.levelDepthSize[l]); const mostDepthPos = levelDepths.reduce((acc, v) => acc + v, 0) + (levelDepths.length - 1) * this.settings.layerSpacing; layout.forEachNode(n => this.setDepthPos(n, this.getDepthPos(n) + mostDepthPos)); } return layout; } protected processLevel(tree: Tree<NodeInfo>, layout: GraphLayout, depthPos: number, breadthParentRange: Range, level: number, parent?: NodeLayout) { const nodes = this.addNodes(tree, layout, level, parent); const parentEdges = parent ? this.graph.getAdjacentEdges(parent.key, ConnectionMode.Outgoing) : []; const maxDepthSize = this.getDirectionValue(this.levelDepthSize[level]); const layerSpacing = this.getDirectionValue(this.settings.layerSpacing); let prevRange: Range; nodes.forEach(node => { const range = Range.fromLength(prevRange ? (prevRange.to + this.settings.columnSpacing) : breadthParentRange.from, this.getBreadthNodeSizeCore(node.info)); node.position = this.getNodePosition(range.from, depthPos, maxDepthSize).clone().offset(node.info.margin.left, node.info.margin.top); this.processLevel(tree, layout, depthPos + maxDepthSize + layerSpacing, range, level + 1, node); this.updateEdgeConnections(layout, parentEdges, node); breadthParentRange.extend(range); prevRange = range; }); if(parent && nodes.length) { const lastChild = nodes[nodes.length - 1]; const childRange = new Range(this.getBreadthPos(nodes[0]), this.getBreadthPos(lastChild) + this.getBreadthNodeSizeCore(lastChild.info, true)); this.alignParent(parent, childRange, breadthParentRange); } } protected addNodes(tree: Tree<NodeInfo>, layout: GraphLayout, level: number, parent?: NodeLayout): NodeLayout[] { if(level === 0) return [layout.addNode(new NodeLayout(tree.root, Point.zero()))]; return tree.getChildren(parent.info) .reduce<NodeLayout[]>((acc, n) => { if(this.nodeToLevel[n.key] === level && !layout.hasNode(n.key)) acc.push(layout.addNode(new NodeLayout(n, Point.zero()))); return acc; }, []); } protected getMaxDepthSize(nodes: NodeInfo[]) { return nodes.reduce((acc, node) => Math.max(acc, this.getDepthNodeSizeCore(node)), 0); } protected getNodePosition(breadthPos: number, depthPos: number, maxDepthSide: number) { if(this.settings.direction === LogicalDirectionKind.Forward) return this.settings.orientation === DataLayoutOrientation.Vertical ? new Point(breadthPos, depthPos) : new Point(depthPos, breadthPos); return this.settings.orientation === DataLayoutOrientation.Vertical ? new Point(breadthPos, depthPos + maxDepthSide) : new Point(depthPos + maxDepthSide, breadthPos); } protected updateEdgeConnections(layout: GraphLayout, edges: Edge[], node: NodeLayout) { edges.filter(e => e.to === node.key).forEach(e => { const beginIndex = this.getBeginEdgeIndex(); const endIndex = this.getEndEdgeIndex(); layout.addEdge(new EdgeLayout(e.key, beginIndex, endIndex)); }); } protected getBeginEdgeIndex() { if(this.settings.direction === LogicalDirectionKind.Forward) return this.isVertical() ? ConnectionPointSide.South : ConnectionPointSide.East; return this.isVertical() ? ConnectionPointSide.North : ConnectionPointSide.West; } protected getEndEdgeIndex() { if(this.settings.direction === LogicalDirectionKind.Forward) return this.isVertical() ? ConnectionPointSide.North : ConnectionPointSide.West; return this.isVertical() ? ConnectionPointSide.South : ConnectionPointSide.East; } protected alignParent(parent: NodeLayout, childRange: Range, availableRange: Range) { if(this.settings.alignment === Alignment.Center) { const alignedPosition = childRange.from + childRange.length / 2 - this.getBreadthNodeSizeCore(parent.info, true) / 2; if(this.settings.orientation === DataLayoutOrientation.Vertical) { parent.position.x = Math.max(availableRange.from + parent.info.margin.left, alignedPosition); parent.position.x = Math.min(availableRange.to - parent.info.size.width - parent.info.margin.right, parent.position.x); } else { parent.position.y = Math.max(availableRange.from + parent.info.margin.top, alignedPosition); parent.position.y = Math.min(availableRange.to - parent.info.size.height - parent.info.margin.bottom, parent.position.y); } } } protected getDepthPos(node: NodeLayout): number { return this.settings.orientation === DataLayoutOrientation.Vertical ? node.position.y : node.position.x; } protected getBreadthPos(node: NodeLayout): number { return this.settings.orientation === DataLayoutOrientation.Vertical ? node.position.x : node.position.y; } protected setDepthPos(node: NodeLayout, pos: number) { if(this.settings.orientation === DataLayoutOrientation.Vertical) node.position.y = pos; else node.position.x = pos; } protected isVertical() { return this.settings.orientation === DataLayoutOrientation.Vertical; } }