Comment détecter les robinets sur MKPolylines / Overlays comme Maps.app?

Lors de l'affichage des directions sur le Maps.app embedded sur l'iPhone, vous pouvez "sélectionner" l'une des trois options de routing habituellement affichées en appuyant dessus. Je ne veux pas reproduire cette fonctionnalité et vérifier si un robinet se trouve dans un MKPolyline donné.

Actuellement, je détecte les taps sur le MapView comme ceci:

// 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]; 

Je gère les robinets comme suit:

 - (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]]; } } } } 

Cela fonctionne comme prévu, maintenant j'ai besoin de vérifier si le robinet se trouve dans le MKPolyline overlayView donné (pas ssortingcte, je l'user tape quelque part près de la polyligne cela devrait être manipulé comme un coup).

Quel est un bon moyen de le faire?

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

Merci!

La question est plutôt ancienne, mais ma réponse peut être utile à d'autres personnes à la search d'une solution à ce problème.

Ce code détecte les touches sur les lignes poly avec une distance maximale de 22 pixels dans chaque niveau de zoom. Il suffit de pointer votre UITapGestureRecognizer à 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 répond dans Swift 4, qui d'ailleurs était la seule solution que j'ai trouvé qui a fonctionné pour moi pour détecter les clics sur 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)) } 

La solution proposée ci-dessous par Jensemann fonctionne très bien. Voir ci-dessous le code adapté pour Swift 2, testé avec succès sur IOS 8 et 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 } 

Vous pouvez consulter ma réponse pour vous aider à find la solution désirée.

J'ai ajouté un geste sur mon MKMapView.

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

C'est comme ça que j'ai manipulé mon geste et que je découvre si le robinet est en mode Overlay ou pas.

  - (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; } } } } } 

Merci. Cela peut vous aider à espérer.

Mise à jour pour 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 } 

Le vrai "cookie" dans ce code est la fonction point -> distance de ligne. J'étais tellement content de le find et ça a bien fonctionné (swift 4, iOS 11). Merci à tout le monde, surtout @Jensemann. Voici mon refactoring:

 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://stackoverflow.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) } }