2016-04-01 22:25:16 +00:00
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
2013-06-24 23:38:50 +00:00
var assert = require ( 'assert' ) ;
/ * !
2013-07-23 18:16:43 +00:00
* Get a near filter from a given where object . For connector use only .
2013-06-24 23:38:50 +00:00
* /
exports . nearFilter = function nearFilter ( where ) {
var result = false ;
2014-01-24 17:09:53 +00:00
if ( where && typeof where === 'object' ) {
2016-04-01 11:48:17 +00:00
Object . keys ( where ) . forEach ( function ( key ) {
2013-06-24 23:38:50 +00:00
var ex = where [ key ] ;
2014-01-24 17:09:53 +00:00
if ( ex && ex . near ) {
2013-06-24 23:38:50 +00:00
result = {
near : ex . near ,
maxDistance : ex . maxDistance ,
2015-08-05 18:18:28 +00:00
unit : ex . unit ,
2016-04-01 11:48:17 +00:00
key : key ,
2013-06-24 23:38:50 +00:00
} ;
}
} ) ;
}
2014-01-24 17:09:53 +00:00
2013-06-24 23:38:50 +00:00
return result ;
2016-04-01 11:48:17 +00:00
} ;
2013-06-24 23:38:50 +00:00
/ * !
* Filter a set of objects using the given ` nearFilter ` .
* /
2016-04-01 11:48:17 +00:00
exports . filter = function ( arr , filter ) {
2013-06-24 22:21:59 +00:00
var origin = filter . near ;
var max = filter . maxDistance > 0 ? filter . maxDistance : false ;
2015-08-05 18:18:28 +00:00
var unit = filter . unit ;
2013-06-24 22:21:59 +00:00
var key = filter . key ;
2014-01-24 17:09:53 +00:00
2013-06-24 22:21:59 +00:00
// create distance index
var distances = { } ;
var result = [ ] ;
2014-01-24 17:09:53 +00:00
2016-04-01 11:48:17 +00:00
arr . forEach ( function ( obj ) {
2013-06-24 22:21:59 +00:00
var loc = obj [ key ] ;
2014-01-24 17:09:53 +00:00
2013-06-24 22:21:59 +00:00
// filter out objects without locations
2014-01-24 17:09:53 +00:00
if ( ! loc ) return ;
if ( ! ( loc instanceof GeoPoint ) ) {
2013-06-26 03:31:00 +00:00
loc = GeoPoint ( loc ) ;
}
2014-01-24 17:09:53 +00:00
if ( typeof loc . lat !== 'number' ) return ;
if ( typeof loc . lng !== 'number' ) return ;
2016-04-01 11:48:17 +00:00
var d = GeoPoint . distanceBetween ( origin , loc , { type : unit } ) ;
2014-01-24 17:09:53 +00:00
if ( max && d > max ) {
2013-06-24 22:21:59 +00:00
// dont add
} else {
distances [ obj . id ] = d ;
result . push ( obj ) ;
}
} ) ;
2014-01-24 17:09:53 +00:00
2016-04-01 11:48:17 +00:00
return result . sort ( function ( objA , objB ) {
2015-08-05 18:18:28 +00:00
var a = objA [ key ] ;
2013-06-24 22:21:59 +00:00
var b = objB [ key ] ;
2014-01-24 17:09:53 +00:00
if ( a && b ) {
2013-06-24 22:21:59 +00:00
var da = distances [ objA . id ] ;
var db = distances [ objB . id ] ;
2014-01-24 17:09:53 +00:00
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 ;
}
} ) ;
2016-04-01 11:48:17 +00:00
} ;
2013-06-24 22:21:59 +00:00
2013-06-24 23:38:50 +00:00
exports . GeoPoint = GeoPoint ;
2016-04-01 11:48:17 +00:00
/ * *
2014-03-12 23:28:46 +00:00
* The GeoPoint object represents a physical location .
2016-04-01 11:48:17 +00:00
*
2014-03-12 23:28:46 +00:00
* For example :
2016-04-01 11:48:17 +00:00
*
2014-03-12 23:28:46 +00:00
* ` ` ` js
2015-05-04 15:45:17 +00:00
* var loopback = require ( ‘ loopback ’ ) ;
* var here = new loopback . GeoPoint ( { lat : 10.32424 , lng : 5.84978 } ) ;
2014-03-12 23:28:46 +00:00
* ` ` `
2016-04-01 11:48:17 +00:00
*
2014-03-12 23:28:46 +00:00
* Embed a latitude / longitude point in a model .
2016-04-01 11:48:17 +00:00
*
2014-03-12 23:28:46 +00:00
* ` ` ` js
* var CoffeeShop = loopback . createModel ( 'coffee-shop' , {
* location : 'GeoPoint'
* } ) ;
* ` ` `
2016-04-01 11:48:17 +00:00
*
2014-03-12 23:28:46 +00:00
* You can query LoopBack models with a GeoPoint property and an attached data source using geo - spatial filters and
* sorting . For example , the following code finds the three nearest coffee shops .
2016-04-01 11:48:17 +00:00
*
2014-03-12 23:28:46 +00:00
* ` ` ` js
* CoffeeShop . attachTo ( oracle ) ;
* var here = new GeoPoint ( { lat : 10.32424 , lng : 5.84978 } ) ;
* CoffeeShop . find ( { where : { location : { near : here } } , limit : 3 } , function ( err , nearbyShops ) {
* console . info ( nearbyShops ) ; // [CoffeeShop, ...]
* } ) ;
* ` ` `
* @ class GeoPoint
2016-04-01 11:48:17 +00:00
* @ property { Number } lat The latitude in degrees .
* @ property { Number } lng The longitude in degrees .
*
2014-06-11 22:47:44 +00:00
* @ options { Object } Options Object with two Number properties : lat and long .
* @ property { Number } lat The latitude point in degrees . Range : - 90 to 90.
2014-12-11 00:36:02 +00:00
* @ property { Number } lng The longitude point in degrees . Range : - 180 to 180.
2014-12-18 15:47:06 +00:00
*
* @ options { Array } Options Array with two Number entries : [ lat , long ] .
* @ property { Number } lat The latitude point in degrees . Range : - 90 to 90.
* @ property { Number } lng The longitude point in degrees . Range : - 180 to 180.
2014-03-12 23:28:46 +00:00
* /
2013-06-24 23:38:50 +00:00
function GeoPoint ( data ) {
2014-01-24 17:09:53 +00:00
if ( ! ( this instanceof GeoPoint ) ) {
2013-06-24 23:38:50 +00:00
return new GeoPoint ( data ) ;
}
2014-01-24 17:09:53 +00:00
2016-04-01 11:48:17 +00:00
if ( arguments . length === 2 ) {
2014-12-18 15:47:06 +00:00
data = {
lat : arguments [ 0 ] ,
2016-04-01 11:48:17 +00:00
lng : arguments [ 1 ] ,
2014-12-18 15:47:06 +00:00
} ;
}
assert ( Array . isArray ( data ) || typeof data === 'object' || typeof data === 'string' , 'must provide valid geo-coordinates array [lat, lng] or object or a "lat, lng" string' ) ;
2014-12-11 00:36:02 +00:00
2014-01-24 17:09:53 +00:00
if ( typeof data === 'string' ) {
2013-06-26 03:31:00 +00:00
data = data . split ( /,\s*/ ) ;
2014-12-11 00:36:02 +00:00
assert ( data . length === 2 , 'must provide a string "lat,lng" creating a GeoPoint with a string' ) ;
2013-06-26 03:31:00 +00:00
}
2014-01-24 17:09:53 +00:00
if ( Array . isArray ( data ) ) {
2013-06-26 03:31:00 +00:00
data = {
2014-12-11 00:36:02 +00:00
lat : Number ( data [ 0 ] ) ,
2016-04-01 11:48:17 +00:00
lng : Number ( data [ 1 ] ) ,
2013-06-26 03:31:00 +00:00
} ;
} else {
data . lng = Number ( data . lng ) ;
data . lat = Number ( data . lat ) ;
}
2014-01-24 17:09:53 +00:00
2013-06-24 23:38:50 +00:00
assert ( typeof data === 'object' , 'must provide a lat and lng object when creating a GeoPoint' ) ;
2013-12-14 17:49:11 +00:00
assert ( typeof data . lat === 'number' && ! isNaN ( data . lat ) , 'lat must be a number when creating a GeoPoint' ) ;
assert ( typeof data . lng === 'number' && ! isNaN ( data . lng ) , 'lng must be a number when creating a GeoPoint' ) ;
2013-06-24 23:38:50 +00:00
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' ) ;
2014-01-24 17:09:53 +00:00
2016-04-01 11:48:17 +00:00
this . lat = data . lat ;
2013-06-24 23:38:50 +00:00
this . lng = data . lng ;
}
/ * *
2014-03-12 23:28:46 +00:00
* Determine the spherical distance between two GeoPoints .
2016-04-01 11:48:17 +00:00
*
2014-06-11 22:47:44 +00:00
* @ param { GeoPoint } pointA Point A
* @ param { GeoPoint } pointB Point B
* @ options { Object } options Options object with one key , 'type' . See below .
* @ property { String } type Unit of measurement , one of :
2016-04-01 11:48:17 +00:00
*
2014-03-12 23:28:46 +00:00
* - ` miles ` ( default )
* - ` radians `
* - ` kilometers `
* - ` meters `
* - ` miles `
* - ` feet `
* - ` degrees `
2013-06-24 23:38:50 +00:00
* /
GeoPoint . distanceBetween = function distanceBetween ( a , b , options ) {
2014-01-24 17:09:53 +00:00
if ( ! ( a instanceof GeoPoint ) ) {
2013-06-26 03:31:00 +00:00
a = GeoPoint ( a ) ;
}
2014-01-24 17:09:53 +00:00
if ( ! ( b instanceof GeoPoint ) ) {
2013-06-26 03:31:00 +00:00
b = GeoPoint ( b ) ;
}
2014-01-24 17:09:53 +00:00
2013-06-24 23:38:50 +00:00
var x1 = a . lat ;
var y1 = a . lng ;
2014-01-24 17:09:53 +00:00
2013-06-24 23:38:50 +00:00
var x2 = b . lat ;
var y2 = b . lng ;
2014-01-24 17:09:53 +00:00
2013-06-24 23:38:50 +00:00
return geoDistance ( x1 , y1 , x2 , y2 , options ) ;
2014-12-11 00:36:02 +00:00
} ;
2013-06-24 23:38:50 +00:00
/ * *
* Determine the spherical distance to the given point .
2014-03-12 23:28:46 +00:00
* Example :
* ` ` ` js
2015-05-04 15:45:17 +00:00
* var loopback = require ( ‘ loopback ’ ) ;
2016-04-01 11:48:17 +00:00
*
2015-05-04 15:45:17 +00:00
* var here = new loopback . GeoPoint ( { lat : 10 , lng : 10 } ) ;
* var there = new loopback . GeoPoint ( { lat : 5 , lng : 5 } ) ;
2016-04-01 11:48:17 +00:00
*
2015-05-04 15:45:17 +00:00
* loopback . GeoPoint . distanceBetween ( here , there , { type : 'miles' } ) // 438
2014-03-12 23:28:46 +00:00
* ` ` `
* @ param { Object } point GeoPoint object to which to measure distance .
2014-06-11 22:47:44 +00:00
* @ options { Object } options Options object with one key , 'type' . See below .
* @ property { String } type Unit of measurement , one of :
2016-04-01 11:48:17 +00:00
*
2014-06-11 22:47:44 +00:00
* - ` miles ` ( default )
* - ` radians `
* - ` kilometers `
* - ` meters `
* - ` miles `
* - ` feet `
* - ` degrees `
2013-06-24 23:38:50 +00:00
* /
2016-04-01 11:48:17 +00:00
GeoPoint . prototype . distanceTo = function ( point , options ) {
2013-06-24 23:38:50 +00:00
return GeoPoint . distanceBetween ( this , point , options ) ;
2014-12-11 00:36:02 +00:00
} ;
2013-06-24 23:38:50 +00:00
2013-06-26 03:31:00 +00:00
/ * *
* Simple serialization .
* /
2016-04-01 11:48:17 +00:00
GeoPoint . prototype . toString = function ( ) {
2014-12-11 00:36:02 +00:00
return this . lat + ',' + this . lng ;
} ;
2013-06-26 03:31:00 +00:00
/ * *
2014-03-12 23:28:46 +00:00
* @ property { Number } DEG2RAD - Factor to convert degrees to radians .
* @ property { Number } RAD2DEG - Factor to convert radians to degrees .
* @ property { Object } EARTH _RADIUS - Radius of the earth .
* /
2013-06-26 03:31:00 +00:00
2014-03-12 23:28:46 +00:00
// factor to convert degrees to radians
2014-01-24 17:09:53 +00:00
var DEG2RAD = 0.01745329252 ;
2013-06-24 23:38:50 +00:00
2014-03-12 23:28:46 +00:00
// factor to convert radians degrees to degrees
2013-06-24 23:38:50 +00:00
var RAD2DEG = 57.29577951308 ;
// radius of the earth
var EARTH _RADIUS = {
kilometers : 6370.99056 ,
meters : 6370990.56 ,
miles : 3958.75 ,
feet : 20902200 ,
radians : 1 ,
2016-04-01 11:48:17 +00:00
degrees : RAD2DEG ,
2013-06-24 23:38:50 +00:00
} ;
function geoDistance ( x1 , y1 , x2 , y2 , options ) {
2014-12-10 21:30:07 +00:00
var type = ( options && options . type ) || 'miles' ;
2013-06-24 23:38:50 +00:00
// Convert to radians
x1 = x1 * DEG2RAD ;
y1 = y1 * DEG2RAD ;
x2 = x2 * DEG2RAD ;
y2 = y2 * DEG2RAD ;
2016-04-01 11:48:17 +00:00
// use the haversine formula to calculate distance for any 2 points on a sphere.
2014-12-10 21:30:07 +00:00
// ref http://en.wikipedia.org/wiki/Haversine_formula
var haversine = function ( a ) {
2014-12-11 00:36:02 +00:00
return Math . pow ( Math . sin ( a / 2.0 ) , 2 ) ;
} ;
2013-06-24 23:38:50 +00:00
2014-12-10 22:08:56 +00:00
var f = Math . sqrt ( haversine ( x2 - x1 ) + Math . cos ( x2 ) * Math . cos ( x1 ) * haversine ( y2 - y1 ) ) ;
2013-06-24 23:38:50 +00:00
2014-12-10 22:08:56 +00:00
return 2 * Math . asin ( f ) * EARTH _RADIUS [ type ] ;
2013-06-26 03:31:00 +00:00
}