Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Expose query feature extensions #13631

Merged
merged 1 commit into from
Jan 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,54 @@ public List<Feature> querySourceFeatures(@Nullable Expression filter) {
return features != null ? Arrays.asList(features) : new ArrayList<Feature>();
}

/**
* Returns the children of a cluster (on the next zoom level) given its id (cluster_id value from feature properties).
* <p>
* Requires configuring this source as a cluster by calling {@link GeoJsonOptions#withCluster(boolean)}.
* </p>
*
* @param cluster cluster from which to retrieve children from
* @return a list of features for the underlying children
*/
@NonNull
public FeatureCollection getClusterChildren(@NonNull Feature cluster) {
checkThread();
return FeatureCollection.fromFeatures(nativeGetClusterChildren(cluster));
}

/**
* Returns all the leaves of a cluster (given its cluster_id), with pagination support: limit is the number of leaves
* to return (set to Infinity for all points), and offset is the amount of points to skip (for pagination).
* <p>
* Requires configuring this source as a cluster by calling {@link GeoJsonOptions#withCluster(boolean)}.
* </p>
*
* @param cluster cluster from which to retrieve leaves from
* @param limit limit is the number of points to return
* @param offset offset is the amount of points to skip (for pagination)
* @return a list of features for the underlying leaves
*/
@NonNull
public FeatureCollection getClusterLeaves(@NonNull Feature cluster, long limit, long offset) {
checkThread();
return FeatureCollection.fromFeatures(nativeGetClusterLeaves(cluster, limit, offset));
}

/**
* Returns the zoom on which the cluster expands into several children (useful for "click to zoom" feature)
* given the cluster's cluster_id (cluster_id value from feature properties).
* <p>
* Requires configuring this source as a cluster by calling {@link GeoJsonOptions#withCluster(boolean)}.
* </p>
*
* @param cluster cluster from which to retrieve the expansion zoom from
* @return the zoom on which the cluster expands into several children
*/
public int getClusterExpansionZoom(@NonNull Feature cluster) {
checkThread();
return nativeGetClusterExpansionZoom(cluster);
}

@Keep
protected native void initialize(String layerId, Object options);

