UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

260 lines (239 loc) 10.2 kB
import { Point } from "@devexpress/utils/lib/geometry/point"; import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle"; export class RTree<T> { private maxWidth: number; private minWidth: number; private root: RTreeNodeBranch; constructor(maxWidth: number = 6) { this.maxWidth = maxWidth; this.minWidth = Math.floor(maxWidth / 2); this.root = new RTreeNodeBranch(new Rectangle(0, 0, 0, 0), []); } search(point: Point): T[] { const result: T[] = []; if(!this.root.rect.containsPoint(point)) return result; const hitsStack: RTreeNode[][] = []; hitsStack.push(this.root.nodes); while(hitsStack.length) { const nodes = hitsStack.pop(); for(let i = nodes.length - 1, node: RTreeNode; node = nodes[i]; i--) if(node.rect.containsPoint(point)) if(isBranch(node)) hitsStack.push(node.nodes); else result.push((<RTreeNodeLeaf<T>>node).obj); } return result; } insert(rect: Rectangle, obj: T): void { const newLeaf = new RTreeNodeLeaf(rect.clone(), obj); if(!this.root.nodes.length) { this.root.rect = rect.clone(); this.root.nodes.push(newLeaf); return; } const treeStack = this.chooseLeafSubtree(newLeaf); let retObj : RTreeNode | RTreeNode[] | Rectangle = newLeaf; let current: RTreeNodeBranch; let previous: RTreeNodeBranch; while(treeStack.length) { if(current && isBranch(current) && !current.nodes.length) { previous = current; current = treeStack.pop(); for(let i = 0, node: RTreeNode; node = current.nodes[i]; i++) { if(isBranch(node) && (node === previous || !node.nodes.length)) { node.nodes.splice(i, 1); break; } } } else current = treeStack.pop(); if(retObj instanceof RTreeNode || Array.isArray(retObj)) { if(Array.isArray(retObj)) { for(let i = 0, retObjChild: RTreeNode; retObjChild = retObj[i]; i++) { expandRect(current.rect, retObjChild.rect); } current.nodes = current.nodes.concat(retObj); } else { expandRect(current.rect, retObj.rect); current.nodes.push(retObj); } if(current.nodes.length <= this.maxWidth) { retObj = current.rect.clone(); } else { const a = this.linearSplit(current.nodes); retObj = a; if(!treeStack.length) { current.nodes.push(a[0]); treeStack.push(current); retObj = a[1]; } } } else { expandRect(current.rect, retObj); retObj = current.rect.clone(); } } } private chooseLeafSubtree(newLeaf: RTreeNodeLeaf<T>): RTreeNodeBranch[] { const result: RTreeNodeBranch[] = []; let bestChoiceIndex = -1; let bestChoiceArea; let first = true; result.push(this.root); let nodes = this.root.nodes; while(first || bestChoiceIndex !== -1) { if(first) first = false; else { result.push(<RTreeNodeBranch>nodes[bestChoiceIndex]); nodes = (<RTreeNodeBranch>nodes[bestChoiceIndex]).nodes; bestChoiceIndex = -1; } for(let i = nodes.length - 1, ltree: RTreeNode; ltree = nodes[i]; i--) { if(!isBranch(ltree)) { bestChoiceIndex = -1; break; } const oldLRatio = squarifiedRatio(ltree.rect.width, ltree.rect.height, ltree.nodes.length + 1); const nw = Math.max(ltree.rect.x + ltree.rect.width, newLeaf.rect.x + newLeaf.rect.width) - Math.min(ltree.rect.x, newLeaf.rect.x); const nh = Math.max(ltree.rect.y + ltree.rect.height, newLeaf.rect.y + newLeaf.rect.height) - Math.min(ltree.rect.y, newLeaf.rect.y); const lratio = squarifiedRatio(nw, nh, ltree.nodes.length + 2); if(bestChoiceIndex < 0 || Math.abs(lratio - oldLRatio) < bestChoiceArea) { bestChoiceArea = Math.abs(lratio - oldLRatio); bestChoiceIndex = i; } } } return result; } private linearSplit(nodes): RTreeNodeBranch[] { const n = this.pickLinear(nodes); while(nodes.length > 0) { this.pickNext(nodes, n[0], n[1]); } return n; } private pickLinear(nodes: RTreeNode[]): RTreeNodeBranch[] { let lowestHighX = nodes.length - 1; let highestLowX = 0; let lowestHighY = nodes.length - 1; let highestLowY = 0; let t1: RTreeNode; let t2: RTreeNode; for(let i = nodes.length - 2, l: RTreeNode; l = nodes[i]; i--) { if(l.rect.x > nodes[highestLowX].rect.x) highestLowX = i; else if(l.rect.right < nodes[lowestHighX].rect.right) lowestHighX = i; if(l.rect.y > nodes[highestLowY].rect.y) highestLowY = i; else if(l.rect.bottom < nodes[lowestHighY].rect.bottom) lowestHighY = i; } const dx = Math.abs((nodes[lowestHighX].rect.right) - nodes[highestLowX].rect.x); const dy = Math.abs((nodes[lowestHighY].rect.bottom) - nodes[highestLowY].rect.y); if(dx > dy) { if(lowestHighX > highestLowX) { t1 = nodes.splice(lowestHighX, 1)[0]; t2 = nodes.splice(highestLowX, 1)[0]; } else { t2 = nodes.splice(highestLowX, 1)[0]; t1 = nodes.splice(lowestHighX, 1)[0]; } } else { if(lowestHighY > highestLowY) { t1 = nodes.splice(lowestHighY, 1)[0]; t2 = nodes.splice(highestLowY, 1)[0]; } else { t2 = nodes.splice(highestLowY, 1)[0]; t1 = nodes.splice(lowestHighY, 1)[0]; } } return [ new RTreeNodeBranch(t1.rect.clone(), [t1]), new RTreeNodeBranch(t2.rect.clone(), [t2]) ]; } private pickNext(nodes: RTreeNode[], a: RTreeNodeBranch, b: RTreeNodeBranch): void { const areaA = squarifiedRatio(a.rect.width, a.rect.height, a.nodes.length + 1); const areaB = squarifiedRatio(b.rect.width, b.rect.height, b.nodes.length + 1); let highAreaDelta: number; let highAreaNode: number; let lowestGrowthGroup: RTreeNodeBranch; for(let i = nodes.length - 1, l: RTreeNode; l = nodes[i]; i--) { const newAreaA = new Rectangle( Math.min(a.rect.x, l.rect.x), Math.min(a.rect.y, l.rect.y), 0, 0 ); newAreaA.width = Math.max(a.rect.right, l.rect.right) - newAreaA.x; newAreaA.height = Math.max(a.rect.bottom, l.rect.bottom) - newAreaA.y; const changeNewAreaA = Math.abs(squarifiedRatio(newAreaA.width, newAreaA.height, a.nodes.length + 2) - areaA); const newAreaB = new Rectangle( Math.min(b.rect.x, l.rect.x), Math.min(b.rect.y, l.rect.y), 0, 0 ); newAreaB.width = Math.max(b.rect.right, l.rect.right) - newAreaB.x; newAreaB.height = Math.max(b.rect.bottom, l.rect.bottom) - newAreaB.y; const changeNewAreaB = Math.abs(squarifiedRatio(newAreaB.width, newAreaB.height, b.nodes.length + 2) - areaB); if(!highAreaNode || !highAreaDelta || Math.abs(changeNewAreaB - changeNewAreaA) < highAreaDelta) { highAreaNode = i; highAreaDelta = Math.abs(changeNewAreaB - changeNewAreaA); lowestGrowthGroup = changeNewAreaB < changeNewAreaA ? b : a; } } const tmp = nodes.splice(highAreaNode, 1)[0]; if(a.nodes.length + nodes.length + 1 <= this.minWidth) { a.nodes.push(tmp); expandRect(a.rect, tmp.rect); } else if(b.nodes.length + nodes.length + 1 <= this.minWidth) { b.nodes.push(tmp); expandRect(b.rect, tmp.rect); } else { lowestGrowthGroup.nodes.push(tmp); expandRect(lowestGrowthGroup.rect, tmp.rect); } } } class RTreeNode { constructor(public rect: Rectangle) { } } class RTreeNodeLeaf<T> extends RTreeNode { constructor(rect: Rectangle, public obj: T) { super(rect); } } class RTreeNodeBranch extends RTreeNode { constructor(rect: Rectangle, public nodes: RTreeNode[]) { super(rect); } } function isBranch(node: RTreeNode): node is RTreeNodeBranch { return "nodes" in node; } function squarifiedRatio(width: number, height: number, fill: number): number { const lperi = (width + height) / 2; const larea = width * height; const lgeo = larea / (lperi * lperi); return larea * fill / lgeo; } function expandRect(rect: Rectangle, other: Rectangle): void { const rx = Math.max(rect.right, other.right); const ry = Math.max(rect.bottom, other.bottom); rect.x = Math.min(rect.x, other.x); rect.y = Math.min(rect.y, other.y); rect.width = rx - rect.x; rect.height = ry - rect.y; }