diff --git a/library/src/com/google/maps/android/PolyUtil.java b/library/src/com/google/maps/android/PolyUtil.java index 78c47316d..fd67dfa6e 100644 --- a/library/src/com/google/maps/android/PolyUtil.java +++ b/library/src/com/google/maps/android/PolyUtil.java @@ -21,10 +21,115 @@ import java.util.List; import java.util.ArrayList; +import static java.lang.Math.*; +import static com.google.maps.android.SphericalUtil.wrap; + public class PolyUtil { private PolyUtil() {} + /** + * Returns mercator Y corresponding to latitude. + * See http://en.wikipedia.org/wiki/Mercator_projection . + */ + static double mercator(double lat) { + return log(tan(lat * 0.5 + PI/4)); + } + + /** + * Returns tan(latitude-at-lng3) on the great circle (lat1, lng1) to (lat2, lng2). lng1==0. + * See http://williams.best.vwh.net/avform.htm . + */ + 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. + */ + 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. + */ + 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 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; + } + /** * Decodes an encoded path string into a sequence of LatLngs. */ diff --git a/library/src/com/google/maps/android/SphericalUtil.java b/library/src/com/google/maps/android/SphericalUtil.java index 192ba2ba2..d9ecd4c55 100644 --- a/library/src/com/google/maps/android/SphericalUtil.java +++ b/library/src/com/google/maps/android/SphericalUtil.java @@ -287,8 +287,8 @@ static int isCCW(LatLng a, LatLng b, LatLng c) { * @param min The minimum. * @param max The maximum. */ - static double wrap(double n, int min, int max) { - return mod(n - min, max - min) + min; + static double wrap(double n, double min, double max) { + return (n >= min && n < max) ? n : (mod(n - min, max - min) + min); } /** @@ -296,7 +296,7 @@ static double wrap(double n, int min, int max) { * @param x The operand. * @param m The modulus. */ - static double mod(double x, int m) { + static double mod(double x, double m) { return ((x % m) + m) % m; } diff --git a/library/tests/src/com/google/maps/android/PolyUtilTest.java b/library/tests/src/com/google/maps/android/PolyUtilTest.java index d450b18d1..bc2305575 100644 --- a/library/tests/src/com/google/maps/android/PolyUtilTest.java +++ b/library/tests/src/com/google/maps/android/PolyUtilTest.java @@ -8,6 +8,8 @@ import java.lang.String; import java.util.List; +import java.util.Arrays; +import java.util.ArrayList; public class PolyUtilTest extends TestCase { private static final String TEST_LINE = "_cqeFf~cjVf@p@fA}AtAoB`ArAx@hA`GbIvDiFv@gAh@t@X\\|@z@`@Z\\Xf@Vf@VpA\\tATJ@NBBkC"; @@ -17,6 +19,63 @@ private static void expectNearNumber(double expected, double actual, double epsi Math.abs(expected - actual) <= epsilon); } + private static List makeList(double... coords) { + int size = coords.length / 2; + ArrayList list = new ArrayList(size); + for (int i = 0; i < size; ++i) { + list.add(new LatLng(coords[i + i], coords[i + i + 1])); + } + return list; + } + + private static void containsCase(List poly, List yes, List no) { + for (LatLng point : yes) { + Assert.assertTrue(PolyUtil.containsLocation(point, poly, true)); + Assert.assertTrue(PolyUtil.containsLocation(point, poly, false)); + } + for (LatLng point : no) { + Assert.assertFalse(PolyUtil.containsLocation(point, poly, true)); + Assert.assertFalse(PolyUtil.containsLocation(point, poly, false)); + } + } + + public void test_containsLocation() { + // Empty. + containsCase(makeList(), + makeList(), + makeList(0, 0)); + + // One point. + containsCase(makeList(1, 2), + makeList(1, 2), + makeList(0, 0)); + + // Two points. + containsCase(makeList(1, 2, 3, 5), + makeList(1, 2, 3, 5), + makeList(0, 0, 40, 4)); + + // Some arbitrary triangle. + containsCase(makeList(0., 0., 10., 12., 20., 5.), + makeList(10., 12., 10, 11, 19, 5), + makeList(0, 1, 11, 12, 30, 5, 0, -180, 0, 90)); + + // Around North Pole. + containsCase(makeList(89, 0, 89, 120, 89, -120), + makeList(90, 0, 90, 180, 90, -90), + makeList(-90, 0, 0, 0)); + + // Around South Pole. + containsCase(makeList(-89, 0, -89, 120, -89, -120), + makeList(90, 0, 90, 180, 90, -90, 0, 0), + makeList(-90, 0, -90, 90)); + + // Over/under segment on meridian and equator. + containsCase(makeList(5, 10, 10, 10, 0, 20, 0, -10), + makeList(2.5, 10, 1, 0), + makeList(15, 10, 0, -15, 0, 25, -1, 0)); + } + public void test_decodePath() { List latLngs = PolyUtil.decode(TEST_LINE);