可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
When displaying directions on the built-in Maps.app on the iPhone you can "select" one of the usually 3 route alternatives that are displayed by tapping on it. I wan't to replicate this functionality and check if a tap lies within a given MKPolyline.
Currently I detect taps on the MapView like this:
// 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];
I handle the taps as follows:
- (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]];
}
}
}
}
This works as expected, now I need to check if the tap lies within the given MKPolyline overlayView (not strict, I the user taps somewhere near the polyline this should be handled as a hit).
What's a good way to do this?
- (void)handleTapAtPoint:(CGPoint)point {
MKPolyline *polyline = self.polyline;
// TODO: detect if point lies withing polyline with some margin
}
Thanks!
回答1:
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);
}
}
}
回答2:
@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 && 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))
}
回答3:
You can refer my answer may it will help you to find desired solution.
I've added gesture on my MKMapView.
[mapV addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(mapTapped:)]];
This is how i handled my gesture and find out whether the tap is on Overlay view or not.
- (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;
}
}
}
}
}
Thanks.
This may help you hopefully.
回答4:
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
}
回答5:
Updated for 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
}
回答6:
@Rashwan L : Updated his answer to 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))
}
回答7:
The real "cookie" in this code is the point -> line distance function. I was so happy to find it and it worked great (swift 4, iOS 11). Thanks to everyone, especially @Jensemann. Here is my refactoring of it:
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)
}
}
回答8:
It's an old thread however I found a different way which may help anyone. Tested on multiple routes overlay in Swift 4.2.
@IBAction func didTapGesture(_ sender: UITapGestureRecognizer) {
let touchPoint = sender.location(in: mapView)
let touchCoordinate = mapView.convert(touchPoint, toCoordinateFrom: mapView)
let mapPoint = MKMapPoint(touchCoordinate)
for overlay in mapView.overlays {
if overlay is MKPolyline {
if let polylineRenderer = mapView.renderer(for: overlay) as? MKPolylineRenderer {
let polylinePoint = polylineRenderer.point(for: mapPoint)
if polylineRenderer.path.contains(polylinePoint) {
print("polyline was tapped")
}
}
}
}
}