// // RCTMapboxGLManager.m // RCTMapboxGL // // Created by Bobby Sudekum on 4/30/15. // Copyright (c) 2015 Mapbox. All rights reserved. // #import "RCTMapboxGLManager.h" #import "RCTMapboxGL.h" #import #import #import #import #import #import #import #import "RCTMapboxGLConversions.h" #import "MGLPolygon+RCTAdditions.h" #import "MGLPolyline+RCTAdditions.h" @implementation RCTMapboxGLManager - (UIView *)view { return [[RCTMapboxGL alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; } @synthesize bridge = _bridge; - (dispatch_queue_t)methodQueue { return _bridge.uiManager.methodQueue; } RCT_EXPORT_MODULE(); // Props RCT_EXPORT_VIEW_PROPERTY(initialCenterCoordinate, CLLocationCoordinate2D); RCT_EXPORT_VIEW_PROPERTY(initialZoomLevel, double); RCT_EXPORT_VIEW_PROPERTY(initialDirection, double); RCT_EXPORT_VIEW_PROPERTY(clipsToBounds, BOOL); RCT_EXPORT_VIEW_PROPERTY(debugActive, BOOL); RCT_EXPORT_VIEW_PROPERTY(rotateEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(minimumZoomLevel, double); RCT_EXPORT_VIEW_PROPERTY(maximumZoomLevel, double); RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL); RCT_EXPORT_VIEW_PROPERTY(styleURL, NSURL); RCT_EXPORT_VIEW_PROPERTY(userTrackingMode, int); RCT_EXPORT_VIEW_PROPERTY(attributionButtonIsHidden, BOOL); RCT_EXPORT_VIEW_PROPERTY(logoIsHidden, BOOL); RCT_EXPORT_VIEW_PROPERTY(compassIsHidden, BOOL); RCT_EXPORT_VIEW_PROPERTY(userLocationVerticalAlignment, int); RCT_EXPORT_VIEW_PROPERTY(annotationsPopUpEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(onRegionDidChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onRegionWillChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onChangeUserTrackingMode, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onOpenAnnotation, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onCloseAnnotation, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onRightAnnotationTapped, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onUpdateUserLocation, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onTap, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onFinishLoadingMap, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onStartLoadingMap, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onLocateUserFailed, RCTDirectEventBlock); RCT_CUSTOM_VIEW_PROPERTY(contentInset, UIEdgeInsetsMake, RCTMapboxGL) { int top = [json[0] doubleValue]; int left = [json[3] doubleValue]; int bottom = [json[2] doubleValue]; int right = [json[1] doubleValue]; UIEdgeInsets inset = UIEdgeInsetsMake(top, left, bottom, right); view.contentInset = inset; } // Constants - (NSDictionary *)constantsToExport { return @{ @"mapStyles": @{ @"light": [[MGLStyle lightStyleURL] absoluteString], @"dark": [[MGLStyle darkStyleURL] absoluteString], @"streets": [[MGLStyle streetsStyleURL] absoluteString], @"emerald": [[MGLStyle emeraldStyleURL] absoluteString], @"satellite": [[MGLStyle satelliteStyleURL] absoluteString], @"hybrid": [[MGLStyle hybridStyleURL] absoluteString], }, @"userTrackingMode": @{ @"none": [NSNumber numberWithUnsignedInt:MGLUserTrackingModeNone], @"follow": [NSNumber numberWithUnsignedInt:MGLUserTrackingModeFollow], @"followWithCourse": [NSNumber numberWithUnsignedInt:MGLUserTrackingModeFollowWithCourse], @"followWithHeading": [NSNumber numberWithUnsignedInt:MGLUserTrackingModeFollowWithHeading] }, @"userLocationVerticalAlignment" : @{ @"top": @(MGLAnnotationVerticalAlignmentTop), @"center": @(MGLAnnotationVerticalAlignmentCenter), @"bottom": @(MGLAnnotationVerticalAlignmentBottom) }, @"unknownResourceCount": @(UINT64_MAX), @"metricsEnabled": @([RCTMapboxGLManager metricsEnabled]) }; }; // Metrics + (BOOL)metricsEnabled { NSUserDefaults * ud = [NSUserDefaults standardUserDefaults]; NSNumber * nr = [ud valueForKey:@"MGLMapboxMetricsEnabled"]; if (!nr || ![nr isKindOfClass:[NSNumber class]]) { return YES; } return nr.boolValue; } RCT_EXPORT_METHOD(setMetricsEnabled:(BOOL)enabled) { [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:@"MGLMapboxMetricsEnabled"]; } // Access token RCT_EXPORT_METHOD(setAccessToken:(nonnull NSString *)accessToken resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ if (!accessToken || ![accessToken length] || [accessToken isEqual:@"your-mapbox.com-access-token"]) { reject(nil, @"Mapbox api token is not valid.", nil); return; } [MGLAccountManager setAccessToken:accessToken]; resolve(nil); }); } // Offline - (id)init { if (!(self = [super init])) { return nil; } return self; } - (void)dealloc { [[MGLOfflineStorage sharedOfflineStorage] removeObserver:self forKeyPath:@"packs"]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)offlinePacksDidFinishLoading { _loadedPacks = YES; NSArray * packs = [MGLOfflineStorage sharedOfflineStorage].packs; if ([_packRequests count]) { NSArray * callbackArray = [self serializePacksArray:packs]; for (RCTPromiseResolveBlock callback in _packRequests) { callback(callbackArray); } [_packRequests removeAllObjects]; } for (MGLOfflinePack * pack in packs) { if (pack.state != MGLOfflinePackStateComplete) { [pack resume]; } } [_bridge.eventDispatcher sendAppEventWithName:@"MapboxOfflinePacksLoaded" body:@{}]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSNumber * changeKind = change[NSKeyValueChangeKindKey]; if (changeKind == [NSNull null]) { return; } if ([changeKind integerValue] != NSKeyValueChangeSetting) { return; } NSArray * packs = [[MGLOfflineStorage sharedOfflineStorage] packs]; if (!packs) { return; } if (_loadedPacks) { return; } [_loadingPacks addObjectsFromArray:packs]; for (MGLOfflinePack * pack in packs) { [pack requestProgress]; } if (!packs.count) { [self offlinePacksDidFinishLoading]; } } - (void)firePackProgress:(MGLOfflinePack*)pack { if (pack.state == MGLOfflinePackStateInvalid) { return; } NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; MGLOfflinePackProgress progress = pack.progress; NSDictionary *event = @{ @"name": userInfo[@"name"], @"metadata": userInfo[@"metadata"], @"countOfResourcesCompleted": @(progress.countOfResourcesCompleted), @"countOfResourcesExpected": @(progress.countOfResourcesExpected), @"countOfBytesCompleted": @(progress.countOfBytesCompleted), @"maximumResourcesExpected": @(progress.maximumResourcesExpected) }; [_bridge.eventDispatcher sendAppEventWithName:@"MapboxOfflineProgressDidChange" body:event]; [_recentPacks addObject:pack]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, _throttleInterval * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ [_recentPacks removeObject:pack]; if ([_throttledPacks containsObject:pack]) { [_throttledPacks removeObject:pack]; [self firePackProgress:pack]; } }); } - (void)flushThrottleForPack:(MGLOfflinePack*)pack { if ([_throttledPacks containsObject:pack]) { [_throttledPacks removeObject:pack]; [self firePackProgress:pack]; } } - (void)discardThrottleForPack:(MGLOfflinePack*)pack { if ([_throttledPacks containsObject:pack]) { [_throttledPacks removeObject:pack]; } } - (void)offlinePackProgressDidChange:(NSNotification *)notification { MGLOfflinePack *pack = notification.object; if (!_loadedPacks && [_loadingPacks containsObject:pack]) { [_loadingPacks removeObject:pack]; if ([_loadingPacks count] == 0) { [self offlinePacksDidFinishLoading]; } } if ([_removedPacks containsObject:pack]) { return; } if ([_recentPacks containsObject:pack]) { [_throttledPacks addObject:pack]; return; } [self firePackProgress:pack]; } - (void)offlinePackDidReceiveMaximumAllowedMapboxTiles:(NSNotification *)notification { MGLOfflinePack *pack = notification.object; [self flushThrottleForPack:pack]; NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; uint64_t maximumCount = [notification.userInfo[MGLOfflinePackMaximumCountUserInfoKey] unsignedLongLongValue]; NSDictionary *event = @{ @"name": userInfo[@"name"], @"maxTiles": @(maximumCount) }; [_bridge.eventDispatcher sendAppEventWithName:@"MapboxOfflineMaxAllowedTiles" body:event]; } - (void)offlinePackDidReceiveError:(NSNotification *)notification { MGLOfflinePack *pack = notification.object; [self flushThrottleForPack:pack]; NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; NSError *error = notification.userInfo[MGLOfflinePackErrorUserInfoKey]; NSDictionary *event = @{ @"name": userInfo[@"name"], @"error": [error localizedDescription] }; [_bridge.eventDispatcher sendAppEventWithName:@"MapboxOfflineError" body:event]; } RCT_EXPORT_METHOD(initializeOfflinePacks) { _recentPacks = [NSMutableSet new]; _throttledPacks = [NSMutableSet new]; _removedPacks = [NSMutableSet new]; _throttleInterval = 300; _loadingPacks = [NSMutableSet new]; _loadedPacks = NO; // Setup pack array loading notifications [[MGLOfflineStorage sharedOfflineStorage] addObserver:self forKeyPath:@"packs" options:NSKeyValueObservingOptionInitial context:NULL]; _packRequests = [NSMutableArray new]; // Setup offline pack notification handlers. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackProgressDidChange:) name:MGLOfflinePackProgressChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveError:) name:MGLOfflinePackErrorNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(offlinePackDidReceiveMaximumAllowedMapboxTiles:) name:MGLOfflinePackMaximumMapboxTilesReachedNotification object:nil]; } RCT_REMAP_METHOD(addOfflinePack, pack:(NSDictionary*)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if (options[@"name"] == nil) { reject(@"invalid_arguments", @"addOfflinePack(): name is required.", nil); return; } if (options[@"minZoomLevel"] == nil) { reject(@"invalid_arguments", @"addOfflinePack(): minZoomLevel is required.", nil); return; } if (options[@"maxZoomLevel"] == nil) { reject(@"invalid_arguments", @"addOfflinePack(): maxZoomLevel is required.", nil); return; } if (options[@"bounds"] == nil) { reject(@"invalid_arguments", @"addOfflinePack(): bounds is required.", nil); return; } if (options[@"styleURL"] == nil) { reject(@"invalid_arguments", @"addOfflinePack(): styleURL is required.", nil); return; } if (!([options[@"type"] isEqualToString:@"bbox"])) { reject(@"invalid_arguments", [NSString stringWithFormat:@"addOfflinePack(): Offline type %@ not supported. Only type \"bbox\" supported.", options[@"type"]] , nil); return; } NSArray *b = [options valueForKey:@"bounds"]; MGLCoordinateBounds bounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake([b[0] floatValue], [b[1] floatValue]), CLLocationCoordinate2DMake([b[2] floatValue], [b[3] floatValue])); NSURL * styleURL = [NSURL URLWithString:[options valueForKey:@"styleURL"]]; float fromZoomLevel = [[options valueForKey:@"minZoomLevel"] floatValue]; float toZoomLevel = [[options valueForKey:@"maxZoomLevel"] floatValue]; NSString * name = [options valueForKey:@"name"]; NSString * type = [options valueForKey:@"type"]; NSDictionary * metadata = [options valueForKey:@"metadata"]; dispatch_async(dispatch_get_main_queue(), ^{ id region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL bounds:bounds fromZoomLevel:fromZoomLevel toZoomLevel:toZoomLevel]; NSMutableDictionary *userInfo = @{ @"name": name, @"metadata": metadata ? metadata : [NSNull null] }; NSData *context = [NSKeyedArchiver archivedDataWithRootObject:userInfo]; [[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack *pack, NSError *error) { if (error != nil) { reject(@"add_pack_failed", error.localizedFailureReason, error); } else { [pack resume]; resolve([NSNull null]); } }]; }); } - (NSArray*)serializePacksArray:(NSArray*)packs { NSMutableArray* callbackArray = [NSMutableArray new]; for (MGLOfflinePack *pack in packs) { NSMutableDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; [callbackArray addObject:@{ @"name": userInfo[@"name"], @"metadata": userInfo[@"metadata"], @"countOfBytesCompleted": @(pack.progress.countOfBytesCompleted), @"countOfResourcesCompleted": @(pack.progress.countOfResourcesCompleted), @"countOfResourcesExpected": @(pack.progress.countOfResourcesExpected), @"maximumResourcesExpected": @(pack.progress.maximumResourcesExpected) }]; } return callbackArray; } RCT_REMAP_METHOD(getOfflinePacks, resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ NSMutableArray* callbackArray = [NSMutableArray new]; if (!_loadedPacks) { [_packRequests addObject:resolve]; } else { MGLOfflinePack *packs = [MGLOfflineStorage sharedOfflineStorage].packs; resolve([self serializePacksArray:packs]); } }); } RCT_REMAP_METHOD(removeOfflinePack, name:(NSString*)packName resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ MGLOfflinePack *packs = [MGLOfflineStorage sharedOfflineStorage].packs; MGLOfflinePack *tempPack; for (MGLOfflinePack *pack in packs) { NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context]; if ([packName isEqualToString:userInfo[@"name"]]) { tempPack = pack; break; } } if (tempPack == nil) { return resolve(@{}); } NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:tempPack.context]; // Workaround for https://github.com/mapbox/mapbox-gl-native/issues/5508 [_removedPacks addObject:tempPack]; [self discardThrottleForPack:tempPack]; [tempPack suspend]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ [_removedPacks removeObject:tempPack]; [[MGLOfflineStorage sharedOfflineStorage] removePack:tempPack withCompletionHandler:^(NSError * _Nullable error) { if (error != nil) { reject(@"remove_pack_failed", error.localizedFailureReason, error); } else { resolve(@{ @"deleted": userInfo[@"name"] }); } }]; }); }); } RCT_EXPORT_METHOD(setOfflinePackProgressThrottleInterval:(nonnull NSNumber *)milis) { _throttleInterval = [milis intValue]; } // View methods RCT_EXPORT_METHOD(spliceAnnotations:(nonnull NSNumber *)reactTag deleteAll:(BOOL)deleteAll toDelete:(nonnull NSArray *)toDelete toAdd:(nonnull NSArray *)toAdd) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; if (deleteAll) { [mapView removeAllAnnotations]; } else { for (NSString * key in toDelete) { [mapView removeAnnotation:key]; } } for (NSObject * annotationObject in toAdd) { [mapView upsertAnnotation:convertToMGLAnnotation(annotationObject)]; } }]; } RCT_EXPORT_METHOD(getCenterCoordinateZoomLevel:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; CLLocationCoordinate2D region = [mapView centerCoordinate]; double zoom = [mapView zoomLevel]; callback(@[ @{ @"latitude": @(region.latitude), @"longitude": @(region.longitude), @"zoomLevel": @(zoom) } ]); }]; } RCT_EXPORT_METHOD(getBounds:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; MGLCoordinateBounds bounds = [mapView visibleCoordinateBounds]; NSMutableArray *callbackArray = [[NSMutableArray alloc] init]; [callbackArray addObject:@(bounds.sw.latitude)]; [callbackArray addObject:@(bounds.sw.longitude)]; [callbackArray addObject:@(bounds.ne.latitude)]; [callbackArray addObject:@(bounds.ne.longitude)]; callback(@[callbackArray]); }]; } RCT_EXPORT_METHOD(getDirection:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; double direction = [mapView direction]; callback(@[ @(direction) ]); }]; } RCT_EXPORT_METHOD(getPitch:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; double pitch = [mapView pitch]; callback(@[ @(pitch) ]); }]; } RCT_EXPORT_METHOD(easeTo:(nonnull NSNumber *)reactTag options:(NSDictionary *)options animated:(BOOL)animated callback:(RCTResponseSenderBlock)callback) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; if ([mapView isKindOfClass:[RCTMapboxGL class]]) { NSNumber * latitude = options[@"latitude"]; NSNumber * longitude = options[@"longitude"]; NSNumber * zoom = options[@"zoomLevel"]; NSNumber * direction = options[@"direction"]; NSNumber * pitch = options[@"pitch"]; NSNumber * altitude = options[@"altitude"]; if (pitch && zoom) { RCTLogError(@"Pitch and zoomLevel can't be set together with MapView.easeTo() on iOS. Use altitude instead of zoomLevel"); return; } if (zoom && altitude) { RCTLogError(@"Altitude and zoomLevel are mutually exclusive with MapView.easeTo()"); return; } CLLocationCoordinate2D _center = (latitude && longitude) ? CLLocationCoordinate2DMake([latitude doubleValue], [longitude doubleValue]) : mapView.centerCoordinate; double _direction = direction ? [direction doubleValue] : mapView.direction; if (pitch || altitude) { MGLMapCamera * oldCamera = (!pitch || !altitude) ? mapView.camera : nil; double _altitude = altitude ? [altitude doubleValue] : oldCamera ? oldCamera.altitude : 0; double _pitch = pitch ? [pitch doubleValue] : oldCamera ? oldCamera.pitch : 0; MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:_center fromDistance:_altitude pitch:_pitch heading:_direction]; [mapView setCamera: camera withDuration: 0.3 animationTimingFunction: [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] completionHandler: ^{ callback(@[[NSNull null]]); }]; } else { double _zoomLevel = zoom ? [zoom doubleValue] : mapView.zoomLevel; [mapView setCenterCoordinate: _center zoomLevel: _zoomLevel direction: _direction animated: animated completionHandler: ^{ callback(@[[NSNull null]]); }]; } } }]; } RCT_EXPORT_METHOD(setVisibleCoordinateBounds:(nonnull NSNumber *)reactTag latitudeSW:(float) latitudeSW longitudeSW:(float) longitudeSW latitudeNE:(float) latitudeNE longitudeNE:(float) longitudeNE paddingTop:(double) paddingTop paddingRight:(double) paddingRight paddingBottom:(double) paddingBottom paddingLeft:(double) paddingLeft animated:(BOOL) animated) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; if ([mapView isKindOfClass:[RCTMapboxGL class]]) { MGLCoordinateBounds coordinatesBounds = MGLCoordinateBoundsMake(CLLocationCoordinate2DMake(latitudeSW, longitudeSW), CLLocationCoordinate2DMake(latitudeNE, longitudeNE)); [mapView setVisibleCoordinateBounds:coordinatesBounds edgePadding:UIEdgeInsetsMake(paddingTop, paddingLeft, paddingBottom, paddingRight) animated:animated]; } }]; } RCT_EXPORT_METHOD(selectAnnotation:(nonnull NSNumber *) reactTag selectedIdentifier:(NSString*)selectedIdentifier animated:(BOOL)animated) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; if ([mapView isKindOfClass:[RCTMapboxGL class]]) { [mapView selectAnnotation:selectedIdentifier animated:animated]; } }]; } RCT_EXPORT_METHOD(deselectAnnotation:(nonnull NSNumber *) reactTag) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; if ([mapView isKindOfClass:[RCTMapboxGL class]]) { [mapView deselectAnnotation]; } }]; } RCT_EXPORT_METHOD(queryRenderedFeatures:(nonnull NSNumber *)reactTag options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTMapboxGL *mapView = viewRegistry[reactTag]; if ([mapView isKindOfClass:[RCTMapboxGL class]]) { NSDictionary *pointDict = options[@"point"]; NSDictionary *rectDict = options[@"rect"]; if ((!pointDict && !rectDict) || (pointDict && rectDict)) { reject(@"invalid_arguments", @"queryRenderedFeatures(): one of 'point' or 'rect' is required.", nil); return; } NSArray> *features; NSArray *styleLayerIdentifiersArray = options[@"layers"]; NSSet *styleLayerIdentifiers; if (styleLayerIdentifiersArray) { styleLayerIdentifiers = [NSSet setWithArray:styleLayerIdentifiersArray]; } if (pointDict) { NSNumber *screenCoordX = pointDict[@"screenCoordX"]; NSNumber *screenCoordY = pointDict[@"screenCoordY"]; CGPoint point = CGPointMake(screenCoordX.floatValue, screenCoordY.floatValue); features = [mapView visibleFeaturesAtPoint:point inStyleLayersWithIdentifiers:styleLayerIdentifiers]; } else { NSNumber *left = rectDict[@"left"]; NSNumber *top = rectDict[@"top"]; NSNumber *right = rectDict[@"right"]; NSNumber *bottom = rectDict[@"bottom"]; CGFloat width = right.floatValue - left.floatValue; CGFloat height = bottom.floatValue - top.floatValue; CGRect rect = CGRectMake(left.floatValue, top.floatValue, width, height); features = [mapView visibleFeaturesInRect:rect inStyleLayersWithIdentifiers:styleLayerIdentifiers]; } NSMutableArray *geoJSONFeatures = [NSMutableArray arrayWithCapacity:features.count]; for (id feature in features) { NSDictionary *geoJSONGeometry = [self geoJSONGeometryFromMGLFeature:feature]; NSDictionary *geoJSON = @{ @"type": @"Feature", @"id": feature.identifier ? feature.identifier : [NSNull null], @"properties": feature.attributes, @"geometry": geoJSONGeometry }; [geoJSONFeatures addObject:geoJSON]; } resolve(geoJSONFeatures); } }]; } - (NSDictionary*)geoJSONGeometryFromMGLFeature:(id )feature { NSString *geometryType; if ([feature isKindOfClass:[MGLShapeCollectionFeature class]]) { geometryType = @"GeometryCollection"; MGLShapeCollectionFeature *shapeCollection = (MGLShapeCollectionFeature *) feature; NSMutableArray *geometries = [[NSMutableArray alloc] init]; for (MGLShape *shape in shapeCollection.shapes) { [geometries addObject:[self geoJSONGeometryFromMGLFeature:shape]]; } return @{ @"type": geometryType, @"geometries": geometries }; } NSMutableArray *coordinates = [[NSMutableArray alloc] init]; if ([feature isKindOfClass:[MGLPointFeature class]]) { geometryType = @"Point"; coordinates = [[NSMutableArray alloc] initWithArray:@[@(feature.coordinate.longitude), @(feature.coordinate.latitude)]]; } else if ([feature isKindOfClass:[MGLPolylineFeature class]]) { geometryType = @"LineString"; MGLPolylineFeature *polyline = (MGLPolylineFeature *)feature; coordinates = polyline.coordinateArray; } else if ([feature isKindOfClass:[MGLPolygonFeature class]]) { geometryType = @"Polygon"; MGLPolygonFeature *polygon = (MGLPolygonFeature *)feature; coordinates = polygon.coordinateArray; } else if ([feature isKindOfClass:[MGLMultiPolylineFeature class]]) { geometryType = @"MultiLineString"; MGLMultiPolylineFeature *multiPolyline = (MGLMultiPolylineFeature *)feature; for (MGLPolyline *polyline in multiPolyline.polylines) { [coordinates addObject:polyline.coordinateArray]; } } else if ([feature isKindOfClass:[MGLMultiPolygonFeature class]]) { geometryType = @"MultiPolygon"; MGLMultiPolygonFeature *multiPolygon = (MGLMultiPolygonFeature *)feature; for (MGLPolygon *polygon in multiPolygon.polygons) { [coordinates addObject:polygon.coordinateArray]; } } else if ([feature isKindOfClass:[MGLMultiPointFeature class]]) { // this is checked last since MGLPolyline and MGLPolygon inherit from MGLMultiPoint geometryType = @"MultiPoint"; MGLMultiPointFeature *multiPoint = (MGLMultiPointFeature *)feature; for (int index = 0; index < multiPoint.pointCount; index++) { CLLocationCoordinate2D coord = multiPoint.coordinates[index]; [coordinates addObject:[[NSMutableArray alloc] initWithArray:@[@(coord.longitude), @(coord.latitude)]]]; } } return @{ @"type": geometryType, @"coordinates": coordinates }; } @end