2012-07-29 6 views
22

Beim Anzeigen von Anweisungen auf der integrierten Maps.app auf dem iPhone können Sie eine der normalerweise angezeigten drei Routenalternativen auswählen, indem Sie darauf tippen. Ich möchte diese Funktionalität replizieren und prüfen, ob ein Tap innerhalb einer bestimmten MKPolyline liegt.Wie erkennen Sie Taps auf MKPolylines/Overlays wie Maps.app?

Derzeit erfassen ich Taps auf dem MapView wie folgt aus:

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

ich die Hähne behandeln wie folgt:

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

Dies funktioniert wie erwartet, jetzt ich überprüfen müssen, wenn der Hahn liegt in die angegebene MKPolyline OverlayView (nicht streng, ich tippe irgendwo in der Nähe der Polylinie, sollte dies als Treffer behandelt werden).

Was ist ein guter Weg, dies zu tun?

- (void)handleTapAtPoint:(CGPoint)point { 
    MKPolyline *polyline = self.polyline; 

    // TODO: detect if point lies withing polyline with some margin 
} 

Vielen Dank!

Antwort

44

Die Frage ist ziemlich alt, aber meine Antwort kann für andere Leute nützlich sein, die nach einer Lösung für dieses Problem suchen.

Dieser Code erkennt Berührungen von Polylinien mit einer maximalen Entfernung von 22 Pixeln in jeder Zoomstufe. Zeigen Sie einfach Ihre 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); 
     } 
    } 
} 
+0

große Lösung, funktioniert gut :) dank – polo987

+0

Dies ist eine gute Lösung. Eine Frage, was genau wird hier berechnet? doppelt u = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta)/(xDelta * xDelta + yDelta * yDelta); ...Ich verlasse mich von da an. Könnten Sie ein paar Kommentare hinzufügen, um zu erklären, was von dort und von unten berechnet wird? – Bocaxica

+1

@Bocaxica dieser Teil ist nicht mein Code. Siehe http://paulbourke.net/geometry/pointlineplane/ – Jensemann

1

Die Lösung unterhalb von Jensemann vorgeschlagen arbeitet groß. Siehe den unten für Swift 2 angepassten Code, erfolgreich getestet auf IOS 8 und 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 
} 
1

Sie können meine Antwort beziehen kann es Ihnen helfen, gewünschte Lösung zu finden.

Ich habe Gesten auf meinem MKMapView hinzugefügt.

So behandelte ich meine Geste und finde heraus, ob der Tap auf der Overlay-Ansicht ist oder nicht.

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

       } 

     } 

     } 
    } 

Danke. Dies kann Ihnen hoffentlich helfen.

0

für Swift 3

Aktualisiert
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 
} 
+0

Wie fügen Sie den Gestenerkenner der Mapview dafür hinzu? – thexande

6

@Jensemanns Antwort in Swift 4, die durch die Art und Weise war die einzige Lösung, die ich gefunden, dass für mich gearbeitet Klicks ein auf MKPolyline zu erkennen:

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

Der eigentliche "Cookie" in diesem Code ist die Funktion Punkt -> Zeilenabstand. Ich war so glücklich, es zu finden und es funktionierte großartig (swift 4, iOS 11). Danke an alle, besonders @Jensemann. Hier ist mein Refactoring davon:

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