[node-rbush] 01/02: Imported Upstream version 1.3.4

Johan Van de Wauw johanvdw-guest at moszumanska.debian.org
Sat Jan 17 11:28:13 UTC 2015


This is an automated email from the git hooks/post-receive script.

johanvdw-guest pushed a commit to branch master
in repository node-rbush.

commit 76871340122533d75d80f5a7616c190587e3a7bc
Author: Johan Van de Wauw <johan.vandewauw at gmail.com>
Date:   Wed Jan 7 20:54:12 2015 +0100

    Imported Upstream version 1.3.4
---
 .gitignore                |   3 +
 .travis.yml               |   3 +
 MIT-LICENSE               |  19 ++
 README.md                 | 216 +++++++++++++++++
 bench/bulk.bench.js       |  21 ++
 bench/bulksearch.bench.js |  38 +++
 bench/gendata.js          |  26 ++
 bench/insert.bench.js     |  33 +++
 bench/perf.js             | 164 +++++++++++++
 bench/search.bench.js     |  40 ++++
 package.json              |  47 ++++
 rbush.js                  | 587 ++++++++++++++++++++++++++++++++++++++++++++++
 test/test.js              | 329 ++++++++++++++++++++++++++
 viz/viz-cluster.html      |  72 ++++++
 viz/viz-uniform.html      |  68 ++++++
 viz/viz.js                |  96 ++++++++
 16 files changed, 1762 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..801e184
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+npm-debug.log
+coverage
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..6e5919d
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: node_js
+node_js:
+  - "0.10"
diff --git a/MIT-LICENSE b/MIT-LICENSE
new file mode 100644
index 0000000..226ace0
--- /dev/null
+++ b/MIT-LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2013 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6022826
--- /dev/null
+++ b/README.md
@@ -0,0 +1,216 @@
+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.
+
+[![Build Status](https://travis-ci.org/mourner/rbush.png?branch=master)](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 v0.10.22 on a Retina Macbook Pro 15 (mid-2012).
+
+Test                         | RBush  | [old RTree](https://github.com/imbcmdth/RTree) | Improvement
+---------------------------- | ------ | ------ | ----
+insert 1M items one by one   | 6.18s  | 16.46s | 2.7x
+1000 searches of 0.01% area  | 0.07s  | 2.52s  | 35x
+1000 searches of 1% area     | 0.53s  | 5.03s  | 9.6x
+1000 searches of 10% area    | 2.45s  | 16.76s | 6.8x
+remove 1000 items one by one | 0.03s  | 3.2s   | 118x
+bulk-insert 1M items         | 1.99s  | n/a    | 8.3x
+
+## 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.
+
+### 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.
+
+## 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)
+```
+
+## Changelog
+
+#### 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.
diff --git a/bench/bulk.bench.js b/bench/bulk.bench.js
new file mode 100644
index 0000000..52e31b4
--- /dev/null
+++ b/bench/bulk.bench.js
@@ -0,0 +1,21 @@
+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();
diff --git a/bench/bulksearch.bench.js b/bench/bulksearch.bench.js
new file mode 100644
index 0000000..39b3af8
--- /dev/null
+++ b/bench/bulksearch.bench.js
@@ -0,0 +1,38 @@
+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();
diff --git a/bench/gendata.js b/bench/gendata.js
new file mode 100644
index 0000000..27bf1ae
--- /dev/null
+++ b/bench/gendata.js
@@ -0,0 +1,26 @@
+
+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;
+}
diff --git a/bench/insert.bench.js b/bench/insert.bench.js
new file mode 100644
index 0000000..6fc7ad5
--- /dev/null
+++ b/bench/insert.bench.js
@@ -0,0 +1,33 @@
+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();
diff --git a/bench/perf.js b/bench/perf.js
new file mode 100644
index 0000000..45c2c79
--- /dev/null
+++ b/bench/perf.js
@@ -0,0 +1,164 @@
+
+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');
diff --git a/bench/search.bench.js b/bench/search.bench.js
new file mode 100644
index 0000000..0b03464
--- /dev/null
+++ b/bench/search.bench.js
@@ -0,0 +1,40 @@
+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();
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7dd10dd
--- /dev/null
+++ b/package.json
@@ -0,0 +1,47 @@
+{
+  "name": "rbush",
+  "version": "1.3.4",
+  "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",
+    "faucet": "0.0.1",
+    "istanbul": "~0.3.0",
+    "jshint": "^2.5.5",
+    "rtree": "~1.4.2",
+    "tape": "^2.14.0"
+  },
+  "scripts": {
+    "test": "jshint 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"
+  },
+  "jshintConfig": {
+    "unused": "true",
+    "undef": true,
+    "trailing": true,
+    "eqeqeq": true,
+    "es3": true,
+    "indent": 4,
+    "node": true,
+    "browser": true,
+    "worker": true,
+    "globals": {
+      "define": false
+    }
+  }
+}
diff --git a/rbush.js b/rbush.js
new file mode 100644
index 0000000..b151197
--- /dev/null
+++ b/rbush.js
@@ -0,0 +1,587 @@
+/*
+ (c) 2013, Vladimir Agafonkin
+ RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles.
+ https://github.com/mourner/rbush
+*/
+
+(function () { 'use strict';
+
+function rbush(maxEntries, format) {
+
+    // jshint newcap: false, validthis: true
+    if (!(this instanceof rbush)) return new rbush(maxEntries, format);
+
+    // 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;
+
+})();
diff --git a/test/test.js b/test/test.js
new file mode 100644
index 0000000..4739142
--- /dev/null
+++ b/test/test.js
@@ -0,0 +1,329 @@
+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('#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('#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]
+    });
+    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();
+});
diff --git a/viz/viz-cluster.html b/viz/viz-cluster.html
new file mode 100644
index 0000000..a6298df
--- /dev/null
+++ b/viz/viz-cluster.html
@@ -0,0 +1,72 @@
+<!doctype html>
+
+<title>RBush Tree Visualization</title>
+<canvas id="canvas" width="701" height="701"></canvas>
+<br>
+<button id="insert1">Insert 50000</button>
+<button id="insert2">Insert 1000</button>
+<button id="insert3">Bulk-insert 50000</button>
+<button id="insert4">Bulk-insert 1000</button>
+<button id="remove">Remove leftmost 10000</button>
+
+<script src="../rbush.js"></script>
+<script src="viz.js"></script>
+<script>
+
+var N = 100000,
+    M = 30,
+    R = 100;
+
+function genData(N, M, R) {
+    var data = [];
+    for (var i = 0; i < M; i++) {
+        var cluster = randClusterPoint(R);
+
+        for (var j = 0; j < N / M; j++) {
+            data.push(randClusterBox(cluster, R, 1));
+        }
+    }
+    return data;
+}
+
+var tree = rbush(16);
+var data = [];
+
+genBulkInsert(N, M)();
+
+function genInsertOneByOne(K, M) {
+    return function () {
+        var data2 = genData(K, M, R);
+
+        console.time('insert ' + K + ' items');
+        for (var i = 0; i < K; i++) {
+            tree.insert(data2[i]);
+        }
+        console.timeEnd('insert ' + K + ' items');
+
+        draw();
+    };
+}
+
+function genBulkInsert(K, M) {
+    return function () {
+        var data2 = genData(K, M, R);
+
+        console.time('bulk-insert ' + K + ' items');
+        tree.load(data2);
+        console.timeEnd('bulk-insert ' + K + ' items');
+
+        data = data.concat(data2);
+
+        draw();
+    };
+}
+
+document.getElementById('insert1').onclick = genInsertOneByOne(50000, M);
+document.getElementById('insert2').onclick = genInsertOneByOne(1000, 1);
+document.getElementById('insert3').onclick = genBulkInsert(50000, M);
+document.getElementById('insert4').onclick = genBulkInsert(1000, 1);
+document.getElementById('canvas').onclick = search;
+document.getElementById('remove').onclick = remove;
+
+</script>
diff --git a/viz/viz-uniform.html b/viz/viz-uniform.html
new file mode 100644
index 0000000..4a5bb3c
--- /dev/null
+++ b/viz/viz-uniform.html
@@ -0,0 +1,68 @@
+<!doctype html>
+
+<title>RBush Tree Visualization</title>
+<canvas id="canvas" width="701" height="701"></canvas>
+<br>
+<button id="insert1">Insert 50000</button>
+<button id="insert2">Insert 1000</button>
+<button id="insert3">Bulk-insert 50000</button>
+<button id="insert4">Bulk-insert 1000</button>
+<button id="remove">Remove leftmost 10000</button>
+
+<script src="../rbush.js"></script>
+<script src="viz.js"></script>
+<script>
+
+var N = 50000;
+
+function genData(N) {
+    var data = [];
+    for (var i = 0; i < N; i++) {
+        data[i] = randBox(1);
+    }
+    return data;
+}
+
+var tree = rbush(10);
+var data = [];
+
+genBulkInsert(N)();
+
+function genInsertOneByOne(K) {
+    return function () {
+        var data2 = genData(K);
+
+        console.time('insert ' + K + ' items');
+        for (var i = 0; i < K; i++) {
+            tree.insert(data2[i]);
+        }
+        console.timeEnd('insert ' + K + ' items');
+
+        data = data.concat(data2);
+
+        draw();
+    };
+}
+
+function genBulkInsert(K) {
+    return function () {
+        var data2 = genData(K);
+
+        console.time('bulk-insert ' + K + ' items');
+        tree.load(data2);
+        console.timeEnd('bulk-insert ' + K + ' items');
+
+        data = data.concat(data2);
+
+        draw();
+    };
+}
+
+document.getElementById('insert1').onclick = genInsertOneByOne(50000);
+document.getElementById('insert2').onclick = genInsertOneByOne(1000);
+document.getElementById('insert3').onclick = genBulkInsert(50000);
+document.getElementById('insert4').onclick = genBulkInsert(1000);
+document.getElementById('canvas').onclick = search;
+document.getElementById('remove').onclick = remove;
+
+</script>
diff --git a/viz/viz.js b/viz/viz.js
new file mode 100644
index 0000000..e6d3714
--- /dev/null
+++ b/viz/viz.js
@@ -0,0 +1,96 @@
+var W = 700,
+    canvas = document.getElementById('canvas'),
+    ctx = canvas.getContext('2d');
+
+function randBox(size) {
+    var x = Math.random() * (W - size),
+        y = Math.random() * (W - size);
+    return [
+        x, y,
+        x + size * Math.random(),
+        y + size * Math.random()
+    ];
+}
+
+function randPoint() {
+    var x = Math.random() * W,
+        y = Math.random() * W;
+    return [x, y];
+}
+
+function randClusterPoint(dist) {
+    var x = dist + Math.random() * (W - dist * 2),
+        y = dist + Math.random() * (W - dist * 2);
+    return [x, y];
+}
+
+function randClusterBox(cluster, dist, size) {
+    var x = cluster[0] - dist + 2 * dist * (Math.random() + Math.random() + Math.random()) / 3,
+        y = cluster[1] - dist + 2 * dist * (Math.random() + Math.random() + Math.random()) / 3;
+
+    return [
+        x, y,
+        x + size * Math.random(),
+        y + size * Math.random()
+    ];
+}
+
+var colors = ['#f40', '#37f', '#0b0'],
+    rects;
+
+function drawTree(node, level) {
+    if (!node) { return; }
+
+    var rect = [];
+
+    rect.push(level ? colors[(level - 1) % colors.length] : 'grey');
+    rect.push(level ? 1 / level : 1);
+    rect.push([
+        Math.round(node.bbox[0]) + 0.5,
+        Math.round(node.bbox[1]) + 0.5,
+        Math.round(node.bbox[2] - node.bbox[0]),
+        Math.round(node.bbox[3] - node.bbox[1])
+    ]);
+
+    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.fillStyle = 'white';
+    ctx.fillRect(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([e.clientX, e.clientY, e.clientX + 1, 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();
+};

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/node-rbush.git



More information about the Pkg-grass-devel mailing list