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 f8737e10a..d082b7bfc 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 @@ -1,11 +1,20 @@ package com.mapbox.rctmgl.components.styles.sources; import android.content.Context; +import android.support.annotation.Nullable; +import android.support.annotation.Size; +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.style.expressions.Expression; import com.mapbox.mapboxsdk.style.sources.VectorSource; +import com.mapbox.rctmgl.events.AndroidCallbackEvent; import com.mapbox.rctmgl.events.FeatureClickEvent; +import java.util.List; + /** * Created by nickitaliano on 9/8/17. */ @@ -34,4 +43,15 @@ public VectorSource makeSource() { } return new VectorSource(mID, mURL); } + + public void querySourceFeatures(String callbackID, + @Size(min = 1) List layerIDs, + @Nullable Expression filter) { + List features = mSource.querySourceFeatures(layerIDs.toArray(new String[layerIDs.size()]), 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/RCTMGLVectorSourceManager.java b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLVectorSourceManager.java index 8763bf0b0..b0cdd27d3 100644 --- a/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLVectorSourceManager.java +++ b/android/rctmgl/src/main/java/com/mapbox/rctmgl/components/styles/sources/RCTMGLVectorSourceManager.java @@ -1,8 +1,10 @@ package com.mapbox.rctmgl.components.styles.sources; +import android.support.annotation.Nullable; import android.view.View; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.ThemedReactContext; @@ -10,6 +12,8 @@ import com.mapbox.rctmgl.components.AbstractEventEmitter; import com.mapbox.rctmgl.components.mapview.RCTMGLMapView; import com.mapbox.rctmgl.events.constants.EventKeys; +import com.mapbox.rctmgl.utils.ConvertUtils; +import com.mapbox.rctmgl.utils.ExpressionParser; import java.util.HashMap; import java.util.Map; @@ -79,6 +83,32 @@ public void setHitbox(RCTMGLVectorSource source, ReadableMap map) { public Map customEvents() { return MapBuilder.builder() .put(EventKeys.VECTOR_SOURCE_LAYER_CLICK, "onMapboxVectorSourcePress") + .put(EventKeys.MAP_ANDROID_CALLBACK, "onAndroidCallback") .build(); } + + //region React Methods + public static final int METHOD_FEATURES = 102; + + @Nullable + @Override + public Map getCommandsMap() { + return MapBuilder.builder() + .put("features", METHOD_FEATURES) + .build(); + } + + @Override + public void receiveCommand(RCTMGLVectorSource vectorSource, int commandID, @Nullable ReadableArray args) { + + switch (commandID) { + case METHOD_FEATURES: + vectorSource.querySourceFeatures( + args.getString(0), + ConvertUtils.toStringList(args.getArray(1)), + ExpressionParser.from(args.getArray(2)) + ); + break; + } + } } diff --git a/example/src/examples/CustomVectorSource.js b/example/src/examples/CustomVectorSource.js index d91e0bf44..2960348d2 100755 --- a/example/src/examples/CustomVectorSource.js +++ b/example/src/examples/CustomVectorSource.js @@ -1,10 +1,12 @@ import React from 'react'; import MapboxGL from '@react-native-mapbox-gl/maps'; +import {Text} from 'react-native'; import sheet from '../styles/sheet'; import BaseExamplePropTypes from './common/BaseExamplePropTypes'; import Page from './common/Page'; +import Bubble from './common/Bubble'; const styles = { boxFill: { @@ -30,7 +32,19 @@ class CustomVectorSource extends React.PureComponent { ...BaseExamplePropTypes, }; + state = { + featuresCount: null, + }; + + queryFeatures = async () => { + const features = await this._vectorSource.features([ + 'react-native-example', + ]); + this.setState({featuresCount: features.features.length}); + }; + render() { + const {featuresCount} = this.state; return ( @@ -42,6 +56,9 @@ class CustomVectorSource extends React.PureComponent { { + this._vectorSource = source; + }} > + + Query features: + {featuresCount && Count: {featuresCount}} + ); } diff --git a/ios/RCTMGL/RCTMGLVectorSource.h b/ios/RCTMGL/RCTMGLVectorSource.h index 43bac54b8..cb0ebbc46 100644 --- a/ios/RCTMGL/RCTMGLVectorSource.h +++ b/ios/RCTMGL/RCTMGLVectorSource.h @@ -13,4 +13,6 @@ @property (nonatomic, copy) NSString *url; +- (NSArray> *)featuresInSourceLayersWithIdentifiers:(NSSet *)sourceLayerIdentifiers predicate:(nullable NSPredicate *)predicate; + @end diff --git a/ios/RCTMGL/RCTMGLVectorSource.m b/ios/RCTMGL/RCTMGLVectorSource.m index fbdb8e9ec..361b4b3dd 100644 --- a/ios/RCTMGL/RCTMGLVectorSource.m +++ b/ios/RCTMGL/RCTMGLVectorSource.m @@ -15,4 +15,11 @@ - (MGLSource*)makeSource return [[MGLVectorTileSource alloc] initWithIdentifier:self.id configurationURL:[NSURL URLWithString:_url]]; } +- (NSArray> *)featuresInSourceLayersWithIdentifiers:(NSSet *)sourceLayerIdentifiers predicate:(nullable NSPredicate *)predicate +{ + MGLVectorTileSource* vectorSource = (MGLVectorTileSource*)self.source; + + return [vectorSource featuresInSourceLayersWithIdentifiers:sourceLayerIdentifiers predicate: predicate]; +} + @end diff --git a/ios/RCTMGL/RCTMGLVectorSourceManager.h b/ios/RCTMGL/RCTMGLVectorSourceManager.h index bc9ee5424..5d1b5cefd 100644 --- a/ios/RCTMGL/RCTMGLVectorSourceManager.h +++ b/ios/RCTMGL/RCTMGLVectorSourceManager.h @@ -7,7 +7,8 @@ // #import "ViewManager.h" +#import -@interface RCTMGLVectorSourceManager : ViewManager +@interface RCTMGLVectorSourceManager : ViewManager @end diff --git a/ios/RCTMGL/RCTMGLVectorSourceManager.m b/ios/RCTMGL/RCTMGLVectorSourceManager.m index 2ac54a13b..d5825f195 100644 --- a/ios/RCTMGL/RCTMGLVectorSourceManager.m +++ b/ios/RCTMGL/RCTMGLVectorSourceManager.m @@ -6,12 +6,16 @@ // Copyright © 2017 Mapbox Inc. All rights reserved. // +#import + #import "RCTMGLVectorSourceManager.h" #import "RCTMGLVectorSource.h" +#import "FilterParser.h" + @implementation RCTMGLVectorSourceManager -RCT_EXPORT_MODULE(); +RCT_EXPORT_MODULE(RCTMGLVectorSource); RCT_EXPORT_VIEW_PROPERTY(id, NSString); @@ -25,4 +29,44 @@ - (UIView*)view RCT_REMAP_VIEW_PROPERTY(onMapboxVectorSourcePress, onPress, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(hitbox, NSDictionary) + +RCT_EXPORT_METHOD(features:(nonnull NSNumber*)reactTag + withLayerIDs:(NSArray*)layerIDs + withFilter:(NSArray *> *)filter + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *manager, NSDictionary *viewRegistry) { + RCTMGLVectorSource* vectorSource = viewRegistry[reactTag]; + + if (![vectorSource isKindOfClass:[RCTMGLVectorSource class]]) { + RCTLogError(@"Invalid react tag, could not find RCTMGLMapView"); + return; + } + + NSSet* layerIDSet = nil; + if (layerIDs != nil && layerIDs.count > 0) { + layerIDSet = [NSSet setWithArray:layerIDs]; + } + NSPredicate* predicate = [FilterParser parse:filter]; + NSArray> *shapes = [vectorSource + featuresInSourceLayersWithIdentifiers: layerIDSet + predicate: predicate]; + + NSMutableArray *features = [[NSMutableArray alloc] initWithCapacity:shapes.count]; + for (int i = 0; i < shapes.count; i++) { + [features addObject:shapes[i].geoJSONDictionary]; + } + + resolve(@{ + @"data": @{ @"type": @"FeatureCollection", @"features": features } + }); + }]; +} + +RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location) +{ + RCTLogInfo(@"Pretending to create an event %@ at %@", name, location); +} + @end diff --git a/javascript/components/MapView.js b/javascript/components/MapView.js index b8d2dbd69..509119719 100644 --- a/javascript/components/MapView.js +++ b/javascript/components/MapView.js @@ -33,7 +33,7 @@ const styles = StyleSheet.create({ /** * MapView backed by Mapbox Native GL */ -class MapView extends NativeBridgeComponent { +class MapView extends NativeBridgeComponent(React.Component) { static propTypes = { ...viewPropTypes, diff --git a/javascript/components/NativeBridgeComponent.js b/javascript/components/NativeBridgeComponent.js index 7ff2b2a85..362b1c008 100644 --- a/javascript/components/NativeBridgeComponent.js +++ b/javascript/components/NativeBridgeComponent.js @@ -2,80 +2,81 @@ import React from 'react'; import {runNativeCommand, isAndroid} from '../utils'; -class NativeBridgeComponent extends React.Component { - static callbackIncrement = 0; +let callbackIncrement = 0; - constructor(props, nativeModuleName) { - super(props); +const NativeBridgeComponent = B => + class extends B { + constructor(props, nativeModuleName) { + super(props); - this._nativeModuleName = nativeModuleName; - this._onAndroidCallback = this._onAndroidCallback.bind(this); - this._callbackMap = new Map(); - this._preRefMapMethodQueue = []; - } + this._nativeModuleName = nativeModuleName; + this._onAndroidCallback = this._onAndroidCallback.bind(this); + this._callbackMap = new Map(); + this._preRefMapMethodQueue = []; + } + + _addAddAndroidCallback(id, callback) { + this._callbackMap.set(id, callback); + } - _addAddAndroidCallback(id, callback) { - this._callbackMap.set(id, callback); - } + _removeAndroidCallback(id) { + this._callbackMap.remove(id); + } - _removeAndroidCallback(id) { - this._callbackMap.remove(id); - } + _onAndroidCallback(e) { + const callbackID = e.nativeEvent.type; + const callback = this._callbackMap.get(callbackID); - _onAndroidCallback(e) { - const callbackID = e.nativeEvent.type; - const callback = this._callbackMap.get(callbackID); + if (!callback) { + return; + } - if (!callback) { - return; + this._callbackMap.delete(callbackID); + callback.call(null, e.nativeEvent.payload); } - this._callbackMap.delete(callbackID); - callback.call(null, e.nativeEvent.payload); - } - - async _runPendingNativeCommands(nativeRef) { - if (nativeRef) - while (this._preRefMapMethodQueue.length > 0) { - const item = this._preRefMapMethodQueue.pop(); + async _runPendingNativeCommands(nativeRef) { + if (nativeRef) + while (this._preRefMapMethodQueue.length > 0) { + const item = this._preRefMapMethodQueue.pop(); - if (item && item.method && item.resolver) { - const res = await this._runNativeCommand( - item.method.name, - nativeRef, - item.method.args, - ); - item.resolver(res); + if (item && item.method && item.resolver) { + const res = await this._runNativeCommand( + item.method.name, + nativeRef, + item.method.args, + ); + item.resolver(res); + } } - } - } + } - _runNativeCommand(methodName, nativeRef, args = []) { - if (!nativeRef) { - return new Promise(resolve => { - this._preRefMapMethodQueue.push({ - method: {name: methodName, args}, - resolver: resolve, + _runNativeCommand(methodName, nativeRef, args = []) { + if (!nativeRef) { + return new Promise(resolve => { + this._preRefMapMethodQueue.push({ + method: {name: methodName, args}, + resolver: resolve, + }); }); - }); - } + } - if (isAndroid()) { - return new Promise(resolve => { - NativeBridgeComponent.callbackIncrement++; - const callbackID = `${methodName}_${NativeBridgeComponent.callbackIncrement}`; - this._addAddAndroidCallback(callbackID, resolve); - args.unshift(callbackID); - runNativeCommand(this._nativeModuleName, methodName, nativeRef, args); - }); + if (isAndroid()) { + return new Promise(resolve => { + callbackIncrement += 1; + const callbackID = `${methodName}_${callbackIncrement}`; + this._addAddAndroidCallback(callbackID, resolve); + args.unshift(callbackID); + runNativeCommand(this._nativeModuleName, methodName, nativeRef, args); + }); + } + return runNativeCommand( + this._nativeModuleName, + methodName, + nativeRef, + args, + ); } - return runNativeCommand( - this._nativeModuleName, - methodName, - nativeRef, - args, - ); - } -} + }; export default NativeBridgeComponent; diff --git a/javascript/components/VectorSource.js b/javascript/components/VectorSource.js index 65c999ad3..b2e8c57f4 100644 --- a/javascript/components/VectorSource.js +++ b/javascript/components/VectorSource.js @@ -2,9 +2,16 @@ import React from 'react'; import PropTypes from 'prop-types'; import {NativeModules, requireNativeComponent} from 'react-native'; -import {cloneReactChildrenWithProps, viewPropTypes, isFunction} from '../utils'; +import { + cloneReactChildrenWithProps, + viewPropTypes, + isFunction, + isAndroid, +} from '../utils'; +import {getFilter} from '../utils/filterUtils'; import AbstractSource from './AbstractSource'; +import NativeBridgeComponent from './NativeBridgeComponent'; const MapboxGL = NativeModules.MGLModule; @@ -14,7 +21,7 @@ export const NATIVE_MODULE_NAME = 'RCTMGLVectorSource'; * VectorSource is a map content source that supplies tiled vector data in Mapbox Vector Tile format to be shown on the map. * The location of and metadata about the tiles are defined either by an option dictionary or by an external file that conforms to the TileJSON specification. */ -class VectorSource extends AbstractSource { +class VectorSource extends NativeBridgeComponent(AbstractSource) { static propTypes = { ...viewPropTypes, @@ -47,6 +54,40 @@ class VectorSource extends AbstractSource { id: MapboxGL.StyleSource.DefaultSourceID, }; + constructor(props) { + super(props, NATIVE_MODULE_NAME); + } + + _setNativeRef(nativeRef) { + this._nativeRef = nativeRef; + super._runPendingNativeCommands(nativeRef); + } + + /** + * Returns all features that match the query parameters regardless of whether or not the feature is + * currently rendered on the map. The domain of the query includes all currently-loaded vector tiles + * and GeoJSON source tiles. This function does not check tiles outside of the visible viewport. + * + * @example + * vectorSource.features(['id1', 'id2']) + * + * @param {Array=} layerIDs - A set of strings that correspond to the names of layers defined in the current style. Only the features contained in these layers are included in the returned array. + * @param {Array=} filter - an optional filter statement to filter the returned Features. + * @return {FeatureCollection} + */ + async features(layerIDs = [], filter = []) { + const res = await this._runNativeCommand('features', this._nativeRef, [ + layerIDs, + getFilter(filter), + ]); + + if (isAndroid()) { + return JSON.parse(res.data); + } + + return res.data; + } + render() { const props = { id: this.props.id, @@ -55,6 +96,8 @@ class VectorSource extends AbstractSource { hasPressListener: isFunction(this.props.onPress), onMapboxVectorSourcePress: this.props.onPress, onPress: undefined, + ref: nativeRef => this._setNativeRef(nativeRef), + onAndroidCallback: isAndroid() ? this._onAndroidCallback : undefined, }; return (