From d730aab674dfa5190e2419fc28d3c2de827c0c3e Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 24 Jun 2013 16:38:50 -0700 Subject: [PATCH] Move geo filter creation into reusable module. --- lib/adapters/memory.js | 22 +------- lib/geo.js | 124 +++++++++++++++++++++++++++++++++++++---- lib/model-builder.js | 2 + 3 files changed, 117 insertions(+), 31 deletions(-) diff --git a/lib/adapters/memory.js b/lib/adapters/memory.js index 17b614bd..39295986 100644 --- a/lib/adapters/memory.js +++ b/lib/adapters/memory.js @@ -133,7 +133,7 @@ Memory.prototype.all = function all(model, filter, callback) { nodes = nodes.sort(sorting.bind(orders)); } - var nearFilter = nearFilter(filter.where); + var nearFilter = geo.nearFilter(filter.where); // geo sorting if(nearFilter) { @@ -170,26 +170,6 @@ Memory.prototype.all = function all(model, filter, callback) { } return 0; } - - 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; - } }; function applyFilter(filter) { diff --git a/lib/geo.js b/lib/geo.js index 69f781ce..9b42a895 100644 --- a/lib/geo.js +++ b/lib/geo.js @@ -1,3 +1,37 @@ +/** + * 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`. + */ + exports.filter = function (arr, filter) { var origin = filter.near; var max = filter.maxDistance > 0 ? filter.maxDistance : false; @@ -15,12 +49,13 @@ exports.filter = function (arr, filter) { if(typeof loc.lat !== 'number') return; if(typeof loc.lng !== 'number') return; - var d = distanceBetween(origin, loc); + var d = GeoPoint.distanceBetween(origin, loc); if(max && d > max) { // dont add } else { distances[obj.id] = d; + loc.distance = d; result.push(obj); } }); @@ -34,20 +69,89 @@ exports.filter = function (arr, filter) { var db = distances[objB.id]; if(db === da) return 0; - return da > db ? -1 : 1; + return da > db ? 1 : -1; } else { return 0; } }); } -var distanceBetween = exports.distanceBetween = function distanceBetween(a, b) { - var xs = 0; - var ys = 0; - xs = a.lat - b.lat; - xs = xs * xs; - ys = a.lng - b.lng; - ys = ys * ys; +/** + * Export the `GeoPoint` class. + */ + +exports.GeoPoint = GeoPoint; + +function GeoPoint(data) { + if(!(this instanceof GeoPoint)) { + return new GeoPoint(data); + } - return Math.sqrt( xs + ys ); + 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'); + + this.lat = data.lat; + this.lng = data.lng; +} + +/** + * Determine the spherical distance between two geo points. + */ + +GeoPoint.distanceBetween = function distanceBetween(a, b, options) { + 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); +} + +// 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; + + a = Math.pow(Math.sin(( y2-y1 ) / 2.0 ), 2); + b = Math.pow(Math.sin(( x2-x1 ) / 2.0 ), 2); + c = Math.sqrt( a + Math.cos( y2 ) * Math.cos( y1 ) * b ); + + var type = (options && options.type) || 'miles'; + + return 2 * Math.asin( c ) * EARTH_RADIUS[type]; } \ No newline at end of file diff --git a/lib/model-builder.js b/lib/model-builder.js index 51b8e534..2e9bdf9f 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -7,6 +7,7 @@ var ModelBaseClass = require('./model.js'); var List = require('./list.js'); var EventEmitter = require('events').EventEmitter; var util = require('util'); +var GeoPoint = require('./geo').GeoPoint; /** * Export public API @@ -46,6 +47,7 @@ ModelBuilder.registerType(Boolean); ModelBuilder.registerType(Date); ModelBuilder.registerType(Buffer, ['Binary']); ModelBuilder.registerType(Array); +ModelBuilder.registerType(GeoPoint); /**