在iOS7中检测MKOverlay(MKOverlayRenderer)上的触摸

26

我有一个MKMapView,可能会绘制数百个多边形。在iOS7上使用MKPolygon和MKPolygonRenderer。

我需要的是一种通过用户触摸其中一个多边形来进行操作的方法。例如,它们表示地图上的某个区域具有特定的人口密度。 在iOS6中,MKOverlays被绘制为MKOverlayViews,因此触摸检测更加直观。现在使用渲染器,我不太清楚应该如何完成这项任务。

我不确定这是否有帮助或相关,但作为参考,我将发布一些代码:

这将使用mapData将所有MKOverlays添加到MKMapView中。

-(void)drawPolygons{
    self.polygonsInfo = [NSMutableDictionary dictionary];
    NSArray *polygons = [self.mapData valueForKeyPath:@"polygons"];

    for(NSDictionary *polygonInfo in polygons){
        NSArray *polygonPoints = [polygonInfo objectForKey:@"boundary"];
        int numberOfPoints = [polygonPoints count];

        CLLocationCoordinate2D *coordinates = malloc(numberOfPoints * sizeof(CLLocationCoordinate2D));
        for (int i = 0; i < numberOfPoints; i++){
            NSDictionary *pointInfo = [polygonPoints objectAtIndex:i];

            CLLocationCoordinate2D point;
            point.latitude = [[pointInfo objectForKey:@"lat"] floatValue];
            point.longitude = [[pointInfo objectForKey:@"long"] floatValue];

            coordinates[i] = point;
        }

        MKPolygon *polygon = [MKPolygon polygonWithCoordinates:coordinates count:numberOfPoints];
        polygon.title = [polygonInfo objectForKey:@"name"];
        free(coordinates);
        [self.mapView addOverlay:polygon];
        [self.polygonsInfo setObject:polygonInfo forKey:polygon.title]; // Saving this element information, indexed by title, for later use on mapview delegate method
    }
}

然后是每个MKOverlay返回MKOverlayRenderer的委托方法:

-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay{
    /* ... */

    MKPolygon *polygon = (MKPolygon*) overlay;
    NSDictionary *polygonInfo = [self.polygonsInfo objectForKey:polygon.title]; // Retrieving element info by element title
    NSDictionary *colorInfo = [polygonInfo objectForKey:@"color"];

    MKPolygonRenderer *polygonRenderer = [[MKPolygonRenderer alloc] initWithPolygon:polygon];

    polygonRenderer.fillColor = [UIColor colorWithRed:[[colorInfo objectForKey:@"red"] floatValue]
                                               green:[[colorInfo objectForKey:@"green"] floatValue]
                                                blue:[[colorInfo objectForKey:@"blue"] floatValue]
                                               alpha:[[polygonInfo objectForKey:@"opacity"] floatValue]];

    return polygonRenderer;

    /* ... */
}

iOS 6中的覆盖层触摸检测“更加简单”是什么意思?您在iOS 6中使用了哪种方法? - user467105
MKOverlayViews 我猜是指覆盖视图。 - Daij-Djan
我不知道IOS7的方式是什么,恐怕无法回答。 - Daij-Djan
iOS6使用MKOverlayViews绘制覆盖层。您可以随时向视图添加手势识别器。在iOS7中,由于没有视图可供添加手势,因此无法这样做。这就是我的意思。 - manecosta
@manecosta,尽管MKOverlayViews是UIView的子类,但我从未成功地让手势识别器与它们实际配合使用。在我的经验中,添加它们并分配目标没有任何效果。 - user467105
1
@Anna 嗯,我从未真正尝试过那个,我只是认为那可能是一个选项。无论如何,我已经通过叠加层完成了它,并使用CGPathContainsPoint进行了测试。 - manecosta
8个回答

26

我做到了。

感谢 incanusAnna

基本上,我在地图视图上添加了一个TapGestureRecognizer手势,将点击的点转换为地图坐标,遍历我的覆盖物并使用CGPathContainsPoint检查。

添加TapGestureRecognizer手势。我采用了添加第二个双击手势的技巧,这样单击手势不会在对地图进行双击缩放时触发。如果有人知道更好的方法,我很乐意听取建议!

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapTap:)];
tap.cancelsTouchesInView = NO;
tap.numberOfTapsRequired = 1;

UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc] init];
tap2.cancelsTouchesInView = NO;
tap2.numberOfTapsRequired = 2;

[self.mapView addGestureRecognizer:tap2];
[self.mapView addGestureRecognizer:tap];
[tap requireGestureRecognizerToFail:tap2]; // Ignore single tap if the user actually double taps

接下来,在水龙头处理程序中:

-(void)handleMapTap:(UIGestureRecognizer*)tap{
    CGPoint tapPoint = [tap locationInView:self.mapView];

    CLLocationCoordinate2D tapCoord = [self.mapView convertPoint:tapPoint toCoordinateFromView:self.mapView];
    MKMapPoint mapPoint = MKMapPointForCoordinate(tapCoord);
    CGPoint mapPointAsCGP = CGPointMake(mapPoint.x, mapPoint.y);

    for (id<MKOverlay> overlay in self.mapView.overlays) {
        if([overlay isKindOfClass:[MKPolygon class]]){
            MKPolygon *polygon = (MKPolygon*) overlay;

            CGMutablePathRef mpr = CGPathCreateMutable();

            MKMapPoint *polygonPoints = polygon.points;

            for (int p=0; p < polygon.pointCount; p++){
                MKMapPoint mp = polygonPoints[p];
                if (p == 0)
                    CGPathMoveToPoint(mpr, NULL, mp.x, mp.y);
                else
                    CGPathAddLineToPoint(mpr, NULL, mp.x, mp.y);
            }

            if(CGPathContainsPoint(mpr , NULL, mapPointAsCGP, FALSE)){
                // ... found it!
            }

            CGPathRelease(mpr);
        }
    }
}

我可以请求 MKPolygonRenderer 已经具有 "path" 属性并使用它,但由于某种原因它总是 nil。我确实看到有人说过我可以在 renderer 上调用 invalidatePath 并且它会填充路径属性,但这似乎是错误的,因为该点从未在任何多边形内找到。这就是为什么我重新从点创建路径的原因。这样,我甚至不需要渲染器,只需利用 MKPolygon 对象即可。


一个值得优化的地方是只对在视图中的MKOverlay运行此检查。但我不确定如何最好地判断MKOverlay何时离开可见边界,这就是为什么我发布了这个问题:http://stackoverflow.com/questions/23147789/how-can-i-tell-when-an-mkoverlay-is-no-longer-within-view - ericsoco

12

更新(适用于 Swift 3 & 4)我不确定为什么有人在地图视图上添加UIGestureRecognizer,当地图视图已经运行了许多手势识别器。我发现这些方法会抑制地图视图的正常功能,特别是点击注释。相反,我建议子类化地图视图并覆盖touchesEnded方法。然后,我们可以使用其他人在此线程中建议的方法,并使用委托方法告诉ViewController执行任何需要执行的操作。 "touches"参数具有一组UITouch对象,我们可以使用它们:

import UIKit
import MapKit

protocol MapViewTouchDelegate: class {
    func polygonsTapped(polygons: [MKPolygon])
}

class MyMapViewSubclass: MapView {

    weak var mapViewTouchDelegate: MapViewTouchDelegate?

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

       if let touch = touches.first {
           if touch.tapCount == 1 {
               let touchLocation = touch.location(in: self)
               let locationCoordinate = self.convert(touchLocation, toCoordinateFrom: self)
               var polygons: [MKPolygon] = []
               for polygon in self.overlays as! [MKPolygon] {
                   let renderer = MKPolygonRenderer(polygon: polygon)
                   let mapPoint = MKMapPointForCoordinate(locationCoordinate)
                   let viewPoint = renderer.point(for: mapPoint)
                   if renderer.path.contains(viewPoint) {
                       polygons.append(polygon)                        
                   }
                   if polygons.count > 0 {
                       //Do stuff here like use a delegate:
                       self.mapViewTouchDelegate?.polygonsTapped(polygons: polygons)
                   }
               }
           }
       }

    super.touchesEnded(touches, with: event)
}

不要忘记将 ViewController 设置为 mapViewTouchDelegate。我还发现为 MKPolygon 创建扩展很方便:

import MapKit
extension MKPolygon {
    func contain(coor: CLLocationCoordinate2D) -> Bool {
        let polygonRenderer = MKPolygonRenderer(polygon: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coor)
        let polygonViewPoint: CGPoint = polygonRenderer.point(for: currentMapPoint)
        if polygonRenderer.path == nil {
          return false
        }else{
          return polygonRenderer.path.contains(polygonViewPoint)
        }
    }
}

