detect if a point is inside a MKPolygon overlay
Asked Answered
M

6

21

I want to be able to tell if tap is within a MKPolygon.

I have a MKPolygon:

CLLocationCoordinate2D  points[4];

points[0] = CLLocationCoordinate2DMake(41.000512, -109.050116);
points[1] = CLLocationCoordinate2DMake(41.002371, -102.052066);
points[2] = CLLocationCoordinate2DMake(36.993076, -102.041981);
points[3] = CLLocationCoordinate2DMake(36.99892, -109.045267);

MKPolygon* poly = [MKPolygon polygonWithCoordinates:points count:4];

[self.mapView addOverlay:poly];  

//create UIGestureRecognizer to detect a tap
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(foundTap:)];
tapRecognizer.numberOfTapsRequired = 1;
tapRecognizer.numberOfTouchesRequired = 1;
[self.mapView addGestureRecognizer:tapRecognizer];

its just a basic outline of the state Colorado.

I got the tap to lat/long conversion set up:

-(IBAction)foundTap:(UITapGestureRecognizer *)recognizer
{
    CGPoint point = [recognizer locationInView:self.mapView];

    CLLocationCoordinate2D tapPoint = [self.mapView convertPoint:point toCoordinateFromView:self.view];
}

but i am unsure how to tech if my tap point is within the MKPolygon. there does not seem to be a method to do this check, so i'm guessing i need to convert the MKPolygon to a CGRect and use CGRectContainsPoint.

MKPolygon has a .points property but i can't seem to get them back out.

any suggestions?

EDIT:

Both solutions below work in iOS 6 or lower, but breaks in iOS 7. In iOS 7 the polygon.path property allways returns NULL. Ms Anna was kind enough to provide a solution in another SO question here. It involves creating your own path from the polygon points to pass into CGPathContainsPoint().

image of my polygon:

enter image description here

Mcglothlin answered 11/4, 2012 at 16:3 Comment(0)
Y
12

I created this MKPolygon category in case anyone wants to use it. Seems to work well. You have to account for the interior polygons (i.e. holes in the polygon):

@interface MKPolygon (PointInPolygon)
  -(BOOL) pointInPolygon:(CLLocationCoordinate2D) point mapView: (MKMapView*) mapView;
@end

@implementation MKPolygon (PointInPolygon)

-(BOOL) pointInPolygon:(CLLocationCoordinate2D) point mapView: (MKMapView*) mapView {
    MKMapPoint mapPoint = MKMapPointForCoordinate(point);
    MKPolygonView * polygonView = (MKPolygonView*)[mapView viewForOverlay:self];
    CGPoint polygonViewPoint = [polygonView pointForMapPoint:mapPoint];
    return CGPathContainsPoint(polygonView.path, NULL, polygonViewPoint, NO) && 
        ![self pointInInteriorPolygons:point mapView:mapView];
}

-(BOOL) pointInInteriorPolygons:(CLLocationCoordinate2D) point mapView: (MKMapView*) mapView {
    return [self pointInInteriorPolygonIndex:0 point:point mapView:mapView];
}

-(BOOL) pointInInteriorPolygonIndex:(int) index point:(CLLocationCoordinate2D) point mapView: (MKMapView*) mapView {
    if(index >= [self.interiorPolygons count])
        return NO;
    return [[self.interiorPolygons objectAtIndex:index] pointInPolygon:point mapView:mapView] || [self pointInInteriorPolygonIndex:(index+1) point:point mapView:mapView];
}

@end
Yaelyager answered 5/3, 2013 at 23:2 Comment(2)
I just implemented the category and works on my simple polygons (no holes). You say need to take that into account? does that mean it won't detect if the tap is inside a hole?Mcglothlin
Ok, this code breaks in iOS7. Something to do with CGPathContainsPoint(polygonView.path, NULL, polygonViewPoint, NO) always returns FALSE now.Mcglothlin
N
9

Your foundTap method:

-(IBAction)foundTap:(UITapGestureRecognizer *)recognizer
{
    CGPoint point = [recognizer locationInView:self.mapView];

    CLLocationCoordinate2D tapPoint = [self.mapView convertPoint:point toCoordinateFromView:self.view];

    [self pointInsideOverlay:tapPoint];

    if (isInside) 
     {
       ....
     }
}

Here is a method to call from the previous to check if the point is inside the overlay:

-(void)pointInsideOverlay:(CLLocationCoordinate2D )tapPoint 
{
    isInside = FALSE; 

    MKPolygonView *polygonView = (MKPolygonView *)[mapView viewForOverlay:polygonOverlay];

    MKMapPoint mapPoint = MKMapPointForCoordinate(tapPoint);

    CGPoint polygonViewPoint = [polygonView pointForMapPoint:mapPoint];

    BOOL mapCoordinateIsInPolygon = CGPathContainsPoint(polygonView.path, NULL, polygonViewPoint, NO);

        if ( !mapCoordinateIsInPolygon )

            //we are finding points that are inside the overlay
        {
            isInside = TRUE;
        }
}
Namangan answered 19/4, 2012 at 15:44 Comment(3)
This is a good way to do a basic check to see if the tap point is within the rect of the polygon. is not accurate to detect taps within the drawn area of a polygon.Mcglothlin
@Log139 Why do you say that? polygonView is the MKPolygonView, path is its CGPathRef, not its bounding rectangle, and CGPathContainsPoint does proper path/point containment test, no? What am I missing here? I think that if you use [polygonView pointInside:polygonViewPoint withEvent:nil], you might be right. But if you're getting the actual path and calling CGPathContainsPoint, I think you're ok.Concertmaster
Ok, I retract my earlier comment, this does work. Well, it works in iOS6 or less, it breaks in iOS7.Mcglothlin
Y
8

