how can I detect if singleTap was tapped or annotation on mapView of mapbox?
Asked Answered
T

8

10

Requirment on iOS for mapBox map. (I am not talking about MKMapView) how can we detect if singleTap was tapped or annotation on mapView? I need that singleTap will be handled only on empty area of map (without pins), and didSelectAnnotation called when i tap on pin.

But I found on android we have method like this

mapboxMap.setOnMapClickListener(new MapboxMap.OnMapClickListener() {
            public void onMapClick(@NonNull LatLng point) {
                Toast.makeText(getActivity(),"on Tap "+point.getLatitude(),Toast.LENGTH_LONG).show();
            }
        });

and along with that

mapboxMap.setInfoWindowAdapter(new MapboxMap.InfoWindowAdapter() { ... }) will display the annotation.

Don't we have same kind of concept in iOS ?

The actual problem is in iOS is, when I add singleTapGesture on mapView of Mapbox

UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
[self.mapView addGestureRecognizer:singleTapGesture];

the delegate method of mapView of mapbox will not call.

- (nullable UIView <MGLCalloutView> *)mapView:(MGLMapView *)mapView calloutViewForAnnotation:(id <MGLAnnotation>)annotation;

to make sure the delegate method must call, then I dont have to use singleTapGesture

Here the situation is either this or that, but as per me need I needed both.

Looking forward of any solution. Thanks,

Traitorous answered 28/6, 2016 at 7:15 Comment(1)
INasir show me your full codingJell
T
2

I made another workaround here

I cloned the release-ios-v3.3.0 and created package using Building the SDK and added one delegate method in MGLMapvViewDelegate.h as per my need something like that. -(void)mapView:(MGLMapView *)mapView tapOnNonAnnotationAreaWithPoints:(CGPoint)points

and in MGLMapView.mm I updated the code like that,

else{
if(self.selectedAnnotation)
      [self deselectAnnotation:self.selectedAnnotation animated:YES];
else if([self.delegate respondsToSelector:@selector(mapView:tapOnNonAnnotationAreaWithPoints:)])
      [self.delegate mapView:self tapOnNonAnnotationAreaWithPoints:tapPoint];
}

which is in -(void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap method.

Its working for me, as I am able to detect single tap on non annotation area. later I convert the passed points to geo coordinates, so that I work with newly tapped coordinate.

Note

  1. Newly Created library size is about 48.7 MB where as downloaded library was 38.3 MB.
  2. Style is not working properly, you can see in attached pic.enter image description hereenter image description here (Tried with different Style URL but none of it is same as previous)
  3. I feel lack in App performance, Zooming and Panning is not smooth as the dynamic library gave by Mapbox guys.

I am still exploring. lemme know if you wanna me to check/explore.

Please accept this answer, if it is helpful.

Thank you,

Happy Coding.

Traitorous answered 9/8, 2016 at 9:50 Comment(0)
C
7

Subclass MGLMapView and delegate its' touchesEnded.

protocol MapViewTapDelegate: class {

    func mapViewTapped(withTouchLocation touchLocation: CLLocationCoordinate2D)
}

class MapView: MGLMapView {

    weak var tapDelegate: MapViewTapDelegate?

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

        guard let touch = touches.first else { return }

        let touchLocation = convert(touch.location(in: self), toCoordinateFrom: nil)

        tapDelegate?.mapViewTapped(withTouchLocation: touchLocation)
    }

}

As touchesEnded isn't triggered when didSelect annotation is called and vice versa, this is what you need.

class ViewController: UIViewController {
    @IBOutlet weak var mapView: MapView! {

        didSet {

            mapView.delegate = self; mapView.tapDelegate = self
        }
    }

}

extension ViewController: MGLMapViewDelegate {

    func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {

        print("didSelect annotationWithCoordinate: \(annotation.coordinate)")
    }

}

extension ViewController: MapViewTapDelegate {

    func mapViewTapped(withTouchLocation touchLocation: CLLocationCoordinate2D) {

        print("mapViewTapped touchLocation: \(touchLocation)")
    }

}
Creamcolored answered 1/6, 2018 at 20:21 Comment(1)
There is an error on this setup when "mapView.tapDelegate = self" Cannot assign value of type 'UIViewController' to type 'MapViewTapDelegate?'Gwenngwenneth
C
2

I think that the method -(void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id<MGLAnnotation>)annotation will solve your problem for selecting annotation.

Conall answered 28/6, 2016 at 7:54 Comment(1)
-(void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id<MGLAnnotation>)annotation will work when callout tapped only, but when I add tapGesture the on mapView none of delegate method works. In my case I need both tapGesture and delegate callsTraitorous
T
2

