From 0ded5a54dea9f1c4e9e9bf0996c14df1407c983e Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 19 Oct 2017 16:18:54 -0700 Subject: [PATCH] Adds support for native assets as an iconImage If you want to use any images in your geojson that are under your drawables dir on Android and *.xcassets on iOS you now can provide an assets key to the images prop for the ShapeSource and the native code will add these images to the map style for you. --- .../java/com/mapbox/rctmgl/RCTMGLPackage.java | 2 +- .../styles/layers/RCTMGLSymbolLayer.java | 1 + .../styles/sources/RCTMGLShapeSource.java | 39 ++++++++++-- .../sources/RCTMGLShapeSourceManager.java | 29 +++++++++ .../mapbox/rctmgl/utils/ResourceUtils.java | 30 ++++++++++ ios/RCTMGL/RCTMGLShapeSource.h | 1 + ios/RCTMGL/RCTMGLShapeSource.m | 26 +++++++- ios/RCTMGL/RCTMGLShapeSourceManager.m | 1 + ios/RCTMGL/RCTMGLUtils.m | 59 ++++++++++--------- javascript/components/ShapeSource.js | 26 ++++++-- 10 files changed, 172 insertions(+), 42 deletions(-) create mode 100644 android/rctmgl/src/main/java/com/mapbox/rctmgl/utils/ResourceUtils.java diff --git a/android/rctmgl/src/main/java/com/mapbox/rctmgl/RCTMGLPackage.java b/android/rctmgl/src/main/java/com/mapbox/rctmgl/RCTMGLPackage.java index af872e7..255bb52 100644 --- a/android/rctmgl/src/main/java/com/mapbox/rctmgl/RCTMGLPackage.java +++ b/android/rctmgl/src/main/java/com/mapbox/rctmgl/RCTMGLPackage.java @@ -54,7 +54,7 @@ public class RCTMGLPackage implements ReactPackage { // sources managers.add(new RCTMGLVectorSourceManager(reactApplicationContext)); - managers.add(new RCTMGLShapeSourceManager()); + managers.add(new RCTMGLShapeSourceManager(reactApplicationContext)); managers.add(new RCTMGLRasterSourceManager()); // layers diff --git a/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/layers/RCTMGLSymbolLayer.java b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/layers/RCTMGLSymbolLayer.java index bb50a7b..f0df579 100644 --- a/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/layers/RCTMGLSymbolLayer.java +++ b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/layers/RCTMGLSymbolLayer.java @@ -4,6 +4,7 @@ import android.content.Context; import com.mapbox.mapboxsdk.style.layers.Filter; import com.mapbox.mapboxsdk.style.layers.SymbolLayer; +import com.mapbox.rctmgl.R; import com.mapbox.rctmgl.components.mapview.RCTMGLMapView; import com.mapbox.rctmgl.components.styles.RCTMGLStyle; import com.mapbox.rctmgl.components.styles.RCTMGLStyleFactory; diff --git a/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLShapeSource.java b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLShapeSource.java index cb405c7..7aae3ac 100644 --- a/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLShapeSource.java +++ b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLShapeSource.java @@ -1,6 +1,8 @@ package com.mapbox.rctmgl.components.styles.sources; import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import com.mapbox.mapboxsdk.maps.MapboxMap; import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions; @@ -34,6 +36,7 @@ public class RCTMGLShapeSource extends RCTSource { private Double mTolerance; private List> mImages; + private List> mNativeImages; public RCTMGLShapeSource(Context context) { super(context); @@ -41,9 +44,22 @@ public class RCTMGLShapeSource extends RCTSource { @Override public void addToMap(final RCTMGLMapView mapView) { - if (hasImages()) { - MapboxMap map = mapView.getMapboxMap(); + if (!hasNativeImages() && !hasImages()) { + super.addToMap(mapView); + return; + } + MapboxMap map = mapView.getMapboxMap(); + + // add all images from drawables folder + if (hasNativeImages()) { + for (Map.Entry nativeImage : mNativeImages) { + map.addImage(nativeImage.getKey(), nativeImage.getValue().getBitmap()); + } + } + + // add all external images from javascript layer + if (hasImages()) { DownloadMapImageTask.OnAllImagesLoaded imagesLoadedCallback = new DownloadMapImageTask.OnAllImagesLoaded() { @Override public void onAllImagesLoaded() { @@ -53,9 +69,10 @@ public class RCTMGLShapeSource extends RCTSource { DownloadMapImageTask task = new DownloadMapImageTask(map, imagesLoadedCallback); task.execute(mImages.toArray(new Map.Entry[mImages.size()])); - } else { - super.addToMap(mapView); + return; } + + super.addToMap(mapView); } @Override @@ -67,6 +84,12 @@ public class RCTMGLShapeSource extends RCTSource { mMap.removeImage(image.getKey()); } } + + if (hasNativeImages()) { + for (Map.Entry image : mNativeImages) { + mMap.removeImage(image.getKey()); + } + } } @Override @@ -124,6 +147,10 @@ public class RCTMGLShapeSource extends RCTSource { mImages = images; } + public void setNativeImages(List> nativeImages) { + mNativeImages = nativeImages; + } + private GeoJsonOptions getOptions() { GeoJsonOptions options = new GeoJsonOptions(); @@ -157,4 +184,8 @@ public class RCTMGLShapeSource extends RCTSource { private boolean hasImages() { return mImages != null && mImages.size() > 0; } + + private boolean hasNativeImages() { + return mNativeImages != null && mNativeImages.size() > 0; + } } diff --git a/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLShapeSourceManager.java b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLShapeSourceManager.java index 294327c..2bad13a 100644 --- a/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLShapeSourceManager.java +++ b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLShapeSourceManager.java @@ -1,12 +1,19 @@ package com.mapbox.rctmgl.components.styles.sources; +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.util.Log; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; +import com.mapbox.rctmgl.components.mapview.RCTMGLMapView; +import com.mapbox.rctmgl.utils.ResourceUtils; import java.net.MalformedURLException; import java.net.URL; @@ -23,6 +30,12 @@ public class RCTMGLShapeSourceManager extends ViewGroupManager> resources = new ArrayList<>(); + + for (int i = 0; i < arr.size(); i++) { + String resourceName = arr.getString(i); + BitmapDrawable drawable = (BitmapDrawable) ResourceUtils.getDrawableByName(mContext, resourceName); + + if (drawable != null) { + resources.add(new AbstractMap.SimpleEntry(resourceName, drawable)); + } + } + + source.setNativeImages(resources); + } } diff --git a/android/rctmgl/src/main/java/com/mapbox/rctmgl/utils/ResourceUtils.java b/android/rctmgl/src/main/java/com/mapbox/rctmgl/utils/ResourceUtils.java new file mode 100644 index 0000000..7fa10e3 --- /dev/null +++ b/android/rctmgl/src/main/java/com/mapbox/rctmgl/utils/ResourceUtils.java @@ -0,0 +1,30 @@ +package com.mapbox.rctmgl.utils; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; + +/** + * Created by nickitaliano on 10/19/17. + */ + +public class ResourceUtils { + public static Drawable getDrawableByName(Context context, String resourceName) { + if (context == null || resourceName == null || resourceName.isEmpty()) { + return null; + } + + Resources resources = context.getResources(); + if (resources == null) { + return null; + } + + final int resID = resources.getIdentifier(resourceName, "drawable", context.getPackageName()); + if (resID == 0) { + return null; + } + + return ContextCompat.getDrawable(context, resID); + } +} diff --git a/ios/RCTMGL/RCTMGLShapeSource.h b/ios/RCTMGL/RCTMGLShapeSource.h index 6fc8f44..70c3fae 100644 --- a/ios/RCTMGL/RCTMGLShapeSource.h +++ b/ios/RCTMGL/RCTMGLShapeSource.h @@ -18,6 +18,7 @@ @property (nonatomic, copy) NSString *url; @property (nonatomic, copy) NSString *shape; @property (nonatomic, strong) NSDictionary *images; +@property (nonatomic, strong) NSArray *nativeImages; @property (nonatomic, assign) NSNumber *cluster; @property (nonatomic, assign) NSNumber *clusterRadius; diff --git a/ios/RCTMGL/RCTMGLShapeSource.m b/ios/RCTMGL/RCTMGLShapeSource.m index a2db600..b74a7c3 100644 --- a/ios/RCTMGL/RCTMGLShapeSource.m +++ b/ios/RCTMGL/RCTMGLShapeSource.m @@ -27,9 +27,15 @@ return; } - if (_images == nil || _images.count == 0) { + if (![self _hasImages] && ![self _hasNativeImages]) { [super addToMap]; } else { + if ([self _hasNativeImages]) { + for (NSString *imageName in _nativeImages) { + UIImage *image = [UIImage imageNamed:imageName]; + [self.map.style setImage:image forName:imageName]; + } + } [RCTMGLUtils fetchImages:_bridge style:self.map.style objects:_images callback:^{ [super addToMap]; }]; } } @@ -42,13 +48,19 @@ [super removeFromMap]; - if (_images != nil && _images.count > 0) { + if ([self _hasImages]) { NSArray *imageNames = _images.allKeys; for (NSString *imageName in imageNames) { [self.map.style removeImageForName:imageName]; } } + + if ([self _hasNativeImages]) { + for (NSString *imageName in _nativeImages) { + [self.map.style removeImageForName:imageName]; + } + } } - (MGLSource*)makeSource @@ -95,4 +107,14 @@ return options; } +- (BOOL)_hasImages +{ + return _images != nil && _images.count > 0; +} + +- (BOOL)_hasNativeImages +{ + return _nativeImages != nil && _nativeImages.count > 0; +} + @end diff --git a/ios/RCTMGL/RCTMGLShapeSourceManager.m b/ios/RCTMGL/RCTMGLShapeSourceManager.m index be6dbfb..9e5de99 100644 --- a/ios/RCTMGL/RCTMGLShapeSourceManager.m +++ b/ios/RCTMGL/RCTMGLShapeSourceManager.m @@ -24,6 +24,7 @@ RCT_EXPORT_VIEW_PROPERTY(maxZoom, NSNumber) RCT_EXPORT_VIEW_PROPERTY(buffer, NSNumber) RCT_EXPORT_VIEW_PROPERTY(tolerance, NSNumber) RCT_EXPORT_VIEW_PROPERTY(images, NSDictionary) +RCT_EXPORT_VIEW_PROPERTY(nativeImages, NSArray) - (UIView*)view { diff --git a/ios/RCTMGL/RCTMGLUtils.m b/ios/RCTMGL/RCTMGLUtils.m index 58bdf94..73f73c8 100644 --- a/ios/RCTMGL/RCTMGLUtils.m +++ b/ios/RCTMGL/RCTMGLUtils.m @@ -65,42 +65,43 @@ static double const MS_TO_S = 0.001; + (void)fetchImages:(RCTBridge *)bridge style:(MGLStyle *)style objects:(NSDictionary*)objects callback:(void (^)())callback { - dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_queue_t mainQueue = dispatch_get_main_queue(); - - dispatch_group_t imageQueueGroup = dispatch_group_create(); + if (objects == nil) { + callback(); + return; + } NSArray *imageNames = objects.allKeys; + if (imageNames.count == 0) { + callback(); + return; + } + + dispatch_semaphore_t sema = dispatch_semaphore_create(0); __block NSUInteger imagesLeftToLoad = imageNames.count; - dispatch_group_async(imageQueueGroup, concurrentQueue, ^{ - dispatch_group_enter(imageQueueGroup); + void (^imageLoadedBlock)() = ^{ + imagesLeftToLoad--; - void (^imageLoadedBlock)() = ^{ - imagesLeftToLoad--; - - if (imagesLeftToLoad == 0) { - dispatch_group_leave(imageQueueGroup); - } - }; - - for (NSString *imageName in imageNames) { - UIImage *foundImage = [style imageForName:imageName]; - - if (foundImage == nil) { - [RCTMGLUtils fetchImage:bridge url:objects[imageName] callback:^(NSError *error, UIImage *image) { - dispatch_async(mainQueue, ^{ - [style setImage:image forName:imageName]; - imageLoadedBlock(); - }); - }]; - } else { - imageLoadedBlock(); - } + if (imagesLeftToLoad == 0) { + dispatch_semaphore_signal(sema); } - }); + }; - dispatch_group_notify(imageQueueGroup, mainQueue, ^{ callback(); }); + for (NSString *imageName in imageNames) { + UIImage *foundImage = [style imageForName:imageName]; + + if (foundImage == nil) { + [RCTMGLUtils fetchImage:bridge url:objects[imageName] callback:^(NSError *error, UIImage *image) { + [style setImage:image forName:imageName]; + imageLoadedBlock(); + }]; + } else { + imageLoadedBlock(); + } + } + + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + callback(); } @end diff --git a/javascript/components/ShapeSource.js b/javascript/components/ShapeSource.js index d950782..93729df 100644 --- a/javascript/components/ShapeSource.js +++ b/javascript/components/ShapeSource.js @@ -8,13 +8,17 @@ const MapboxGL = NativeModules.MGLModule; export const NATIVE_MODULE_NAME = 'RCTMGLShapeSource'; -const RCTMGLShapeSource = requireNativeComponent(NATIVE_MODULE_NAME, ShapeSource); +const RCTMGLShapeSource = requireNativeComponent(NATIVE_MODULE_NAME, ShapeSource, { + nativeOnly: { nativeImages: true }, +}); /** * ShapeSource is a map content source that supplies vector shapes to be shown on the map. * The shape may be a url or a GeoJSON object */ class ShapeSource extends React.Component { + static NATIVE_ASSETS_KEY = 'assets'; + static propTypes = { /** * A string that uniquely identifies the source. @@ -73,8 +77,10 @@ class ShapeSource extends React.Component { tolerance: PropTypes.number, /** -- * Specifies the external images in key-value pairs required for the shape source. -- */ + * Specifies the external images in key-value pairs required for the shape source. + * If you have an asset under Image.xcassets on iOS and the drawables directory on android + * you can specify an array of string names with assets as the key `{ assets: ['pin'] }`. + */ images: PropTypes.object, }; @@ -96,17 +102,25 @@ class ShapeSource extends React.Component { } let images = {}; + let nativeImages = []; const imageNames = Object.keys(this.props.images); for (let imageName of imageNames) { - const res = resolveAssetSource(this.props.images[imageName]); + if (imageName === ShapeSource.NATIVE_ASSETS_KEY && Array.isArray(this.props.images[ShapeSource.NATIVE_ASSETS_KEY])) { + nativeImages = this.props.images[ShapeSource.NATIVE_ASSETS_KEY]; + continue; + } + const res = resolveAssetSource(this.props.images[imageName]); if (res && res.uri) { images[imageName] = res.uri; } } - return images; + return { + images: images, + nativeImages: nativeImages, + }; } render () { @@ -120,7 +134,7 @@ class ShapeSource extends React.Component { maxZoomLevel: this.props.maxZoomLevel, buffer: this.props.buffer, tolerance: this.props.tolerance, - images: this._getImages(), + ...this._getImages(), }; return (