那么这个函数可以变得更加简洁,这个扩展也可能在其他地方有用。而且它更符合 Swift 的风格!

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

    if let touch = touches.first {
        if touch.tapCount == 1 {
            let touchLocation = touch.location(in: self)
            let locationCoordinate = self.convert(touchLocation, toCoordinateFrom: self)            
            var polygons: [MKPolygon] = []
            for polygon in self.overlays as! [MKPolygon] {
                if polygon.contains(coordinate: locationCoordinate) {
                    polygons.append(polygon)
                }
            }
            if polygons.count > 0 {
            //Do stuff here like use a delegate:
                self.mapViewTouchDelegate?.polygonsTapped(polygons: polygons)
            }
        }
    }

    super.touchesEnded(touches, with: event)
}

这个功能很好,但是如果用户点击了一个注释,它仍然会触发,我不知道如何取消它? - David
非常有帮助!对于折线,可以扩展MKPolyline,在contains()函数中添加renderer.path.copy(strokingWithWidth: - Roselle Tanner
苹果公司关于 MKMapView 的文档指出:“虽然你不应该直接继承 MKMapView 类…” - Mojo66
@Mojo66,自我写了这个以来已经有一段时间了,你能否验证一下?我甚至没有参与这个项目,所以我无法访问代码 - 但如果是这样的话,我会更新答案,使用返回或其他语句作为那些不想让注释可点击的人的可能性。 - davidrynn

5

我找到了一种类似于 @manecosta 的解决方案,但是它使用现有的苹果 API 更容易地检测交叉点。

在视图中从触摸位置创建一个 MKMapRect。我使用 0.000005 作为经/纬度增量来表示用户的触摸。

    CGPoint tapPoint = [tap locationInView:self.mapView];
    CLLocationCoordinate2D tapCoordinate = [self.mapView convertPoint:tapPoint toCoordinateFromView:self.mapView];
    MKCoordinateRegion tapCoordinateRection = MKCoordinateRegionMake(tapCoordinate, MKCoordinateSpanMake(0.000005, 0.000005));
    MKMapRect touchMapRect = MKMapRectForCoordinateRegion(tapCoordinateRection);

搜索所有MapView覆盖层,并使用'intersectsMapRect:'函数确定您当前的覆盖层是否与上面创建的MapRect相交。

    for (id<MKOverlay> overlay in self.mapView.overlays) {
        if([overlay isKindOfClass:[MKPolyline class]]){
            MKPolyline *polygon = (MKPolyline*) overlay;
            if([polygon intersectsMapRect:touchMapRect]){
                NSLog(@"found polygon:%@",polygon);
            }
        }
    }

这也允许您创建一个漂亮的缓冲区,以便用户不必精确地命中多边形,这对于狭窄的覆盖层非常有用。 - Austin Marusco
1
抱歉 - 但在这行代码中出现了一个错误:MKMapRect touchMapRect=MKMapRectForCoordinateRegion(tapCoordinateRection); “在C99中声明函数'MKMapRectForCoordinateRegion'无效” - JMIT
@JakobPipenbringMikkelsen - 谷歌把我带到这里https://dev59.com/F2ox5IYBdhLWcg3wVS8J - anders
1
无法处理复杂几何形状,因为它只是在覆盖区域周围创建一个正方形。 - anders
1
使用缓冲区(例如您的示例中的0.000005度)的想法是使触摸更容易的好主意,但不应在地图坐标中完成。它应该在屏幕点中完成,这代表了触摸接近用户预期的一致度量。 - Nate

4

这里是我用Swift的方法:

@IBAction func revealRegionDetailsWithLongPressOnMap(sender: UILongPressGestureRecognizer) {
    if sender.state != UIGestureRecognizerState.Began { return }
    let touchLocation = sender.locationInView(protectedMapView)
    let locationCoordinate = protectedMapView.convertPoint(touchLocation, toCoordinateFromView: protectedMapView)
    //println("Taped at lat: \(locationCoordinate.latitude) long: \(locationCoordinate.longitude)")


    var point = MKMapPointForCoordinate(locationCoordinate)
    var mapRect = MKMapRectMake(point.x, point.y, 0, 0);

    for polygon in protectedMapView.overlays as! [MKPolygon] {
        if polygon.intersectsMapRect(mapRect) {
            println("found")
        }
    }
}

我最初使用了您的方法,但是我发现将locationCoordinate转换为mapRect并不能准确地工作。虽然它很接近,但对于我需要检测的点(一个城市)来说还不够接近。这只是一个提示。 - davidrynn

1

使用Apple提供的API无法确定这一点。使用MapKit,您最好能够维护一个单独的坐标多边形数据库,并记录呈现版本的顺序。然后,当用户触摸某个点时,可以在次要数据上执行空间查询,以找到相关的多边形,再结合堆叠顺序来确定他们所接触的那个。

如果多边形相对静态,则更容易完成的方法是在TileMill中创建地图覆盖层,并具有自己的交互数据。以下是包含国家互动数据的示例地图:

https://a.tiles.mapbox.com/v3/examples.map-zmy97flj/page.html

注意,在网络版本中,当鼠标悬停时会检索一些名称和图像数据。使用MapBox iOS SDK,它是一个开源的MapKit克隆版,您可以在任意手势上读取相同的数据。这里有一个展示的示例应用程序:

https://github.com/mapbox/mapbox-ios-example

那个解决方案可能适用于您的问题,与次要数据库和即时计算所触及面积相比,它非常轻巧。

2
第一句话不太准确(你可以使用内置的API来完成这个任务,只是没有那么容易)。将一个轻拍手势识别器添加到地图视图中以检测触摸坐标,然后循环遍历多边形覆盖层本身(它们充当“多边形坐标数据库”),并使用CGPathContainsPoint。请参见http://stackoverflow.com/questions/14524718/is-cgpoint-in-mkpolygonview和https://dev59.com/Y2Mk5IYBdhLWcg3wvAVo。 - user467105
谢谢你的回答。我想我会采用添加一个轻拍手势识别器的方法,然后找出被触摸的多边形。感谢Anna提供的CGPathContainsPoint,我会尝试一下。 - manecosta

0

在SWIFT 2.1中查找多边形中的点/坐标

以下是在没有触摸手势的情况下,在多边形内查找注释的逻辑。

 //create a polygon
var areaPoints = [CLLocationCoordinate2DMake(50.911864, 8.062454),CLLocationCoordinate2DMake(50.912351, 8.068247),CLLocationCoordinate2DMake(50.908536, 8.068376),CLLocationCoordinate2DMake(50.910159, 8.061552)]


func addDriveArea() {
    //add the polygon
    let polygon = MKPolygon(coordinates: &areaPoints, count: areaPoints.count)
    MapDrive.addOverlay(polygon) //starts the mapView-Function
}

  func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer! {


    if overlay is MKPolygon {

        let renderer = MKPolygonRenderer(overlay: overlay)
        renderer.strokeColor = UIColor.blueColor()
        renderer.lineWidth = 2 

        let coordinate = CLLocationCoordinate2D(latitude: CLLocationDegrees(50.917627), longitude: CLLocationDegrees(8.069562))

        let mappoint = MKMapPointForCoordinate(coordinate)
        let point = polygonView.pointForMapPoint(mappoint)
        let mapPointAsCGP = CGPointMake(point.x, point.y);

        let isInside = CGPathContainsPoint(renderer.path, nil, mapPointAsCGP, false)

        print("IsInside \(isInside)") //true = found

        return renderer
    } else {
        return nil
    }
}

1
这不是一个好的检查方式,因为此时renderer.path通常等于nil。 - Tristan Beaton

0

我考虑同时使用覆盖和图钉注释。 我从与覆盖相关联的图钉获取触摸。


0

基于@davidrynn的答案,我已经实现了更加动态和更新的结果。

Swift 5

子类化MKMapView:

public class MapView: MKMapView {

public var mapViewProtocol: MapViewProtocol?

public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

    if let touch = touches.first {

        if touch.tapCount == 1 {

            let touchLocation: CGPoint = touch.location(in: self)
            let locationCoordinate: CLLocationCoordinate2D = self.convert(touchLocation, toCoordinateFrom: self)

            var mkCircleList: [MKCircle] = self.overlays.compactMap { $0 as? MKCircle }
            mkCircleList = mkCircleList.filter { $0.contains(locationCoordinate) }
            if !mkCircleList.isEmpty {

                self.mapViewProtocol?.didTapMKCircles(self, mkCircleList)
            }

            var mkMultiPolygonList: [MKMultiPolygon] = self.overlays.compactMap { $0 as? MKMultiPolygon }
            mkMultiPolygonList = mkMultiPolygonList.filter { $0.contains(locationCoordinate) }
            if !mkMultiPolygonList.isEmpty {

                self.mapViewProtocol?.didTapMKMultiPolygons(self, mkMultiPolygonList)
            }

            var mkPolygonList: [MKPolygon] = self.overlays.compactMap { $0 as? MKPolygon }
            mkPolygonList = mkPolygonList.filter { $0.contains(locationCoordinate) }
            if !mkPolygonList.isEmpty {

                self.mapViewProtocol?.didTapMKPolygons(self, mkPolygonList)
            }

            var mkMultiPolylineList: [MKMultiPolyline] = self.overlays.compactMap { $0 as? MKMultiPolyline }
            mkMultiPolylineList = mkMultiPolylineList.filter { $0.contains(locationCoordinate) }
            if !mkMultiPolylineList.isEmpty {

                self.mapViewProtocol?.didTapMKMultiPolylines(self, mkMultiPolylineList)
            }

            var mkPolylineList: [MKPolyline] = self.overlays.compactMap { $0 as? MKPolyline }
            mkPolylineList = mkPolylineList.filter { $0.contains(locationCoordinate) }
            if !mkPolylineList.isEmpty {

                self.mapViewProtocol?.didTapMKPolylines(self, mkPolylineList)
            }

            //TODO
            //var mkTileOverlayList: [MKTileOverlay] = self.overlays.compactMap { $0 as? MKTileOverlay }
            //mkTileOverlayList = mkTileOverlayList.filter { $0.contains(locationCoordinate) }


            self.mapViewProtocol?.didTapMap(self, locationCoordinate)
        }
    }

    super.touchesEnded(touches, with: event)
}

}

