2012-07-29 8 views
22

Podczas wyświetlania wskazówek na wbudowanej aplikacji Maps.app na iPhonie, możesz "wybrać" jedną z 3 najczęściej wyświetlanych tras, naciskając na nią. Nie chcę replikować tej funkcjonalności i sprawdzać, czy dany kran znajduje się w danej MKPolinie.Jak wykrywać krany na MKPolylines/Overlays jak Maps.app?

Obecnie wykryć kranów na MapView tak:

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

obsłużyć kurki następująco:

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

to działa zgodnie z oczekiwaniami, teraz muszę sprawdzić, czy kran leży dana nakładka widoku MKPolyline (nie ścisłe, ja użytkownik klika gdzieś w pobliżu polilinii powinno to być traktowane jako trafienie).

Co to jest dobry sposób na zrobienie tego?

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

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

Dzięki!

Odpowiedz

44

Pytanie jest dość stare, ale moja odpowiedź może być przydatna dla innych osób szukających rozwiązania tego problemu.

Ten kod wykrywa dotknięcia na liniach wielowierszowych z maksymalną odległością 22 pikseli na każdym poziomie powiększenia. Wystarczy przesunąć UITapGestureRecognizer do 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

świetne rozwiązanie, działa dobrze :), dzięki – polo987

+0

To jest dobre rozwiązanie. Jedno pytanie, co dokładnie tutaj jest obliczane? podwójny u = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta)/(xDelta * xDelta + yDelta * yDelta); ...W pewnym sensie się zgubiłem, czy mógłbyś dodać komentarze, by wyjaśnić, co jest obliczane z tego miejsca i poniżej? – Bocaxica

+1

@Bocaxica ta część nie jest moim kodem. Proszę zapoznać się z http://paulbourke.net/geometry/pointlineplane/ – Jensemann

1

Proponowany poniżej Jensemann rozwiązanie działa świetnie. Zobacz poniżej kod przystosowany do Swift 2, pomyślnie przetestowany na IOS 8 i 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

Możesz polecić moją odpowiedź, która pomoże Ci znaleźć pożądane rozwiązanie.

Dodałem gest na moim MKMapView.

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

Oto jak poradziłem sobie z moim gestem i sprawdziłem, czy kliknięcie jest w widoku nakładki czy nie.

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

       } 

     } 

     } 
    } 

Dzięki. Może ci to pomóc.

0

Aktualizacja dla 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 
} 
+0

Jak dodać narzędzie do rozpoznawania gestów do widoku mapy? – thexande

6

@Jensemanns odpowiedź w Swift 4, który nawiasem mówiąc był jedynym rozwiązaniem, które znalazłem, że pracował dla mnie, aby wykryć kliknie 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)) 
} 
0

Rzeczywistym "ciastkiem" w tym kodzie jest funkcja punktu -> odległość linii. Byłem bardzo szczęśliwy, mogąc go znaleźć i działało świetnie (szybkie 4, iOS 11). Dziękujemy wszystkim, zwłaszcza @Jensemann. Oto mój refaktoryzacji:

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