loopback-datasource-juggler/lib/geo.js

289 lines
7.2 KiB
JavaScript
Raw Normal View History

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
2016-08-22 19:55:22 +00:00
'use strict';
var assert = require('assert');
/*!
* Get a near filter from a given where object. For connector use only.
*/
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) {
var ex = where[key];
2014-01-24 17:09:53 +00:00
if (ex && ex.near) {
result = {
near: ex.near,
maxDistance: ex.maxDistance,
minDistance: ex.minDistance,
unit: ex.unit,
2016-04-01 11:48:17 +00:00
key: key,
};
}
});
}
2014-01-24 17:09:53 +00:00
return result;
2016-04-01 11:48:17 +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;
var min = filter.minDistance > 0 ? filter.minDistance : false;
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-08-19 17:46:59 +00:00
var d = GeoPoint.distanceBetween(origin, loc, {type: unit});
2014-01-24 17:09:53 +00:00
if (min && d < min) {
return;
}
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) {
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;
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
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.
* @property {Number} lng The longitude point in degrees. Range: -180 to 180.
*
* @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
*/
function GeoPoint(data) {
2014-01-24 17:09:53 +00:00
if (!(this instanceof GeoPoint)) {
return new GeoPoint(data);
}
2014-01-24 17:09:53 +00:00
2016-04-01 11:48:17 +00:00
if (arguments.length === 2) {
data = {
lat: arguments[0],
2016-04-01 11:48:17 +00:00
lng: arguments[1],
};
}
2016-04-01 13:23:42 +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-01-24 17:09:53 +00:00
if (typeof data === 'string') {
2013-06-26 03:31:00 +00:00
data = data.split(/,\s*/);
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 = {
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
assert(typeof data === 'object', 'must provide a lat and lng object when creating a GeoPoint');
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');
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;
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`
*/
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
var x1 = a.lat;
var y1 = a.lng;
2014-01-24 17:09:53 +00:00
var x2 = b.lat;
var y2 = b.lng;
2014-01-24 17:09:53 +00:00
return geoDistance(x1, y1, x2, y2, options);
};
/**
* 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`
*/
2016-04-01 11:48:17 +00:00
GeoPoint.prototype.distanceTo = function(point, options) {
return GeoPoint.distanceBetween(this, point, options);
};
2013-06-26 03:31:00 +00:00
/**
* Simple serialization.
*/
2016-04-01 11:48:17 +00:00
GeoPoint.prototype.toString = function() {
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;
2014-03-12 23:28:46 +00:00
// factor to convert radians degrees to degrees
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,
};
function geoDistance(x1, y1, x2, y2, options) {
var type = (options && options.type) || 'miles';
// 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.
// ref http://en.wikipedia.org/wiki/Haversine_formula
var haversine = function(a) {
return Math.pow(Math.sin(a / 2.0), 2);
};
2014-12-10 22:08:56 +00:00
var f = Math.sqrt(haversine(x2 - x1) + Math.cos(x2) * Math.cos(x1) * haversine(y2 - y1));
2014-12-10 22:08:56 +00:00
return 2 * Math.asin(f) * EARTH_RADIUS[type];
2013-06-26 03:31:00 +00:00
}