pax_global_header 0000666 0000000 0000000 00000000064 12634366436 0014527 g ustar 00root root 0000000 0000000 52 comment=d214f50cc3f485248b4dcec005e86df86ee7860c rbush-1.4.2/ 0000775 0000000 0000000 00000000000 12634366436 0012656 5 ustar 00root root 0000000 0000000 rbush-1.4.2/.gitignore 0000664 0000000 0000000 00000000044 12634366436 0014644 0 ustar 00root root 0000000 0000000 node_modules npm-debug.log coverage rbush-1.4.2/.travis.yml 0000664 0000000 0000000 00000000046 12634366436 0014767 0 ustar 00root root 0000000 0000000 language: node_js node_js: - "0.10" rbush-1.4.2/MIT-LICENSE 0000664 0000000 0000000 00000002046 12634366436 0014314 0 ustar 00root root 0000000 0000000 Copyright (c) 2015 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-1.4.2/README.md 0000664 0000000 0000000 00000020720 12634366436 0014136 0 ustar 00root root 0000000 0000000 RBush ===== RBush is a high-performance JavaScript library for 2D **spatial indexing** of points and rectangles by [Vladimir Agafonkin](http://github.com/mourner), 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. [](https://travis-ci.org/mourner/rbush) ## 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. * [uniformly distributed random data](http://mourner.github.io/rbush/viz/viz-uniform.html) * [randomly clustered data](http://mourner.github.io/rbush/viz/viz-cluster.html) ## 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 v5.2.0 on a Retina Macbook Pro 15 (mid-2012). Test | RBush | [old RTree](https://github.com/imbcmdth/RTree) | Improvement ---------------------------- | ------ | ------ | ---- insert 1M items one by one | 4.7s | 9.26s | 2x 1000 searches of 0.01% area | 0.06s | 1.12s | 20x 1000 searches of 1% area | 0.43s | 2.73s | 6.3x 1000 searches of 10% area | 2.19s | 11.56s | 5.3x remove 1000 items one by one | 0.02s | 1.44s | 65x bulk-insert 1M items | 1.38s | n/a | 6.7x ## Usage ### Creating a Tree ```js var tree = rbush(9); ``` An optional argument to `rbush` defines the maximum number of entries in a tree node. It drastically affects the performance, so you should adjust it considering the type of data and search queries you perform. ### Adding and Removing Data Insert an item: ```js var item = [20, 40, 30, 50]; // [x1, y1, x2, y2] tree.insert(item); ``` Remove a previously inserted item: ```js tree.remove(item); ``` Clear all items: ```js tree.clear(); ``` Items inserted in the tree can have other arbitrary properties/elements that you can access later: ```js var item1 = [20, 40, 30, 50, {foo: 'bar'}]; tree.insert(item1); var item2 = [15, 15, 30, 30]; item2.foo = 'bar'; tree.insert(item2); ``` ### Data Format By default, RBush assumes the format of data points to be `[minX, minY, maxX, maxY]` (bounding box coordinates, or just `[x, y, x, y]` for points). You can customize this by providing an array with `minX`, `minY`, `maxX`, `maxY` accessor strings as a second argument to `rbush` like this: ```js var tree = rbush(9, ['.minLng', '.minLat', '.maxLng', '.maxLat']); tree.insert({id: 'foo', minLng: 30, minLat: 50, maxLng: 40, maxLat: 60}); ``` ### Bulk-Inserting Data Bulk-insert the given data into the tree: ```js tree.load([ [10, 10, 15, 20], [12, 15, 40, 64.5], ... ]); ``` 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. 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 are close to each other), but makes query performance worse if the data is scattered. ### Search ```js var result = tree.search([40, 20, 80, 70]); ``` Returns an array of data items (points or rectangles) that the given bounding box (`[minX, minY, maxX, maxY]`) intersects. ```js var allItems = tree.all(); ``` Returns all items of the tree. ### Collisions ```js var result = tree.collides([40, 20, 80, 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. ### 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). ## 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 #### 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-1.4.2/bench/ 0000775 0000000 0000000 00000000000 12634366436 0013735 5 ustar 00root root 0000000 0000000 rbush-1.4.2/bench/bulk.bench.js 0000664 0000000 0000000 00000000735 12634366436 0016313 0 ustar 00root root 0000000 0000000 var 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-1.4.2/bench/bulksearch.bench.js 0000664 0000000 0000000 00000001640 12634366436 0017475 0 ustar 00root root 0000000 0000000 var 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-1.4.2/bench/gendata.js 0000664 0000000 0000000 00000001136 12634366436 0015677 0 ustar 00root root 0000000 0000000 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-1.4.2/bench/insert.bench.js 0000664 0000000 0000000 00000001421 12634366436 0016653 0 ustar 00root root 0000000 0000000 var 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-1.4.2/bench/perf.js 0000664 0000000 0000000 00000006530 12634366436 0015233 0 ustar 00root root 0000000 0000000 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 [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; } var data = genData(N, 1); var rbush = typeof require !== 'undefined' ? require('../rbush.js') : rbush; var tree = rbush(maxFill); console.time('insert one by one'); for (i = 0; i < N; i++) { tree.insert(data[i]); } console.timeEnd('insert one by one'); // console.time('bulk load'); // tree.load(data); // console.timeEnd('bulk load'); var bboxes100 = genData(1000, 100 * Math.sqrt(0.1)); console.time('1000 searches 10%'); for (i = 0; i < 1000; i++) { tree.search(bboxes100[i]); } console.timeEnd('1000 searches 10%'); var bboxes10 = genData(1000, 10); console.time('1000 searches 1%'); for (i = 0; i < 1000; i++) { tree.search(bboxes10[i]); } console.timeEnd('1000 searches 1%'); var bboxes1 = genData(1000, 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'); var data2 = genData(N, 1); 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%'); // console.time('100 naive searches 1%'); // var result; // for (var j = 0; j < 100; j++) { // result = []; // for (i = 0; i < N; i++) { // if (tree._intersects(bboxes10[j], data[i])) { // result.push(data[i]); // } // } // } // console.timeEnd('100 naive searches 1%'); var RTree = typeof require !== 'undefined' ? require('rtree') : RTree; var tree2 = new RTree(maxFill); function convertData(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; } var datab = convertData(data); console.time('old RTree load one by one'); for (var i = 0; i < N; i++) { tree2.insert(datab[i], i); } console.timeEnd('old RTree load one by one'); var bboxes100b = convertData(bboxes100); console.time('1000 searches 10% 2'); for (i = 0; i < 1000; i++) { tree2.search(bboxes100b[i]); } console.timeEnd('1000 searches 10% 2'); var bboxes10b = convertData(bboxes10); console.time('1000 searches 1% 2'); for (i = 0; i < 1000; i++) { tree2.search(bboxes10b[i]); } console.timeEnd('1000 searches 1% 2'); var bboxes1b = convertData(bboxes1); console.time('1000 searches 0.01% 2'); for (i = 0; i < 1000; i++) { tree2.search(bboxes1b[i]); } console.timeEnd('1000 searches 0.01% 2'); console.time('old RTree remove 1000 one by one'); for (var i = 0; i < 1000; i++) { tree2.remove(datab[i], i); } console.timeEnd('old RTree remove 1000 one by one'); rbush-1.4.2/bench/search.bench.js 0000664 0000000 0000000 00000001711 12634366436 0016616 0 ustar 00root root 0000000 0000000 var 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-1.4.2/package.json 0000664 0000000 0000000 00000002025 12634366436 0015143 0 ustar 00root root 0000000 0000000 { "name": "rbush", "version": "1.4.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", "keywords": [ "spatial", "tree", "search", "rectangle", "index", "math" ], "author": "Vladimir Agafonkin", "repository": { "type": "git", "url": "git://github.com/mourner/rbush.git" }, "main": "rbush.js", "devDependencies": { "benchmark": "^1.0.0", "eslint": "^1.10.3", "eslint-config-mourner": "^1.0.1", "faucet": "0.0.1", "istanbul": "~0.4.1", "rtree": "~1.4.2", "tape": "^4.0.0" }, "scripts": { "test": "eslint rbush.js test/test.js && node test/test.js | faucet", "perf": "node ./debug/perf.js", "cov": "istanbul cover test/test.js -x test/test.js" }, "eslintConfig": { "extends": "mourner", "rules": { "indent": 0, "strict": 0, "new-cap": 0 }, "env": { "amd": true } } } rbush-1.4.2/rbush.js 0000664 0000000 0000000 00000043110 12634366436 0014336 0 ustar 00root root 0000000 0000000 /* (c) 2015, 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); // 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; }, collides: function (bbox) { var node = this.data, toBBox = this.toBBox; if (!intersects(bbox, node.bbox)) 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.bbox; 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 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)); } node = { children: [], height: height, bbox: null, leaf: false }; // 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 splitIndex = this._chooseSplitIndex(node, m, M); var newNode = { children: node.children.splice(splitIndex, node.children.length - splitIndex), height: node.height, bbox: null, leaf: false }; 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, bbox: null, 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.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); } } // Floyd-Rivest selection algorithm: // sort an 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('rbush', function () { return rbush; }); else if (typeof module !== 'undefined') module.exports = rbush; else if (typeof self !== 'undefined') self.rbush = rbush; else window.rbush = rbush; })(); rbush-1.4.2/test/ 0000775 0000000 0000000 00000000000 12634366436 0013635 5 ustar 00root root 0000000 0000000 rbush-1.4.2/test/test.js 0000664 0000000 0000000 00000024764 12634366436 0015167 0 ustar 00root root 0000000 0000000 'use strict'; /*eslint key-spacing: 0, comma-spacing: 0, quotes: 0*/ var rbush = require('../rbush.js'), t = require('tape'); function sortedEqual(t, a, b, compare) { t.same(a.slice().sort(compare), b.slice().sort(compare)); } function someData(n) { var data = []; for (var i = 0; i < n; i++) { data.push([i, i, i, i]); } return data; } 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]]; var testTree = {"children":[{ "children":[ {"children":[[0,0,0,0],[10,10,10,10],[20,20,20,20],[25,0,25,0]],"height":1,"bbox":[0,0,25,20],"leaf":true}, {"children":[[0,25,0,25],[10,35,10,35],[25,25,25,25],[20,45,20,45]],"height":1,"bbox":[0,25,25,45],"leaf":true}, {"children":[[50,0,50,0],[35,10,35,10],[60,10,60,10],[45,20,45,20]],"height":1,"bbox":[35,0,60,20],"leaf":true}, {"children":[[50,25,50,25],[35,35,35,35],[60,35,60,35],[45,45,45,45]],"height":1,"bbox":[35,25,60,45],"leaf":true} ],"height":2,"bbox":[0,0,60,45] },{ "children":[ {"children":[[0,50,0,50],[25,50,25,50],[10,60,10,60],[20,70,20,70]],"height":1,"bbox":[0,50,25,70],"leaf":true}, {"children":[[0,75,0,75],[25,75,25,75],[10,85,10,85],[20,95,20,95]],"height":1,"bbox":[0,75,25,95],"leaf":true}, {"children":[[35,60,35,60],[50,50,50,50],[60,60,60,60],[45,70,45,70]],"height":1,"bbox":[35,50,60,70],"leaf":true}, {"children":[[50,75,50,75],[60,85,60,85],[45,95,45,95],[35,85,35,85]],"height":1,"bbox":[35,75,60,95],"leaf":true} ],"height":2,"bbox":[0,50,60,95] },{ "children":[ {"children":[[70,20,70,20],[75,0,75,0],[75,25,75,25],[70,45,70,45]],"height":1,"bbox":[70,0,75,45],"leaf":true}, {"children":[[75,50,75,50],[70,70,70,70],[75,75,75,75],[70,95,70,95]],"height":1,"bbox":[70,50,75,95],"leaf":true}, {"children":[[85,10,85,10],[95,20,95,20],[85,35,85,35],[95,45,95,45]],"height":1,"bbox":[85,10,95,45],"leaf":true}, {"children":[[85,60,85,60],[95,70,95,70],[85,85,85,85],[95,95,95,95]],"height":1,"bbox":[85,60,95,95],"leaf":true} ],"height":2,"bbox":[70,0,95,95] }],"height":3,"bbox":[0,0,95,95]}; 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}), [1, 2, 3, 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 [item.minLng, item.minLat, item.maxLng, 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([-180, -90, 180, 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([-180, -90, 0, 90]), [ {minLng: -115, minLat: 45, maxLng: -105, maxLat: 55}, {minLng: -115, minLat: -55, maxLng: -105, maxLat: -45} ], byLngLat); sortedEqual(t, tree.search([0, -90, 180, 90]), [ {minLng: 105, minLat: 45, maxLng: 115, maxLat: 55}, {minLng: 105, minLat: -55, maxLng: 115, maxLat: -45} ], byLngLat); sortedEqual(t, tree.search([-180, 0, 180, 90]), [ {minLng: -115, minLat: 45, maxLng: -105, maxLat: 55}, {minLng: 105, minLat: 45, maxLng: 115, maxLat: 55} ], byLngLat); sortedEqual(t, tree.search([-180, -90, 180, 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(), rbush(4).fromJSON(testTree).all()); 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 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([40, 20, 80, 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] ]); t.end(); }); t('#collides returns true when search finds matching points', function (t) { var tree = rbush(4).load(data); var result = tree.collides([40, 20, 80, 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([0, 0, 100, 100]), data); t.end(); }); t('#toJSON & #fromJSON exports and imports search tree in JSON format', function (t) { var tree = rbush(4); tree.fromJSON(testTree); var tree2 = rbush(4).load(data); sortedEqual(t, tree.all(), tree2.all()); t.end(); }); t('#insert adds an item to an existing tree correctly', function (t) { var tree = rbush(4).load([ [0, 0, 0, 0], [1, 1, 1, 1], [2, 2, 2, 2] ]); tree.insert([3, 3, 3, 3]); t.same(tree.toJSON(), { 'children':[[0,0,0,0],[1,1,1,1],[2,2,2,2],[3,3,3,3]], 'leaf':true, 'height':1, 'bbox':[0,0,3,3] }); tree.insert([1, 1, 2, 2]); t.same(tree.toJSON(), { 'children':[ {'children':[[0,0,0,0],[1,1,1,1]],'leaf':true,'height':1,'bbox':[0,0,1,1]}, {'children':[[1,1,2,2],[2,2,2,2],[3,3,3,3]],'height':1,'leaf':true,'bbox':[1,1,3,3]} ], 'height':2, 'bbox':[0,0,3,3], 'leaf':false }); 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('#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]) .fromJSON(testTree); }); t.end(); }); rbush-1.4.2/viz/ 0000775 0000000 0000000 00000000000 12634366436 0013466 5 ustar 00root root 0000000 0000000 rbush-1.4.2/viz/viz-cluster.html 0000664 0000000 0000000 00000003375 12634366436 0016653 0 ustar 00root root 0000000 0000000