// // AIRGoogleMapManager.m // AirMaps // // Created by Gil Birman on 9/1/16. // #import "AIRGoogleMapManager.h" #import #import #import #import #import #import #import #import #import "RCTConvert+GMSMapViewType.h" #import "AIRGoogleMap.h" #import "AIRMapMarker.h" #import "AIRMapPolyline.h" #import "AIRMapPolygon.h" #import "AIRMapCircle.h" #import "SMCalloutView.h" #import "AIRGoogleMapMarker.h" #import "RCTConvert+AirMap.h" #import #import static NSString *const RCTMapViewKey = @"MapView"; @interface AIRGoogleMapManager() { BOOL didCallOnMapReady; } @end @implementation AIRGoogleMapManager RCT_EXPORT_MODULE() - (UIView *)view { AIRGoogleMap *map = [AIRGoogleMap new]; map.delegate = self; return map; } RCT_EXPORT_VIEW_PROPERTY(initialRegion, MKCoordinateRegion) RCT_EXPORT_VIEW_PROPERTY(region, MKCoordinateRegion) RCT_EXPORT_VIEW_PROPERTY(showsBuildings, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL) //RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL) // Not supported by GoogleMaps RCT_EXPORT_VIEW_PROPERTY(showsTraffic, BOOL) RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(rotateEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsMyLocationButton, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsIndoorLevelPicker, BOOL) RCT_EXPORT_VIEW_PROPERTY(customMapStyleString, NSString) RCT_EXPORT_VIEW_PROPERTY(mapPadding, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(onMapReady, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMarkerPress, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onRegionChangeComplete, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(mapType, GMSMapViewType) RCT_EXPORT_VIEW_PROPERTY(minZoomLevel, CGFloat) RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat) RCT_EXPORT_METHOD(animateToRegion:(nonnull NSNumber *)reactTag withRegion:(MKCoordinateRegion)region withDuration:(CGFloat)duration) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { id view = viewRegistry[reactTag]; if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); } else { // Core Animation must be used to control the animation's duration // See http://stackoverflow.com/a/15663039/171744 [CATransaction begin]; [CATransaction setAnimationDuration:duration/1000]; AIRGoogleMap *mapView = (AIRGoogleMap *)view; GMSCameraPosition *camera = [AIRGoogleMap makeGMSCameraPositionFromMap:mapView andMKCoordinateRegion:region]; [mapView animateToCameraPosition:camera]; [CATransaction commit]; } }]; } RCT_EXPORT_METHOD(animateToCoordinate:(nonnull NSNumber *)reactTag withRegion:(CLLocationCoordinate2D)latlng withDuration:(CGFloat)duration) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { id view = viewRegistry[reactTag]; if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); } else { [CATransaction begin]; [CATransaction setAnimationDuration:duration/1000]; [(AIRGoogleMap *)view animateToLocation:latlng]; [CATransaction commit]; } }]; } RCT_EXPORT_METHOD(animateToViewingAngle:(nonnull NSNumber *)reactTag withAngle:(double)angle withDuration:(CGFloat)duration) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { id view = viewRegistry[reactTag]; if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); } else { [CATransaction begin]; [CATransaction setAnimationDuration:duration/1000]; AIRGoogleMap *mapView = (AIRGoogleMap *)view; [mapView animateToViewingAngle:angle]; [CATransaction commit]; } }]; } RCT_EXPORT_METHOD(animateToBearing:(nonnull NSNumber *)reactTag withBearing:(CGFloat)bearing withDuration:(CGFloat)duration) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { id view = viewRegistry[reactTag]; if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); } else { [CATransaction begin]; [CATransaction setAnimationDuration:duration/1000]; AIRGoogleMap *mapView = (AIRGoogleMap *)view; [mapView animateToBearing:bearing]; [CATransaction commit]; } }]; } RCT_EXPORT_METHOD(fitToElements:(nonnull NSNumber *)reactTag animated:(BOOL)animated) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { id view = viewRegistry[reactTag]; if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); } else { AIRGoogleMap *mapView = (AIRGoogleMap *)view; CLLocationCoordinate2D myLocation = ((AIRGoogleMapMarker *)(mapView.markers.firstObject)).realMarker.position; GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:myLocation coordinate:myLocation]; for (AIRGoogleMapMarker *marker in mapView.markers) bounds = [bounds includingCoordinate:marker.realMarker.position]; [mapView animateWithCameraUpdate:[GMSCameraUpdate fitBounds:bounds withPadding:55.0f]]; } }]; } RCT_EXPORT_METHOD(fitToSuppliedMarkers:(nonnull NSNumber *)reactTag markers:(nonnull NSArray *)markers animated:(BOOL)animated) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { id view = viewRegistry[reactTag]; if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); } else { AIRGoogleMap *mapView = (AIRGoogleMap *)view; NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { AIRGoogleMapMarker *marker = (AIRGoogleMapMarker *)evaluatedObject; return [marker isKindOfClass:[AIRGoogleMapMarker class]] && [markers containsObject:marker.identifier]; }]; NSArray *filteredMarkers = [mapView.markers filteredArrayUsingPredicate:filterMarkers]; CLLocationCoordinate2D myLocation = ((AIRGoogleMapMarker *)(filteredMarkers.firstObject)).realMarker.position; GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:myLocation coordinate:myLocation]; for (AIRGoogleMapMarker *marker in filteredMarkers) bounds = [bounds includingCoordinate:marker.realMarker.position]; [mapView animateWithCameraUpdate:[GMSCameraUpdate fitBounds:bounds withPadding:55.0f]]; } }]; } RCT_EXPORT_METHOD(fitToCoordinates:(nonnull NSNumber *)reactTag coordinates:(nonnull NSArray *)coordinates edgePadding:(nonnull NSDictionary *)edgePadding animated:(BOOL)animated) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { id view = viewRegistry[reactTag]; if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); } else { AIRGoogleMap *mapView = (AIRGoogleMap *)view; CLLocationCoordinate2D myLocation = coordinates.firstObject.coordinate; GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:myLocation coordinate:myLocation]; for (AIRMapCoordinate *coordinate in coordinates) bounds = [bounds includingCoordinate:coordinate.coordinate]; // Set Map viewport CGFloat top = [RCTConvert CGFloat:edgePadding[@"top"]]; CGFloat right = [RCTConvert CGFloat:edgePadding[@"right"]]; CGFloat bottom = [RCTConvert CGFloat:edgePadding[@"bottom"]]; CGFloat left = [RCTConvert CGFloat:edgePadding[@"left"]]; [mapView animateWithCameraUpdate:[GMSCameraUpdate fitBounds:bounds withEdgeInsets:UIEdgeInsetsMake(top, left, bottom, right)]]; } }]; } RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)reactTag withWidth:(nonnull NSNumber *)width withHeight:(nonnull NSNumber *)height withRegion:(MKCoordinateRegion)region format:(nonnull NSString *)format quality:(nonnull NSNumber *)quality result:(nonnull NSString *)result withCallback:(RCTResponseSenderBlock)callback) { NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970]; NSString *pathComponent = [NSString stringWithFormat:@"Documents/snapshot-%.20lf.%@", timeStamp, format]; NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent: pathComponent]; [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { id view = viewRegistry[reactTag]; if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); } else { AIRGoogleMap *mapView = (AIRGoogleMap *)view; // TODO: currently we are ignoring width, height, region UIGraphicsBeginImageContextWithOptions(mapView.frame.size, YES, 0.0f); [mapView.layer renderInContext:UIGraphicsGetCurrentContext()]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); NSData *data; if ([format isEqualToString:@"png"]) { data = UIImagePNGRepresentation(image); } else if([format isEqualToString:@"jpg"]) { data = UIImageJPEGRepresentation(image, quality.floatValue); } if ([result isEqualToString:@"file"]) { [data writeToFile:filePath atomically:YES]; callback(@[[NSNull null], filePath]); } else if ([result isEqualToString:@"base64"]) { callback(@[[NSNull null], [data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn]]); } else if ([result isEqualToString:@"legacy"]) { // In the initial (iOS only) implementation of takeSnapshot, // both the uri and the base64 encoded string were returned. // Returning both is rarely useful and in fact causes a // performance penalty when only the file URI is desired. // In that case the base64 encoded string was always marshalled // over the JS-bridge (which is quite slow). // A new more flexible API was created to cover this. // This code should be removed in a future release when the // old API is fully deprecated. [data writeToFile:filePath atomically:YES]; NSDictionary *snapshotData = @{ @"uri": filePath, @"data": [data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn] }; callback(@[[NSNull null], snapshotData]); } } UIGraphicsEndImageContext(); }]; } RCT_EXPORT_METHOD(setMapBoundaries:(nonnull NSNumber *)reactTag northEast:(CLLocationCoordinate2D)northEast southWest:(CLLocationCoordinate2D)southWest) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { id view = viewRegistry[reactTag]; if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); } else { AIRGoogleMap *mapView = (AIRGoogleMap *)view; GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:northEast coordinate:southWest]; mapView.cameraTargetBounds = bounds; } }]; } + (BOOL)requiresMainQueueSetup { return YES; } - (NSDictionary *)constantsToExport { return @{ @"legalNotice": [GMSServices openSourceLicenseInfo] }; } - (void)mapViewDidStartTileRendering:(GMSMapView *)mapView { if (didCallOnMapReady) return; didCallOnMapReady = YES; AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView; [googleMapView didPrepareMap]; } - (BOOL)mapView:(GMSMapView *)mapView didTapMarker:(GMSMarker *)marker { AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView; return [googleMapView didTapMarker:marker]; } - (void)mapView:(GMSMapView *)mapView didTapOverlay:(GMSPolygon *)polygon { AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView; [googleMapView didTapPolygon:polygon]; } - (void)mapView:(GMSMapView *)mapView didTapAtCoordinate:(CLLocationCoordinate2D)coordinate { AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView; [googleMapView didTapAtCoordinate:coordinate]; } - (void)mapView:(GMSMapView *)mapView didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate { AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView; [googleMapView didLongPressAtCoordinate:coordinate]; } - (void)mapView:(GMSMapView *)mapView didChangeCameraPosition:(GMSCameraPosition *)position { AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView; [googleMapView didChangeCameraPosition:position]; } - (void)mapView:(GMSMapView *)mapView idleAtCameraPosition:(GMSCameraPosition *)position { AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView; [googleMapView idleAtCameraPosition:position]; } - (UIView *)mapView:(GMSMapView *)mapView markerInfoWindow:(GMSMarker *)marker { AIRGMSMarker *aMarker = (AIRGMSMarker *)marker; return [aMarker.fakeMarker markerInfoWindow];} - (UIView *)mapView:(GMSMapView *)mapView markerInfoContents:(GMSMarker *)marker { AIRGMSMarker *aMarker = (AIRGMSMarker *)marker; return [aMarker.fakeMarker markerInfoContents]; } - (void)mapView:(GMSMapView *)mapView didTapInfoWindowOfMarker:(GMSMarker *)marker { AIRGMSMarker *aMarker = (AIRGMSMarker *)marker; [aMarker.fakeMarker didTapInfoWindowOfMarker:aMarker]; } - (void)mapView:(GMSMapView *)mapView didBeginDraggingMarker:(GMSMarker *)marker { AIRGMSMarker *aMarker = (AIRGMSMarker *)marker; [aMarker.fakeMarker didBeginDraggingMarker:aMarker]; } - (void)mapView:(GMSMapView *)mapView didEndDraggingMarker:(GMSMarker *)marker { AIRGMSMarker *aMarker = (AIRGMSMarker *)marker; [aMarker.fakeMarker didEndDraggingMarker:aMarker]; } - (void)mapView:(GMSMapView *)mapView didDragMarker:(GMSMarker *)marker { AIRGMSMarker *aMarker = (AIRGMSMarker *)marker; [aMarker.fakeMarker didDragMarker:aMarker]; } @end