mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 01:02:58 +00:00
replace RTree with RBush
This commit is contained in:
2
Makefile
2
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 \
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<script src='js/lib/d3.value.js'></script>
|
||||
<script src='js/lib/d3-compat.js'></script>
|
||||
<script src='js/lib/bootstrap-tooltip.js'></script>
|
||||
<script src='js/lib/rtree.js'></script>
|
||||
<script src='js/lib/rbush.js'></script>
|
||||
<script src='js/lib/togeojson.js'></script>
|
||||
<script src='js/lib/marked.js'></script>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
496
js/lib/rbush.js
Normal file
496
js/lib/rbush.js
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
})();
|
||||
711
js/lib/rtree.js
711
js/lib/rtree.js
@@ -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<ret_obj.nodes.length;t++)
|
||||
_insert_subtree(ret_obj.nodes[t], tree);
|
||||
ret_obj.nodes.length = 0;
|
||||
if(hit_stack.length == 0 && tree.nodes.length <= 1) { // Underflow..on root!
|
||||
ret_obj.nodes = _search_subtree(tree, true, ret_obj.nodes, tree);
|
||||
tree.nodes.length = 0;
|
||||
hit_stack.push(tree);
|
||||
count_stack.push(1);
|
||||
} else if(hit_stack.length > 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<bc.nodes.length;t++)
|
||||
if(bc.nodes[t] === pbc || bc.nodes[t].nodes.length == 0) {
|
||||
bc.nodes.splice(t, 1);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
bc = tree_stack.pop();
|
||||
}
|
||||
|
||||
// If there is data attached to this ret_obj
|
||||
if("leaf" in ret_obj || "nodes" in ret_obj || isArray(ret_obj)) {
|
||||
// Do Insert
|
||||
if(isArray(ret_obj)) {
|
||||
for(var ai = 0; ai < ret_obj.length; ai++) {
|
||||
RTree.Rectangle.expand_rectangle(bc, ret_obj[ai]);
|
||||
}
|
||||
bc.nodes = bc.nodes.concat(ret_obj);
|
||||
} else {
|
||||
RTree.Rectangle.expand_rectangle(bc, ret_obj);
|
||||
bc.nodes.push(ret_obj); // Do Insert
|
||||
}
|
||||
|
||||
if(bc.nodes.length <= _Max_Width) { // Start Resizeing Up the Tree
|
||||
ret_obj = {x:bc.x,y:bc.y,w:bc.w,h:bc.h};
|
||||
} else { // Otherwise Split this Node
|
||||
// linear_split() returns an array containing two new nodes
|
||||
// formed from the split of the previous node's overflow
|
||||
var a = _linear_split(bc.nodes);
|
||||
ret_obj = a;//[1];
|
||||
|
||||
if(tree_stack.length < 1) { // If are splitting the root..
|
||||
bc.nodes.push(a[0]);
|
||||
tree_stack.push(bc); // Reconsider the root element
|
||||
ret_obj = a[1];
|
||||
} /*else {
|
||||
delete bc;
|
||||
}*/
|
||||
}
|
||||
} else { // Otherwise Do Resize
|
||||
//Just keep applying the new bounding rectangle to the parents..
|
||||
RTree.Rectangle.expand_rectangle(bc, ret_obj);
|
||||
ret_obj = {x:bc.x,y:bc.y,w:bc.w,h:bc.h};
|
||||
}
|
||||
} while(tree_stack.length > 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);
|
||||
};
|
||||
@@ -31,7 +31,7 @@
|
||||
<script src='../js/lib/d3.value.js'></script>
|
||||
<script src='../js/lib/d3-compat.js'></script>
|
||||
<script src='../js/lib/bootstrap-tooltip.js'></script>
|
||||
<script src='../js/lib/rtree.js'></script>
|
||||
<script src='../js/lib/rbush.js'></script>
|
||||
<script src='../js/lib/togeojson.js'></script>
|
||||
<script src='../js/lib/osmauth.js'></script>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user