ios mapkit mkmapview mkoverlay mkpolyline

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



mapkit mkmapview (7)

@Jensemanns responde en Swift 4, que, por cierto, fue la única solución que encontré que funcionó para que detectara clics en un 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)) }

Al mostrar direcciones en el Maps.app incorporado en el iPhone, puede "seleccionar" una de las 3 alternativas de ruta que normalmente se muestran al tocarlo. No voy a replicar esta funcionalidad y comprobar si hay un toque dentro de un MKPolyline dado.

Actualmente detecto grifos 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.numberOfTapsRequired > 1) { [tap requireGestureRecognizerToFail:systemTap]; } } else { [tap requireGestureRecognizerToFail:gesture]; } } [self addGestureRecognizer:tap];

Yo manejo los grifos 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 toque se encuentra dentro del MKPolyline overlayView dado (no es estricto, si el usuario toca en algún lugar cerca de la polilínea, esto debería 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!


@Rashwan L: Actualizó su respuesta a Swift 4.2

let map = MKMapView() let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:))) map.addGestureRecognizer(mapTap) @objc private func mapTapped(_ tap: UITapGestureRecognizer) { if tap.state == .recognized && tap.state == .recognized { // Get map coordinate from touch point let touchPt: CGPoint = tap.location(in: skyMap) let coord: CLLocationCoordinate2D = skyMap.convert(touchPt, toCoordinateFrom: skyMap) let maxMeters: Double = meters(fromPixel: 22, at: touchPt) var nearestDistance: Float = MAXFLOAT var nearestPoly: MKPolyline? = nil // for every overlay ... for overlay: MKOverlay in skyMap.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: /(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 = skyMap.convert(pt, toCoordinateFrom: skyMap) let coordB: CLLocationCoordinate2D = skyMap.convert(ptB, toCoordinateFrom: skyMap) return MKMapPoint(coordA).distance(to: MKMapPoint(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 }


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 de polietileno con una distancia máxima de 22 píxeles en cada nivel de zoom. Simplemente apunte su UITapGestureRecognizer para 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. Vea a continuación el código 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 }


La verdadera "cookie" en este código es el punto -> función de distancia de línea. Estaba tan feliz de encontrarlo y funcionó muy bien (swift 4, iOS 11). Gracias a todos, especialmente a @Jensemann. Aquí está mi refactorización de la misma:

public extension MKPolyline { // Return the point on the polyline that is the closest to the given point // along with the distance between that closest point and the given point. // // Thanks to: // http://paulbourke.net/geometry/pointlineplane/ // https://.com/questions/11713788/how-to-detect-taps-on-mkpolylines-overlays-like-maps-app public func closestPoint(to: MKMapPoint) -> (point: MKMapPoint, distance: CLLocationDistance) { var closestPoint = MKMapPoint() var distanceTo = CLLocationDistance.infinity let points = self.points() for i in 0 ..< pointCount - 1 { let endPointA = points[i] let endPointB = points[i + 1] let deltaX: Double = endPointB.x - endPointA.x let deltaY: Double = endPointB.y - endPointA.y if deltaX == 0.0 && deltaY == 0.0 { continue } // Points must not be equal let u: Double = ((to.x - endPointA.x) * deltaX + (to.y - endPointA.y) * deltaY) / (deltaX * deltaX + deltaY * deltaY) // The magic sauce. See the Paul Bourke link above. let closest: MKMapPoint if u < 0.0 { closest = endPointA } else if u > 1.0 { closest = endPointB } else { closest = MKMapPointMake(endPointA.x + u * deltaX, endPointA.y + u * deltaY) } let distance = MKMetersBetweenMapPoints(closest, to) if distance < distanceTo { closestPoint = closest distanceTo = distance } } return (closestPoint, distanceTo) } }


Puede referirse a mi respuesta y le ayudará a encontrar la solución deseada.

He añadido gesto en mi MKMapView.

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

Así es como manejé mi gesto y descubro si el grifo está en la vista Superposición 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 ayudarte esperanzadamente.