/*
* 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.google.maps.android;
import com.google.android.gms.maps.model.LatLng;
import java.util.List;
import java.util.ArrayList;
import static java.lang.Math.*;
import static com.google.maps.android.MathUtil.*;
public class PolyUtil {
private PolyUtil() {}
/**
* Returns tan(latitude-at-lng3) on the great circle (lat1, lng1) to (lat2, lng2). lng1==0.
* See http://williams.best.vwh.net/avform.htm .
*/
private static double tanLatGC(double lat1, double lat2, double lng2, double lng3) {
return (tan(lat1) * sin(lng2 - lng3) + tan(lat2) * sin(lng3)) / sin(lng2);
}
/**
* Returns mercator(latitude-at-lng3) on the Rhumb line (lat1, lng1) to (lat2, lng2). lng1==0.
*/
private static double mercatorLatRhumb(double lat1, double lat2, double lng2, double lng3) {
return (mercator(lat1) * (lng2 - lng3) + mercator(lat2) * lng3) / lng2;
}
/**
* Computes whether the vertical segment (lat3, lng3) to South Pole intersects the segment
* (lat1, lng1) to (lat2, lng2).
* Longitudes are offset by -lng1; the implicit lng1 becomes 0.
*/
private static boolean intersects(double lat1, double lat2, double lng2,
double lat3, double lng3, boolean geodesic) {
// Both ends on the same side of lng3.
if ((lng3 >= 0 && lng3 >= lng2) || (lng3 < 0 && lng3 < lng2)) {
return false;
}
// Point is South Pole.
if (lat3 <= -PI/2) {
return false;
}
// Any segment end is a pole.
if (lat1 <= -PI/2 || lat2 <= -PI/2 || lat1 >= PI/2 || lat2 >= PI/2) {
return false;
}
if (lng2 <= -PI) {
return false;
}
double linearLat = (lat1 * (lng2 - lng3) + lat2 * lng3) / lng2;
// Northern hemisphere and point under lat-lng line.
if (lat1 >= 0 && lat2 >= 0 && lat3 < linearLat) {
return false;
}
// Southern hemisphere and point above lat-lng line.
if (lat1 <= 0 && lat2 <= 0 && lat3 >= linearLat) {
return true;
}
// North Pole.
if (lat3 >= PI/2) {
return true;
}
// Compare lat3 with latitude on the GC/Rhumb segment corresponding to lng3.
// Compare through a strictly-increasing function (tan() or mercator()) as convenient.
return geodesic ?
tan(lat3) >= tanLatGC(lat1, lat2, lng2, lng3) :
mercator(lat3) >= mercatorLatRhumb(lat1, lat2, lng2, lng3);
}
/**
* Computes whether the given point lies inside the specified polygon.
* The polygon is always cosidered closed, regardless of whether the last point equals
* the first or not.
* Inside is defined as not containing the South Pole -- the South Pole is always outside.
* The polygon is formed of great circle segments if geodesic is true, and of rhumb
* (loxodromic) segments otherwise.
*/
public static boolean containsLocation(LatLng point, List<LatLng> polygon, boolean geodesic) {
final int size = polygon.size();
if (size == 0) {
return false;
}
double lat3 = toRadians(point.latitude);
double lng3 = toRadians(point.longitude);
LatLng prev = polygon.get(size - 1);
double lat1 = toRadians(prev.latitude);
double lng1 = toRadians(prev.longitude);
int nIntersect = 0;
for (LatLng point2 : polygon) {
double dLng3 = wrap(lng3 - lng1, -PI, PI);
// Special case: point equal to vertex is inside.
if (lat3 == lat1 && dLng3 == 0) {
return true;
}
double lat2 = toRadians(point2.latitude);
double lng2 = toRadians(point2.longitude);
// Offset longitudes by -lng1.
if (intersects(lat1, lat2, wrap(lng2 - lng1, -PI, PI), lat3, dLng3, geodesic)) {
++nIntersect;
}
lat1 = lat2;
lng1 = lng2;
}
return (nIntersect & 1) != 0;
}
private static final double DEFAULT_TOLERANCE = 0.1; // meters.
/**
* Computes whether the given point lies on or near the edge of a polygon, within a specified
* tolerance in meters. The polygon edge is composed of great circle segments if geodesic
* is true, and of Rhumb segments otherwise. The polygon edge is implicitly closed -- the
* closing segment between the first point and the last point is included.
*/
public static boolean isLocationOnEdge(LatLng point, List<LatLng> polygon, boolean geodesic,
double tolerance) {
return isLocationOnEdgeOrPath(point, polygon, true, geodesic, tolerance);
}
/**
* Same as {@link #isLocationOnEdge(LatLng, List, boolean, double)}
* with a default tolerance of 0.1 meters.
*/
public static boolean isLocationOnEdge(LatLng point, List<LatLng> polygon, boolean geodesic) {
return isLocationOnEdge(point, polygon, geodesic, DEFAULT_TOLERANCE);
}
/**
* Computes whether the given point lies on or near a polyline, within a specified
* tolerance in meters. The polyline is composed of great circle segments if geodesic
* is true, and of Rhumb segments otherwise. The polyline is not closed -- the closing
* segment between the first point and the last point is not included.
*/
public static boolean isLocationOnPath(LatLng point, List<LatLng> polyline,
boolean geodesic, double tolerance) {
return isLocationOnEdgeOrPath(point, polyline, false, geodesic, tolerance);
}
/**
* Same as {@link #isLocationOnPath(LatLng, List, boolean, double)}
*
* with a default tolerance of 0.1 meters.
*/
public static boolean isLocationOnPath(LatLng point, List<LatLng> polyline,
boolean geodesic) {
return isLocationOnPath(point, polyline, geodesic, DEFAULT_TOLERANCE);
}
private static boolean isLocationOnEdgeOrPath(LatLng point, List<LatLng> poly, boolean closed,
boolean geodesic, double toleranceEarth) {
int size = poly.size();
if (size == 0) {
return false;
}
double tolerance = toleranceEarth / EARTH_RADIUS;
double havTolerance = hav(tolerance);
double lat3 = toRadians(point.latitude);
double lng3 = toRadians(point.longitude);
LatLng prev = poly.get(closed ? size - 1 : 0);
double lat1 = toRadians(prev.latitude);
double lng1 = toRadians(prev.longitude);
if (geodesic) {
for (LatLng point2 : poly) {
double lat2 = toRadians(point2.latitude);
double lng2 = toRadians(point2.longitude);
if (isOnSegmentGC(lat1, lng1, lat2, lng2, lat3, lng3, havTolerance)) {
return true;
}
lat1 = lat2;
lng1 = lng2;
}
} else {
// We project the points to mercator space, where the Rhumb segment is a straight line,
// and compute the geodesic distance between point3 and the closest point on the
// segment. This method is an approximation, because it uses "closest" in mercator
// space which is not "closest" on the sphere -- but the error is small because
// "tolerance" is small.
double minAcceptable = lat3 - tolerance;
double maxAcceptable = lat3 + tolerance;
double y1 = mercator(lat1);
double y3 = mercator(lat3);
double[] xTry = new double[3];
for (LatLng point2 : poly) {
double lat2 = toRadians(point2.latitude);
double y2 = mercator(lat2);
double lng2 = toRadians(point2.longitude);
if (max(lat1, lat2) >= minAcceptable && min(lat1, lat2) <= maxAcceptable) {
// We offset longitudes by -lng1; the implicit x1 is 0.
double x2 = wrap(lng2 - lng1, -PI, PI);
double x3Base = wrap(lng3 - lng1, -PI, PI);
xTry[0] = x3Base;
// Also explore wrapping of x3Base around the world in both directions.
xTry[1] = x3Base + 2 * PI;
xTry[2] = x3Base - 2 * PI;
for (double x3 : xTry) {
double dy = y2 - y1;
double len2 = x2 * x2 + dy * dy;
double t = len2 <= 0 ? 0 : clamp((x3 * x2 + (y3 - y1) * dy) / len2, 0, 1);
double xClosest = t * x2;
double yClosest = y1 + t * dy;
double latClosest = inverseMercator(yClosest);
double havDist = havDistance(lat3, latClosest, x3 - xClosest);
if (havDist < havTolerance) {
return true;
}
}
}
lat1 = lat2;
lng1 = lng2;
y1 = y2;
}
}
return false;
}
/**
* Returns sin(initial bearing from (lat1,lng1) to (lat3,lng3) minus initial bearing
* from (lat1, lng1) to (lat2,lng2)).
*/
private static double sinDeltaBearing(double lat1, double lng1, double lat2, double lng2,
double lat3, double lng3) {
double sinLat1 = sin(lat1);
double cosLat2 = cos(lat2);
double cosLat3 = cos(lat3);
double lat31 = lat3 - lat1;
double lng31 = lng3 - lng1;
double lat21 = lat2 - lat1;
double lng21 = lng2 - lng1;
double a = sin(lng31) * cosLat3;
double c = sin(lng21) * cosLat2;
double b = sin(lat31) + 2 * sinLat1 * cosLat3 * hav(lng31);
double d = sin(lat21) + 2 * sinLat1 * cosLat2 * hav(lng21);
double denom = (a * a + b * b) * (c * c + d * d);
return denom <= 0 ? 1 : (a * d - b * c) / sqrt(denom);
}
private static boolean isOnSegmentGC(double lat1, double lng1, double lat2, double lng2,
double lat3, double lng3, double havTolerance) {
double havDist13 = havDistance(lat1, lat3, lng1 - lng3);
if (havDist13 <= havTolerance) {
return true;
}
double havDist23 = havDistance(lat2, lat3, lng2 - lng3);
if (havDist23 <= havTolerance) {
return true;
}
double sinBearing = sinDeltaBearing(lat1, lng1, lat2, lng2, lat3, lng3);
double sinDist13 = sinFromHav(havDist13);
double havCrossTrack = havFromSin(sinDist13 * sinBearing);
if (havCrossTrack > havTolerance) {
return false;
}
double havDist12 = havDistance(lat1, lat2, lng1 - lng2);
double term = havDist12 + havCrossTrack * (1 - 2 * havDist12);
if (havDist13 > term || havDist23 > term) {
return false;
}
if (havDist12 < 0.74) {
return true;
}
double cosCrossTrack = 1 - 2 * havCrossTrack;
double havAlongTrack13 = (havDist13 - havCrossTrack) / cosCrossTrack;
double havAlongTrack23 = (havDist23 - havCrossTrack) / cosCrossTrack;
double sinSumAlongTrack = sinSumFromHav(havAlongTrack13, havAlongTrack23);
return sinSumAlongTrack > 0; // Compare with half-circle == PI using sign of sin().
}
/**
* Decodes an encoded path string into a sequence of LatLngs.
*/
public static List<LatLng> decode(final String encodedPath) {
int len = encodedPath.length();
// For speed we preallocate to an upper bound on the final length, then
// truncate the array before returning.
final List<LatLng> path = new ArrayList<LatLng>();
int index = 0;
int lat = 0;
int lng = 0;
for (int pointIndex = 0; index < len; ++pointIndex) {
int result = 1;
int shift = 0;
int b;
do {
b = encodedPath.charAt(index++) - 63 - 1;
result += b << shift;
shift += 5;
} while (b >= 0x1f);
lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
result = 1;
shift = 0;
do {
b = encodedPath.charAt(index++) - 63 - 1;
result += b << shift;
shift += 5;
} while (b >= 0x1f);
lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
path.add(new LatLng(lat * 1e-5, lng * 1e-5));
}
return path;
}
/**
* Encodes a sequence of LatLngs into an encoded path string.
*/
public static String encode(final List<LatLng> path) {
long lastLat = 0;
long lastLng = 0;
final StringBuffer result = new StringBuffer();
for (final LatLng point : path) {
long lat = Math.round(point.latitude * 1e5);
long lng = Math.round(point.longitude * 1e5);
long dLat = lat - lastLat;
long dLng = lng - lastLng;
encode(dLat, result);
encode(dLng, result);
lastLat = lat;
lastLng = lng;
}
return result.toString();
}
private static void encode(long v, StringBuffer result) {
v = v < 0 ? ~(v << 1) : v << 1;
while (v >= 0x20) {
result.append(Character.toChars((int) ((0x20 | (v & 0x1f)) + 63)));
v >>= 5;
}
result.append(Character.toChars((int) (v + 63)));
}
}