// // AIRGoogleMap.m // AirMaps // // Created by Gil Birman on 9/1/16. // #import "AIRGoogleMap.h" #import "AIRGoogleMapMarker.h" #import "AIRGoogleMapPolygon.h" #import "AIRGoogleMapPolyline.h" #import "AIRGoogleMapCircle.h" #import "AIRGoogleMapUrlTile.h" #import #import #import #import "RCTConvert+AirMap.h" 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; @end @implementation AIRGoogleMap { NSMutableArray *_reactSubviews; BOOL _initialRegionSet; } - (instancetype)init { if ((self = [super init])) { _reactSubviews = [NSMutableArray new]; _markers = [NSMutableArray array]; _polygons = [NSMutableArray array]; _polylines = [NSMutableArray array]; _circles = [NSMutableArray array]; _tiles = [NSMutableArray array]; _initialRegionSet = false; } return self; } - (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 { 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 { 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 - (void)setInitialRegion:(MKCoordinateRegion)initialRegion { if (_initialRegionSet) return; _initialRegionSet = true; self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:initialRegion]; } - (void)setRegion:(MKCoordinateRegion)region { // TODO: The JS component is repeatedly setting region unnecessarily. We might want to deal with that in here. self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:region]; } - (void)didPrepareMap { if (self.onMapReady) self.onMapReady(@{}); } - (BOOL)didTapMarker:(GMSMarker *)marker { AIRGMSMarker *airMarker = (AIRGMSMarker *)marker; id event = @{@"action": @"marker-press", @"id": airMarker.identifier ?: @"unknown", }; 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 { id event = @{@"continuous": @YES, @"region": regionAsJSON([AIRGoogleMap makeGMSCameraPositionFromMap:self andGMSCameraPosition:position]), }; if (self.onChange) self.onChange(event); } - (void)idleAtCameraPosition:(GMSCameraPosition *)position { id event = @{@"continuous": @NO, @"region": regionAsJSON([AIRGoogleMap makeGMSCameraPositionFromMap:self andGMSCameraPosition:position]), }; if (self.onChange) self.onChange(event); // complete } - (void)setMapPadding:(UIEdgeInsets)mapPadding { self.padding = mapPadding; } - (UIEdgeInsets)mapPadding { return self.padding; } - (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)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)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]; } @end