接着,我为每种mkOverlay类型创建了多个扩展:

MKKCircle

import Foundation
import MapKit

extension MKCircle {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        let renderer = MKCircleRenderer(circle: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate)
        let viewPoint: CGPoint = renderer.point(for: currentMapPoint)
        if renderer.path == nil {

            return false
        } else {

            return renderer.path.contains(viewPoint)
        }
    }
}

MKMultiPolygon

import Foundation
import MapKit

@available(iOS 13.0, *)
extension MKMultiPolygon {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        return self.polygons.filter { $0.contains(coordinate2D) }.isEmpty ? false : true
    }
}

MKMultiPolyline

    import Foundation
import MapKit

@available(iOS 13.0, *)
extension MKMultiPolyline {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        return self.polylines.filter { $0.contains(coordinate2D) }.isEmpty ? false : true
    }
}

MKPolygon

import Foundation
import MapKit

extension MKPolygon {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        let renderer = MKPolygonRenderer(polygon: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate2D)
        let viewPoint: CGPoint = renderer.point(for: currentMapPoint)
        if renderer.path == nil {

            return false
        } else {

            return renderer.path.contains(viewPoint)
        }
    }
}

MKPolyline

import Foundation
import MapKit

extension MKPolyline {

    func contains(_ coordinate2D: CLLocationCoordinate2D) -> Bool {

        let renderer = MKPolylineRenderer(polyline: self)
        let currentMapPoint: MKMapPoint = MKMapPoint(coordinate2D)
        let viewPoint: CGPoint = renderer.point(for: currentMapPoint)
        if renderer.path == nil {

            return false
        } else {

            return renderer.path.contains(viewPoint)
        }
    }
}

最后创建并实现协议:

    public protocol MapViewProtocol {

    func didTapMKPolygons(_ mapView: MKMapView, _ mkPolygons: [MKPolygon])

    func didTapMKCircles(_ mapView: MKMapView, _ mkCircles: [MKCircle])

    func didTapMKPolylines(_ mapView: MKMapView, _ mkPolylines: [MKPolyline])

    func didTapMKMultiPolygons(_ mapView: MKMapView, _ mkMultiPolygons: [MKMultiPolygon])

    func didTapMKMultiPolylines(_ mapView: MKMapView, _ mkMultiPolylines: [MKMultiPolyline])

    func didTapMap(_ mapView: MKMapView, _ clLocationCoordinate2D: CLLocationCoordinate2D)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接