2013-06-24 23:38:50 +00:00
|
|
|
/**
|
|
|
|
* Dependencies.
|
|
|
|
*/
|
|
|
|
|
|
|
|
var assert = require('assert');
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* Get a near filter from a given where object. For adapter use only.
|
|
|
|
*/
|
|
|
|
|
|
|
|
exports.nearFilter = function nearFilter(where) {
|
|
|
|
var result = false;
|
|
|
|
|
|
|
|
if(where && typeof where === 'object') {
|
|
|
|
Object.keys(where).forEach(function (key) {
|
|
|
|
var ex = where[key];
|
|
|
|
|
|
|
|
if(ex && ex.near) {
|
|
|
|
result = {
|
|
|
|
near: ex.near,
|
|
|
|
maxDistance: ex.maxDistance,
|
|
|
|
key: key
|
|
|
|
};
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* Filter a set of objects using the given `nearFilter`.
|
|
|
|
*/
|
|
|
|
|
2013-06-24 22:21:59 +00:00
|
|
|
exports.filter = function (arr, filter) {
|
|
|
|
var origin = filter.near;
|
|
|
|
var max = filter.maxDistance > 0 ? filter.maxDistance : false;
|
|
|
|
var key = filter.key;
|
|
|
|
|
|
|
|
// create distance index
|
|
|
|
var distances = {};
|
|
|
|
var result = [];
|
|
|
|
|
|
|
|
arr.forEach(function (obj) {
|
|
|
|
var loc = obj[key];
|
|
|
|
|
|
|
|
// filter out objects without locations
|
|
|
|
if(!loc) return;
|
2013-06-26 03:31:00 +00:00
|
|
|
|
|
|
|
if(!(loc instanceof GeoPoint)) {
|
|
|
|
loc = GeoPoint(loc);
|
|
|
|
}
|
|
|
|
|
2013-06-24 22:21:59 +00:00
|
|
|
if(typeof loc.lat !== 'number') return;
|
2013-06-24 22:32:34 +00:00
|
|
|
if(typeof loc.lng !== 'number') return;
|
2013-06-24 22:21:59 +00:00
|
|
|
|
2013-06-24 23:38:50 +00:00
|
|
|
var d = GeoPoint.distanceBetween(origin, loc);
|
2013-06-24 22:21:59 +00:00
|
|
|
|
|
|
|
if(max && d > max) {
|
|
|
|
// dont add
|
|
|
|
} else {
|
|
|
|
distances[obj.id] = d;
|
|
|
|
result.push(obj);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return result.sort(function (objA, objB) {
|
|
|
|
var a = objB[key];
|
|
|
|
var b = objB[key];
|
|
|
|
|
|
|
|
if(a && b) {
|
|
|
|
var da = distances[objA.id];
|
|
|
|
var db = distances[objB.id];
|
|
|
|
|
|
|
|
if(db === da) return 0;
|
2013-06-24 23:38:50 +00:00
|
|
|
return da > db ? 1 : -1;
|
2013-06-24 22:21:59 +00:00
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2013-06-24 23:38:50 +00:00
|
|
|
/**
|
|
|
|
* Export the `GeoPoint` class.
|
|
|
|
*/
|
|
|
|
|
|
|
|
exports.GeoPoint = GeoPoint;
|
|
|
|
|
|
|
|
function GeoPoint(data) {
|
|
|
|
if(!(this instanceof GeoPoint)) {
|
|
|
|
return new GeoPoint(data);
|
|
|
|
}
|
|
|
|
|
2013-06-26 03:31:00 +00:00
|
|
|
if(typeof data === 'string') {
|
|
|
|
data = data.split(/,\s*/);
|
|
|
|
assert(data.length === 2, 'must provide a string "lng,lat" creating a GeoPoint with a string');
|
|
|
|
}
|
|
|
|
if(Array.isArray(data)) {
|
|
|
|
data = {
|
|
|
|
lng: Number(data[0]),
|
|
|
|
lat: Number(data[1])
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
data.lng = Number(data.lng);
|
|
|
|
data.lat = Number(data.lat);
|
|
|
|
}
|
|
|
|
|
2013-06-24 23:38:50 +00:00
|
|
|
assert(typeof data === 'object', 'must provide a lat and lng object when creating a GeoPoint');
|
|
|
|
assert(typeof data.lat === 'number', 'lat must be a number when creating a GeoPoint');
|
|
|
|
assert(typeof data.lng === 'number', 'lng must be a number when creating a GeoPoint');
|
|
|
|
assert(data.lng <= 180, 'lng must be <= 180');
|
|
|
|
assert(data.lng >= -180, 'lng must be >= -180');
|
|
|
|
assert(data.lat <= 90, 'lat must be <= 90');
|
|
|
|
assert(data.lat >= -90, 'lat must be >= -90');
|
2013-06-24 22:21:59 +00:00
|
|
|
|
2013-06-24 23:38:50 +00:00
|
|
|
this.lat = data.lat;
|
|
|
|
this.lng = data.lng;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine the spherical distance between two geo points.
|
|
|
|
*/
|
|
|
|
|
|
|
|
GeoPoint.distanceBetween = function distanceBetween(a, b, options) {
|
2013-06-26 03:31:00 +00:00
|
|
|
if(!(a instanceof GeoPoint)) {
|
|
|
|
a = GeoPoint(a);
|
|
|
|
}
|
|
|
|
if(!(b instanceof GeoPoint)) {
|
|
|
|
b = GeoPoint(b);
|
|
|
|
}
|
|
|
|
|
2013-06-24 23:38:50 +00:00
|
|
|
var x1 = a.lat;
|
|
|
|
var y1 = a.lng;
|
|
|
|
|
|
|
|
var x2 = b.lat;
|
|
|
|
var y2 = b.lng;
|
|
|
|
|
|
|
|
return geoDistance(x1, y1, x2, y2, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine the spherical distance to the given point.
|
|
|
|
*/
|
|
|
|
|
|
|
|
GeoPoint.prototype.distanceTo = function (point, options) {
|
|
|
|
return GeoPoint.distanceBetween(this, point, options);
|
|
|
|
}
|
|
|
|
|
2013-06-26 03:31:00 +00:00
|
|
|
/**
|
|
|
|
* Simple serialization.
|
|
|
|
*/
|
|
|
|
|
|
|
|
GeoPoint.prototype.toString = function () {
|
|
|
|
return this.lng + ',' + this.lat;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Si
|
|
|
|
*/
|
|
|
|
|
2013-06-24 23:38:50 +00:00
|
|
|
// ratio of a circle's circumference to its diameter
|
|
|
|
var PI = 3.1415926535897932384626433832795;
|
|
|
|
|
|
|
|
// factor to convert decimal degrees to radians
|
|
|
|
var DEG2RAD = 0.01745329252;
|
|
|
|
|
|
|
|
// factor to convert decimal degrees to radians
|
|
|
|
var RAD2DEG = 57.29577951308;
|
|
|
|
|
|
|
|
// radius of the earth
|
|
|
|
var EARTH_RADIUS = {
|
|
|
|
kilometers: 6370.99056,
|
|
|
|
meters: 6370990.56,
|
|
|
|
miles: 3958.75,
|
|
|
|
feet: 20902200,
|
|
|
|
radians: 1,
|
|
|
|
degrees: RAD2DEG
|
|
|
|
};
|
|
|
|
|
|
|
|
function geoDistance(x1, y1, x2, y2, options) {
|
|
|
|
// Convert to radians
|
|
|
|
x1 = x1 * DEG2RAD;
|
|
|
|
y1 = y1 * DEG2RAD;
|
|
|
|
x2 = x2 * DEG2RAD;
|
|
|
|
y2 = y2 * DEG2RAD;
|
|
|
|
|
2013-06-27 19:21:31 +00:00
|
|
|
var a = Math.pow(Math.sin(( y2-y1 ) / 2.0 ), 2);
|
|
|
|
var b = Math.pow(Math.sin(( x2-x1 ) / 2.0 ), 2);
|
|
|
|
var c = Math.sqrt( a + Math.cos( y2 ) * Math.cos( y1 ) * b );
|
2013-06-24 23:38:50 +00:00
|
|
|
|
|
|
|
var type = (options && options.type) || 'miles';
|
|
|
|
|
|
|
|
return 2 * Math.asin( c ) * EARTH_RADIUS[type];
|
2013-06-26 03:31:00 +00:00
|
|
|
}
|
|
|
|
|