Detectando toques en MKOverlay en iOS7(MKOverlayRenderer)
touch mkmapview (7)
Tengo un MKMapView con posiblemente cientos de polígonos dibujados. El uso de MKPolygon y MKPolygonRenderer como uno se supone en iOS7.
Lo que necesito es una forma de actuar sobre el usuario tocando uno de los polígonos. Representan un área en el mapa con una cierta densidad de población, por ejemplo. En iOS6, los MKOverlays se dibujaron como MKOverlayViews, por lo que la detección táctil fue más sencilla. Ahora, usando renderizadores realmente no veo cómo se supone que se haga esto.
No estoy seguro de que esto ayude o incluso sea relevante, pero como referencia, publicaré un código:
Esto agrega todos los MKOverlays al MKMapView usando 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
}
}
Luego está el método delegado para devolver un MKOverlayRenderer para cada 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;
/* ... */
}
Aquí está mi camino en 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")
}
}
}
Estoy considerando usar tanto la superposición como la anotación de pin. Recibo el toque del pin asociado a la superposición.
He encontrado una solución similar a @manecosta, pero utiliza las API de Apple existentes para detectar la intersección más fácilmente.
Cree un MKMapRect desde la ubicación del grifo en la Vista. Utilicé 0.000005 como delta lat / long para representar el toque de un usuario.
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);
Busque en todas las superposiciones de MapView y use la función ''intersectsMapRect:'' para determinar si su superposición actual se interseca con el MapRect que creó anteriormente.
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);
}
}
}
Lo he hecho.
Gracias a incanus y Anna !
Básicamente, agrego un TapGestureRecognizer al MapView, convierto el punto tocado para coordinar las coordenadas, reviso mis superposiciones y verifico con CGPathContainsPoint.
Añadiendo TapGestureRecognizer. Hice ese truco de agregar un segundo gesto de doble toque, de modo que el gesto de un solo toque no se dispare al hacer un doble toque para hacer zoom en el mapa. Si alguien sabe una mejor manera, me alegro de escuchar!
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
Luego, en el controlador de grifo:
-(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);
}
}
}
Podría pedir el MKPolygonRenderer que ya tiene la propiedad "ruta" y usarlo, pero por alguna razón siempre es nulo. Leí a alguien diciendo que podría llamar a invalidatePath en el renderizador y llena la propiedad de ruta, pero parece que está mal ya que el punto nunca se encuentra dentro de ninguno de los polígonos. Es por eso que reconstruyo el camino desde los puntos. De esta manera, ni siquiera necesito el renderizador y solo uso el objeto MKPolygon.
No vas a poder determinar esto usando las API que proporciona Apple. Lo mejor que podría hacer con MapKit sería mantener una base de datos separada de todas sus coordenadas de polígono , así como el orden en que se apilan las versiones renderizadas. Luego, cuando el usuario toca un punto, puede hacer una consulta espacial en sus datos secundarios para encontrar el (los) polígono (s) en cuestión combinado con el orden de apilamiento para determinar cuál de ellos tocó.
Una forma más fácil de hacer esto si los polígonos son relativamente estáticos sería crear una superposición de mapas en TileMill con sus propios datos de interactividad. Aquí hay un mapa de ejemplo que contiene datos de interactividad para países:
https://a.tiles.mapbox.com/v3/examples.map-zmy97flj/page.html
Observe cómo algunos datos de nombre e imagen se recuperan cuando se pasa el ratón en la versión web. Usando el SDK de MapBox para iOS , que es un clon de código abierto de MapKit, puede leer los mismos datos en gestos arbitrarios. Una aplicación de ejemplo que muestra esto está aquí:
https://github.com/mapbox/mapbox-ios-example
Esa solución podría funcionar para su problema y es bastante liviana en comparación con una base de datos secundaria y un cálculo justo a tiempo del área tocada.
ACTUALIZADO (para Swift 3 y 4 ) No estoy seguro de por qué la gente está agregando un UIGestureRecognizer a mapView cuando mapView ya tiene varios reconocedores de gestos en ejecución. Descubrí que estos métodos inhiben la funcionalidad normal de mapView, en particular, al tocar una anotación. En su lugar, recomiendo subclasificar el mapView y anular el método touchesEnded. Luego podemos usar los métodos que otros han sugerido en este hilo y usar un método de delegado para decirle a ViewController que haga lo que sea necesario. El parámetro "toques" tiene un conjunto de objetos UITouch que podemos usar:
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)
}
No te olvides de configurar el ViewController como mapViewTouchDelegate. También me pareció útil hacer una extensión para 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)
}
}
Luego, la función puede ser un poco más limpia y la extensión puede ser útil en otro lugar. ¡Además es más rápido!
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)
}
PARA SWIFT 2.1 Encontrar un punto / coordenada en un polígono
Aquí está la lógica, sin gestos de toque, para encontrar una anotación dentro de un polígono.
//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
}
}