Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions library/src/com/google/maps/android/PolyUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<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;
}

/**
* Decodes an encoded path string into a sequence of LatLngs.
*/
Expand Down
6 changes: 3 additions & 3 deletions library/src/com/google/maps/android/SphericalUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,16 @@ 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);
}

/**
* Returns the non-negative remainder of x / m.
* @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;
}

Expand Down
59 changes: 59 additions & 0 deletions library/tests/src/com/google/maps/android/PolyUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -17,6 +19,63 @@ private static void expectNearNumber(double expected, double actual, double epsi
Math.abs(expected - actual) <= epsilon);
}

private static List<LatLng> makeList(double... coords) {
int size = coords.length / 2;
ArrayList<LatLng> list = new ArrayList<LatLng>(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<LatLng> poly, List<LatLng> yes, List<LatLng> 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<LatLng> latLngs = PolyUtil.decode(TEST_LINE);

Expand Down