3
votes

Currently I'm working on a project that requires me to use react-native. The problem that I'm facing seems simple considering how this is handled in the Mapbox Android SDK. The situation is the following:

I'm showing a full screen map with some buttons for zooming and centering on user location. On this map there are several 'points of interest'. Currently I'm clustering them using a ShapeSource for the data and a SymbolLayer for single and a SymbolLayer for clustered 'points of interest'.

What I'm trying to achieve is get access to all the leaves of one of the clusters I'm clicking on. In the Android SDK this could be achieved using the following code:

 String[] layers = new String[] {"cluster-route-0", "cluster-route-1", "cluster-route-2", "cluster-route-3", "cluster-route-4"};


 features = mMapboxMap.queryRenderedFeatures(pixel, layers);
 GeoJsonSource source = mMapboxMap.getStyle().getSourceAs(ROUTE_SOURCE_ID);

Then I would loop through all the features and get the cluster leaves like so

for (Feature feature : features) {
   FeatureCollection collection = source.getClusterLeaves(feature, 100,0);
}

Then the collection variable would be a FeatureCollection that contains all the features or leaves that are contained in a cluster.

Looking through the documentation I couldn't find something similar in the React Native Mapbox variant. So I was wondering if anyone here was able to find a solution for this. Or maybe I've overlooked something in either the examples or documentation.

1

1 Answers

1
votes

Recently I was able to take a deeper look at the problem described above and actually come to a solution that was good enough for my use case. There are several solutions possible to overcome this problem.

1) Using another cluster solution in combination with Mapbox eg Supercluster

2) Fork the Mapbox repro and add a method that invokes the getClusterLeaves() method from the native SDK

Solution 1

The first solution uses the Supercluster npm package and this solution is entirely based on the information found on this page. I'll share the code I ended up with but for more information, I would like to encourage you to read the resource linked.

//Initialize supercluster, these values worked great for me
const cluster = new Supercluster({ radius: 40, maxZoom: 16 });
cluster.load(collection.features);

The collection variable used in the load method of Supercluster should represent a FeatureCollection value. Which you can create in several ways. The one that worked well for me was the one below.

const collection = MapboxGL.geoUtils.makeFeatureCollection(groupFeatures);

The groupFeatures variable can be an array consisting of Features to display on the map. In order for easy access, I've stored the cluster variable in my state before moving on. I like to keep things separate so it is easy for me to see which portion of the code is responsible for what. So my Render method looks something like below.

<MapboxGL.MapView
  ref={c => (this._map = c)}
  onRegionDidChange={this.updateClusters}
  zoomEnabled
  style={[{ flex: 1 }]}>

 //This is where we render the clusters
 {this.renderPoints()}

</MapboxGL.MapView>

The next step is to get the actual clusters using supercluster in order to do that we need the bounds of the map we're displaying and the current zoom level (using Math.round since decimal values could result in errors within supercluster).

const bounds = await this._map.getVisibleBounds();
const zoom = Math.round(await this._map.getZoom());
const westLng = bounds[1][0];
const southLat = bounds[1][1];
const eastLng = bounds[0][0];
const northLat = bounds[0][1];

Now we're able to retrieve the actual clusters doing something like such:

const sc = this.state.superCluster;
const clusters = sc.getClusters(
    [westLng, southLat, eastLng, northLat],
    zoom
)
this.setState({clusters: clusters})

And now we are able to render the clusters in the Mapbox Shapesource

renderPoints = () => {
  const { clusters } = this.state;
  return (
   <MapboxGL.ShapeSource
     id="symbolLocationSource"
     hitbox={{ width: 18, height: 18 }}
     onPress={this.onMarkerSelected}
     shape={{ type: "FeatureCollection", features: superClusterClusters }}
    >
    <MapboxGL.SymbolLayer
     id="pointCount"
     minZoomLevel={6}
     style={mapStyles.clusterCount}
    />

    <MapboxGL.CircleLayer
     id="clusteredPoints"
     minZoomLevel={6}
     belowLayerID="pointCount"
     filter={[">", "point_count", 1]}
     style={mapStyles.clusteredPoints}
    />

    <MapboxGL.SymbolLayer
     id="symbolLocationSymbols"
     minZoomLevel={6}
     filter={["!", ["has", "point_count"]]}
     style={mapStyles.icon}
    />
   </MapboxGL.ShapeSource>
 );
}

So now for the last piece of the puzzle, we are able to get the actual leaves of a cluster. In the last part of the code above I've already added the onMarkerPressed method for the onPress. This is were we obtain the leaves of the cluster.

onMarkerSelected = event => {
  const point = event.nativeEvent.payload;
  const { name, cluster } = point.properties;

  if (cluster) {
    const sc = this.state.superCluster;
    const points = sc
      .getLeaves(point.properties.cluster_id, Infinity)
    //This will output an array with all leaves of the selected cluster
    console.log(points);
  }
}

Solution 2

So the solution mentioned above is the solution I chose for my own situation. So I didn't explore the solution of forking the Mapbox repro and adding functionality to it. I however found some pointers to get you started following the link here and here