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

后端 未结 8 812
梦谈多话
梦谈多话 2020-12-23 02:09

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

相关标签:
8条回答
  • 2020-12-23 02:45

    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
    }
    
    0 讨论(0)
  • 2020-12-23 02:45

    @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))
    }
    
    0 讨论(0)
  • 2020-12-23 02:50

    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.

    0 讨论(0)
  • 2020-12-23 02:52

    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")
                        }
                    }
                }
            }
     }
    
    0 讨论(0)
  • 2020-12-23 02:57

    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)
        }
    }
    
    0 讨论(0)
  • 2020-12-23 03:00

    @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 {
            // 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))
    }
    

    Swift 5.x version

    let map = MKMapView()
    let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped))
    map.addGestureRecognizer(mapTap)
    
    @objc func mapTapped(_ tap: UITapGestureRecognizer) {
        if 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: 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: \(String(describing: 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 = map.convert(pt, toCoordinateFrom: map)
        let coordB: CLLocationCoordinate2D = map.convert(ptB, toCoordinateFrom: map)
        return MKMapPoint(coordA).distance(to: MKMapPoint(coordB))
    }
    
    0 讨论(0)
提交回复
热议问题