Add runtime styling APIs on MapSnapshotter (#268)

* Add styleable snapshotter.

* Add style API for snapshotter

* Add observer interface to snapshotter

* WIP Add styling to snapshotter activity

* Update interface and implement addlayer functions.

* Deprecate methods instead of removing

* Implement addSource functions.

* Implement addImage function

* Still use the previous callback interface.

* Expose onStyleDidloaded interface

* Add setStyle method

* Start native snapshotter from start method

* Fix snapshotter test.

* Fix check style

* Keep the original interface

* Update gl-native

* Add demo for snapshotter heatmap layer

* bump gl-native

Co-authored-by: Alexander Shalamov <alexander.shalamov@mapbox.com>
This commit is contained in:
Kevin Li
2020-03-25 09:59:38 +08:00
committed by GitHub
parent 02b5e60f86
commit da6d8a2b06
19 changed files with 565 additions and 88 deletions

View File

@@ -5,13 +5,14 @@ import androidx.test.rule.ActivityTestRule
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.style.layers.BackgroundLayer
import com.mapbox.mapboxsdk.style.layers.PropertyFactory
import com.mapbox.mapboxsdk.testapp.activity.FeatureOverviewActivity
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import junit.framework.Assert.assertNotNull
import org.junit.Assert
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -20,7 +21,6 @@ import org.junit.runner.RunWith
* Integration test that validates if a snapshotter creation
*/
@RunWith(AndroidJUnit4::class)
@Ignore("Ignored until https://github.com/mapbox/mapbox-gl-native/issues/11669 is resolved.")
class MapSnapshotterTest {
@Rule
@@ -31,18 +31,22 @@ class MapSnapshotterTest {
@Test
fun mapSnapshotter() {
var mapSnapshotter: MapSnapshotter?
rule.activity.runOnUiThread {
val bg = BackgroundLayer("rand_tint")
bg.setProperties(PropertyFactory.backgroundColor("rgba(255,128,0,0.7)"))
val options = MapSnapshotter.Options(512, 512)
.withPixelRatio(1.0f)
.withStyle(Style.SATELLITE_STREETS)
.withStyleBuilder(Style.Builder().fromUri(Style.SATELLITE_STREETS)
.withLayerAbove(bg, "country-label"))
.withCameraPosition(
CameraPosition.Builder()
.zoom(12.0)
.target(LatLng(51.145495, 5.742234))
.build()
)
val mapSnapshotter = MapSnapshotter(rule.activity, options)
mapSnapshotter.start({
mapSnapshotter = MapSnapshotter(rule.activity, options)
mapSnapshotter!!.start({
assertNotNull(it)
assertNotNull(it.bitmap)
countDownLatch.countDown()

View File

@@ -363,9 +363,21 @@
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.snapshot.MapSnapshotterMarkerActivity"
android:description="@string/description_map_snapshotter_marker"
android:label="@string/activity_map_snapshotter_marker">
android:name=".activity.snapshot.MapSnapshotterBitMapOverlayActivity"
android:description="@string/description_map_snapshotter_bitmap_overlay"
android:label="@string/activity_map_snapshotter_bitmap_overlay">
<meta-data
android:name="@string/category"
android:value="@string/category_imagegenerator" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.snapshot.MapSnapshotterHeatMapActivity"
android:description="@string/description_map_snapshotter_heatmap_layer"
android:exported="true"
android:label="@string/activity_map_snapshotter_heatmap_layer">
<meta-data
android:name="@string/category"
android:value="@string/category_imagegenerator" />

View File

@@ -68,8 +68,8 @@ public class CustomLayerActivity extends AppCompatActivity {
}
private void updateLayer() {
if (customLayer != null) {
customLayer.update();
if (mapboxMap != null) {
mapboxMap.triggerRepaint();
}
}

View File

@@ -1,5 +1,6 @@
package com.mapbox.mapboxsdk.testapp.activity.render;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
public class RenderTestDefinition {
@@ -75,7 +76,7 @@ public class RenderTestDefinition {
public MapSnapshotter.Options toOptions() {
return new MapSnapshotter
.Options(getWidth(), getHeight())
.withStyleJson(styleJson)
.withStyleBuilder(new Style.Builder().fromJson(styleJson))
.withPixelRatio(getPixelRatio())
.withLogo(false);
}

View File

@@ -1,31 +1,64 @@
package com.mapbox.mapboxsdk.testapp.activity.snapshot;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.view.ViewTreeObserver;
import android.widget.GridLayout;
import android.widget.ImageView;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.constants.MapboxConstants;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
import com.mapbox.mapboxsdk.style.layers.Property;
import com.mapbox.mapboxsdk.style.layers.RasterLayer;
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import com.mapbox.mapboxsdk.style.sources.RasterSource;
import com.mapbox.mapboxsdk.style.sources.Source;
import com.mapbox.mapboxsdk.testapp.R;
import com.mapbox.mapboxsdk.utils.BitmapUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
import static com.mapbox.mapboxsdk.style.expressions.Expression.switchCase;
import static com.mapbox.mapboxsdk.style.expressions.Expression.toBool;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAnchor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
/**
* Test activity showing how to use a the {@link com.mapbox.mapboxsdk.snapshotter.MapSnapshotter}
*/
public class MapSnapshotterActivity extends AppCompatActivity {
private static final String ID_FEATURE_PROPERTY = "id";
private static final String SELECTED_FEATURE_PROPERTY = "selected";
private static final String TITLE_FEATURE_PROPERTY = "title";
// layer & source constants
private static final String MARKER_SOURCE = "marker-source";
private static final String MARKER_LAYER = "marker-layer";
private GridLayout grid;
public GridLayout grid;
private List<MapSnapshotter> snapshotters = new ArrayList<>();
@Override
@@ -58,6 +91,9 @@ public class MapSnapshotterActivity extends AppCompatActivity {
}
private void startSnapShot(final int row, final int column) {
// Optionally the style
Style.Builder builder = new Style.Builder()
.fromUri((column + row) % 2 == 0 ? Style.MAPBOX_STREETS : Style.DARK);
// Define the dimensions
MapSnapshotter.Options options = new MapSnapshotter.Options(
@@ -66,9 +102,6 @@ public class MapSnapshotterActivity extends AppCompatActivity {
)
// Optionally the pixel ratio
.withPixelRatio(1)
// Optionally the style
.withStyle((column + row) % 2 == 0 ? Style.MAPBOX_STREETS : Style.DARK)
.withLocalIdeographFontFamily(MapboxConstants.DEFAULT_FONT);
// Optionally the visible region
@@ -88,14 +121,52 @@ public class MapSnapshotterActivity extends AppCompatActivity {
: new LatLng(randomInRange(-80, 80), randomInRange(-160, 160)))
.bearing(randomInRange(0, 360))
.tilt(randomInRange(0, 60))
.zoom(randomInRange(0, 20))
.zoom(randomInRange(0, 10))
.padding(1, 1, 1, 1)
.build()
);
}
if (row == 0 && column == 0) {
// Add a source
Source source = new RasterSource("my-raster-source", "mapbox://mapbox.satellite", 512);
builder.withLayerAbove(new RasterLayer("satellite-layer", "my-raster-source"), "country-label");
builder.withSource(source);
} else if (row == 0 && column == 2) {
Bitmap carBitmap = BitmapUtils.getBitmapFromDrawable(
getResources().getDrawable(R.drawable.ic_directions_car_black));
// marker source
FeatureCollection markerCollection = FeatureCollection.fromFeatures(new Feature[] {
Feature.fromGeometry(Point.fromLngLat(4.91638, 52.34673), featureProperties("2", "Car"))
});
Source markerSource = new GeoJsonSource(MARKER_SOURCE, markerCollection);
// marker layer
SymbolLayer markerSymbolLayer = new SymbolLayer(MARKER_LAYER, MARKER_SOURCE)
.withProperties(
iconImage(get(TITLE_FEATURE_PROPERTY)),
iconIgnorePlacement(true),
iconAllowOverlap(true),
iconSize(switchCase(toBool(get(SELECTED_FEATURE_PROPERTY)), literal(1.5f), literal(1.0f))),
iconAnchor(Property.ICON_ANCHOR_BOTTOM),
iconColor(Color.BLUE)
);
builder.withImage("Car", Objects.requireNonNull(carBitmap), false)
.withSources(markerSource)
.withLayers(markerSymbolLayer);
options.withCameraPosition(new CameraPosition.Builder()
.target(new LatLng(5.537109374999999,
52.07950600379697))
.zoom(1)
.padding(1, 1, 1, 1)
.build()
);
}
options.withStyleBuilder(builder);
MapSnapshotter snapshotter = new MapSnapshotter(MapSnapshotterActivity.this, options);
snapshotter.start(snapshot -> {
Timber.i("Got the snapshot");
ImageView imageView = new ImageView(MapSnapshotterActivity.this);
@@ -125,4 +196,11 @@ public class MapSnapshotterActivity extends AppCompatActivity {
return (random.nextFloat() * (max - min)) + min;
}
private JsonObject featureProperties(@NonNull String id, @NonNull String title) {
JsonObject object = new JsonObject();
object.add(ID_FEATURE_PROPERTY, new JsonPrimitive(id));
object.add(TITLE_FEATURE_PROPERTY, new JsonPrimitive(title));
object.add(SELECTED_FEATURE_PROPERTY, new JsonPrimitive(false));
return object;
}
}

View File

@@ -6,26 +6,29 @@ import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshot;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
import com.mapbox.mapboxsdk.testapp.R;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import timber.log.Timber;
/**
* Test activity showing how to use a the {@link MapSnapshotter} and overlay
* {@link android.graphics.Bitmap}s on top.
*/
public class MapSnapshotterMarkerActivity extends AppCompatActivity implements MapSnapshotter.SnapshotReadyCallback {
public class MapSnapshotterBitMapOverlayActivity extends AppCompatActivity
implements MapSnapshotter.SnapshotReadyCallback {
private MapSnapshotter mapSnapshotter;
private MapSnapshot mapSnapshot;
@@ -49,10 +52,10 @@ public class MapSnapshotterMarkerActivity extends AppCompatActivity implements M
getApplicationContext(),
new MapSnapshotter
.Options(Math.min(container.getMeasuredWidth(), 1024), Math.min(container.getMeasuredHeight(), 1024))
.withStyle(Style.OUTDOORS)
.withStyleBuilder(new Style.Builder().fromUri(Style.OUTDOORS))
.withCameraPosition(new CameraPosition.Builder().target(new LatLng(52.090737, 5.121420)).zoom(15).build())
);
mapSnapshotter.start(MapSnapshotterMarkerActivity.this);
mapSnapshotter.start(MapSnapshotterBitMapOverlayActivity.this);
}
});
}

View File

@@ -0,0 +1,174 @@
package com.mapbox.mapboxsdk.testapp.activity.snapshot;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshot;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
import com.mapbox.mapboxsdk.style.layers.HeatmapLayer;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import com.mapbox.mapboxsdk.style.sources.Source;
import com.mapbox.mapboxsdk.testapp.R;
import org.jetbrains.annotations.NotNull;
import java.net.URI;
import java.net.URISyntaxException;
import androidx.appcompat.app.AppCompatActivity;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
import static com.mapbox.mapboxsdk.style.expressions.Expression.heatmapDensity;
import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
import static com.mapbox.mapboxsdk.style.expressions.Expression.linear;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
import static com.mapbox.mapboxsdk.style.expressions.Expression.rgb;
import static com.mapbox.mapboxsdk.style.expressions.Expression.rgba;
import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapIntensity;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapOpacity;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapRadius;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapWeight;
/**
* Test activity showing how to use a the {@link MapSnapshotter} and heatmap layer on it.
*/
public class MapSnapshotterHeatMapActivity extends AppCompatActivity implements MapSnapshotter.SnapshotReadyCallback {
private static final String EARTHQUAKE_SOURCE_URL = "https://www.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson";
private static final String EARTHQUAKE_SOURCE_ID = "earthquakes";
private static final String HEATMAP_LAYER_ID = "earthquakes-heat";
private static final String HEATMAP_LAYER_SOURCE = "earthquakes";
private MapSnapshotter mapSnapshotter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map_snapshotter_marker);
final View container = findViewById(R.id.container);
container.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//noinspection deprecation
container.getViewTreeObserver().removeGlobalOnLayoutListener(this);
Timber.i("Starting snapshot");
Style.Builder builder = new Style.Builder().fromUri(Style.OUTDOORS)
.withSource(getEarthquakeSource())
.withLayerAbove(getHeatmapLayer(), "waterway-label");
mapSnapshotter = new MapSnapshotter(
getApplicationContext(),
new MapSnapshotter
.Options(container.getMeasuredWidth(), container.getMeasuredHeight())
.withStyleBuilder(builder)
.withCameraPosition(new CameraPosition.Builder()
.target(new LatLng(15, -94))
.zoom(5)
.padding(1, 1, 1, 1)
.build()
)
);
mapSnapshotter.start(MapSnapshotterHeatMapActivity.this);
}
});
}
@NotNull
private HeatmapLayer getHeatmapLayer() {
HeatmapLayer layer = new HeatmapLayer(HEATMAP_LAYER_ID, EARTHQUAKE_SOURCE_ID);
layer.setMaxZoom(9);
layer.setSourceLayer(HEATMAP_LAYER_SOURCE);
layer.setProperties(
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
// Begin color ramp at 0-stop with a 0-transparency color
// to create a blur-like effect.
heatmapColor(
interpolate(
linear(), heatmapDensity(),
literal(0), rgba(33, 102, 172, 0),
literal(0.2), rgb(103, 169, 207),
literal(0.4), rgb(209, 229, 240),
literal(0.6), rgb(253, 219, 199),
literal(0.8), rgb(239, 138, 98),
literal(1), rgb(178, 24, 43)
)
),
// Increase the heatmap weight based on frequency and property magnitude
heatmapWeight(
interpolate(
linear(), get("mag"),
stop(0, 0),
stop(6, 1)
)
),
// Increase the heatmap color weight weight by zoom level
// heatmap-intensity is a multiplier on top of heatmap-weight
heatmapIntensity(
interpolate(
linear(), zoom(),
stop(0, 1),
stop(9, 3)
)
),
// Adjust the heatmap radius by zoom level
heatmapRadius(
interpolate(
linear(), zoom(),
stop(0, 2),
stop(9, 20)
)
),
// Transition from heatmap to circle layer by zoom level
heatmapOpacity(
interpolate(
linear(), zoom(),
stop(7, 1),
stop(9, 0)
)
)
);
return layer;
}
@Override
protected void onStop() {
super.onStop();
mapSnapshotter.cancel();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onSnapshotReady(MapSnapshot snapshot) {
Timber.i("Snapshot ready");
ImageView imageView = findViewById(R.id.snapshot_image);
imageView.setImageBitmap(snapshot.getBitmap());
}
private Source getEarthquakeSource() {
Source source = null;
try {
source = new GeoJsonSource(EARTHQUAKE_SOURCE_ID, new URI(EARTHQUAKE_SOURCE_URL));
} catch (URISyntaxException uriSyntaxException) {
Timber.e(uriSyntaxException, "That's not an url... ");
}
return source;
}
}

View File

@@ -7,6 +7,7 @@ import android.view.ViewTreeObserver;
import android.widget.ImageView;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshot;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
import com.mapbox.mapboxsdk.testapp.R;
@@ -48,7 +49,7 @@ public class MapSnapshotterLocalStyleActivity extends AppCompatActivity
getApplicationContext(),
new MapSnapshotter
.Options(Math.min(container.getMeasuredWidth(), 1024), Math.min(container.getMeasuredHeight(), 1024))
.withStyleJson(styleJson)
.withStyleBuilder(new Style.Builder().fromJson(styleJson))
.withCameraPosition(new CameraPosition.Builder().target(new LatLng(52.090737, 5.121420)).zoom(18).build())
);
mapSnapshotter.start(MapSnapshotterLocalStyleActivity.this, error -> Timber.e(error));

View File

@@ -1,20 +1,21 @@
package com.mapbox.mapboxsdk.testapp.activity.snapshot;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshot;
import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
import com.mapbox.mapboxsdk.testapp.R;
import java.util.Random;
import androidx.appcompat.app.AppCompatActivity;
/**
* Test activity showing how to use a the {@link MapSnapshotter}
*/
@@ -49,7 +50,7 @@ public class MapSnapshotterReuseActivity extends AppCompatActivity implements Ma
mapSnapshotter = new MapSnapshotter(
getApplicationContext(),
new MapSnapshotter.Options(512, 512)
new MapSnapshotter.Options(512, 512).withStyleBuilder(new Style.Builder().fromUri(getRandomStyle()))
);
mapSnapshotter.start(MapSnapshotterReuseActivity.this);

View File

@@ -61,7 +61,8 @@
<string name="description_bottom_sheet">Show 2 MapView on screen with a bottom sheet</string>
<string name="description_map_snapshotter">Show a static bitmap taken with the MapSnapshotter</string>
<string name="description_map_snapshotter_reuse">Show how to reuse a MapSnapshotter instance</string>
<string name="description_map_snapshotter_marker">Show how to add a marker to a Snapshot</string>
<string name="description_map_snapshotter_bitmap_overlay">Show how to add a bitmap overlay to a Snapshot</string>
<string name="description_map_snapshotter_heatmap_layer">Show how to add a heatmap layer to a Snapshot</string>
<string name="description_map_snapshotter_local_style">Show how to load a local style with a Snapshot</string>
<string name="description_camera_animator">Use Android SDK Animators to animate camera position changes</string>
<string name="description_symbol_generator">Use Android SDK Views as symbols</string>

View File

@@ -59,7 +59,8 @@
<string name="activity_bottom_sheet">Bottom sheet</string>
<string name="activity_map_snapshotter">Map Snapshotter</string>
<string name="activity_map_snapshotter_reuse">Map Snapshotter Reuse</string>
<string name="activity_map_snapshotter_marker">Map Snapshot with marker</string>
<string name="activity_map_snapshotter_bitmap_overlay">Map Snapshot with bitmap overlay</string>
<string name="activity_map_snapshotter_heatmap_layer">Map Snapshot with heatmap layer</string>
<string name="activity_map_snapshotter_local_style">Map Snapshot with local style</string>
<string name="activity_camera_animator">Animator animation</string>
<string name="activity_symbol_generator">SymbolGenerator</string>