2019-05-08 15:45:37 +00:00
|
|
|
|
// Copyright IBM Corp. 2013,2018. All Rights Reserved.
|
2016-04-01 22:25:16 +00:00
|
|
|
|
// 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';
|
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const assert = require('assert');
|
2013-06-24 23:38:50 +00:00
|
|
|
|
|
|
|
|
|
/*!
|
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) {
|
2018-12-07 15:22:36 +00:00
|
|
|
|
const nearResults = [];
|
|
|
|
|
nearSearch(where);
|
|
|
|
|
return (!nearResults.length ? false : nearResults);
|
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
function nearSearch(clause, parentKeys) {
|
|
|
|
|
if (typeof clause !== 'object') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
parentKeys = parentKeys || [];
|
|
|
|
|
|
|
|
|
|
Object.keys(clause).forEach(function(clauseKey) {
|
2017-04-24 23:28:11 +00:00
|
|
|
|
if (typeof clause[clauseKey] !== 'object' || !clause[clauseKey]) return;
|
2017-04-13 21:05:05 +00:00
|
|
|
|
if (Array.isArray(clause[clauseKey])) {
|
|
|
|
|
clause[clauseKey].forEach(function(el, index) {
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const ret = nearSearch(el, parentKeys.concat(clauseKey).concat(index));
|
2017-04-13 21:05:05 +00:00
|
|
|
|
if (ret) return ret;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
if (clause[clauseKey].hasOwnProperty('near')) {
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const result = clause[clauseKey];
|
2017-04-13 21:05:05 +00:00
|
|
|
|
nearResults.push({
|
|
|
|
|
near: result.near,
|
|
|
|
|
maxDistance: result.maxDistance,
|
|
|
|
|
minDistance: result.minDistance,
|
|
|
|
|
unit: result.unit,
|
|
|
|
|
// If key is at root, define a single string, otherwise append it to the full path array
|
|
|
|
|
mongoKey: parentKeys.length ? parentKeys.concat(clauseKey) : clauseKey,
|
|
|
|
|
key: clauseKey,
|
|
|
|
|
});
|
|
|
|
|
}
|
2018-12-07 14:43:40 +00:00
|
|
|
|
}
|
2013-06-24 23:38:50 +00:00
|
|
|
|
});
|
|
|
|
|
}
|
2016-04-01 11:48:17 +00:00
|
|
|
|
};
|
2013-06-24 23:38:50 +00:00
|
|
|
|
|
|
|
|
|
/*!
|
2017-04-13 21:05:05 +00:00
|
|
|
|
* Filter a set of results using the given filters returned by `nearFilter()`.
|
|
|
|
|
* Can support multiple locations, but will include results from all of them.
|
|
|
|
|
*
|
|
|
|
|
* WARNING: "or" operator with GeoPoint does not work as expected, eg:
|
|
|
|
|
* {where: {or: [{location: {near: (29,-90)}},{name:'Sean'}]}}
|
|
|
|
|
* Will actually work as if you had used "and". This is because geo filtering
|
|
|
|
|
* takes place outside of the SQL query, so the result set of "name = Sean" is
|
|
|
|
|
* returned by the database, and then the location filtering happens in the app
|
|
|
|
|
* logic. So the "near" operator is always an "and" of the SQL filters, and "or"
|
|
|
|
|
* of other GeoPoint filters.
|
|
|
|
|
*
|
|
|
|
|
* Additionally, since this step occurs after the SQL result set is returned,
|
|
|
|
|
* if using GeoPoints with pagination the result set may be smaller than the
|
|
|
|
|
* page size. The page size is enforced at the DB level, and then we may
|
|
|
|
|
* remove results at the Geo-app level. If we "limit: 25", but 4 of those results
|
|
|
|
|
* do not have a matching geopoint field, the request will only return 21 results.
|
|
|
|
|
* This may make it erroneously look like a given page is the end of the result set.
|
2013-06-24 23:38:50 +00:00
|
|
|
|
*/
|
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
exports.filter = function(rawResults, filters) {
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const distances = {};
|
|
|
|
|
const results = [];
|
2014-01-24 17:09:53 +00:00
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
filters.forEach(function(filter) {
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const origin = filter.near;
|
|
|
|
|
const max = filter.maxDistance > 0 ? filter.maxDistance : false;
|
|
|
|
|
const min = filter.minDistance > 0 ? filter.minDistance : false;
|
|
|
|
|
const unit = filter.unit;
|
|
|
|
|
const key = filter.key;
|
2014-01-24 17:09:53 +00:00
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
// create distance index
|
|
|
|
|
rawResults.forEach(function(result) {
|
2018-12-07 14:54:29 +00:00
|
|
|
|
let loc = result[key];
|
2014-01-24 17:09:53 +00:00
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
// filter out results without locations
|
|
|
|
|
if (!loc) return;
|
2014-01-24 17:09:53 +00:00
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
if (!(loc instanceof GeoPoint)) loc = GeoPoint(loc);
|
2014-01-24 17:09:53 +00:00
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
if (typeof loc.lat !== 'number') return;
|
|
|
|
|
if (typeof loc.lng !== 'number') return;
|
2014-01-24 17:09:53 +00:00
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const d = GeoPoint.distanceBetween(origin, loc, {type: unit});
|
2014-01-24 17:09:53 +00:00
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
// filter result if distance is either < minDistance or > maxDistance
|
|
|
|
|
if ((min && d < min) || (max && d > max)) return;
|
2017-04-06 12:25:04 +00:00
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
distances[result.id] = d;
|
|
|
|
|
results.push(result);
|
|
|
|
|
});
|
2017-04-06 12:25:04 +00:00
|
|
|
|
|
2017-04-13 21:05:05 +00:00
|
|
|
|
results.sort(function(resA, resB) {
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const a = resA[key];
|
|
|
|
|
const b = resB[key];
|
2017-04-13 21:05:05 +00:00
|
|
|
|
|
|
|
|
|
if (a && b) {
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const da = distances[resA.id];
|
|
|
|
|
const db = distances[resB.id];
|
2017-04-13 21:05:05 +00:00
|
|
|
|
|
|
|
|
|
if (db === da) return 0;
|
|
|
|
|
return da > db ? 1 : -1;
|
|
|
|
|
} else {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
2013-06-24 22:21:59 +00:00
|
|
|
|
});
|
2017-04-13 21:05:05 +00:00
|
|
|
|
|
|
|
|
|
return results;
|
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
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
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-12-11 00:36:02 +00:00
|
|
|
|
|
2014-01-24 17:09:53 +00:00
|
|
|
|
if (typeof data === 'string') {
|
2017-04-13 21:05:05 +00:00
|
|
|
|
try {
|
|
|
|
|
data = JSON.parse(data);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
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 = {
|
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
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const x1 = a.lat;
|
|
|
|
|
const y1 = a.lng;
|
2014-01-24 17:09:53 +00:00
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const x2 = b.lat;
|
|
|
|
|
const 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
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const 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
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const RAD2DEG = 57.29577951308;
|
2013-06-24 23:38:50 +00:00
|
|
|
|
|
|
|
|
|
// radius of the earth
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const EARTH_RADIUS = {
|
2013-06-24 23:38:50 +00:00
|
|
|
|
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) {
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const type = (options && options.type) || 'miles';
|
2014-12-10 21:30:07 +00:00
|
|
|
|
|
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
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const 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
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
|
const 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
|
|
|
|
}
|