diff --git a/Makefile b/Makefile index 1110d4193..2afb6498b 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ dist/iD.js: \ js/lib/jxon.js \ js/lib/lodash.js \ js/lib/osmauth.js \ - js/lib/rtree.js \ + js/lib/rbush.js \ js/lib/togeojson.js \ js/lib/marked.js \ js/id/start.js \ diff --git a/index.html b/index.html index 6b49c7595..de2f1f51a 100644 --- a/index.html +++ b/index.html @@ -29,7 +29,7 @@ - + diff --git a/js/id/core/tree.js b/js/id/core/tree.js index dcf0fbd1e..84bda6872 100644 --- a/js/id/core/tree.js +++ b/js/id/core/tree.js @@ -1,26 +1,30 @@ iD.Tree = function(graph) { - var rtree = new RTree(), + var rtree = rbush(), m = 1000 * 1000 * 100, head = graph, queuedCreated = [], queuedModified = [], + rectangles = {}, x, y, dx, dy, rebased; function extentRectangle(extent) { - x = m * extent[0][0], - y = m * extent[0][1], - dx = Math.max(m * extent[1][0] - x, 1), - dy = Math.max(m * extent[1][1] - y, 1); - return new RTree.Rectangle(~~x, ~~y, ~~dx, ~~dy); + return [ + ~~(m * extent[0][0]), + ~~(m * extent[0][1]), + ~~(m * extent[1][0]), + ~~(m * extent[1][1]) + ]; } function insert(entity) { - rtree.insert(extentRectangle(entity.extent(head)), entity.id); + var rect = rectangles[entity.id] = extentRectangle(entity.extent(head)); + rect.id = entity.id; + rtree.insert(rect); } function remove(entity) { - rtree.remove(extentRectangle(entity.extent(graph)), entity.id); + rtree.remove(rectangles[entity.id]); } function reinsert(entity) { @@ -79,8 +83,9 @@ iD.Tree = function(graph) { rebased = false; } - return rtree.search(extentRectangle(extent)) - .map(function(id) { return graph.entity(id); }); + return rtree.search(extentRectangle(extent)).map(function (rect) { + return graph.entities[rect.id]; + }); }, graph: function() { diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 2e8fe8eee..75190e604 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -233,15 +233,15 @@ iD.svg.Labels = function(projection, context) { var mouse = context.mouse(), pad = 50, - rect = new RTree.Rectangle(mouse[0] - pad, mouse[1] - pad, 2*pad, 2*pad), - ids = _.pluck(rtree.search(rect, this), 'leaf'); + rect = [mouse[0] - pad, mouse[1] - pad, mouse[0] + pad, mouse[1] + pad], + ids = _.pluck(rtree.search(rect), 'id'); if (!ids.length) return; layers.selectAll('.' + ids.join(', .')) .classed('proximate', true); } - var rtree = new RTree(), + var rtree = rbush(), rectangles = {}; function labels(surface, graph, entities, filter, dimensions, fullRedraw) { @@ -252,11 +252,11 @@ iD.svg.Labels = function(projection, context) { for (i = 0; i < label_stack.length; i++) labelable.push([]); if (fullRedraw) { - rtree = new RTree(); + rtree.clear(); rectangles = {}; } else { for (i = 0; i < entities.length; i++) { - rtree.remove(rectangles[entities[i].id], entities[i].id); + rtree.remove(rectangles[entities[i].id]); } } @@ -325,7 +325,7 @@ iD.svg.Labels = function(projection, context) { y: coord[1] + offset[1], textAnchor: offset[2] }; - var rect = new RTree.Rectangle(p.x - m, p.y - m, width + 2*m, height + 2*m); + var rect = [p.x - m, p.y - m, p.x + width + m, p.y + height + m]; if (tryInsert(rect, entity.id)) return p; } @@ -342,12 +342,12 @@ iD.svg.Labels = function(projection, context) { if (start < 0 || start + width > length) continue; var sub = subpath(nodes, start, start + width), rev = reverse(sub), - rect = new RTree.Rectangle( - Math.min(sub[0][0], sub[sub.length - 1][0]) - 10, - Math.min(sub[0][1], sub[sub.length - 1][1]) - 10, - Math.abs(sub[0][0] - sub[sub.length - 1][0]) + 20, - Math.abs(sub[0][1] - sub[sub.length - 1][1]) + 30 - ); + rect = [ + Math.min(sub[0][0], sub[sub.length - 1][0]) - 10, + Math.min(sub[0][1], sub[sub.length - 1][1]) - 10, + Math.max(sub[0][0], sub[sub.length - 1][0]) + 20, + Math.max(sub[0][1], sub[sub.length - 1][1]) + 30 + ]; if (rev) sub = sub.reverse(); if (tryInsert(rect, entity.id)) return { 'font-size': height + 2, @@ -379,9 +379,9 @@ iD.svg.Labels = function(projection, context) { p.y = centroid[1] + textOffset; p.textAnchor = 'middle'; p.height = height; - rect = new RTree.Rectangle(p.x - width/2, p.y, width, height + textOffset); + rect = [p.x - width/2, p.y, p.x + width/2, p.y + height + textOffset]; } else { - rect = new RTree.Rectangle(iconX, iconY, iconSize, iconSize); + rect = [iconX, iconY, iconX + iconSize, iconY + iconSize]; } if (tryInsert(rect, entity.id)) return p; @@ -390,11 +390,12 @@ iD.svg.Labels = function(projection, context) { function tryInsert(rect, id) { // Check that label is visible - if (rect.x1 < 0 || rect.y1 < 0 || rect.x2 > dimensions[0] || - rect.y2 > dimensions[1]) return false; - var v = rtree.search(rect, true).length === 0; + if (rect[0] < 0 || rect[1] < 0 || rect[2] > dimensions[0] || + rect[3] > dimensions[1]) return false; + var v = rtree.search(rect).length === 0; if (v) { - rtree.insert(rect, id); + rect.id = id; + rtree.insert(rect); rectangles[id] = rect; } return v; diff --git a/js/lib/rbush.js b/js/lib/rbush.js new file mode 100644 index 000000000..4105e9951 --- /dev/null +++ b/js/lib/rbush.js @@ -0,0 +1,496 @@ +/* + (c) 2013, Vladimir Agafonkin + RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles. + https://github.com/mourner/rbush +*/ + +(function () { 'use strict'; + +function rbush(maxEntries, format) { + + // jshint newcap: false, validthis: true + if (!(this instanceof rbush)) { return new rbush(maxEntries, format); } + + this._maxEntries = Math.max(4, maxEntries || 9); + this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4)); + + this._initFormat(format); + + this.clear(); +} + +rbush.prototype = { + + search: function (bbox) { + + var node = this.data, + result = []; + + if (!this._intersects(bbox, node.bbox)) { return result; } + + var nodesToSearch = [], + i, len, child, childBBox; + + while (node) { + for (i = 0, len = node.children.length; i < len; i++) { + child = node.children[i]; + childBBox = node.leaf ? this._toBBox(child) : child.bbox; + + if (this._intersects(bbox, childBBox)) { + (node.leaf ? result : nodesToSearch).push(child); + } + } + + node = nodesToSearch.pop(); + } + + return result; + }, + + load: function (data) { + if (!(data && data.length)) { return this; } + + if (data.length < this._minEntries) { + for (var i = 0, len = data.length; i < len; i++) { + this.insert(data[i]); + } + return this; + } + + // recursively build the tree with the given data from stratch using OMT algorithm + var node = this._build(data.slice(), 0); + this._calcBBoxes(node, true); + + if (!this.data.children.length) { + // save as is if tree is empty + this.data = node; + + } else if (this.data.height === node.height) { + // split root if trees have the same height + this._splitRoot(this.data, node); + + } else { + if (this.data.height < node.height) { + // swap trees if inserted one is bigger + var tmpNode = this.data; + this.data = node; + node = tmpNode; + } + + // insert the small tree into the large tree at appropriate level + this._insert(node, this.data.height - node.height - 1, true); + } + + return this; + }, + + insert: function (item) { + if (item) { + this._insert(item, this.data.height - 1); + } + return this; + }, + + clear: function () { + this.data = { + children: [], + leaf: true, + bbox: this._infinite(), + height: 1 + }; + return this; + }, + + remove: function (item) { + if (!item) { return this; } + + var node = this.data, + bbox = this._toBBox(item), + path = [], + indexes = [], + i, parent, index, goingUp; + + // depth-first iterative tree traversal + while (node || path.length) { + + if (!node) { // go up + node = path.pop(); + parent = path[path.length - 1]; + i = indexes.pop(); + goingUp = true; + } + + if (node.leaf) { // check current node + index = node.children.indexOf(item); + + if (index !== -1) { + // item found, remove the item and condense tree upwards + node.children.splice(index, 1); + path.push(node); + this._condense(path); + return this; + } + } + + if (!goingUp && !node.leaf && this._intersects(bbox, node.bbox)) { // go down + path.push(node); + indexes.push(i); + i = 0; + parent = node; + node = node.children[0]; + + } else if (parent) { // go right + i++; + node = parent.children[i]; + goingUp = false; + + } else { // nothing found + node = null; + } + } + + return this; + }, + + toJSON: function () { return this.data; }, + + fromJSON: function (data) { + this.data = data; + return this; + }, + + _build: function (items, level, height) { + + var N = items.length, + M = this._maxEntries; + + if (N <= M) { + return { + children: items, + leaf: true, + height: 1 + }; + } + + if (!level) { + // target height of the bulk-loaded tree + height = Math.ceil(Math.log(N) / Math.log(M)); + + // target number of root entries to maximize storage utilization + M = Math.ceil(N / Math.pow(M, height - 1)); + + items.sort(this._compareMinX); + } + + // TODO eliminate recursion? + + var node = { + children: [], + height: height + }; + + var N1 = Math.ceil(N / M) * Math.ceil(Math.sqrt(M)), + N2 = Math.ceil(N / M), + compare = level % 2 === 1 ? this._compareMinX : this._compareMinY, + i, j, slice, sliceLen, childNode; + + // split the items into M mostly square tiles + for (i = 0; i < N; i += N1) { + slice = items.slice(i, i + N1).sort(compare); + + for (j = 0, sliceLen = slice.length; j < sliceLen; j += N2) { + // pack each entry recursively + childNode = this._build(slice.slice(j, j + N2), level + 1, height - 1); + node.children.push(childNode); + } + } + + return node; + }, + + _chooseSubtree: function (bbox, node, level, path) { + + var i, len, child, targetNode, area, enlargement, minArea, minEnlargement; + + while (true) { + path.push(node); + + if (node.leaf || path.length - 1 === level) { break; } + + minArea = minEnlargement = Infinity; + + for (i = 0, len = node.children.length; i < len; i++) { + child = node.children[i]; + area = this._area(child.bbox); + enlargement = this._enlargedArea(bbox, child.bbox) - area; + + // choose entry with the least area enlargement + if (enlargement < minEnlargement) { + minEnlargement = enlargement; + minArea = area < minArea ? area : minArea; + targetNode = child; + + } else if (enlargement === minEnlargement) { + // otherwise choose one with the smallest area + if (area < minArea) { + minArea = area; + targetNode = child; + } + } + } + + node = targetNode; + } + + return node; + }, + + _insert: function (item, level, isNode, root) { + + var bbox = isNode ? item.bbox : this._toBBox(item), + insertPath = []; + + // find the best node for accommodating the item, saving all nodes along the path too + var node = this._chooseSubtree(bbox, root || this.data, level, insertPath), + splitOccured; + + // put the item into the node + node.children.push(item); + this._extend(node.bbox, bbox); + + // split on node overflow; propagate upwards if necessary + do { + splitOccured = false; + if (insertPath[level].children.length > this._maxEntries) { + this._split(insertPath, level); + splitOccured = true; + level--; + } + } while (level >= 0 && splitOccured); + + // adjust bboxes along the insertion path + this._adjustParentBBoxes(bbox, insertPath, level); + }, + + // split overflowed node into two + _split: function (insertPath, level) { + + var node = insertPath[level], + M = node.children.length, + m = this._minEntries; + + this._chooseSplitAxis(node, m, M); + + var newNode = { + children: node.children.splice(this._chooseSplitIndex(node, m, M)), + height: node.height + }; + + if (node.leaf) { + newNode.leaf = true; + } + + this._calcBBoxes(node); + this._calcBBoxes(newNode); + + if (level) { + insertPath[level - 1].children.push(newNode); + } else { + this._splitRoot(node, newNode); + } + }, + + _splitRoot: function (node, newNode) { + // split root node + this.data = {}; + this.data.children = [node, newNode]; + this.data.height = node.height + 1; + this._calcBBoxes(this.data); + }, + + _chooseSplitIndex: function (node, m, M) { + + var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index; + + minOverlap = minArea = Infinity; + + for (i = m; i <= M - m; i++) { + bbox1 = this._distBBox(node, 0, i); + bbox2 = this._distBBox(node, i, M); + + overlap = this._intersectionArea(bbox1, bbox2); + area = this._area(bbox1) + this._area(bbox2); + + // choose distribution with minimum overlap + if (overlap < minOverlap) { + minOverlap = overlap; + index = i; + + minArea = area < minArea ? area : minArea; + + } else if (overlap === minOverlap) { + // otherwise choose distribution with minimum area + if (area < minArea) { + minArea = area; + index = i; + } + } + } + + return index; + }, + + // sorts node children by the best axis for split + _chooseSplitAxis: function (node, m, M) { + + var compareMinX = node.leaf ? this._compareMinX : this._compareNodeMinX, + compareMinY = node.leaf ? this._compareMinY : this._compareNodeMinY, + xMargin = this._allDistMargin(node, m, M, compareMinX), + yMargin = this._allDistMargin(node, m, M, compareMinY); + + // if total distributions margin value is minimal for x, sort by minX, + // otherwise it's already sorted by minY + + if (xMargin < yMargin) { + node.children.sort(compareMinX); + } + }, + + // total margin of all possible split distributions where each node is at least m full + _allDistMargin: function (node, m, M, compare) { + + node.children.sort(compare); + + var leftBBox = this._distBBox(node, 0, m), + rightBBox = this._distBBox(node, M - m, M), + margin = this._margin(leftBBox) + this._margin(rightBBox), + i, child; + + for (i = m; i < M - m; i++) { + child = node.children[i]; + this._extend(leftBBox, node.leaf ? this._toBBox(child) : child.bbox); + margin += this._margin(leftBBox); + } + + for (i = M - m - 1; i >= 0; i--) { + child = node.children[i]; + this._extend(rightBBox, node.leaf ? this._toBBox(child) : child.bbox); + margin += this._margin(rightBBox); + } + + return margin; + }, + + // min bounding rectangle of node children from k to p-1 + _distBBox: function (node, k, p) { + var bbox = this._infinite(); + + for (var i = k, child; i < p; i++) { + child = node.children[i]; + this._extend(bbox, node.leaf ? this._toBBox(child) : child.bbox); + } + + return bbox; + }, + + _calcBBoxes: function (node, recursive) { + // TODO eliminate recursion + node.bbox = this._infinite(); + + for (var i = 0, len = node.children.length, child; i < len; i++) { + child = node.children[i]; + + if (node.leaf) { + this._extend(node.bbox, this._toBBox(child)); + } else { + if (recursive) { + this._calcBBoxes(child, recursive); + } + this._extend(node.bbox, child.bbox); + } + } + }, + + _adjustParentBBoxes: function (bbox, path, level) { + // adjust bboxes along the given tree path + for (var i = level; i >= 0; i--) { + this._extend(path[i].bbox, bbox); + } + }, + + _condense: function (path) { + // go through the path, removing empty nodes and updating bboxes + for (var i = path.length - 1, parent; i >= 0; i--) { + if (i > 0 && path[i].children.length === 0) { + parent = path[i - 1].children; + parent.splice(parent.indexOf(path[i]), 1); + } else { + this._calcBBoxes(path[i]); + } + } + }, + + _intersects: function (a, b) { + return b[0] <= a[2] && + b[1] <= a[3] && + b[2] >= a[0] && + b[3] >= a[1]; + }, + + _extend: function (a, b) { + a[0] = Math.min(a[0], b[0]); + a[1] = Math.min(a[1], b[1]); + a[2] = Math.max(a[2], b[2]); + a[3] = Math.max(a[3], b[3]); + return a; + }, + + _area: function (a) { return (a[2] - a[0]) * (a[3] - a[1]); }, + _margin: function (a) { return (a[2] - a[0]) + (a[3] - a[1]); }, + + _enlargedArea: function (a, b) { + return (Math.max(b[2], a[2]) - Math.min(b[0], a[0])) * + (Math.max(b[3], a[3]) - Math.min(b[1], a[1])); + }, + + _intersectionArea: function (a, b) { + var minX = Math.max(a[0], b[0]), + minY = Math.max(a[1], b[1]), + maxX = Math.min(a[2], b[2]), + maxY = Math.min(a[3], b[3]); + + return Math.max(0, maxX - minX) * + Math.max(0, maxY - minY); + }, + + _infinite: function () { return [Infinity, Infinity, -Infinity, -Infinity]; }, + + _compareNodeMinX: function (a, b) { return a.bbox[0] - b.bbox[0]; }, + _compareNodeMinY: function (a, b) { return a.bbox[1] - b.bbox[1]; }, + + _initFormat: function (format) { + // data format (minX, minY, maxX, maxY accessors) + format = format || ['[0]', '[1]', '[2]', '[3]']; + + // uses eval-type function compilation instead of just accepting a toBBox function + // because the algorithms are very sensitive to sorting functions performance, + // so they should be dead simple and without inner calls + + // jshint evil: true + + var compareArr = ['return a', ' - b', ';']; + + this._compareMinX = new Function('a', 'b', compareArr.join(format[0])); + this._compareMinY = new Function('a', 'b', compareArr.join(format[1])); + + this._toBBox = new Function('a', 'return [a' + format.join(', a') + '];'); + } +}; + +if (typeof module !== 'undefined') { + module.exports = rbush; +} else { + window.rbush = rbush; +} + +})(); diff --git a/js/lib/rtree.js b/js/lib/rtree.js deleted file mode 100644 index 7f3b4a461..000000000 --- a/js/lib/rtree.js +++ /dev/null @@ -1,711 +0,0 @@ -/****************************************************************************** - rtree.js - General-Purpose Non-Recursive Javascript R-Tree Library - Version 0.6.2, December 5st 2009 - -@license 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 -******************************************************************************/ - -/** - * RTree - A simple r-tree structure for great results. - * @constructor - */ -var RTree = function(width){ - // Variables to control tree-dimensions - var _Min_Width = 3; // Minimum width of any node before a merge - var _Max_Width = 6; // Maximum width of any node before a split - if(!isNaN(width)){ _Min_Width = Math.floor(width/2.0); _Max_Width = width;} - // Start with an empty root-tree - var _T = {x:0, y:0, w:0, h:0, id:"root", nodes:[] }; - - var isArray = function(o) { - return Object.prototype.toString.call(o) === '[object Array]'; - }; - - /**@function - * @description Function to generate unique strings for element IDs - * @param {String} n The prefix to use for the IDs generated. - * @return {String} A guarenteed unique ID. - */ - var _name_to_id = (function() { - // hide our idCache inside this closure - var idCache = {}; - - // return the api: our function that returns a unique string with incrementing number appended to given idPrefix - return function(idPrefix) { - var idVal = 0; - if(idPrefix in idCache) { - idVal = idCache[idPrefix]++; - } else { - idCache[idPrefix] = 0; - } - return idPrefix + "_" + idVal; - } - })(); - - // 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 - RTree.Rectangle.squarified_ratio = function(l, w, fill) { - // Area of new enlarged rectangle - var lperi = (l + w) / 2.0; // Average size of a side of the new rectangle - var larea = 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 lgeo = larea / (lperi*lperi); - return(larea * fill / lgeo); - }; - - /**find the best specific node(s) for object to be deleted from - * [ leaf node parent ] = _remove_subtree(rectangle, object, root) - * @private - */ - var _remove_subtree = function(rect, obj, root) { - var hit_stack = []; // Contains the elements that overlap - var count_stack = []; // Contains the elements that overlap - var ret_array = []; - var current_depth = 1; - - if(!rect || !RTree.Rectangle.overlap_rectangle(rect, root)) - return ret_array; - - var ret_obj = {x:rect.x, y:rect.y, w:rect.w, h:rect.h, target:obj}; - - count_stack.push(root.nodes.length); - hit_stack.push(root); - - do { - var tree = hit_stack.pop(); - var i = count_stack.pop()-1; - - if("target" in ret_obj) { // We are searching for a target - while(i >= 0) { - var ltree = tree.nodes[i]; - if(RTree.Rectangle.overlap_rectangle(ret_obj, ltree)) { - if( (ret_obj.target && "leaf" in ltree && ltree.leaf === ret_obj.target) - ||(!ret_obj.target && ("leaf" in ltree || RTree.Rectangle.contains_rectangle(ltree, ret_obj)))) { // 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... - ret_array = _search_subtree(ltree, true, [], ltree); - tree.nodes.splice(i, 1); - } else { - ret_array = tree.nodes.splice(i, 1); - } - // Resize MBR down... - RTree.Rectangle.make_MBR(tree.nodes, tree); - delete ret_obj.target; - if(tree.nodes.length < _Min_Width) { // Underflow - ret_obj.nodes = _search_subtree(tree, true, [], tree); - } - break; - }/* else if("load" in ltree) { // A load - }*/ else if("nodes" in ltree) { // Not a Leaf - current_depth += 1; - count_stack.push(i); - hit_stack.push(tree); - tree = ltree; - i = ltree.nodes.length; - } - } - i -= 1; - } - } else if("nodes" in ret_obj) { // We are unsplitting - tree.nodes.splice(i+1, 1); // Remove unsplit node - // ret_obj.nodes contains a list of elements removed from the tree so far - if(tree.nodes.length > 0) - RTree.Rectangle.make_MBR(tree.nodes, tree); - for(var t = 0;t 0 && tree.nodes.length < _Min_Width) { // Underflow..AGAIN! - ret_obj.nodes = _search_subtree(tree, true, ret_obj.nodes, tree); - tree.nodes.length = 0; - }else { - delete ret_obj.nodes; // Just start resizing - } - } else { // we are just resizing - RTree.Rectangle.make_MBR(tree.nodes, tree); - } - current_depth -= 1; - }while(hit_stack.length > 0); - - return(ret_array); - }; - - /**choose the best damn node for rectangle to be inserted into - * [ leaf node parent ] = _choose_leaf_subtree(rectangle, root to start search at) - * @private - */ - var _choose_leaf_subtree = function(rect, root) { - var best_choice_index = -1; - var best_choice_stack = []; - var best_choice_area; - - var load_callback = function(local_tree, local_node){ - return(function(data) { - local_tree._attach_data(local_node, data); - }); - }; - - best_choice_stack.push(root); - var nodes = root.nodes; - - do { - if(best_choice_index != -1) { - best_choice_stack.push(nodes[best_choice_index]); - nodes = nodes[best_choice_index].nodes; - best_choice_index = -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 - best_choice_index = -1; - break; - } /*else if(ltree.load) { - throw( "Can't insert into partially loaded tree ... yet!"); - //jQuery.getJSON(ltree.load, load_callback(this, ltree)); - //delete ltree.load; - }*/ - // Area of new enlarged rectangle - var old_lratio = RTree.Rectangle.squarified_ratio(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 = RTree.Rectangle.squarified_ratio(nw, nh, ltree.nodes.length+2); - - if(best_choice_index < 0 || Math.abs(lratio - old_lratio) < best_choice_area) { - best_choice_area = Math.abs(lratio - old_lratio); best_choice_index = i; - } - } - }while(best_choice_index != -1); - - return(best_choice_stack); - }; - - /**split a set of nodes into two roughly equally-filled nodes - * [ an array of two new arrays of nodes ] = linear_split(array of nodes) - * @private - */ - var _linear_split = function(nodes) { - var n = _pick_linear(nodes); - while(nodes.length > 0) { - _pick_next(nodes, n[0], n[1]); - } - return(n); - }; - - /**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 - */ - var _pick_next = function(nodes, a, b) { - // Area of new enlarged rectangle - var area_a = RTree.Rectangle.squarified_ratio(a.w, a.h, a.nodes.length+1); - var area_b = RTree.Rectangle.squarified_ratio(b.w, b.h, b.nodes.length+1); - var high_area_delta; - var high_area_node; - var lowest_growth_group; - - for(var i = nodes.length-1; i>=0;i--) { - var l = nodes[i]; - var new_area_a = {}; - new_area_a.x = Math.min(a.x, l.x); new_area_a.y = Math.min(a.y, l.y); - new_area_a.w = Math.max(a.x+a.w, l.x+l.w) - new_area_a.x; new_area_a.h = Math.max(a.y+a.h, l.y+l.h) - new_area_a.y; - var change_new_area_a = Math.abs(RTree.Rectangle.squarified_ratio(new_area_a.w, new_area_a.h, a.nodes.length+2) - area_a); - - var new_area_b = {}; - new_area_b.x = Math.min(b.x, l.x); new_area_b.y = Math.min(b.y, l.y); - new_area_b.w = Math.max(b.x+b.w, l.x+l.w) - new_area_b.x; new_area_b.h = Math.max(b.y+b.h, l.y+l.h) - new_area_b.y; - var change_new_area_b = Math.abs(RTree.Rectangle.squarified_ratio(new_area_b.w, new_area_b.h, b.nodes.length+2) - area_b); - - if( !high_area_node || !high_area_delta || Math.abs( change_new_area_b - change_new_area_a ) < high_area_delta ) { - high_area_node = i; - high_area_delta = Math.abs(change_new_area_b-change_new_area_a); - lowest_growth_group = change_new_area_b < change_new_area_a ? b : a; - } - } - var temp_node = nodes.splice(high_area_node, 1)[0]; - if(a.nodes.length + nodes.length + 1 <= _Min_Width) { - a.nodes.push(temp_node); - RTree.Rectangle.expand_rectangle(a, temp_node); - } else if(b.nodes.length + nodes.length + 1 <= _Min_Width) { - b.nodes.push(temp_node); - RTree.Rectangle.expand_rectangle(b, temp_node); - } - else { - lowest_growth_group.nodes.push(temp_node); - RTree.Rectangle.expand_rectangle(lowest_growth_group, temp_node); - } - }; - - /**pick the "best" two starter nodes to use as seeds using the "linear" criteria - * [ an array of two new arrays of nodes ] = pick_linear(array of source nodes) - * @private - */ - var _pick_linear = function(nodes) { - var lowest_high_x = nodes.length-1; - var highest_low_x = 0; - var lowest_high_y = nodes.length-1; - var highest_low_y = 0; - var t1, t2; - - for(var i = nodes.length-2; i>=0;i--) { - var l = nodes[i]; - if(l.x > nodes[highest_low_x].x ) highest_low_x = i; - else if(l.x+l.w < nodes[lowest_high_x].x+nodes[lowest_high_x].w) lowest_high_x = i; - if(l.y > nodes[highest_low_y].y ) highest_low_y = i; - else if(l.y+l.h < nodes[lowest_high_y].y+nodes[lowest_high_y].h) lowest_high_y = i; - } - var dx = Math.abs((nodes[lowest_high_x].x+nodes[lowest_high_x].w) - nodes[highest_low_x].x); - var dy = Math.abs((nodes[lowest_high_y].y+nodes[lowest_high_y].h) - nodes[highest_low_y].y); - if( dx > dy ) { - if(lowest_high_x > highest_low_x) { - t1 = nodes.splice(lowest_high_x, 1)[0]; - t2 = nodes.splice(highest_low_x, 1)[0]; - } else { - t2 = nodes.splice(highest_low_x, 1)[0]; - t1 = nodes.splice(lowest_high_x, 1)[0]; - } - } else { - if(lowest_high_y > highest_low_y) { - t1 = nodes.splice(lowest_high_y, 1)[0]; - t2 = nodes.splice(highest_low_y, 1)[0]; - } else { - t2 = nodes.splice(highest_low_y, 1)[0]; - t1 = nodes.splice(lowest_high_y, 1)[0]; - } - } - return([{x:t1.x, y:t1.y, w:t1.w, h:t1.h, nodes:[t1]}, - {x:t2.x, y:t2.y, w:t2.w, h:t2.h, nodes:[t2]} ]); - }; - - var _attach_data = function(node, more_tree){ - node.nodes = more_tree.nodes; - node.x = more_tree.x; node.y = more_tree.y; - node.w = more_tree.w; node.h = more_tree.h; - return(node); - }; - - /**non-recursive internal search function - * [ nodes | objects ] = _search_subtree(rectangle, [return node data], [array to fill], root to begin search at) - * @private - */ - var _search_subtree = function(rect, return_node, return_array, root) { - var hit_stack = []; // Contains the elements that overlap - - if(!RTree.Rectangle.overlap_rectangle(rect, root)) - return(return_array); - - var load_callback = function(local_tree, local_node){ - return(function(data) { - local_tree._attach_data(local_node, data); - }); - }; - - hit_stack.push(root.nodes); - - do { - var nodes = hit_stack.pop(); - - for(var i = nodes.length-1; i >= 0; i--) { - var ltree = nodes[i]; - if(RTree.Rectangle.overlap_rectangle(rect, ltree)) { - if("nodes" in ltree) { // Not a Leaf - hit_stack.push(ltree.nodes); - } else if("leaf" in ltree) { // A Leaf !! - if(!return_node) - return_array.push(ltree.leaf); - else - return_array.push(ltree); - }/* else if("load" in ltree) { // We need to fetch a URL for some more tree data - jQuery.getJSON(ltree.load, load_callback(this, ltree)); - delete ltree.load; - // i++; // Replay this entry - }*/ - } - } - }while(hit_stack.length > 0); - - return(return_array); - }; - - /**non-recursive internal insert function - * [] = _insert_subtree(rectangle, object to insert, root to begin insertion at) - * @private - */ - var _insert_subtree = 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 - // choose_leaf returns an array of all tree levels (including root) - // that were traversed while trying to find the leaf - var tree_stack = _choose_leaf_subtree(node, root); - var ret_obj = 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 = tree_stack.pop(); - for(var t=0;t 0); - }; - - /**quick 'n' dirty function for plugins or manually drawing the tree - * [ tree ] = RTree.get_tree(): returns the raw tree data. useful for adding - * @public - * !! DEPRECATED !! - */ - this.get_tree = function() { - return _T; - }; - - /**quick 'n' dirty function for plugins or manually loading the tree - * [ tree ] = RTree.set_tree(sub-tree, where to attach): returns the raw tree data. useful for adding - * @public - * !! DEPRECATED !! - */ - this.set_tree = function(new_tree, where) { - if(!where) - where = _T; - return(_attach_data(where, new_tree)); - }; - - /**non-recursive search function - * [ nodes | objects ] = RTree.search(rectangle, [return node data], [array to fill]) - * @public - */ - this.search = function(rect, return_node, return_array) { - if(arguments.length < 1) - throw "Wrong number of arguments. RT.Search requires at least a bounding rectangle." - - switch(arguments.length) { - case 1: - arguments[1] = false;// Add an "return node" flag - may be removed in future - case 2: - arguments[2] = []; // Add an empty array to contain results - case 3: - arguments[3] = _T; // Add root node to end of argument list - default: - arguments.length = 4; - } - return(_search_subtree.apply(this, arguments)); - }; - - /**partially-recursive toJSON function - * [ string ] = RTree.toJSON([rectangle], [tree]) - * @public - */ - this.toJSON = function(rect, tree) { - var hit_stack = []; // Contains the elements that overlap - var count_stack = []; // Contains the elements that overlap - var return_stack = {}; // Contains the elements that overlap - var max_depth = 3; // This triggers recursion and tree-splitting - var current_depth = 1; - var return_string = ""; - - if(rect && !RTree.Rectangle.overlap_rectangle(rect, _T)) - return ""; - - if(!tree) { - count_stack.push(_T.nodes.length); - hit_stack.push(_T.nodes); - return_string += "var main_tree = {x:"+_T.x.toFixed()+",y:"+_T.y.toFixed()+",w:"+_T.w.toFixed()+",h:"+_T.h.toFixed()+",nodes:["; - } else { - max_depth += 4; - count_stack.push(tree.nodes.length); - hit_stack.push(tree.nodes); - return_string += "var main_tree = {x:"+tree.x.toFixed()+",y:"+tree.y.toFixed()+",w:"+tree.w.toFixed()+",h:"+tree.h.toFixed()+",nodes:["; - } - - do { - var nodes = hit_stack.pop(); - var i = count_stack.pop()-1; - - if(i >= 0 && i < nodes.length-1) - return_string += ","; - - while(i >= 0) { - var ltree = nodes[i]; - if(!rect || RTree.Rectangle.overlap_rectangle(rect, ltree)) { - if(ltree.nodes) { // Not a Leaf - if(current_depth >= max_depth) { - var len = return_stack.length; - var nam = _name_to_id("saved_subtree"); - return_string += "{x:"+ltree.x.toFixed()+",y:"+ltree.y.toFixed()+",w:"+ltree.w.toFixed()+",h:"+ltree.h.toFixed()+",load:'"+nam+".js'}"; - return_stack[nam] = this.toJSON(rect, ltree); - if(i > 0) - return_string += "," - } else { - return_string += "{x:"+ltree.x.toFixed()+",y:"+ltree.y.toFixed()+",w:"+ltree.w.toFixed()+",h:"+ltree.h.toFixed()+",nodes:["; - current_depth += 1; - count_stack.push(i); - hit_stack.push(nodes); - nodes = ltree.nodes; - i = ltree.nodes.length; - } - } else if(ltree.leaf) { // A Leaf !! - var data = ltree.leaf.toJSON ? ltree.leaf.toJSON() : JSON.stringify(ltree.leaf); - return_string += "{x:"+ltree.x.toFixed()+",y:"+ltree.y.toFixed()+",w:"+ltree.w.toFixed()+",h:"+ltree.h.toFixed()+",leaf:" + data + "}"; - if(i > 0) - return_string += "," - } else if(ltree.load) { // A load - return_string += "{x:"+ltree.x.toFixed()+",y:"+ltree.y.toFixed()+",w:"+ltree.w.toFixed()+",h:"+ltree.h.toFixed()+",load:'" + ltree.load + "'}"; - if(i > 0) - return_string += "," - } - } - i -= 1; - } - if(i < 0) { - return_string += "]}"; current_depth -= 1; - } - }while(hit_stack.length > 0); - - return_string+=";"; - - for(var my_key in return_stack) { - return_string += "\nvar " + my_key + " = function(){" + return_stack[my_key] + " return(main_tree);};"; - } - return(return_string); - }; - - /**non-recursive function that deletes a specific - * [ number ] = RTree.remove(rectangle, obj) - */ - this.remove = function(rect, obj) { - if(arguments.length < 1) - throw "Wrong number of arguments. RT.remove requires at least a bounding rectangle." - - switch(arguments.length) { - case 1: - arguments[1] = false; // obj == false for conditionals - case 2: - arguments[2] = _T; // Add root node to end of argument list - default: - arguments.length = 3; - } - if(arguments[1] === false) { // Do area-wide delete - var numberdeleted = 0; - var ret_array = []; - do { - numberdeleted=ret_array.length; - ret_array = ret_array.concat(_remove_subtree.apply(this, arguments)); - }while( numberdeleted != ret_array.length); - return ret_array; - } - else { // Delete a specific item - return(_remove_subtree.apply(this, arguments)); - } - }; - - /**non-recursive insert function - * [] = RTree.insert(rectangle, object to insert) - */ - this.insert = function(rect, obj) { -/* if(arguments.length < 2) - throw "Wrong number of arguments. RT.Insert requires at least a bounding rectangle and an object."*/ - - return(_insert_subtree({x:rect.x,y:rect.y,w:rect.w,h:rect.h,leaf:obj}, _T)); - }; - - /**non-recursive delete function - * [deleted object] = RTree.remove(rectangle, [object to delete]) - */ - -//End of RTree -}; - -/**Rectangle - Generic rectangle object - Not yet used */ - -RTree.Rectangle = function(ix, iy, iw, ih) { // new Rectangle(bounds) or new Rectangle(x, y, w, h) - var x, x2, y, y2, w, h; - - if(ix.x) { - x = ix.x; y = ix.y; - if(ix.w !== 0 && !ix.w && ix.x2){ - w = ix.x2-ix.x; h = ix.y2-ix.y; - } else { - w = ix.w; h = ix.h; - } - x2 = x + w; y2 = y + h; // For extra fastitude - } else { - x = ix; y = iy; w = iw; h = ih; - x2 = x + w; y2 = y + h; // For extra fastitude - } - - this.x1 = this.x = x; - this.y1 = this.y = y; - this.x2 = x2; - this.y2 = y2; - this.w = w; - this.h = h; - - this.toJSON = function() { - return('{"x":'+x.toString()+', "y":'+y.toString()+', "w":'+w.toString()+', "h":'+h.toString()+'}'); - }; - - this.overlap = function(a) { - return(this.x() < a.x2() && this.x2() > a.x() && this.y() < a.y2() && this.y2() > a.y()); - }; - - this.expand = function(a) { - var nx = Math.min(this.x(), a.x()); - var ny = Math.min(this.y(), a.y()); - w = Math.max(this.x2(), a.x2()) - nx; - h = Math.max(this.y2(), a.y2()) - ny; - x = nx; y = ny; - return(this); - }; - - this.setRect = function(ix, iy, iw, ih) { - var x, x2, y, y2, w, h; - if(ix.x) { - x = ix.x; y = ix.y; - if(ix.w !== 0 && !ix.w && ix.x2) { - w = ix.x2-ix.x; h = ix.y2-ix.y; - } else { - w = ix.w; h = ix.h; - } - x2 = x + w; y2 = y + h; // For extra fastitude - } else { - x = ix; y = iy; w = iw; h = ih; - x2 = x + w; y2 = y + h; // For extra fastitude - } - }; -//End of RTree.Rectangle -}; - - -/**returns true if rectangle 1 overlaps rectangle 2 - * [ boolean ] = overlap_rectangle(rectangle a, rectangle b) - * @static function - */ -RTree.Rectangle.overlap_rectangle = 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); -}; - -/**returns true if rectangle a is contained in rectangle b - * [ boolean ] = contains_rectangle(rectangle a, rectangle b) - * @static function - */ -RTree.Rectangle.contains_rectangle = 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 - * [ rectangle a ] = expand_rectangle(rectangle a, rectangle b) - * @static function - */ -RTree.Rectangle.expand_rectangle = 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. - * [ rectangle a ] = make_MBR(rectangle array nodes, rectangle rect) - * @static function - */ -RTree.Rectangle.make_MBR = function(nodes, rect) { - if(nodes.length < 1) - return({x:0, y:0, w:0, h:0}); - //throw "make_MBR: nodes must contain at least one rectangle!"; - 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--) - RTree.Rectangle.expand_rectangle(rect, nodes[i]); - - return(rect); -}; diff --git a/test/index.html b/test/index.html index 60105e915..ef21d8ba2 100644 --- a/test/index.html +++ b/test/index.html @@ -31,7 +31,7 @@ - + diff --git a/test/spec/core/tree.js b/test/spec/core/tree.js index c46f858f6..369a1358b 100644 --- a/test/spec/core/tree.js +++ b/test/spec/core/tree.js @@ -32,7 +32,7 @@ describe("iD.Tree", function() { expect(tree.intersects(iD.geo.Extent([0, 0], [1, 1]), g)).to.eql([]); var node = iD.Node({id: 'n', loc: [0.5, 0.5]}); g = tree.graph().replace(node); - expect(tree.intersects(iD.geo.Extent([0, 0], [1, 1]), g)).to.eql([way, node]); + expect(tree.intersects(iD.geo.Extent([0, 0], [1, 1]), g)).to.eql([node, way]); }); it("includes entities that used to have missing children, after rebase added them", function() { @@ -43,7 +43,7 @@ describe("iD.Tree", function() { var node = iD.Node({id: 'n', loc: [0.5, 0.5]}); base.rebase({ 'n': node }); tree.rebase(['n']); - expect(tree.intersects(iD.geo.Extent([0, 0], [1, 1]), g)).to.eql([way, node]); + expect(tree.intersects(iD.geo.Extent([0, 0], [1, 1]), g)).to.eql([node, way]); }); it("includes entities within extent, excludes those without", function() {