cordova.define("cordova-plugin-googlemaps.spherical", function(require, exports, module) {
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
};

});
