This repository has been archived by the owner on Apr 25, 2020. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Jason LaPorte
committed
Oct 11, 2012
0 parents
commit 1e07e09
Showing
5 changed files
with
308 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
sphere-nn | ||
========= | ||
|
||
`sphere-nn` is a Node.JS module that provides fast nearest-neighbor lookups on | ||
a sphere. This is useful if, for example, you have a database of geographic | ||
points (latitude, longitude) and want to swiftly look up which points in the | ||
database are closest to it. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
var lib = require("./lib") | ||
|
||
module.exports = function(points) { | ||
/* Quick! Inflate the toad! */ | ||
var root = lib.build(points) | ||
|
||
/* Return the lookup function. */ | ||
return function(lat, lon, n) { | ||
return lib.lookup(lat, lon, root, n) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
/* FIXME: Break out the KD tree stuff into it's own file, and then wrap it with | ||
* the spherical conversion stuff. */ | ||
|
||
var rad = Math.PI / 180 | ||
|
||
function spherical2cartesian(lat, lon) { | ||
lat *= rad | ||
lon *= rad | ||
var sin = Math.sin(lat) | ||
return [Math.cos(lon) * sin, Math.sin(lon) * sin, Math.cos(lat)] | ||
} | ||
|
||
function Node(axis, split, left, right) { | ||
this.axis = axis | ||
this.split = split | ||
this.left = left | ||
this.right = right | ||
} | ||
|
||
function Leaf(object, lat, lon) { | ||
this.object = object | ||
this.position = spherical2cartesian( | ||
object.lat || object.latitude, | ||
object.lon || object.longitude | ||
) | ||
} | ||
|
||
function Candidate(leaf, position) { | ||
var dx = position[0] - leaf.position[0], | ||
dy = position[1] - leaf.position[1], | ||
dz = position[2] - leaf.position[2] | ||
|
||
this.object = leaf.object | ||
this.dist = dx * dx + dy * dy + dz * dz | ||
} | ||
|
||
function byDistance(a, b) { | ||
return a.dist - b.dist | ||
} | ||
|
||
/* FIXME: We can precalculate this in `build()` and then pass it along in | ||
* `buildrec()`, only modifying the dimension that matters based on the | ||
* splitting plane. That would greatly speed up KD-tree creation by lowering | ||
* the constant. */ | ||
function boundingDimensions(array) { | ||
var i = array.length - 1, | ||
min = array[i].position.slice(0), | ||
max = array[i].position.slice(0), | ||
j | ||
|
||
while(i--) { | ||
j = array[i].position.length | ||
|
||
while(j--) { | ||
if(array[i].position[j] < min[j]) | ||
min[j] = array[i].position[j] | ||
|
||
if(array[i].position[j] > max[j]) | ||
max[j] = array[i].position[j] | ||
} | ||
} | ||
|
||
j = max.length | ||
while(j--) | ||
max[j] -= min[j] | ||
|
||
return max | ||
} | ||
|
||
function indexOfMax(array) { | ||
var i = array.length - 1, | ||
j = i | ||
|
||
while(i--) | ||
if(array[i] > array[j]) | ||
j = i | ||
|
||
return j | ||
} | ||
|
||
function buildrec(array) { | ||
/* This should only happen if you request a kd-tree with zero elements. */ | ||
if(array.length === 0) | ||
return null | ||
|
||
/* If there's only one item, then it's a leaf node! */ | ||
if(array.length === 1) | ||
return array[0] | ||
|
||
/* Uh oh. Well, we have to partition the data set and recurse. Start by | ||
* finding the bounding box of the given points; whichever side is the | ||
* longest is the one we'll use for the splitting plane. */ | ||
var axis = indexOfMax(boundingDimensions(array)) | ||
|
||
/* Sort the points along the splitting plane. */ | ||
array.sort(function(a, b) { | ||
return a.position[axis] - b.position[axis] | ||
}) | ||
|
||
/* Find the median point. It's position is going to be the location of the | ||
* splitting plane. */ | ||
var i = Math.floor(array.length * 0.5) | ||
|
||
/* Split, recurse, yadda yadda. */ | ||
return new Node( | ||
axis, | ||
array[i].position[axis], | ||
buildrec(array.slice(0, i)), | ||
buildrec(array.slice(i)) | ||
) | ||
} | ||
|
||
function build(input) { | ||
var i = input.length, | ||
output = new Array(i) | ||
|
||
while(i--) | ||
output[i] = new Leaf(input[i]) | ||
|
||
return buildrec(output) | ||
} | ||
|
||
function lookup(lat, lon, node, n) { | ||
var array = [] | ||
|
||
/* Degenerate cases. */ | ||
if(node === null || n <= 0) | ||
return array | ||
|
||
var position = spherical2cartesian(lat, lon), | ||
stack = [node, 0], | ||
dist | ||
|
||
while(stack.length) { | ||
dist = stack.pop() | ||
node = stack.pop() | ||
|
||
if(array.length === n && array[array.length - 1].dist < dist * dist) | ||
continue | ||
|
||
while(node instanceof Node) { | ||
if(position[node.axis] < node.split) { | ||
stack.push(node.right, node.split - position[node.axis]) | ||
node = node.left | ||
} | ||
|
||
else { | ||
stack.push(node.left, position[node.axis] - node.split) | ||
node = node.right | ||
} | ||
} | ||
|
||
/* FIXME: This is like the worst possible way to do this. Binary insertion, | ||
* please! */ | ||
array.push(new Candidate(node, position)) | ||
array.sort(byDistance) | ||
|
||
if(array.length > n) | ||
array.pop() | ||
} | ||
|
||
/* Strip candidate wrapper objects. */ | ||
var i = array.length | ||
|
||
while(i--) | ||
array[i] = array[i].object | ||
|
||
return array | ||
} | ||
|
||
exports.build = build | ||
exports.lookup = lookup |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
var assert = require("chai").assert, | ||
lib = require("./lib") | ||
|
||
describe("sphere-nn", function() { | ||
describe("lib", function() { | ||
function City(name, lat, lon) { | ||
this.name = name | ||
this.lat = lat | ||
this.lon = lon | ||
} | ||
|
||
var boston = new City("Boston", 42.358, -71.064), | ||
troy = new City("Troy", 42.732, -73.693), | ||
newYork = new City("New York", 40.664, -73.939), | ||
miami = new City("Miami", 25.788, -80.224), | ||
london = new City("London", 51.507, -0.128), | ||
paris = new City("Paris", 48.857, 2.351), | ||
vienna = new City("Vienna", 48.208, 16.373), | ||
rome = new City("Rome", 41.900, 12.500), | ||
beijing = new City("Beijing", 39.914, 116.392), | ||
hongKong = new City("Hong Kong", 22.278, 114.159), | ||
seoul = new City("Seoul", 37.567, 126.978), | ||
tokyo = new City("Tokyo", 35.690, 139.692), | ||
cities = [ | ||
boston, troy, newYork, miami, london, paris, vienna, rome, beijing, | ||
hongKong, seoul, tokyo | ||
] | ||
|
||
describe("build", function() { | ||
it("should return null given an empty array", function() { | ||
assert.isNull(lib.build([])) | ||
}) | ||
|
||
it("should construct a KD Tree from the raw data", function() { | ||
var root = lib.build(cities) | ||
|
||
assert.equal(root.axis, 0) | ||
assert.closeTo(root.split, 0.1905, 0.0001) | ||
|
||
assert.equal(root.left.axis, 1) | ||
assert.closeTo(root.left.split, 0.3774, 0.0001) | ||
|
||
assert.equal(root.left.left.axis, 1) | ||
assert.closeTo(root.left.left.split, -0.4287, 0.0001) | ||
|
||
assert.deepEqual(root.left.left.left.object, newYork) | ||
|
||
assert.equal(root.left.left.right.axis, 1) | ||
assert.closeTo(root.left.left.right.split, 0.3459, 0.0001) | ||
|
||
assert.deepEqual(root.left.left.right.left.object, miami) | ||
assert.deepEqual(root.left.left.right.right.object, hongKong) | ||
|
||
assert.equal(root.left.right.axis, 1) | ||
assert.closeTo(root.left.right.split, 0.4871, 0.0001) | ||
|
||
assert.deepEqual(root.left.right.left.object, tokyo) | ||
|
||
assert.equal(root.left.right.right.axis, 1) | ||
assert.closeTo(root.left.right.right.split, 0.5748, 0.0001) | ||
|
||
assert.deepEqual(root.left.right.right.left.object, seoul) | ||
assert.deepEqual(root.left.right.right.right.object, beijing) | ||
|
||
assert.equal(root.right.axis, 1) | ||
assert.closeTo(root.right.split, 0.0309, 0.0001) | ||
|
||
assert.equal(root.right.left.axis, 1) | ||
assert.closeTo(root.right.left.split, -0.6373, 0.0001) | ||
|
||
assert.deepEqual(root.right.left.left.object, troy) | ||
|
||
assert.equal(root.right.left.right.axis, 1) | ||
assert.closeTo(root.right.left.right.split, -0.0017, 0.0001) | ||
|
||
assert.deepEqual(root.right.left.right.left.object, boston) | ||
assert.deepEqual(root.right.left.right.right.object, london) | ||
|
||
assert.equal(root.right.right.axis, 1) | ||
assert.closeTo(root.right.right.split, 0.1445, 0.0001) | ||
|
||
assert.deepEqual(root.right.right.left.object, paris) | ||
|
||
assert.equal(root.right.right.right.axis, 2) | ||
assert.closeTo(root.right.right.right.split, 0.7443, 0.0001) | ||
|
||
assert.deepEqual(root.right.right.right.left.object, vienna) | ||
assert.deepEqual(root.right.right.right.right.object, rome) | ||
}) | ||
}) | ||
|
||
describe("lookup", function() { | ||
var tree = lib.build(cities) | ||
|
||
it("should return New York, Troy, Boston, and Miami as the four closest cities to Philadelphia", function() { | ||
assert.deepEqual( | ||
lib.lookup(39.95, -75.17, tree, 4), | ||
[newYork, troy, boston, miami] | ||
) | ||
}) | ||
|
||
it("should return Vienna, Paris, Rome, and London as the four closest cities to Berlin", function() { | ||
assert.deepEqual( | ||
lib.lookup(52.50, 13.40, tree, 4), | ||
[vienna, paris, rome, london] | ||
) | ||
}) | ||
|
||
it("should return Miami and Hong Kong as the two closest cities to Hawaii", function() { | ||
assert.deepEqual( | ||
lib.lookup(21.31, -157.80, tree, 2), | ||
[miami, hongKong] | ||
) | ||
}) | ||
}) | ||
}) | ||
}) |