diff --git a/src/ol/extent.js b/src/ol/extent.js index dee6c57968..dc937c2df2 100644 --- a/src/ol/extent.js +++ b/src/ol/extent.js @@ -70,8 +70,7 @@ ol.extent.containsCoordinate = function(extent, coordinate) { /** - * Checks if the passed extent is contained or on the edge of the - * extent. + * Checks if `extent2` is contained by or on the edge of `extent1`. * * @param {ol.Extent} extent1 Extent 1. * @param {ol.Extent} extent2 Extent 2. diff --git a/src/ol/layer/vectorlayer.js b/src/ol/layer/vectorlayer.js index a3f5ca98cc..cb934c612e 100644 --- a/src/ol/layer/vectorlayer.js +++ b/src/ol/layer/vectorlayer.js @@ -102,8 +102,9 @@ ol.layer.FeatureCache.prototype.getFeaturesObject = function(opt_filter) { } } if (extentFilter && geometryFilter) { - features = this.rTree_.find( - extentFilter.getExtent(), geometryFilter.getType()); + var type = geometryFilter.getType(); + features = goog.object.isEmpty(this.geometryTypeIndex_[type]) ? {} : + this.rTree_.find(extentFilter.getExtent(), type); } } } diff --git a/src/ol/structs/rtree.js b/src/ol/structs/rtree.js index b56164f527..4500334be2 100644 --- a/src/ol/structs/rtree.js +++ b/src/ol/structs/rtree.js @@ -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.require('goog.object'); +goog.require('goog.array'); goog.require('ol.extent'); +/** + * @typedef {{extent: (ol.Extent), leaf: (Object|undefined), + * nodes: (Array.|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 - * @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.} nodes Array of nodes. + * @return {Array.>} 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.} + * Insert the best source rectangle into the best fitting parent node: a or b. + * + * @param {Array.} 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.} + * Pick the "best" two starter nodes to use as seeds using the "linear" + * criteria. + * + * @param {Array.} nodes Array of source nodes. + * @return {Array.} 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; -}; - - -/** - * Find all objects intersected by a rectangle. - * @param {ol.Extent} bounds Bounding box. - * @param {Object.} results Target object for results. - * @param {string=} opt_type Type for another indexing dimension. - */ -ol.structs.RTreeNode_.prototype.find = function(bounds, results, opt_type) { - if ((!goog.isDef(opt_type) || this.types[opt_type] === true) && - ol.extent.intersects(this.bounds, bounds)) { - var numChildren = this.children.length; - if (numChildren === 0) { - if (goog.isDef(this.object)) { - results[this.objectId] = this.object; + for (var i = nodes.length - 2; i >= 0; --i) { + var l = nodes[i]; + if (l.extent[0] > nodes[highestLowX].extent[0]) { + highestLowX = i; + } else if (l.extent[1] < nodes[lowestHighX].extent[2]) { + lowestHighX = i; + } + if (l.extent[2] > nodes[highestLowY].extent[2]) { + highestLowY = i; + } else if (l.extent[3] < nodes[lowestHighY].extent[3]) { + lowestHighY = i; + } + } + var dx = Math.abs(nodes[lowestHighX].extent[1] - + nodes[highestLowX].extent[0]); + 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 { - for (var i = 0; i < numChildren; ++i) { - this.children[i].find(bounds, results, opt_type); + 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 [ + /** @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. - * @param {ol.Extent} bounds Bounding box. - * @return {ol.structs.RTreeNode_|undefined} Matching node. - */ -ol.structs.RTreeNode_.prototype.get = function(bounds) { - if (ol.extent.intersects(this.bounds, bounds)) { - 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; - } -}; - - -/** - * Update boxes up to the root to ensure correct bounding - * @param {ol.Extent} bounds Bounding box. - */ -ol.structs.RTreeNode_.prototype.update = function(bounds) { - ol.extent.extend(this.bounds, bounds); - if (!goog.isNull(this.parent)) { - this.parent.update(bounds); - } -}; - - -/** - * 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; - } - - var half = Math.ceil(numChildren / 2), - child, node; - - for (var i = 0; i < numChildren; ++i) { - child = this.children[i]; - if (i % half === 0) { - node = new ol.structs.RTreeNode_( - child.bounds.slice(), this, this.level + 1); - goog.object.extend(this.types, node.types); - this.children.push(node); - } - 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); - } -}; - - - -/** - * @constructor - */ -ol.structs.RTree = function() { - /** - * @private - * @type {ol.structs.RTreeNode_} + * Non-recursive internal search function + * + * @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. */ - this.root_ = new ol.structs.RTreeNode_( - [-Infinity, Infinity, -Infinity, Infinity], null, 0); + var searchSubtree = function(rect, returnNode, result, root, opt_type) { + var hitStack = []; // Contains the elements that overlap -}; + if (!ol.extent.intersects(rect.extent, root.extent)) { + return result; + } + hitStack.push(root.nodes); -/** - * @param {ol.Extent} bounds Bounding box. - * @param {string=} opt_type Type for another indexing dimension. - * @return {Object.} Results for the passed bounding box. - */ -ol.structs.RTree.prototype.find = function(bounds, opt_type) { - var results = /** @type {Object.} */ ({}); - this.root_.find(bounds, results, opt_type); - return results; -}; + 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); -/** - * @param {ol.Extent} bounds Bounding box. - * @param {Object} object Object to store with the passed bounds. - * @param {string=} opt_type Type for another indexing dimension. - */ -ol.structs.RTree.prototype.put = function(bounds, object, opt_type) { - var found = this.root_.get(bounds); - if (found) { - var node = new ol.structs.RTreeNode_(bounds, found, found.level + 1); - node.object = object; - node.objectId = goog.getUid(object).toString(); + return result; + }; - found.children.push(node); - found.update(bounds); + /** + * Non-recursive internal insert function. + * + * @param {ol.structs.RTreeNode} node Node to insert. + * @param {ol.structs.RTreeNode} root Root to begin insertion at. + */ + 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.extent = node.extent.concat(); + 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 workingObject = node; + + // Walk back up the tree resizing and inserting as needed + do { + //handle the case of an empty node (from a split) + if (bc && goog.isDef(bc.nodes) && bc.nodes.length === 0) { + var pbc = bc; // Past bc + bc = treeStack.pop(); + 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; + } + } + } 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.. + bc.nodes.push(a[0]); + treeStack.push(bc); // Reconsider the root element + 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); + }; + + /** + * Non-recursive search function + * + * @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.find = function(extent, opt_type) { + 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 {Object=} opt_obj Object. + * @return {Array} Result. + * @this {ol.structs.RTree} + */ + this.remove = function(extent, opt_obj) { + arguments[0] = /** @type {ol.structs.RTreeNode} */ ({extent: extent}); + switch (arguments.length) { + 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} extent Extent. + * @param {Object} obj Object to insert. + * @param {string=} opt_type Optional type to store along with the object. + */ + this.put = function(extent, obj, opt_type) { + var node = /** @type {ol.structs.RTreeNode} */ + ({extent: extent, leaf: obj}); if (goog.isDef(opt_type)) { - node.types[opt_type] = true; - found.types[opt_type] = true; + node.type = opt_type; } + insertSubtree(node, rootTree); + }; - if (found.children.length >= ol.structs.RTree.MAX_OBJECTS && - found.level < ol.structs.RTree.MAX_SUB_DIVISIONS) { - found.divide(); - } - } + //End of RTree }; - - -/** - * @type {number} - */ -ol.structs.RTree.MAX_SUB_DIVISIONS = 6; - - -/** - * @type {number} - */ -ol.structs.RTree.MAX_OBJECTS = 6; diff --git a/test/spec/ol/structs/rtree.test.js b/test/spec/ol/structs/rtree.test.js index b2e774fcd5..e5ee9baef6 100644 --- a/test/spec/ol/structs/rtree.test.js +++ b/test/spec/ol/structs/rtree.test.js @@ -3,23 +3,94 @@ goog.provide('ol.test.structs.RTree'); describe('ol.structs.RTree', function() { - describe('put and find', function() { - var rTree = new ol.structs.RTree(); - rTree.put([0, 1, 0, 1], 1); - rTree.put([1, 4, 1, 4], 2); - rTree.put([2, 3, 2, 3], 3); - rTree.put([-5, -4, -5, -4], 4); - rTree.put([-4, -1, -4, -1], 5); - rTree.put([-3, -2, -3, -2], 6); + var rTree = new ol.structs.RTree(); - 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); + 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([1, 4, 1, 4], 2); + rTree.put([2, 3, 2, 3], 3); + rTree.put([-5, -4, -5, -4], 4); + rTree.put([-4, -1, -4, -1], 5); + rTree.put([-3, -2, -3, -2], 6); + var result; result = goog.object.getValues(rTree.find([2, 3, 2, 3])); 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); }); - 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); - }); - }); });