I made another workaround here

I cloned the release-ios-v3.3.0 and created package using Building the SDK and added one delegate method in MGLMapvViewDelegate.h as per my need something like that. -(void)mapView:(MGLMapView *)mapView tapOnNonAnnotationAreaWithPoints:(CGPoint)points

and in MGLMapView.mm I updated the code like that,

else{
if(self.selectedAnnotation)
      [self deselectAnnotation:self.selectedAnnotation animated:YES];
else if([self.delegate respondsToSelector:@selector(mapView:tapOnNonAnnotationAreaWithPoints:)])
      [self.delegate mapView:self tapOnNonAnnotationAreaWithPoints:tapPoint];
}

which is in -(void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap method.

Its working for me, as I am able to detect single tap on non annotation area. later I convert the passed points to geo coordinates, so that I work with newly tapped coordinate.

Note

  1. Newly Created library size is about 48.7 MB where as downloaded library was 38.3 MB.
  2. Style is not working properly, you can see in attached pic.enter image description hereenter image description here (Tried with different Style URL but none of it is same as previous)
  3. I feel lack in App performance, Zooming and Panning is not smooth as the dynamic library gave by Mapbox guys.

I am still exploring. lemme know if you wanna me to check/explore.

Please accept this answer, if it is helpful.

Thank you,

Happy Coding.

Traitorous answered 9/8, 2016 at 9:50 Comment(0)
L
1

Try swift 3

//determined is Point inner to realm polygons
func determinedInPoligon(point:CLLocationCoordinate2D, poligon:[CLLocationCoordinate2D]) -> Bool {

    var result:Bool = false
    var j = poligon.count - 1

    for i in 0 ..< poligon.count {

        if  (poligon[i].longitude < point.longitude && poligon[j].longitude >= point.longitude || poligon[j].longitude < point.longitude && poligon[i].longitude >= point.longitude) &&
            (poligon[i].latitude + (point.longitude - poligon[i].longitude) / (poligon[j].longitude - poligon[i].longitude) * (poligon[j].latitude - poligon[i].latitude) < point.latitude) {
            result = !result;
        }
        j = i;

    }
    return result
}

func squareFrom(location: CGPoint, radius: Double) -> CGRect {//return rect with CGPoint center and radius
    let length = radius
    return CGRect(x: Double(location.x - CGFloat(length / 2)), y: 
   Double(location.y - CGFloat(length / 2)), width: length, height: length)
} 

function handle

func handleTap(_ gesture: UITapGestureRecognizer) {

    // Get the CGPoint where the user tapped.
    let spot = gesture.location(in: mapView)

    let my_features = mapView.visibleFeatures(at: spot)
    let strZoomValue = mapView.zoomLevel > 15 ? "6" : "4"
    //the feature structur object value not equal annotation object
    for value in my_features.enumerated() {// feature items
        if value.element is MGLPointAnnotation  {

            for annot in (mapView.annotations?.enumerated())! { // annotation items
                if annot.element is MGLPointAnnotation {

                    //rounded lat and lng value
                    var arr_cllocation = [CLLocationCoordinate2D]()
                    for cllocation in [(annot.element as! MGLPointAnnotation).coordinate, (value.element as! MGLPointAnnotation).coordinate] {

                        let strLat = String(format: "%."+strZoomValue+"f", cllocation.latitude)
                        let strLon = String(format: "%."+strZoomValue+"f", cllocation.longitude)
                        arr_cllocation.append(
                            CLLocationCoordinate2D(latitude: CLLocationDegrees(strLat)!, longitude: CLLocationDegrees(strLon)!) )
                    }

                    if arr_cllocation.count == 2 &&
                        memcmp(&arr_cllocation[0], &arr_cllocation[1], MemoryLayout<CLLocationCoordinate2D>.size) == 0 {// 0 if equal object
                        // to do custom popup view
                        let instViewPopupLineClass = UINib(nibName: "ViewPopupBase", bundle: nil).instantiate(withOwner: self, options: nil).first as! UIView

                        for objectInst in instViewPopupLineClass.subviews.enumerated() {

                            if objectInst.element is UILabel {
                                let asdasdas = (annot.element as! MGLPointAnnotation).subtitle
                                (objectInst.element as! UILabel).text = asdasdas
                                MyCustomPopup(customView: instViewPopupLineClass, positionXY: spot)
                                break
                            }
                        }
                    }
                }
            } //for end

        }
    }
}

OR not accurate method, but a worker ;)

