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

Community
  • 1
  • 1
Padin215
  • 7,444
  • 13
  • 65
  • 103

6 Answers6

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
Daniel Boyd
  • 136
  • 1
  • 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? – Padin215 Mar 05 '13 at 23:46
  • Ok, this code breaks in iOS7. Something to do with `CGPathContainsPoint(polygonView.path, NULL, polygonViewPoint, NO)` always returns `FALSE` now. – Padin215 Sep 25 '13 at 20:16
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;
        }
}
Hari
  • 1,509
  • 15
  • 34
  • 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. – Padin215 Dec 10 '12 at 17:13
  • 2
    @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. – Rob Feb 12 '13 at 10:38
  • 1
    Ok, I retract my earlier comment, this does work. Well, it works in iOS6 or less, it breaks in iOS7. – Padin215 Sep 25 '13 at 20:29
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)
        }
    }
}
Ilesh P
  • 3,940
  • 1
  • 24
  • 49
ninja_iOS
  • 1,183
  • 1
  • 13
  • 20
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"

BlackM
  • 3,927
  • 8
  • 39
  • 69
Harsh Jaiswal
  • 256
  • 1
  • 3
  • 5
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)
        }
    }
}
Oliver Pearmain
  • 19,885
  • 13
  • 86
  • 90
srstomp
  • 328
  • 5
  • 12
  • 1
    This works, and has the benefit of not requiring a MKMapView like the MKPolygonView approaches described in other answers. – oh7lzb Jul 21 '17 at 21:07
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
DRVic
  • 2,481
  • 1
  • 15
  • 22
  • 1
    ok, gotcha. working on it now, just trying to figure out how to detect if a ray intersects with a line segment. – Padin215 Apr 11 '12 at 20:52
  • 1
    Adrian Bowyer's A Programmer's Geometry has pseudo-code for it. But you don't need the general case: assume a horizontal ray. – DRVic Apr 11 '12 at 23:07