¿Cómo detectar los toques en MKPolylines / Overlays como Maps.app?

Al mostrar indicaciones en la aplicación Maps.app incorporada en el iPhone, puede "seleccionar" una de las alternativas de ruta que generalmente se muestran presionándola. No quería replicar esta funcionalidad y comprobar si un grifo se encuentra dentro de una determinada línea MKPolyline.

Actualmente detecto toques en el MapView así:

// Add Gesture Recognizer to MapView to detect taps UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapTap:)]; // we require all gesture recognizer except other single-tap gesture recognizers to fail for (UIGestureRecognizer *gesture in self.gestureRecognizers) { if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) { UITapGestureRecognizer *systemTap = (UITapGestureRecognizer *)gesture; if (systemTap.numberOfTapsRequinetworking > 1) { [tap requireGestureRecognizerToFail:systemTap]; } } else { [tap requireGestureRecognizerToFail:gesture]; } } [self addGestureRecognizer:tap]; 

Manejo los toques de la siguiente manera:

 - (void)handleMapTap:(UITapGestureRecognizer *)tap { if ((tap.state & UIGestureRecognizerStateRecognized) == UIGestureRecognizerStateRecognized) { // Check if the overlay got tapped if (overlayView != nil) { // Get view frame rect in the mapView's coordinate system CGRect viewFrameInMapView = [overlayView.superview convertRect:overlayView.frame toView:self]; // Get touch point in the mapView's coordinate system CGPoint point = [tap locationInView:self]; // Check if the touch is within the view bounds if (CGRectContainsPoint(viewFrameInMapView, point)) { [overlayView handleTapAtPoint:[tap locationInView:self.directionsOverlayView]]; } } } } 

Esto funciona como se esperaba, ahora necesito verificar si el tap está dentro de la vista de superposition de MKPolyline (no es estricto, yo el usuario toca en algún lugar cerca de la polilínea esto debe manejarse como un golpe).

¿Qué es una buena manera de hacer esto?

 - (void)handleTapAtPoint:(CGPoint)point { MKPolyline *polyline = self.polyline; // TODO: detect if point lies withing polyline with some margin } 

¡Gracias!

La pregunta es bastante antigua, pero mi respuesta puede ser útil para otras personas que buscan una solución para este problema.

Este código detecta toques en líneas poly con una distancia máxima de 22 píxeles en cada nivel de zoom. Simplemente apunte su UITapGestureRecognizer a 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); } } } 

La solución propuesta a continuación por Jensemann está funcionando muy bien. Consulte el código a continuación adaptado para Swift 2, probado con éxito en IOS 8 y 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 } 

Puedes referirme a mi respuesta, ya que te ayudará a encontrar la solución deseada.

Agregué un gesto en mi MKMapView.

 [mapV addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(mapTapped:)]]; 

Así es como maneja mi gesto y descubro si el toque está en la vista de superposition o no.

  - (void)mapTapped:(UITapGestureRecognizer *)recognizer { MKMapView *mapView = (MKMapView *)recognizer.view; CGPoint tapPoint = [recognizer locationInView:mapView]; NSLog(@"tapPoint = %f,%f",tapPoint.x, tapPoint.y); //convert screen CGPoint tapPoint to CLLocationCoordinate2D... CLLocationCoordinate2D tapCoordinate = [mapView convertPoint:tapPoint toCoordinateFromView:mapView]; //convert CLLocationCoordinate2D tapCoordinate to MKMapPoint... MKMapPoint point = MKMapPointForCoordinate(tapCoordinate); if (mapView.overlays.count > 0 ) { for (id<MKOverlay> overlay in mapView.overlays) { if ([overlay isKindOfClass:[MKCircle class]]) { MKCircle *circle = overlay; MKCircleRenderer *circleRenderer = (MKCircleRenderer *)[mapView rendererForOverlay:circle]; //convert MKMapPoint tapMapPoint to point in renderer's context... CGPoint datpoint = [circleRenderer pointForMapPoint:point]; [circleRenderer invalidatePath]; if (CGPathContainsPoint(circleRenderer.path, nil, datpoint, false)){ NSLog(@"tapped on overlay"); break; } } } } } 

Gracias. Esto puede ayudarlo con suerte.

Respuesta de @Jensemanns en Swift 4, que por cierto fue la única solución que encontré que me funcionó para detectar los clics en una línea MKPolyline :

 let map = MKMapView() let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:))) map.addGestureRecognizer(mapTap) func mapTapped(_ tap: UITapGestureRecognizer) { if tap.state == .recognized && 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)) } 

Actualizado para Swift 3

 func isTappedOnPolygon(with tapGesture:UITapGestureRecognizer, on mapView: MKMapView) -> Bool { let tappedMapView = tapGesture.view let tappedPoint = tapGesture.location(in: tappedMapView) let tappedCoordinates = mapView.convert(tappedPoint, toCoordinateFrom: tappedMapView) let point:MKMapPoint = MKMapPointForCoordinate(tappedCoordinates) let overlays = mapView.overlays.filter { o in o is MKPolygon } for overlay in overlays { let polygonRenderer = MKPolygonRenderer(overlay: overlay) let datPoint = polygonRenderer.point(for: point) polygonRenderer.invalidatePath() return polygonRenderer.path.contains(datPoint) } return false }