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