Обнаружение касаний на MKOverlay в iOS7 (MKOverlayRenderer)
У меня есть MKMapView с сотнями нарисованных полигонов. Использование MKPolygon и MKPolygonRenderer, как предполагается на iOS7.
Что мне нужно, так это способ воздействия на пользователя, касающегося одного из полигонов. Например, они представляют область на карте с определенной плотностью населения. На iOS6 MKOverlay были нарисованы как MKOverlayViews, так что обнаружение касания было более простым. Теперь, используя рендереры, я не понимаю, как это можно сделать.
Я не уверен, что это поможет или даже уместно, но в качестве ссылки я выложу некоторый код:
Это добавляет все MKOverlays к MKMapView, используя mapData.
-(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
}
}
Затем есть метод делегата для возврата MKOverlayRenderer для каждого MKOverlay:
-(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;
/* ... */
}
7 ответов
Я сделал это.
Спасибо Анкану и Анне!
По сути, я добавляю TapGestureRecognizer в MapView, преобразовываю точку, повернутую в координаты карты, прохожу мои наложения и проверяю с помощью 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, и использовать его, но по какой-то причине оно всегда равно нулю. Я читал, что кто-то сказал, что я могу вызвать invalidatePath в рендере, и он заполняет свойство пути, но это просто кажется неправильным, так как точка не найдена ни в одном из полигонов. Вот почему я перестраиваю путь из точек. Таким образом, я даже не нуждаюсь в рендерере и просто использую объект MKPolygon.
ОБНОВЛЕНО(для Swift 3 и 4) Я не уверен, почему люди добавляют UIGestureRecognizer в mapView, когда в mapView уже работает несколько распознавателей жестов. Я обнаружил, что эти методы препятствуют нормальной функциональности mapView, в частности, нажав на аннотацию. Вместо этого я бы рекомендовал создать подкласс mapView и переопределить метод touchesEnded. Затем мы можем использовать методы, которые другие предложили в этом потоке, и использовать метод делегата, чтобы сказать ViewController делать все, что ему нужно. Параметр "touch" содержит набор объектов 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 contains(coordinate: CLLocationCoordinate2D) -> Bool {
let renderer = MKPolygonRenderer(polygon: self)
let mapPoint = MKMapPointForCoordinate(coordinate)
let viewPoint = renderer.point(for: mapPoint)
return renderer.path.contains(viewPoint)
}
}
Тогда функция может быть немного чище, а расширение может быть полезным в другом месте. Плюс это быстро!
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)
}
Я нашел решение, похожее на @manecosta, но оно использует существующие API Apple для более удобного обнаружения пересечения.
Создайте 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);
}
}
}
Вот мой путь в 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")
}
}
}
Вы не сможете определить это, используя API, предоставляемые Apple. Лучшее, что вы можете сделать с MapKit, - это поддерживать отдельную базу данных всех ваших координат многоугольника, а также порядок, в котором рендеринг версий складывается. Затем, когда пользователь касается точки, вы можете выполнить пространственный запрос к вторичным данным, чтобы найти рассматриваемый полигон (ы) в сочетании с порядком наложения, чтобы определить, к какому из них он прикоснулся.
Более простой способ сделать это, если полигоны относительно статичны, - создать наложение карты в TileMill с собственными данными интерактивности. Вот пример карты, которая содержит данные об интерактивности для стран:
https://a.tiles.mapbox.com/v3/examples.map-zmy97flj/page.html
Обратите внимание, как некоторые данные об имени и изображении извлекаются при наведении в веб-версии. Используя MapBox iOS SDK, который является клоном MapKit с открытым исходным кодом, вы можете считывать эти же данные произвольными жестами. Пример приложения, показывающего это, здесь:
https://github.com/mapbox/mapbox-ios-example
Это решение может работать для вашей проблемы и является довольно легковесным по сравнению со вторичной базой данных и точным расчетом площади, к которой обращаются.
Основываясь на ответе @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
}
}
МКПолигон
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)
}
ДЛЯ 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
}
}
Я рассматриваю возможность использования как оверлея, так и пин-аннотации. Я получаю прикосновение от булавки, связанной с наложением.