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}