15

I'm trying to figure out how best to do this, I have a map with one Polygon drawn on it. Since it doesn't seem as though the Google Maps API V2 has a touch detection on a Polygon. I was wonder if it is possible to detect whether the touch point is inside the Polygon? If so then how, my main goal is to outline a state on a map and when the user taps that state it will show more details inside a custom view. As of now I am able to capture the MapOnClick of the map but when the user taps inside the Polygon I want the polygon.getID() set on the Toast. I am a newbie so I apologize if I am not clear enough.

googleMap.setOnMapClickListener(new OnMapClickListener() 
    {
        public void onMapClick(LatLng point) 
        {
        boolean checkPoly = true;

        Toast.makeText(MainActivity.this,"The Location is outside of the Area", Toast.LENGTH_LONG).show();
        }    
     });
     }
     }
   catch (Exception e) {
         Log.e("APP","Failed", e);
     }    

Ok this is what I have semi-working so far

    private boolean rayCastIntersect(LatLng tap, LatLng vertA, LatLng vertB) {

    double aY = vertA.latitude;
    double bY = vertB.latitude;
    double aX = vertA.longitude;
    double bX = vertB.longitude;
    double pY = tap.latitude;
    double pX = tap.longitude;
     if (aY > bY) {
            aX = vertB.longitude;
            aY = vertB.latitude;
            bX = vertA.longitude;
            bX = vertA.latitude;
        }
    System.out.println("aY: "+aY+" aX : "+aX);
    System.out.println("bY: "+bY+" bX : "+bX);

     if (pX < 0) pX += 360;
        if (aX < 0) aX += 360;
        if (bX < 0) bX += 360;

        if (pY == aY || pY == bY) pY += 0.00000001;
        if ((pY > bY || pY < aY) || (pX > Math.max(aX, bX))) return false;
        if (pX < Math.min(aX, bX))

            return true;
//  }

    double m = (aX != bX) ? ((bY - aY) / (bX - aX)) : aX;
    double bee = (aX != pX) ? ((pY - aY) / (pX - aX)) : aX;
    double x = (pY - bee) / m;

    return x > pX;
}

}

The issue that I am having is the touch is true to the left of each polygon until it reaches another one. What's wrong with my algorithm that would cause this issue? Any help would be appreciated.

Dwill
  • 506
  • 1
  • 5
  • 22
  • So to clarify, are you trying to figure out whether or not your touch occurred inside the geographic bounds of the polygon? – Matt Jan 18 '13 at 18:53

7 Answers7

25

The problem you're trying to solve is the Point in Polygon test.

To help visualize the concept of Ray Casting:

Draw a Polygon on a piece of paper. Then, starting at any random point, draw a straight line to the right of the page. If your line intersected with your polygon an odd number of times, this means your starting point was inside the Polygon.


So, how do you do that in code?

Your polygon is comprised of a list of vertices: ArrayList<Geopoint> vertices. You need to look at each Line Segment individually, and see if your Ray intersects it

private boolean isPointInPolygon(Geopoint tap, ArrayList<Geopoint> vertices) {
    int intersectCount = 0;
    for(int j=0; j<vertices.size()-1; j++) {
        if( rayCastIntersect(tap, vertices.get(j), vertices.get(j+1)) ) {
            intersectCount++;
        }
    }

    return (intersectCount%2) == 1); // odd = inside, even = outside;
}

private boolean rayCastIntersect(Geopoint tap, Geopoint vertA, Geopoint vertB) {

    double aY = vertA.getLatitude();
    double bY = vertB.getLatitude();
    double aX = vertA.getLongitude();
    double bX = vertB.getLongitude();
    double pY = tap.getLatitude();
    double pX = tap.getLongitude();

    if ( (aY>pY && bY>pY) || (aY<pY && bY<pY) || (aX<pX && bX<pX) ) {
        return false; // a and b can't both be above or below pt.y, and a or b must be east of pt.x
    }

    double m = (aY-bY) / (aX-bX);               // Rise over run
    double bee = (-aX) * m + aY;                // y = mx + b
    double x = (pY - bee) / m;                  // algebra is neat!

    return x > pX;
}
Matt
  • 5,461
  • 7
  • 36
  • 43
  • Ok Matt, I got it to work my only problem is if two polygons are on the same x path it treats them as one and uses the same id number for them. Any thoughts? – Dwill Jan 22 '13 at 22:54
  • Hmm, could you elaborate further? What I am imagining you're saying is that if two polygons share an edge, the app treats them as one polygon? – Matt Jan 23 '13 at 17:22
  • I'm not sure what the issue is. That function should return true if your tap was to the left of the line segment you are testing. You need to run this function on every set of adjacent points in your polygon, and gather the number of times this function returned true. – Matt Feb 26 '13 at 20:00
  • How to do for `Polyline`? – Muhammad Babar Oct 19 '14 at 17:28
  • @MuhammadBabar are you asking how to figure out if a Point is on a Polyline? Or if a Polyline is inside a Polygon? – Matt Oct 20 '14 at 20:48
  • @Matt for `Point is on a Polyline`. Actually i want to detect touch/click on `Polyline` in android. – Muhammad Babar Oct 21 '14 at 04:55
  • 2
    @MuhammadBabar I would ask that on a separate thread. There's a couple ways of approaching that problem. It's tricky because of the need for a tappable area buffer (the odds the user would tap exactly on the line are very slim) – Matt Oct 21 '14 at 18:15
  • mMap.setOnPolygonClickListener you can get the polygon points. – Arpit Patel Jun 18 '20 at 15:40
