From 79e4ee27176c27d827ac73a3433931b8fbf1815d Mon Sep 17 00:00:00 2001 From: ahocevar Date: Sun, 19 May 2013 01:47:21 +0200 Subject: [PATCH] More sophisticated RTree implementation This new implementation is based on http://github.com/imbcmdth/RTree/, with only a few modifications to add the optional type and provide the API of the previous implementation. There is still room for optimization, but this is such an improvement over the previous RTree already that it's worth bringing it in now. --- src/ol/structs/rtree.js | 696 ++++++++++++++++++++++------- test/spec/ol/structs/rtree.test.js | 5 +- 2 files changed, 536 insertions(+), 165 deletions(-) diff --git a/src/ol/structs/rtree.js b/src/ol/structs/rtree.js index b56164f527..5c18b488b8 100644 --- a/src/ol/structs/rtree.js +++ b/src/ol/structs/rtree.js @@ -1,208 +1,582 @@ +/****************************************************************************** + 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.RTreeRectangle'); goog.require('goog.object'); -goog.require('ol.extent'); /** - * @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 tree = {x: 0, y: 0, w: 0, h: 0, nodes: []}; - /** - * @type {ol.Extent} + // 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 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; + }; + + /* find the best specific node(s) for object to be deleted from + * [ leaf node parent ] = removeSubtree(rectangle, object, root) + * @private */ - this.bounds = bounds; + 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; - /** - * @type {Object} + if (!rect || !ol.structs.RTreeRectangle.overlapRectangle(rect, root)) { + return returnArray; + } + + var workingObject = { + x: rect.x, y: rect.y, w: rect.w, h: rect.h, target: obj + }; + + countStack.push(root.nodes.length); + hitStack.push(root); + + do { + var tree = hitStack.pop(); + var i = countStack.pop() - 1; + + if ('target' in workingObject) { // We are searching for a target + while (i >= 0) { + var lTree = tree.nodes[i]; + if (ol.structs.RTreeRectangle.overlapRectangle( + workingObject, lTree)) { + if ((workingObject.target && 'leaf' in lTree && + lTree.leaf === workingObject.target) || + (!workingObject.target && ('leaf' in lTree || + ol.structs.RTreeRectangle.containsRectangle( + lTree, workingObject)))) { // 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... + returnArray = searchSubtree(lTree, true, [], lTree); + tree.nodes.splice(i, 1); + } else { + returnArray = tree.nodes.splice(i, 1); + } + // Resize MBR down... + ol.structs.RTreeRectangle.makeMBR(tree.nodes, tree); + delete workingObject.target; + if (tree.nodes.length < minWidth) { // Underflow + workingObject.nodes = searchSubtree(tree, true, [], tree); + } + break; + } else if ('nodes' in lTree) { // Not a Leaf + currentDepth += 1; + countStack.push(i); + hitStack.push(tree); + tree = lTree; + i = lTree.nodes.length; + } + } + i -= 1; + } + } else if ('nodes' in workingObject) { // 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) + ol.structs.RTreeRectangle.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 = + 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 = + searchSubtree(tree, true, workingObject.nodes, tree); + tree.nodes.length = 0; + } else { + delete workingObject.nodes; // Just start resizing + } + } else { // we are just resizing + ol.structs.RTreeRectangle.makeMBR(tree.nodes, tree); + } + currentDepth -= 1; + } while (hitStack.length > 0); + + return returnArray; + }; + + /* choose the best damn node for rectangle to be inserted into + * [ leaf node parent ] = chooseLeafSubtree(rectangle, root to start search + * at) + * @private */ - this.object; + var chooseLeafSubtree = function(rect, root) { + var bestChoiceIndex = -1; + var bestChoiceStack = []; + var bestChoiceArea; - /** - * @type {string} + 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 ('leaf' in lTree) { + // Bail out of everything and start inserting + bestChoiceIndex = -1; + break; + } + // Area of new enlarged rectangle + var oldLRatio = 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 = 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; + }; + + /* split a set of nodes into two roughly equally-filled nodes + * [ an array of two new arrays of nodes ] = linearSplit(array of nodes) + * @private */ - this.objectId; + var linearSplit = function(nodes) { + var n = pickLinear(nodes); + while (nodes.length > 0) { + pickNext(nodes, n[0], n[1]); + } + return n; + }; - /** - * @type {ol.structs.RTreeNode_} + /* insert the best source rectangle into the best fitting parent node: a or b + * [] = pick_next(array of source nodes, target node array a, target node + * array b) + * @private */ - this.parent = parent; + var pickNext = function(nodes, a, b) { + // Area of new enlarged rectangle + var areaA = squarifiedRatio(a.w, a.h, a.nodes.length + 1); + var areaB = squarifiedRatio(b.w, b.h, b.nodes.length + 1); + var highAreaDelta; + var highAreaNode; + var lowestGrowthGroup; - /** - * @type {number} + 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(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(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); + ol.structs.RTreeRectangle.expandRectangle(a, tempNode); + } else if (b.nodes.length + nodes.length + 1 <= minWidth) { + b.nodes.push(tempNode); + ol.structs.RTreeRectangle.expandRectangle(b, tempNode); + } + else { + lowestGrowthGroup.nodes.push(tempNode); + ol.structs.RTreeRectangle.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 */ - this.level = level; + var pickLinear = function(nodes) { + var lowestHighX = nodes.length - 1; + var highestLowX = 0; + var lowestHighY = nodes.length - 1; + var highestLowY = 0; + var t1, t2; - /** - * @type {Object.} - */ - this.types = {}; - - /** - * @type {Array.} - */ - this.children = []; - -}; - - -/** - * 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.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 { - 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 [ + {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]} + ]; + }; - -/** - * 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() { + 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; + }; /** - * @private - * @type {ol.structs.RTreeNode_} + * Non-recursive internal search function + * + * @param {Object} rect Rectangle. + * @param {boolean} returnNode Do we return nodes? + * @param {Array|Object} result Result. + * @param {Object} 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.structs.RTreeRectangle.overlapRectangle(rect, root)) { + 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.structs.RTreeRectangle.overlapRectangle(rect, lTree)) { + if ('nodes' in lTree) { // Not a Leaf + hitStack.push(lTree.nodes); + } else if ('leaf' in lTree) { // 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)] = 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 + * [] = 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 workingObject = node;//{x:rect.x,y:rect.y,w:rect.w,h:rect.h, leaf: obj}; + + // Walk back up the tree resizing and inserting as needed + do { + //handle the case of an empty node (from a split) + if (bc && 'nodes' in bc && 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 ('leaf' in workingObject || 'nodes' in workingObject || isArray) { + // Do Insert + if (isArray) { + for (var ai = 0, aii = workingObject.length; ai < aii; ++ai) { + ol.structs.RTreeRectangle.expandRectangle(bc, workingObject[ai]); + } + bc.nodes = bc.nodes.concat(workingObject); + } else { + ol.structs.RTreeRectangle.expandRectangle(bc, workingObject); + bc.nodes.push(workingObject); // Do Insert + } + + if (bc.nodes.length <= maxWidth) { // Start Resizeing Up the Tree + workingObject = {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); + 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.structs.RTreeRectangle.expandRectangle(bc, workingObject); + workingObject = {x: bc.x, y: bc.y, w: bc.w, h: bc.h}; + } + } while (treeStack.length > 0); + }; + + /** + * Non-recursive search function + * + * @param {Object} rect Rectangle. + * @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(rect, opt_type) { + rect = {x: rect[0], y: rect[2], w: rect[1] - rect[0], h: rect[3] - rect[2]}; + return searchSubtree.apply(this, [rect, false, {}, tree, opt_type]); + }; + + /* non-recursive function that deletes a specific + * [ number ] = RTree.remove(rectangle, obj) + */ + this.remove = function(rect, opt_obj) { + switch (arguments.length) { + case 1: + arguments[1] = false; // opt_obj == false for conditionals + case 2: + arguments[2] = tree; // Add root node to end of argument list + default: + arguments.length = 3; + } + if (arguments[1] === false) { // Do area-wide delete + 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 + * [] = RTree.put(rectangle, object to insert) + */ + this.put = function(rect, obj, opt_type) { + var node = { + x: rect[0], y: rect[2], w: rect[1] - rect[0], h: rect[3] - rect[2], + leaf: obj + }; if (goog.isDef(opt_type)) { - node.types[opt_type] = true; - found.types[opt_type] = true; + node.type = opt_type; } + insertSubtree(node, tree); + }; - if (found.children.length >= ol.structs.RTree.MAX_OBJECTS && - found.level < ol.structs.RTree.MAX_SUB_DIVISIONS) { - found.divide(); - } - } + //End of RTree }; /** - * @type {number} + * Returns true if rectangle 1 overlaps rectangle 2. + * + * @param {Object} a Rectangle A. + * @param {Object} b Rectangle B. + * @return {boolean} Does a overlap b? */ -ol.structs.RTree.MAX_SUB_DIVISIONS = 6; +ol.structs.RTreeRectangle.overlapRectangle = function(a, b) { + return a.x <= (b.x + b.w) && (a.x + a.w) >= b.x && a.y <= (b.y + b.h) && + (a.y + a.h) >= b.y; +}; /** - * @type {number} + * Returns true if rectangle a is contained in rectangle b. + * + * @param {Object} a Rectangle A. + * @param {Object} b Rectangle B. + * @return {boolean} Is a contained in b? */ -ol.structs.RTree.MAX_OBJECTS = 6; +ol.structs.RTreeRectangle.containsRectangle = function(a, b) { + return (a.x + a.w) <= (b.x + b.w) && a.x >= b.x && (a.y + a.h) <= + (b.y + b.h) && a.y >= b.y; +}; + + +/** + * Expands rectangle A to include rectangle B, rectangle B is untouched. + * + * @param {Object} a Rectangle A. + * @param {Object} b Rectangle B. + * @return {Object} Rectangle A. + */ +ol.structs.RTreeRectangle.expandRectangle = function(a, b) { + var nx = Math.min(a.x, b.x); + var ny = Math.min(a.y, b.y); + a.w = Math.max(a.x + a.w, b.x + b.w) - nx; + a.h = Math.max(a.y + a.h, b.y + b.h) - ny; + a.x = nx; + a.y = ny; + return a; +}; + + +/** + * Generates a minimally bounding rectangle for all rectangles in + * array "nodes". If rect is set, it is modified into the MBR. Otherwise, + * a new rectangle is generated and returned. + * + * @param {Array} nodes Nodes. + * @param {Object} rect Rectangle. + * @return {Object} Rectangle. + */ +ol.structs.RTreeRectangle.makeMBR = function(nodes, rect) { + if (nodes.length < 1) { + return {x: 0, y: 0, w: 0, h: 0}; + } + if (!rect) { + rect = {x: nodes[0].x, y: nodes[0].y, w: nodes[0].w, h: nodes[0].h}; + } + else { + rect.x = nodes[0].x; + rect.y = nodes[0].y; + rect.w = nodes[0].w; + rect.h = nodes[0].h; + } + + for (var i = nodes.length - 1; i > 0; --i) { + ol.structs.RTreeRectangle.expandRectangle(rect, nodes[i]); + } + + return rect; +}; diff --git a/test/spec/ol/structs/rtree.test.js b/test/spec/ol/structs/rtree.test.js index b2e774fcd5..f8fbb83e84 100644 --- a/test/spec/ol/structs/rtree.test.js +++ b/test/spec/ol/structs/rtree.test.js @@ -13,10 +13,7 @@ describe('ol.structs.RTree', function() { 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); + expect(goog.object.getCount(rTree.find([-100, 100, -100, 100]))).to.be(6); }); it('filters by rectangle', function() {