devexpress-diagram
Version:
DevExpress Diagram Control
260 lines (239 loc) • 10.2 kB
text/typescript
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;
}