1//
2//  AIRGoogleMap.m
3//  AirMaps
4//
5//  Created by Gil Birman on 9/1/16.
6//
7
8#ifdef HAVE_GOOGLE_MAPS
9
10#import "AIRGoogleMap.h"
11#import "AIRGoogleMapMarker.h"
12#import "AIRGoogleMapMarkerManager.h"
13#import "AIRGoogleMapPolygon.h"
14#import "AIRGoogleMapPolyline.h"
15#import "AIRGoogleMapCircle.h"
16#import "AIRGoogleMapHeatmap.h"
17#import "AIRGoogleMapUrlTile.h"
18#import "AIRGoogleMapWMSTile.h"
19#import "AIRGoogleMapOverlay.h"
20#import <GoogleMaps/GoogleMaps.h>
21#import <MapKit/MapKit.h>
22#import <React/UIView+React.h>
23#import <React/RCTBridge.h>
24#import "RCTConvert+AirMap.h"
25#import <objc/runtime.h>
26
27#ifdef HAVE_GOOGLE_MAPS_UTILS
28#import <Google-Maps-iOS-Utils/GMUKMLParser.h>
29#import <Google-Maps-iOS-Utils/GMUPlacemark.h>
30#import <Google-Maps-iOS-Utils/GMUPoint.h>
31#import <Google-Maps-iOS-Utils/GMUGeometryRenderer.h>
32#define REQUIRES_GOOGLE_MAPS_UTILS(feature) do {} while (0)
33#else
34#define GMUKMLParser void
35#define GMUPlacemark void
36#define REQUIRES_GOOGLE_MAPS_UTILS(feature) do { \
37 [NSException raise:@"ReactNativeMapsDependencyMissing" \
38             format:@"Use of " feature "requires Google-Maps-iOS-Utils, you  must install via CocoaPods to use this feature"]; \
39} while (0)
40#endif
41
42
43id regionAsJSON(MKCoordinateRegion region) {
44  return @{
45           @"latitude": [NSNumber numberWithDouble:region.center.latitude],
46           @"longitude": [NSNumber numberWithDouble:region.center.longitude],
47           @"latitudeDelta": [NSNumber numberWithDouble:region.span.latitudeDelta],
48           @"longitudeDelta": [NSNumber numberWithDouble:region.span.longitudeDelta],
49           };
50}
51
52@interface AIRGoogleMap () <GMSIndoorDisplayDelegate>
53
54- (id)eventFromCoordinate:(CLLocationCoordinate2D)coordinate;
55
56@property (nonatomic, strong) NSMutableDictionary<NSNumber *, NSDictionary*> *origGestureRecognizersMeta;
57
58@end
59
60@implementation AIRGoogleMap
61{
62  NSMutableArray<UIView *> *_reactSubviews;
63  MKCoordinateRegion _initialRegion;
64  MKCoordinateRegion _region;
65  BOOL _initialRegionSet;
66  BOOL _initialCameraSet;
67  BOOL _didLayoutSubviews;
68  BOOL _didPrepareMap;
69  BOOL _didCallOnMapReady;
70  BOOL _zoomTapEnabled;
71}
72
73- (instancetype)init
74{
75  if ((self = [super init])) {
76    _reactSubviews = [NSMutableArray new];
77    _markers = [NSMutableArray array];
78    _polygons = [NSMutableArray array];
79    _polylines = [NSMutableArray array];
80    _circles = [NSMutableArray array];
81    _heatmaps = [NSMutableArray array];
82    _tiles = [NSMutableArray array];
83    _overlays = [NSMutableArray array];
84    _initialCamera = nil;
85    _cameraProp = nil;
86    _initialRegion = MKCoordinateRegionMake(CLLocationCoordinate2DMake(0.0, 0.0), MKCoordinateSpanMake(0.0, 0.0));
87    _region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(0.0, 0.0), MKCoordinateSpanMake(0.0, 0.0));
88    _initialRegionSet = false;
89    _initialCameraSet = false;
90    _didLayoutSubviews = false;
91    _didPrepareMap = false;
92    _didCallOnMapReady = false;
93    _zoomTapEnabled = YES;
94
95    // Listen to the myLocation property of GMSMapView.
96    [self addObserver:self
97           forKeyPath:@"myLocation"
98              options:NSKeyValueObservingOptionNew
99              context:NULL];
100
101    self.origGestureRecognizersMeta = [[NSMutableDictionary alloc] init];
102
103    self.indoorDisplay.delegate = self;
104  }
105  return self;
106}
107
108- (void)dealloc {
109  [self removeObserver:self
110            forKeyPath:@"myLocation"
111               context:NULL];
112}
113
114- (id)eventFromCoordinate:(CLLocationCoordinate2D)coordinate {
115
116  CGPoint touchPoint = [self.projection pointForCoordinate:coordinate];
117
118  return @{
119           @"coordinate": @{
120               @"latitude": @(coordinate.latitude),
121               @"longitude": @(coordinate.longitude),
122               },
123           @"position": @{
124               @"x": @(touchPoint.x),
125               @"y": @(touchPoint.y),
126               },
127           };
128}
129
130#pragma clang diagnostic push
131#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
132- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex {
133  // Our desired API is to pass up markers/overlays as children to the mapview component.
134  // This is where we intercept them and do the appropriate underlying mapview action.
135  if ([subview isKindOfClass:[AIRGoogleMapMarker class]]) {
136    AIRGoogleMapMarker *marker = (AIRGoogleMapMarker*)subview;
137    marker.realMarker.map = self;
138    [self.markers addObject:marker];
139  } else if ([subview isKindOfClass:[AIRGoogleMapPolygon class]]) {
140    AIRGoogleMapPolygon *polygon = (AIRGoogleMapPolygon*)subview;
141    polygon.polygon.map = self;
142    [self.polygons addObject:polygon];
143  } else if ([subview isKindOfClass:[AIRGoogleMapPolyline class]]) {
144    AIRGoogleMapPolyline *polyline = (AIRGoogleMapPolyline*)subview;
145    polyline.polyline.map = self;
146    [self.polylines addObject:polyline];
147  } else if ([subview isKindOfClass:[AIRGoogleMapCircle class]]) {
148    AIRGoogleMapCircle *circle = (AIRGoogleMapCircle*)subview;
149    circle.circle.map = self;
150    [self.circles addObject:circle];
151  } else if ([subview isKindOfClass:[AIRGoogleMapUrlTile class]]) {
152    AIRGoogleMapUrlTile *tile = (AIRGoogleMapUrlTile*)subview;
153    tile.tileLayer.map = self;
154    [self.tiles addObject:tile];
155  } else if ([subview isKindOfClass:[AIRGoogleMapWMSTile class]]) {
156    AIRGoogleMapWMSTile *tile = (AIRGoogleMapWMSTile*)subview;
157    tile.tileLayer.map = self;
158    [self.tiles addObject:tile];
159  } else if ([subview isKindOfClass:[AIRGoogleMapOverlay class]]) {
160    AIRGoogleMapOverlay *overlay = (AIRGoogleMapOverlay*)subview;
161    overlay.overlay.map = self;
162    [self.overlays addObject:overlay];
163  } else if ([subview isKindOfClass:[AIRGoogleMapHeatmap class]]){
164    AIRGoogleMapHeatmap *heatmap = (AIRGoogleMapHeatmap*)subview;
165    heatmap.heatmap.map = self;
166    [self.heatmaps addObject:heatmap];
167  } else {
168    NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
169    for (int i = 0; i < childSubviews.count; i++) {
170      [self insertReactSubview:(UIView *)childSubviews[i] atIndex:atIndex];
171    }
172  }
173  [_reactSubviews insertObject:(UIView *)subview atIndex:(NSUInteger) atIndex];
174}
175#pragma clang diagnostic pop
176
177
178#pragma clang diagnostic push
179#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
180- (void)removeReactSubview:(id<RCTComponent>)subview {
181  // similarly, when the children are being removed we have to do the appropriate
182  // underlying mapview action here.
183  if ([subview isKindOfClass:[AIRGoogleMapMarker class]]) {
184    AIRGoogleMapMarker *marker = (AIRGoogleMapMarker*)subview;
185    marker.realMarker.map = nil;
186    [self.markers removeObject:marker];
187  } else if ([subview isKindOfClass:[AIRGoogleMapPolygon class]]) {
188    AIRGoogleMapPolygon *polygon = (AIRGoogleMapPolygon*)subview;
189    polygon.polygon.map = nil;
190    [self.polygons removeObject:polygon];
191  } else if ([subview isKindOfClass:[AIRGoogleMapPolyline class]]) {
192    AIRGoogleMapPolyline *polyline = (AIRGoogleMapPolyline*)subview;
193    polyline.polyline.map = nil;
194    [self.polylines removeObject:polyline];
195  } else if ([subview isKindOfClass:[AIRGoogleMapCircle class]]) {
196    AIRGoogleMapCircle *circle = (AIRGoogleMapCircle*)subview;
197    circle.circle.map = nil;
198    [self.circles removeObject:circle];
199  } else if ([subview isKindOfClass:[AIRGoogleMapUrlTile class]]) {
200    AIRGoogleMapUrlTile *tile = (AIRGoogleMapUrlTile*)subview;
201    tile.tileLayer.map = nil;
202    [self.tiles removeObject:tile];
203  } else if ([subview isKindOfClass:[AIRGoogleMapWMSTile class]]) {
204    AIRGoogleMapWMSTile *tile = (AIRGoogleMapWMSTile*)subview;
205    tile.tileLayer.map = nil;
206    [self.tiles removeObject:tile];
207  } else if ([subview isKindOfClass:[AIRGoogleMapOverlay class]]) {
208    AIRGoogleMapOverlay *overlay = (AIRGoogleMapOverlay*)subview;
209    overlay.overlay.map = nil;
210    [self.overlays removeObject:overlay];
211  } else if ([subview isKindOfClass:[AIRGoogleMapHeatmap class]]){
212    AIRGoogleMapHeatmap *heatmap = (AIRGoogleMapHeatmap*)subview;
213    heatmap.heatmap.map = nil;
214    [self.heatmaps removeObject:heatmap];
215  } else {
216    NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
217    for (int i = 0; i < childSubviews.count; i++) {
218      [self removeReactSubview:(UIView *)childSubviews[i]];
219    }
220  }
221  [_reactSubviews removeObject:(UIView *)subview];
222}
223#pragma clang diagnostic pop
224
225#pragma clang diagnostic push
226#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
227- (NSArray<id<RCTComponent>> *)reactSubviews {
228  return _reactSubviews;
229}
230#pragma clang diagnostic pop
231
232- (NSArray *)getMapBoundaries
233{
234    GMSVisibleRegion visibleRegion = self.projection.visibleRegion;
235    GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion:visibleRegion];
236
237    CLLocationCoordinate2D northEast = bounds.northEast;
238    CLLocationCoordinate2D southWest = bounds.southWest;
239
240    return @[
241        @[
242            [NSNumber numberWithDouble:northEast.longitude],
243            [NSNumber numberWithDouble:northEast.latitude]
244        ],
245        @[
246            [NSNumber numberWithDouble:southWest.longitude],
247            [NSNumber numberWithDouble:southWest.latitude]
248        ]
249    ];
250}
251
252- (void)layoutSubviews {
253  [super layoutSubviews];
254  if(_didLayoutSubviews) return;
255  _didLayoutSubviews = true;
256
257  if (_initialCamera != nil) {
258    self.camera = _initialCamera;
259    _initialCameraSet = true;
260  }
261  else if (_initialRegion.span.latitudeDelta != 0.0 &&
262      _initialRegion.span.longitudeDelta != 0.0) {
263    self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:_initialRegion];
264    _initialRegionSet = true;
265  } else if (_region.span.latitudeDelta != 0.0 &&
266      _region.span.longitudeDelta != 0.0) {
267    self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:_region];
268  }
269}
270
271- (void)setInitialRegion:(MKCoordinateRegion)initialRegion {
272  _initialRegion = initialRegion;
273  if(!_initialRegionSet && _didLayoutSubviews){
274    self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:initialRegion];
275    _initialRegionSet = true;
276  }
277}
278
279- (void)setInitialCamera:(GMSCameraPosition*)initialCamera {
280    _initialCamera = initialCamera;
281    if(!_initialCameraSet && _didLayoutSubviews){
282      self.camera = initialCamera;
283      _initialCameraSet = true;
284    }
285}
286
287- (void)setRegion:(MKCoordinateRegion)region {
288  // TODO: The JS component is repeatedly setting region unnecessarily. We might want to deal with that in here.
289  _region = region;
290  if(_didLayoutSubviews) {
291    self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self  andMKCoordinateRegion:region];
292  }
293}
294
295- (void)setCameraProp:(GMSCameraPosition*)camera {
296    _initialCamera = camera;
297    self.camera = camera;
298}
299
300- (void)setOnMapReady:(RCTBubblingEventBlock)onMapReady {
301    _onMapReady = onMapReady;
302    if(!_didCallOnMapReady && _didPrepareMap) {
303      self.onMapReady(@{});
304      _didCallOnMapReady = true;
305    }
306}
307
308- (void)didPrepareMap {
309  UIView* mapView = [self valueForKey:@"mapView"]; //GMSVectorMapView
310  [self overrideGestureRecognizersForView:mapView];
311
312  if (!_didCallOnMapReady && self.onMapReady) {
313    self.onMapReady(@{});
314    _didCallOnMapReady = true;
315  }
316  _didPrepareMap = true;
317}
318
319- (void)mapViewDidFinishTileRendering {
320  if (self.onMapLoaded) self.onMapLoaded(@{});
321}
322
323- (BOOL)didTapMarker:(GMSMarker *)marker {
324  AIRGMSMarker *airMarker = (AIRGMSMarker *)marker;
325
326  id event = @{@"action": @"marker-press",
327               @"id": airMarker.identifier ?: @"unknown",
328               @"coordinate": @{
329                   @"latitude": @(airMarker.position.latitude),
330                   @"longitude": @(airMarker.position.longitude)
331                   }
332               };
333
334  if (airMarker.onPress) airMarker.onPress(event);
335  if (self.onMarkerPress) self.onMarkerPress(event);
336
337  // TODO: not sure why this is necessary
338  [self setSelectedMarker:marker];
339  return NO;
340}
341
342- (void)didTapPolyline:(GMSOverlay *)polyline {
343  AIRGMSPolyline *airPolyline = (AIRGMSPolyline *)polyline;
344
345  id event = @{@"action": @"polyline-press",
346               @"id": airPolyline.identifier ?: @"unknown",
347               };
348
349   if (airPolyline.onPress) airPolyline.onPress(event);
350}
351
352- (void)didTapPolygon:(GMSOverlay *)polygon {
353    AIRGMSPolygon *airPolygon = (AIRGMSPolygon *)polygon;
354
355    id event = @{@"action": @"polygon-press",
356                 @"id": airPolygon.identifier ?: @"unknown",
357                 };
358
359    if (airPolygon.onPress) airPolygon.onPress(event);
360}
361
362- (void)didTapAtCoordinate:(CLLocationCoordinate2D)coordinate {
363  if (!self.onPress) return;
364  self.onPress([self eventFromCoordinate:coordinate]);
365}
366
367- (void)didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate {
368  if (!self.onLongPress) return;
369  self.onLongPress([self eventFromCoordinate:coordinate]);
370}
371
372- (void)didChangeCameraPosition:(GMSCameraPosition *)position isGesture:(BOOL)isGesture{
373  id event = @{@"continuous": @YES,
374               @"region": regionAsJSON([AIRGoogleMap makeGMSCameraPositionFromMap:self andGMSCameraPosition:position]),
375               @"isGesture": [NSNumber numberWithBool:isGesture],
376               };
377
378  if (self.onChange) self.onChange(event);
379}
380
381- (void)didTapPOIWithPlaceID:(NSString *)placeID
382                        name:(NSString *)name
383                    location:(CLLocationCoordinate2D)location {
384  id event = @{@"placeId": placeID,
385               @"name": name,
386               @"coordinate": @{
387                   @"latitude": @(location.latitude),
388                   @"longitude": @(location.longitude)
389                   }
390               };
391
392  if (self.onPoiClick) self.onPoiClick(event);
393}
394
395- (void)idleAtCameraPosition:(GMSCameraPosition *)position  isGesture:(BOOL)isGesture{
396  id event = @{@"continuous": @NO,
397               @"region": regionAsJSON([AIRGoogleMap makeGMSCameraPositionFromMap:self andGMSCameraPosition:position]),
398               @"isGesture": [NSNumber numberWithBool:isGesture],
399               };
400  if (self.onChange) self.onChange(event);  // complete
401}
402
403- (void)setMapPadding:(UIEdgeInsets)mapPadding {
404  self.padding = mapPadding;
405}
406
407- (UIEdgeInsets)mapPadding {
408  return self.padding;
409}
410
411- (void)setPaddingAdjustmentBehaviorString:(NSString *)str
412{
413  if ([str isEqualToString:@"never"])
414  {
415    self.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorNever;
416  }
417  else if ([str isEqualToString:@"automatic"])
418  {
419    self.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorAutomatic;
420  }
421  else //if ([str isEqualToString:@"always"]) <-- default
422  {
423    self.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorAlways;
424  }
425}
426
427- (NSString *)paddingAdjustmentBehaviorString
428{
429  switch (self.paddingAdjustmentBehavior)
430  {
431    case kGMSMapViewPaddingAdjustmentBehaviorNever:
432      return @"never";
433    case kGMSMapViewPaddingAdjustmentBehaviorAutomatic:
434      return @"automatic";
435    case kGMSMapViewPaddingAdjustmentBehaviorAlways:
436      return @"always";
437
438    default:
439      return @"unknown";
440  }
441}
442
443- (void)setScrollEnabled:(BOOL)scrollEnabled {
444  self.settings.scrollGestures = scrollEnabled;
445}
446
447- (BOOL)scrollEnabled {
448  return self.settings.scrollGestures;
449}
450
451- (void)setZoomEnabled:(BOOL)zoomEnabled {
452  self.settings.zoomGestures = zoomEnabled;
453}
454
455- (BOOL)zoomEnabled {
456  return self.settings.zoomGestures;
457}
458
459- (void)setScrollDuringRotateOrZoomEnabled:(BOOL)enableScrollGesturesDuringRotateOrZoom {
460  self.settings.allowScrollGesturesDuringRotateOrZoom = enableScrollGesturesDuringRotateOrZoom;
461}
462
463- (BOOL)scrollDuringRotateOrZoomEnabled {
464  return self.settings.allowScrollGesturesDuringRotateOrZoom;
465}
466
467- (void)setZoomTapEnabled:(BOOL)zoomTapEnabled {
468    _zoomTapEnabled = zoomTapEnabled;
469}
470
471- (BOOL)zoomTapEnabled {
472    return _zoomTapEnabled;
473}
474
475- (void)setRotateEnabled:(BOOL)rotateEnabled {
476  self.settings.rotateGestures = rotateEnabled;
477}
478
479- (BOOL)rotateEnabled {
480  return self.settings.rotateGestures;
481}
482
483- (void)setPitchEnabled:(BOOL)pitchEnabled {
484  self.settings.tiltGestures = pitchEnabled;
485}
486
487- (BOOL)pitchEnabled {
488  return self.settings.tiltGestures;
489}
490
491- (void)setShowsTraffic:(BOOL)showsTraffic {
492  self.trafficEnabled = showsTraffic;
493}
494
495- (BOOL)showsTraffic {
496  return self.trafficEnabled;
497}
498
499- (void)setShowsBuildings:(BOOL)showsBuildings {
500  self.buildingsEnabled = showsBuildings;
501}
502
503- (BOOL)showsBuildings {
504  return self.buildingsEnabled;
505}
506
507- (void)setShowsCompass:(BOOL)showsCompass {
508  self.settings.compassButton = showsCompass;
509}
510
511- (void)setCustomMapStyleString:(NSString *)customMapStyleString {
512  NSError *error;
513
514  GMSMapStyle *style = [GMSMapStyle styleWithJSONString:customMapStyleString error:&error];
515
516  if (!style) {
517    NSLog(@"The style definition could not be loaded: %@", error);
518  }
519
520  self.mapStyle = style;
521}
522
523- (BOOL)showsCompass {
524  return self.settings.compassButton;
525}
526
527- (void)setShowsUserLocation:(BOOL)showsUserLocation {
528  self.myLocationEnabled = showsUserLocation;
529}
530
531- (BOOL)showsUserLocation {
532  return self.myLocationEnabled;
533}
534
535- (void)setShowsMyLocationButton:(BOOL)showsMyLocationButton {
536  self.settings.myLocationButton = showsMyLocationButton;
537}
538
539- (BOOL)showsMyLocationButton {
540  return self.settings.myLocationButton;
541}
542
543- (void)setMinZoomLevel:(CGFloat)minZoomLevel {
544  [self setMinZoom:minZoomLevel maxZoom:self.maxZoom ];
545}
546
547- (void)setMaxZoomLevel:(CGFloat)maxZoomLevel {
548  [self setMinZoom:self.minZoom maxZoom:maxZoomLevel ];
549}
550
551- (void)setShowsIndoors:(BOOL)showsIndoors {
552  self.indoorEnabled = showsIndoors;
553}
554
555- (BOOL)showsIndoors {
556  return self.indoorEnabled;
557}
558
559- (void)setShowsIndoorLevelPicker:(BOOL)showsIndoorLevelPicker {
560  self.settings.indoorPicker = showsIndoorLevelPicker;
561}
562
563- (BOOL)showsIndoorLevelPicker {
564  return self.settings.indoorPicker;
565}
566
567+ (MKCoordinateRegion) makeGMSCameraPositionFromMap:(GMSMapView *)map andGMSCameraPosition:(GMSCameraPosition *)position {
568  // solution from here: http://stackoverflow.com/a/16587735/1102215
569  GMSVisibleRegion visibleRegion = map.projection.visibleRegion;
570  GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion: visibleRegion];
571  CLLocationCoordinate2D center;
572  CLLocationDegrees longitudeDelta;
573  CLLocationDegrees latitudeDelta = bounds.northEast.latitude - bounds.southWest.latitude;
574
575  if(bounds.northEast.longitude >= bounds.southWest.longitude) {
576    //Standard case
577    center = CLLocationCoordinate2DMake((bounds.southWest.latitude + bounds.northEast.latitude) / 2,
578                                        (bounds.southWest.longitude + bounds.northEast.longitude) / 2);
579    longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude;
580  } else {
581    //Region spans the international dateline
582    center = CLLocationCoordinate2DMake((bounds.southWest.latitude + bounds.northEast.latitude) / 2,
583                                        (bounds.southWest.longitude + bounds.northEast.longitude + 360) / 2);
584    longitudeDelta = bounds.northEast.longitude + 360 - bounds.southWest.longitude;
585  }
586  MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
587  return MKCoordinateRegionMake(center, span);
588}
589
590+ (GMSCameraPosition*) makeGMSCameraPositionFromMap:(GMSMapView *)map andMKCoordinateRegion:(MKCoordinateRegion)region {
591  float latitudeDelta = region.span.latitudeDelta * 0.5;
592  float longitudeDelta = region.span.longitudeDelta * 0.5;
593
594  CLLocationCoordinate2D a = CLLocationCoordinate2DMake(region.center.latitude + latitudeDelta,
595                                                        region.center.longitude + longitudeDelta);
596  CLLocationCoordinate2D b = CLLocationCoordinate2DMake(region.center.latitude - latitudeDelta,
597                                                        region.center.longitude - longitudeDelta);
598  GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:a coordinate:b];
599  return [map cameraForBounds:bounds insets:UIEdgeInsetsZero];
600}
601
602#pragma mark - Utils
603
604- (CGRect) frameForMarker:(AIRGoogleMapMarker*) mrkView {
605    CGPoint mrkAnchor = mrkView.realMarker.groundAnchor;
606    CGPoint mrkPoint = [self.projection pointForCoordinate:mrkView.coordinate];
607    CGSize mrkSize = mrkView.realMarker.iconView ? mrkView.realMarker.iconView.bounds.size : CGSizeMake(20, 30);
608    CGRect mrkFrame = CGRectMake(mrkPoint.x, mrkPoint.y, mrkSize.width, mrkSize.height);
609    mrkFrame.origin.y -= mrkAnchor.y * mrkSize.height;
610    mrkFrame.origin.x -= mrkAnchor.x * mrkSize.width;
611    return mrkFrame;
612}
613
614- (NSDictionary*) getMarkersFramesWithOnlyVisible:(BOOL)onlyVisible {
615    NSMutableDictionary* markersFrames = [NSMutableDictionary new];
616    for (AIRGoogleMapMarker* mrkView in self.markers) {
617        CGRect frame = [self frameForMarker:mrkView];
618        CGPoint point = [self.projection pointForCoordinate:mrkView.coordinate];
619        NSDictionary* frameDict = @{
620                                    @"x": @(frame.origin.x),
621                                    @"y": @(frame.origin.y),
622                                    @"width": @(frame.size.width),
623                                    @"height": @(frame.size.height)
624                                    };
625        NSDictionary* pointDict = @{
626                                    @"x": @(point.x),
627                                    @"y": @(point.y)
628                                    };
629        NSString* k = mrkView.identifier;
630        BOOL isVisible = CGRectIntersectsRect(self.bounds, frame);
631        if (k != nil && (!onlyVisible || isVisible)) {
632            [markersFrames setObject:@{ @"frame": frameDict, @"point": pointDict } forKey:k];
633        }
634    }
635    return markersFrames;
636}
637
638- (AIRGoogleMapMarker*) markerAtPoint:(CGPoint)point {
639    AIRGoogleMapMarker* mrk = nil;
640    for (AIRGoogleMapMarker* mrkView in self.markers) {
641        CGRect frame = [self frameForMarker:mrkView];
642        if (CGRectContainsPoint(frame, point)) {
643            mrk = mrkView;
644            break;
645        }
646    }
647    return mrk;
648}
649
650-(SEL)getActionForTarget:(NSObject*)target {
651    SEL action = nil;
652    uint32_t ivarCount;
653    Ivar *ivars = class_copyIvarList([target class], &ivarCount);
654    if (ivars) {
655        for (uint32_t i = 0 ; i < ivarCount ; i++) {
656            Ivar ivar = ivars[i];
657            const char* type = ivar_getTypeEncoding(ivar);
658            const char* ivarName = ivar_getName(ivar);
659            NSString* name = [NSString stringWithCString: ivarName encoding: NSASCIIStringEncoding];
660            if (type[0] == ':' && [name isEqualToString:@"_action"]) {
661                SEL sel = ((SEL (*)(id, Ivar))object_getIvar)(target, ivar);
662                action = sel;
663                break;
664            }
665        }
666    }
667    free(ivars);
668    return action;
669}
670
671#pragma mark - Overrides for Callout behavior
672
673-(void)overrideGestureRecognizersForView:(UIView*)view {
674    NSArray* grs = view.gestureRecognizers;
675    for (UIGestureRecognizer* gestureRecognizer in grs) {
676        NSNumber* grHash = [NSNumber numberWithUnsignedInteger:gestureRecognizer.hash];
677        if([self.origGestureRecognizersMeta objectForKey:grHash] != nil)
678            continue; //already patched
679
680        //get original handlers
681        NSArray* origTargets = [gestureRecognizer valueForKey:@"targets"];
682        NSMutableArray* origTargetsActions = [[NSMutableArray alloc] init];
683        BOOL isZoomTapGesture = NO;
684        for (NSObject* trg in origTargets) {
685            NSObject* target = [trg valueForKey:@"target"];
686            SEL action = [self getActionForTarget:trg];
687            isZoomTapGesture = [NSStringFromSelector(action) isEqualToString:@"handleZoomTapGesture:"];
688            [origTargetsActions addObject:@{
689                                            @"target": [NSValue valueWithNonretainedObject:target],
690                                            @"action": NSStringFromSelector(action)
691                                            }];
692        }
693        if (isZoomTapGesture && self.zoomTapEnabled == NO) {
694            [view removeGestureRecognizer:gestureRecognizer];
695            continue;
696        }
697
698        //replace with extendedMapGestureHandler
699        for (NSDictionary* origTargetAction in origTargetsActions) {
700            NSValue* targetValue = [origTargetAction objectForKey:@"target"];
701            NSObject* target = [targetValue nonretainedObjectValue];
702            NSString* actionString = [origTargetAction objectForKey:@"action"];
703            SEL action = NSSelectorFromString(actionString);
704            [gestureRecognizer removeTarget:target action:action];
705        }
706        [gestureRecognizer addTarget:self action:@selector(extendedMapGestureHandler:)];
707
708        [self.origGestureRecognizersMeta setObject:@{@"targets": origTargetsActions}
709                                            forKey:grHash];
710    }
711}
712
713- (id)extendedMapGestureHandler:(UIGestureRecognizer*)gestureRecognizer {
714    NSNumber* grHash = [NSNumber numberWithUnsignedInteger:gestureRecognizer.hash];
715    UIWindow* win = [[[UIApplication sharedApplication] windows] firstObject];
716    NSObject* bubbleProvider = [self valueForKey:@"bubbleProvider"]; //GMSbubbleEntityProvider
717    CGRect bubbleAbsoluteFrame = [bubbleProvider accessibilityFrame];
718    CGRect bubbleFrame = [win convertRect:bubbleAbsoluteFrame toView:self];
719    UIView* bubbleView = [bubbleProvider valueForKey:@"view"];
720
721    BOOL performOriginalActions = YES;
722    BOOL isTap = [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] || [gestureRecognizer isMemberOfClass:[UITapGestureRecognizer class]];
723    if (isTap) {
724        BOOL isTapInsideBubble = NO;
725    CGPoint tapPoint = CGPointZero;
726    CGPoint tapPointInBubble = CGPointZero;
727
728    NSArray* touches = [gestureRecognizer valueForKey:@"touches"];
729    UITouch* oneTouch = [touches firstObject];
730    NSArray* delayedTouches = [gestureRecognizer valueForKey:@"delayedTouches"];
731    NSObject* delayedTouch = [delayedTouches firstObject]; //UIGestureDeleayedTouch
732    UITouch* tapTouch = [delayedTouch valueForKey:@"stateWhenDelayed"];
733    if (!tapTouch)
734        tapTouch = oneTouch;
735        tapPoint = [tapTouch locationInView:self];
736        isTapInsideBubble = tapTouch != nil && CGRectContainsPoint(bubbleFrame, tapPoint);
737        if (isTapInsideBubble) {
738            tapPointInBubble = CGPointMake(tapPoint.x - bubbleFrame.origin.x, tapPoint.y - bubbleFrame.origin.y);
739        }
740        if (isTapInsideBubble) {
741            //find bubble's marker
742            AIRGoogleMapMarker* markerView = nil;
743            AIRGMSMarker* marker = nil;
744            for (AIRGoogleMapMarker* mrk in self.markers) {
745                if ([mrk.calloutView isEqual:bubbleView]) {
746                    markerView = mrk;
747                    marker = markerView.realMarker;
748                    break;
749                }
750            }
751
752            //find real tap target subview
753            UIView* realSubview = [(RCTView*)bubbleView hitTest:tapPointInBubble withEvent:nil];
754            AIRGoogleMapCalloutSubview* realPressableSubview = nil;
755            if (realSubview) {
756                UIView* tmp = realSubview;
757                while (tmp && tmp != win && tmp != bubbleView) {
758                    if ([tmp respondsToSelector:@selector(onPress)]) {
759                        realPressableSubview = (AIRGoogleMapCalloutSubview*) tmp;
760                        break;
761                    }
762                    tmp = tmp.superview;
763                }
764            }
765
766            if (markerView) {
767                BOOL isInsideCallout = [markerView.calloutView isPointInside:tapPointInBubble];
768                if (isInsideCallout) {
769                    [markerView didTapInfoWindowOfMarker:marker subview:realPressableSubview point:tapPointInBubble frame:bubbleFrame];
770                } else {
771                    AIRGoogleMapMarker* markerAtTapPoint = [self markerAtPoint:tapPoint];
772                    if (markerAtTapPoint != nil) {
773                        [self didTapMarker:markerAtTapPoint.realMarker];
774                    } else {
775                        CLLocationCoordinate2D coord = [self.projection coordinateForPoint:tapPoint];
776                        [markerView hideCalloutView];
777                        [self didTapAtCoordinate:coord];
778                    }
779                }
780
781                performOriginalActions = NO;
782            }
783        }
784    }
785
786    if (performOriginalActions) {
787        NSDictionary* origMeta = [self.origGestureRecognizersMeta objectForKey:grHash];
788        NSDictionary* origTargets = [origMeta objectForKey:@"targets"];
789        for (NSDictionary* origTarget in origTargets) {
790            NSValue* targetValue = [origTarget objectForKey:@"target"];
791            NSObject* target = [targetValue nonretainedObjectValue];
792            NSString* actionString = [origTarget objectForKey:@"action"];
793            SEL action = NSSelectorFromString(actionString);
794#pragma clang diagnostic push
795#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
796            [target performSelector:action withObject:gestureRecognizer];
797#pragma clang diagnostic pop
798        }
799    }
800
801    return nil;
802}
803
804
805#pragma mark - KVO updates
806
807- (void)observeValueForKeyPath:(NSString *)keyPath
808                      ofObject:(id)object
809                        change:(NSDictionary *)change
810                       context:(void *)context {
811  if ([keyPath isEqualToString:@"myLocation"]){
812    CLLocation *location = [object myLocation];
813
814    id event = @{@"coordinate": @{
815                    @"latitude": @(location.coordinate.latitude),
816                    @"longitude": @(location.coordinate.longitude),
817                    @"altitude": @(location.altitude),
818                    @"timestamp": @(location.timestamp.timeIntervalSinceReferenceDate * 1000),
819                    @"accuracy": @(location.horizontalAccuracy),
820                    @"altitudeAccuracy": @(location.verticalAccuracy),
821                    @"speed": @(location.speed),
822                    @"heading": @(location.course),
823                    }
824                };
825
826  if (self.onUserLocationChange) self.onUserLocationChange(event);
827  } else {
828    // This message is not for me; pass it on to super.
829    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
830  }
831}
832
833+ (NSString *)GetIconUrl:(GMUPlacemark *) marker parser:(GMUKMLParser *) parser {
834#ifdef HAVE_GOOGLE_MAPS_UTILS
835  if (marker.style.styleID != nil) {
836    for (GMUStyle *style in parser.styles) {
837      if (style.styleID == marker.style.styleID) {
838        return style.iconUrl;
839      }
840    }
841  }
842
843  return marker.style.iconUrl;
844#else
845    REQUIRES_GOOGLE_MAPS_UTILS("GetIconUrl:parser:"); return @"";
846#endif
847}
848
849- (NSString *)KmlSrc {
850  return _kmlSrc;
851}
852
853- (void)setKmlSrc:(NSString *)kmlUrl {
854#ifdef HAVE_GOOGLE_MAPS_UTILS
855
856  _kmlSrc = kmlUrl;
857
858  NSURL *url = [NSURL URLWithString:kmlUrl];
859  NSData *urlData = nil;
860
861  if ([url isFileURL]) {
862    urlData = [NSData dataWithContentsOfURL:url];
863  } else {
864    urlData = [[NSFileManager defaultManager] contentsAtPath:kmlUrl];
865  }
866
867  GMUKMLParser *parser = [[GMUKMLParser alloc] initWithData:urlData];
868  [parser parse];
869
870  NSUInteger index = 0;
871  NSMutableArray *markers = [[NSMutableArray alloc]init];
872
873  for (GMUPlacemark *place in parser.placemarks) {
874
875    CLLocationCoordinate2D location =((GMUPoint *) place.geometry).coordinate;
876
877    AIRGoogleMapMarker *marker = (AIRGoogleMapMarker *)[[AIRGoogleMapMarkerManager alloc] view];
878    if (!marker.bridge) {
879      marker.bridge = _bridge;
880    }
881    marker.identifier = place.title;
882    marker.coordinate = location;
883    marker.title = place.title;
884    marker.subtitle = place.snippet;
885    marker.pinColor = place.style.fillColor;
886    marker.imageSrc = [AIRGoogleMap GetIconUrl:place parser:parser];
887    marker.layer.backgroundColor = [UIColor clearColor].CGColor;
888    marker.layer.position = CGPointZero;
889
890    [self insertReactSubview:(UIView *) marker atIndex:index];
891
892    [markers addObject:@{@"id": marker.identifier,
893                         @"title": marker.title,
894                         @"description": marker.subtitle,
895                         @"coordinate": @{
896                             @"latitude": @(location.latitude),
897                             @"longitude": @(location.longitude)
898                             }
899                         }];
900
901    index++;
902  }
903
904  id event = @{@"markers": markers};
905  if (self.onKmlReady) self.onKmlReady(event);
906#else
907    REQUIRES_GOOGLE_MAPS_UTILS();
908#endif
909}
910
911
912- (void) didChangeActiveBuilding: (nullable GMSIndoorBuilding *) building {
913    if (!building) {
914        if (!self.onIndoorBuildingFocused) {
915            return;
916        }
917        self.onIndoorBuildingFocused(@{
918            @"IndoorBuilding": @{
919                    @"activeLevelIndex": @0,
920                    @"underground": @false,
921                    @"levels": [[NSMutableArray alloc]init]
922            }
923        });
924    }
925    NSInteger i = 0;
926    NSMutableArray *arrayLevels = [[NSMutableArray alloc]init];
927    for (GMSIndoorLevel *level in building.levels) {
928        [arrayLevels addObject: @{
929            @"index": @(i),
930            @"name" : level.name,
931            @"shortName" : level.shortName,
932        }];
933        i++;
934    }
935    if (!self.onIndoorBuildingFocused) {
936        return;
937    }
938    self.onIndoorBuildingFocused(@{
939        @"IndoorBuilding": @{
940                @"activeLevelIndex": @(building.defaultLevelIndex),
941                @"underground": @(building.underground),
942                @"levels": arrayLevels
943        }
944    });
945}
946
947- (void) didChangeActiveLevel: (nullable GMSIndoorLevel *)     level {
948    if (!self.onIndoorLevelActivated || !self.indoorDisplay  || !level) {
949        return;
950    }
951    NSInteger i = 0;
952    for (GMSIndoorLevel *buildingLevel in self.indoorDisplay.activeBuilding.levels) {
953        if (buildingLevel.name == level.name && buildingLevel.shortName == level.shortName) {
954            break;
955        }
956        i++;
957    }
958    self.onIndoorLevelActivated(@{
959        @"IndoorLevel": @{
960                @"activeLevelIndex": @(i),
961                @"name": level.name,
962                @"shortName": level.shortName
963        }
964    });
965}
966
967
968@end
969
970#endif
971