491 lines
16 KiB
JavaScript
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]';
|
|
};
|
|
}
|