How to detect taps on MKPolylines/Overlays like Maps.app? How to detect taps on MKPolylines/Overlays like Maps.app? ios ios

How to detect taps on MKPolylines/Overlays like Maps.app?


The question is rather old, but my answer may be useful to other people looking for a solution for this problem.

This code detects touches on poly lines with a maximum distance of 22 pixels in every zoom level. Just point your UITapGestureRecognizer to handleTap:

/** Returns the distance of |pt| to |poly| in meters * * from http://paulbourke.net/geometry/pointlineplane/DistancePoint.java * */- (double)distanceOfPoint:(MKMapPoint)pt toPoly:(MKPolyline *)poly{    double distance = MAXFLOAT;    for (int n = 0; n < poly.pointCount - 1; n++) {        MKMapPoint ptA = poly.points[n];        MKMapPoint ptB = poly.points[n + 1];        double xDelta = ptB.x - ptA.x;        double yDelta = ptB.y - ptA.y;        if (xDelta == 0.0 && yDelta == 0.0) {            // Points must not be equal            continue;        }        double u = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta);        MKMapPoint ptClosest;        if (u < 0.0) {            ptClosest = ptA;        }        else if (u > 1.0) {            ptClosest = ptB;        }        else {            ptClosest = MKMapPointMake(ptA.x + u * xDelta, ptA.y + u * yDelta);        }        distance = MIN(distance, MKMetersBetweenMapPoints(ptClosest, pt));    }    return distance;}/** Converts |px| to meters at location |pt| */- (double)metersFromPixel:(NSUInteger)px atPoint:(CGPoint)pt{    CGPoint ptB = CGPointMake(pt.x + px, pt.y);    CLLocationCoordinate2D coordA = [mapView convertPoint:pt toCoordinateFromView:mapView];    CLLocationCoordinate2D coordB = [mapView convertPoint:ptB toCoordinateFromView:mapView];    return MKMetersBetweenMapPoints(MKMapPointForCoordinate(coordA), MKMapPointForCoordinate(coordB));}#define MAX_DISTANCE_PX 22.0f- (void)handleTap:(UITapGestureRecognizer *)tap{    if ((tap.state & UIGestureRecognizerStateRecognized) == UIGestureRecognizerStateRecognized) {        // Get map coordinate from touch point        CGPoint touchPt = [tap locationInView:mapView];        CLLocationCoordinate2D coord = [mapView convertPoint:touchPt toCoordinateFromView:mapView];        double maxMeters = [self metersFromPixel:MAX_DISTANCE_PX atPoint:touchPt];        float nearestDistance = MAXFLOAT;        MKPolyline *nearestPoly = nil;        // for every overlay ...        for (id <MKOverlay> overlay in mapView.overlays) {            // .. if MKPolyline ...            if ([overlay isKindOfClass:[MKPolyline class]]) {                // ... get the distance ...                float distance = [self distanceOfPoint:MKMapPointForCoordinate(coord)                                                toPoly:overlay];                // ... and find the nearest one                if (distance < nearestDistance) {                    nearestDistance = distance;                    nearestPoly = overlay;                }            }        }        if (nearestDistance <= maxMeters) {            NSLog(@"Touched poly: %@\n"                   "    distance: %f", nearestPoly, nearestDistance);        }    }}


@Jensemanns answer in Swift 4, which by the way was the only solution that I found that worked for me to detect clicks on a MKPolyline:

let map = MKMapView()let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))map.addGestureRecognizer(mapTap)func mapTapped(_ tap: UITapGestureRecognizer) {    if tap.state == .recognized {        // Get map coordinate from touch point        let touchPt: CGPoint = tap.location(in: map)        let coord: CLLocationCoordinate2D = map.convert(touchPt, toCoordinateFrom: map)        let maxMeters: Double = meters(fromPixel: 22, at: touchPt)        var nearestDistance: Float = MAXFLOAT        var nearestPoly: MKPolyline? = nil        // for every overlay ...        for overlay: MKOverlay in map.overlays {            // .. if MKPolyline ...            if (overlay is MKPolyline) {                // ... get the distance ...                let distance: Float = Float(distanceOf(pt: MKMapPointForCoordinate(coord), toPoly: overlay as! MKPolyline))                // ... and find the nearest one                if distance < nearestDistance {                    nearestDistance = distance                    nearestPoly = overlay as! MKPolyline                }            }        }        if Double(nearestDistance) <= maxMeters {            print("Touched poly: \(nearestPoly) distance: \(nearestDistance)")        }    }}func distanceOf(pt: MKMapPoint, toPoly poly: MKPolyline) -> Double {    var distance: Double = Double(MAXFLOAT)    for n in 0..<poly.pointCount - 1 {        let ptA = poly.points()[n]        let ptB = poly.points()[n + 1]        let xDelta: Double = ptB.x - ptA.x        let yDelta: Double = ptB.y - ptA.y        if xDelta == 0.0 && yDelta == 0.0 {            // Points must not be equal            continue        }        let u: Double = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta)        var ptClosest: MKMapPoint        if u < 0.0 {            ptClosest = ptA        }        else if u > 1.0 {            ptClosest = ptB        }        else {            ptClosest = MKMapPointMake(ptA.x + u * xDelta, ptA.y + u * yDelta)        }        distance = min(distance, MKMetersBetweenMapPoints(ptClosest, pt))    }    return distance}func meters(fromPixel px: Int, at pt: CGPoint) -> Double {    let ptB = CGPoint(x: pt.x + CGFloat(px), y: pt.y)    let coordA: CLLocationCoordinate2D = map.convert(pt, toCoordinateFrom: map)    let coordB: CLLocationCoordinate2D = map.convert(ptB, toCoordinateFrom: map)    return MKMetersBetweenMapPoints(MKMapPointForCoordinate(coordA), MKMapPointForCoordinate(coordB))}

Swift 5.x version

let map = MKMapView()let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped))map.addGestureRecognizer(mapTap)@objc func mapTapped(_ tap: UITapGestureRecognizer) {    if tap.state == .recognized {        // Get map coordinate from touch point        let touchPt: CGPoint = tap.location(in: map)        let coord: CLLocationCoordinate2D = map.convert(touchPt, toCoordinateFrom: map)        let maxMeters: Double = meters(fromPixel: 22, at: touchPt)        var nearestDistance: Float = MAXFLOAT        var nearestPoly: MKPolyline? = nil        // for every overlay ...        for overlay: MKOverlay in map.overlays {            // .. if MKPolyline ...            if (overlay is MKPolyline) {                // ... get the distance ...                let distance: Float = Float(distanceOf(pt: MKMapPoint(coord), toPoly: overlay as! MKPolyline))                // ... and find the nearest one                if distance < nearestDistance {                    nearestDistance = distance                    nearestPoly = overlay as? MKPolyline                }            }        }        if Double(nearestDistance) <= maxMeters {            print("Touched poly: \(String(describing: nearestPoly)) distance: \(nearestDistance)")        }    }}private func distanceOf(pt: MKMapPoint, toPoly poly: MKPolyline) -> Double {    var distance: Double = Double(MAXFLOAT)    for n in 0..<poly.pointCount - 1 {        let ptA = poly.points()[n]        let ptB = poly.points()[n + 1]        let xDelta: Double = ptB.x - ptA.x        let yDelta: Double = ptB.y - ptA.y        if xDelta == 0.0 && yDelta == 0.0 {            // Points must not be equal            continue        }        let u: Double = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta)        var ptClosest: MKMapPoint        if u < 0.0 {            ptClosest = ptA        }        else if u > 1.0 {            ptClosest = ptB        }        else {            ptClosest = MKMapPoint(x: ptA.x + u * xDelta, y: ptA.y + u * yDelta)        }        distance = min(distance, ptClosest.distance(to: pt))    }    return distance}private func meters(fromPixel px: Int, at pt: CGPoint) -> Double {    let ptB = CGPoint(x: pt.x + CGFloat(px), y: pt.y)    let coordA: CLLocationCoordinate2D = map.convert(pt, toCoordinateFrom: map)    let coordB: CLLocationCoordinate2D = map.convert(ptB, toCoordinateFrom: map)    return MKMapPoint(coordA).distance(to: MKMapPoint(coordB))}


The solution proposed below by Jensemann is working great. See below code adapted for Swift 2, tested successfully on IOS 8 and 9 (XCode 7.1).

func didTapMap(gestureRecognizer: UIGestureRecognizer) {    tapPoint = gestureRecognizer.locationInView(mapView)    NSLog("tapPoint = %f,%f",tapPoint.x, tapPoint.y)    //convert screen CGPoint tapPoint to CLLocationCoordinate2D...    let tapCoordinate = mapView.convertPoint(tapPoint, toCoordinateFromView: mapView)    let tapMapPoint = MKMapPointForCoordinate(tapCoordinate)    print("tap coordinates = \(tapCoordinate)")    print("tap map point = \(tapMapPoint)")    // Now we test to see if one of the overlay MKPolyline paths were tapped    var nearestDistance = Double(MAXFLOAT)    let minDistance = 2000      // in meters, adjust as needed    var nearestPoly = MKPolyline()    // arrayPolyline below is an array of MKPolyline overlaid on the mapView    for poly in arrayPolyline {                        // ... get the distance ...        let distance = distanceOfPoint(tapMapPoint, poly: poly)        print("distance = \(distance)")        // ... and find the nearest one        if (distance < nearestDistance) {            nearestDistance = distance            nearestPoly = poly        }    }    if (nearestDistance <= minDistance) {        NSLog("Touched poly: %@\n    distance: %f", nearestPoly, nearestDistance);    }}func distanceOfPoint(pt: MKMapPoint, poly: MKPolyline) -> Double {    var distance: Double = Double(MAXFLOAT)    var linePoints: [MKMapPoint] = []    var polyPoints = UnsafeMutablePointer<MKMapPoint>.alloc(poly.pointCount)    for point in UnsafeBufferPointer(start: poly.points(), count: poly.pointCount) {        linePoints.append(point)        print("point: \(point.x),\(point.y)")    }    for n in 0...linePoints.count - 2 {        let ptA = linePoints[n]        let ptB = linePoints[n+1]        let xDelta = ptB.x - ptA.x        let yDelta = ptB.y - ptA.y        if (xDelta == 0.0 && yDelta == 0.0) {            // Points must not be equal            continue        }        let u: Double = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta)        var ptClosest = MKMapPoint()        if (u < 0.0) {            ptClosest = ptA        } else if (u > 1.0) {            ptClosest = ptB        } else {            ptClosest = MKMapPointMake(ptA.x + u * xDelta, ptA.y + u * yDelta);        }        distance = min(distance, MKMetersBetweenMapPoints(ptClosest, pt))    }    return distance}