1/**
2 * Copyright (c) 2015-present, Facebook, Inc.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree. An additional grant
7 * of patent rights can be found in the PATENTS file in the same directory.
8 */
9
10#import "AIRMapManager.h"
11
12#import <React/RCTBridge.h>
13#import <React/RCTUIManager.h>
14#import <React/RCTConvert.h>
15#import <React/RCTConvert+CoreLocation.h>
16#import <React/RCTEventDispatcher.h>
17#import <React/RCTViewManager.h>
18#import <React/UIView+React.h>
19#import "AIRMap.h"
20#import "AIRMapMarker.h"
21#import "AIRMapPolyline.h"
22#import "AIRMapPolygon.h"
23#import "AIRMapCircle.h"
24#import "SMCalloutView.h"
25#import "AIRMapUrlTile.h"
26#import "AIRMapWMSTile.h"
27#import "AIRMapLocalTile.h"
28#import "AIRMapSnapshot.h"
29#import "RCTConvert+AirMap.h"
30#import "AIRMapOverlay.h"
31#import <MapKit/MapKit.h>
32
33static NSString *const RCTMapViewKey = @"MapView";
34
35
36@interface AIRMapManager() <MKMapViewDelegate, UIGestureRecognizerDelegate>
37
38- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
39
40@end
41
42@implementation AIRMapManager{
43   BOOL _hasObserver;
44}
45
46RCT_EXPORT_MODULE()
47
48- (UIView *)view
49{
50    AIRMap *map = [AIRMap new];
51    map.delegate = self;
52
53    map.isAccessibilityElement = NO;
54    map.accessibilityElementsHidden = NO;
55
56    // MKMapView doesn't report tap events, so we attach gesture recognizers to it
57    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapTap:)];
58    UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapDoubleTap:)];
59    [doubleTap setNumberOfTapsRequired:2];
60    [tap requireGestureRecognizerToFail:doubleTap];
61
62    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapLongPress:)];
63    UIPanGestureRecognizer *drag = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapDrag:)];
64    [drag setMinimumNumberOfTouches:1];
65    // setting this to NO allows the parent MapView to continue receiving marker selection events
66    tap.cancelsTouchesInView = NO;
67    doubleTap.cancelsTouchesInView = NO;
68    longPress.cancelsTouchesInView = NO;
69
70    doubleTap.delegate = self;
71
72    // disable drag by default
73    drag.enabled = NO;
74    drag.delegate = self;
75
76    [map addGestureRecognizer:tap];
77    [map addGestureRecognizer:doubleTap];
78    [map addGestureRecognizer:longPress];
79    [map addGestureRecognizer:drag];
80
81    return map;
82}
83
84RCT_EXPORT_VIEW_PROPERTY(isAccessibilityElement, BOOL)
85RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString)
86RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL)
87RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
88RCT_EXPORT_VIEW_PROPERTY(userLocationAnnotationTitle, NSString)
89RCT_EXPORT_VIEW_PROPERTY(userInterfaceStyle, NSString)
90RCT_EXPORT_VIEW_PROPERTY(followsUserLocation, BOOL)
91RCT_EXPORT_VIEW_PROPERTY(userLocationCalloutEnabled, BOOL)
92RCT_EXPORT_VIEW_PROPERTY(showsPointsOfInterest, BOOL)
93RCT_EXPORT_VIEW_PROPERTY(showsBuildings, BOOL)
94RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL)
95RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL)
96RCT_EXPORT_VIEW_PROPERTY(showsTraffic, BOOL)
97RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
98RCT_EXPORT_VIEW_PROPERTY(kmlSrc, NSString)
99RCT_EXPORT_VIEW_PROPERTY(rotateEnabled, BOOL)
100RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL)
101RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)
102RCT_EXPORT_VIEW_PROPERTY(cacheEnabled, BOOL)
103RCT_EXPORT_VIEW_PROPERTY(loadingEnabled, BOOL)
104RCT_EXPORT_VIEW_PROPERTY(loadingBackgroundColor, UIColor)
105RCT_EXPORT_VIEW_PROPERTY(loadingIndicatorColor, UIColor)
106RCT_EXPORT_VIEW_PROPERTY(handlePanDrag, BOOL)
107RCT_EXPORT_VIEW_PROPERTY(maxDelta, CGFloat)
108RCT_EXPORT_VIEW_PROPERTY(minDelta, CGFloat)
109RCT_EXPORT_VIEW_PROPERTY(compassOffset, CGPoint)
110RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets)
111RCT_EXPORT_VIEW_PROPERTY(mapPadding, UIEdgeInsets)
112RCT_EXPORT_VIEW_PROPERTY(mapType, MKMapType)
113RCT_EXPORT_VIEW_PROPERTY(onMapReady, RCTBubblingEventBlock)
114RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
115RCT_EXPORT_VIEW_PROPERTY(onPanDrag, RCTBubblingEventBlock)
116RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
117RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTBubblingEventBlock)
118RCT_EXPORT_VIEW_PROPERTY(onDoublePress, RCTBubblingEventBlock)
119RCT_EXPORT_VIEW_PROPERTY(onMarkerPress, RCTDirectEventBlock)
120RCT_EXPORT_VIEW_PROPERTY(onMarkerSelect, RCTDirectEventBlock)
121RCT_EXPORT_VIEW_PROPERTY(onMarkerDeselect, RCTDirectEventBlock)
122RCT_EXPORT_VIEW_PROPERTY(onMarkerDragStart, RCTDirectEventBlock)
123RCT_EXPORT_VIEW_PROPERTY(onMarkerDrag, RCTDirectEventBlock)
124RCT_EXPORT_VIEW_PROPERTY(onMarkerDragEnd, RCTDirectEventBlock)
125RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock)
126RCT_EXPORT_VIEW_PROPERTY(onUserLocationChange, RCTBubblingEventBlock)
127RCT_CUSTOM_VIEW_PROPERTY(initialRegion, MKCoordinateRegion, AIRMap)
128{
129    if (json == nil) return;
130
131    // don't emit region change events when we are setting the initialRegion
132    BOOL originalIgnore = view.ignoreRegionChanges;
133    view.ignoreRegionChanges = YES;
134    [view setInitialRegion:[RCTConvert MKCoordinateRegion:json]];
135    view.ignoreRegionChanges = originalIgnore;
136}
137RCT_CUSTOM_VIEW_PROPERTY(initialCamera, MKMapCamera, AIRMap)
138{
139    if (json == nil) return;
140
141    // don't emit region change events when we are setting the initialCamera
142    BOOL originalIgnore = view.ignoreRegionChanges;
143    view.ignoreRegionChanges = YES;
144    [view setInitialCamera:[RCTConvert MKMapCamera:json]];
145    view.ignoreRegionChanges = originalIgnore;
146}
147
148
149RCT_EXPORT_VIEW_PROPERTY(minZoomLevel, CGFloat)
150RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat)
151
152
153RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, AIRMap)
154{
155    if (json == nil) return;
156
157    // don't emit region change events when we are setting the region
158    BOOL originalIgnore = view.ignoreRegionChanges;
159    view.ignoreRegionChanges = YES;
160    [view setRegion:[RCTConvert MKCoordinateRegion:json] animated:NO];
161    view.ignoreRegionChanges = originalIgnore;
162}
163
164RCT_CUSTOM_VIEW_PROPERTY(camera, MKMapCamera*, AIRMap)
165{
166    if (json == nil) return;
167
168    // don't emit region change events when we are setting the camera
169    BOOL originalIgnore = view.ignoreRegionChanges;
170    view.ignoreRegionChanges = YES;
171    [view setCamera:[RCTConvert MKMapCamera:json] animated:NO];
172    view.ignoreRegionChanges = originalIgnore;
173}
174
175
176#pragma mark exported MapView methods
177
178RCT_EXPORT_METHOD(getMapBoundaries:(nonnull NSNumber *)reactTag
179                  resolver:(RCTPromiseResolveBlock)resolve
180                  rejecter:(RCTPromiseRejectBlock)reject)
181{
182    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
183        id view = viewRegistry[reactTag];
184        if (![view isKindOfClass:[AIRMap class]]) {
185            RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
186        } else {
187            NSArray *boundingBox = [view getMapBoundaries];
188
189            resolve(@{
190                @"northEast" : @{
191                    @"longitude" : boundingBox[0][0],
192                    @"latitude" : boundingBox[0][1]
193                },
194                @"southWest" : @{
195                    @"longitude" : boundingBox[1][0],
196                    @"latitude" : boundingBox[1][1]
197                }
198            });
199        }
200    }];
201}
202
203
204
205RCT_EXPORT_METHOD(getCamera:(nonnull NSNumber *)reactTag
206                  resolver: (RCTPromiseResolveBlock)resolve
207                  rejecter:(RCTPromiseRejectBlock)reject)
208{
209    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
210        id view = viewRegistry[reactTag];
211        AIRMap *mapView = (AIRMap *)view;
212        if (![view isKindOfClass:[AIRMap class]]) {
213            reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL);
214        } else {
215            MKMapCamera *camera = [mapView camera];
216            resolve(@{
217                      @"center": @{
218                              @"latitude": @(camera.centerCoordinate.latitude),
219                              @"longitude": @(camera.centerCoordinate.longitude),
220                      },
221                      @"pitch": @(camera.pitch),
222                      @"heading": @(camera.heading),
223                      @"altitude": @(camera.altitude),
224            });
225        }
226    }];
227}
228
229
230RCT_EXPORT_METHOD(setCamera:(nonnull NSNumber *)reactTag
231                  camera:(id)json)
232{
233    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
234        id view = viewRegistry[reactTag];
235        if (![view isKindOfClass:[AIRMap class]]) {
236            RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
237        } else {
238            AIRMap *mapView = (AIRMap *)view;
239
240            // Merge the changes given with the current camera
241            MKMapCamera *camera = [RCTConvert MKMapCameraWithDefaults:json existingCamera:[mapView camera]];
242
243            // don't emit region change events when we are setting the camera
244            BOOL originalIgnore = mapView.ignoreRegionChanges;
245            mapView.ignoreRegionChanges = YES;
246            [mapView setCamera:camera animated:NO];
247            mapView.ignoreRegionChanges = originalIgnore;
248        }
249    }];
250}
251
252
253RCT_EXPORT_METHOD(animateCamera:(nonnull NSNumber *)reactTag
254                  withCamera:(id)json
255                  withDuration:(CGFloat)duration)
256{
257    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
258        id view = viewRegistry[reactTag];
259        if (![view isKindOfClass:[AIRMap class]]) {
260            RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
261        } else {
262            AIRMap *mapView = (AIRMap *)view;
263
264            // Merge the changes given with the current camera
265            MKMapCamera *camera = [RCTConvert MKMapCameraWithDefaults:json existingCamera:[mapView camera]];
266
267            // don't emit region change events when we are setting the camera
268            BOOL originalIgnore = mapView.ignoreRegionChanges;
269            mapView.ignoreRegionChanges = YES;
270            [AIRMap animateWithDuration:duration/1000 animations:^{
271                [mapView setCamera:camera animated:YES];
272            } completion:^(BOOL finished){
273                mapView.ignoreRegionChanges = originalIgnore;
274            }];
275        }
276    }];
277}
278
279RCT_EXPORT_METHOD(animateToRegion:(nonnull NSNumber *)reactTag
280        withRegion:(MKCoordinateRegion)region
281        withDuration:(CGFloat)duration)
282{
283    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
284        id view = viewRegistry[reactTag];
285        if (![view isKindOfClass:[AIRMap class]]) {
286            RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
287        } else {
288            [AIRMap animateWithDuration:duration/1000 animations:^{
289                [(AIRMap *)view setRegion:region animated:YES];
290            }];
291        }
292    }];
293}
294
295RCT_EXPORT_METHOD(fitToElements:(nonnull NSNumber *)reactTag
296        edgePadding:(nonnull NSDictionary *)edgePadding
297        animated:(BOOL)animated)
298{
299    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
300        id view = viewRegistry[reactTag];
301        if (![view isKindOfClass:[AIRMap class]]) {
302            RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
303        } else {
304            AIRMap *mapView = (AIRMap *)view;
305            // TODO(lmr): we potentially want to include overlays here... and could concat the two arrays together.
306            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
307                [mapView showAnnotations:mapView.annotations animated:animated];
308            });
309        }
310    }];
311}
312
313RCT_EXPORT_METHOD(fitToSuppliedMarkers:(nonnull NSNumber *)reactTag
314                  markers:(nonnull NSArray *)markers
315                  edgePadding:(nonnull NSDictionary *)edgePadding
316                  animated:(BOOL)animated)
317{
318    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
319        id view = viewRegistry[reactTag];
320        if (![view isKindOfClass:[AIRMap class]]) {
321            RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
322        } else {
323            AIRMap *mapView = (AIRMap *)view;
324            // TODO(lmr): we potentially want to include overlays here... and could concat the two arrays together.
325            // id annotations = mapView.annotations;
326
327            NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
328                AIRMapMarker *marker = (AIRMapMarker *)evaluatedObject;
329                return [marker isKindOfClass:[AIRMapMarker class]] && [markers containsObject:marker.identifier];
330            }];
331
332            NSArray *filteredMarkers = [mapView.annotations filteredArrayUsingPredicate:filterMarkers];
333
334            [mapView showAnnotations:filteredMarkers animated:animated];
335
336        }
337    }];
338}
339
340RCT_EXPORT_METHOD(fitToCoordinates:(nonnull NSNumber *)reactTag
341                  coordinates:(nonnull NSArray<AIRMapCoordinate *> *)coordinates
342                  edgePadding:(nonnull NSDictionary *)edgePadding
343                  animated:(BOOL)animated)
344{
345    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
346        id view = viewRegistry[reactTag];
347        if (![view isKindOfClass:[AIRMap class]]) {
348            RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
349        } else {
350            AIRMap *mapView = (AIRMap *)view;
351
352            // Create Polyline with coordinates
353            CLLocationCoordinate2D coords[coordinates.count];
354            for(int i = 0; i < coordinates.count; i++)
355            {
356                coords[i] = coordinates[i].coordinate;
357            }
358            MKPolyline *polyline = [MKPolyline polylineWithCoordinates:coords count:coordinates.count];
359
360            // Set Map viewport
361            CGFloat top = [RCTConvert CGFloat:edgePadding[@"top"]];
362            CGFloat right = [RCTConvert CGFloat:edgePadding[@"right"]];
363            CGFloat bottom = [RCTConvert CGFloat:edgePadding[@"bottom"]];
364            CGFloat left = [RCTConvert CGFloat:edgePadding[@"left"]];
365
366            [mapView setVisibleMapRect:[polyline boundingMapRect] edgePadding:UIEdgeInsetsMake(top, left, bottom, right) animated:animated];
367
368        }
369    }];
370}
371
372RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)reactTag
373        width:(nonnull NSNumber *)width
374        height:(nonnull NSNumber *)height
375        region:(MKCoordinateRegion)region
376        format:(nonnull NSString *)format
377        quality:(nonnull NSNumber *)quality
378        result:(nonnull NSString *)result
379        callback:(RCTResponseSenderBlock)callback)
380{
381    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
382        id view = viewRegistry[reactTag];
383        if (![view isKindOfClass:[AIRMap class]]) {
384            RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
385        } else {
386            AIRMap *mapView = (AIRMap *)view;
387            MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init];
388
389            options.mapType = mapView.mapType;
390            options.region = (region.center.latitude && region.center.longitude) ? region : mapView.region;
391            options.size = CGSizeMake(
392              ([width floatValue] == 0) ? mapView.bounds.size.width : [width floatValue],
393              ([height floatValue] == 0) ? mapView.bounds.size.height : [height floatValue]
394            );
395            options.scale = [[UIScreen mainScreen] scale];
396
397            MKMapSnapshotter *snapshotter = [[MKMapSnapshotter alloc] initWithOptions:options];
398
399            [self takeMapSnapshot:mapView
400                snapshotter:snapshotter
401                format:format
402                quality:quality.floatValue
403                result:result
404                callback:callback];
405        }
406    }];
407}
408
409RCT_EXPORT_METHOD(pointForCoordinate:(nonnull NSNumber *)reactTag
410                  coordinate: (NSDictionary *)coordinate
411                  resolver: (RCTPromiseResolveBlock)resolve
412                  rejecter:(RCTPromiseRejectBlock)reject)
413{
414    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
415        id view = viewRegistry[reactTag];
416        AIRMap *mapView = (AIRMap *)view;
417        if (![view isKindOfClass:[AIRMap class]]) {
418            reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL);
419        } else {
420            CGPoint touchPoint = [mapView convertCoordinate:
421                                  CLLocationCoordinate2DMake(
422                                                             [coordinate[@"latitude"] doubleValue],
423                                                             [coordinate[@"longitude"] doubleValue]
424                                                             )
425                                              toPointToView:mapView];
426
427            resolve(@{
428                      @"x": @(touchPoint.x),
429                      @"y": @(touchPoint.y),
430                      });
431        }
432    }];
433}
434
435RCT_EXPORT_METHOD(getMarkersFrames:(nonnull NSNumber *)reactTag
436                  onlyVisible:(BOOL)onlyVisible
437                  resolver: (RCTPromiseResolveBlock)resolve
438                  rejecter:(RCTPromiseRejectBlock)reject)
439{
440    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
441        id view = viewRegistry[reactTag];
442        AIRMap *mapView = (AIRMap *)view;
443        if (![view isKindOfClass:[AIRMap class]]) {
444            reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL);
445        } else {
446            resolve([mapView getMarkersFramesWithOnlyVisible:onlyVisible]);
447        }
448    }];
449}
450
451
452
453RCT_EXPORT_METHOD(coordinateForPoint:(nonnull NSNumber *)reactTag
454                  point:(NSDictionary *)point
455                  resolver: (RCTPromiseResolveBlock)resolve
456                  rejecter:(RCTPromiseRejectBlock)reject)
457{
458    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
459        id view = viewRegistry[reactTag];
460        AIRMap *mapView = (AIRMap *)view;
461        if (![view isKindOfClass:[AIRMap class]]) {
462            reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL);
463        } else {
464            CLLocationCoordinate2D coordinate = [mapView convertPoint:
465                                                 CGPointMake(
466                                                             [point[@"x"] doubleValue],
467                                                             [point[@"y"] doubleValue]
468                                                             )
469                                                 toCoordinateFromView:mapView];
470
471            resolve(@{
472                      @"latitude": @(coordinate.latitude),
473                      @"longitude": @(coordinate.longitude),
474                      });
475        }
476    }];
477}
478
479RCT_EXPORT_METHOD(getAddressFromCoordinates:(nonnull NSNumber *)reactTag
480                                 coordinate: (NSDictionary *)coordinate
481                                   resolver: (RCTPromiseResolveBlock)resolve
482                                   rejecter:(RCTPromiseRejectBlock)reject)
483{
484    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
485        id view = viewRegistry[reactTag];
486        if (![view isKindOfClass:[AIRMap class]]) {
487            reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRMap, got: %@", view], NULL);
488        } else {
489            if (coordinate == nil ||
490                ![[coordinate allKeys] containsObject:@"latitude"] ||
491                ![[coordinate allKeys] containsObject:@"longitude"]) {
492                reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid coordinate format"], NULL);
493            }
494            CLLocation *location = [[CLLocation alloc] initWithLatitude:[coordinate[@"latitude"] doubleValue]
495                                                              longitude:[coordinate[@"longitude"] doubleValue]];
496            CLGeocoder *geoCoder = [[CLGeocoder alloc] init];
497            [geoCoder reverseGeocodeLocation:location
498                           completionHandler:^(NSArray *placemarks, NSError *error) {
499                    if (error == nil && [placemarks count] > 0){
500                        CLPlacemark *placemark = placemarks[0];
501                        resolve(@{
502                            @"name" : [NSString stringWithFormat:@"%@", placemark.name],
503                            @"thoroughfare" : [NSString stringWithFormat:@"%@", placemark.thoroughfare],
504                            @"subThoroughfare" : [NSString stringWithFormat:@"%@", placemark.subThoroughfare],
505                            @"locality" : [NSString stringWithFormat:@"%@", placemark.locality],
506                            @"subLocality" : [NSString stringWithFormat:@"%@", placemark.subLocality],
507                            @"administrativeArea" : [NSString stringWithFormat:@"%@", placemark.administrativeArea],
508                            @"subAdministrativeArea" : [NSString stringWithFormat:@"%@", placemark.subAdministrativeArea],
509                            @"postalCode" : [NSString stringWithFormat:@"%@", placemark.postalCode],
510                            @"countryCode" : [NSString stringWithFormat:@"%@", placemark.ISOcountryCode],
511                            @"country" : [NSString stringWithFormat:@"%@", placemark.country],
512                        });
513                    } else {
514                        reject(@"Invalid argument", [NSString stringWithFormat:@"Can not get address location"], NULL);
515                    }
516            }];
517        }
518    }];
519}
520
521#pragma mark Take Snapshot
522- (void)takeMapSnapshot:(AIRMap *)mapView
523        snapshotter:(MKMapSnapshotter *) snapshotter
524        format:(NSString *)format
525        quality:(CGFloat) quality
526        result:(NSString *)result
527        callback:(RCTResponseSenderBlock) callback {
528    NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970];
529    NSString *pathComponent = [NSString stringWithFormat:@"Documents/snapshot-%.20lf.%@", timeStamp, format];
530    NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent: pathComponent];
531
532    [snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
533              completionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
534                  if (error) {
535                      callback(@[error]);
536                      return;
537                  }
538                  MKAnnotationView *pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:nil];
539
540                  UIImage *image = snapshot.image;
541                  UIGraphicsBeginImageContextWithOptions(image.size, YES, image.scale);
542                  {
543                      [image drawAtPoint:CGPointMake(0.0f, 0.0f)];
544
545                      CGRect rect = CGRectMake(0.0f, 0.0f, image.size.width, image.size.height);
546
547                      for (id <AIRMapSnapshot> overlay in mapView.overlays) {
548                          if ([overlay respondsToSelector:@selector(drawToSnapshot:context:)]) {
549                                  [overlay drawToSnapshot:snapshot context:UIGraphicsGetCurrentContext()];
550                          }
551                      }
552
553                      for (id <MKAnnotation> annotation in mapView.annotations) {
554                          CGPoint point = [snapshot pointForCoordinate:annotation.coordinate];
555
556                          MKAnnotationView* anView = [mapView viewForAnnotation: annotation];
557
558                          if (anView){
559                              pin = anView;
560                          }
561
562                          if (CGRectContainsPoint(rect, point)) {
563                              point.x = point.x + pin.centerOffset.x - (pin.bounds.size.width / 2.0f);
564                              point.y = point.y + pin.centerOffset.y - (pin.bounds.size.height / 2.0f);
565                              if (pin.image) {
566                                  [pin.image drawAtPoint:point];
567                              } else {
568                                  CGRect pinRect = CGRectMake(point.x, point.y, pin.bounds.size.width, pin.bounds.size.height);
569                                  [pin drawViewHierarchyInRect:pinRect afterScreenUpdates:NO];
570                              }
571                          }
572                      }
573
574                      UIImage *compositeImage = UIGraphicsGetImageFromCurrentImageContext();
575
576                      NSData *data;
577                      if ([format isEqualToString:@"png"]) {
578                          data = UIImagePNGRepresentation(compositeImage);
579                      }
580                      else if([format isEqualToString:@"jpg"]) {
581                          data = UIImageJPEGRepresentation(compositeImage, quality);
582                      }
583
584                      if ([result isEqualToString:@"file"]) {
585                          [data writeToFile:filePath atomically:YES];
586                          callback(@[[NSNull null], filePath]);
587                      }
588                      else if ([result isEqualToString:@"base64"]) {
589                          callback(@[[NSNull null], [data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn]]);
590                      }
591                  }
592                  UIGraphicsEndImageContext();
593              }];
594}
595
596#pragma mark Gesture Recognizer Handlers
597
598#define MAX_DISTANCE_PX 10.0f
599- (void)handleMapTap:(UITapGestureRecognizer *)recognizer {
600    AIRMap *map = (AIRMap *)recognizer.view;
601
602    CGPoint tapPoint = [recognizer locationInView:map];
603    CLLocationCoordinate2D tapCoordinate = [map convertPoint:tapPoint toCoordinateFromView:map];
604    MKMapPoint mapPoint = MKMapPointForCoordinate(tapCoordinate);
605    CGPoint mapPointAsCGP = CGPointMake(mapPoint.x, mapPoint.y);
606
607    double maxMeters = [self metersFromPixel:MAX_DISTANCE_PX atPoint:tapPoint forMap:map];
608    float nearestDistance = MAXFLOAT;
609    AIRMapPolyline *nearestPolyline = nil;
610
611    for (id<MKOverlay> overlay in map.overlays) {
612        if([overlay isKindOfClass:[AIRMapPolygon class]]){
613            AIRMapPolygon *polygon = (AIRMapPolygon*) overlay;
614            if (polygon.onPress) {
615                CGMutablePathRef mpr = CGPathCreateMutable();
616
617                for(int i = 0; i < polygon.coordinates.count; i++) {
618                    AIRMapCoordinate *c = polygon.coordinates[i];
619                    MKMapPoint mp = MKMapPointForCoordinate(c.coordinate);
620                    if (i == 0) {
621                        CGPathMoveToPoint(mpr, NULL, mp.x, mp.y);
622                    } else {
623                        CGPathAddLineToPoint(mpr, NULL, mp.x, mp.y);
624                    }
625                }
626
627                if (CGPathContainsPoint(mpr, NULL, mapPointAsCGP, FALSE)) {
628                    id event = @{
629                                @"action": @"polygon-press",
630                                @"coordinate": @{
631                                    @"latitude": @(tapCoordinate.latitude),
632                                    @"longitude": @(tapCoordinate.longitude),
633                                },
634                            };
635                    polygon.onPress(event);
636                }
637
638                CGPathRelease(mpr);
639            }
640        }
641
642        if([overlay isKindOfClass:[AIRMapPolyline class]]){
643            AIRMapPolyline *polyline = (AIRMapPolyline*) overlay;
644            if (polyline.onPress) {
645                float distance = [self distanceOfPoint:MKMapPointForCoordinate(tapCoordinate)
646                                          toPoly:polyline];
647                if (distance < nearestDistance) {
648                    nearestDistance = distance;
649                    nearestPolyline = polyline;
650                }
651            }
652        }
653
654        if ([overlay isKindOfClass:[AIRMapOverlay class]]) {
655            AIRMapOverlay *imageOverlay = (AIRMapOverlay*) overlay;
656            if (MKMapRectContainsPoint(imageOverlay.boundingMapRect, mapPoint)) {
657                if (imageOverlay.onPress) {
658                    id event = @{
659                                 @"action": @"image-overlay-press",
660                                 @"name": imageOverlay.name ?: @"unknown",
661                                 @"coordinate": @{
662                                         @"latitude": @(imageOverlay.coordinate.latitude),
663                                         @"longitude": @(imageOverlay.coordinate.longitude)
664                                         }
665                                 };
666                    imageOverlay.onPress(event);
667                }
668            }
669        }
670
671    }
672
673    if (nearestDistance <= maxMeters) {
674        id event = @{
675                   @"action": @"polyline-press",
676                   @"coordinate": @{
677                       @"latitude": @(tapCoordinate.latitude),
678                       @"longitude": @(tapCoordinate.longitude)
679                   }
680                   };
681        nearestPolyline.onPress(event);
682    }
683
684    if (!map.onPress) return;
685    map.onPress(@{
686            @"coordinate": @{
687                    @"latitude": @(tapCoordinate.latitude),
688                    @"longitude": @(tapCoordinate.longitude),
689            },
690            @"position": @{
691                    @"x": @(tapPoint.x),
692                    @"y": @(tapPoint.y),
693            },
694    });
695
696}
697
698- (void)handleMapDrag:(UIPanGestureRecognizer*)recognizer {
699    AIRMap *map = (AIRMap *)recognizer.view;
700    if (!map.onPanDrag) return;
701
702    CGPoint touchPoint = [recognizer locationInView:map];
703    CLLocationCoordinate2D coord = [map convertPoint:touchPoint toCoordinateFromView:map];
704    map.onPanDrag(@{
705                  @"coordinate": @{
706                          @"latitude": @(coord.latitude),
707                          @"longitude": @(coord.longitude),
708                          },
709                  @"position": @{
710                          @"x": @(touchPoint.x),
711                          @"y": @(touchPoint.y),
712                          },
713                  });
714
715}
716
717- (void)handleMapDoubleTap:(UIPanGestureRecognizer*)recognizer {
718    AIRMap *map = (AIRMap *)recognizer.view;
719    if (!map.onDoublePress) return;
720
721    CGPoint touchPoint = [recognizer locationInView:map];
722    CLLocationCoordinate2D coord = [map convertPoint:touchPoint toCoordinateFromView:map];
723    map.onDoublePress(@{
724                    @"coordinate": @{
725                            @"latitude": @(coord.latitude),
726                            @"longitude": @(coord.longitude),
727                            },
728                    @"position": @{
729                            @"x": @(touchPoint.x),
730                            @"y": @(touchPoint.y),
731                            },
732                    });
733
734}
735
736
737- (void)handleMapLongPress:(UITapGestureRecognizer *)recognizer {
738
739    // NOTE: android only does the equivalent of "began", so we only send in this case
740    if (recognizer.state != UIGestureRecognizerStateBegan) return;
741
742    AIRMap *map = (AIRMap *)recognizer.view;
743    if (!map.onLongPress) return;
744
745    CGPoint touchPoint = [recognizer locationInView:map];
746    CLLocationCoordinate2D coord = [map convertPoint:touchPoint toCoordinateFromView:map];
747
748    map.onLongPress(@{
749            @"coordinate": @{
750                    @"latitude": @(coord.latitude),
751                    @"longitude": @(coord.longitude),
752            },
753            @"position": @{
754                    @"x": @(touchPoint.x),
755                    @"y": @(touchPoint.y),
756            },
757    });
758}
759
760#pragma mark MKMapViewDelegate
761
762#pragma mark Polyline stuff
763
764- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>)overlay{
765    if ([overlay isKindOfClass:[AIRMapPolyline class]]) {
766        return ((AIRMapPolyline *)overlay).renderer;
767    } else if ([overlay isKindOfClass:[AIRMapPolygon class]]) {
768        return ((AIRMapPolygon *)overlay).renderer;
769    } else if ([overlay isKindOfClass:[AIRMapCircle class]]) {
770        return ((AIRMapCircle *)overlay).renderer;
771    } else if ([overlay isKindOfClass:[AIRMapUrlTile class]]) {
772        return ((AIRMapUrlTile *)overlay).renderer;
773    } else if ([overlay isKindOfClass:[AIRMapWMSTile class]]) {
774        return ((AIRMapWMSTile *)overlay).renderer;
775    } else if ([overlay isKindOfClass:[AIRMapLocalTile class]]) {
776        return ((AIRMapLocalTile *)overlay).renderer;
777    } else if ([overlay isKindOfClass:[AIRMapOverlay class]]) {
778        return ((AIRMapOverlay *)overlay).renderer;
779    } else if([overlay isKindOfClass:[MKTileOverlay class]]) {
780        return [[MKTileOverlayRenderer alloc] initWithTileOverlay:overlay];
781    } else {
782        return nil;
783    }
784}
785
786
787#pragma mark Annotation Stuff
788
789- (void)mapView:(AIRMap *)mapView didAddAnnotationViews:(NSArray<MKAnnotationView *> *)views
790{
791    if(!mapView.userLocationCalloutEnabled){
792        for(MKAnnotationView* view in views){
793            if ([view.annotation isKindOfClass:[MKUserLocation class]]){
794                [view setEnabled:NO];
795                [view setCanShowCallout:NO];
796                break;
797            }
798        }
799    }
800}
801
802
803- (void)mapView:(AIRMap *)mapView didSelectAnnotationView:(MKAnnotationView *)view
804{
805    if ([view.annotation isKindOfClass:[AIRMapMarker class]]) {
806        [(AIRMapMarker *)view.annotation showCalloutView];
807    } else if ([view.annotation isKindOfClass:[MKUserLocation class]] && mapView.userLocationAnnotationTitle != nil && view.annotation.title != mapView.userLocationAnnotationTitle) {
808        [(MKUserLocation*)view.annotation setTitle: mapView.userLocationAnnotationTitle];
809    }
810
811}
812
813- (void)mapView:(AIRMap *)mapView didDeselectAnnotationView:(MKAnnotationView *)view {
814    if ([view.annotation isKindOfClass:[AIRMapMarker class]]) {
815        [(AIRMapMarker *)view.annotation hideCalloutView];
816    }
817}
818
819- (MKAnnotationView *)mapView:(__unused AIRMap *)mapView viewForAnnotation:(AIRMapMarker *)marker
820{
821    if (![marker isKindOfClass:[AIRMapMarker class]]) {
822        if ([marker isKindOfClass:[MKUserLocation class]] && mapView.userLocationAnnotationTitle != nil) {
823            [(MKUserLocation*)marker setTitle: mapView.userLocationAnnotationTitle];
824            return nil;
825        }
826        return nil;
827    }
828
829    marker.map = mapView;
830    return [marker getAnnotationView];
831}
832
833static int kDragCenterContext;
834
835- (void)mapView:(AIRMap *)mapView
836    annotationView:(MKAnnotationView *)view
837    didChangeDragState:(MKAnnotationViewDragState)newState
838    fromOldState:(MKAnnotationViewDragState)oldState
839{
840    if (![view.annotation isKindOfClass:[AIRMapMarker class]]) return;
841    AIRMapMarker *marker = (AIRMapMarker *)view.annotation;
842
843    BOOL isPinView = [view isKindOfClass:[MKPinAnnotationView class]];
844
845    id event = @{
846                 @"id": marker.identifier ?: @"unknown",
847                 @"coordinate": @{
848                         @"latitude": @(marker.coordinate.latitude),
849                         @"longitude": @(marker.coordinate.longitude)
850                         }
851                 };
852
853    if (newState == MKAnnotationViewDragStateEnding || newState == MKAnnotationViewDragStateCanceling) {
854        if (!isPinView) {
855            [view setDragState:MKAnnotationViewDragStateNone animated:NO];
856        }
857        if (mapView.onMarkerDragEnd) mapView.onMarkerDragEnd(event);
858        if (marker.onDragEnd) marker.onDragEnd(event);
859
860       if(_hasObserver) [view removeObserver:self forKeyPath:@"center"];
861        _hasObserver = NO;
862    } else if (newState == MKAnnotationViewDragStateStarting) {
863        // MapKit doesn't emit continuous drag events. To get around this, we are going to use KVO.
864        [view addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionNew context:&kDragCenterContext];
865        _hasObserver = YES;
866        if (mapView.onMarkerDragStart) mapView.onMarkerDragStart(event);
867        if (marker.onDragStart) marker.onDragStart(event);
868    }
869}
870
871- (void)observeValueForKeyPath:(NSString *)keyPath
872                      ofObject:(id)object
873                        change:(NSDictionary *)change
874                       context:(void *)context
875{
876    if ([keyPath isEqualToString:@"center"] && [object isKindOfClass:[MKAnnotationView class]]) {
877        MKAnnotationView *view = (MKAnnotationView *)object;
878        AIRMapMarker *marker = (AIRMapMarker *)view.annotation;
879
880        // a marker we don't control might be getting dragged. Check just in case.
881        if (!marker) return;
882
883        AIRMap *map = marker.map;
884
885        // don't waste time calculating if there are no events to listen to it
886        if (!map.onMarkerDrag && !marker.onDrag) return;
887
888        CGPoint position = CGPointMake(view.center.x - view.centerOffset.x, view.center.y - view.centerOffset.y);
889        CLLocationCoordinate2D coordinate = [map convertPoint:position toCoordinateFromView:map];
890
891        id event = @{
892                @"id": marker.identifier ?: @"unknown",
893                @"position": @{
894                        @"x": @(position.x),
895                        @"y": @(position.y),
896                },
897                @"coordinate": @{
898                        @"latitude": @(coordinate.latitude),
899                        @"longitude": @(coordinate.longitude),
900                }
901        };
902
903        if (map.onMarkerDrag) map.onMarkerDrag(event);
904        if (marker.onDrag) marker.onDrag(event);
905
906    } else {
907        // This message is not for me; pass it on to super.
908        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
909    }
910}
911
912- (void)mapView:(AIRMap *)mapView didUpdateUserLocation:(MKUserLocation *)location
913{
914    id event = @{@"coordinate": @{
915                         @"latitude": @(location.coordinate.latitude),
916                         @"longitude": @(location.coordinate.longitude),
917                         @"altitude": @(location.location.altitude),
918                         @"timestamp": @(location.location.timestamp.timeIntervalSinceReferenceDate * 1000),
919                         @"accuracy": @(location.location.horizontalAccuracy),
920                         @"altitudeAccuracy": @(location.location.verticalAccuracy),
921                         @"speed": @(location.location.speed),
922                         @"heading": @(location.location.course),
923                         }
924                 };
925
926    if (mapView.onUserLocationChange) {
927        mapView.onUserLocationChange(event);
928    }
929
930    if (mapView.followUserLocation) {
931        [mapView setCenterCoordinate:location.coordinate animated:YES];
932    }
933
934}
935
936- (void)mapViewDidChangeVisibleRegion:(AIRMap *)mapView
937{
938    [self _regionChanged:mapView];
939}
940
941- (void)mapView:(AIRMap *)mapView regionDidChangeAnimated:(__unused BOOL)animated
942{
943    CGFloat zoomLevel = [self zoomLevel:mapView];
944
945    // Don't send region did change events until map has
946    // started rendering, as these won't represent the final location
947    if(mapView.hasStartedRendering){
948        [self _regionChanged:mapView];
949    }
950
951    if (zoomLevel < mapView.minZoomLevel) {
952      [self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.minZoomLevel animated:TRUE mapView:mapView];
953    }
954    else if (zoomLevel > mapView.maxZoomLevel) {
955      [self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.maxZoomLevel animated:TRUE mapView:mapView];
956    }
957
958    // Don't send region did change events until map has
959    // started rendering, as these won't represent the final location
960    if (mapView.hasStartedRendering) {
961        [self _emitRegionChangeEvent:mapView continuous:NO];
962    };
963
964    mapView.pendingCenter = mapView.region.center;
965    mapView.pendingSpan = mapView.region.span;
966}
967
968- (void)mapViewWillStartRenderingMap:(AIRMap *)mapView
969{
970    if (!mapView.hasStartedRendering) {
971      mapView.onMapReady(@{});
972      mapView.hasStartedRendering = YES;
973    }
974    [mapView beginLoading];
975}
976
977- (void)mapViewDidFinishRenderingMap:(AIRMap *)mapView fullyRendered:(BOOL)fullyRendered
978{
979    [mapView finishLoading];
980}
981
982#pragma mark Private
983
984- (void)_regionChanged:(AIRMap *)mapView
985{
986    BOOL needZoom = NO;
987    CGFloat newLongitudeDelta = 0.0f;
988    MKCoordinateRegion region = mapView.region;
989    // On iOS 7, it's possible that we observe invalid locations during initialization of the map.
990    // Filter those out.
991    if (!CLLocationCoordinate2DIsValid(region.center)) {
992        return;
993    }
994    // Calculation on float is not 100% accurate. If user zoom to max/min and then move, it's likely the map will auto zoom to max/min from time to time.
995    // So let's try to make map zoom back to 99% max or 101% min so that there are some buffer that moving the map won't constantly hitting the max/min bound.
996    if (mapView.maxDelta > FLT_EPSILON && region.span.longitudeDelta > mapView.maxDelta) {
997        needZoom = YES;
998        newLongitudeDelta = mapView.maxDelta * (1 - AIRMapZoomBoundBuffer);
999    } else if (mapView.minDelta > FLT_EPSILON && region.span.longitudeDelta < mapView.minDelta) {
1000        needZoom = YES;
1001        newLongitudeDelta = mapView.minDelta * (1 + AIRMapZoomBoundBuffer);
1002    }
1003    if (needZoom) {
1004        region.span.latitudeDelta = region.span.latitudeDelta / region.span.longitudeDelta * newLongitudeDelta;
1005        region.span.longitudeDelta = newLongitudeDelta;
1006        mapView.region = region;
1007    }
1008
1009    // Continuously observe region changes
1010    [self _emitRegionChangeEvent:mapView continuous:YES];
1011}
1012
1013- (void)_emitRegionChangeEvent:(AIRMap *)mapView continuous:(BOOL)continuous
1014{
1015    if (!mapView.ignoreRegionChanges && mapView.onChange) {
1016        MKCoordinateRegion region = mapView.region;
1017        if (!CLLocationCoordinate2DIsValid(region.center)) {
1018            return;
1019        }
1020
1021#define FLUSH_NAN(value) (isnan(value) ? 0 : value)
1022        mapView.onChange(@{
1023                @"continuous": @(continuous),
1024                @"region": @{
1025                        @"latitude": @(FLUSH_NAN(region.center.latitude)),
1026                        @"longitude": @(FLUSH_NAN(region.center.longitude)),
1027                        @"latitudeDelta": @(FLUSH_NAN(region.span.latitudeDelta)),
1028                        @"longitudeDelta": @(FLUSH_NAN(region.span.longitudeDelta)),
1029                }
1030        });
1031    }
1032}
1033
1034/** Returns the distance of |pt| to |poly| in meters
1035 *
1036 *
1037 */
1038- (double)distanceOfPoint:(MKMapPoint)pt toPoly:(AIRMapPolyline *)poly
1039{
1040    double distance = MAXFLOAT;
1041    for (int n = 0; n < poly.coordinates.count - 1; n++) {
1042
1043        MKMapPoint ptA = MKMapPointForCoordinate(poly.coordinates[n].coordinate);
1044        MKMapPoint ptB = MKMapPointForCoordinate(poly.coordinates[n + 1].coordinate);
1045
1046        double xDelta = ptB.x - ptA.x;
1047        double yDelta = ptB.y - ptA.y;
1048
1049        if (xDelta == 0.0 && yDelta == 0.0) {
1050            continue;
1051        }
1052
1053        double u = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta);
1054        MKMapPoint ptClosest;
1055        if (u < 0.0) {
1056            ptClosest = ptA;
1057        }
1058        else if (u > 1.0) {
1059            ptClosest = ptB;
1060        }
1061        else {
1062            ptClosest = MKMapPointMake(ptA.x + u * xDelta, ptA.y + u * yDelta);
1063        }
1064
1065        distance = MIN(distance, MKMetersBetweenMapPoints(ptClosest, pt));
1066    }
1067
1068    return distance;
1069}
1070
1071
1072/** Converts |px| to meters at location |pt| */
1073- (double)metersFromPixel:(NSUInteger)px atPoint:(CGPoint)pt forMap:(AIRMap *)mapView
1074{
1075    CGPoint ptB = CGPointMake(pt.x + px, pt.y);
1076
1077    CLLocationCoordinate2D coordA = [mapView convertPoint:pt toCoordinateFromView:mapView];
1078    CLLocationCoordinate2D coordB = [mapView convertPoint:ptB toCoordinateFromView:mapView];
1079
1080    return MKMetersBetweenMapPoints(MKMapPointForCoordinate(coordA), MKMapPointForCoordinate(coordB));
1081}
1082
1083+ (double)longitudeToPixelSpaceX:(double)longitude
1084{
1085    return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0);
1086}
1087
1088+ (double)latitudeToPixelSpaceY:(double)latitude
1089{
1090	if (latitude == 90.0) {
1091		return 0;
1092	} else if (latitude == -90.0) {
1093		return MERCATOR_OFFSET * 2;
1094	} else {
1095		return round(MERCATOR_OFFSET - MERCATOR_RADIUS * logf((1 + sinf(latitude * M_PI / 180.0)) / (1 - sinf(latitude * M_PI / 180.0))) / 2.0);
1096	}
1097}
1098
1099+ (double)pixelSpaceXToLongitude:(double)pixelX
1100{
1101    return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI;
1102}
1103
1104+ (double)pixelSpaceYToLatitude:(double)pixelY
1105{
1106    return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI;
1107}
1108
1109#pragma mark -
1110#pragma mark Helper methods
1111
1112- (MKCoordinateSpan)coordinateSpanWithMapView:(AIRMap *)mapView
1113                             centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
1114                                 andZoomLevel:(double)zoomLevel
1115{
1116    // convert center coordiate to pixel space
1117    double centerPixelX = [AIRMapManager longitudeToPixelSpaceX:centerCoordinate.longitude];
1118    double centerPixelY = [AIRMapManager latitudeToPixelSpaceY:centerCoordinate.latitude];
1119
1120    // determine the scale value from the zoom level
1121    double zoomExponent = AIRMapMaxZoomLevel - zoomLevel;
1122    double zoomScale = pow(2, zoomExponent);
1123
1124    // scale the map’s size in pixel space
1125    CGSize mapSizeInPixels = mapView.bounds.size;
1126    double scaledMapWidth = mapSizeInPixels.width * zoomScale;
1127    double scaledMapHeight = mapSizeInPixels.height * zoomScale;
1128
1129    // figure out the position of the top-left pixel
1130    double topLeftPixelX = centerPixelX - (scaledMapWidth / 2);
1131    double topLeftPixelY = centerPixelY - (scaledMapHeight / 2);
1132
1133    // find delta between left and right longitudes
1134    CLLocationDegrees minLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX];
1135    CLLocationDegrees maxLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth];
1136    CLLocationDegrees longitudeDelta = maxLng - minLng;
1137
1138    // find delta between top and bottom latitudes
1139    CLLocationDegrees minLat = [AIRMapManager pixelSpaceYToLatitude:topLeftPixelY];
1140    CLLocationDegrees maxLat = [AIRMapManager pixelSpaceYToLatitude:topLeftPixelY + scaledMapHeight];
1141    CLLocationDegrees latitudeDelta = -1 * (maxLat - minLat);
1142
1143    // create and return the lat/lng span
1144    MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
1145    return span;
1146}
1147
1148#pragma mark -
1149#pragma mark Public methods
1150
1151- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
1152                  zoomLevel:(double)zoomLevel
1153                   animated:(BOOL)animated
1154                    mapView:(AIRMap *)mapView
1155{
1156    // clamp large numbers to 28
1157    zoomLevel = MIN(zoomLevel, AIRMapMaxZoomLevel);
1158
1159    // use the zoom level to compute the region
1160    MKCoordinateSpan span = [self coordinateSpanWithMapView:mapView centerCoordinate:centerCoordinate andZoomLevel:zoomLevel];
1161    MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span);
1162
1163    // set the region like normal
1164    [mapView setRegion:region animated:animated];
1165}
1166
1167//KMapView cannot display tiles that cross the pole (as these would involve wrapping the map from top to bottom, something that a Mercator projection just cannot do).
1168-(MKCoordinateRegion)coordinateRegionWithMapView:(AIRMap *)mapView
1169                                centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
1170                                    andZoomLevel:(double)zoomLevel
1171{
1172	// clamp lat/long values to appropriate ranges
1173	centerCoordinate.latitude = MIN(MAX(-90.0, centerCoordinate.latitude), 90.0);
1174	centerCoordinate.longitude = fmod(centerCoordinate.longitude, 180.0);
1175
1176	// convert center coordiate to pixel space
1177	double centerPixelX = [AIRMapManager longitudeToPixelSpaceX:centerCoordinate.longitude];
1178	double centerPixelY = [AIRMapManager latitudeToPixelSpaceY:centerCoordinate.latitude];
1179
1180	// determine the scale value from the zoom level
1181	double zoomExponent = AIRMapMaxZoomLevel - zoomLevel;
1182	double zoomScale = pow(2, zoomExponent);
1183
1184	// scale the map’s size in pixel space
1185	CGSize mapSizeInPixels = mapView.bounds.size;
1186	double scaledMapWidth = mapSizeInPixels.width * zoomScale;
1187	double scaledMapHeight = mapSizeInPixels.height * zoomScale;
1188
1189	// figure out the position of the left pixel
1190	double topLeftPixelX = centerPixelX - (scaledMapWidth / 2);
1191
1192	// find delta between left and right longitudes
1193	CLLocationDegrees minLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX];
1194	CLLocationDegrees maxLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth];
1195	CLLocationDegrees longitudeDelta = maxLng - minLng;
1196
1197	// if we’re at a pole then calculate the distance from the pole towards the equator
1198	// as MKMapView doesn’t like drawing boxes over the poles
1199	double topPixelY = centerPixelY - (scaledMapHeight / 2);
1200	double bottomPixelY = centerPixelY + (scaledMapHeight / 2);
1201	BOOL adjustedCenterPoint = NO;
1202	if (topPixelY > MERCATOR_OFFSET * 2) {
1203		topPixelY = centerPixelY - scaledMapHeight;
1204		bottomPixelY = MERCATOR_OFFSET * 2;
1205		adjustedCenterPoint = YES;
1206	}
1207
1208	// find delta between top and bottom latitudes
1209	CLLocationDegrees minLat = [AIRMapManager pixelSpaceYToLatitude:topPixelY];
1210	CLLocationDegrees maxLat = [AIRMapManager pixelSpaceYToLatitude:bottomPixelY];
1211	CLLocationDegrees latitudeDelta = -1 * (maxLat - minLat);
1212
1213	// create and return the lat/lng span
1214	MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
1215	MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span);
1216	// once again, MKMapView doesn’t like drawing boxes over the poles
1217	// so adjust the center coordinate to the center of the resulting region
1218	if (adjustedCenterPoint) {
1219		region.center.latitude = [AIRMapManager pixelSpaceYToLatitude:((bottomPixelY + topPixelY) / 2.0)];
1220	}
1221
1222	return region;
1223}
1224
1225- (double) zoomLevel:(AIRMap *)mapView {
1226    MKCoordinateRegion region = mapView.region;
1227
1228    double centerPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude];
1229    double topLeftPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude - region.span.longitudeDelta / 2];
1230
1231    double scaledMapWidth = (centerPixelX - topLeftPixelX) * 2;
1232    CGSize mapSizeInPixels = mapView.bounds.size;
1233    double zoomScale = scaledMapWidth / mapSizeInPixels.width;
1234    double zoomExponent = log(zoomScale) / log(2);
1235    double zoomLevel = AIRMapMaxZoomLevel - zoomExponent;
1236
1237    return zoomLevel;
1238}
1239
1240#pragma mark MKMapViewDelegate - Tracking the User Location
1241
1242- (void)mapView:(AIRMap *)mapView didFailToLocateUserWithError:(NSError *)error {
1243    id event = @{@"error": @{ @"message": error.localizedDescription }};
1244    if (mapView.onUserLocationChange) {
1245        mapView.onUserLocationChange(event);
1246    }
1247}
1248
1249- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
1250    return YES;
1251}
1252
1253@end
1254