From 69050ddabbc254b12fa3ac61536f4eece7459249 Mon Sep 17 00:00:00 2001 From: Ivo van Dongen Date: Tue, 22 Aug 2017 16:32:45 +0300 Subject: [android] map snapshotter --- .../mapboxsdk/snapshotter/MapSnapshotter.java | 259 +++++++++++++++++++++ .../src/main/AndroidManifest.xml | 7 + .../activity/snapshot/MapSnapshotterActivity.java | 131 +++++++++++ .../main/res/layout/activity_map_snapshotter.xml | 14 ++ .../src/main/res/values/strings.xml | 2 + platform/android/config.cmake | 12 + platform/android/src/jni.cpp | 4 + platform/android/src/map/camera_position.cpp | 18 ++ platform/android/src/map/camera_position.hpp | 2 + .../android/src/snapshotter/map_snapshotter.cpp | 110 +++++++++ .../android/src/snapshotter/map_snapshotter.hpp | 61 +++++ 11 files changed, 620 insertions(+) create mode 100644 platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotter.java create mode 100644 platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/snapshot/MapSnapshotterActivity.java create mode 100644 platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter.xml create mode 100644 platform/android/src/snapshotter/map_snapshotter.cpp create mode 100644 platform/android/src/snapshotter/map_snapshotter.hpp diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotter.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotter.java new file mode 100644 index 0000000000..5db5f5f4b9 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotter.java @@ -0,0 +1,259 @@ +package com.mapbox.mapboxsdk.snapshotter; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.constants.Style; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.storage.FileSource; + +/** + * The map snapshotter creates a bitmap of the map, rendered + * off the UI thread. The snapshotter itself must be used on + * the UI thread (for access to the main looper) + */ +@UiThread +public class MapSnapshotter { + + /** + * Can be used to get notified of errors + * in snapshot generation + * + * @see MapSnapshotter#start(MapboxMap.SnapshotReadyCallback, ErrorHandler) + */ + public static interface ErrorHandler { + + /** + * Called on error. Snapshotting will not + * continue + * + * @param error the error message + */ + void onError(String error); + } + + private static final int LOGO_MARGIN_PX = 4; + + // Holds the pointer to JNI NativeMapView + private long nativePtr = 0; + + private final Context context; + private MapboxMap.SnapshotReadyCallback callback; + private ErrorHandler errorHandler; + + /** + * MapSnapshotter options + */ + public static class Options { + private int pixelRatio = 1; + private int width; + private int height; + private String styleUrl = Style.MAPBOX_STREETS; + private LatLngBounds region; + private CameraPosition cameraPosition; + + /** + * @param width the width of the image + * @param height the height of the image + */ + public Options(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * @param url The style URL to use + * @return the mutated {@link Options} + */ + public Options withStyle(String url) { + this.styleUrl = url; + return this; + } + + /** + * @param region the region to show in the snapshot. + * This is applied after the camera position + * @return the mutated {@link Options} + */ + public Options withRegion(LatLngBounds region) { + this.region = region; + return this; + } + + /** + * @param pixelRatio the pixel ratio to use (default: 1) + * @return the mutated {@link Options} + */ + public Options withPixelRatio(int pixelRatio) { + this.pixelRatio = pixelRatio; + return this; + } + + /** + * @param cameraPosition The camera position to use, + * the {@link CameraPosition#target} is overridden + * by region if set in conjunction. + * @return the mutated {@link Options} + */ + public Options withCameraPosition(CameraPosition cameraPosition) { + this.cameraPosition = cameraPosition; + return this; + } + + /** + * @return the width of the image + */ + public int getWidth() { + return width; + } + + /** + * @return the height of the image + */ + public int getHeight() { + return height; + } + + /** + * @return the pixel ratio + */ + public int getPixelRatio() { + return pixelRatio; + } + + /** + * @return the region + */ + @Nullable + public LatLngBounds getRegion() { + return region; + } + + /** + * @return the style url + */ + public String getStyleUrl() { + return styleUrl; + } + + /** + * @return the camera position + */ + @Nullable + public CameraPosition getCameraPosition() { + return cameraPosition; + } + } + + /** + * Creates the Map snapshotter, but doesn't start rendering or + * loading yet. + * + * @param context the Context that is or contains the Application context + * @param options the options to use for the snapshot + */ + public MapSnapshotter(@NonNull Context context, @NonNull Options options) { + this.context = context.getApplicationContext(); + FileSource fileSource = FileSource.getInstance(context); + String programCacheDir = context.getCacheDir().getAbsolutePath(); + + nativeInitialize(this, fileSource, options.pixelRatio, options.width, + options.height, options.styleUrl, options.region, options.cameraPosition, + programCacheDir); + } + + /** + * Starts loading and rendering the snapshot. The callback will be fired + * on the calling thread. + * + * @param callback the callback to use when the snapshot is ready + */ + public void start(@NonNull MapboxMap.SnapshotReadyCallback callback) { + this.start(callback, null); + } + + /** + * Starts loading and rendering the snapshot. The callbacks will be fired + * on the calling thread. + * + * @param callback the callback to use when the snapshot is ready + * @param errorHandler the error handler to use on snapshot errors + */ + public void start(@NonNull MapboxMap.SnapshotReadyCallback callback, ErrorHandler errorHandler) { + if (this.callback != null) { + throw new IllegalStateException("Snapshotter was already started"); + } + + this.callback = callback; + nativeStart(); + } + + /** + * Must be called in on the thread + * the object was created on. + */ + public void cancel() { + callback = null; + nativeCancel(); + } + + protected void addOverlay(Bitmap original) { + float margin = context.getResources().getDisplayMetrics().density * LOGO_MARGIN_PX; + Canvas canvas = new Canvas(original); + Bitmap logo = BitmapFactory.decodeResource(context.getResources(), R.drawable.mapbox_logo_icon, null); + canvas.drawBitmap(logo, margin, original.getHeight() - (logo.getHeight() + margin), null); + } + + /** + * Called by JNI peer when snapshot is ready. + * Always called on the origin (main) thread. + * + * @param bitmap the generated snapshot + */ + protected void onSnapshotReady(Bitmap bitmap) { + if (callback != null) { + addOverlay(bitmap); + callback.onSnapshotReady(bitmap); + reset(); + } + } + + /** + * Called by JNI peer when snapshot has failed. + * Always called on the origin (main) thread. + * + * @param reason the exception string + */ + protected void onSnapshotFailed(String reason) { + if (errorHandler != null) { + errorHandler.onError(reason); + reset(); + } + } + + protected void reset() { + callback = null; + errorHandler = null; + } + + protected native void nativeInitialize(MapSnapshotter mapSnapshotter, + FileSource fileSource, float pixelRatio, + int width, int height, String styleUrl, + LatLngBounds region, CameraPosition position, + String programCacheDir); + + protected native void nativeStart(); + + protected native void nativeCancel(); + + @Override + protected native void finalize() throws Throwable; +} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml index 5dc322a530..24de706bdb 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml @@ -358,6 +358,13 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".activity.FeatureOverviewActivity"/> + + + snapshotters = new ArrayList<>(); + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_map_snapshotter); + + // Find the grid view and start snapshotting as soon + // as the view is measured + grid = (GridLayout) findViewById(R.id.snapshot_grid); + grid.getViewTreeObserver() + .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + //noinspection deprecation + grid.getViewTreeObserver().removeGlobalOnLayoutListener(this); + addSnapshots(); + } + }); + } + + private void addSnapshots() { + Timber.i("Creating snapshotters"); + + for (int row = 0; row < grid.getRowCount(); row++) { + for (int column = 0; column < grid.getColumnCount(); column++) { + startSnapShot(row, column); + } + } + } + + private void startSnapShot(final int row, final int column) { + + // Define the dimensions + MapSnapshotter.Options options = new MapSnapshotter.Options( + grid.getMeasuredWidth() / grid.getColumnCount(), + grid.getMeasuredHeight() / grid.getRowCount() + ) + // Optionally the pixel ratio + .withPixelRatio(1) + + // Optionally the style + .withStyle((column + row) % 2 == 0 ? Style.TRAFFIC_DAY : Style.DARK); + + // Optionally the visible region + if (row % 2 == 0) { + options.withRegion(new LatLngBounds.Builder() + .include(new LatLng(randomInRange(-80, 80), randomInRange(-160, 160))) + .include(new LatLng(randomInRange(-80, 80), randomInRange(-160, 160))) + .build() + ); + } + + // Optionally the camera options + if (column % 2 == 0) { + options.withCameraPosition(new CameraPosition.Builder() + .target(options.getRegion() != null + ? options.getRegion().getCenter() + : new LatLng(randomInRange(-80, 80), randomInRange(-160, 160))) + .bearing(randomInRange(0, 360)) + .tilt(randomInRange(0, 60)) + .zoom(randomInRange(0, 20)) + .build() + ); + } + + MapSnapshotter snapshotter = new MapSnapshotter(MapSnapshotterActivity.this, options); + + snapshotter.start(new MapboxMap.SnapshotReadyCallback() { + @Override + public void onSnapshotReady(Bitmap snapshot) { + Timber.i("Got the snapshot"); + ImageView imageView = new ImageView(MapSnapshotterActivity.this); + imageView.setImageBitmap(snapshot); + grid.addView( + imageView, + new GridLayout.LayoutParams(GridLayout.spec(row), GridLayout.spec(column)) + ); + } + }); + snapshotters.add(snapshotter); + } + + @Override + public void onPause() { + super.onPause(); + + // Make sure to stop the snapshotters on pause + for (MapSnapshotter snapshotter : snapshotters) { + snapshotter.cancel(); + } + snapshotters.clear(); + } + + private static Random random = new Random(); + + public static float randomInRange(float min, float max) { + return (random.nextFloat() * (max - min)) + min; + } + +} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter.xml new file mode 100644 index 0000000000..30ad494dca --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml index 0fbf9754c5..b1f354aad5 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml @@ -63,6 +63,7 @@ Building layer Animated Image Source Bottom sheet + Map Snapshotter Tracks the location of the user @@ -125,6 +126,7 @@ Shows how to show 3D extruded buildings Shows how to animate georeferenced images Show 2 MapView on screen with a bottom sheet + Show a static bitmap taken with the MapSnapshotter category diff --git a/platform/android/config.cmake b/platform/android/config.cmake index 390e4842f4..dc6b1c80a6 100644 --- a/platform/android/config.cmake +++ b/platform/android/config.cmake @@ -75,6 +75,18 @@ macro(mbgl_platform_core) # Rendering PRIVATE platform/android/src/android_renderer_frontend.cpp PRIVATE platform/android/src/android_renderer_frontend.hpp + + # Snapshots + PRIVATE platform/default/mbgl/gl/headless_backend.cpp + PRIVATE platform/default/mbgl/gl/headless_backend.hpp + PRIVATE platform/default/mbgl/gl/headless_frontend.cpp + PRIVATE platform/default/mbgl/gl/headless_frontend.hpp + PRIVATE platform/default/mbgl/map/map_snapshotter.cpp + PRIVATE platform/default/mbgl/map/map_snapshotter.hpp + PRIVATE platform/linux/src/headless_backend_egl.cpp + PRIVATE platform/linux/src/headless_display_egl.cpp + PRIVATE platform/android/src/snapshotter/map_snapshotter.cpp + PRIVATE platform/android/src/snapshotter/map_snapshotter.hpp ) target_include_directories(mbgl-core diff --git a/platform/android/src/jni.cpp b/platform/android/src/jni.cpp index db8dd1dbdf..f7d1e4afbc 100755 --- a/platform/android/src/jni.cpp +++ b/platform/android/src/jni.cpp @@ -47,6 +47,7 @@ #include "style/layers/layers.hpp" #include "style/sources/sources.hpp" #include "style/light.hpp" +#include "snapshotter/map_snapshotter.hpp" namespace mbgl { namespace android { @@ -177,6 +178,9 @@ void registerNatives(JavaVM *vm) { OfflineTilePyramidRegionDefinition::registerNative(env); OfflineRegionError::registerNative(env); OfflineRegionStatus::registerNative(env); + + // Snapshotter + MapSnapshotter::registerNative(env); } } // namespace android diff --git a/platform/android/src/map/camera_position.cpp b/platform/android/src/map/camera_position.cpp index d6f2cb83e8..1fc5f9789f 100644 --- a/platform/android/src/map/camera_position.cpp +++ b/platform/android/src/map/camera_position.cpp @@ -27,6 +27,24 @@ jni::Object CameraPosition::New(jni::JNIEnv &env, mbgl::CameraOp return CameraPosition::javaClass.New(env, constructor, LatLng::New(env, center), options.zoom.value_or(0), tilt_degrees, bearing_degrees); } +mbgl::CameraOptions CameraPosition::getCameraOptions(jni::JNIEnv& env, jni::Object position) { + static auto bearing = CameraPosition::javaClass.GetField(env, "bearing"); + static auto target = CameraPosition::javaClass.GetField>(env, "target"); + static auto tilt = CameraPosition::javaClass.GetField(env, "tilt"); + static auto zoom = CameraPosition::javaClass.GetField(env, "zoom"); + + auto center = LatLng::getLatLng(env, position.Get(env, target)); + + return mbgl::CameraOptions { + center, + {}, + {}, + position.Get(env, zoom), + position.Get(env, bearing) * util::DEG2RAD, + position.Get(env, tilt) + }; +} + void CameraPosition::registerNative(jni::JNIEnv &env) { // Lookup the class CameraPosition::javaClass = *jni::Class::Find(env).NewGlobalRef(env).release(); diff --git a/platform/android/src/map/camera_position.hpp b/platform/android/src/map/camera_position.hpp index b9f1646cc9..4eee8be758 100644 --- a/platform/android/src/map/camera_position.hpp +++ b/platform/android/src/map/camera_position.hpp @@ -15,6 +15,8 @@ public: static jni::Object New(jni::JNIEnv&, mbgl::CameraOptions); + static mbgl::CameraOptions getCameraOptions(jni::JNIEnv&, jni::Object); + static jni::Class javaClass; static void registerNative(jni::JNIEnv&); diff --git a/platform/android/src/snapshotter/map_snapshotter.cpp b/platform/android/src/snapshotter/map_snapshotter.cpp new file mode 100644 index 0000000000..d64218d11a --- /dev/null +++ b/platform/android/src/snapshotter/map_snapshotter.cpp @@ -0,0 +1,110 @@ +#include "map_snapshotter.hpp" + +#include +#include +#include +#include +#include +#include + +#include "../attach_env.hpp" +#include "../bitmap.hpp" + +namespace mbgl { +namespace android { + +MapSnapshotter::MapSnapshotter(jni::JNIEnv& _env, + jni::Object _obj, + jni::Object jFileSource, + jni::jfloat _pixelRatio, + jni::jint width, + jni::jint height, + jni::String styleURL, + jni::Object region, + jni::Object position, + jni::String _programCacheDir) + : javaPeer(SeizeGenericWeakRef(_env, jni::Object(jni::NewWeakGlobalRef(_env, _obj.Get()).release()))) + , pixelRatio(_pixelRatio) + , threadPool(sharedThreadPool()) { + + // Get a reference to the JavaVM for callbacks + if (_env.GetJavaVM(&vm) < 0) { + _env.ExceptionDescribe(); + return; + } + + auto& fileSource = mbgl::android::FileSource::getDefaultFileSource(_env, jFileSource); + auto size = mbgl::Size { static_cast(width), static_cast(height) }; + auto cameraOptions = position ? CameraPosition::getCameraOptions(_env, position) : CameraOptions(); + optional bounds; + if (region) { + bounds = LatLngBounds::getLatLngBounds(_env, region); + } + + // Create the core snapshotter + snapshotter = std::make_unique(fileSource, + *threadPool, + jni::Make(_env, styleURL), + size, + pixelRatio, + cameraOptions, + bounds, + jni::Make(_env, _programCacheDir)); + +} + +MapSnapshotter::~MapSnapshotter() = default; + +void MapSnapshotter::start(JNIEnv&) { + MBGL_VERIFY_THREAD(tid); + + snapshotCallback = std::make_unique>(*Scheduler::GetCurrent(), [this](std::exception_ptr err, PremultipliedImage image) { + MBGL_VERIFY_THREAD(tid); + android::UniqueEnv _env = android::AttachEnv(); + + if (err) { + // error handler callback + static auto onSnapshotFailed = javaClass.GetMethod(*_env, "onSnapshotFailed"); + javaPeer->Call(*_env, onSnapshotFailed, jni::Make(*_env, util::toString(err))); + } else { + // Create the bitmap + auto bitmap = Bitmap::CreateBitmap(*_env, std::move(image)); + + // invoke callback + static auto onSnapshotReady = javaClass.GetMethod)>(*_env, "onSnapshotReady"); + javaPeer->Call(*_env, onSnapshotReady, bitmap); + } + }); + + snapshotter->snapshot(snapshotCallback->self()); +} + +void MapSnapshotter::cancel(JNIEnv&) { + MBGL_VERIFY_THREAD(tid); + + snapshotCallback.reset(); + snapshotter.reset(); +} + +// Static methods // + +jni::Class MapSnapshotter::javaClass; + +void MapSnapshotter::registerNative(jni::JNIEnv& env) { + // Lookup the class + MapSnapshotter::javaClass = *jni::Class::Find(env).NewGlobalRef(env).release(); + +#define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer(env, MapSnapshotter::javaClass, "nativePtr", + std::make_unique, jni::Object, jni::jfloat, jni::jint, jni::jint, jni::String, jni::Object, jni::Object, jni::String>, + "nativeInitialize", + "finalize", + METHOD(&MapSnapshotter::start, "nativeStart"), + METHOD(&MapSnapshotter::cancel, "nativeCancel") + ); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/platform/android/src/snapshotter/map_snapshotter.hpp b/platform/android/src/snapshotter/map_snapshotter.hpp new file mode 100644 index 0000000000..093f589c05 --- /dev/null +++ b/platform/android/src/snapshotter/map_snapshotter.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include "../file_source.hpp" +#include "../geometry/lat_lng_bounds.hpp" +#include "../map/camera_position.hpp" + +#include +#include "../jni/generic_global_ref_deleter.hpp" + +#include + +namespace mbgl { +namespace android { + +class SnapshotterRendererFrontend; + +class MapSnapshotter { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/snapshotter/MapSnapshotter"; }; + + static jni::Class javaClass; + + static void registerNative(jni::JNIEnv&); + + MapSnapshotter(jni::JNIEnv&, + jni::Object, + jni::Object, + jni::jfloat pixelRatio, + jni::jint width, + jni::jint height, + jni::String styleURL, + jni::Object region, + jni::Object position, + jni::String programCacheDir); + + ~MapSnapshotter(); + + void start(JNIEnv&); + + void cancel(JNIEnv&); + +private: + MBGL_STORE_THREAD(tid); + + JavaVM *vm = nullptr; + GenericUniqueWeakObject javaPeer; + + float pixelRatio; + + std::shared_ptr threadPool; + std::unique_ptr> snapshotCallback; + std::unique_ptr snapshotter; +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file -- cgit v1.2.1