Expand All @@ -311,6 +359,15 @@ public List<Feature> querySourceFeatures(@Nullable Expression filter) {
@Keep
private native Feature[] querySourceFeatures(Object[] filter);

@Keep
private native Feature[] nativeGetClusterChildren(Feature feature);

@Keep
private native Feature[] nativeGetClusterLeaves(Feature feature, long limit, long offset);

@Keep
private native int nativeGetClusterExpansionZoom(Feature feature);

@Override
@Keep
protected native void finalize() throws Throwable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -802,14 +802,14 @@ class LocationComponentTest : EspressoTest() {
assertThat(mapboxMap.style?.getSource(LOCATION_SOURCE), notNullValue())
mapboxMap.waitForLayer(uiController, location, FOREGROUND_LAYER)
mapboxMap.waitForSource(uiController, LOCATION_SOURCE)
assertEquals(77.0, mapboxMap.querySourceFeatures(LOCATION_SOURCE)[0].getNumberProperty(PROPERTY_GPS_BEARING) as Double, 0.1)
assertEquals(77f, mapboxMap.querySourceFeatures(LOCATION_SOURCE)[0].getNumberProperty(PROPERTY_GPS_BEARING).toFloat(), 0.1f)

location.bearing = 92f
component.forceLocationUpdate(location)
uiController.loopMainThreadForAtLeast(MAX_ANIMATION_DURATION_MS + MAP_RENDER_DELAY) // Waiting for the animation to finish
mapboxMap.waitForSource(uiController, LOCATION_SOURCE)
mapboxMap.waitForLayer(uiController, location, FOREGROUND_LAYER)
assertEquals(92.0, mapboxMap.querySourceFeatures(LOCATION_SOURCE)[0].getNumberProperty(PROPERTY_GPS_BEARING) as Double, 0.1)
assertEquals(92.0f, mapboxMap.querySourceFeatures(LOCATION_SOURCE)[0].getNumberProperty(PROPERTY_GPS_BEARING).toFloat(), 0.1f)
}
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.mapbox.mapboxsdk.testapp.activity.style;

import android.graphics.Color;
import android.graphics.Point;
import android.graphics.PointF;
import android.os.Bundle;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.MenuItem;
import android.widget.Toast;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapView;
Expand All @@ -16,11 +21,13 @@
import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import com.mapbox.mapboxsdk.testapp.R;
import com.mapbox.mapboxsdk.testapp.utils.GeoParseUtil;
import com.mapbox.mapboxsdk.utils.BitmapUtils;
import timber.log.Timber;

import java.net.MalformedURLException;
import java.net.URL;
import java.io.IOException;
import java.util.List;
import java.util.Objects;

import static com.mapbox.mapboxsdk.style.expressions.Expression.all;
import static com.mapbox.mapboxsdk.style.expressions.Expression.division;
Expand Down Expand Up @@ -54,6 +61,9 @@ public class GeoJsonClusteringActivity extends AppCompatActivity {
private MapView mapView;
private MapboxMap mapboxMap;

private GeoJsonSource clusterSource;
private int clickOptionCounter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand All @@ -68,19 +78,119 @@ protected void onCreate(Bundle savedInstanceState) {
mapboxMap = map;
mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(37.7749, 122.4194), 0));

mapboxMap.setStyle(Style.MAPBOX_STREETS, new Style.OnStyleLoaded() {
@Override
public void onStyleLoaded(Style style) {
style.addImage(
"icon-id",
BitmapUtils.getBitmapFromDrawable(getResources().getDrawable(R.drawable.ic_hearing_black_24dp)),
true
);
// Add a clustered source with some layers
addClusteredGeoJsonSource(style);
final int[][] clusterLayers = new int[][] {
new int[] {150, ResourcesCompat.getColor(getResources(), R.color.redAccent, getTheme())},
new int[] {20, ResourcesCompat.getColor(getResources(), R.color.greenAccent, getTheme())},
new int[] {0, ResourcesCompat.getColor(getResources(), R.color.blueAccent, getTheme())}
};

try {
mapboxMap.setStyle(new Style.Builder()
.fromUrl(Style.LIGHT)
.withSource(clusterSource = createClusterSource())
.withLayer(createSymbolLayer())
.withLayer(createClusterLevelLayer(0, clusterLayers))
.withLayer(createClusterLevelLayer(1, clusterLayers))
.withLayer(createClusterLevelLayer(2, clusterLayers))
.withLayer(createClusterTextLayer())
.withImage("icon-id", Objects.requireNonNull(
BitmapUtils.getBitmapFromDrawable(getResources().getDrawable(R.drawable.ic_hearing_black_24dp))), true
)
);
} catch (IOException exception) {
Timber.e(exception);
}

mapboxMap.addOnMapClickListener(latLng -> {
PointF point = mapboxMap.getProjection().toScreenLocation(latLng);
List<Feature> features = mapboxMap.queryRenderedFeatures(point, "cluster-0", "cluster-1", "cluster-2");
if (!features.isEmpty()) {
onClusterClick(features.get(0), new Point((int) point.x, (int) point.y));
}
return true;
});
});

findViewById(R.id.fab).setOnClickListener(v -> {
updateClickOptionCounter();
notifyClickOptionUpdate();
});
}

private void onClusterClick(Feature cluster, Point clickPoint) {
if (clickOptionCounter == 0) {
double nextZoomLevel = clusterSource.getClusterExpansionZoom(cluster);
double zoomDelta = nextZoomLevel - mapboxMap.getCameraPosition().zoom;
mapboxMap.animateCamera(CameraUpdateFactory.zoomBy(zoomDelta, clickPoint));
Toast.makeText(this, "Zooming to " + nextZoomLevel, Toast.LENGTH_SHORT).show();
} else if (clickOptionCounter == 1) {
FeatureCollection collection = clusterSource.getClusterChildren(cluster);
Toast.makeText(this, "Children: " + collection.toJson(), Toast.LENGTH_SHORT).show();
} else {
FeatureCollection collection = clusterSource.getClusterLeaves(cluster, 2, 1);
Toast.makeText(this, "Leaves: " + collection.toJson(), Toast.LENGTH_SHORT).show();
}
}

private GeoJsonSource createClusterSource() throws IOException {
String earthQuakes = GeoParseUtil.loadStringFromAssets(this, "earthquakes.geojson");
FeatureCollection featureCollection = FeatureCollection.fromJson(earthQuakes);
return new GeoJsonSource("earthquakes", featureCollection, new GeoJsonOptions()
.withCluster(true)
.withClusterMaxZoom(14)
.withClusterRadius(50)
);
}

private SymbolLayer createSymbolLayer() {
return new SymbolLayer("unclustered-points", "earthquakes")
.withProperties(
iconImage("icon-id"),
iconSize(
division(
get("mag"), literal(4.0f)
)
),
iconColor(
interpolate(exponential(1), get("mag"),
stop(2.0, rgb(0, 255, 0)),
stop(4.5, rgb(0, 0, 255)),
stop(7.0, rgb(255, 0, 0))
)
)
)
.withFilter(has("mag"));
}

private CircleLayer createClusterLevelLayer(int level, int[][] layerColors) {
CircleLayer circles = new CircleLayer("cluster-" + level, "earthquakes");
circles.setProperties(
circleColor(layerColors[level][1]),
circleRadius(18f)
);

Expression pointCount = toNumber(get("point_count"));
circles.setFilter(
level == 0
? all(has("point_count"),
gte(pointCount, literal(layerColors[level][0]))
) : all(has("point_count"),
gt(pointCount, literal(layerColors[level][0])),
lt(pointCount, literal(layerColors[level - 1][0]))
)
);
return circles;
}

private SymbolLayer createClusterTextLayer() {
return new SymbolLayer("count", "earthquakes")
.withProperties(
textField(Expression.toString(get("point_count"))),
textSize(12f),
textColor(Color.WHITE),
textIgnorePlacement(true),
textAllowOverlap(true)
);
}

@Override
Expand Down Expand Up @@ -136,82 +246,30 @@ public boolean onOptionsItemSelected(MenuItem item) {
}
}