func handleTap(_ gesture: UITapGestureRecognizer) {// Get the CGPoint where the user tapped.

    let spot = gesture.location(in: mapView)
    let cllcordinatTap = mapView.convert(spot, toCoordinateFrom: mapView)
                 //for determined zoom scala
            let  deltScalaMap = abs(self.mapView.maximumZoomLevel - self.mapView.zoomLevel) 
           //The large is zoom maps, then smal size is tap location, and vice versa.
            let checkScalaMap = deltScalaMap == 0 ? 1 : deltScalaMap
            let _rect = squareFrom(location: CGPoint(x: cllcordinatTap.latitude, y: cllcordinatTap.longitude), radius: 0.00005 * checkScalaMap)
  for annot in (mapView.annotations?.enumerated())! {

       if annot.element is MGLPointAnnotation {
          let _cordinatCurrentAnnotation = (annot.element as! MGLPointAnnotation).coordinate

          if  determinedInPoligon(point: _cordinatCurrentAnnotation, poligon:
              [CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.minX), longitude: CLLocationDegrees(_rect.minY)),
               CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.maxX), longitude: CLLocationDegrees(_rect.minY)),
               CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.maxX), longitude: CLLocationDegrees(_rect.maxY)),
               CLLocationCoordinate2D(latitude: CLLocationDegrees(_rect.minX), longitude: CLLocationDegrees(_rect.maxY))
          ]) {

              // to do, if tap MGLPointAnnotation, annot.element

          }
      }
  }
}
Likable answered 7/6, 2017 at 6:57 Comment(0)
T
0

Here I got some workaround which helped to work with my requirement. As per my need I am able to get single tap detection on both mapbox annotation marker and as well as on empty area of map

I created category for MGLMapView, (MGLMapView+EDCMapboxView) and overrides the touch methods

-touchesBegan:withEvent:
-touchesMoved:withEvent:
-touchesEnded:withEvent:
-touchesCancelled:withEvent:

MGLMapView+EDCMapboxView.h

@protocol EDCMapboxViewDelegate <NSObject>
@optional
- (void)mapboxViewDidCreateNewTicket:(MGLMapView*)mapView;
@end

@interface MGLMapView (EDCMapboxView)

@property (assign, nonatomic) BOOL shouldCreateNewTicket;

@property (weak, nonatomic) id <EDCMapboxViewDelegate> mapboxDelegate;
@end

MGLMapView+EDCMapboxView.m

@implementation MGLMapView (EDCMapboxView)
@dynamic mapboxDelegate;

