Skip to content
This repository has been archived by the owner on Apr 25, 2020. It is now read-only.

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason LaPorte committed Oct 11, 2012
0 parents commit 1e07e09
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
/node_modules
7 changes: 7 additions & 0 deletions README.md
@@ -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.
11 changes: 11 additions & 0 deletions index.js
@@ -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)
}
}
172 changes: 172 additions & 0 deletions lib.js
@@ -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
117 changes: 117 additions & 0 deletions test.js
@@ -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]
)
})
})
})
})

0 comments on commit 1e07e09

Please sign in to comment.