Skip to content

Commit

Permalink
Merge pull request #11 from preda/master
Browse files Browse the repository at this point in the history
Add PolyUtil.containsLocation().
  • Loading branch information
broady committed Aug 12, 2013
2 parents 0f7ccda + ee8d001 commit 0db5ebc
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 3 deletions.
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

0 comments on commit 0db5ebc

Please sign in to comment.