Here is Swift 4.2 updated version thanks to @Steve Stomp

extension MKPolygon {
    func contain(coor: CLLocationCoordinate2D) -> Bool {
        let polygonRenderer = MKPolygonRenderer(polygon: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coor)
        let polygonViewPoint: CGPoint = polygonRenderer.point(for: currentMapPoint)
        if polygonRenderer.path == nil {
            return false
        }else{
            return polygonRenderer.path.contains(polygonViewPoint)
        }
    }
}
Yeeyegg answered 28/7, 2017 at 11:2 Comment(0)
S
7

I am getting MKPolygon Data points from xml file in string. I parse data string to Array of points and use approach give in http://alienryderflex.com/polygon/

It works for me....

-(BOOL)isPoint:(CLLocationCoordinate2D)findLocation inPloygon:(NSArray*)polygon{

    NSMutableArray *tempPolygon=[NSMutableArray arrayWithArray:polygon];
    int   i, j=(int)tempPolygon.count-1 ;
    bool  oddNodes=NO;
    double x=findLocation.latitude;
    double y=findLocation.longitude;

    for (i=0; i<tempPolygon.count; i++) {
        NSString*coordString=[tempPolygon objectAtIndex:i];
        NSArray*pointsOfCoordString=[coordString componentsSeparatedByString:@","];
        CLLocationCoordinate2D point=CLLocationCoordinate2DMake([[pointsOfCoordString objectAtIndex:1] doubleValue], [[pointsOfCoordString objectAtIndex:0] doubleValue]);
        NSString*nextCoordString=[tempPolygon objectAtIndex:j];
        NSArray*nextPointsOfCoordString=[nextCoordString componentsSeparatedByString:@","];
        CLLocationCoordinate2D nextPoint=CLLocationCoordinate2DMake([[nextPointsOfCoordString objectAtIndex:1] doubleValue], [[nextPointsOfCoordString objectAtIndex:0] doubleValue]);


        if ((point.longitude<y && nextPoint.longitude>=y)
            ||  (nextPoint.longitude<y && point.longitude>=y)) {
            if (point.latitude+(y-point.longitude)/(nextPoint.longitude-point.longitude)*(nextPoint.latitude-point.latitude)<x) {
                oddNodes=!oddNodes; }}
        j=i; }


    return oddNodes;

}

my polygon(NSArray) objects are in string for e.g. @"-89.860021,44.944266,0"

Spherulite answered 16/6, 2015 at 7:12 Comment(4)
Could you please elaborate more your answer adding a little more description about the solution you provide?Lagomorph
this method checks your given location lies inside polygon made by data points (Data points are array of geo location). For complete algorithm Please go through alienryderflex.com/polygon.Spherulite
Worked great for me, but I had to change the order of the components of polygon: @"longitude, latitude,0" thank you!Fuss
Perfect SolutionFordone
J
7

This worked for me in #Swift 4.2:

extension MKPolygon {
    func isCoordinateInsidePolyon(coordinate: CLLocationCoordinate2D) -> Bool {
        let polygonRenderer = MKPolygonRenderer(polygon: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate)
        let polygonViewPoint: CGPoint = polygonRenderer.point(for: currentMapPoint)
        if polygonRenderer.path == nil {
            return false
        } else {
            return polygonRenderer.path.contains(polygonViewPoint)
        }
    }
}
Jewelljewelle answered 25/9, 2016 at 19:15 Comment(1)
This works, and has the benefit of not requiring a MKMapView like the MKPolygonView approaches described in other answers.Discommon
U
5

Determining whether a point is in an arbitrary polygon is non-trivial and it is unsurprising that Apple doesn't supply it as part of MKPolygon. You can access the points, which allows you to iterate over the edges.

To determine whether a point p is inside a polygon s, consider each edge as a directed line segment in s. If a ray from p in any fixed direction (typically parallel to either the X or Y axis) intersects the segment, take the sign of the Z component of the cross product of the ray with that directed line segment. If the Z component is > 0, add 1 to a counter. If it is < 0, subtract 1. The trick in implementing this is to avoid issues when the edge is nearly parallel to the ray, or when the ray passes through a vertex (it has to count only once, not once for each edge).

When you have done this for all edges in s, you will have counted the number of times your ray intersects the outline of the polygon, where if the edge was going from left to right you added, and if it was going from right to left, you subtracted. If the resulting sum is zero, you are outside the polygon. Otherwise you are inside it.

There are numerous optimizations possible. One such is to do a quick bounding box test before the more complete test. Another is to have a data structure with bounds on all the edges to trivially discard edges that do not intersect the ray.

Edit: the Z component of A X B ( the cross product of A with B ) is given by:

a.x * b.y - a.y * b.x

since all you care about is the sign, you can check

a.x * b.y > a.y * b.x
Underestimate answered 11/4, 2012 at 18:32 Comment(2)
ok, gotcha. working on it now, just trying to figure out how to detect if a ray intersects with a line segment.Mcglothlin
Adrian Bowyer's A Programmer's Geometry has pseudo-code for it. But you don't need the general case: assume a horizontal ray.Underestimate

© 2022 - 2024 — McMap. All rights reserved.