iostouchmkmapviewmapkitmkoverlay

Detecting touches on MKOverlay in iOS7 (MKOverlayRenderer)


I have an MKMapView with possibly hundreds of polygons drawn. Using MKPolygon and MKPolygonRenderer as one is suppose to on iOS7.

What I need is a way of acting upon the user touching one of the polygons. They represent an area on the map with a certain population density for example. On iOS6 the MKOverlays were drawn as MKOverlayViews so touch detection was more straightforward. Now using renderers I don't really see how this is suppose to be done.

I'm not sure this will help or is even relevant but as a reference I'll post some code:

This adds all the MKOverlays to the MKMapView using mapData.

-(void)drawPolygons{
    self.polygonsInfo = [NSMutableDictionary dictionary];
    NSArray *polygons = [self.mapData valueForKeyPath:@"polygons"];

    for(NSDictionary *polygonInfo in polygons){
        NSArray *polygonPoints = [polygonInfo objectForKey:@"boundary"];
        int numberOfPoints = [polygonPoints count];

        CLLocationCoordinate2D *coordinates = malloc(numberOfPoints * sizeof(CLLocationCoordinate2D));
        for (int i = 0; i < numberOfPoints; i++){
            NSDictionary *pointInfo = [polygonPoints objectAtIndex:i];

            CLLocationCoordinate2D point;
            point.latitude = [[pointInfo objectForKey:@"lat"] floatValue];
            point.longitude = [[pointInfo objectForKey:@"long"] floatValue];

            coordinates[i] = point;
        }

        MKPolygon *polygon = [MKPolygon polygonWithCoordinates:coordinates count:numberOfPoints];
        polygon.title = [polygonInfo objectForKey:@"name"];
        free(coordinates);
        [self.mapView addOverlay:polygon];
        [self.polygonsInfo setObject:polygonInfo forKey:polygon.title]; // Saving this element information, indexed by title, for later use on mapview delegate method
    }
}

Then there is the delegate method for returning a MKOverlayRenderer for each MKOverlay:

-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay{
    /* ... */

    MKPolygon *polygon = (MKPolygon*) overlay;
    NSDictionary *polygonInfo = [self.polygonsInfo objectForKey:polygon.title]; // Retrieving element info by element title
    NSDictionary *colorInfo = [polygonInfo objectForKey:@"color"];

    MKPolygonRenderer *polygonRenderer = [[MKPolygonRenderer alloc] initWithPolygon:polygon];

    polygonRenderer.fillColor = [UIColor colorWithRed:[[colorInfo objectForKey:@"red"] floatValue]
                                               green:[[colorInfo objectForKey:@"green"] floatValue]
                                                blue:[[colorInfo objectForKey:@"blue"] floatValue]
                                               alpha:[[polygonInfo objectForKey:@"opacity"] floatValue]];

    return polygonRenderer;

    /* ... */
}

Solution

  • I've done it.

    Thanks to incanus and Anna!

    Basically I add a TapGestureRecognizer to the MapView, convert the point tapped to map coordinates, go through my overlays and check with CGPathContainsPoint.

    Adding TapGestureRecognizer. I did that trick of adding a second double tap gesture, so that the single tap gesture isn't fired when doing a double tap to zoom on map. If anyone knows a better way, I'm glad to hear!

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapTap:)];
    tap.cancelsTouchesInView = NO;
    tap.numberOfTapsRequired = 1;
    
    UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc] init];
    tap2.cancelsTouchesInView = NO;
    tap2.numberOfTapsRequired = 2;
    
    [self.mapView addGestureRecognizer:tap2];
    [self.mapView addGestureRecognizer:tap];
    [tap requireGestureRecognizerToFail:tap2]; // Ignore single tap if the user actually double taps
    

    Then, on the tap handler:

    -(void)handleMapTap:(UIGestureRecognizer*)tap{
        CGPoint tapPoint = [tap locationInView:self.mapView];
    
        CLLocationCoordinate2D tapCoord = [self.mapView convertPoint:tapPoint toCoordinateFromView:self.mapView];
        MKMapPoint mapPoint = MKMapPointForCoordinate(tapCoord);
        CGPoint mapPointAsCGP = CGPointMake(mapPoint.x, mapPoint.y);
    
        for (id<MKOverlay> overlay in self.mapView.overlays) {
            if([overlay isKindOfClass:[MKPolygon class]]){
                MKPolygon *polygon = (MKPolygon*) overlay;
    
                CGMutablePathRef mpr = CGPathCreateMutable();
    
                MKMapPoint *polygonPoints = polygon.points;
    
                for (int p=0; p < polygon.pointCount; p++){
                    MKMapPoint mp = polygonPoints[p];
                    if (p == 0)
                        CGPathMoveToPoint(mpr, NULL, mp.x, mp.y);
                    else
                        CGPathAddLineToPoint(mpr, NULL, mp.x, mp.y);
                }
    
                if(CGPathContainsPoint(mpr , NULL, mapPointAsCGP, FALSE)){
                    // ... found it!
                }
    
                CGPathRelease(mpr);
            }
        }
    }
    

    I could ask for the MKPolygonRenderer which already has the "path" property and use it, but for some reason it is always nil. I did read someone saying that I could call invalidatePath on the renderer and it does fill the path property but it just seems wrong as the point is never found inside any of the polygons. That is why I rebuild the path from the points. This way I don't even need the renderer and just make use of the MKPolygon object.