Created
March 14, 2017 11:09
-
-
Save iursevla/90b7e35b04cbcad90e12744a20b159fd to your computer and use it in GitHub Desktop.
rtree
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Exports a `PolygonLookup` constructor, which constructs a data-structure for | |
* quickly finding the polygon that a point intersects in a (potentially very | |
* large) set. | |
*/ | |
var pointInPolygon = function (point, vs) { | |
// ray-casting algorithm based on | |
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html | |
var x = point[0], y = point[1]; | |
var inside = false; | |
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) { | |
var xi = vs[i][0], yi = vs[i][1]; | |
var xj = vs[j][0], yj = vs[j][1]; | |
var intersect = ((yi > y) != (yj > y)) | |
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi); | |
if (intersect) inside = !inside; | |
} | |
return inside; | |
}; | |
/** | |
* @property {rbush} rtree A spatial index for `this.polygons`. | |
* @property {object} polgons A GeoJSON feature collection. | |
* | |
* @param {object} [featureCollection] An optional GeoJSON feature collection | |
* to pass to `loadFeatureCollection()`. | |
*/ | |
function PolygonLookup( featureCollection ){ | |
this.search = function search( x, y ){ | |
var bboxes = this.rtree.search( [ x, y, x, y ] ); | |
var pt = [ x, y ]; | |
for( var ind = 0; ind < bboxes.length; ind++ ){ | |
var polyObj = this.polygons[ bboxes[ ind ].polyId ]; | |
var polyCoords = polyObj.geometry.coordinates[ 0 ]; | |
if( pointInPolygon( pt, polyCoords ) ){ | |
var inHole = false; | |
for( var subPolyInd = 1; subPolyInd < polyObj.geometry.coordinates.length; subPolyInd++ ){ | |
if( pointInPolygon( pt, polyObj.geometry.coordinates[ subPolyInd ] ) ){ | |
inHole = true; | |
break; | |
} | |
} | |
if( !inHole ){ | |
return polyObj; | |
} | |
} | |
} | |
}; | |
this.loadFeatureCollection = function loadFeatureCollection( collection ){ | |
var bboxes = []; | |
var polygons = []; | |
var polyId = 0; | |
function getBoundingBox( poly ){ | |
var firstPt = poly[ 0 ]; | |
var bbox = [ | |
firstPt[ 0 ], firstPt[ 1 ], | |
firstPt[ 0 ], firstPt[ 1 ] | |
]; | |
for( var ind = 1; ind < poly.length; ind++ ){ | |
var pt = poly[ ind ]; | |
var x = pt[ 0 ]; | |
if( x < bbox[ 0 ] ){ | |
bbox[ 0 ] = x; | |
} | |
else if( x > bbox[ 2 ] ){ | |
bbox[ 2 ] = x; | |
} | |
var y = pt[ 1 ]; | |
if( y < bbox[ 1 ] ){ | |
bbox[ 1 ] = y; | |
} | |
else if( y > bbox[ 3 ] ){ | |
bbox[ 3 ] = y; | |
} | |
} | |
return bbox; | |
} | |
function indexPolygon( poly ){ | |
polygons.push(poly); | |
var bbox = getBoundingBox( poly.geometry.coordinates[ 0 ] ); | |
bbox.polyId = polyId++; | |
bboxes.push(bbox); | |
} | |
function indexFeature( poly ){ | |
if( poly.geometry.coordinates[ 0 ] !== undefined && | |
poly.geometry.coordinates[ 0 ].length > 0){ | |
switch( poly.geometry.type ){ | |
case 'Polygon': | |
indexPolygon( poly ); | |
break; | |
case 'MultiPolygon': | |
var childPolys = poly.geometry.coordinates; | |
for( var ind = 0; ind < childPolys.length; ind++ ){ | |
var childPoly = { | |
type: 'Feature', | |
properties: poly.properties, | |
geometry: { | |
type: 'Polygon', | |
coordinates: childPolys[ ind ] | |
} | |
}; | |
indexPolygon( childPoly ); | |
} | |
break; | |
} | |
} | |
} | |
var rBush = (function rbush() { | |
this.RBush = function(maxEntries, format) { | |
// jshint newcap: false, validthis: true | |
if (!(this instanceof RBush)) return new RBush(maxEntries, format); | |
// max entries in a node is 9 by default; min node fill is 40% for best performance | |
this._maxEntries = Math.max(4, maxEntries || 9); | |
this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4)); | |
if (format) { | |
this._initFormat(format); | |
} | |
this.clear(); | |
} | |
RBush.prototype = { | |
all: function () { | |
return this._all(this.data, []); | |
}, | |
search: function (bbox) { | |
var node = this.data, | |
result = [], | |
toBBox = this.toBBox; | |
if (!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 ? toBBox(child) : child.bbox; | |
if (intersects(bbox, childBBox)) { | |
if (node.leaf) result.push(child); | |
else if (contains(bbox, childBBox)) this._all(child, result); | |
else 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, data.length - 1, 0); | |
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: [], | |
height: 1, | |
bbox: empty(), | |
leaf: true | |
}; | |
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 && contains(node.bbox, 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 node = null; // nothing found | |
} | |
return this; | |
}, | |
toBBox: function (item) { return item; }, | |
compareMinX: function (a, b) { return a[0] - b[0]; }, | |
compareMinY: function (a, b) { return a[1] - b[1]; }, | |
toJSON: function () { return this.data; }, | |
fromJSON: function (data) { | |
this.data = data; | |
return this; | |
}, | |
_all: function (node, result) { | |
var nodesToSearch = []; | |
while (node) { | |
if (node.leaf) result.push.apply(result, node.children); | |
else nodesToSearch.push.apply(nodesToSearch, node.children); | |
node = nodesToSearch.pop(); | |
} | |
return result; | |
}, | |
_build: function (items, left, right, height) { | |
var N = right - left + 1, | |
M = this._maxEntries, | |
node; | |
if (N <= M) { | |
// reached leaf level; return leaf | |
node = { | |
children: items.slice(left, right + 1), | |
height: 1, | |
bbox: null, | |
leaf: true | |
}; | |
calcBBox(node, this.toBBox); | |
return node; | |
} | |
if (!height) { | |
// 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)); | |
} | |
// TODO eliminate recursion? | |
node = { | |
children: [], | |
height: height, | |
bbox: null | |
}; | |
// split the items into M mostly square tiles | |
var N2 = Math.ceil(N / M), | |
N1 = N2 * Math.ceil(Math.sqrt(M)), | |
i, j, right2, right3; | |
multiSelect(items, left, right, N1, this.compareMinX); | |
for (i = left; i <= right; i += N1) { | |
right2 = Math.min(i + N1 - 1, right); | |
multiSelect(items, i, right2, N2, this.compareMinY); | |
for (j = i; j <= right2; j += N2) { | |
right3 = Math.min(j + N2 - 1, right2); | |
// pack each entry recursively | |
node.children.push(this._build(items, j, right3, height - 1)); | |
} | |
} | |
calcBBox(node, this.toBBox); | |
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 = bboxArea(child.bbox); | |
enlargement = 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) { | |
var toBBox = this.toBBox, | |
bbox = isNode ? item.bbox : toBBox(item), | |
insertPath = []; | |
// find the best node for accommodating the item, saving all nodes along the path too | |
var node = this._chooseSubtree(bbox, this.data, level, insertPath); | |
// put the item into the node | |
node.children.push(item); | |
extend(node.bbox, bbox); | |
// split on node overflow; propagate upwards if necessary | |
while (level >= 0) { | |
if (insertPath[level].children.length > this._maxEntries) { | |
this._split(insertPath, level); | |
level--; | |
} else break; | |
} | |
// 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; | |
calcBBox(node, this.toBBox); | |
calcBBox(newNode, this.toBBox); | |
if (level) insertPath[level - 1].children.push(newNode); | |
else this._splitRoot(node, newNode); | |
}, | |
_splitRoot: function (node, newNode) { | |
// split root node | |
this.data = { | |
children: [node, newNode], | |
height: node.height + 1 | |
}; | |
calcBBox(this.data, this.toBBox); | |
}, | |
_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 = distBBox(node, 0, i, this.toBBox); | |
bbox2 = distBBox(node, i, M, this.toBBox); | |
overlap = intersectionArea(bbox1, bbox2); | |
area = bboxArea(bbox1) + bboxArea(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 : compareNodeMinX, | |
compareMinY = node.leaf ? this.compareMinY : 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 toBBox = this.toBBox, | |
leftBBox = distBBox(node, 0, m, toBBox), | |
rightBBox = distBBox(node, M - m, M, toBBox), | |
margin = bboxMargin(leftBBox) + bboxMargin(rightBBox), | |
i, child; | |
for (i = m; i < M - m; i++) { | |
child = node.children[i]; | |
extend(leftBBox, node.leaf ? toBBox(child) : child.bbox); | |
margin += bboxMargin(leftBBox); | |
} | |
for (i = M - m - 1; i >= m; i--) { | |
child = node.children[i]; | |
extend(rightBBox, node.leaf ? toBBox(child) : child.bbox); | |
margin += bboxMargin(rightBBox); | |
} | |
return margin; | |
}, | |
_adjustParentBBoxes: function (bbox, path, level) { | |
// adjust bboxes along the given tree path | |
for (var i = level; i >= 0; i--) { | |
extend(path[i].bbox, bbox); | |
} | |
}, | |
_condense: function (path) { | |
// go through the path, removing empty nodes and updating bboxes | |
for (var i = path.length - 1, siblings; i >= 0; i--) { | |
if (path[i].children.length === 0) { | |
if (i > 0) { | |
siblings = path[i - 1].children; | |
siblings.splice(siblings.indexOf(path[i]), 1); | |
} else this.clear(); | |
} else calcBBox(path[i], this.toBBox); | |
} | |
}, | |
_initFormat: function (format) { | |
// data format (minX, minY, maxX, maxY accessors) | |
// 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') + '];'); | |
} | |
}; | |
// calculate node's bbox from bboxes of its children | |
function calcBBox(node, toBBox) { | |
node.bbox = distBBox(node, 0, node.children.length, toBBox); | |
} | |
// min bounding rectangle of node children from k to p-1 | |
function distBBox(node, k, p, toBBox) { | |
var bbox = empty(); | |
for (var i = k, child; i < p; i++) { | |
child = node.children[i]; | |
extend(bbox, node.leaf ? toBBox(child) : child.bbox); | |
} | |
return bbox; | |
} | |
function empty() { return [Infinity, Infinity, -Infinity, -Infinity]; } | |
function extend(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; | |
} | |
function compareNodeMinX(a, b) { return a.bbox[0] - b.bbox[0]; } | |
function compareNodeMinY(a, b) { return a.bbox[1] - b.bbox[1]; } | |
function bboxArea(a) { return (a[2] - a[0]) * (a[3] - a[1]); } | |
function bboxMargin(a) { return (a[2] - a[0]) + (a[3] - a[1]); } | |
function enlargedArea(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])); | |
} | |
function intersectionArea (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); | |
} | |
function contains(a, b) { | |
return a[0] <= b[0] && | |
a[1] <= b[1] && | |
b[2] <= a[2] && | |
b[3] <= a[3]; | |
} | |
function intersects (a, b) { | |
return b[0] <= a[2] && | |
b[1] <= a[3] && | |
b[2] >= a[0] && | |
b[3] >= a[1]; | |
} | |
// sort an array so that items come in groups of n unsorted items, with groups sorted between each other; | |
// combines selection algorithm with binary divide & conquer approach | |
function multiSelect(arr, left, right, n, compare) { | |
var stack = [left, right], | |
mid; | |
while (stack.length) { | |
right = stack.pop(); | |
left = stack.pop(); | |
if (right - left <= n) continue; | |
mid = left + Math.ceil((right - left) / n / 2) * n; | |
select(arr, left, right, mid, compare); | |
stack.push(left, mid, mid, right); | |
} | |
} | |
// sort array between left and right (inclusive) so that the smallest k elements come first (unordered) | |
function select(arr, left, right, k, compare) { | |
var n, i, z, s, sd, newLeft, newRight, t, j; | |
while (right > left) { | |
if (right - left > 600) { | |
n = right - left + 1; | |
i = k - left + 1; | |
z = Math.log(n); | |
s = 0.5 * Math.exp(2 * z / 3); | |
sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (i - n / 2 < 0 ? -1 : 1); | |
newLeft = Math.max(left, Math.floor(k - i * s / n + sd)); | |
newRight = Math.min(right, Math.floor(k + (n - i) * s / n + sd)); | |
select(arr, newLeft, newRight, k, compare); | |
} | |
t = arr[k]; | |
i = left; | |
j = right; | |
swap(arr, left, k); | |
if (compare(arr[right], t) > 0) swap(arr, left, right); | |
while (i < j) { | |
swap(arr, i, j); | |
i++; | |
j--; | |
while (compare(arr[i], t) < 0) i++; | |
while (compare(arr[j], t) > 0) j--; | |
} | |
if (compare(arr[left], t) === 0) swap(arr, left, j); | |
else { | |
j++; | |
swap(arr, j, right); | |
} | |
if (j <= k) left = j + 1; | |
if (k <= j) right = j - 1; | |
} | |
} | |
function swap(arr, i, j) { | |
var tmp = arr[i]; | |
arr[i] = arr[j]; | |
arr[j] = tmp; | |
} | |
// export as AMD/CommonJS module or global variable | |
if (typeof define === 'function' && define.amd) define(function() { return rbush; }); | |
else if (typeof module !== 'undefined') module.exports = rbush; | |
else if (typeof self !== 'undefined') self.rbush = rbush; | |
else window.rbush = rbush; | |
})(); | |
collection.features.forEach( indexFeature ); | |
this.rtree = (new rbush()).RBush().load( bboxes ); | |
this.polygons = polygons; | |
}; | |
if( featureCollection !== undefined ){ | |
this.loadFeatureCollection( featureCollection ); | |
} | |
} | |
/** | |
* Find the polygon that a point intersects. Execute a bounding-box search to | |
* narrow down the candidate polygons to a small subset, and then perform | |
* additional point-in-polygon intersections to resolve any ambiguities. | |
* | |
* @param {number} x The x-coordinate of the point. | |
* @param {number} y The y-coordinate of the point. | |
* @return {undefined|object} If one or more bounding box intersections are | |
* found, return the first polygon that intersects (`x`, `y`); otherwise, | |
* `undefined`. | |
* | |
PolygonLookup.prototype.search = function search( x, y ){ | |
var bboxes = this.rtree.search( [ x, y, x, y ] ); | |
var pt = [ x, y ]; | |
for( var ind = 0; ind < bboxes.length; ind++ ){ | |
var polyObj = this.polygons[ bboxes[ ind ].polyId ]; | |
var polyCoords = polyObj.geometry.coordinates[ 0 ]; | |
if( pointInPolygon( pt, polyCoords ) ){ | |
var inHole = false; | |
for( var subPolyInd = 1; subPolyInd < polyObj.geometry.coordinates.length; subPolyInd++ ){ | |
if( pointInPolygon( pt, polyObj.geometry.coordinates[ subPolyInd ] ) ){ | |
inHole = true; | |
break; | |
} | |
} | |
if( !inHole ){ | |
return polyObj; | |
} | |
} | |
} | |
}; | |
*/ | |
/** | |
* Build a spatial index for a set of polygons, and store both the polygons and | |
* the index in this `PolygonLookup`. | |
* | |
* @param {object} collection A GeoJSON-formatted FeatureCollection. | |
* | |
PolygonLookup.prototype.loadFeatureCollection = function loadFeatureCollection( collection ){ | |
var bboxes = []; | |
var polygons = []; | |
var polyId = 0; | |
function indexPolygon( poly ){ | |
polygons.push(poly); | |
var bbox = getBoundingBox( poly.geometry.coordinates[ 0 ] ); | |
bbox.polyId = polyId++; | |
bboxes.push(bbox); | |
} | |
function indexFeature( poly ){ | |
if( poly.geometry.coordinates[ 0 ] !== undefined && | |
poly.geometry.coordinates[ 0 ].length > 0){ | |
switch( poly.geometry.type ){ | |
case 'Polygon': | |
indexPolygon( poly ); | |
break; | |
case 'MultiPolygon': | |
var childPolys = poly.geometry.coordinates; | |
for( var ind = 0; ind < childPolys.length; ind++ ){ | |
var childPoly = { | |
type: 'Feature', | |
properties: poly.properties, | |
geometry: { | |
type: 'Polygon', | |
coordinates: childPolys[ ind ] | |
} | |
}; | |
indexPolygon( childPoly ); | |
} | |
break; | |
} | |
} | |
} | |
collection.features.forEach( indexFeature ); | |
this.rtree = new RBush().load( bboxes ); | |
this.polygons = polygons; | |
}; | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment