Domanda

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;

    /* ... */
}
È stato utile?

Soluzione

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.

Altri suggerimenti

UPDATED (For Swift 3 & 4) I'm not sure why people are adding a UIGestureRecognizer to the mapView when mapView already has a number of gesture recognizers running. I found that these methods inhibit the normal functionality of the mapView, in particular, tapping on an annotation. Instead, I'd recommend subclassing the mapView and overriding the touchesEnded method. We can then use the methods others have suggested in this thread and use a delegate method to tell the ViewController to do whatever it needs to do. The "touches" parameter has a set of UITouch objects that we can use:

import UIKit
import MapKit

protocol MapViewTouchDelegate: class {
    func polygonsTapped(polygons: [MKPolygon])
}

class MyMapViewSubclass: MapView {

    weak var mapViewTouchDelegate: MapViewTouchDelegate?

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

       if let touch = touches.first {
           if touch.tapCount == 1 {
               let touchLocation = touch.location(in: self)
               let locationCoordinate = self.convert(touchLocation, toCoordinateFrom: self)
               var polygons: [MKPolygon] = []
               for polygon in self.overlays as! [MKPolygon] {
                   let renderer = MKPolygonRenderer(polygon: polygon)
                   let mapPoint = MKMapPointForCoordinate(locationCoordinate)
                   let viewPoint = renderer.point(for: mapPoint)
                   if renderer.path.contains(viewPoint) {
                       polygons.append(polygon)                        
                   }
                   if polygons.count > 0 {
                       //Do stuff here like use a delegate:
                       self.mapViewTouchDelegate?.polygonsTapped(polygons: polygons)
                   }
               }
           }
       }

    super.touchesEnded(touches, with: event)
}

Don't forget to set the ViewController as the mapViewTouchDelegate. I also found it handy to make an extension for MKPolygon:

import MapKit
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)
        }
    }
}

Then the function can be a little cleaner and the extension may be helpful somewhere else. Plus it's swifty-ier!

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

    if let touch = touches.first {
        if touch.tapCount == 1 {
            let touchLocation = touch.location(in: self)
            let locationCoordinate = self.convert(touchLocation, toCoordinateFrom: self)            
            var polygons: [MKPolygon] = []
            for polygon in self.overlays as! [MKPolygon] {
                if polygon.contains(coordinate: locationCoordinate) {
                    polygons.append(polygon)
                }
            }
            if polygons.count > 0 {
            //Do stuff here like use a delegate:
                self.mapViewTouchDelegate?.polygonsTapped(polygons: polygons)
            }
        }
    }

    super.touchesEnded(touches, with: event)
}

I've found a solution that is similar to @manecosta, but it uses existing Apple APIs to detect intersection more easily.

Create an MKMapRect from the tap location in the View. I used 0.000005 as the lat/long delta to represent a user's touch.

    CGPoint tapPoint = [tap locationInView:self.mapView];
    CLLocationCoordinate2D tapCoordinate = [self.mapView convertPoint:tapPoint toCoordinateFromView:self.mapView];
    MKCoordinateRegion tapCoordinateRection = MKCoordinateRegionMake(tapCoordinate, MKCoordinateSpanMake(0.000005, 0.000005));
    MKMapRect touchMapRect = MKMapRectForCoordinateRegion(tapCoordinateRection);

Search through all MapView overlays and use the 'intersectsMapRect:' function to determine if your current overlay intersect w/ the MapRect you created above.

    for (id<MKOverlay> overlay in self.mapView.overlays) {
        if([overlay isKindOfClass:[MKPolyline class]]){
            MKPolyline *polygon = (MKPolyline*) overlay;
            if([polygon intersectsMapRect:touchMapRect]){
                NSLog(@"found polygon:%@",polygon);
            }
        }
    }

Here is my way in Swift

