From 6000aa3fb834bb77cb0eec62f617fa63971894b8 Mon Sep 17 00:00:00 2001 From: Miklos Fazekas Date: Tue, 9 Jun 2020 16:59:04 +0200 Subject: [PATCH] Added ShapeSource.features and features can throw if source is not yet available --- .../styles/sources/RCTMGLShapeSource.java | 23 +++++++++++ .../sources/RCTMGLShapeSourceManager.java | 27 +++++++++++++ .../styles/sources/RCTMGLVectorSource.java | 7 ++++ .../mapbox/rctmgl/utils/ExpressionParser.java | 4 +- docs/ShapeSource.md | 16 ++++++++ docs/docs.json | 27 +++++++++++++ .../components/NativeBridgeComponent.js | 15 +++++--- javascript/components/ShapeSource.js | 38 ++++++++++++++++++- 8 files changed, 150 insertions(+), 7 deletions(-) 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 447b262d5..c5467dd0a 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 @@ -5,16 +5,22 @@ import android.graphics.drawable.BitmapDrawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.Size; import androidx.core.content.res.ResourcesCompat; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; import com.mapbox.geojson.Feature; +import com.mapbox.geojson.FeatureCollection; import com.mapbox.mapboxsdk.maps.MapboxMap; import com.mapbox.mapboxsdk.maps.Style; +import com.mapbox.mapboxsdk.style.expressions.Expression; import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions; import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; import com.mapbox.mapboxsdk.utils.BitmapUtils; import com.mapbox.rctmgl.R; import com.mapbox.rctmgl.components.mapview.RCTMGLMapView; +import com.mapbox.rctmgl.events.AndroidCallbackEvent; import com.mapbox.rctmgl.events.FeatureClickEvent; import com.mapbox.rctmgl.utils.DownloadMapImageTask; import com.mapbox.rctmgl.utils.ImageEntry; @@ -148,4 +154,21 @@ private GeoJsonOptions getOptions() { return options; } + + public void querySourceFeatures(String callbackID, + @Nullable Expression filter) { + if (mSource == null) { + WritableMap payload = new WritableNativeMap(); + payload.putString("error", "source is not yet loaded"); + AndroidCallbackEvent event = new AndroidCallbackEvent(this, callbackID, payload); + mManager.handleEvent(event); + return; + } + List features = mSource.querySourceFeatures(filter); + WritableMap payload = new WritableNativeMap(); + payload.putString("data", FeatureCollection.fromFeatures(features).toJson()); + + AndroidCallbackEvent event = new AndroidCallbackEvent(this, callbackID, payload); + mManager.handleEvent(event); + } } 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 51ecfbead..75669814a 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 @@ -7,6 +7,8 @@ import android.util.Log; import android.view.View; +import androidx.annotation.Nullable; + import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -21,6 +23,7 @@ import com.mapbox.rctmgl.components.mapview.RCTMGLMapView; import com.mapbox.rctmgl.components.styles.layers.RCTLayer; import com.mapbox.rctmgl.events.constants.EventKeys; +import com.mapbox.rctmgl.utils.ExpressionParser; import com.mapbox.rctmgl.utils.ImageEntry; import com.mapbox.rctmgl.utils.ResourceUtils; @@ -139,6 +142,30 @@ public void setHitbox(RCTMGLShapeSource source, ReadableMap map) { public Map customEvents() { return MapBuilder.builder() .put(EventKeys.SHAPE_SOURCE_LAYER_CLICK, "onMapboxShapeSourcePress") + .put(EventKeys.MAP_ANDROID_CALLBACK, "onAndroidCallback") + .build(); + } + + //region React Methods + public static final int METHOD_FEATURES = 103; + + @Nullable + @Override + public Map getCommandsMap() { + return MapBuilder.builder() + .put("features", METHOD_FEATURES) .build(); } + + @Override + public void receiveCommand(RCTMGLShapeSource source, int commandID, @Nullable ReadableArray args) { + switch (commandID) { + case METHOD_FEATURES: + source.querySourceFeatures( + args.getString(0), + ExpressionParser.from(args.getArray(1)) + ); + break; + } + } } diff --git a/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLVectorSource.java b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLVectorSource.java index 6975aa747..de69eadc3 100644 --- a/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLVectorSource.java +++ b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLVectorSource.java @@ -47,6 +47,13 @@ public VectorSource makeSource() { public void querySourceFeatures(String callbackID, @Size(min = 1) List layerIDs, @Nullable Expression filter) { + if (mSource == null) { + WritableMap payload = new WritableNativeMap(); + payload.putString("error", "source is not yet loaded"); + AndroidCallbackEvent event = new AndroidCallbackEvent(this, callbackID, payload); + mManager.handleEvent(event); + return; + } List features = mSource.querySourceFeatures(layerIDs.toArray(new String[layerIDs.size()]), filter); WritableMap payload = new WritableNativeMap(); payload.putString("data", FeatureCollection.fromFeatures(features).toJson()); diff --git a/android/rctmgl/src/main/java/com/mapbox/rctmgl/utils/ExpressionParser.java b/android/rctmgl/src/main/java/com/mapbox/rctmgl/utils/ExpressionParser.java index a97944d2e..d63aa5130 100644 --- a/android/rctmgl/src/main/java/com/mapbox/rctmgl/utils/ExpressionParser.java +++ b/android/rctmgl/src/main/java/com/mapbox/rctmgl/utils/ExpressionParser.java @@ -11,6 +11,8 @@ import java.util.Locale; +import javax.annotation.Nullable; + public class ExpressionParser { static final String TYPE_STRING = "string"; static final String TYPE_ARRAY = "array"; @@ -18,7 +20,7 @@ public class ExpressionParser { static final String TYPE_MAP = "hashmap"; static final String TYPE_BOOL = "boolean"; - public static Expression from(ReadableArray rawExpressions) { + public static @Nullable Expression from(@Nullable ReadableArray rawExpressions) { if (rawExpressions == null || rawExpressions.size() == 0) { return null; } diff --git a/docs/ShapeSource.md b/docs/ShapeSource.md index 4c2f1b2bf..2dfb9bd0c 100644 --- a/docs/ShapeSource.md +++ b/docs/ShapeSource.md @@ -20,6 +20,22 @@ |   height | `number` | `none` | `true` | FIX ME NO DESCRIPTION | ### methods +#### features([filter]) + +Returns all features from the source that match the query parameters regardless of whether or not the feature is
currently rendered on the map. + +##### arguments +| Name | Type | Required | Description | +| ---- | :--: | :------: | :----------: | +| `filter` | `Array` | `No` | an optional filter statement to filter the returned Features. | + + + +```javascript +shapeSource.features() +``` + + #### onPress(event) diff --git a/docs/docs.json b/docs/docs.json index 428e3d2df..096a7913d 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -3530,6 +3530,33 @@ "description": "ShapeSource is a map content source that supplies vector shapes to be shown on the map.\nThe shape may be a url or a GeoJSON object", "displayName": "ShapeSource", "methods": [ + { + "name": "features", + "docblock": "Returns all features from the source that match the query parameters regardless of whether or not the feature is\ncurrently rendered on the map.\n\n@example\nshapeSource.features()\n\n@param {Array=} filter - an optional filter statement to filter the returned Features.\n@return {FeatureCollection}", + "modifiers": [ + "async" + ], + "params": [ + { + "name": "filter", + "description": "an optional filter statement to filter the returned Features.", + "type": { + "name": "Array" + }, + "optional": true + } + ], + "returns": { + "description": null, + "type": { + "name": "FeatureCollection" + } + }, + "description": "Returns all features from the source that match the query parameters regardless of whether or not the feature is\ncurrently rendered on the map.", + "examples": [ + "\nshapeSource.features()\n\n" + ] + }, { "name": "onPress", "docblock": null, diff --git a/javascript/components/NativeBridgeComponent.js b/javascript/components/NativeBridgeComponent.js index 733141b05..9e4c4824f 100644 --- a/javascript/components/NativeBridgeComponent.js +++ b/javascript/components/NativeBridgeComponent.js @@ -13,8 +13,8 @@ const NativeBridgeComponent = (B) => this._preRefMapMethodQueue = []; } - _addAddAndroidCallback(id, callback) { - this._callbackMap.set(id, callback); + _addAddAndroidCallback(id, resolve, reject) { + this._callbackMap.set(id, {resolve, reject}); } _removeAndroidCallback(id) { @@ -30,7 +30,12 @@ const NativeBridgeComponent = (B) => } this._callbackMap.delete(callbackID); - callback.call(null, e.nativeEvent.payload); + let {payload} = e.nativeEvent; + if (payload.error) { + callback.reject.call(null, new Error(payload.error)); + } else { + callback.resolve.call(null, payload); + } } async _runPendingNativeCommands(nativeRef) { @@ -61,10 +66,10 @@ const NativeBridgeComponent = (B) => } if (isAndroid()) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { callbackIncrement += 1; const callbackID = `${methodName}_${callbackIncrement}`; - this._addAddAndroidCallback(callbackID, resolve); + this._addAddAndroidCallback(callbackID, resolve, reject); args.unshift(callbackID); runNativeCommand(this._nativeModuleName, methodName, nativeRef, args); }); diff --git a/javascript/components/ShapeSource.js b/javascript/components/ShapeSource.js index 9f7449145..05b7e303c 100644 --- a/javascript/components/ShapeSource.js +++ b/javascript/components/ShapeSource.js @@ -2,15 +2,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import {NativeModules, requireNativeComponent} from 'react-native'; +import {getFilter} from '../utils/filterUtils'; import { toJSONString, cloneReactChildrenWithProps, viewPropTypes, isFunction, + isAndroid, } from '../utils'; import {copyPropertiesAsDeprecated} from '../utils/deprecation'; import AbstractSource from './AbstractSource'; +import NativeBridgeComponent from './NativeBridgeComponent'; const MapboxGL = NativeModules.MGLModule; @@ -20,7 +23,7 @@ export const NATIVE_MODULE_NAME = 'RCTMGLShapeSource'; * 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 AbstractSource { +class ShapeSource extends NativeBridgeComponent(AbstractSource) { static NATIVE_ASSETS_KEY = 'assets'; static propTypes = { @@ -107,6 +110,37 @@ class ShapeSource extends AbstractSource { id: MapboxGL.StyleSource.DefaultSourceID, }; + constructor(props) { + super(props, NATIVE_MODULE_NAME); + } + + _setNativeRef(nativeRef) { + this._nativeRef = nativeRef; + super._runPendingNativeCommands(nativeRef); + } + + /** + * Returns all features from the source that match the query parameters regardless of whether or not the feature is + * currently rendered on the map. + * + * @example + * shapeSource.features() + * + * @param {Array=} filter - an optional filter statement to filter the returned Features. + * @return {FeatureCollection} + */ + async features(filter = []) { + const res = await this._runNativeCommand('features', this._nativeRef, [ + getFilter(filter), + ]); + + if (isAndroid()) { + return JSON.parse(res.data); + } + + return res.data; + } + setNativeProps(props) { const shallowProps = Object.assign({}, props); @@ -169,6 +203,8 @@ class ShapeSource extends AbstractSource { buffer: this.props.buffer, tolerance: this.props.tolerance, onPress: undefined, + ref: (nativeRef) => this._setNativeRef(nativeRef), + onAndroidCallback: isAndroid() ? this._onAndroidCallback : undefined, }; return (