pax_global_header00006660000000000000000000000064132167003140014507gustar00rootroot0000000000000052 comment=a345dbd03e76552d8471115122d03293ad32c054 rbush-2.0.2/000077500000000000000000000000001321670031400126335ustar00rootroot00000000000000rbush-2.0.2/.gitignore000066400000000000000000000000721321670031400146220ustar00rootroot00000000000000node_modules npm-debug.log coverage rbush.js rbush.min.js rbush-2.0.2/.travis.yml000066400000000000000000000000601321670031400147400ustar00rootroot00000000000000language: node_js node_js: - "4" - "stable" rbush-2.0.2/LICENSE000066400000000000000000000020631321670031400136410ustar00rootroot00000000000000MIT License Copyright (c) 2016 Vladimir Agafonkin 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. rbush-2.0.2/README.md000066400000000000000000000241701321670031400141160ustar00rootroot00000000000000RBush ===== RBush is a high-performance JavaScript library for 2D **spatial indexing** of points and rectangles. It's based on an optimized **R-tree** data structure with **bulk insertion** support. *Spatial index* is a special data structure for points and rectangles that allows you to perform queries like "all items within this bounding box" very efficiently (e.g. hundreds of times faster than looping over all items). It's most commonly used in maps and data visualizations. [![Build Status](https://travis-ci.org/mourner/rbush.svg?branch=master)](https://travis-ci.org/mourner/rbush) [![](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/projects) ## Demos The demos contain visualization of trees generated from 50k bulk-loaded random points. Open web console to see benchmarks; click on buttons to insert or remove items; click to perform search under the cursor. * [randomly clustered data](http://mourner.github.io/rbush/viz/viz-cluster.html) * [uniformly distributed random data](http://mourner.github.io/rbush/viz/viz-uniform.html) ## Install Install with NPM (`npm install rbush`), or use CDN links for browsers: [rbush.js](https://unpkg.com/rbush@2.0.1/rbush.js), [rbush.min.js](https://unpkg.com/rbush@2.0.1/rbush.min.js) ## Usage ### Creating a Tree ```js var tree = rbush(); ``` An optional argument to `rbush` defines the maximum number of entries in a tree node. `9` (used by default) is a reasonable choice for most applications. Higher value means faster insertion and slower search, and vice versa. ```js var tree = rbush(16); ``` ### Adding Data Insert an item: ```js var item = { minX: 20, minY: 40, maxX: 30, maxY: 50, foo: 'bar' }; tree.insert(item); ``` ### Removing Data Remove a previously inserted item: ```js tree.remove(item); ``` By default, RBush removes objects by reference. However, you can pass a custom `equals` function to compare by value for removal, which is useful when you only have a copy of the object you need removed (e.g. loaded from server): ```js tree.remove(itemCopy, function (a, b) { return a.id === b.id; }); ``` Remove all items: ```js tree.clear(); ``` ### Data Format By default, RBush assumes the format of data points to be an object with `minX`, `minY`, `maxX` and `maxY` properties. You can customize this by providing an array with corresponding accessor strings as a second argument to `rbush` like this: ```js var tree = rbush(9, ['[0]', '[1]', '[0]', '[1]']); // accept [x, y] points tree.insert([20, 50]); ``` If you're indexing a static list of points (you don't need to add/remove points after indexing), you should use [kdbush](https://github.com/mourner/kdbush) which performs point indexing 5-8x faster than RBush. ### Bulk-Inserting Data Bulk-insert the given data into the tree: ```js tree.load([item1, item2, ...]); ``` Bulk insertion is usually ~2-3 times faster than inserting items one by one. After bulk loading (bulk insertion into an empty tree), subsequent query performance is also ~20-30% better. Note that when you do bulk insertion into an existing tree, it bulk-loads the given data into a separate tree and inserts the smaller tree into the larger tree. This means that bulk insertion works very well for clustered data (where items in one update are close to each other), but makes query performance worse if the data is scattered. ### Search ```js var result = tree.search({ minX: 40, minY: 20, maxX: 80, maxY: 70 }); ``` Returns an array of data items (points or rectangles) that the given bounding box intersects. Note that the `search` method accepts a bounding box in `{minX, minY, maxX, maxY}` format regardless of the format specified in the constructor (which only affects inserted objects). ```js var allItems = tree.all(); ``` Returns all items of the tree. ### Collisions ```js var result = tree.collides({minX: 40, minY: 20, maxX: 80, maxY: 70}); ``` Returns `true` if there are any items intersecting the given bounding box, otherwise `false`. ### Export and Import ```js // export data as JSON object var treeData = tree.toJSON(); // import previously exported data var tree = rbush(9).fromJSON(treeData); ``` Importing and exporting as JSON allows you to use RBush on both the server (using Node.js) and the browser combined, e.g. first indexing the data on the server and and then importing the resulting tree data on the client for searching. Note that the `nodeSize` option passed to the constructor must be the same in both trees for export/import to work properly. ### K-Nearest Neighbors For "_k_ nearest neighbors around a point" type of queries for RBush, check out [rbush-knn](https://github.com/mourner/rbush-knn). ## Performance The following sample performance test was done by generating random uniformly distributed rectangles of ~0.01% area and setting `maxEntries` to `16` (see `debug/perf.js` script). Performed with Node.js v6.2.2 on a Retina Macbook Pro 15 (mid-2012). Test | RBush | [old RTree](https://github.com/imbcmdth/RTree) | Improvement ---------------------------- | ------ | ------ | ---- insert 1M items one by one | 3.18s | 7.83s | 2.5x 1000 searches of 0.01% area | 0.03s | 0.93s | 30x 1000 searches of 1% area | 0.35s | 2.27s | 6.5x 1000 searches of 10% area | 2.18s | 9.53s | 4.4x remove 1000 items one by one | 0.02s | 1.18s | 50x bulk-insert 1M items | 1.25s | n/a | 6.7x ## Algorithms Used * single insertion: non-recursive R-tree insertion with overlap minimizing split routine from R\*-tree (split is very effective in JS, while other R\*-tree modifications like reinsertion on overflow and overlap minimizing subtree search are too slow and not worth it) * single deletion: non-recursive R-tree deletion using depth-first tree traversal with free-at-empty strategy (entries in underflowed nodes are not reinserted, instead underflowed nodes are kept in the tree and deleted only when empty, which is a good compromise of query vs removal performance) * bulk loading: OMT algorithm (Overlap Minimizing Top-down Bulk Loading) combined with Floyd–Rivest selection algorithm * bulk insertion: STLT algorithm (Small-Tree-Large-Tree) * search: standard non-recursive R-tree search ## Papers * [R-trees: a Dynamic Index Structure For Spatial Searching](http://www-db.deis.unibo.it/courses/SI-LS/papers/Gut84.pdf) * [The R*-tree: An Efficient and Robust Access Method for Points and Rectangles+](http://dbs.mathematik.uni-marburg.de/publications/myPapers/1990/BKSS90.pdf) * [OMT: Overlap Minimizing Top-down Bulk Loading Algorithm for R-tree](http://ftp.informatik.rwth-aachen.de/Publications/CEUR-WS/Vol-74/files/FORUM_18.pdf) * [Bulk Insertions into R-Trees Using the Small-Tree-Large-Tree Approach](http://www.cs.arizona.edu/~bkmoon/papers/dke06-bulk.pdf) * [R-Trees: Theory and Applications (book)](http://www.apress.com/9781852339777) ## Development ```bash npm install # install dependencies npm test # check the code with JSHint and run tests npm run perf # run performance benchmarks npm run cov # report test coverage (with more detailed report in coverage/lcov-report/index.html) ``` ## Compatibility RBush should run on Node and all major browsers. The only caveat: IE 8 needs an [Array#indexOf polyfill](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Polyfill) for `remove` method to work. ## Changelog #### 2.0.1 — June 29, 2016 - Fixed browser builds in NPM. #### 2.0.0 — June 29, 2016 - **Breaking:** changed the default format of inserted items from `[20, 40, 30, 50]` to `{minX: 20, minY: 40, maxX: 30, maxY: 50}`. - **Breaking:** changed the `search` method argument format from `[20, 40, 30, 50]` to `{minX: 20, minY: 40, maxX: 30, maxY: 50}`. - Improved performance by up to 30%. - Added `equalsFn` optional argument to `remove` to be able to remove by value rather than by reference. - Changed the source code to use CommonJS module format. Browser builds are automatically built and published to NPM. - Quickselect algorithm (used internally) is now a [separate module](https://github.com/mourner/quickselect). #### 1.4.3 — May 17, 2016 - Fixed an error when inserting many empty bounding boxes. #### 1.4.2 — Dec 16, 2015 - 50% faster insertion. #### 1.4.1 — Sep 16, 2015 - Fixed insertion in IE8. #### 1.4.0 — Apr 22, 2015 - Added `collides` method for fast collision detection. #### 1.3.4 — Aug 31, 2014 - Improved bulk insertion performance for a large number of items (e.g. up to 100% for inserting a million items). - Fixed performance regression for high node sizes. #### 1.3.3 — Aug 30, 2014 - Improved bulk insertion performance by ~60-70%. - Improved insertion performance by ~40%. - Improved search performance by ~30%. #### 1.3.2 — Nov 25, 2013 - Improved removal performance by ~50%. [#18](https://github.com/mourner/rbush/pull/18) #### 1.3.1 — Nov 24, 2013 - Fixed minor error in the choose split axis algorithm. [#17](https://github.com/mourner/rbush/pull/17) - Much better test coverage (near 100%). [#6](https://github.com/mourner/rbush/issues/6) #### 1.3.0 — Nov 21, 2013 - Significantly improved search performance (especially on large-bbox queries — up to 3x faster). [#11](https://github.com/mourner/rbush/pull/11) - Added `all` method for getting all of the tree items. [#11](https://github.com/mourner/rbush/pull/11) - Made `toBBox`, `compareMinX`, `compareMinY` methods public, made it possible to avoid Content Security Policy issues by overriding them for custom format. [#14](https://github.com/mourner/rbush/pull/14) [#12](https://github.com/mourner/rbush/pull/12) #### 1.2.5 — Nov 5, 2013 - Fixed a bug where insertion failed on a tree that had all items removed previously. [#10](https://github.com/mourner/rbush/issues/10) #### 1.2.4 — Oct 25, 2013 - Added Web Workers support. [#9](https://github.com/mourner/rbush/pull/9) #### 1.2.3 — Aug 30, 2013 - Added AMD support. [#8](https://github.com/mourner/rbush/pull/8) #### 1.2.2 — Aug 27, 2013 - Eliminated recursion when recalculating node bboxes (on insert, remove, load). #### 1.2.0 — Jul 19, 2013 First fully functional RBush release. rbush-2.0.2/bench/000077500000000000000000000000001321670031400137125ustar00rootroot00000000000000rbush-2.0.2/bench/bulk.bench.js000066400000000000000000000007351321670031400162700ustar00rootroot00000000000000var Benchmark = require('benchmark'), rbush = require('../rbush'), genData = require('./gendata'); var N = 10000, maxFill = 16; var data = genData(N, 1); new Benchmark.Suite() .add('bulk loading ' + N + ' items (' + maxFill + ' node size)', function () { var tree = rbush(maxFill); tree.load(data); }) .on('error', function(event) { console.log(event.target.error); }) .on('cycle', function(event) { console.log(String(event.target)); }) .run(); rbush-2.0.2/bench/bulksearch.bench.js000066400000000000000000000016401321670031400174520ustar00rootroot00000000000000var Benchmark = require('benchmark'), rbush = require('../rbush'), genData = require('./gendata'); var N = 10000, maxFill = 16; var data = genData(N, 1); var bboxes100 = genData(1000, 100 * Math.sqrt(0.1)); var bboxes10 = genData(1000, 10); var bboxes1 = genData(1000, 1); var tree = rbush(maxFill); tree.load(data); new Benchmark.Suite() .add('1000 searches 10% after bulk loading ' + N, function () { for (i = 0; i < 1000; i++) { tree.search(bboxes100[i]); } }) .add('1000 searches 1% after bulk loading ' + N, function () { for (i = 0; i < 1000; i++) { tree.search(bboxes10[i]); } }) .add('1000 searches 0.01% after bulk loading ' + N, function () { for (i = 0; i < 1000; i++) { tree.search(bboxes1[i]); } }) .on('error', function(event) { console.log(event.target.error); }) .on('cycle', function(event) { console.log(String(event.target)); }) .run(); rbush-2.0.2/bench/gendata.js000066400000000000000000000011361321670031400156540ustar00rootroot00000000000000 module.exports = genData; function randBox(size) { var x = Math.random() * (100 - size), y = Math.random() * (100 - size); return [x, y, x + size * Math.random(), y + size * Math.random()]; } function genData(N, size) { var data = []; for (var i = 0; i < N; i++) { data.push(randBox(size)); } return data; }; genData.convert = function (data) { var result = []; for (var i = 0; i < data.length; i++) { result.push({x: data[i][0], y: data[i][1], w: data[i][2] - data[i][0], h: data[i][3] - data[i][1]}); } return result; } rbush-2.0.2/bench/insert.bench.js000066400000000000000000000014211321670031400166300ustar00rootroot00000000000000var Benchmark = require('benchmark'), rbush = require('../rbush'), genData = require('./gendata'); var RTree = require('rtree'); var N = 10000, maxFill = 16; var data = genData(N, 1); var data2 = genData.convert(data); new Benchmark.Suite() .add('insert ' + N + ' items (' + maxFill + ' node size)', function () { var tree = rbush(maxFill); for (var i = 0; i < N; i++) { tree.insert(data[i]); } }) .add('insert ' + N + ' items (' + maxFill + ' node size), old RTree', function () { var tree2 = new RTree(maxFill); for (var i = 0; i < N; i++) { tree2.insert(data2[i], i); } }) .on('error', function(event) { console.log(event.target.error); }) .on('cycle', function(event) { console.log(String(event.target)); }) .run(); rbush-2.0.2/bench/perf.js000066400000000000000000000034341321670031400152100ustar00rootroot00000000000000'use strict'; var N = 1000000, maxFill = 16; console.log('number: ' + N); console.log('maxFill: ' + maxFill); function randBox(size) { var x = Math.random() * (100 - size), y = Math.random() * (100 - size); return { minX: x, minY: y, maxX: x + size * Math.random(), maxY: y + size * Math.random() }; } function genData(N, size) { var data = []; for (var i = 0; i < N; i++) { data.push(randBox(size)); } return data; } var data = genData(N, 1); var data2 = genData(N, 1); var bboxes100 = genData(1000, 100 * Math.sqrt(0.1)); var bboxes10 = genData(1000, 10); var bboxes1 = genData(1000, 1); var rbush = typeof require !== 'undefined' ? require('..') : rbush; var tree = rbush(maxFill); console.time('insert one by one'); for (var i = 0; i < N; i++) { tree.insert(data[i]); } console.timeEnd('insert one by one'); console.time('1000 searches 10%'); for (i = 0; i < 1000; i++) { tree.search(bboxes100[i]); } console.timeEnd('1000 searches 10%'); console.time('1000 searches 1%'); for (i = 0; i < 1000; i++) { tree.search(bboxes10[i]); } console.timeEnd('1000 searches 1%'); console.time('1000 searches 0.01%'); for (i = 0; i < 1000; i++) { tree.search(bboxes1[i]); } console.timeEnd('1000 searches 0.01%'); console.time('remove 1000 one by one'); for (i = 0; i < 1000; i++) { tree.remove(data[i]); } console.timeEnd('remove 1000 one by one'); console.time('bulk-insert 1M more'); tree.load(data2); console.timeEnd('bulk-insert 1M more'); console.time('1000 searches 1%'); for (i = 0; i < 1000; i++) { tree.search(bboxes10[i]); } console.timeEnd('1000 searches 1%'); console.time('1000 searches 0.01%'); for (i = 0; i < 1000; i++) { tree.search(bboxes1[i]); } console.timeEnd('1000 searches 0.01%'); rbush-2.0.2/bench/search.bench.js000066400000000000000000000017111321670031400165730ustar00rootroot00000000000000var Benchmark = require('benchmark'), rbush = require('../rbush'), genData = require('./gendata'); var N = 10000, maxFill = 16; var data = genData(N, 1); var bboxes100 = genData(1000, 100 * Math.sqrt(0.1)); var bboxes10 = genData(1000, 10); var bboxes1 = genData(1000, 1); var tree = rbush(maxFill); for (var i = 0; i < N; i++) { tree.insert(data[i]); } new Benchmark.Suite() .add('1000 searches 10% after bulk loading ' + N, function () { for (i = 0; i < 1000; i++) { tree.search(bboxes100[i]); } }) .add('1000 searches 1% after bulk loading ' + N, function () { for (i = 0; i < 1000; i++) { tree.search(bboxes10[i]); } }) .add('1000 searches 0.01% after bulk loading ' + N, function () { for (i = 0; i < 1000; i++) { tree.search(bboxes1[i]); } }) .on('error', function(event) { console.log(event.target.error); }) .on('cycle', function(event) { console.log(String(event.target)); }) .run(); rbush-2.0.2/index.js000066400000000000000000000400711321670031400143020ustar00rootroot00000000000000'use strict'; module.exports = rbush; module.exports.default = rbush; var quickselect = require('quickselect'); function rbush(maxEntries, format) { 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)) 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; 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; }, collides: function (bbox) { var node = this.data, toBBox = this.toBBox; if (!intersects(bbox, node)) return false; 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; if (intersects(bbox, childBBox)) { if (node.leaf || contains(bbox, childBBox)) return true; nodesToSearch.push(child); } } node = nodesToSearch.pop(); } return false; }, 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 scratch 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 = createNode([]); return this; }, remove: function (item, equalsFn) { 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 = findItem(item, node.children, equalsFn); 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)) { // 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: compareNodeMinX, compareMinY: compareNodeMinY, 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 = createNode(items.slice(left, right + 1)); 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)); } node = createNode([]); node.leaf = false; node.height = height; // 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); enlargement = enlargedArea(bbox, child) - 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 || node.children[0]; } return node; }, _insert: function (item, level, isNode) { var toBBox = this.toBBox, bbox = isNode ? item : 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); // 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 splitIndex = this._chooseSplitIndex(node, m, M); var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex)); newNode.height = node.height; newNode.leaf = node.leaf; 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 = createNode([node, newNode]); this.data.height = node.height + 1; this.data.leaf = false; 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); margin += bboxMargin(leftBBox); } for (i = M - m - 1; i >= m; i--) { child = node.children[i]; extend(rightBBox, node.leaf ? toBBox(child) : child); 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); } }, _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 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 {minX: a' + format[0] + ', minY: a' + format[1] + ', maxX: a' + format[2] + ', maxY: a' + format[3] + '};'); } }; function findItem(item, items, equalsFn) { if (!equalsFn) return items.indexOf(item); for (var i = 0; i < items.length; i++) { if (equalsFn(item, items[i])) return i; } return -1; } // calculate node's bbox from bboxes of its children function calcBBox(node, toBBox) { distBBox(node, 0, node.children.length, toBBox, node); } // min bounding rectangle of node children from k to p-1 function distBBox(node, k, p, toBBox, destNode) { if (!destNode) destNode = createNode(null); destNode.minX = Infinity; destNode.minY = Infinity; destNode.maxX = -Infinity; destNode.maxY = -Infinity; for (var i = k, child; i < p; i++) { child = node.children[i]; extend(destNode, node.leaf ? toBBox(child) : child); } return destNode; } function extend(a, b) { a.minX = Math.min(a.minX, b.minX); a.minY = Math.min(a.minY, b.minY); a.maxX = Math.max(a.maxX, b.maxX); a.maxY = Math.max(a.maxY, b.maxY); return a; } function compareNodeMinX(a, b) { return a.minX - b.minX; } function compareNodeMinY(a, b) { return a.minY - b.minY; } function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); } function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); } function enlargedArea(a, b) { return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) * (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY)); } function intersectionArea(a, b) { var minX = Math.max(a.minX, b.minX), minY = Math.max(a.minY, b.minY), maxX = Math.min(a.maxX, b.maxX), maxY = Math.min(a.maxY, b.maxY); return Math.max(0, maxX - minX) * Math.max(0, maxY - minY); } function contains(a, b) { return a.minX <= b.minX && a.minY <= b.minY && b.maxX <= a.maxX && b.maxY <= a.maxY; } function intersects(a, b) { return b.minX <= a.maxX && b.minY <= a.maxY && b.maxX >= a.minX && b.maxY >= a.minY; } function createNode(children) { return { children: children, height: 1, leaf: true, minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }; } // 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; quickselect(arr, mid, left, right, compare); stack.push(left, mid, mid, right); } } rbush-2.0.2/package.json000066400000000000000000000026051321670031400151240ustar00rootroot00000000000000{ "name": "rbush", "version": "2.0.2", "description": "High-performance 2D spatial index for rectangles (based on R*-tree with bulk loading and bulk insertion algorithms)", "homepage": "https://github.com/mourner/rbush", "repository": { "type": "git", "url": "git://github.com/mourner/rbush.git" }, "keywords": [ "spatial", "tree", "search", "rectangle", "index", "math" ], "author": "Vladimir Agafonkin", "license": "MIT", "main": "index.js", "jsdelivr": "rbush.js", "unpkg": "rbush.js", "devDependencies": { "benchmark": "^2.1.4", "browserify": "^14.5.0", "eslint": "^4.13.1", "eslint-config-mourner": "^2.0.3", "faucet": "0.0.1", "istanbul": "~0.4.5", "tape": "^4.8.0", "uglify-js": "^3.2.2" }, "scripts": { "test": "eslint index.js test/test.js && node test/test.js | faucet", "perf": "node ./bench/perf.js", "cov": "istanbul cover test/test.js -x test/test.js", "build": "browserify index.js -s rbush -o rbush.js", "build-min": "browserify index.js -s rbush | uglifyjs -c warnings=false -m > rbush.min.js", "prepare": "npm run build && npm run build-min" }, "files": [ "rbush.js", "rbush.min.js" ], "eslintConfig": { "extends": "mourner", "rules": { "new-cap": 0, "consistent-return": 0 } }, "dependencies": { "quickselect": "^1.0.1" } } rbush-2.0.2/test/000077500000000000000000000000001321670031400136125ustar00rootroot00000000000000rbush-2.0.2/test/test.js000066400000000000000000000245011321670031400151310ustar00rootroot00000000000000'use strict'; /*eslint key-spacing: 0, comma-spacing: 0 */ var rbush = require('..'), t = require('tape'); function sortedEqual(t, a, b, compare) { compare = compare || defaultCompare; t.same(a.slice().sort(compare), b.slice().sort(compare)); } function defaultCompare(a, b) { return (a.minX - b.minX) || (a.minY - b.minY) || (a.maxX - b.maxX) || (a.maxY - b.maxY); } function someData(n) { var data = []; for (var i = 0; i < n; i++) { data.push({minX: i, minY: i, maxX: i, maxY: i}); } return data; } function arrToBBox(arr) { return { minX: arr[0], minY: arr[1], maxX: arr[2], maxY: arr[3] }; } var data = [[0,0,0,0],[10,10,10,10],[20,20,20,20],[25,0,25,0],[35,10,35,10],[45,20,45,20],[0,25,0,25],[10,35,10,35], [20,45,20,45],[25,25,25,25],[35,35,35,35],[45,45,45,45],[50,0,50,0],[60,10,60,10],[70,20,70,20],[75,0,75,0], [85,10,85,10],[95,20,95,20],[50,25,50,25],[60,35,60,35],[70,45,70,45],[75,25,75,25],[85,35,85,35],[95,45,95,45], [0,50,0,50],[10,60,10,60],[20,70,20,70],[25,50,25,50],[35,60,35,60],[45,70,45,70],[0,75,0,75],[10,85,10,85], [20,95,20,95],[25,75,25,75],[35,85,35,85],[45,95,45,95],[50,50,50,50],[60,60,60,60],[70,70,70,70],[75,50,75,50], [85,60,85,60],[95,70,95,70],[50,75,50,75],[60,85,60,85],[70,95,70,95],[75,75,75,75],[85,85,85,85],[95,95,95,95]] .map(arrToBBox); var emptyData = [[-Infinity, -Infinity, Infinity, Infinity],[-Infinity, -Infinity, Infinity, Infinity], [-Infinity, -Infinity, Infinity, Infinity],[-Infinity, -Infinity, Infinity, Infinity], [-Infinity, -Infinity, Infinity, Infinity],[-Infinity, -Infinity, Infinity, Infinity]].map(arrToBBox); t('constructor accepts a format argument to customize the data format', function (t) { var tree = rbush(4, ['.minLng', '.minLat', '.maxLng', '.maxLat']); t.same(tree.toBBox({minLng: 1, minLat: 2, maxLng: 3, maxLat: 4}), {minX: 1, minY: 2, maxX: 3, maxY: 4}); t.end(); }); t('constructor uses 9 max entries by default', function (t) { var tree = rbush().load(someData(9)); t.equal(tree.toJSON().height, 1); var tree2 = rbush().load(someData(10)); t.equal(tree2.toJSON().height, 2); t.end(); }); t('#toBBox, #compareMinX, #compareMinY can be overriden to allow custom data structures', function (t) { var tree = rbush(4); tree.toBBox = function (item) { return { minX: item.minLng, minY: item.minLat, maxX: item.maxLng, maxY: item.maxLat }; }; tree.compareMinX = function (a, b) { return a.minLng - b.minLng; }; tree.compareMinY = function (a, b) { return a.minLat - b.minLat; }; var data = [ {minLng: -115, minLat: 45, maxLng: -105, maxLat: 55}, {minLng: 105, minLat: 45, maxLng: 115, maxLat: 55}, {minLng: 105, minLat: -55, maxLng: 115, maxLat: -45}, {minLng: -115, minLat: -55, maxLng: -105, maxLat: -45} ]; tree.load(data); function byLngLat(a, b) { return a.minLng - b.minLng || a.minLat - b.minLat; } sortedEqual(t, tree.search({minX: -180, minY: -90, maxX: 180, maxY: 90}), [ {minLng: -115, minLat: 45, maxLng: -105, maxLat: 55}, {minLng: 105, minLat: 45, maxLng: 115, maxLat: 55}, {minLng: 105, minLat: -55, maxLng: 115, maxLat: -45}, {minLng: -115, minLat: -55, maxLng: -105, maxLat: -45} ], byLngLat); sortedEqual(t, tree.search({minX: -180, minY: -90, maxX: 0, maxY: 90}), [ {minLng: -115, minLat: 45, maxLng: -105, maxLat: 55}, {minLng: -115, minLat: -55, maxLng: -105, maxLat: -45} ], byLngLat); sortedEqual(t, tree.search({minX: 0, minY: -90, maxX: 180, maxY: 90}), [ {minLng: 105, minLat: 45, maxLng: 115, maxLat: 55}, {minLng: 105, minLat: -55, maxLng: 115, maxLat: -45} ], byLngLat); sortedEqual(t, tree.search({minX: -180, minY: 0, maxX: 180, maxY: 90}), [ {minLng: -115, minLat: 45, maxLng: -105, maxLat: 55}, {minLng: 105, minLat: 45, maxLng: 115, maxLat: 55} ], byLngLat); sortedEqual(t, tree.search({minX: -180, minY: -90, maxX: 180, maxY: 0}), [ {minLng: 105, minLat: -55, maxLng: 115, maxLat: -45}, {minLng: -115, minLat: -55, maxLng: -105, maxLat: -45} ], byLngLat); t.end(); }); t('#load bulk-loads the given data given max node entries and forms a proper search tree', function (t) { var tree = rbush(4).load(data); sortedEqual(t, tree.all(), data); t.end(); }); t('#load uses standard insertion when given a low number of items', function (t) { var tree = rbush(8) .load(data) .load(data.slice(0, 3)); var tree2 = rbush(8) .load(data) .insert(data[0]) .insert(data[1]) .insert(data[2]); t.same(tree.toJSON(), tree2.toJSON()); t.end(); }); t('#load does nothing if loading empty data', function (t) { var tree = rbush().load([]); t.same(tree.toJSON(), rbush().toJSON()); t.end(); }); t('#load handles the insertion of maxEntries + 2 empty bboxes', function (t) { var tree = rbush(4) .load(emptyData); t.equal(tree.toJSON().height, 2); sortedEqual(t, tree.all(), emptyData); t.end(); }); t('#insert handles the insertion of maxEntries + 2 empty bboxes', function (t) { var tree = rbush(4); emptyData.forEach(function (datum) { tree.insert(datum); }); t.equal(tree.toJSON().height, 2); sortedEqual(t, tree.all(), emptyData); t.end(); }); t('#load properly splits tree root when merging trees of the same height', function (t) { var tree = rbush(4) .load(data) .load(data); t.equal(tree.toJSON().height, 4); sortedEqual(t, tree.all(), data.concat(data)); t.end(); }); t('#load properly merges data of smaller or bigger tree heights', function (t) { var smaller = someData(10); var tree1 = rbush(4) .load(data) .load(smaller); var tree2 = rbush(4) .load(smaller) .load(data); t.equal(tree1.toJSON().height, tree2.toJSON().height); sortedEqual(t, tree1.all(), data.concat(smaller)); sortedEqual(t, tree2.all(), data.concat(smaller)); t.end(); }); t('#search finds matching points in the tree given a bbox', function (t) { var tree = rbush(4).load(data); var result = tree.search({minX: 40, minY: 20, maxX: 80, maxY: 70}); sortedEqual(t, result, [ [70,20,70,20],[75,25,75,25],[45,45,45,45],[50,50,50,50],[60,60,60,60],[70,70,70,70], [45,20,45,20],[45,70,45,70],[75,50,75,50],[50,25,50,25],[60,35,60,35],[70,45,70,45] ].map(arrToBBox)); t.end(); }); t('#collides returns true when search finds matching points', function (t) { var tree = rbush(4).load(data); var result = tree.collides({minX: 40, minY: 20, maxX: 80, maxY: 70}); t.same(result, true); t.end(); }); t('#search returns an empty array if nothing found', function (t) { var result = rbush(4).load(data).search([200, 200, 210, 210]); t.same(result, []); t.end(); }); t('#collides returns false if nothing found', function (t) { var result = rbush(4).load(data).collides([200, 200, 210, 210]); t.same(result, false); t.end(); }); t('#all returns all points in the tree', function (t) { var tree = rbush(4).load(data); var result = tree.all(); sortedEqual(t, result, data); sortedEqual(t, tree.search({minX: 0, minY: 0, maxX: 100, maxY: 100}), data); t.end(); }); t('#toJSON & #fromJSON exports and imports search tree in JSON format', function (t) { var tree = rbush(4).load(data); var tree2 = rbush(4).fromJSON(tree.data); sortedEqual(t, tree.all(), tree2.all()); t.end(); }); t('#insert adds an item to an existing tree correctly', function (t) { var items = [ [0, 0, 0, 0], [1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3], [1, 1, 2, 2] ].map(arrToBBox); var tree = rbush(4).load(items.slice(0, 3)); tree.insert(items[3]); t.equal(tree.toJSON().height, 1); sortedEqual(t, tree.all(), items.slice(0, 4)); tree.insert(items[4]); t.equal(tree.toJSON().height, 2); sortedEqual(t, tree.all(), items); t.end(); }); t('#insert does nothing if given undefined', function (t) { t.same( rbush().load(data), rbush().load(data).insert()); t.end(); }); t('#insert forms a valid tree if items are inserted one by one', function (t) { var tree = rbush(4); for (var i = 0; i < data.length; i++) { tree.insert(data[i]); } var tree2 = rbush(4).load(data); t.ok(tree.toJSON().height - tree2.toJSON().height <= 1); sortedEqual(t, tree.all(), tree2.all()); t.end(); }); t('#remove removes items correctly', function (t) { var tree = rbush(4).load(data); var len = data.length; tree.remove(data[0]); tree.remove(data[1]); tree.remove(data[2]); tree.remove(data[len - 1]); tree.remove(data[len - 2]); tree.remove(data[len - 3]); sortedEqual(t, data.slice(3, len - 3), tree.all()); t.end(); }); t('#remove does nothing if nothing found', function (t) { t.same( rbush().load(data), rbush().load(data).remove([13, 13, 13, 13])); t.end(); }); t('#remove does nothing if given undefined', function (t) { t.same( rbush().load(data), rbush().load(data).remove()); t.end(); }); t('#remove brings the tree to a clear state when removing everything one by one', function (t) { var tree = rbush(4).load(data); for (var i = 0; i < data.length; i++) { tree.remove(data[i]); } t.same(tree.toJSON(), rbush(4).toJSON()); t.end(); }); t('#remove accepts an equals function', function (t) { var tree = rbush(4).load(data); var item = {minX: 20, minY: 70, maxX: 20, maxY: 70, foo: 'bar'}; tree.insert(item); tree.remove(JSON.parse(JSON.stringify(item)), function (a, b) { return a.foo === b.foo; }); sortedEqual(t, tree.all(), data); t.end(); }); t('#clear should clear all the data in the tree', function (t) { t.same( rbush(4).load(data).clear().toJSON(), rbush(4).toJSON()); t.end(); }); t('should have chainable API', function (t) { t.doesNotThrow(function () { rbush() .load(data) .insert(data[0]) .remove(data[0]); }); t.end(); }); rbush-2.0.2/viz/000077500000000000000000000000001321670031400134435ustar00rootroot00000000000000rbush-2.0.2/viz/viz-cluster.html000066400000000000000000000035401321670031400166220ustar00rootroot00000000000000 RBush Tree Visualization
rbush-2.0.2/viz/viz-uniform.html000066400000000000000000000031511321670031400166160ustar00rootroot00000000000000 RBush Tree Visualization
rbush-2.0.2/viz/viz.js000066400000000000000000000047221321670031400146160ustar00rootroot00000000000000var W = 700, canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d'); if (window.devicePixelRatio > 1) { canvas.style.width = canvas.width + 'px'; canvas.style.height = canvas.height + 'px'; canvas.width = canvas.width * 2; canvas.height = canvas.height * 2; ctx.scale(2, 2); } function randBox(size) { var x = Math.random() * (W - size), y = Math.random() * (W - size); return { minX: x, minY: y, maxX: x + size * Math.random(), maxY: y + size * Math.random() }; } function randClusterPoint(dist) { var x = dist + Math.random() * (W - dist * 2), y = dist + Math.random() * (W - dist * 2); return {x: x, y: y}; } function randClusterBox(cluster, dist, size) { var x = cluster.x - dist + 2 * dist * (Math.random() + Math.random() + Math.random()) / 3, y = cluster.y - dist + 2 * dist * (Math.random() + Math.random() + Math.random()) / 3; return { minX: x, minY: y, maxX: x + size * Math.random(), maxY: y + size * Math.random(), item: true }; } var colors = ['#f40', '#0b0', '#37f'], rects; function drawTree(node, level) { if (!node) { return; } var rect = []; rect.push(level ? colors[(node.height - 1) % colors.length] : 'grey'); rect.push(level ? 1 / Math.pow(level, 1.2) : 0.2); rect.push([ Math.round(node.minX), Math.round(node.minY), Math.round(node.maxX - node.minX), Math.round(node.maxY - node.minY) ]); rects.push(rect); if (node.leaf) return; if (level === 6) { return; } for (var i = 0; i < node.children.length; i++) { drawTree(node.children[i], level + 1); } } function draw() { rects = []; drawTree(tree.data, 0); ctx.clearRect(0, 0, W + 1, W + 1); for (var i = rects.length - 1; i >= 0; i--) { ctx.strokeStyle = rects[i][0]; ctx.globalAlpha = rects[i][1]; ctx.strokeRect.apply(ctx, rects[i][2]); } } function search(e) { console.time('1 pixel search'); tree.search({ minX: e.clientX, minY: e.clientY, maxX: e.clientX + 1, maxY: e.clientY + 1 }); console.timeEnd('1 pixel search'); } function remove() { data.sort(tree.compareMinX); console.time('remove 10000'); for (var i = 0; i < 10000; i++) { tree.remove(data[i]); } console.timeEnd('remove 10000'); data.splice(0, 10000); draw(); };