282 lines
8.8 KiB
JavaScript
282 lines
8.8 KiB
JavaScript
var LatLng = require('./LatLng');
|
|
var common = require('./Common');
|
|
var EARTH_RADIUS = 6371009;
|
|
|
|
/**
|
|
* Port from android-maps-utils
|
|
* https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/SphericalUtil.java
|
|
*/
|
|
|
|
/**
|
|
* Returns the non-negative remainder of x / m.
|
|
* @param x The operand.
|
|
* @param m The modulus.
|
|
*/
|
|
function mod(x, m) {
|
|
return ((x % m) + m) % m;
|
|
}
|
|
/**
|
|
* Wraps the given value into the inclusive-exclusive interval between min and max.
|
|
* @param n The value to wrap.
|
|
* @param min The minimum.
|
|
* @param max The maximum.
|
|
*/
|
|
function wrap(n, min, max) {
|
|
return (n >= min && n < max) ? n : (mod(n - min, max - min) + min);
|
|
}
|
|
|
|
/**
|
|
* Returns haversine(angle-in-radians).
|
|
* hav(x) == (1 - cos(x)) / 2 == sin(x / 2)^2.
|
|
*/
|
|
function hav(x) {
|
|
var sinHalf = Math.sin(x * 0.5);
|
|
return sinHalf * sinHalf;
|
|
}
|
|
|
|
/**
|
|
* Computes inverse haversine. Has good numerical stability around 0.
|
|
* arcHav(x) == acos(1 - 2 * x) == 2 * asin(sqrt(x)).
|
|
* The argument must be in [0, 1], and the result is positive.
|
|
*/
|
|
function arcHav(x) {
|
|
return 2 * Math.asin(Math.sqrt(x));
|
|
}
|
|
|
|
/**
|
|
* Returns hav() of distance from (lat1, lng1) to (lat2, lng2) on the unit sphere.
|
|
*/
|
|
function havDistance(lat1, lat2, dLng) {
|
|
return hav(lat1 - lat2) + hav(dLng) * Math.cos(lat1) * Math.cos(lat2);
|
|
}
|
|
|
|
/**
|
|
* Returns distance on the unit sphere; the arguments are in radians.
|
|
*/
|
|
function distanceRadians(lat1, lng1, lat2, lng2) {
|
|
return arcHav(havDistance(lat1, lat2, lng1 - lng2));
|
|
}
|
|
|
|
/**
|
|
* Returns the angle between two LatLngs, in radians. This is the same as the distance
|
|
* on the unit sphere.
|
|
*/
|
|
function computeAngleBetween(from, to) {
|
|
return distanceRadians(toRadians(from.lat), toRadians(from.lng),
|
|
toRadians(to.lat), toRadians(to.lng));
|
|
}
|
|
|
|
/**
|
|
* Returns the distance between two LatLngs, in meters.
|
|
*/
|
|
function computeDistanceBetween(from, to) {
|
|
return computeAngleBetween(from, to) * EARTH_RADIUS;
|
|
}
|
|
|
|
/**
|
|
* Returns the distance between two LatLngs, in meters.
|
|
*/
|
|
function computeOffset(from, distance, heading) {
|
|
distance /= EARTH_RADIUS;
|
|
heading = toRadians(heading);
|
|
|
|
// http://williams.best.vwh.net/avform.htm#LL
|
|
var fromLat = toRadians(from.lat);
|
|
var fromLng = toRadians(from.lng);
|
|
var cosDistance = Math.cos(distance);
|
|
var sinDistance = Math.sin(distance);
|
|
var sinFromLat = Math.sin(fromLat);
|
|
var cosFromLat = Math.cos(fromLat);
|
|
var sinLat = cosDistance * sinFromLat + sinDistance * cosFromLat * Math.cos(heading);
|
|
var dLng = Math.atan2(
|
|
sinDistance * cosFromLat * Math.sin(heading),
|
|
cosDistance - sinFromLat * sinLat);
|
|
return new LatLng(toDegrees(Math.asin(sinLat)), toDegrees(fromLng + dLng));
|
|
}
|
|
|
|
function toRadians(d) {
|
|
return d * Math.PI / 180;
|
|
}
|
|
|
|
function toDegrees(r) {
|
|
return r * 180 / Math.PI;
|
|
}
|
|
|
|
/*
|
|
* Returns the signed area of a closed path on a sphere of given radius.
|
|
*/
|
|
function computeSignedArea(path) {
|
|
var radius = EARTH_RADIUS;
|
|
path = common.convertToPositionArray(path);
|
|
|
|
var size = path.length;
|
|
if (size < 3) {
|
|
return 0;
|
|
}
|
|
var total = 0;
|
|
|
|
var prev = path[size - 1];
|
|
var prevTanLat = Math.tan((Math.PI / 2 - toRadians(prev.lat)) / 2);
|
|
var prevLng = toRadians(prev.lng);
|
|
|
|
// For each edge, accumulate the signed area of the triangle formed by the North Pole
|
|
// and that edge ("polar triangle").
|
|
path.forEach(function (position) {
|
|
var tanLat = Math.tan((Math.PI / 2 - toRadians(position.lat)) / 2);
|
|
var lng = toRadians(position.lng);
|
|
total += polarTriangleArea(tanLat, lng, prevTanLat, prevLng);
|
|
prevTanLat = tanLat;
|
|
prevLng = lng;
|
|
});
|
|
return total * (radius * radius);
|
|
}
|
|
|
|
/*
|
|
* Returns the signed area of a triangle which has North Pole as a vertex.
|
|
* Formula derived from "Area of a spherical triangle given two edges and the included angle"
|
|
* as per "Spherical Trigonometry" by Todhunter, page 71, section 103, point 2.
|
|
* See http://books.google.com/books?id=3uBHAAAAIAAJ&pg=PA71
|
|
* The arguments named "tan" are tan((pi/2 - latitude)/2).
|
|
*/
|
|
function polarTriangleArea(tan1, lng1, tan2, lng2) {
|
|
|
|
var deltaLng = lng1 - lng2;
|
|
var t = tan1 * tan2;
|
|
return 2 * Math.atan2(t * Math.sin(deltaLng), 1 + t * Math.cos(deltaLng));
|
|
}
|
|
|
|
function computeArea(path) {
|
|
return Math.abs(computeSignedArea(path));
|
|
}
|
|
|
|
/**
|
|
* Returns the heading from one LatLng to another LatLng. Headings are
|
|
* expressed in degrees clockwise from North within the range [-180,180).
|
|
* @return The heading in degrees clockwise from north.
|
|
*/
|
|
function computeHeading(from, to) {
|
|
// http://williams.best.vwh.net/avform.htm#Crs
|
|
var fromLat = toRadians(from.lat);
|
|
var fromLng = toRadians(from.lng);
|
|
var toLat = toRadians(to.lat);
|
|
var toLng = toRadians(to.lng);
|
|
var dLng = toLng - fromLng;
|
|
var heading = Math.atan2(
|
|
Math.sin(dLng) * Math.cos(toLat),
|
|
Math.cos(fromLat) * Math.sin(toLat) - Math.sin(fromLat) * Math.cos(toLat) * Math.cos(dLng));
|
|
return wrap(toDegrees(heading), -180, 180);
|
|
}
|
|
|
|
/**
|
|
* Returns the location of origin when provided with a LatLng destination,
|
|
* meters travelled and original heading. Headings are expressed in degrees
|
|
* clockwise from North. This function returns null when no solution is
|
|
* available.
|
|
* @param to The destination LatLng.
|
|
* @param distance The distance travelled, in meters.
|
|
* @param heading The heading in degrees clockwise from north.
|
|
*/
|
|
function computeOffsetOrigin(to, distance, heading) {
|
|
heading = toRadians(heading);
|
|
distance /= EARTH_RADIUS;
|
|
// http://lists.maptools.org/pipermail/proj/2008-October/003939.html
|
|
var n1 = Math.cos(distance);
|
|
var n2 = Math.sin(distance) * Math.cos(heading);
|
|
var n3 = Math.sin(distance) * Math.sin(heading);
|
|
var n4 = Math.sin(toRadians(to.lat));
|
|
// There are two solutions for b. b = n2 * n4 +/- sqrt(), one solution results
|
|
// in the latitude outside the [-90, 90] range. We first try one solution and
|
|
// back off to the other if we are outside that range.
|
|
var n12 = n1 * n1;
|
|
var discriminant = n2 * n2 * n12 + n12 * n12 - n12 * n4 * n4;
|
|
if (discriminant < 0) {
|
|
// No real solution which would make sense in LatLng-space.
|
|
return null;
|
|
}
|
|
var b = n2 * n4 + Math.sqrt(discriminant);
|
|
b /= n1 * n1 + n2 * n2;
|
|
var a = (n4 - n2 * b) / n1;
|
|
var fromLatRadians = Math.atan2(a, b);
|
|
if (fromLatRadians < -Math.PI / 2 || fromLatRadians > Math.PI / 2) {
|
|
b = n2 * n4 - Math.sqrt(discriminant);
|
|
b /= n1 * n1 + n2 * n2;
|
|
fromLatRadians = Math.atan2(a, b);
|
|
}
|
|
if (fromLatRadians < -Math.PI / 2 || fromLatRadians > Math.PI / 2) {
|
|
// No solution which would make sense in LatLng-space.
|
|
return null;
|
|
}
|
|
var fromLngRadians = toRadians(to.lng) -
|
|
Math.atan2(n3, n1 * Math.cos(fromLatRadians) - n2 * Math.sin(fromLatRadians));
|
|
return new LatLng(toDegrees(fromLatRadians), toDegrees(fromLngRadians));
|
|
}
|
|
|
|
/**
|
|
* Returns the LatLng which lies the given fraction of the way between the
|
|
* origin LatLng and the destination LatLng.
|
|
* @param from The LatLng from which to start.
|
|
* @param to The LatLng toward which to travel.
|
|
* @param fraction A fraction of the distance to travel.
|
|
* @return The interpolated LatLng.
|
|
*/
|
|
function interpolate(from, to, fraction) {
|
|
// http://en.wikipedia.org/wiki/Slerp
|
|
var fromLat = toRadians(from.lat);
|
|
var fromLng = toRadians(from.lng);
|
|
var toLat = toRadians(to.lat);
|
|
var toLng = toRadians(to.lng);
|
|
var cosFromLat = Math.cos(fromLat);
|
|
var cosToLat = Math.cos(toLat);
|
|
|
|
// Computes Spherical interpolation coefficients.
|
|
var angle = computeAngleBetween(from, to);
|
|
var sinAngle = Math.sin(angle);
|
|
if (sinAngle < 1E-6) {
|
|
return from;
|
|
}
|
|
var a = Math.sin((1 - fraction) * angle) / sinAngle;
|
|
var b = Math.sin(fraction * angle) / sinAngle;
|
|
|
|
// Converts from polar to vector and interpolate.
|
|
var x = a * cosFromLat * Math.cos(fromLng) + b * cosToLat * Math.cos(toLng);
|
|
var y = a * cosFromLat * Math.sin(fromLng) + b * cosToLat * Math.sin(toLng);
|
|
var z = a * Math.sin(fromLat) + b * Math.sin(toLat);
|
|
|
|
// Converts interpolated vector back to polar.
|
|
var lat = Math.atan2(z, Math.sqrt(x * x + y * y));
|
|
var lng = Math.atan2(y, x);
|
|
return new LatLng(toDegrees(lat), toDegrees(lng));
|
|
}
|
|
|
|
/**
|
|
* Returns the length of the given path, in meters, on Earth.
|
|
*/
|
|
function computeLength(path) {
|
|
path = common.convertToPositionArray(path);
|
|
if (path.length < 2) {
|
|
return 0;
|
|
}
|
|
var length = 0;
|
|
var prev = path[0];
|
|
var prevLat = toRadians(prev.lat);
|
|
var prevLng = toRadians(prev.lng);
|
|
path.forEach(function (point) {
|
|
var lat = toRadians(point.lat);
|
|
var lng = toRadians(point.lng);
|
|
length += distanceRadians(prevLat, prevLng, lat, lng);
|
|
prevLat = lat;
|
|
prevLng = lng;
|
|
});
|
|
return length * EARTH_RADIUS;
|
|
}
|
|
module.exports = {
|
|
computeDistanceBetween: computeDistanceBetween,
|
|
computeOffset: computeOffset,
|
|
computeOffsetOrigin: computeOffsetOrigin,
|
|
computeArea: computeArea,
|
|
computeSignedArea: computeSignedArea,
|
|
computeHeading: computeHeading,
|
|
interpolate: interpolate,
|
|
computeLength: computeLength
|
|
};
|