@IBAction func revealRegionDetailsWithLongPressOnMap(sender: UILongPressGestureRecognizer) {
    if sender.state != UIGestureRecognizerState.Began { return }
    let touchLocation = sender.locationInView(protectedMapView)
    let locationCoordinate = protectedMapView.convertPoint(touchLocation, toCoordinateFromView: protectedMapView)
    //println("Taped at lat: \(locationCoordinate.latitude) long: \(locationCoordinate.longitude)")


    var point = MKMapPointForCoordinate(locationCoordinate)
    var mapRect = MKMapRectMake(point.x, point.y, 0, 0);

    for polygon in protectedMapView.overlays as! [MKPolygon] {
        if polygon.intersectsMapRect(mapRect) {
            println("found")
        }
    }
}

You're not going to able to determine this using the APIs that Apple provides. The best you could do with MapKit would be to maintain a separate database of all of your polygon coordinates as well as the order that the rendered versions are stacked. Then, when the user touches a point, you could do a spatial query on your secondary data to find the polygon(s) in question combined with the stacking order to determine which one they touched.

An easier way to do this if the polygons are relatively static would be to create a map overlay in TileMill with its own interactivity data. Here is an example map that contains interactivity data for countries:

https://a.tiles.mapbox.com/v3/examples.map-zmy97flj/page.html

Notice how some name & image data is retrieved when moused over in the web version. Using the MapBox iOS SDK, which is an open source MapKit clone, you can read that same data out on arbitrary gestures. An example app showing this is here:

https://github.com/mapbox/mapbox-ios-example

That solution might work for your problem and is pretty lightweight as compared to a secondary database and just-in-time calculation of the area touched.

I'm considering to use both overlay and pin Annotation. I get the touch from the pin associated to the overlay.

FOR SWIFT 2.1 Find a point/coordinate in a Polygon

Here is the logic, without tap gestures, to find an annotation inside a polygon.

 //create a polygon
var areaPoints = [CLLocationCoordinate2DMake(50.911864, 8.062454),CLLocationCoordinate2DMake(50.912351, 8.068247),CLLocationCoordinate2DMake(50.908536, 8.068376),CLLocationCoordinate2DMake(50.910159, 8.061552)]


func addDriveArea() {
    //add the polygon
    let polygon = MKPolygon(coordinates: &areaPoints, count: areaPoints.count)
    MapDrive.addOverlay(polygon) //starts the mapView-Function
}

  func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer! {


    if overlay is MKPolygon {

        let renderer = MKPolygonRenderer(overlay: overlay)
        renderer.strokeColor = UIColor.blueColor()
        renderer.lineWidth = 2 

        let coordinate = CLLocationCoordinate2D(latitude: CLLocationDegrees(50.917627), longitude: CLLocationDegrees(8.069562))

        let mappoint = MKMapPointForCoordinate(coordinate)
        let point = polygonView.pointForMapPoint(mappoint)
        let mapPointAsCGP = CGPointMake(point.x, point.y);

        let isInside = CGPathContainsPoint(renderer.path, nil, mapPointAsCGP, false)

        print("IsInside \(isInside)") //true = found

        return renderer
    } else {
        return nil
    }
}

Based on @davidrynn answer i've accomplished a more dynamic and updated result.

Swift 5

Subclass MKMapView:

public class MapView: MKMapView {

public var mapViewProtocol: MapViewProtocol?

public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

