Merge pull request #719 from ahocevar/fast-rtree

More sophisticated RTree implementation. r=@tschaub
This commit is contained in:
ahocevar
2013-05-21 13:01:16 -07:00
4 changed files with 617 additions and 195 deletions

View File

@@ -70,8 +70,7 @@ ol.extent.containsCoordinate = function(extent, coordinate) {
/** /**
* Checks if the passed extent is contained or on the edge of the * Checks if `extent2` is contained by or on the edge of `extent1`.
* extent.
* *
* @param {ol.Extent} extent1 Extent 1. * @param {ol.Extent} extent1 Extent 1.
* @param {ol.Extent} extent2 Extent 2. * @param {ol.Extent} extent2 Extent 2.

View File

@@ -102,8 +102,9 @@ ol.layer.FeatureCache.prototype.getFeaturesObject = function(opt_filter) {
} }
} }
if (extentFilter && geometryFilter) { if (extentFilter && geometryFilter) {
features = this.rTree_.find( var type = geometryFilter.getType();
extentFilter.getExtent(), geometryFilter.getType()); features = goog.object.isEmpty(this.geometryTypeIndex_[type]) ? {} :
this.rTree_.find(extentFilter.getExtent(), type);
} }
} }
} }

View File

@@ -1,208 +1,579 @@
/******************************************************************************
rtree.js - General-Purpose Non-Recursive Javascript R-Tree Library
Version 0.6.2, December 5st 2009
Copyright (c) 2009 Jon-Carlos Rivera
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Jon-Carlos Rivera - imbcmdth@hotmail.com
******************************************************************************/
goog.provide('ol.structs.RTree'); goog.provide('ol.structs.RTree');
goog.require('goog.object'); goog.require('goog.array');
goog.require('ol.extent'); goog.require('ol.extent');
/**
* @typedef {{extent: (ol.Extent), leaf: (Object|undefined),
* nodes: (Array.<ol.structs.RTreeNode>|undefined),
* target: (Object|undefined), type: (string|undefined)}}
*/
ol.structs.RTreeNode;
/** /**
* @private * @param {number=} opt_width Width before a node is split. Default is 6.
* @constructor * @constructor
* @param {ol.Extent} bounds Extent.
* @param {ol.structs.RTreeNode_} parent Parent node.
* @param {number} level Level in the tree hierarchy.
*/ */
ol.structs.RTreeNode_ = function(bounds, parent, level) { ol.structs.RTree = function(opt_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(opt_width)) {
minWidth = Math.floor(opt_width / 2);
maxWidth = opt_width;
}
// Start with an empty root-tree
var rootTree = /** @type {ol.structs.RTreeNode} */
({extent: [0, 0, 0, 0], nodes: []});
/** /**
* @type {ol.Extent} * This is Jon-Carlos Rivera's special addition to the world of r-trees.
* Every other (simple) method he found produced crap trees.
* This skews insertions to prefering squarer and emptier nodes.
*
* @param {number} l L.
* @param {number} w W.
* @param {number} fill Fill.
* @return {number} Squarified ratio.
*/ */
this.bounds = bounds; var squarifiedRatio = function(l, w, fill) {
// Area of new enlarged rectangle
var peri = (l + w) / 2; // Average size of a side of the new rectangle
var area = l * w; // Area of new rectangle
// return the ratio of the perimeter to the area - the closer to 1 we are,
// the more "square" a rectangle is. conversly, when approaching zero the
// more elongated a rectangle is
var geo = area / (peri * peri);
return area * fill / geo;
};
/** /**
* @type {Object} * Generates a minimally bounding rectangle for all rectangles in
* array "nodes". `rect` is modified into the MBR.
*
* @param {Array} nodes Nodes.
* @param {ol.structs.RTreeNode} rect Rectangle.
* @return {ol.structs.RTreeNode} Rectangle.
*/ */
this.object; var makeMBR = function(nodes, rect) {
if (nodes.length < 1) {
return {extent: [0, 0, 0, 0]};
}
rect.extent = nodes[0].extent.concat();
for (var i = nodes.length - 1; i > 0; --i) {
ol.extent.extend(rect.extent, nodes[i].extent);
}
return rect;
};
/** /**
* @type {string} * Find the best specific node(s) for object to be deleted from.
*
* @param {ol.structs.RTreeNode} rect Rectangle.
* @param {Object} obj Object.
* @param {ol.structs.RTreeNode} root Root to start search.
* @return {Array} Leaf node parent.
*/ */
this.objectId; var removeSubtree = function(rect, obj, root) {
var hitStack = []; // Contains the elements that overlap
var countStack = []; // Contains the elements that overlap
var returnArray = [];
var currentDepth = 1;
if (!rect || !ol.extent.intersects(rect.extent, root.extent)) {
return returnArray;
}
/** @type {ol.structs.RTreeNode} */
var workingObject = /** @type {ol.structs.RTreeNode} */
({extent: rect.extent.concat(), target: obj});
countStack.push(root.nodes.length);
hitStack.push(root);
do {
var tree = hitStack.pop();
var i = countStack.pop() - 1;
if (goog.isDef(workingObject.target)) {
// We are searching for a target
while (i >= 0) {
var lTree = tree.nodes[i];
if (ol.extent.intersects(workingObject.extent, lTree.extent)) {
if ((workingObject.target && goog.isDef(lTree.leaf) &&
lTree.leaf === workingObject.target) ||
(!workingObject.target && (goog.isDef(lTree.leaf) ||
ol.extent.containsExtent(workingObject.extent, lTree.extent))))
{ // A Match !!
// Yup we found a match...
// we can cancel search and start walking up the list
if (goog.isDef(lTree.nodes)) {
// If we are deleting a node not a leaf...
returnArray = searchSubtree(lTree, true, [], lTree);
tree.nodes.splice(i, 1);
} else {
returnArray = tree.nodes.splice(i, 1);
}
// Resize MBR down...
makeMBR(tree.nodes, tree);
workingObject.target = undefined;
if (tree.nodes.length < minWidth) { // Underflow
workingObject.nodes = /** @type {Array} */
(searchSubtree(tree, true, [], tree));
}
break;
} else if (goog.isDef(lTree.nodes)) {
// Not a Leaf
currentDepth += 1;
countStack.push(i);
hitStack.push(tree);
tree = lTree;
i = lTree.nodes.length;
}
}
i -= 1;
}
} else if (goog.isDef(workingObject.nodes)) {
// We are unsplitting
tree.nodes.splice(i + 1, 1); // Remove unsplit node
// workingObject.nodes contains a list of elements removed from the
// tree so far
if (tree.nodes.length > 0) {
makeMBR(tree.nodes, tree);
}
for (var t = 0, tt = workingObject.nodes.length; t < tt; ++t) {
insertSubtree(workingObject.nodes[t], tree);
}
workingObject.nodes.length = 0;
if (hitStack.length === 0 && tree.nodes.length <= 1) {
// Underflow..on root!
workingObject.nodes = /** @type {Array} */
(searchSubtree(tree, true, workingObject.nodes, tree));
tree.nodes.length = 0;
hitStack.push(tree);
countStack.push(1);
} else if (hitStack.length > 0 && tree.nodes.length < minWidth) {
// Underflow..AGAIN!
workingObject.nodes = /** @type {Array} */
(searchSubtree(tree, true, workingObject.nodes, tree));
tree.nodes.length = 0;
} else {
workingObject.nodes = undefined; // Just start resizing
}
} else { // we are just resizing
makeMBR(tree.nodes, tree);
}
currentDepth -= 1;
} while (hitStack.length > 0);
return returnArray;
};
/** /**
* @type {ol.structs.RTreeNode_} * Choose the best damn node for rectangle to be inserted into.
*
* @param {ol.structs.RTreeNode} rect Rectangle.
* @param {ol.structs.RTreeNode} root Root to start search.
* @return {Array} Leaf node parent.
*/ */
this.parent = parent; var chooseLeafSubtree = function(rect, root) {
var bestChoiceIndex = -1;
var bestChoiceStack = [];
var bestChoiceArea;
bestChoiceStack.push(root);
var nodes = root.nodes;
do {
if (bestChoiceIndex != -1) {
bestChoiceStack.push(nodes[bestChoiceIndex]);
nodes = nodes[bestChoiceIndex].nodes;
bestChoiceIndex = -1;
}
for (var i = nodes.length - 1; i >= 0; --i) {
var lTree = nodes[i];
if (goog.isDef(lTree.leaf)) {
// Bail out of everything and start inserting
bestChoiceIndex = -1;
break;
}
// Area of new enlarged rectangle
var oldLRatio = squarifiedRatio(lTree.extent[1] - lTree.extent[0],
lTree.extent[3] - lTree.extent[2], lTree.nodes.length + 1);
// Enlarge rectangle to fit new rectangle
var nw = (lTree.extent[1] > rect.extent[1] ?
lTree.extent[1] : rect.extent[1]) -
(lTree.extent[0] < rect.extent[0] ?
lTree.extent[0] : rect.extent[0]);
var nh = (lTree.extent[3] > rect.extent[3] ?
lTree.extent[3] : rect.extent[3]) -
(lTree.extent[2] < rect.extent[2] ?
lTree.extent[2] : rect.extent[2]);
// Area of new enlarged rectangle
var lRatio = squarifiedRatio(nw, nh, lTree.nodes.length + 2);
if (bestChoiceIndex < 0 ||
Math.abs(lRatio - oldLRatio) < bestChoiceArea) {
bestChoiceArea = Math.abs(lRatio - oldLRatio);
bestChoiceIndex = i;
}
}
} while (bestChoiceIndex != -1);
return bestChoiceStack;
};
/** /**
* @type {number} * Split a set of nodes into two roughly equally-filled nodes.
*
* @param {Array.<ol.structs.RTreeNode>} nodes Array of nodes.
* @return {Array.<Array.<ol.structs.RTreeNode>>} An array of two new arrays
* of nodes.
*/ */
this.level = level; var linearSplit = function(nodes) {
var n = pickLinear(nodes);
while (nodes.length > 0) {
pickNext(nodes, n[0], n[1]);
}
return n;
};
/** /**
* @type {Object.<string, boolean>} * Insert the best source rectangle into the best fitting parent node: a or b.
*
* @param {Array.<ol.structs.RTreeNode>} nodes Source node array.
* @param {ol.structs.RTreeNode} a Target node array a.
* @param {ol.structs.RTreeNode} b Target node array b.
*/ */
this.types = {}; var pickNext = function(nodes, a, b) {
// Area of new enlarged rectangle
var areaA = squarifiedRatio(a.extent[1] - a.extent[0],
a.extent[3] - a.extent[2], a.nodes.length + 1);
var areaB = squarifiedRatio(b.extent[1] - b.extent[0],
b.extent[3] - b.extent[2], 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 = [
a.extent[0] < l.extent[0] ? a.extent[0] : l.extent[0],
a.extent[1] > l.extent[1] ? a.extent[1] : l.extent[1],
a.extent[2] < l.extent[2] ? a.extent[2] : l.extent[2],
a.extent[3] > l.extent[3] ? a.extent[3] : l.extent[3]
];
var changeNewAreaA = Math.abs(squarifiedRatio(newAreaA[1] - newAreaA[0],
newAreaA[3] - newAreaA[2], a.nodes.length + 2) - areaA);
var newAreaB = [
b.extent[0] < l.extent[0] ? b.extent[0] : l.extent[0],
b.extent[1] > l.extent[1] ? b.extent[1] : l.extent[1],
b.extent[2] < l.extent[2] ? b.extent[2] : l.extent[2],
b.extent[3] > l.extent[3] ? b.extent[3] : l.extent[3]
];
var changeNewAreaB = Math.abs(squarifiedRatio(
newAreaB[1] - newAreaB[0], newAreaB[3] - newAreaB[2],
b.nodes.length + 2) - areaB);
var changeNewAreaDelta = Math.abs(changeNewAreaB - changeNewAreaA);
if (!highAreaNode || !highAreaDelta ||
changeNewAreaDelta < highAreaDelta) {
highAreaNode = i;
highAreaDelta = changeNewAreaDelta;
lowestGrowthGroup = changeNewAreaB < changeNewAreaA ? b : a;
}
}
var tempNode = nodes.splice(highAreaNode, 1)[0];
if (a.nodes.length + nodes.length + 1 <= minWidth) {
a.nodes.push(tempNode);
ol.extent.extend(a.extent, tempNode.extent);
} else if (b.nodes.length + nodes.length + 1 <= minWidth) {
b.nodes.push(tempNode);
ol.extent.extend(b.extent, tempNode.extent);
}
else {
lowestGrowthGroup.nodes.push(tempNode);
ol.extent.extend(lowestGrowthGroup.extent, tempNode.extent);
}
};
/** /**
* @type {Array.<ol.structs.RTreeNode_>} * Pick the "best" two starter nodes to use as seeds using the "linear"
* criteria.
*
* @param {Array.<ol.structs.RTreeNode>} nodes Array of source nodes.
* @return {Array.<ol.structs.RTreeNode>} An array of two new arrays
* of nodes.
*/ */
this.children = []; 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.extent[0] > nodes[highestLowX].extent[0]) {
/** highestLowX = i;
* Find all objects intersected by a rectangle. } else if (l.extent[1] < nodes[lowestHighX].extent[2]) {
* @param {ol.Extent} bounds Bounding box. lowestHighX = i;
* @param {Object.<string, Object>} results Target object for results. }
* @param {string=} opt_type Type for another indexing dimension. if (l.extent[2] > nodes[highestLowY].extent[2]) {
*/ highestLowY = i;
ol.structs.RTreeNode_.prototype.find = function(bounds, results, opt_type) { } else if (l.extent[3] < nodes[lowestHighY].extent[3]) {
if ((!goog.isDef(opt_type) || this.types[opt_type] === true) && lowestHighY = i;
ol.extent.intersects(this.bounds, bounds)) { }
var numChildren = this.children.length; }
if (numChildren === 0) { var dx = Math.abs(nodes[lowestHighX].extent[1] -
if (goog.isDef(this.object)) { nodes[highestLowX].extent[0]);
results[this.objectId] = this.object; var dy = Math.abs(nodes[lowestHighY].extent[3] -
nodes[highestLowY].extent[2]);
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 { } else {
for (var i = 0; i < numChildren; ++i) { if (lowestHighY > highestLowY) {
this.children[i].find(bounds, results, opt_type); 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 [
}; /** @type {ol.structs.RTreeNode} */
({extent: t1.extent.concat(), nodes: [t1]}),
/** @type {ol.structs.RTreeNode} */
({extent: t2.extent.concat(), nodes: [t2]})
];
};
/** /**
* Find the appropriate node for insertion. * Non-recursive internal search function
* @param {ol.Extent} bounds Bounding box. *
* @return {ol.structs.RTreeNode_|undefined} Matching node. * @param {ol.structs.RTreeNode} rect Rectangle.
* @param {boolean} returnNode Do we return nodes?
* @param {Array|Object} result Result.
* @param {ol.structs.RTreeNode} root Root.
* @param {string=} opt_type Optional type to search for.
* @return {Array|Object} Result.
*/ */
ol.structs.RTreeNode_.prototype.get = function(bounds) { var searchSubtree = function(rect, returnNode, result, root, opt_type) {
if (ol.extent.intersects(this.bounds, bounds)) { var hitStack = []; // Contains the elements that overlap
var numChildren = this.children.length;
if (numChildren === 0) {
return goog.isNull(this.parent) ? this : this.parent;
}
var node;
for (var i = 0; i < numChildren; ++i) {
node = this.children[i].get(bounds);
if (goog.isDef(node)) {
return node;
}
}
return this;
}
};
if (!ol.extent.intersects(rect.extent, root.extent)) {
return result;
}
/** hitStack.push(root.nodes);
* Update boxes up to the root to ensure correct bounding
* @param {ol.Extent} bounds Bounding box. do {
var nodes = hitStack.pop();
for (var i = nodes.length - 1; i >= 0; --i) {
var lTree = nodes[i];
if (ol.extent.intersects(rect.extent, lTree.extent)) {
if (goog.isDef(lTree.nodes)) { // Not a Leaf
hitStack.push(lTree.nodes);
} else if (goog.isDef(lTree.leaf)) { // A Leaf !!
if (!returnNode) {
// TODO keep track of type on all nodes so we don't have to
// walk all the way in to the leaf to know that we don't need it
if (!goog.isDef(opt_type) || lTree.type == opt_type) {
var obj = lTree.leaf;
result[goog.getUid(obj).toString()] = obj;
}
} else {
result.push(lTree);
}
}
}
}
} while (hitStack.length > 0);
return result;
};
/**
* Non-recursive internal insert function.
*
* @param {ol.structs.RTreeNode} node Node to insert.
* @param {ol.structs.RTreeNode} root Root to begin insertion at.
*/ */
ol.structs.RTreeNode_.prototype.update = function(bounds) { var insertSubtree = function(node, root) {
ol.extent.extend(this.bounds, bounds); var bc; // Best Current node
if (!goog.isNull(this.parent)) { // Initial insertion is special because we resize the Tree and we don't
this.parent.update(bounds); // care about any overflow (seriously, how can the first object overflow?)
} if (root.nodes.length === 0) {
}; root.extent = node.extent.concat();
root.nodes.push(node);
/**
* Divide @this node's children in half and create two new boxes containing
* the split items. The top left will be the topmost leftmost child and the
* bottom right will be the rightmost bottommost child.
*/
ol.structs.RTreeNode_.prototype.divide = function() {
var numChildren = this.children.length;
if (numChildren === 0) {
return; return;
} }
var half = Math.ceil(numChildren / 2), // Find the best fitting leaf node
child, 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 workingObject = node;
for (var i = 0; i < numChildren; ++i) { // Walk back up the tree resizing and inserting as needed
child = this.children[i]; do {
if (i % half === 0) { //handle the case of an empty node (from a split)
node = new ol.structs.RTreeNode_( if (bc && goog.isDef(bc.nodes) && bc.nodes.length === 0) {
child.bounds.slice(), this, this.level + 1); var pbc = bc; // Past bc
goog.object.extend(this.types, node.types); bc = treeStack.pop();
this.children.push(node); for (var t = 0, tt = bc.nodes.length; t < tt; ++t) {
if (bc.nodes[t] === pbc || bc.nodes[t].nodes.length === 0) {
bc.nodes.splice(t, 1);
break;
} }
child.parent = /** @type {ol.structs.RTreeNode_} */ (node);
goog.object.extend(node.types, child.types);
node.children.push(child);
ol.extent.extend(node.bounds, child.bounds);
} }
}; } else {
bc = treeStack.pop();
}
// If there is data attached to this workingObject
var isArray = goog.isArray(workingObject);
if (goog.isDef(workingObject.leaf) ||
goog.isDef(workingObject.nodes) || isArray) {
// Do Insert
if (isArray) {
for (var ai = 0, aii = workingObject.length; ai < aii; ++ai) {
ol.extent.extend(bc.extent, workingObject[ai].extent);
}
bc.nodes = bc.nodes.concat(workingObject);
} else {
ol.extent.extend(bc.extent, workingObject.extent);
bc.nodes.push(workingObject); // Do Insert
}
if (bc.nodes.length <= maxWidth) { // Start Resizeing Up the Tree
workingObject = {extent: bc.extent.concat()};
} 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);
workingObject = a;//[1];
/** if (treeStack.length < 1) { // If are splitting the root..
* @constructor bc.nodes.push(a[0]);
*/ treeStack.push(bc); // Reconsider the root element
ol.structs.RTree = function() { workingObject = a[1];
}
}
} else { // Otherwise Do Resize
//Just keep applying the new bounding rectangle to the parents..
ol.extent.extend(bc.extent, workingObject.extent);
workingObject = ({extent: bc.extent.concat()});
}
} while (treeStack.length > 0);
};
/** /**
* @private * Non-recursive search function
* @type {ol.structs.RTreeNode_} *
* @param {ol.Extent} extent Extent.
* @param {string=} opt_type Optional type of the objects we want to find.
* @return {Object} Result. Keys are UIDs of the values.
* @this {ol.structs.RTree}
*/ */
this.root_ = new ol.structs.RTreeNode_( this.find = function(extent, opt_type) {
[-Infinity, Infinity, -Infinity, Infinity], null, 0); var rect = /** @type {ol.structs.RTreeNode} */ ({extent: extent});
return searchSubtree.apply(this, [rect, false, {}, rootTree, opt_type]);
};
}; /**
* Non-recursive function that deletes a specific region.
*
/** * @param {ol.Extent} extent Extent.
* @param {ol.Extent} bounds Bounding box. * @param {Object=} opt_obj Object.
* @param {string=} opt_type Type for another indexing dimension. * @return {Array} Result.
* @return {Object.<string, Object>} Results for the passed bounding box. * @this {ol.structs.RTree}
*/ */
ol.structs.RTree.prototype.find = function(bounds, opt_type) { this.remove = function(extent, opt_obj) {
var results = /** @type {Object.<string, Object>} */ ({}); arguments[0] = /** @type {ol.structs.RTreeNode} */ ({extent: extent});
this.root_.find(bounds, results, opt_type); switch (arguments.length) {
return results; case 1:
}; arguments[1] = false; // opt_obj == false for conditionals
case 2:
arguments[2] = rootTree; // Add root node to end of argument list
default:
arguments.length = 3;
}
if (arguments[1] === false) { // Do area-wide †
var numberDeleted = 0;
var result = [];
do {
numberDeleted = result.length;
result = result.concat(removeSubtree.apply(this, arguments));
} while (numberDeleted != result.length);
return result;
} else { // Delete a specific item
return removeSubtree.apply(this, arguments);
}
};
/**
/** * Non-recursive insert function.
* @param {ol.Extent} bounds Bounding box. *
* @param {Object} object Object to store with the passed bounds. * @param {ol.Extent} extent Extent.
* @param {string=} opt_type Type for another indexing dimension. * @param {Object} obj Object to insert.
* @param {string=} opt_type Optional type to store along with the object.
*/ */
ol.structs.RTree.prototype.put = function(bounds, object, opt_type) { this.put = function(extent, obj, opt_type) {
var found = this.root_.get(bounds); var node = /** @type {ol.structs.RTreeNode} */
if (found) { ({extent: extent, leaf: obj});
var node = new ol.structs.RTreeNode_(bounds, found, found.level + 1);
node.object = object;
node.objectId = goog.getUid(object).toString();
found.children.push(node);
found.update(bounds);
if (goog.isDef(opt_type)) { if (goog.isDef(opt_type)) {
node.types[opt_type] = true; node.type = opt_type;
found.types[opt_type] = true;
} }
insertSubtree(node, rootTree);
};
if (found.children.length >= ol.structs.RTree.MAX_OBJECTS && //End of RTree
found.level < ol.structs.RTree.MAX_SUB_DIVISIONS) {
found.divide();
}
}
}; };
/**
* @type {number}
*/
ol.structs.RTree.MAX_SUB_DIVISIONS = 6;
/**
* @type {number}
*/
ol.structs.RTree.MAX_OBJECTS = 6;

View File

@@ -3,8 +3,87 @@ goog.provide('ol.test.structs.RTree');
describe('ol.structs.RTree', function() { describe('ol.structs.RTree', function() {
describe('put and find', function() {
var rTree = new ol.structs.RTree(); var rTree = new ol.structs.RTree();
describe('creation', function() {
it('can insert 1k objects', function() {
var i = 1000;
while (i > 0) {
var bounds = new Array(4);
bounds[0] = Math.random() * 10000;
bounds[1] = bounds[0] + Math.random() * 500;
bounds[2] = Math.random() * 10000;
bounds[3] = bounds[2] + Math.random() * 500;
rTree.put(bounds, 'JUST A TEST OBJECT!_' + i);
i--;
}
expect(goog.object.getCount(rTree.find([0, 10600, 0, 10600])))
.to.be(1000);
});
it('can insert 1k more objects', function() {
var i = 1000;
while (i > 0) {
var bounds = new Array(4);
bounds[0] = Math.random() * 10000;
bounds[1] = bounds[0] + Math.random() * 500;
bounds[2] = Math.random() * 10000;
bounds[3] = bounds[2] + Math.random() * 500;
rTree.put(bounds, 'JUST A TEST OBJECT!_' + i);
i--;
}
expect(goog.object.getCount(rTree.find([0, 10600, 0, 10600])))
.to.be(2000);
});
});
describe('search', function() {
it('can perform 1k out-of-bounds searches', function() {
var i = 1000;
var len = 0;
while (i > 0) {
var bounds = new Array(4);
bounds[0] = -(Math.random() * 10000 + 501);
bounds[1] = bounds[0] + Math.random() * 500;
bounds[2] = -(Math.random() * 10000 + 501);
bounds[3] = bounds[2] + Math.random() * 500;
len += goog.object.getCount(rTree.find(bounds));
i--;
}
expect(len).to.be(0);
});
it('can perform 1k in-bounds searches', function() {
var i = 1000;
var len = 0;
while (i > 0) {
var bounds = new Array(4);
bounds[0] = -Math.random() * 10000 + 501;
bounds[1] = bounds[0] + Math.random() * 500;
bounds[2] = -Math.random() * 10000 + 501;
bounds[3] = bounds[2] + Math.random() * 500;
len += goog.object.getCount(rTree.find(bounds));
i--;
}
expect(len).not.to.be(0);
});
});
describe('deletion', function() {
var len = 0;
it('can delete half the RTree', function() {
var bounds = [5000, 10500, 0, 10500];
len += rTree.remove(bounds).length;
expect(len).to.not.be(0);
});
it('can delete the other half of the RTree', function() {
var bounds = [0, 5000, 0, 10500];
len += rTree.remove(bounds).length;
expect(len).to.be(2000);
});
});
describe('result plausibility', function() {
it('filters by rectangle', function() {
rTree.put([0, 1, 0, 1], 1); rTree.put([0, 1, 0, 1], 1);
rTree.put([1, 4, 1, 4], 2); rTree.put([1, 4, 1, 4], 2);
rTree.put([2, 3, 2, 3], 3); rTree.put([2, 3, 2, 3], 3);
@@ -12,14 +91,6 @@ describe('ol.structs.RTree', function() {
rTree.put([-4, -1, -4, -1], 5); rTree.put([-4, -1, -4, -1], 5);
rTree.put([-3, -2, -3, -2], 6); rTree.put([-3, -2, -3, -2], 6);
it('stores items', function() {
expect(goog.object.getCount(rTree.find([
Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY
]))).to.be(6);
});
it('filters by rectangle', function() {
var result; var result;
result = goog.object.getValues(rTree.find([2, 3, 2, 3])); result = goog.object.getValues(rTree.find([2, 3, 2, 3]));
expect(result).to.contain(2); expect(result).to.contain(2);
@@ -34,26 +105,6 @@ describe('ol.structs.RTree', function() {
expect(goog.object.getCount(rTree.find([5, 6, 5, 6]))).to.be(0); expect(goog.object.getCount(rTree.find([5, 6, 5, 6]))).to.be(0);
}); });
it('can store thousands of items and find fast', function() {
for (var i = 7; i <= 10000; ++i) {
rTree.put([
Math.random() * -10, Math.random() * 10,
Math.random() * -10, Math.random() * 10
], i);
}
expect(goog.object.getCount(rTree.find([-10, 10, -10, 10]))).to.be(10000);
var result = rTree.find([0, 0, 0, 0]);
expect(goog.object.getCount(result)).to.be(9995);
var values = goog.object.getValues(result);
expect(values).to.contain(1);
expect(values).not.to.contain(2);
expect(values).not.to.contain(3);
expect(values).not.to.contain(4);
expect(values).not.to.contain(5);
expect(values).not.to.contain(6);
expect(values).to.contain(7);
});
}); });
}); });