geolocation-utils
Version:
A utility library for calculations with geolocations
642 lines (548 loc) • 18.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.EARTH_RADIUS = undefined;
exports.isEqual = isEqual;
exports.isLatLon = isLatLon;
exports.isLatLng = isLatLng;
exports.isLatitudeLongitude = isLatitudeLongitude;
exports.isLonLatTuple = isLonLatTuple;
exports.getLocationType = getLocationType;
exports.createLocation = createLocation;
exports.toLatLon = toLatLon;
exports.toLatLng = toLatLng;
exports.toLatitudeLongitude = toLatitudeLongitude;
exports.toLonLatTuple = toLonLatTuple;
exports.getLongitude = getLongitude;
exports.getLatitude = getLatitude;
exports.moveTo = moveTo;
exports.headingDistanceTo = headingDistanceTo;
exports.headingTo = headingTo;
exports.distanceTo = distanceTo;
exports.insideBoundingBox = insideBoundingBox;
exports.insidePolygon = insidePolygon;
exports.insideCircle = insideCircle;
exports.normalizeHeading = normalizeHeading;
exports.normalizeLatitude = normalizeLatitude;
exports.normalizeLongitude = normalizeLongitude;
exports.normalizeLocation = normalizeLocation;
exports.average = average;
exports.getBoundingBox = getBoundingBox;
var _pointInPolygon = require('point-in-polygon');
var _pointInPolygon2 = _interopRequireDefault(_pointInPolygon);
var _convert = require('./convert');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
var EARTH_RADIUS = exports.EARTH_RADIUS = 6378137; // Earth's radius in meters
/**
* Test whether two locations are equal or approximately equal
* @param {Location} location1 A location in any of the supported location formats
* @param {Location} location2 A location in any of the supported location formats
* @param {number} [epsilon=0] The maximum absolute difference between the
* two latitudes and between the two longitudes.
* Use for example 1e-12 to get rid of round-off errors.
* The epsilon value itself is included.
* Optional, default value is 0.
*/
function isEqual(location1, location2) {
var epsilon = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
return location1 && location2 ? Math.abs(getLatitude(location1) - getLatitude(location2)) <= epsilon && Math.abs(getLongitude(location1) - getLongitude(location2)) <= epsilon : false;
}
/**
* Test whether an object is an object containing numeric properties `lat` and `lon`
* @param {*} object Anything
* @param {boolean} Returns true when object is of type LatLon
*/
function isLatLon(object) {
return !!object && typeof object.lat === 'number' && typeof object.lon === 'number';
}
/**
* Test whether an object is an object containing numeric properties `lat` and `lng`
* @param {*} object Anything
* @param {boolean} Returns true when object is of type LatLng
*/
function isLatLng(object) {
return !!object && typeof object.lat === 'number' && typeof object.lng === 'number';
}
/**
* Test whether an object is an object containing numeric properties `latitude` and `longitude`
* @param {*} object Anything
* @param {boolean} Returns true when object is of type LatitudeLongitude
*/
function isLatitudeLongitude(object) {
return !!object && typeof object.latitude === 'number' && typeof object.longitude === 'number';
}
/**
* Test whether an object is an array containing two numbers (longitude and latitude)
*
* IMPORTANT: this function cannot see the difference between an array with lat/lon
* or an array with lon/lat numbers. It assumes an order lon/lat.
*
* @param {*} object Anything
* @param {boolean} Returns true when object is of type LonLatTuple
*/
function isLonLatTuple(object) {
return Array.isArray(object) && typeof object[0] === 'number' && typeof object[1] === 'number';
}
/**
* Get the type of a location object
* @param {Location} location
* @return {string} Returns the type of the location object
* Recognized types: 'LonLatTuple', 'LatLon', 'LatLng', 'LatitudeLongitude'
*/
function getLocationType(location) {
if (isLonLatTuple(location)) {
return 'LonLatTuple';
}
if (isLatLon(location)) {
return 'LatLon';
}
if (isLatLng(location)) {
return 'LatLng';
}
if (isLatitudeLongitude(location)) {
return 'LatitudeLongitude';
}
throw new Error('Unknown location format ' + JSON.stringify(location));
}
/**
* Create a location object of a specific type
* @param {number} latitude
* @param {number} longitude
* @param {string} type Available types: 'LonLatTuple', 'LatLon', 'LatLng', 'LatitudeLongitude'
*/
function createLocation(latitude, longitude, type) {
if (type === 'LonLatTuple') {
return [longitude, latitude];
}
if (type === 'LatLon') {
return { lat: latitude, lon: longitude };
}
if (type === 'LatLng') {
return { lat: latitude, lng: longitude };
}
if (type === 'LatitudeLongitude') {
return { latitude: latitude, longitude: longitude };
}
throw new Error('Unknown location format ' + JSON.stringify(location));
}
/**
* Convert a location into an object with properties `lat` and `lon`
* @param {Location} location
* @returns {LatLon}
*/
function toLatLon(location) {
if (isLonLatTuple(location)) {
return {
lat: location[1],
lon: location[0]
};
}
if (isLatLon(location)) {
return {
lat: location.lat,
lon: location.lon
};
}
if (isLatLng(location)) {
return {
lat: location.lat,
lon: location.lng
};
}
if (isLatitudeLongitude(location)) {
return {
lat: location.latitude,
lon: location.longitude
};
}
throw new Error('Unknown location format ' + JSON.stringify(location));
}
/**
* Convert a location into an object with properties `lat` and `lng`
* @param {Location} location
* @returns {LatLng}
*/
function toLatLng(location) {
if (isLonLatTuple(location)) {
return {
lat: location[1],
lng: location[0]
};
}
if (isLatLon(location)) {
return {
lat: location.lat,
lng: location.lon
};
}
if (isLatLng(location)) {
return {
lat: location.lat,
lng: location.lng
};
}
if (isLatitudeLongitude(location)) {
return {
lat: location.latitude,
lng: location.longitude
};
}
throw new Error('Unknown location format ' + JSON.stringify(location));
}
/**
* Convert a location into an object with properties `latitude` and `longitude`
* @param {Location} location
* @returns {LatitudeLongitude}
*/
function toLatitudeLongitude(location) {
if (isLonLatTuple(location)) {
return {
latitude: location[1],
longitude: location[0]
};
}
if (isLatLon(location)) {
return {
latitude: location.lat,
longitude: location.lon
};
}
if (isLatLng(location)) {
return {
latitude: location.lat,
longitude: location.lng
};
}
if (isLatitudeLongitude(location)) {
return {
latitude: location.latitude,
longitude: location.longitude
};
}
throw new Error('Unknown location format ' + JSON.stringify(location));
}
/**
* Convert a location into a tuple `[longitude, latitude]`, as used in the geojson standard
*
* Note that for example Leaflet uses a tuple `[latitude, longitude]` instead, be careful!
*
* @param {Location} location
* @returns {LonLatTuple}
*/
function toLonLatTuple(location) {
if (isLonLatTuple(location)) {
return [location[0], location[1]];
}
if (isLatLon(location)) {
return [location.lon, location.lat];
}
if (isLatLng(location)) {
return [location.lng, location.lat];
}
if (isLatitudeLongitude(location)) {
return [location.longitude, location.latitude];
}
throw new Error('Unknown location format ' + JSON.stringify(location));
}
/**
* Get the longitude of a location
* @param {Location} location
* @return {number} Returns the longitude
*/
function getLongitude(location) {
if (isLonLatTuple(location)) {
return location[0];
}
if (isLatLon(location)) {
return location.lon;
}
if (isLatLng(location)) {
return location.lng;
}
if (isLatitudeLongitude(location)) {
return location.longitude;
}
throw new Error('Unknown location format ' + JSON.stringify(location));
}
/**
* Get the latitude of a location object or array
* @param {Location} location
* @return {number} Returns the latitude
*/
function getLatitude(location) {
if (isLonLatTuple(location)) {
return location[1];
}
if (isLatLon(location)) {
return location.lat;
}
if (isLatLng(location)) {
return location.lat;
}
if (isLatitudeLongitude(location)) {
return location.latitude;
}
throw new Error('Unknown location format ' + JSON.stringify(location));
}
/**
* Move to a new location from a start location, heading, and distance
*
* This is a rough estimation.
*
* Source:
*
* http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters
*
* @param {Location} from Start location
* @param {HeadingDistance} headingDistance An object with property `heading` in degrees and `distance` in meters
* @return {Location} Returns the moved location
*/
function moveTo(from, headingDistance) {
// TODO: improve precision of this function moveTo
var lat = getLatitude(from);
var lon = getLongitude(from);
var heading = headingDistance.heading,
distance = headingDistance.distance;
var dLat = distance * Math.cos((0, _convert.degToRad)(heading)) / EARTH_RADIUS;
var dLon = distance * Math.sin((0, _convert.degToRad)(heading)) / (EARTH_RADIUS * Math.cos((0, _convert.degToRad)(lat)));
return createLocation(lat + (0, _convert.radToDeg)(dLat), lon + (0, _convert.radToDeg)(dLon), getLocationType(from));
}
/**
* Calculate the heading and distance between two locations
*
* Sources:
*
* http://www.movable-type.co.uk/scripts/latlong.html
* http://mathforum.org/library/drmath/view/55417.html
*
* @param {Location} from Start location
* @param {Location} to End location
* @return {HeadingDistance} Returns an object with `heading` in degrees and `distance` in meters
*/
function headingDistanceTo(from, to) {
var fromLat = getLatitude(from);
var fromLon = getLongitude(from);
var toLat = getLatitude(to);
var toLon = getLongitude(to);
var lat1 = (0, _convert.degToRad)(fromLat);
var lat2 = (0, _convert.degToRad)(toLat);
var dlat = (0, _convert.degToRad)(toLat - fromLat);
var dlon = (0, _convert.degToRad)(toLon - fromLon);
var a = Math.sin(dlat / 2) * Math.sin(dlat / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dlon / 2) * Math.sin(dlon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var distance = EARTH_RADIUS * c;
var y = Math.sin(dlon) * Math.cos(lat2);
var x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dlon);
var heading = (0, _convert.radToDeg)(Math.atan2(y, x));
return { distance: distance, heading: heading };
}
/**
* Calculate the heading from one location to another location
* @param {Location} center
* @param {Location} location
* @return {number} Returns an heading in degrees
*/
function headingTo(center, location) {
return headingDistanceTo(center, location).heading;
}
/**
* Calculate the distance between two locations
* @param {Location} center
* @param {Location} location
* @return {number} Returns the distance in meters
*/
function distanceTo(center, location) {
return headingDistanceTo(center, location).distance;
}
/**
* Test whether a location lies inside a given bounding box.
* @param {Location} location
* @param {BoundingBox} boundingBox
* A bounding box containing a top left and bottom right location.
* The order doesn't matter.
* @return {boolean} Returns true when the location is inside the bounding box
* or on the edge.
*/
function insideBoundingBox(location, boundingBox) {
var lat = getLatitude(location);
var lon = getLongitude(location);
var topLeftLon = getLongitude(boundingBox.topLeft);
var topLeftLat = getLatitude(boundingBox.topLeft);
var bottomRightLon = getLongitude(boundingBox.bottomRight);
var bottomRightLat = getLatitude(boundingBox.bottomRight);
var minLat = Math.min(topLeftLat, bottomRightLat);
var maxLat = Math.max(topLeftLat, bottomRightLat);
var minLon = Math.min(topLeftLon, bottomRightLon);
var maxLon = Math.max(topLeftLon, bottomRightLon);
return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat;
}
/**
* Test whether a location lies inside a given polygon
* @param {Location} location
* @param {Location[]} polygon
* @return {boolean} Returns true when the location is inside the bounding box
* or on the edge.
*/
function insidePolygon(location, polygon) {
if (!polygon || !Array.isArray(polygon)) {
throw new TypeError('Invalid polygon. Array with locations expected');
}
if (polygon.length === 0) {
throw new TypeError('Invalid polygon. Non-empty Array expected');
}
return (0, _pointInPolygon2.default)(toLonLatTuple(location), polygon.map(toLonLatTuple));
}
/**
* Test whether a location lies inside a circle with certain radius
* @param {Location} location
* @param {Location} center
* @param {number} radius A radius in meters
* @return {boolean} Returns true when the location lies inside or
* on the edge of the circle
*/
function insideCircle(location, center, radius) {
return distanceTo(center, location) <= radius;
}
/**
* Normalize an heading into the range [0, 360)
* @param {number} heading An heading in degrees
* @return {number} Returns the normalized heading (degrees)
*/
function normalizeHeading(heading) {
var normalized = heading % 360;
if (normalized < 0) {
normalized += 360;
}
if (normalized >= 360) {
normalized -= 360;
}
return normalized;
}
/**
* Normalize a latitude into the range [-90, 90] (upper and lower bound included)
*
* See https://stackoverflow.com/questions/13368525/modulus-to-limit-latitude-and-longitude-values
*
* @param {number} latitude
* @return {number} Returns the normalized latitude
*/
function normalizeLatitude(latitude) {
return Math.asin(Math.sin(latitude / 180 * Math.PI)) * (180 / Math.PI);
}
/**
* Normalize a longitude into the range (-180, 180] (lower bound excluded, upper bound included)
*
* @param {number} longitude
* @return {number} Returns the normalized longitude
*/
function normalizeLongitude(longitude) {
var normalized = longitude % 360;
if (normalized > 180) {
normalized -= 360;
}
if (normalized <= -180) {
normalized += 360;
}
return normalized;
}
/**
* Normalize the longitude and latitude of a location.
* Latitude will be in the range [-90, 90] (upper and lower bound included)
* Lontitude will be in the range (-180, 180] (lower bound excluded, upper bound included)
* @param {Location} location
* @return {Location} Returns the normalized location
*/
function normalizeLocation(location) {
if (isLonLatTuple(location)) {
return [normalizeLongitude(location[0]), normalizeLatitude(location[1])];
}
if (isLatLon(location)) {
return {
lat: normalizeLatitude(location.lat),
lon: normalizeLongitude(location.lon)
};
}
if (isLatLng(location)) {
return {
lat: normalizeLatitude(location.lat),
lng: normalizeLongitude(location.lng)
};
}
if (isLatitudeLongitude(location)) {
return {
latitude: normalizeLatitude(location.latitude),
longitude: normalizeLongitude(location.longitude)
};
}
throw new Error('Unknown location format ' + JSON.stringify(location));
}
/**
* Calculate the average of a list with locations
* @param {Location[]} locations
* @return {Location} Returns the average location or null when the list is empty
* Location has the same structure as the first location from
* the input array.
*/
function average(locations) {
if (!Array.isArray(locations) || locations.length === 0) {
return null;
}
var first = locations[0];
var latitude = avg(locations.map(getLatitude));
var longitude = avg(locations.map(getLongitude));
return createLocation(latitude, longitude, getLocationType(first));
}
/**
* Get the bounding box of a list with locations
* @param {Locations[]} locations
* @param {number} [margin] Optional margin in meters. Zero by default.
* @return {BoundingBox} Returns a bounding box described by it's top left
* and bottom right location
*/
function getBoundingBox(locations) {
var margin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
if (!Array.isArray(locations) || locations.length === 0) {
return {
topLeft: null,
bottomRight: null
};
}
var type = getLocationType(locations[0]);
var topLeftLat = Math.max.apply(Math, _toConsumableArray(locations.map(getLatitude)));
var topLeftLon = Math.min.apply(Math, _toConsumableArray(locations.map(getLongitude)));
var bottomRightLat = Math.min.apply(Math, _toConsumableArray(locations.map(getLatitude)));
var bottomRightLon = Math.max.apply(Math, _toConsumableArray(locations.map(getLongitude)));
var topLeft = createLocation(topLeftLat, topLeftLon, type);
var bottomRight = createLocation(bottomRightLat, bottomRightLon, type);
if (margin === null || margin === 0) {
// no margin
return { topLeft: topLeft, bottomRight: bottomRight };
} else {
// add a margin in meters
var distance = Math.SQRT2 * margin;
return {
topLeft: moveTo(topLeft, { heading: 315, distance: distance }),
bottomRight: moveTo(bottomRight, { heading: 135, distance: distance })
};
}
}
/**
* Calculate the average of a list with numbers
* @param {number[]} values
* @return {number}
*/
function avg(values) {
return sum(values) / values.length;
}
/**
* calculate the sum of a list with numbers
* @param {number[]} values
* @return {number} Returns the sum
*/
function sum(values) {
return values.reduce(function (a, b) {
return a + b;
}, 0);
}