/* * Copyright 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.car2go.maps.util; import com.car2go.maps.model.LatLng; import java.util.Iterator; import java.util.List; public class SphericalUtil { private SphericalUtil() { } /** * 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. */ public static double computeHeading(LatLng from, LatLng to) { double fromLat = Math.toRadians(from.latitude); double fromLng = Math.toRadians(from.longitude); double toLat = Math.toRadians(to.latitude); double toLng = Math.toRadians(to.longitude); double dLng = toLng - fromLng; double 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 MathUtil.wrap(Math.toDegrees(heading), -180.0D, 180.0D); } /** * Returns the LatLng resulting from moving a distance from an origin * in the specified heading (expressed in degrees clockwise from north). * * @param from The LatLng from which to start. * @param distance The distance to travel. * @param heading The heading in degrees clockwise from north. */ public static LatLng computeOffset(LatLng from, double distance, double heading) { distance /= 6371009.0D; heading = Math.toRadians(heading); double fromLat = Math.toRadians(from.latitude); double fromLng = Math.toRadians(from.longitude); double cosDistance = Math.cos(distance); double sinDistance = Math.sin(distance); double sinFromLat = Math.sin(fromLat); double cosFromLat = Math.cos(fromLat); double sinLat = cosDistance * sinFromLat + sinDistance * cosFromLat * Math.cos(heading); double dLng = Math.atan2(sinDistance * cosFromLat * Math.sin(heading), cosDistance - sinFromLat * sinLat); return new LatLng(Math.toDegrees(Math.asin(sinLat)), Math.toDegrees(fromLng + dLng)); } /** * 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. */ public static LatLng computeOffsetOrigin(LatLng to, double distance, double heading) { heading = Math.toRadians(heading); distance /= 6371009.0D; double n1 = Math.cos(distance); double n2 = Math.sin(distance) * Math.cos(heading); double n3 = Math.sin(distance) * Math.sin(heading); double n4 = Math.sin(Math.toRadians(to.latitude)); double n12 = n1 * n1; double discriminant = n2 * n2 * n12 + n12 * n12 - n12 * n4 * n4; if (discriminant < 0.0D) { return null; } else { double b = n2 * n4 + Math.sqrt(discriminant); b /= n1 * n1 + n2 * n2; double a = (n4 - n2 * b) / n1; double fromLatRadians = Math.atan2(a, b); if (fromLatRadians < -1.5707963267948966D || fromLatRadians > 1.5707963267948966D) { b = n2 * n4 - Math.sqrt(discriminant); b /= n1 * n1 + n2 * n2; fromLatRadians = Math.atan2(a, b); } if (fromLatRadians >= -1.5707963267948966D && fromLatRadians <= 1.5707963267948966D) { double fromLngRadians = Math.toRadians(to.longitude) - Math.atan2(n3, n1 * Math.cos(fromLatRadians) - n2 * Math.sin(fromLatRadians)); return new LatLng(Math.toDegrees(fromLatRadians), Math.toDegrees(fromLngRadians)); } else { return null; } } } /** * 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. */ public static LatLng interpolate(LatLng from, LatLng to, double fraction) { double fromLat = Math.toRadians(from.latitude); double fromLng = Math.toRadians(from.longitude); double toLat = Math.toRadians(to.latitude); double toLng = Math.toRadians(to.longitude); double cosFromLat = Math.cos(fromLat); double cosToLat = Math.cos(toLat); double angle = computeAngleBetween(from, to); double sinAngle = Math.sin(angle); if (sinAngle < 1.0E-6D) { return from; } else { double a = Math.sin((1.0D - fraction) * angle) / sinAngle; double b = Math.sin(fraction * angle) / sinAngle; double x = a * cosFromLat * Math.cos(fromLng) + b * cosToLat * Math.cos(toLng); double y = a * cosFromLat * Math.sin(fromLng) + b * cosToLat * Math.sin(toLng); double z = a * Math.sin(fromLat) + b * Math.sin(toLat); double lat = Math.atan2(z, Math.sqrt(x * x + y * y)); double lng = Math.atan2(y, x); return new LatLng(Math.toDegrees(lat), Math.toDegrees(lng)); } } /** * Returns distance on the unit sphere; the arguments are in radians. */ private static double distanceRadians(double lat1, double lng1, double lat2, double lng2) { return MathUtil.arcHav(MathUtil.havDistance(lat1, lat2, lng1 - lng2)); } /** * Returns the angle between two LatLngs, in radians. This is the same as the distance * on the unit sphere. */ static double computeAngleBetween(LatLng from, LatLng to) { return distanceRadians(Math.toRadians(from.latitude), Math.toRadians(from.longitude), Math.toRadians(to.latitude), Math.toRadians(to.longitude)); } /** * Returns the distance between two LatLngs, in meters. */ public static double computeDistanceBetween(LatLng from, LatLng to) { return computeAngleBetween(from, to) * 6371009.0D; } /** * Returns the length of the given path, in meters, on Earth. */ public static double computeLength(List<LatLng> path) { if (path.size() < 2) { return 0.0D; } else { double length = 0.0D; LatLng prev = (LatLng) path.get(0); double prevLat = Math.toRadians(prev.latitude); double prevLng = Math.toRadians(prev.longitude); double lng; for (Iterator i$ = path.iterator(); i$.hasNext(); prevLng = lng) { LatLng point = (LatLng) i$.next(); double lat = Math.toRadians(point.latitude); lng = Math.toRadians(point.longitude); length += distanceRadians(prevLat, prevLng, lat, lng); prevLat = lat; } return length * 6371009.0D; } } /** * Returns the area of a closed path on Earth. * * @param path A closed path. * @return The path's area in square meters. */ public static double computeArea(List<LatLng> path) { return Math.abs(computeSignedArea(path)); } /** * Returns the signed area of a closed path on Earth. The sign of the area may be used to * determine the orientation of the path. * "inside" is the surface that does not contain the South Pole. * * @param path A closed path. * @return The loop's area in square meters. */ public static double computeSignedArea(List<LatLng> path) { return computeSignedArea(path, 6371009.0D); } /** * Returns the signed area of a closed path on a sphere of given radius. * The computed area uses the same units as the radius squared. * Used by SphericalUtilTest. */ static double computeSignedArea(List<LatLng> path, double radius) { int size = path.size(); if (size < 3) { return 0.0D; } else { double total = 0.0D; LatLng prev = (LatLng) path.get(size - 1); double prevTanLat = Math.tan((1.5707963267948966D - Math.toRadians(prev.latitude)) / 2.0D); double prevLng = Math.toRadians(prev.longitude); double lng; for (Iterator i$ = path.iterator(); i$.hasNext(); prevLng = lng) { LatLng point = (LatLng) i$.next(); double tanLat = Math.tan((1.5707963267948966D - Math.toRadians(point.latitude)) / 2.0D); lng = Math.toRadians(point.longitude); total += polarTriangleArea(tanLat, lng, prevTanLat, prevLng); prevTanLat = tanLat; } 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). */ private static double polarTriangleArea(double tan1, double lng1, double tan2, double lng2) { double deltaLng = lng1 - lng2; double t = tan1 * tan2; return 2.0D * Math.atan2(t * Math.sin(deltaLng), 1.0D + t * Math.cos(deltaLng)); } }