Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to detect taps on MKPolylines/Overlays like Maps.app?

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!

like image 406
myell0w Avatar asked Jul 29 '12 23:07

myell0w


1 Answers

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);         }     } } 
like image 130
Jenson Avatar answered Nov 05 '22 09:11

Jenson