    if let touch = touches.first {

        if touch.tapCount == 1 {

            let touchLocation: CGPoint = touch.location(in: self)
            let locationCoordinate: CLLocationCoordinate2D = self.convert(touchLocation, toCoordinateFrom: self)

            var mkCircleList: [MKCircle] = self.overlays.compactMap { $0 as? MKCircle }
            mkCircleList = mkCircleList.filter { $0.contains(locationCoordinate) }
            if !mkCircleList.isEmpty {

                self.mapViewProtocol?.didTapMKCircles(self, mkCircleList)
            }

            var mkMultiPolygonList: [MKMultiPolygon] = self.overlays.compactMap { $0 as? MKMultiPolygon }
            mkMultiPolygonList = mkMultiPolygonList.filter { $0.contains(locationCoordinate) }
            if !mkMultiPolygonList.isEmpty {

                self.mapViewProtocol?.didTapMKMultiPolygons(self, mkMultiPolygonList)
            }

            var mkPolygonList: [MKPolygon] = self.overlays.compactMap { $0 as? MKPolygon }
            mkPolygonList = mkPolygonList.filter { $0.contains(locationCoordinate) }
            if !mkPolygonList.isEmpty {

                self.mapViewProtocol?.didTapMKPolygons(self, mkPolygonList)
            }

            var mkMultiPolylineList: [MKMultiPolyline] = self.overlays.compactMap { $0 as? MKMultiPolyline }
            mkMultiPolylineList = mkMultiPolylineList.filter { $0.contains(locationCoordinate) }
            if !mkMultiPolylineList.isEmpty {

                self.mapViewProtocol?.didTapMKMultiPolylines(self, mkMultiPolylineList)
            }

            var mkPolylineList: [MKPolyline] = self.overlays.compactMap { $0 as? MKPolyline }
            mkPolylineList = mkPolylineList.filter { $0.contains(locationCoordinate) }
            if !mkPolylineList.isEmpty {

                self.mapViewProtocol?.didTapMKPolylines(self, mkPolylineList)
            }

            //TODO
            //var mkTileOverlayList: [MKTileOverlay] = self.overlays.compactMap { $0 as? MKTileOverlay }
            //mkTileOverlayList = mkTileOverlayList.filter { $0.contains(locationCoordinate) }


            self.mapViewProtocol?.didTapMap(self, locationCoordinate)
        }
    }

    super.touchesEnded(touches, with: event)
}

}

After that i created multiple extensions for each mkOverlay type:

MKKCircle

import Foundation
import MapKit

extension MKCircle {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        let renderer = MKCircleRenderer(circle: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate)
        let viewPoint: CGPoint = renderer.point(for: currentMapPoint)
        if renderer.path == nil {

            return false
        } else {

            return renderer.path.contains(viewPoint)
        }
    }
}

MKMultiPolygon

import Foundation
import MapKit

@available(iOS 13.0, *)
extension MKMultiPolygon {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        return self.polygons.filter { $0.contains(coordinate2D) }.isEmpty ? false : true
    }
}

MKMultiPolyline

    import Foundation
import MapKit

@available(iOS 13.0, *)
extension MKMultiPolyline {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        return self.polylines.filter { $0.contains(coordinate2D) }.isEmpty ? false : true
    }
}

MKPolygon

import Foundation
import MapKit

extension MKPolygon {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        let renderer = MKPolygonRenderer(polygon: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate2D)
        let viewPoint: CGPoint = renderer.point(for: currentMapPoint)
        if renderer.path == nil {

            return false
        } else {

            return renderer.path.contains(viewPoint)
        }
    }
}

MKPolyline

import Foundation
import MapKit

extension MKPolyline {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        let renderer = MKPolylineRenderer(polyline: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate2D)
        let viewPoint: CGPoint = renderer.point(for: currentMapPoint)
        if renderer.path == nil {

            return false
        } else {

            return renderer.path.contains(viewPoint)
        }
    }
}

And finally create and implement the protocol:

    public protocol MapViewProtocol {

    func didTapMKPolygons(_ mapView: MKMapView, _ mkPolygons: [MKPolygon])

    func didTapMKCircles(_ mapView: MKMapView, _ mkCircles: [MKCircle])

    func didTapMKPolylines(_ mapView: MKMapView, _ mkPolylines: [MKPolyline])

    func didTapMKMultiPolygons(_ mapView: MKMapView, _ mkMultiPolygons: [MKMultiPolygon])

    func didTapMKMultiPolylines(_ mapView: MKMapView, _ mkMultiPolylines: [MKMultiPolyline])

    func didTapMap(_ mapView: MKMapView, _ clLocationCoordinate2D: CLLocationCoordinate2D)
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top