// // AIRGoogleMap.m // AirMaps // // Created by Gil Birman on 9/1/16. // #ifdef HAVE_GOOGLE_MAPS #import "AIRGoogleMap.h" #import "AIRGoogleMapMarker.h" #import "AIRGoogleMapMarkerManager.h" #import "AIRGoogleMapPolygon.h" #import "AIRGoogleMapPolyline.h" #import "AIRGoogleMapCircle.h" #import "AIRGoogleMapHeatmap.h" #import "AIRGoogleMapUrlTile.h" #import "AIRGoogleMapWMSTile.h" #import "AIRGoogleMapOverlay.h" #import #import #import #import #import "RCTConvert+AirMap.h" #import #ifdef HAVE_GOOGLE_MAPS_UTILS #import #import #import #import #define REQUIRES_GOOGLE_MAPS_UTILS(feature) do {} while (0) #else #define GMUKMLParser void #define GMUPlacemark void #define REQUIRES_GOOGLE_MAPS_UTILS(feature) do { \ [NSException raise:@"ReactNativeMapsDependencyMissing" \ format:@"Use of " feature "requires Google-Maps-iOS-Utils, you must install via CocoaPods to use this feature"]; \ } while (0) #endif id regionAsJSON(MKCoordinateRegion region) { return @{ @"latitude": [NSNumber numberWithDouble:region.center.latitude], @"longitude": [NSNumber numberWithDouble:region.center.longitude], @"latitudeDelta": [NSNumber numberWithDouble:region.span.latitudeDelta], @"longitudeDelta": [NSNumber numberWithDouble:region.span.longitudeDelta], }; } @interface AIRGoogleMap () - (id)eventFromCoordinate:(CLLocationCoordinate2D)coordinate; @property (nonatomic, strong) NSMutableDictionary *origGestureRecognizersMeta; @end @implementation AIRGoogleMap { NSMutableArray *_reactSubviews; MKCoordinateRegion _initialRegion; MKCoordinateRegion _region; BOOL _initialRegionSet; BOOL _initialCameraSet; BOOL _didLayoutSubviews; BOOL _didPrepareMap; BOOL _didCallOnMapReady; BOOL _zoomTapEnabled; } - (instancetype)init { if ((self = [super init])) { _reactSubviews = [NSMutableArray new]; _markers = [NSMutableArray array]; _polygons = [NSMutableArray array]; _polylines = [NSMutableArray array]; _circles = [NSMutableArray array]; _heatmaps = [NSMutableArray array]; _tiles = [NSMutableArray array]; _overlays = [NSMutableArray array]; _initialCamera = nil; _cameraProp = nil; _initialRegion = MKCoordinateRegionMake(CLLocationCoordinate2DMake(0.0, 0.0), MKCoordinateSpanMake(0.0, 0.0)); _region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(0.0, 0.0), MKCoordinateSpanMake(0.0, 0.0)); _initialRegionSet = false; _initialCameraSet = false; _didLayoutSubviews = false; _didPrepareMap = false; _didCallOnMapReady = false; _zoomTapEnabled = YES; // Listen to the myLocation property of GMSMapView. [self addObserver:self forKeyPath:@"myLocation" options:NSKeyValueObservingOptionNew context:NULL]; self.origGestureRecognizersMeta = [[NSMutableDictionary alloc] init]; self.indoorDisplay.delegate = self; } return self; } - (void)dealloc { [self removeObserver:self forKeyPath:@"myLocation" context:NULL]; } - (id)eventFromCoordinate:(CLLocationCoordinate2D)coordinate { CGPoint touchPoint = [self.projection pointForCoordinate:coordinate]; return @{ @"coordinate": @{ @"latitude": @(coordinate.latitude), @"longitude": @(coordinate.longitude), }, @"position": @{ @"x": @(touchPoint.x), @"y": @(touchPoint.y), }, }; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-missing-super-calls" - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex { // Our desired API is to pass up markers/overlays as children to the mapview component. // This is where we intercept them and do the appropriate underlying mapview action. if ([subview isKindOfClass:[AIRGoogleMapMarker class]]) { AIRGoogleMapMarker *marker = (AIRGoogleMapMarker*)subview; marker.realMarker.map = self; [self.markers addObject:marker]; } else if ([subview isKindOfClass:[AIRGoogleMapPolygon class]]) { AIRGoogleMapPolygon *polygon = (AIRGoogleMapPolygon*)subview; polygon.polygon.map = self; [self.polygons addObject:polygon]; } else if ([subview isKindOfClass:[AIRGoogleMapPolyline class]]) { AIRGoogleMapPolyline *polyline = (AIRGoogleMapPolyline*)subview; polyline.polyline.map = self; [self.polylines addObject:polyline]; } else if ([subview isKindOfClass:[AIRGoogleMapCircle class]]) { AIRGoogleMapCircle *circle = (AIRGoogleMapCircle*)subview; circle.circle.map = self; [self.circles addObject:circle]; } else if ([subview isKindOfClass:[AIRGoogleMapUrlTile class]]) { AIRGoogleMapUrlTile *tile = (AIRGoogleMapUrlTile*)subview; tile.tileLayer.map = self; [self.tiles addObject:tile]; } else if ([subview isKindOfClass:[AIRGoogleMapWMSTile class]]) { AIRGoogleMapWMSTile *tile = (AIRGoogleMapWMSTile*)subview; tile.tileLayer.map = self; [self.tiles addObject:tile]; } else if ([subview isKindOfClass:[AIRGoogleMapOverlay class]]) { AIRGoogleMapOverlay *overlay = (AIRGoogleMapOverlay*)subview; overlay.overlay.map = self; [self.overlays addObject:overlay]; } else if ([subview isKindOfClass:[AIRGoogleMapHeatmap class]]){ AIRGoogleMapHeatmap *heatmap = (AIRGoogleMapHeatmap*)subview; heatmap.heatmap.map = self; [self.heatmaps addObject:heatmap]; } else { NSArray> *childSubviews = [subview reactSubviews]; for (int i = 0; i < childSubviews.count; i++) { [self insertReactSubview:(UIView *)childSubviews[i] atIndex:atIndex]; } } [_reactSubviews insertObject:(UIView *)subview atIndex:(NSUInteger) atIndex]; } #pragma clang diagnostic pop #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-missing-super-calls" - (void)removeReactSubview:(id)subview { // similarly, when the children are being removed we have to do the appropriate // underlying mapview action here. if ([subview isKindOfClass:[AIRGoogleMapMarker class]]) { AIRGoogleMapMarker *marker = (AIRGoogleMapMarker*)subview; marker.realMarker.map = nil; [self.markers removeObject:marker]; } else if ([subview isKindOfClass:[AIRGoogleMapPolygon class]]) { AIRGoogleMapPolygon *polygon = (AIRGoogleMapPolygon*)subview; polygon.polygon.map = nil; [self.polygons removeObject:polygon]; } else if ([subview isKindOfClass:[AIRGoogleMapPolyline class]]) { AIRGoogleMapPolyline *polyline = (AIRGoogleMapPolyline*)subview; polyline.polyline.map = nil; [self.polylines removeObject:polyline]; } else if ([subview isKindOfClass:[AIRGoogleMapCircle class]]) { AIRGoogleMapCircle *circle = (AIRGoogleMapCircle*)subview; circle.circle.map = nil; [self.circles removeObject:circle]; } else if ([subview isKindOfClass:[AIRGoogleMapUrlTile class]]) { AIRGoogleMapUrlTile *tile = (AIRGoogleMapUrlTile*)subview; tile.tileLayer.map = nil; [self.tiles removeObject:tile]; } else if ([subview isKindOfClass:[AIRGoogleMapWMSTile class]]) { AIRGoogleMapWMSTile *tile = (AIRGoogleMapWMSTile*)subview; tile.tileLayer.map = nil; [self.tiles removeObject:tile]; } else if ([subview isKindOfClass:[AIRGoogleMapOverlay class]]) { AIRGoogleMapOverlay *overlay = (AIRGoogleMapOverlay*)subview; overlay.overlay.map = nil; [self.overlays removeObject:overlay]; } else if ([subview isKindOfClass:[AIRGoogleMapHeatmap class]]){ AIRGoogleMapHeatmap *heatmap = (AIRGoogleMapHeatmap*)subview; heatmap.heatmap.map = nil; [self.heatmaps removeObject:heatmap]; } else { NSArray> *childSubviews = [subview reactSubviews]; for (int i = 0; i < childSubviews.count; i++) { [self removeReactSubview:(UIView *)childSubviews[i]]; } } [_reactSubviews removeObject:(UIView *)subview]; } #pragma clang diagnostic pop #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-missing-super-calls" - (NSArray> *)reactSubviews { return _reactSubviews; } #pragma clang diagnostic pop - (NSArray *)getMapBoundaries { GMSVisibleRegion visibleRegion = self.projection.visibleRegion; GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion:visibleRegion]; CLLocationCoordinate2D northEast = bounds.northEast; CLLocationCoordinate2D southWest = bounds.southWest; return @[ @[ [NSNumber numberWithDouble:northEast.longitude], [NSNumber numberWithDouble:northEast.latitude] ], @[ [NSNumber numberWithDouble:southWest.longitude], [NSNumber numberWithDouble:southWest.latitude] ] ]; } - (void)layoutSubviews { [super layoutSubviews]; if(_didLayoutSubviews) return; _didLayoutSubviews = true; if (_initialCamera != nil) { self.camera = _initialCamera; _initialCameraSet = true; } else if (_initialRegion.span.latitudeDelta != 0.0 && _initialRegion.span.longitudeDelta != 0.0) { self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:_initialRegion]; _initialRegionSet = true; } else if (_region.span.latitudeDelta != 0.0 && _region.span.longitudeDelta != 0.0) { self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:_region]; } } - (void)setInitialRegion:(MKCoordinateRegion)initialRegion { _initialRegion = initialRegion; if(!_initialRegionSet && _didLayoutSubviews){ self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:initialRegion]; _initialRegionSet = true; } } - (void)setInitialCamera:(GMSCameraPosition*)initialCamera { _initialCamera = initialCamera; if(!_initialCameraSet && _didLayoutSubviews){ self.camera = initialCamera; _initialCameraSet = true; } } - (void)setRegion:(MKCoordinateRegion)region { // TODO: The JS component is repeatedly setting region unnecessarily. We might want to deal with that in here. _region = region; if(_didLayoutSubviews) { self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:region]; } } - (void)setCameraProp:(GMSCameraPosition*)camera { _initialCamera = camera; self.camera = camera; } - (void)setOnMapReady:(RCTBubblingEventBlock)onMapReady { _onMapReady = onMapReady; if(!_didCallOnMapReady && _didPrepareMap) { self.onMapReady(@{}); _didCallOnMapReady = true; } } - (void)didPrepareMap { UIView* mapView = [self valueForKey:@"mapView"]; //GMSVectorMapView [self overrideGestureRecognizersForView:mapView]; if (!_didCallOnMapReady && self.onMapReady) { self.onMapReady(@{}); _didCallOnMapReady = true; } _didPrepareMap = true; } - (void)mapViewDidFinishTileRendering { if (self.onMapLoaded) self.onMapLoaded(@{}); } - (BOOL)didTapMarker:(GMSMarker *)marker { AIRGMSMarker *airMarker = (AIRGMSMarker *)marker; id event = @{@"action": @"marker-press", @"id": airMarker.identifier ?: @"unknown", @"coordinate": @{ @"latitude": @(airMarker.position.latitude), @"longitude": @(airMarker.position.longitude) } }; if (airMarker.onPress) airMarker.onPress(event); if (self.onMarkerPress) self.onMarkerPress(event); // TODO: not sure why this is necessary [self setSelectedMarker:marker]; return NO; } - (void)didTapPolyline:(GMSOverlay *)polyline { AIRGMSPolyline *airPolyline = (AIRGMSPolyline *)polyline; id event = @{@"action": @"polyline-press", @"id": airPolyline.identifier ?: @"unknown", }; if (airPolyline.onPress) airPolyline.onPress(event); } - (void)didTapPolygon:(GMSOverlay *)polygon { AIRGMSPolygon *airPolygon = (AIRGMSPolygon *)polygon; id event = @{@"action": @"polygon-press", @"id": airPolygon.identifier ?: @"unknown", }; if (airPolygon.onPress) airPolygon.onPress(event); } - (void)didTapAtCoordinate:(CLLocationCoordinate2D)coordinate { if (!self.onPress) return; self.onPress([self eventFromCoordinate:coordinate]); } - (void)didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate { if (!self.onLongPress) return; self.onLongPress([self eventFromCoordinate:coordinate]); } - (void)didChangeCameraPosition:(GMSCameraPosition *)position isGesture:(BOOL)isGesture{ id event = @{@"continuous": @YES, @"region": regionAsJSON([AIRGoogleMap makeGMSCameraPositionFromMap:self andGMSCameraPosition:position]), @"isGesture": [NSNumber numberWithBool:isGesture], }; if (self.onChange) self.onChange(event); } - (void)didTapPOIWithPlaceID:(NSString *)placeID name:(NSString *)name location:(CLLocationCoordinate2D)location { id event = @{@"placeId": placeID, @"name": name, @"coordinate": @{ @"latitude": @(location.latitude), @"longitude": @(location.longitude) } }; if (self.onPoiClick) self.onPoiClick(event); } - (void)idleAtCameraPosition:(GMSCameraPosition *)position isGesture:(BOOL)isGesture{ id event = @{@"continuous": @NO, @"region": regionAsJSON([AIRGoogleMap makeGMSCameraPositionFromMap:self andGMSCameraPosition:position]), @"isGesture": [NSNumber numberWithBool:isGesture], }; if (self.onChange) self.onChange(event); // complete } - (void)setMapPadding:(UIEdgeInsets)mapPadding { self.padding = mapPadding; } - (UIEdgeInsets)mapPadding { return self.padding; } - (void)setPaddingAdjustmentBehaviorString:(NSString *)str { if ([str isEqualToString:@"never"]) { self.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorNever; } else if ([str isEqualToString:@"automatic"]) { self.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorAutomatic; } else //if ([str isEqualToString:@"always"]) <-- default { self.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorAlways; } } - (NSString *)paddingAdjustmentBehaviorString { switch (self.paddingAdjustmentBehavior) { case kGMSMapViewPaddingAdjustmentBehaviorNever: return @"never"; case kGMSMapViewPaddingAdjustmentBehaviorAutomatic: return @"automatic"; case kGMSMapViewPaddingAdjustmentBehaviorAlways: return @"always"; default: return @"unknown"; } } - (void)setScrollEnabled:(BOOL)scrollEnabled { self.settings.scrollGestures = scrollEnabled; } - (BOOL)scrollEnabled { return self.settings.scrollGestures; } - (void)setZoomEnabled:(BOOL)zoomEnabled { self.settings.zoomGestures = zoomEnabled; } - (BOOL)zoomEnabled { return self.settings.zoomGestures; } - (void)setScrollDuringRotateOrZoomEnabled:(BOOL)enableScrollGesturesDuringRotateOrZoom { self.settings.allowScrollGesturesDuringRotateOrZoom = enableScrollGesturesDuringRotateOrZoom; } - (BOOL)scrollDuringRotateOrZoomEnabled { return self.settings.allowScrollGesturesDuringRotateOrZoom; } - (void)setZoomTapEnabled:(BOOL)zoomTapEnabled { _zoomTapEnabled = zoomTapEnabled; } - (BOOL)zoomTapEnabled { return _zoomTapEnabled; } - (void)setRotateEnabled:(BOOL)rotateEnabled { self.settings.rotateGestures = rotateEnabled; } - (BOOL)rotateEnabled { return self.settings.rotateGestures; } - (void)setPitchEnabled:(BOOL)pitchEnabled { self.settings.tiltGestures = pitchEnabled; } - (BOOL)pitchEnabled { return self.settings.tiltGestures; } - (void)setShowsTraffic:(BOOL)showsTraffic { self.trafficEnabled = showsTraffic; } - (BOOL)showsTraffic { return self.trafficEnabled; } - (void)setShowsBuildings:(BOOL)showsBuildings { self.buildingsEnabled = showsBuildings; } - (BOOL)showsBuildings { return self.buildingsEnabled; } - (void)setShowsCompass:(BOOL)showsCompass { self.settings.compassButton = showsCompass; } - (void)setCustomMapStyleString:(NSString *)customMapStyleString { NSError *error; GMSMapStyle *style = [GMSMapStyle styleWithJSONString:customMapStyleString error:&error]; if (!style) { NSLog(@"The style definition could not be loaded: %@", error); } self.mapStyle = style; } - (BOOL)showsCompass { return self.settings.compassButton; } - (void)setShowsUserLocation:(BOOL)showsUserLocation { self.myLocationEnabled = showsUserLocation; } - (BOOL)showsUserLocation { return self.myLocationEnabled; } - (void)setShowsMyLocationButton:(BOOL)showsMyLocationButton { self.settings.myLocationButton = showsMyLocationButton; } - (BOOL)showsMyLocationButton { return self.settings.myLocationButton; } - (void)setMinZoomLevel:(CGFloat)minZoomLevel { [self setMinZoom:minZoomLevel maxZoom:self.maxZoom ]; } - (void)setMaxZoomLevel:(CGFloat)maxZoomLevel { [self setMinZoom:self.minZoom maxZoom:maxZoomLevel ]; } - (void)setShowsIndoors:(BOOL)showsIndoors { self.indoorEnabled = showsIndoors; } - (BOOL)showsIndoors { return self.indoorEnabled; } - (void)setShowsIndoorLevelPicker:(BOOL)showsIndoorLevelPicker { self.settings.indoorPicker = showsIndoorLevelPicker; } - (BOOL)showsIndoorLevelPicker { return self.settings.indoorPicker; } + (MKCoordinateRegion) makeGMSCameraPositionFromMap:(GMSMapView *)map andGMSCameraPosition:(GMSCameraPosition *)position { // solution from here: http://stackoverflow.com/a/16587735/1102215 GMSVisibleRegion visibleRegion = map.projection.visibleRegion; GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion: visibleRegion]; CLLocationCoordinate2D center; CLLocationDegrees longitudeDelta; CLLocationDegrees latitudeDelta = bounds.northEast.latitude - bounds.southWest.latitude; if(bounds.northEast.longitude >= bounds.southWest.longitude) { //Standard case center = CLLocationCoordinate2DMake((bounds.southWest.latitude + bounds.northEast.latitude) / 2, (bounds.southWest.longitude + bounds.northEast.longitude) / 2); longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude; } else { //Region spans the international dateline center = CLLocationCoordinate2DMake((bounds.southWest.latitude + bounds.northEast.latitude) / 2, (bounds.southWest.longitude + bounds.northEast.longitude + 360) / 2); longitudeDelta = bounds.northEast.longitude + 360 - bounds.southWest.longitude; } MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta); return MKCoordinateRegionMake(center, span); } + (GMSCameraPosition*) makeGMSCameraPositionFromMap:(GMSMapView *)map andMKCoordinateRegion:(MKCoordinateRegion)region { float latitudeDelta = region.span.latitudeDelta * 0.5; float longitudeDelta = region.span.longitudeDelta * 0.5; CLLocationCoordinate2D a = CLLocationCoordinate2DMake(region.center.latitude + latitudeDelta, region.center.longitude + longitudeDelta); CLLocationCoordinate2D b = CLLocationCoordinate2DMake(region.center.latitude - latitudeDelta, region.center.longitude - longitudeDelta); GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:a coordinate:b]; return [map cameraForBounds:bounds insets:UIEdgeInsetsZero]; } #pragma mark - Utils - (CGRect) frameForMarker:(AIRGoogleMapMarker*) mrkView { CGPoint mrkAnchor = mrkView.realMarker.groundAnchor; CGPoint mrkPoint = [self.projection pointForCoordinate:mrkView.coordinate]; CGSize mrkSize = mrkView.realMarker.iconView ? mrkView.realMarker.iconView.bounds.size : CGSizeMake(20, 30); CGRect mrkFrame = CGRectMake(mrkPoint.x, mrkPoint.y, mrkSize.width, mrkSize.height); mrkFrame.origin.y -= mrkAnchor.y * mrkSize.height; mrkFrame.origin.x -= mrkAnchor.x * mrkSize.width; return mrkFrame; } - (NSDictionary*) getMarkersFramesWithOnlyVisible:(BOOL)onlyVisible { NSMutableDictionary* markersFrames = [NSMutableDictionary new]; for (AIRGoogleMapMarker* mrkView in self.markers) { CGRect frame = [self frameForMarker:mrkView]; CGPoint point = [self.projection pointForCoordinate:mrkView.coordinate]; NSDictionary* frameDict = @{ @"x": @(frame.origin.x), @"y": @(frame.origin.y), @"width": @(frame.size.width), @"height": @(frame.size.height) }; NSDictionary* pointDict = @{ @"x": @(point.x), @"y": @(point.y) }; NSString* k = mrkView.identifier; BOOL isVisible = CGRectIntersectsRect(self.bounds, frame); if (k != nil && (!onlyVisible || isVisible)) { [markersFrames setObject:@{ @"frame": frameDict, @"point": pointDict } forKey:k]; } } return markersFrames; } - (AIRGoogleMapMarker*) markerAtPoint:(CGPoint)point { AIRGoogleMapMarker* mrk = nil; for (AIRGoogleMapMarker* mrkView in self.markers) { CGRect frame = [self frameForMarker:mrkView]; if (CGRectContainsPoint(frame, point)) { mrk = mrkView; break; } } return mrk; } -(SEL)getActionForTarget:(NSObject*)target { SEL action = nil; uint32_t ivarCount; Ivar *ivars = class_copyIvarList([target class], &ivarCount); if (ivars) { for (uint32_t i = 0 ; i < ivarCount ; i++) { Ivar ivar = ivars[i]; const char* type = ivar_getTypeEncoding(ivar); const char* ivarName = ivar_getName(ivar); NSString* name = [NSString stringWithCString: ivarName encoding: NSASCIIStringEncoding]; if (type[0] == ':' && [name isEqualToString:@"_action"]) { SEL sel = ((SEL (*)(id, Ivar))object_getIvar)(target, ivar); action = sel; break; } } } free(ivars); return action; } #pragma mark - Overrides for Callout behavior -(void)overrideGestureRecognizersForView:(UIView*)view { NSArray* grs = view.gestureRecognizers; for (UIGestureRecognizer* gestureRecognizer in grs) { NSNumber* grHash = [NSNumber numberWithUnsignedInteger:gestureRecognizer.hash]; if([self.origGestureRecognizersMeta objectForKey:grHash] != nil) continue; //already patched //get original handlers NSArray* origTargets = [gestureRecognizer valueForKey:@"targets"]; NSMutableArray* origTargetsActions = [[NSMutableArray alloc] init]; BOOL isZoomTapGesture = NO; for (NSObject* trg in origTargets) { NSObject* target = [trg valueForKey:@"target"]; SEL action = [self getActionForTarget:trg]; isZoomTapGesture = [NSStringFromSelector(action) isEqualToString:@"handleZoomTapGesture:"]; [origTargetsActions addObject:@{ @"target": [NSValue valueWithNonretainedObject:target], @"action": NSStringFromSelector(action) }]; } if (isZoomTapGesture && self.zoomTapEnabled == NO) { [view removeGestureRecognizer:gestureRecognizer]; continue; } //replace with extendedMapGestureHandler for (NSDictionary* origTargetAction in origTargetsActions) { NSValue* targetValue = [origTargetAction objectForKey:@"target"]; NSObject* target = [targetValue nonretainedObjectValue]; NSString* actionString = [origTargetAction objectForKey:@"action"]; SEL action = NSSelectorFromString(actionString); [gestureRecognizer removeTarget:target action:action]; } [gestureRecognizer addTarget:self action:@selector(extendedMapGestureHandler:)]; [self.origGestureRecognizersMeta setObject:@{@"targets": origTargetsActions} forKey:grHash]; } } - (id)extendedMapGestureHandler:(UIGestureRecognizer*)gestureRecognizer { NSNumber* grHash = [NSNumber numberWithUnsignedInteger:gestureRecognizer.hash]; UIWindow* win = [[[UIApplication sharedApplication] windows] firstObject]; NSObject* bubbleProvider = [self valueForKey:@"bubbleProvider"]; //GMSbubbleEntityProvider CGRect bubbleAbsoluteFrame = [bubbleProvider accessibilityFrame]; CGRect bubbleFrame = [win convertRect:bubbleAbsoluteFrame toView:self]; UIView* bubbleView = [bubbleProvider valueForKey:@"view"]; BOOL performOriginalActions = YES; BOOL isTap = [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] || [gestureRecognizer isMemberOfClass:[UITapGestureRecognizer class]]; if (isTap) { BOOL isTapInsideBubble = NO; CGPoint tapPoint = CGPointZero; CGPoint tapPointInBubble = CGPointZero; NSArray* touches = [gestureRecognizer valueForKey:@"touches"]; UITouch* oneTouch = [touches firstObject]; NSArray* delayedTouches = [gestureRecognizer valueForKey:@"delayedTouches"]; NSObject* delayedTouch = [delayedTouches firstObject]; //UIGestureDeleayedTouch UITouch* tapTouch = [delayedTouch valueForKey:@"stateWhenDelayed"]; if (!tapTouch) tapTouch = oneTouch; tapPoint = [tapTouch locationInView:self]; isTapInsideBubble = tapTouch != nil && CGRectContainsPoint(bubbleFrame, tapPoint); if (isTapInsideBubble) { tapPointInBubble = CGPointMake(tapPoint.x - bubbleFrame.origin.x, tapPoint.y - bubbleFrame.origin.y); } if (isTapInsideBubble) { //find bubble's marker AIRGoogleMapMarker* markerView = nil; AIRGMSMarker* marker = nil; for (AIRGoogleMapMarker* mrk in self.markers) { if ([mrk.calloutView isEqual:bubbleView]) { markerView = mrk; marker = markerView.realMarker; break; } } //find real tap target subview UIView* realSubview = [(RCTView*)bubbleView hitTest:tapPointInBubble withEvent:nil]; AIRGoogleMapCalloutSubview* realPressableSubview = nil; if (realSubview) { UIView* tmp = realSubview; while (tmp && tmp != win && tmp != bubbleView) { if ([tmp respondsToSelector:@selector(onPress)]) { realPressableSubview = (AIRGoogleMapCalloutSubview*) tmp; break; } tmp = tmp.superview; } } if (markerView) { BOOL isInsideCallout = [markerView.calloutView isPointInside:tapPointInBubble]; if (isInsideCallout) { [markerView didTapInfoWindowOfMarker:marker subview:realPressableSubview point:tapPointInBubble frame:bubbleFrame]; } else { AIRGoogleMapMarker* markerAtTapPoint = [self markerAtPoint:tapPoint]; if (markerAtTapPoint != nil) { [self didTapMarker:markerAtTapPoint.realMarker]; } else { CLLocationCoordinate2D coord = [self.projection coordinateForPoint:tapPoint]; [markerView hideCalloutView]; [self didTapAtCoordinate:coord]; } } performOriginalActions = NO; } } } if (performOriginalActions) { NSDictionary* origMeta = [self.origGestureRecognizersMeta objectForKey:grHash]; NSDictionary* origTargets = [origMeta objectForKey:@"targets"]; for (NSDictionary* origTarget in origTargets) { NSValue* targetValue = [origTarget objectForKey:@"target"]; NSObject* target = [targetValue nonretainedObjectValue]; NSString* actionString = [origTarget objectForKey:@"action"]; SEL action = NSSelectorFromString(actionString); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [target performSelector:action withObject:gestureRecognizer]; #pragma clang diagnostic pop } } return nil; } #pragma mark - KVO updates - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"myLocation"]){ CLLocation *location = [object myLocation]; id event = @{@"coordinate": @{ @"latitude": @(location.coordinate.latitude), @"longitude": @(location.coordinate.longitude), @"altitude": @(location.altitude), @"timestamp": @(location.timestamp.timeIntervalSinceReferenceDate * 1000), @"accuracy": @(location.horizontalAccuracy), @"altitudeAccuracy": @(location.verticalAccuracy), @"speed": @(location.speed), @"heading": @(location.course), } }; if (self.onUserLocationChange) self.onUserLocationChange(event); } else { // This message is not for me; pass it on to super. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } + (NSString *)GetIconUrl:(GMUPlacemark *) marker parser:(GMUKMLParser *) parser { #ifdef HAVE_GOOGLE_MAPS_UTILS if (marker.style.styleID != nil) { for (GMUStyle *style in parser.styles) { if (style.styleID == marker.style.styleID) { return style.iconUrl; } } } return marker.style.iconUrl; #else REQUIRES_GOOGLE_MAPS_UTILS("GetIconUrl:parser:"); return @""; #endif } - (NSString *)KmlSrc { return _kmlSrc; } - (void)setKmlSrc:(NSString *)kmlUrl { #ifdef HAVE_GOOGLE_MAPS_UTILS _kmlSrc = kmlUrl; NSURL *url = [NSURL URLWithString:kmlUrl]; NSData *urlData = nil; if ([url isFileURL]) { urlData = [NSData dataWithContentsOfURL:url]; } else { urlData = [[NSFileManager defaultManager] contentsAtPath:kmlUrl]; } GMUKMLParser *parser = [[GMUKMLParser alloc] initWithData:urlData]; [parser parse]; NSUInteger index = 0; NSMutableArray *markers = [[NSMutableArray alloc]init]; for (GMUPlacemark *place in parser.placemarks) { CLLocationCoordinate2D location =((GMUPoint *) place.geometry).coordinate; AIRGoogleMapMarker *marker = (AIRGoogleMapMarker *)[[AIRGoogleMapMarkerManager alloc] view]; if (!marker.bridge) { marker.bridge = _bridge; } marker.identifier = place.title; marker.coordinate = location; marker.title = place.title; marker.subtitle = place.snippet; marker.pinColor = place.style.fillColor; marker.imageSrc = [AIRGoogleMap GetIconUrl:place parser:parser]; marker.layer.backgroundColor = [UIColor clearColor].CGColor; marker.layer.position = CGPointZero; [self insertReactSubview:(UIView *) marker atIndex:index]; [markers addObject:@{@"id": marker.identifier, @"title": marker.title, @"description": marker.subtitle, @"coordinate": @{ @"latitude": @(location.latitude), @"longitude": @(location.longitude) } }]; index++; } id event = @{@"markers": markers}; if (self.onKmlReady) self.onKmlReady(event); #else REQUIRES_GOOGLE_MAPS_UTILS(); #endif } - (void) didChangeActiveBuilding: (nullable GMSIndoorBuilding *) building { if (!building) { if (!self.onIndoorBuildingFocused) { return; } self.onIndoorBuildingFocused(@{ @"IndoorBuilding": @{ @"activeLevelIndex": @0, @"underground": @false, @"levels": [[NSMutableArray alloc]init] } }); } NSInteger i = 0; NSMutableArray *arrayLevels = [[NSMutableArray alloc]init]; for (GMSIndoorLevel *level in building.levels) { [arrayLevels addObject: @{ @"index": @(i), @"name" : level.name, @"shortName" : level.shortName, }]; i++; } if (!self.onIndoorBuildingFocused) { return; } self.onIndoorBuildingFocused(@{ @"IndoorBuilding": @{ @"activeLevelIndex": @(building.defaultLevelIndex), @"underground": @(building.underground), @"levels": arrayLevels } }); } - (void) didChangeActiveLevel: (nullable GMSIndoorLevel *) level { if (!self.onIndoorLevelActivated || !self.indoorDisplay || !level) { return; } NSInteger i = 0; for (GMSIndoorLevel *buildingLevel in self.indoorDisplay.activeBuilding.levels) { if (buildingLevel.name == level.name && buildingLevel.shortName == level.shortName) { break; } i++; } self.onIndoorLevelActivated(@{ @"IndoorLevel": @{ @"activeLevelIndex": @(i), @"name": level.name, @"shortName": level.shortName } }); } @end #endif