private void addClusteredGeoJsonSource(Style style) {
// Add a clustered source
try {
style.addSource(
new GeoJsonSource("earthquakes",
new URL("https://www.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson"),
new GeoJsonOptions()
.withCluster(true)
.withClusterMaxZoom(14)
.withClusterRadius(50)
)
);
} catch (MalformedURLException malformedUrlException) {
Timber.e(malformedUrlException, "That's not an url... ");
private void updateClickOptionCounter() {
if (clickOptionCounter == 2) {
clickOptionCounter = 0;
} else {
clickOptionCounter++;
}
}

// Add unclustered layer
int[][] layers = new int[][] {
new int[] {150, ResourcesCompat.getColor(getResources(), R.color.redAccent, getTheme())},
new int[] {20, ResourcesCompat.getColor(getResources(), R.color.greenAccent, getTheme())},
new int[] {0, ResourcesCompat.getColor(getResources(), R.color.blueAccent, getTheme())}
};

SymbolLayer unclustered = new SymbolLayer("unclustered-points", "earthquakes");
unclustered.setProperties(
iconImage("icon-id"),
iconSize(
division(
get("mag"), literal(4.0f)
)
),
iconColor(
interpolate(exponential(1), get("mag"),
stop(2.0, rgb(0, 255, 0)),
stop(4.5, rgb(0, 0, 255)),
stop(7.0, rgb(255, 0, 0))
)
)
);
unclustered.setFilter(has("mag"));
style.addLayer(unclustered);

for (int i = 0; i < layers.length; i++) {
// Add some nice circles
CircleLayer circles = new CircleLayer("cluster-" + i, "earthquakes");
circles.setProperties(
circleColor(layers[i][1]),
circleRadius(18f)
);

Expression pointCount = toNumber(get("point_count"));
circles.setFilter(
i == 0
? all(has("point_count"),
gte(pointCount, literal(layers[i][0]))
) : all(has("point_count"),
gt(pointCount, literal(layers[i][0])),
lt(pointCount, literal(layers[i - 1][0]))
)
);
style.addLayer(circles);
private void notifyClickOptionUpdate() {
if (clickOptionCounter == 0) {
Toast.makeText(
GeoJsonClusteringActivity.this,
"Clicking a cluster will zoom to the level where it dissolves",
Toast.LENGTH_SHORT).show();
} else if (clickOptionCounter == 1) {
Toast.makeText(
GeoJsonClusteringActivity.this,
"Clicking a cluster will show the details of the cluster children",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(
GeoJsonClusteringActivity.this,
"Clicking a cluster will show the details of the cluster leaves with an offset and limit",
Toast.LENGTH_SHORT).show();
}

// Add the count labels
SymbolLayer count = new SymbolLayer("count", "earthquakes");
count.setProperties(
textField(Expression.toString(get("point_count"))),
textSize(12f),
textColor(Color.WHITE),
textIgnorePlacement(true),
textAllowOverlap(true)
);
style.addLayer(count);


// Zoom out to start
mapboxMap.animateCamera(CameraUpdateFactory.zoomTo(1));
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.mapbox.mapboxsdk.maps.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:id="@id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_refresh"
app:backgroundTint="@color/primaryDark"/>

</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
Loading