├── README.md ├── bench.js ├── index.js ├── package.json └── test.js /README.md: -------------------------------------------------------------------------------- 1 | # georbush 2 | Geographical extension for rbush (https://github.com/mourner/rbush) 3 | 4 | Based on the spatial search libraries from Vladimir Agafonkin (https://github.com/mourner). 5 | 6 | This is a combination of https://github.com/mourner/geoflatbush and https://github.com/mourner/rbush-knn 7 | 8 | 9 | Benchmark results: 10 | 11 | index 138398 points: 420.519ms 12 | 13 | query 1000 closest: 7.832ms 14 | 15 | query 50000 closest: 57.489ms 16 | 17 | query all 138398: 106.827ms 18 | 19 | 1000 random queries of 1 closest: 34.288ms 20 | 21 | 22 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | var cities = require('all-the-cities'); 2 | var rbush = require('rbush'); 3 | var georbush = require('./'); 4 | 5 | console.log('=== georbush benchmark ==='); 6 | 7 | var n = cities.length; 8 | var k = 1000; 9 | 10 | var randomPoints = []; 11 | for (var i = 0; i < k; i++) randomPoints.push({ 12 | lon: -180 + 360 * Math.random(), 13 | lat: -60 + 140 * Math.random() 14 | }); 15 | 16 | console.time(`index ${n} points`); 17 | var index = rbush(9, ['.lon', '.lat', '.lon', '.lat']); 18 | for(i=0; i= minLng && lng <= maxLng) { 66 | if (lat <= minLat) return earthCircumference * (minLat - lat) / 360; // south 67 | if (lat >= maxLat) return earthCircumference * (lat - maxLat) / 360; // north 68 | return 0; // inside the bbox 69 | } 70 | 71 | // query point is west or east of the bounding box; 72 | // calculate the extremum for great circle distance from query point to the closest longitude 73 | const closestLng = (minLng - lng + 360) % 360 <= (lng - maxLng + 360) % 360 ? minLng : maxLng; 74 | const cosLngDelta = Math.cos((closestLng - lng) * rad); 75 | const extremumLat = Math.atan(sinLat / (cosLat * cosLngDelta)) / rad; 76 | 77 | // calculate distances to lower and higher bbox corners and extremum (if it's within this range); 78 | // one of the three distances will be the lower bound of great circle distance to bbox 79 | let d = Math.max( 80 | greatCircleDistPart(minLat, cosLat, sinLat, cosLngDelta), 81 | greatCircleDistPart(maxLat, cosLat, sinLat, cosLngDelta)); 82 | 83 | if (extremumLat > minLat && extremumLat < maxLat) { 84 | d = Math.max(d, greatCircleDistPart(extremumLat, cosLat, sinLat, cosLngDelta)); 85 | } 86 | 87 | return earthRadius * Math.acos(d); 88 | } 89 | 90 | // distance using spherical law of cosines; should be precise enough for our needs 91 | function greatCircleDist(lng, lat, lng2, lat2, cosLat, sinLat) { 92 | const cosLngDelta = Math.cos((lng2 - lng) * rad); 93 | return earthRadius * Math.acos(greatCircleDistPart(lat2, cosLat, sinLat, cosLngDelta)); 94 | } 95 | 96 | // partial greatCircleDist to reduce trigonometric calculations 97 | function greatCircleDistPart(lat, cosLat, sinLat, cosLngDelta) { 98 | const d = sinLat * Math.sin(lat * rad) + cosLat * Math.cos(lat * rad) * cosLngDelta; 99 | return Math.min(d, 1); 100 | } 101 | 102 | exports.distance = function(lng, lat, lng2, lat2) { 103 | return greatCircleDist(lng, lat, lng2, lat2, Math.cos(lat * rad), Math.sin(lat * rad)); 104 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "georbush", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test.js" 8 | }, 9 | "author": "Christopher Irlam", 10 | "license": "ISC", 11 | "dependencies": { 12 | "all-the-cities": "^2.0.1", 13 | "rbush": "^2.0.2", 14 | "tape": "^4.9.1", 15 | "tinyqueue": "^2.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape').test; 2 | var rbush = require('rbush'); 3 | var georbush = require('./'); 4 | var cities = require('all-the-cities'); 5 | 6 | console.log("test"); 7 | 8 | var index = rbush(9, ['.lon', '.lat', '.lon', '.lat']); 9 | for(i=0; i { 17 | console.log('performs search according to maxResults'); 18 | const points = georbush.around(index, -119.7051, 34.4363, 5, Infinity, null); 19 | t.same(points.map(i => i.name).join(', '), 'Mission Canyon, Santa Barbara, Montecito, Summerland, Goleta'); 20 | t.end(); 21 | }); 22 | 23 | console.log("test1 done"); 24 | 25 | test('performs search within maxDistance', (t) => { 26 | const points = georbush.around(index, 30.5, 50.5, Infinity, 20, null); 27 | t.same(points.map(i => i.name).join(', '), 28 | 'Kiev, Vyshhorod, Kotsyubyns’ke, Sofiyivska Borschagivka, Vyshneve, Kriukivschina, Irpin’, Hostomel’, Khotiv'); 29 | t.end(); 30 | }); 31 | 32 | test('performs search using filter function', (t) => { 33 | const points = georbush.around(index, 30.5, 50.5, 10, Infinity, i => i.population > 1000000); 34 | t.same(points.map(i => i.name).join(', '), 35 | 'Kiev, Dnipropetrovsk, Kharkiv, Minsk, Odessa, Donets’k, Warsaw, Bucharest, Moscow, Rostov-na-Donu'); 36 | t.end(); 37 | }); 38 | 39 | test('performs exhaustive search in correct order', (t) => { 40 | const points = georbush.around(index, 30.5, 50.5);//.map(i => cities[i]); 41 | 42 | const c = {lon: 30.5, lat: 50.5}; 43 | const sorted = cities 44 | .map(item => ({item, dist: georbush.distance(c.lon, c.lat, item.lon, item.lat)})) 45 | .sort((a, b) => a.dist - b.dist); 46 | 47 | for (let i = 0; i < sorted.length; i++) { 48 | const dist = georbush.distance(points[i].lon, points[i].lat, c.lon, c.lat); 49 | if (dist !== sorted[i].dist) { 50 | t.fail(`${points[i].name} vs ${sorted[i].item.name}`); 51 | break; 52 | } 53 | } 54 | t.pass('all points in correct order'); 55 | 56 | t.end(); 57 | }); 58 | 59 | test('calculates great circle distance', (t) => { 60 | t.equal(10131.7396, Math.round(1e4 * georbush.distance(30.5, 50.5, -119.7, 34.4)) / 1e4); 61 | t.end(); 62 | }); --------------------------------------------------------------------------------