jam/js/rtree/rtree.js

491 lines
16 KiB
JavaScript

'use strict';
var rectangle = Require('rtree/rectangle');
var geojson = Require('rtree/geojson');
function RTree(width) {
if (!(this instanceof RTree)) {
return new RTree(width);
}
// Variables to control tree-dimensions
var minWidth = 3; // Minimum width of any node before a merge
var maxWidth = 6; // Maximum width of any node before a split
if (!isNaN(width)) {
minWidth = Math.floor(width / 2.0);
maxWidth = width;
}
// Start with an empty root-tree
var rootTree = {x: 0, y: 0, w: 0, h: 0, id: 'root', nodes: [] };
this.root = rootTree;
// This is my special addition to the world of r-trees
// every other (simple) method I found produced crap trees
// this skews insertions to prefering squarer and emptier nodes
var flatten = function (tree) {
var todo = tree.slice();
var done = [];
var current;
while (todo.length) {
current = todo.pop();
if (current.nodes) {
todo = todo.concat(current.nodes);
} else if (current.leaf) {
done.push(current);
}
}
return done;
};
/* find the best specific node(s) for object to be deleted from
* [ leaf node parent ] = removeSubtree(rectangle, object, root)
* @private
*/
var removeSubtree = function (rect, obj, root) {
var hitStack = []; // Contains the elements that overlap
var countStack = []; // Contains the elements that overlap
var retArray = [];
var currentDepth = 1;
var tree, i, ltree;
if (!rect || !rectangle.overlapRectangle(rect, root)) {
return retArray;
}
var retObj = {x: rect.x, y: rect.y, w: rect.w, h: rect.h, target: obj};
countStack.push(root.nodes.length);
hitStack.push(root);
while (hitStack.length > 0) {
tree = hitStack.pop();
i = countStack.pop() - 1;
if ('target' in retObj) { // will this ever be false?
while (i >= 0) {
ltree = tree.nodes[i];
if (rectangle.overlapRectangle(retObj, ltree)) {
if ((retObj.target && 'leaf' in ltree && ltree.leaf === retObj.target) || (!retObj.target && ('leaf' in ltree || rectangle.containsRectangle(ltree, retObj)))) {
// A Match !!
// Yup we found a match...
// we can cancel search and start walking up the list
if ('nodes' in ltree) {// If we are deleting a node not a leaf...
retArray = flatten(tree.nodes.splice(i, 1));
} else {
retArray = tree.nodes.splice(i, 1);
}
// Resize MBR down...
rectangle.makeMBR(tree.nodes, tree);
delete retObj.target;
//if (tree.nodes.length < minWidth) { // Underflow
// retObj.nodes = searchSubtree(tree, true, [], tree);
//}
break;
} else if ('nodes' in ltree) { // Not a Leaf
currentDepth++;
countStack.push(i);
hitStack.push(tree);
tree = ltree;
i = ltree.nodes.length;
}
}
i--;
}
} else if ('nodes' in retObj) { // We are unsplitting
tree.nodes.splice(i + 1, 1); // Remove unsplit node
if (tree.nodes.length > 0) {
rectangle.makeMBR(tree.nodes, tree);
}
for (var t = 0;t < retObj.nodes.length;t++) {
insertSubtree(retObj.nodes[t], tree);
}
retObj.nodes = [];
if (hitStack.length === 0 && tree.nodes.length <= 1) { // Underflow..on root!
retObj.nodes = searchSubtree(tree, true, retObj.nodes, tree);
tree.nodes = [];
hitStack.push(tree);
countStack.push(1);
} else if (hitStack.length > 0 && tree.nodes.length < minWidth) { // Underflow..AGAIN!
retObj.nodes = searchSubtree(tree, true, retObj.nodes, tree);
tree.nodes = [];
} else {
delete retObj.nodes; // Just start resizing
}
} else { // we are just resizing
rectangle.makeMBR(tree.nodes, tree);
}
currentDepth -= 1;
}
return retArray;
};
/* choose the best damn node for rectangle to be inserted into
* [ leaf node parent ] = chooseLeafSubtree(rectangle, root to start search at)
* @private
*/
var chooseLeafSubtree = function (rect, root) {
var bestChoiceIndex = -1;
var bestChoiceStack = [];
var bestChoiceArea;
var first = true;
bestChoiceStack.push(root);
var nodes = root.nodes;
while (first || bestChoiceIndex !== -1) {
if (first) {
first = false;
} else {
bestChoiceStack.push(nodes[bestChoiceIndex]);
nodes = nodes[bestChoiceIndex].nodes;
bestChoiceIndex = -1;
}
for (var i = nodes.length - 1; i >= 0; i--) {
var ltree = nodes[i];
if ('leaf' in ltree) {
// Bail out of everything and start inserting
bestChoiceIndex = -1;
break;
}
// Area of new enlarged rectangle
var oldLRatio = rectangle.squarifiedRatio(ltree.w, ltree.h, ltree.nodes.length + 1);
// Enlarge rectangle to fit new rectangle
var nw = Math.max(ltree.x + ltree.w, rect.x + rect.w) - Math.min(ltree.x, rect.x);
var nh = Math.max(ltree.y + ltree.h, rect.y + rect.h) - Math.min(ltree.y, rect.y);
// Area of new enlarged rectangle
var lratio = rectangle.squarifiedRatio(nw, nh, ltree.nodes.length + 2);
if (bestChoiceIndex < 0 || Math.abs(lratio - oldLRatio) < bestChoiceArea) {
bestChoiceArea = Math.abs(lratio - oldLRatio);
bestChoiceIndex = i;
}
}
}
return bestChoiceStack;
};
/* split a set of nodes into two roughly equally-filled nodes
* [ an array of two new arrays of nodes ] = linearSplit(array of nodes)
* @private
*/
var linearSplit = function (nodes) {
var n = pickLinear(nodes);
while (nodes.length > 0) {
pickNext(nodes, n[0], n[1]);
}
return n;
};
/* insert the best source rectangle into the best fitting parent node: a or b
* [] = pickNext(array of source nodes, target node array a, target node array b)
* @private
*/
var pickNext = function (nodes, a, b) {
// Area of new enlarged rectangle
var areaA = rectangle.squarifiedRatio(a.w, a.h, a.nodes.length + 1);
var areaB = rectangle.squarifiedRatio(b.w, b.h, b.nodes.length + 1);
var highAreaDelta;
var highAreaNode;
var lowestGrowthGroup;
for (var i = nodes.length - 1; i >= 0;i--) {
var l = nodes[i];
var newAreaA = {};
newAreaA.x = Math.min(a.x, l.x);
newAreaA.y = Math.min(a.y, l.y);
newAreaA.w = Math.max(a.x + a.w, l.x + l.w) - newAreaA.x;
newAreaA.h = Math.max(a.y + a.h, l.y + l.h) - newAreaA.y;
var changeNewAreaA = Math.abs(rectangle.squarifiedRatio(newAreaA.w, newAreaA.h, a.nodes.length + 2) - areaA);
var newAreaB = {};
newAreaB.x = Math.min(b.x, l.x);
newAreaB.y = Math.min(b.y, l.y);
newAreaB.w = Math.max(b.x + b.w, l.x + l.w) - newAreaB.x;
newAreaB.h = Math.max(b.y + b.h, l.y + l.h) - newAreaB.y;
var changeNewAreaB = Math.abs(rectangle.squarifiedRatio(newAreaB.w, newAreaB.h, 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;
}
}
var tempNode = nodes.splice(highAreaNode, 1)[0];
if (a.nodes.length + nodes.length + 1 <= minWidth) {
a.nodes.push(tempNode);
rectangle.expandRectangle(a, tempNode);
} else if (b.nodes.length + nodes.length + 1 <= minWidth) {
b.nodes.push(tempNode);
rectangle.expandRectangle(b, tempNode);
}
else {
lowestGrowthGroup.nodes.push(tempNode);
rectangle.expandRectangle(lowestGrowthGroup, tempNode);
}
};
/* pick the 'best' two starter nodes to use as seeds using the 'linear' criteria
* [ an array of two new arrays of nodes ] = pickLinear(array of source nodes)
* @private
*/
var pickLinear = function (nodes) {
var lowestHighX = nodes.length - 1;
var highestLowX = 0;
var lowestHighY = nodes.length - 1;
var highestLowY = 0;
var t1, t2;
for (var i = nodes.length - 2; i >= 0;i--) {
var l = nodes[i];
if (l.x > nodes[highestLowX].x) {
highestLowX = i;
} else if (l.x + l.w < nodes[lowestHighX].x + nodes[lowestHighX].w) {
lowestHighX = i;
}
if (l.y > nodes[highestLowY].y) {
highestLowY = i;
} else if (l.y + l.h < nodes[lowestHighY].y + nodes[lowestHighY].h) {
lowestHighY = i;
}
}
var dx = Math.abs((nodes[lowestHighX].x + nodes[lowestHighX].w) - nodes[highestLowX].x);
var dy = Math.abs((nodes[lowestHighY].y + nodes[lowestHighY].h) - nodes[highestLowY].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 [
{x: t1.x, y: t1.y, w: t1.w, h: t1.h, nodes: [t1]},
{x: t2.x, y: t2.y, w: t2.w, h: t2.h, nodes: [t2]}
];
};
var attachData = function (node, moreTree) {
node.nodes = moreTree.nodes;
node.x = moreTree.x;
node.y = moreTree.y;
node.w = moreTree.w;
node.h = moreTree.h;
return node;
};
/* non-recursive internal search function
* [ nodes | objects ] = searchSubtree(rectangle, [return node data], [array to fill], root to begin search at)
* @private
*/
var searchSubtree = function (rect, returnNode, returnArray, root) {
var hitStack = []; // Contains the elements that overlap
if (!rectangle.overlapRectangle(rect, root)) {
return returnArray;
}
hitStack.push(root.nodes);
while (hitStack.length > 0) {
var nodes = hitStack.pop();
for (var i = nodes.length - 1; i >= 0; i--) {
var ltree = nodes[i];
if (rectangle.overlapRectangle(rect, ltree)) {
if ('nodes' in ltree) { // Not a Leaf
hitStack.push(ltree.nodes);
} else if ('leaf' in ltree) { // A Leaf !!
if (!returnNode) {
returnArray.push(ltree.leaf);
} else {
returnArray.push(ltree);
}
}
}
}
}
return returnArray;
};
/* non-recursive internal insert function
* [] = insertSubtree(rectangle, object to insert, root to begin insertion at)
* @private
*/
var insertSubtree = function (node, root) {
var bc; // Best Current node
// Initial insertion is special because we resize the Tree and we don't
// care about any overflow (seriously, how can the first object overflow?)
if (root.nodes.length === 0) {
root.x = node.x;
root.y = node.y;
root.w = node.w;
root.h = node.h;
root.nodes.push(node);
return;
}
// Find the best fitting leaf node
// chooseLeaf returns an array of all tree levels (including root)
// that were traversed while trying to find the leaf
var treeStack = chooseLeafSubtree(node, root);
var retObj = node;//{x:rect.x,y:rect.y,w:rect.w,h:rect.h, leaf:obj};
var pbc;
// Walk back up the tree resizing and inserting as needed
while (treeStack.length > 0) {
//handle the case of an empty node (from a split)
if (bc && 'nodes' in bc && bc.nodes.length === 0) {
pbc = bc; // Past bc
bc = treeStack.pop();
for (var t = 0;t < bc.nodes.length;t++) {
if (bc.nodes[t] === pbc || bc.nodes[t].nodes.length === 0) {
bc.nodes.splice(t, 1);
break;
}
}
} else {
bc = treeStack.pop();
}
// If there is data attached to this retObj
if ('leaf' in retObj || 'nodes' in retObj || Array.isArray(retObj)) {
// Do Insert
if (Array.isArray(retObj)) {
for (var ai = 0; ai < retObj.length; ai++) {
rectangle.expandRectangle(bc, retObj[ai]);
}
bc.nodes = bc.nodes.concat(retObj);
} else {
rectangle.expandRectangle(bc, retObj);
bc.nodes.push(retObj); // Do Insert
}
if (bc.nodes.length <= maxWidth) { // Start Resizeing Up the Tree
retObj = {x: bc.x, y: bc.y, w: bc.w, h: bc.h};
} else { // Otherwise Split this Node
// linearSplit() returns an array containing two new nodes
// formed from the split of the previous node's overflow
var a = linearSplit(bc.nodes);
retObj = a;//[1];
if (treeStack.length < 1) { // If are splitting the root..
bc.nodes.push(a[0]);
treeStack.push(bc); // Reconsider the root element
retObj = a[1];
} /*else {
delete bc;
}*/
}
} else { // Otherwise Do Resize
//Just keep applying the new bounding rectangle to the parents..
rectangle.expandRectangle(bc, retObj);
retObj = {x: bc.x, y: bc.y, w: bc.w, h: bc.h};
}
}
};
this.insertSubtree = insertSubtree;
/* quick 'n' dirty function for plugins or manually drawing the tree
* [ tree ] = RTree.getTree(): returns the raw tree data. useful for adding
* @public
* !! DEPRECATED !!
*/
this.getTree = function () {
return rootTree;
};
/* quick 'n' dirty function for plugins or manually loading the tree
* [ tree ] = RTree.setTree(sub-tree, where to attach): returns the raw tree data. useful for adding
* @public
* !! DEPRECATED !!
*/
this.setTree = function (newTree, where) {
if (!where) {
where = rootTree;
}
return attachData(where, newTree);
};
/* non-recursive search function
* [ nodes | objects ] = RTree.search(rectangle, [return node data], [array to fill])
* @public
*/
this.search = function (rect, returnNode, returnArray) {
returnArray = returnArray || [];
return searchSubtree(rect, returnNode, returnArray, rootTree);
};
var removeArea = function (rect) {
var numberDeleted = 1,
retArray = [],
deleted;
while (numberDeleted > 0) {
deleted = removeSubtree(rect, false, rootTree);
numberDeleted = deleted.length;
retArray = retArray.concat(deleted);
}
return retArray;
};
var removeObj = function (rect, obj) {
var retArray = removeSubtree(rect, obj, rootTree);
return retArray;
};
/* non-recursive delete function
* [deleted object] = RTree.remove(rectangle, [object to delete])
*/
this.remove = function (rect, obj) {
if (!obj || typeof obj === 'function') {
return removeArea(rect, obj);
} else {
return removeObj(rect, obj);
}
};
/* non-recursive insert function
* [] = RTree.insert(rectangle, object to insert)
*/
this.insert = function (rect, obj) {
var retArray = insertSubtree({x: rect.x, y: rect.y, w: rect.w, h: rect.h, leaf: obj}, rootTree);
return retArray;
};
}
RTree.prototype.toJSON = function (printing) {
return JSON.stringify(this.root, false, printing);
};
RTree.fromJSON = function (json) {
var rt = new RTree();
rt.setTree(JSON.parse(json));
return rt;
};
RTree.prototype.bbox = geojson.bbox;
RTree.prototype.geoJSON = geojson.geoJSON;
RTree.Rectangle = rectangle;
module.exports = RTree;
/**
* Polyfill for the Array.isArray function
* todo: Test on IE7 and IE8
* Taken from https://github.com/geraintluff/tv4/issues/20
*/
if (typeof Array.isArray !== 'function') {
Array.isArray = function (a) {
return typeof a === 'object' && {}.toString.call(a) === '[object Array]';
};
}