如何检测MKPolylines /覆盖如Maps.app上的水龙头?

当在iPhone上的内置Maps.app上显示方向时,您可以通过点击来select通常显示的3个path选项之一。 我不想复制这个function,并检查一个水龙头是否在一个给定的MKPolyline。

目前我检测到像这样的MapView的水龙头:

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

我处理的水龙头如下:

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

这工作如预期,现在我需要检查是否水龙头位于给定的MKPolyline overlayView(不严格,我用户点击折线附近的地方,这应该作为一个命中处理)。

什么是这样做的好方法?

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

谢谢!

这个问题是相当古老的,但是我的答案可能对寻求这个问题的解决scheme的其他人有用。

此代码检测多行上的触摸,每个缩放级别的最大距离为22像素。 只要指出你的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在Swift 4中的回答,顺便说一句,我发现这个解决scheme是为我检测MKPolyline上的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)) } 

Jensemann下面提出的解决scheme工作得很好。 请参阅下面适用于Swift 2的代码,在IOS 8和9(XCode 7.1)上成功testing。

 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 } 

你可以参考我的答案可能会帮助你find理想的解决scheme。

我在我的MKMapView上添加了手势。

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

这是我如何处理我的手势,并找出是否水龙头在覆盖视图或不。

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

谢谢。 这可以帮助你希望。

更新了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 } 

这个代码中真正的“cookie”是点 – >行距函数。 我很高兴find它,它运作得很好(迅速4,iOS 11)。 感谢大家,尤其是@Jensemann。 这是我的重构:

 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) } }