// 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.array'); goog.require('ol.extent'); /** * @typedef {{extent: ol.Extent, * leaf: (Object|undefined), * nodes: (Array.|undefined), * target: (Object|undefined), * type: (string|undefined)}} */ ol.structs.RTreeNode; /** * @constructor * @param {number=} opt_maxWidth Width before a node is split. Default is 6. */ ol.structs.RTree = function(opt_maxWidth) { /** * Maximum width of any node before a split. * @private * @type {number} */ this.maxWidth_ = goog.isDef(opt_maxWidth) ? opt_maxWidth : 6; /** * Minimum width of any node before a merge. * @private * @type {number} */ this.minWidth_ = Math.floor(this.maxWidth_ / 2); /** * Start with an empty root-tree. * @private * @type {ol.structs.RTreeNode} */ this.rootTree_ = /** @type {ol.structs.RTreeNode} */ ({extent: ol.extent.createEmpty(), nodes: []}); }; /** * @param {ol.structs.RTreeNode} node Node. * @private */ ol.structs.RTree.recalculateExtent_ = function(node) { var n = node.nodes.length; var extent = node.extent; if (n === 0) { ol.extent.empty(extent); } else { var firstNodeExtent = node.nodes[0].extent; extent[0] = firstNodeExtent[0]; extent[1] = firstNodeExtent[1]; extent[2] = firstNodeExtent[2]; extent[3] = firstNodeExtent[3]; var i; for (i = 1; i < n; ++i) { ol.extent.extend(extent, node.nodes[i].extent); } } }; /** * This is Jon-Carlos Rivera's special addition to the world of r-trees. * Every other (simple) method he found produced poor trees. * This skews insertions to prefering squarer and emptier nodes. * * @param {number} l L. * @param {number} w W. * @param {number} fill Fill. * @private * @return {number} Squarified ratio. */ ol.structs.RTree.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; }; /** * Choose the best for rectangle to be inserted into. * * @param {ol.structs.RTreeNode} rect Rectangle. * @param {ol.structs.RTreeNode} root Root to start search. * @private * @return {Array} Leaf node parent. */ ol.structs.RTree.prototype.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 = ol.structs.RTree.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 = ol.structs.RTree.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; }; /** * 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. */ ol.structs.RTree.prototype.insert = function(extent, obj, opt_type) { var node = /** @type {ol.structs.RTreeNode} */ ({extent: extent, leaf: obj}); if (goog.isDef(opt_type)) { node.type = opt_type; } this.insertSubtree_(node, this.rootTree_); }; /** * Non-recursive internal insert function. * * @param {ol.structs.RTreeNode} node Node to insert. * @param {ol.structs.RTreeNode} root Root to begin insertion at. * @private */ ol.structs.RTree.prototype.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 = this.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 <= this.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 = this.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); }; /** * Split a set of nodes into two roughly equally-filled nodes. * * @param {Array.} nodes Array of nodes. * @private * @return {Array.} An array of two nodes. */ ol.structs.RTree.prototype.linearSplit_ = function(nodes) { var n = this.pickLinear_(nodes); while (nodes.length > 0) { this.pickNext_(nodes, n[0], n[1]); } return n; }; /** * Pick the "best" two starter nodes to use as seeds using the "linear" * criteria. * * @param {Array.} nodes Array of source nodes. * @private * @return {Array.} An array of two nodes. */ ol.structs.RTree.prototype.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; } 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 { 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]}) ]; }; /** * 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. * @private */ ol.structs.RTree.prototype.pickNext_ = function(nodes, a, b) { // Area of new enlarged rectangle var areaA = ol.structs.RTree.squarifiedRatio_(a.extent[1] - a.extent[0], a.extent[3] - a.extent[2], a.nodes.length + 1); var areaB = ol.structs.RTree.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(ol.structs.RTree.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(ol.structs.RTree.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 <= this.minWidth_) { a.nodes.push(tempNode); ol.extent.extend(a.extent, tempNode.extent); } else if (b.nodes.length + nodes.length + 1 <= this.minWidth_) { b.nodes.push(tempNode); ol.extent.extend(b.extent, tempNode.extent); } else { lowestGrowthGroup.nodes.push(tempNode); ol.extent.extend(lowestGrowthGroup.extent, tempNode.extent); } }; /** * 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} */ ol.structs.RTree.prototype.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] = this.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(this.removeSubtree_.apply(this, arguments)); } while (numberDeleted != result.length); return result; } else { // Delete a specific item return this.removeSubtree_.apply(this, arguments); } }; /** * 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. * @private * @return {Array} Leaf node parent. */ ol.structs.RTree.prototype.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 = this.searchSubtree_(lTree, true, [], lTree); tree.nodes.splice(i, 1); } else { returnArray = tree.nodes.splice(i, 1); } // Resize MBR down... ol.structs.RTree.recalculateExtent_(tree); workingObject.target = undefined; if (tree.nodes.length < this.minWidth_) { // Underflow workingObject.nodes = /** @type {Array} */ (this.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) { ol.structs.RTree.recalculateExtent_(tree); } for (var t = 0, tt = workingObject.nodes.length; t < tt; ++t) { this.insertSubtree_(workingObject.nodes[t], tree); } workingObject.nodes.length = 0; if (hitStack.length === 0 && tree.nodes.length <= 1) { // Underflow..on root! workingObject.nodes = /** @type {Array} */ (this.searchSubtree_(tree, true, workingObject.nodes, tree)); tree.nodes.length = 0; hitStack.push(tree); countStack.push(1); } else if (hitStack.length > 0 && tree.nodes.length < this.minWidth_) { // Underflow..AGAIN! workingObject.nodes = /** @type {Array} */ (this.searchSubtree_(tree, true, workingObject.nodes, tree)); tree.nodes.length = 0; } else { workingObject.nodes = undefined; // Just start resizing } } else { // we are just resizing ol.structs.RTree.recalculateExtent_(tree); } currentDepth -= 1; } while (hitStack.length > 0); return returnArray; }; /** * Non-recursive search function * * @param {ol.Extent} extent Extent. * @param {string=} opt_type Optional type of the objects we want to find. * @return {Array} Result. * @this {ol.structs.RTree} */ ol.structs.RTree.prototype.search = function(extent, opt_type) { var rect = /** @type {ol.structs.RTreeNode} */ ({extent: extent}); return /** @type {Array} */ ( this.searchSubtree_(rect, false, [], this.rootTree_, opt_type)); }; /** * 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} */ ol.structs.RTree.prototype.searchReturningObject = function(extent, opt_type) { var rect = /** @type {ol.structs.RTreeNode} */ ({extent: extent}); return /** @type {Object} */ ( this.searchSubtree_(rect, false, [], this.rootTree_, opt_type, true)); }; /** * 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. * @param {boolean=} opt_resultAsObject If set, result will be an object keyed * by UID. * @private * @return {Array|Object} Result. */ ol.structs.RTree.prototype.searchSubtree_ = function( rect, returnNode, result, root, opt_type, opt_resultAsObject) { var resultObject = {}; var hitStack = []; // Contains the elements that overlap if (!ol.extent.intersects(rect.extent, root.extent)) { return result; } hitStack.push(root.nodes); 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; if (goog.isDef(opt_resultAsObject)) { resultObject[goog.getUid(obj).toString()] = obj; } else { result.push(obj); } } } else { result.push(lTree); } } } } } while (hitStack.length > 0); if (goog.isDef(opt_resultAsObject)) { return resultObject; } else { return result; } };