├── .travis.yml ├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── lib └── quadtree.js └── test └── quadtree_test.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "4" 5 | - "0.12" 6 | - "0.8" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quadtree", 3 | "description": "geospatial quadtree encoding and decoding.", 4 | "version": "1.1.3", 5 | "licenses": [{ 6 | "type": "MIT", 7 | "url": "https://github.com/B2MSolutions/node-quadtree/raw/master/LICENSE" 8 | }], 9 | "author": { "name": "B2M Solutions", "url": "https://github.com/B2MSolutions" }, 10 | "contributors": [ 11 | { "name": "Roy Lines", "url":"http://roylines.co.uk" }, 12 | { "name": "James Bloomer", "url":"https://github.com/jamesbloomer" } 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "http://github.com/B2MSolutions/node-quadtree.git" 17 | }, 18 | "keywords": ["quadtree", "geo", "geospatial", "map"], 19 | "dependencies": { 20 | }, 21 | "devDependencies" : { 22 | "sinon" : "x.x.x", 23 | "mocha" : "x.x.x" 24 | }, 25 | "main": "./lib/quadtree", 26 | "scripts": { "test": "mocha test/*_test.js --reporter spec" }, 27 | "engines": { "node": ">= 0.6.0" } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2009-2012 B2M Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-quadtree 2 | [![Build Status](https://secure.travis-ci.org/B2MSolutions/node-quadtree.png)](http://travis-ci.org/B2MSolutions/node-quadtree) 3 | ## Description 4 | Quadtree is a node module that encodes and decodes geospatial [quadtrees](http://en.wikipedia.org/wiki/Quadtree). 5 | 6 | The encoding algorithm orders the tiles using the [bing maps ordering](http://msdn.microsoft.com/en-us/library/bb259689.aspx) shown below: 7 | 8 | ![Quadtree ordering](http://i.msdn.microsoft.com/dynimg/IC96238.jpg) 9 | 10 | ## Installation 11 | $ npm install quadtree 12 | 13 | ## Usage 14 | var quadtree = require('quadtree'); 15 | 16 | var coordinate = { 17 | lat: -27.093364, 18 | lng: -109.367523 19 | }; 20 | 21 | var precision = 8; 22 | var encoded = quadtree.encode(coordinate, precision); // returns "20310230") 23 | 24 | var decoded = quadtree.decode(encoded); // returns { origin: { lng: -108.984375, lat: -27.0703125 }, error: { lng: 0.703125, lat: 0.3515625 } } 25 | 26 | var neighbour = quadtree.neighbour(encoded, 1, 1); // returns "20310213" 27 | 28 | var boundingBox = quadtree.bbox(encoded); // returns { minlng: -109.6875, minlat: -27.421875, maxlng: -108.28125, maxlat: -26.71875 } 29 | 30 | var enveloped = quadtree.envelop(boundingBox, precision); // returns [ '20310230', '20310231', '20310212', '20310213' ] 31 | 32 | ### quadtree.encode(coordinate, precision) 33 | Encodes a coordinate into a quadtree of the specified precision. 34 | 35 | ### quadtree.decode(quadtree) 36 | Decodes a quadtree into an origin and error. 37 | 38 | ### quadtree.neighbour(encoded, north, east) 39 | Finds the neighbour of the given quadtree, walking a number of tiles in the supplied direction. 40 | 41 | ### quadtree.bbox(encoded) 42 | Returns the bounding box of the tile containing the supplied quadtree. 43 | 44 | ### quadtree.envelop(bbox, precision) 45 | Returns an array of quadtrees that are enveloped by the bbox at the supplied precision. 46 | 47 | ## Contributors 48 | Pair programmed by [Roy Lines](http://roylines.co.uk) and [James Bloomer](https://github.com/jamesbloomer). 49 | 50 | -------------------------------------------------------------------------------- /lib/quadtree.js: -------------------------------------------------------------------------------- 1 | 2 | var quadtree = {}; 3 | 4 | quadtree.encode = function(coordinate, precision) { 5 | 6 | var origin = { lng: 0, lat: 0 }; 7 | var range = { lng: 180, lat: 90 }; 8 | 9 | var result = ''; 10 | 11 | while(precision > 0) { 12 | range.lng /= 2; 13 | range.lat /= 2; 14 | 15 | if((coordinate.lng < origin.lng) && (coordinate.lat >= origin.lat)) { 16 | origin.lng -= range.lng; 17 | origin.lat += range.lat; 18 | result += '0'; 19 | } else if((coordinate.lng >= origin.lng) && (coordinate.lat >= origin.lat)) { 20 | origin.lng += range.lng; 21 | origin.lat += range.lat; 22 | result += '1'; 23 | } else if((coordinate.lng < origin.lng) && (coordinate.lat < origin.lat)) { 24 | origin.lng -= range.lng; 25 | origin.lat -= range.lat; 26 | result += '2'; 27 | } else { 28 | origin.lng += range.lng; 29 | origin.lat -= range.lat; 30 | result += '3'; 31 | } 32 | 33 | --precision; 34 | } 35 | 36 | return result; 37 | }; 38 | 39 | quadtree.decode = function(encoded) { 40 | var origin = { lng: 0, lat: 0 }; 41 | var error = { lng: 180, lat: 90 }; 42 | 43 | var precision = encoded.length; 44 | var currentPrecision = 0; 45 | 46 | while(currentPrecision < precision) { 47 | error.lng /= 2; 48 | error.lat /= 2; 49 | 50 | var quadrant = encoded[currentPrecision]; 51 | if(quadrant === '0') { 52 | origin.lng -= error.lng; 53 | origin.lat += error.lat; 54 | } else if(quadrant === '1') { 55 | origin.lng += error.lng; 56 | origin.lat += error.lat; 57 | } else if(quadrant === '2') { 58 | origin.lng -= error.lng; 59 | origin.lat -= error.lat; 60 | } else { 61 | origin.lng += error.lng; 62 | origin.lat -= error.lat; 63 | } 64 | 65 | ++currentPrecision; 66 | } 67 | 68 | return { origin: origin, error: error }; 69 | }; 70 | 71 | quadtree.neighbour = function(encoded, north, east) { 72 | var decoded = quadtree.decode(encoded); 73 | var neighbour = { 74 | lng: decoded.origin.lng + decoded.error.lng * east * 2, 75 | lat: decoded.origin.lat + decoded.error.lat * north * 2 76 | }; 77 | 78 | return quadtree.encode(neighbour, encoded.length); 79 | }; 80 | 81 | quadtree.bbox = function(encoded) { 82 | var decoded = quadtree.decode(encoded); 83 | 84 | return { 85 | minlng: decoded.origin.lng - decoded.error.lng, 86 | minlat: decoded.origin.lat - decoded.error.lat, 87 | maxlng: decoded.origin.lng + decoded.error.lng, 88 | maxlat: decoded.origin.lat + decoded.error.lat 89 | }; 90 | }; 91 | 92 | quadtree.envelop = function(bbox, precision) { 93 | var end = quadtree.encode({ lng: bbox.maxlng, lat: bbox.maxlat }, precision); 94 | 95 | var rowStart = quadtree.encode({ lng: bbox.minlng, lat: bbox.minlat }, precision); 96 | var rowEnd = quadtree.encode({ lng: bbox.maxlng, lat: bbox.minlat }, precision); 97 | 98 | var current = rowStart; 99 | 100 | var quadtrees = []; 101 | while (true) { 102 | while(current != rowEnd) { 103 | quadtrees.push(current); 104 | current = quadtree.neighbour(current, 0, 1); 105 | } 106 | 107 | if(current == end) break; 108 | 109 | quadtrees.push(rowEnd); 110 | 111 | rowEnd = quadtree.neighbour(rowEnd, 1, 0); 112 | rowStart = quadtree.neighbour(rowStart, 1, 0); 113 | current = rowStart; 114 | } 115 | 116 | quadtrees.push(end); 117 | return quadtrees; 118 | }; 119 | 120 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 121 | module.exports = quadtree; 122 | } 123 | else { 124 | if (typeof define === 'function' && define.amd) { 125 | define([], function() { 126 | return quadtree; 127 | }); 128 | } 129 | else { 130 | window.quadtree = quadtree; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/quadtree_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | quadtree = require('../lib/quadtree'); 3 | 4 | describe('quadtree', function(){ 5 | describe('#encode()', function(){ 6 | it('should return expected for precision 1', function(){ 7 | assert.equal(quadtree.encode({lng : 156.0, lat : 35.0}, 1), '1'); 8 | }); 9 | it('should return expected for precision 2', function(){ 10 | assert.equal(quadtree.encode({lng : 156.0, lat : 35.0}, 2), '13'); 11 | }); 12 | it('should return expected for precision 3', function(){ 13 | assert.equal(quadtree.encode({lng : 156.0, lat : 35.0}, 3), '131'); 14 | }); 15 | it('should return expected for precision 12', function(){ 16 | assert.equal(quadtree.encode({lng : 156.0, lat : 35.0}, 12), '131033301132'); 17 | }); 18 | it('should return expected for precision 100', function(){ 19 | assert.equal(quadtree.encode({lng : 156.0, lat : 35.0}, 100), '1310333011323110333011323110333011323110333011323111231111111111111111111111111111111111111111111111'); 20 | }); 21 | }); 22 | describe('#decode()', function(){ 23 | it('should return expected for given quadtree precision 100', function() { 24 | assert.deepEqual(quadtree.decode('1310333011323110333011323110333011323110333011323111231111111111111111111111111111111111111111111111').origin, {lng : 156.0, lat : 35.0}); 25 | }); 26 | it('should return expected for given quadtree precision 12', function(){ 27 | assert.deepEqual(quadtree.decode('1310333011323').origin, {"lng":155.98388671875,"lat":34.991455078125}); 28 | }); 29 | it('should return expected for given quadtree precision 20', function(){ 30 | assert.deepEqual(quadtree.decode('131033301132301103330').origin, {"lng":155.98088264465332,"lat":35.00119686126709}); 31 | }); 32 | it('should return expected for given quadtree precision 1', function(){ 33 | assert.deepEqual(quadtree.decode('0').origin, {lng : -90.0, lat : 45.0}); 34 | assert.deepEqual(quadtree.decode('1').origin, {lng : 90.0, lat : 45.0}); 35 | assert.deepEqual(quadtree.decode('2').origin, {lng : -90.0, lat : -45.0}); 36 | assert.deepEqual(quadtree.decode('3').origin, {lng : 90.0, lat : -45.0}); 37 | }); 38 | }); 39 | 40 | describe('#bbox()', function(){ 41 | it('should return expected for given quadtree precision 12', function(){ 42 | assert.deepEqual(quadtree.bbox('1310333011323'), { minlng: 155.9619140625, minlat: 34.98046875, maxlng: 156.005859375, maxlat: 35.00244140625 }); 43 | }); 44 | it('should return expected for given quadtree precision 20', function(){ 45 | assert.deepEqual(quadtree.bbox('131033301132301103330'), { minlng: 155.98079681396484, minlat: 35.00115394592285, maxlng: 155.9809684753418, maxlat: 35.00123977661133 }); 46 | }); 47 | it('should return expected for given quadtree precision 1', function(){ 48 | assert.deepEqual(quadtree.bbox('0'), { minlng: -180.0, minlat: 0.0, maxlng: 0.0, maxlat: 90.0 }); 49 | assert.deepEqual(quadtree.bbox('1'), { minlng: 0.0, minlat: 0.0, maxlng: 180.0, maxlat: 90.0 }); 50 | assert.deepEqual(quadtree.bbox('2'), { minlng: -180.0, minlat: -90.0, maxlng: 0.0, maxlat: 0.0 }); 51 | assert.deepEqual(quadtree.bbox('3'), { minlng: 0.0, minlat: -90.0, maxlng: 180.0, maxlat: 0.0 }); 52 | }); 53 | }); 54 | 55 | describe('#neighbour()', function(){ 56 | it('should return expected for given quadtree precision 12', function(){ 57 | assert.deepEqual(quadtree.neighbour('1310333011320', 0, 1), '1310333011321'); 58 | }); 59 | it('should return expected for given quadtree precision 1 and quadrant 0', function(){ 60 | assert.equal(quadtree.neighbour('0', 0, 0), '0'); 61 | assert.equal(quadtree.neighbour('0', 0, 1), '1'); 62 | assert.equal(quadtree.neighbour('0', -1, 0), '2'); 63 | assert.equal(quadtree.neighbour('0', -1, 1), '3'); 64 | }); 65 | 66 | it('should return expected for given quadtree precision 1 and quadrant 1', function(){ 67 | assert.equal(quadtree.neighbour('1', 0, -1), '0'); 68 | assert.equal(quadtree.neighbour('1', 0, 0), '1'); 69 | assert.equal(quadtree.neighbour('1', -1, -1), '2'); 70 | assert.equal(quadtree.neighbour('1', -1, 0), '3'); 71 | }); 72 | }); 73 | 74 | describe('#envelop()', function(){ 75 | it('should return expected for given bbox precision 1', function() { 76 | assert.equal(JSON.stringify(quadtree.envelop({ minlng: -10, minlat: -10, maxlng: 20, maxlat: 20 }, 1)), JSON.stringify(['2','3','0','1'])); 77 | }); 78 | 79 | it('should return expected for given bbox precision 3', function() { 80 | var bbox = quadtree.bbox(quadtree.encode({lng: 0,lat: 0}, 3)); 81 | var end = quadtree.encode({ lng: bbox.maxlng, lat: bbox.maxlat }, 3); 82 | var start = quadtree.encode({ lng: bbox.minlng, lat: bbox.minlat }, 3); 83 | var envelop = quadtree.envelop(bbox,3); 84 | assert.deepEqual([ '122', '123', '120', '121' ], envelop); 85 | }); 86 | }); 87 | 88 | describe('examples', function() { 89 | it('should return values described in the README', function() { 90 | var coordinate = { 91 | lat: -27.093364, 92 | lng: -109.367523 93 | }; 94 | 95 | var precision = 8; 96 | var encoded = quadtree.encode(coordinate, precision); // returns "20310230") 97 | 98 | var decoded = quadtree.decode(encoded); // returns { origin: { lng: -108.984375, lat: -27.0703125 }, error: { lng: 0.703125, lat: 0.3515625 } } 99 | 100 | var neighbour = quadtree.neighbour(encoded, 1, 1); // returns "20310213" 101 | 102 | var boundingBox = quadtree.bbox(encoded); // returns { minlng: -109.6875, minlat: -27.421875, maxlng: -108.28125, maxlat: -26.71875 } 103 | 104 | var enveloped = quadtree.envelop(boundingBox, precision); // returns [ '20310230', '20310231', '20310212', '20310213' ] 105 | 106 | assert.equal(encoded, '20310230'); 107 | assert.deepEqual(decoded, { origin: { lng: -108.984375, lat: -27.0703125 }, error: { lng: 0.703125, lat: 0.3515625 } }); 108 | assert.equal(neighbour, '20310213'); 109 | assert.deepEqual(boundingBox, { minlng: -109.6875, minlat: -27.421875, maxlng: -108.28125, maxlat: -26.71875 }); 110 | assert.equal(JSON.stringify(enveloped), '["20310230","20310231","20310212","20310213"]'); 111 | }); 112 | }); 113 | 114 | describe('issues', function() { 115 | it('bbox should enclose original coordinate', function() { 116 | var coords = {lat: -27.093364, lng: -109.367523}; 117 | var encoded = quadtree.encode(coords, 20); 118 | var bbox = quadtree.bbox(encoded); 119 | assert(bbox.minlng <= coords.lng); 120 | assert(bbox.minlat <= coords.lat); 121 | assert(bbox.maxlng >= coords.lng); 122 | assert(bbox.maxlat >= coords.lat); 123 | }); 124 | 125 | it('encode then decode should return coordinates within error of the original point', function() { 126 | var coords = {lat: -27.093364, lng: -109.367523}; 127 | var encoded = quadtree.encode(coords, 20); 128 | var decoded = quadtree.decode(encoded); 129 | assert(decoded.origin.lat - decoded.error.lat <= coords.lat); 130 | assert(decoded.origin.lat + decoded.error.lat >= coords.lat); 131 | assert(decoded.origin.lng - decoded.error.lng <= coords.lng); 132 | assert(decoded.origin.lng + decoded.error.lng >= coords.lng); 133 | }); 134 | }); 135 | }); --------------------------------------------------------------------------------