#pragma mark -- Accessor
- (BOOL)shouldCreateNewTicket {
    return [objc_getAssociatedObject(self, @selector(shouldCreateNewTicket)) boolValue];
}
- (void)setShouldCreateNewTicket:(BOOL)flag {
    objc_setAssociatedObject(self, @selector(shouldCreateNewTicket), @(flag), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(id<EDCMapboxViewDelegate>)mapboxDelegate{
    return objc_getAssociatedObject(self, @selector(mapboxDelegate));
}
- (void)setMapboxDelegate:(id<EDCMapboxViewDelegate>)mapboxDelegate{
    objc_setAssociatedObject(self, @selector(mapboxDelegate), mapboxDelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark -- Overrided method for UIResponder
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
    NSLog(@"touchesBegan");
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
    NSLog(@"touchesMoved");
    self.shouldCreateNewTicket = NO;
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
    NSLog(@"touchesEnded");
}

- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event{
    NSLog(@"touchesCancelled");

    [self createNewTicket];
}

- (void)createNewTicket{
    if(self.shouldCreateNewTicket){
        NSLog(@"Allowed to Create New ticket");
        // Tells that tap is on empty area.
        if([self.mapboxDelegate respondsToSelector:@selector(mapboxViewDidCreateNewTicket:)]){
            [self.mapboxDelegate mapboxViewDidCreateNewTicket:self];
        }
    }
    else{
        NSLog(@"Not allowed to create New ticket");
        // Tells tap is on annotation marker.
        self.shouldCreateNewTicket = YES;
    }
}

EDCMapboxViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    self.mapView.shouldCreateNewTicket = YES;   
    .....
    .......
    ........
}

- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation {
    NSLog(@"annotationCanShowCallout");

    // Tells that annotation is tapped, then do not allow to create ticket.
    self.mapView.shouldCreateNewTicket = NO;

    return YES;
}

- (void)mapboxViewDidCreateNewTicket:(MGLMapView*)mapView{
   // Tells that tap is on empty area and not on marker, then allow to create ticket. 
}

It worked for me, hope it will help you guys as well. Thanks.

Traitorous answered 11/7, 2016 at 10:32 Comment(2)
Here shouldCreateNewTicket flag will detect difference between tap on empty area and tap on marker.Traitorous
Make sure that you must not end up by creating retain cycle. Update MGLMapView+EDCMapboxView.m category with OBJC_ASSOCIATION_ASSIGN instead of OBJC_ASSOCIATION_RETAIN_NONATOMIC Traitorous
E
0

Here is how I solved it, you should get the concept.

func onMapSingleTapped(recognizer: UITapGestureRecognizer)
{
    let viewLocation: CGPoint = recognizer.locationInView(map)

    // check if any annotations are hit
    if(map.annotations != nil)
    {
        for annotation in map.annotations!
        {
            if(annotation.isKindOfClass(MapCheckInAnnotation))
            {
                let pin: MapCheckInAnnotation = annotation as! MapCheckInAnnotation

                if let pinView = pin.view
                {
                    print("pinview \(pinView.frame.origin)")

                    // check if hit pin instead of just map
                    if(viewLocation.x >= pinView.frame.origin.x && viewLocation.x < pinView.frame.origin.x + pinView.frame.width)
                    {
                        if(viewLocation.y >= pinView.frame.origin.y && viewLocation.y < pinView.frame.origin.y + pinView.frame.height)
                        {
                            mapView(map, didSelectAnnotationView: pinView)
                            return
                        }
                    }
                }
            }
        }
    }

    // nope, no annotations were clicked
    let mapLocation: CLLocationCoordinate2D = map.convertPoint(viewLocation, toCoordinateFromView: map)

    print("onMapSingleTapped \(mapLocation)")

}
Eisenberg answered 16/8, 2016 at 22:12 Comment(0)
J
-1

I tried sample project for your question and it works well.

.h

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
@interface ViewController : UIViewController<MKMapViewDelegate>
@property (strong, nonatomic) IBOutlet MKMapView *mapTapAnnotation;
@end

.m

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController


@synthesize mapTapAnnotation;

- (void)viewDidLoad {
 [super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
  [self setMapViewWithAnnnoationPin];
}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

-(void)setMapViewWithAnnnoationPin
{

  mapTapAnnotation.showsUserLocation = YES;
  mapTapAnnotation.mapType = MKMapTypeHybrid;

  CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(12.900988, 80.227930);

  MKCoordinateSpan span = MKCoordinateSpanMake(0.005,  0.005);
  MKCoordinateRegion region = {coord, span};

  MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
  [annotation setCoordinate:coord];
  [annotation setTitle:@"Single Tap"]; //You can set the subtitle too


  [mapTapAnnotation setRegion:region];
  [mapTapAnnotation addAnnotation:annotation];


  UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(detectSingleTap)];
  tapGesture.numberOfTapsRequired = 1;
  [mapTapAnnotation addGestureRecognizer:tapGesture];
}

-(void)detectSingleTap
{
   NSLog(@"Finded the single tap on map view");
}


 - (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
    NSLog(@"The single tap annotation pin point is - %ld",(long)view.tag);
}

The printed output results are

Finded the single tap on map view
The single tap annotation pin point is - 0
Jell answered 28/6, 2016 at 9:29 Comment(3)
Thank you, I appreciate your help, But in my case MKMapView is not used, its MGLMapView (mapbox)Traitorous
Upload your full codingJell
I have uploaded the files here jmp.sh/hUUviDX . It also consist of code of data fetching from CBL, Please ignore that, let me know anything more detail you need. Thank You.Traitorous
T
-1

As I haven't been successful with the solutions above I implemented a small delay after the always recognized map tap event. If an annotation is selected in this small time frame, then nothing is called any further. Otherwise only the map view was tapped and no annotation. Works pretty well in my case.

MapView part

 protocol MapDelegate: class { 
     func didTapMapView() 
 }

 class AppleMapView: MKMapView {

     weak var mapDelegate: MapDelegate?

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

             if touches.first != nil {
                 mapDelegate?.didTapMapView()
             }
        }
        ........
}

MapViewController part

// property to store last tap at annotation
var lastAnnotationSelectionTime = Date()

// this method is called on every tap 
func didTapMapView() {
    let overlaySelectionDelayTime: TimeInterval = 0.2
    DispatchQueue.main.asyncAfter(deadline: .now() + overlaySelectionDelayTime, execute: { [weak self] in

         guard let self = self else { return }

         if self.lastAnnotationSelectionTime.distance(to: Date()) > overlaySelectionDelayTime {
                 // only called if no annotation was tapped meanwhile
                 [ HANDLE TAP ON MAP HERE ]
            }
         })
   }

// MARK: Marker Selection
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
     lastAnnotationSelectionTime = Date() // setten if annotation is tapped
     [ HANDLE TAP ON ANNOTATION HERE ]
}
Tessietessier answered 7/5, 2020 at 13:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.