16

The Google Maps Support library now has a static method that does this check for you:

PolyUtil.containsLocation(LatLng point, List<LatLng>polygon, boolean geodesic);

Although the docs don't mention it explicitly in the guide the method is there

Maps Support Library docs

user1504495
  • 666
  • 8
  • 17
  • This is more Accurate method. And This is really working like charm. Appreciate this answer. Should believe this method as Its from Google Map Utility itself. – Sagar Shah Aug 13 '15 at 12:27
10

With the release of Google Play Services 8.4.0, the Maps API has included support for adding an OnPolygonClickListener to Polygons. Both polygons, polylines and overlays support similar events.

You just need to call GoogleMap.setOnPolygonClickListener(OnPolygonClickListener listener) to set it up, and correspondingly for the other listeners (setOnPolylineClickListener, &c):

map.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() {  
    @Override  
    public void onPolygonClick(Polygon polygon) {  
        // Handle click ...  
    }  
});  

Although a bit late, it solves this use case quite nicely.

matiash
  • 54,791
  • 16
  • 125
  • 154
  • how to detect which polygon is clicked? – Samad May 28 '16 at 11:39
  • @Delta it's the polygon you receive in the listener. – matiash May 28 '16 at 16:34
  • I made a class for polygons that contains IDs but I can't set an ID for a polygon,so how can I figure out polygon's Id? – Samad May 29 '16 at 04:02
  • 1
    @Delta7 You could just create a Map for the references, and add there any Polygons you add to the map. That way you can do a lookup to find the associated id. – matiash May 29 '16 at 05:09
  • Can you retrieve clicked coordinate from Polygon? – Gereltod Aug 08 '16 at 03:49
  • @Gereltod As far as I know, no. Sorry. – matiash Aug 08 '16 at 18:06
  • How to get info associated with Polygon? Let's say some description and PolygonID. – NoWar Dec 25 '17 at 09:00
  • You have to make the polygon clickable, e.g., `polygon.setClickable(true);`. Otherwise, `onPolygonClick()` is not called. (cf. https://stackoverflow.com/a/36043409/661414). @el-programmer: You can add a tag to a polygon which takes any object (e.g., and ID). See https://developers.google.com/android/reference/com/google/android/gms/maps/model/Polygon.html#setTag(java.lang.Object)) – Leukipp Mar 07 '18 at 20:33
4

Though user1504495 has answered in short as I have used it. But instead of using whole Map Utility Library Use this methods.

From your activity class pass params accordingly:

if (area.containsLocation(Touchablelatlong, listLatlong, true))
                isMarkerINSide = true;
            else
                isMarkerINSide = false;

and put following in a Separate class :

/**
     * 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;
    }

    /**
     * Wraps the given value into the inclusive-exclusive interval between min and max.
     * @param n   The value to wrap.
     * @param min The minimum.
     * @param max The maximum.
     */
    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, double m) {
        return ((x % m) + m) % m;
    }

    /**
     * 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);
    }

    /**
     * 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 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 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;
    } 
Sagar Shah
  • 4,272
  • 2
  • 25
  • 36
  • 1
    This answer works for me. Is it possible though to add a tolerance? So if the given location is let's say 50 meters outside of the polygon it still verifies it as valid? – Emanuel Jul 30 '20 at 13:24
1

Here's a full working example to know if a touch happened on a polygon. Some of the answers are more complicated than they need to be. This solution uses the "android-maps-utils"

// compile 'com.google.maps.android:android-maps-utils:0.3.4'
private ArrayList<Polygon> polygonList = new ArrayList<>();

private void addMyPolygons() {
    PolygonOptions options = new PolygonOptions();
    // TODO: make your polygon's however you want
    Polygon polygon = googleMap.addPolygon(options);
    polygonList.add(polygon);
}

@Override
public void onMapClick(LatLng point) {
    boolean contains = false;
    for (Polygon p : polygonList) {
        contains = PolyUtil.containsLocation(point, p.getPoints(), false);
        if (contains) break;
    }
    Toast.makeText(getActivity(), "Click in polygon? "
            + contains, Toast.LENGTH_SHORT).show();
}

@Override
protected void onMapReady(View view, Bundle savedInstanceState) {
    googleMap.setOnMapClickListener(this);
    addMyPolygons();
}
whizzle
  • 2,053
  • 17
  • 21
0

I know I am posting this very late but I had some issue with the answer posted here, so I studied both the top answers and an article (which I think is the origin of this method) and modified Matt Answer to compile something that works best for me.

Problem with Matt Answer: It doesn't calculate the last line of polygon (i.e. one created by the last vertex and the first vertex)

Problem with Dwill Answer: It seems complex and daunting especially when you are already frustrated on how to make things work

Other checks I have added:

  • Checked if a polygon is actually created
  • Checked if any side of polygon is parallel to y-axis

I have tried to comment and explain as much I as I could hope this would be helpful for someone

One more thing, this is written in Dart and mainly focused on finding if current position is inside a geofence.

Future<bool> checkIfLocationIsInsideBoundary({
  required LatLng positionToCheck,
  required List<LatLng> boundaryVertices,
}) async {

  // If there are less than 3 points then there will be no polygon
  if (boundaryVertices.length < 3) return false;

  int intersectCount = 0;
  // Check Ray-cast for lines created by all the vertices in our List
  for (int j = 0; j < boundaryVertices.length - 1; j++) {
    if (_rayCastIntersect(
      positionToCheck,
      boundaryVertices[j],
      boundaryVertices[j + 1],
    )) {
      intersectCount++;
    }
  }
  // Check for line created by the last vertex and the first vertex of the List
  if (_rayCastIntersect(
    positionToCheck,
    boundaryVertices.last,
    boundaryVertices.first,
  )) {
    intersectCount++;
  }

  // If our point is inside the polygon they will always intersect odd number of
  // times, else they will intersect even number of times
  return (intersectCount % 2) == 1; // odd = inside, even = outside
}

bool _rayCastIntersect(LatLng point, LatLng vertA, LatLng vertB) {
  final double aY = vertA.latitude;
  final double bY = vertB.latitude;
  final double aX = vertA.longitude;
  final double bX = vertB.longitude;
  final double pY = point.latitude;
  final double pX = point.longitude;

  // If vertices A and B are both above our point P then obviously the line made
  // by A and B cannot intersect with ray-cast of P. Note: Only y-coordinates of
  // each points can be used to check this.
  if (aY > pY && bY > pY) return false;

  // If vertices A and B are both below our point P then obviously the line made
  // by A and B cannot intersect with ray-cast of P. Note: Only y-coordinates of
  // each points can be used to check this.
  if (aY < pY && bY < pY) return false;

  // Since we will be casting ray on east side from our point P, at least one of
  // the vertex (either A or B) must be east of P else line made by A nd B
  // cannot intersect with ray-cast of P. Note: Only x-coordinates of each
  // points can be used to check this.
  if (aY < pY && bY < pY) return false;

  // If line made by vertices is parallel to Y-axis then we will get
  // 'Divided by zero` exception when calculating slope. In such case we can
  // only check if the line is on the east or the west relative to our point. If
  // it is on the east we count is as intersection. Note: we can be sure our
  // ray-cast will intersect the line because it is a vertical line, our
  // ray-cast is horizontal and finally we already made sure that both the
  // vertices are neither above nor below our point. Finally, since `aX == bX`
  // we can check if either aX or bX is on the right/east of pX
  if (aX == bX) return aX > pX;

  // Calculate slope of the line `m` made by vertices A and B using the formula
  // `m = (y2-y1) / (x2-x1)`
  final double m = (aY - bY) / (aX - bX); // Rise over run

  // Calculate the value of y-intersect `b` using the equation of line
  final double b = aY - (aX * m); // y = mx + b => b = y - mx

  // Now we translate our point P along X-axis such that it intersects our line.
  // This means we can pluck y-coordinate of our point P into the equation of
  // our line and calculate a new x-coordinate
  final double x = (pY - b) / m; // y = mx + b => x = (y - b) / m

  // Till now we have only calculated this new translated point but we don't
  // know if this point was translated towards west(left) of towards
  // east(right). This can be determined in the same way as we have done above,
  // if the x-coordinate of this new point is greater than x-coordinate of our
  // original point then it has shifted east, which means it has intersected our
  // line
  return x > pX;
}
kshitiz_s
  • 1
  • 1
-1

Just for consistency - onMapClick is not called when user taps on a polygon (or other overlay), and it's mentioned in javadoc.

I made a workaround to intercept taps events before MapFragment handles them, and project point to map coordinates and check if the point is inside any polygon, as suggested in other answer.

See more details here

Community
  • 1
  • 1
Mixaz
  • 4,068
  • 1
  • 29
  • 55
  • 1
    This is somewhat incorrect. [`onMapClick(LatLng point)` Called when the user makes a tap gesture on the map, but only if none of the overlays of the map handled the gesture.](http://developer.android.com/reference/com/google/android/gms/maps/GoogleMap.OnMapClickListener.html#onMapClick(com.google.android.gms.maps.model.LatLng)). I cannot find any way for permit a polygon or overlay to handle such an event. So it seems that `onMapClick()` will be called. – dm78 Oct 06 '13 at 18:50