diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshot.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshot.java
new file mode 100644
index 00000000000..aefa962402d
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshot.java
@@ -0,0 +1,54 @@
+package com.mapbox.mapboxsdk.snapshotter;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+/**
+ * A completed snapshot.
+ *
+ * @see MapSnapshotter
+ */
+public class MapSnapshot {
+
+ private long nativePtr = 0;
+ private Bitmap bitmap;
+ private String[] attributions;
+
+ /**
+ * Created from native side
+ */
+ private MapSnapshot(long nativePtr, Bitmap bitmap, String[] attributions) {
+ this.nativePtr = nativePtr;
+ this.bitmap = bitmap;
+ this.attributions = attributions;
+ }
+
+ /**
+ * @return the bitmap
+ */
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+
+ /**
+ * Calculate the point in pixels on the Image from geographical coordinates.
+ *
+ * @param latLng the geographical coordinates
+ * @return the point on the image
+ */
+ public native PointF pixelForLatLng(LatLng latLng);
+
+ /**
+ * @return The attributions for the sources of this snapshot.
+ */
+ protected String[] getAttributions() {
+ return attributions;
+ }
+
+ // Unused, needed for peer binding
+ private native void initialize();
+
+ protected native void finalize();
+}
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
index 72df86d80da..37d05fc3280 100644
--- 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
@@ -12,7 +12,6 @@
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;
/**
@@ -23,11 +22,27 @@
@UiThread
public class MapSnapshotter {
+ /**
+ * Get notified on snapshot completion.
+ *
+ * @see MapSnapshotter#start(SnapshotReadyCallback, ErrorHandler)
+ */
+ public interface SnapshotReadyCallback {
+
+ /**
+ * Called when the snapshot is complete.
+ *
+ * @param snapshot the snapshot
+ */
+ void onSnapshotReady(MapSnapshot snapshot);
+
+ }
+
/**
* Can be used to get notified of errors
* in snapshot generation
*
- * @see MapSnapshotter#start(MapboxMap.SnapshotReadyCallback, ErrorHandler)
+ * @see MapSnapshotter#start(SnapshotReadyCallback, ErrorHandler)
*/
public interface ErrorHandler {
@@ -46,7 +61,7 @@ public interface ErrorHandler {
private long nativePtr = 0;
private final Context context;
- private MapboxMap.SnapshotReadyCallback callback;
+ private SnapshotReadyCallback callback;
private ErrorHandler errorHandler;
/**
@@ -176,7 +191,7 @@ public MapSnapshotter(@NonNull Context context, @NonNull Options options) {
*
* @param callback the callback to use when the snapshot is ready
*/
- public void start(@NonNull MapboxMap.SnapshotReadyCallback callback) {
+ public void start(@NonNull SnapshotReadyCallback callback) {
this.start(callback, null);
}
@@ -184,10 +199,10 @@ public void start(@NonNull MapboxMap.SnapshotReadyCallback callback) {
* 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 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) {
+ public void start(@NonNull SnapshotReadyCallback callback, ErrorHandler errorHandler) {
if (this.callback != null) {
throw new IllegalStateException("Snapshotter was already started");
}
@@ -197,12 +212,42 @@ public void start(@NonNull MapboxMap.SnapshotReadyCallback callback, ErrorHandle
nativeStart();
}
+ /**
+ * Updates the snapshotter with a new size
+ *
+ * @param width the width
+ * @param height the height
+ */
+ public native void setSize(int width, int height);
+
+ /**
+ * Updates the snapshotter with a new {@link CameraPosition}
+ *
+ * @param cameraPosition the camera position
+ */
+ public native void setCameraPosition(CameraPosition cameraPosition);
+
+ /**
+ * Updates the snapshotter with a new {@link LatLngBounds}
+ *
+ * @param region the region
+ */
+ public native void setRegion(LatLngBounds region);
+
+ /**
+ * Updates the snapshotter with a new style url
+ *
+ * @param styleUrl the style url
+ */
+ public native void setStyleUrl(String styleUrl);
+
+
/**
* Must be called in on the thread
* the object was created on.
*/
public void cancel() {
- callback = null;
+ reset();
nativeCancel();
}
@@ -217,12 +262,12 @@ protected void addOverlay(Bitmap original) {
* Called by JNI peer when snapshot is ready.
* Always called on the origin (main) thread.
*
- * @param bitmap the generated snapshot
+ * @param snapshot the generated snapshot
*/
- protected void onSnapshotReady(Bitmap bitmap) {
+ protected void onSnapshotReady(MapSnapshot snapshot) {
if (callback != null) {
- addOverlay(bitmap);
- callback.onSnapshotReady(bitmap);
+ addOverlay(snapshot.getBitmap());
+ callback.onSnapshotReady(snapshot);
reset();
}
}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
index bf97749b9e4..7f955cb45c3 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
@@ -377,6 +377,26 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter_reuse.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter_reuse.xml
new file mode 100644
index 00000000000..5ce25a69d0b
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter_reuse.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
index 4d1f7eac388..a2bf1d85961 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
@@ -60,6 +60,8 @@
Shows how to animate georeferenced images
Show 2 MapView on screen with a bottom sheet
Show a static bitmap taken with the MapSnapshotter
+ Show how to reuse a MapSnapshotter instance
+ Show how to add a marker to a Snapshot
Use Android SDK Animators to animate camera position changes
Use Android SDK Views as symbols
\ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
index 8f394d0eb4f..1fb2f6ba7fd 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
@@ -60,6 +60,8 @@
Animated Image Source
Bottom sheet
Map Snapshotter
+ Map Snapshotter Reuse
+ Map Snapshot with marker
Animator animation
SymbolGenerator
\ No newline at end of file
diff --git a/platform/android/config.cmake b/platform/android/config.cmake
index 53f9b40d7fa..4af7a4293df 100644
--- a/platform/android/config.cmake
+++ b/platform/android/config.cmake
@@ -59,7 +59,7 @@ macro(mbgl_platform_core)
PRIVATE platform/android/src/android_renderer_frontend.cpp
PRIVATE platform/android/src/android_renderer_frontend.hpp
- # Snapshots
+ # Snapshots (core)
PRIVATE platform/default/mbgl/gl/headless_backend.cpp
PRIVATE platform/default/mbgl/gl/headless_backend.hpp
PRIVATE platform/default/mbgl/gl/headless_frontend.cpp
@@ -68,8 +68,6 @@ macro(mbgl_platform_core)
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
@@ -293,6 +291,12 @@ add_library(mbgl-android STATIC
platform/android/src/offline/offline_region_status.cpp
platform/android/src/offline/offline_region_status.hpp
+ # Snapshots (SDK)
+ platform/android/src/snapshotter/map_snapshotter.cpp
+ platform/android/src/snapshotter/map_snapshotter.hpp
+ platform/android/src/snapshotter/map_snapshot.cpp
+ platform/android/src/snapshotter/map_snapshot.hpp
+
# Main jni bindings
platform/android/src/attach_env.cpp
platform/android/src/attach_env.hpp
diff --git a/platform/android/src/jni.cpp b/platform/android/src/jni.cpp
index 6acb6a36642..f39aeb63741 100755
--- a/platform/android/src/jni.cpp
+++ b/platform/android/src/jni.cpp
@@ -50,6 +50,7 @@
#include "style/sources/sources.hpp"
#include "style/light.hpp"
#include "snapshotter/map_snapshotter.hpp"
+#include "snapshotter/map_snapshot.hpp"
namespace mbgl {
namespace android {
@@ -185,6 +186,7 @@ void registerNatives(JavaVM *vm) {
// Snapshotter
MapSnapshotter::registerNative(env);
+ MapSnapshot::registerNative(env);
}
} // namespace android
diff --git a/platform/android/src/jni/collection.hpp b/platform/android/src/jni/collection.hpp
new file mode 100644
index 00000000000..5f94ec29ce5
--- /dev/null
+++ b/platform/android/src/jni/collection.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+namespace jni {
+
+inline Array MakeAnything(ThingToMake>, JNIEnv& env, const std::vector& vector)
+{
+ static auto clazz = *Class::Find(env).NewGlobalRef(env).release();
+ auto result = Array::New(env, vector.size(), clazz);
+
+ std::size_t index = 0;
+ for (auto&& item : vector) {
+ auto element = Make(env, item);
+ result.Set(env, index, element);
+ DeleteLocalRef(env, element);
+ index++;
+ }
+
+ return result;
+}
+
+}
diff --git a/platform/android/src/snapshotter/map_snapshot.cpp b/platform/android/src/snapshotter/map_snapshot.cpp
new file mode 100644
index 00000000000..843a8a487a3
--- /dev/null
+++ b/platform/android/src/snapshotter/map_snapshot.cpp
@@ -0,0 +1,59 @@
+#include "map_snapshot.hpp"
+
+#include "../bitmap.hpp"
+#include "../jni/collection.hpp"
+
+#include
+
+namespace mbgl {
+namespace android {
+
+MapSnapshot::MapSnapshot(float pixelRatio_, MapSnapshot::PointForFn pointForFn_)
+ : pixelRatio(pixelRatio_)
+ , pointForFn(std::move(pointForFn_)) {
+}
+
+MapSnapshot::~MapSnapshot() = default;
+
+jni::Object MapSnapshot::pixelForLatLng(jni::JNIEnv& env, jni::Object jLatLng) {
+ ScreenCoordinate point = pointForFn(LatLng::getLatLng(env, jLatLng));
+ return PointF::New(env, point.x * pixelRatio, point.y * pixelRatio);
+}
+
+
+// Static methods //
+
+jni::Object MapSnapshot::New(JNIEnv& env,
+ PremultipliedImage&& image,
+ float pixelRatio,
+ std::vector attributions,
+ mbgl::MapSnapshotter::PointForFn pointForFn) {
+ // Create the bitmap
+ auto bitmap = Bitmap::CreateBitmap(env, std::move(image));
+
+ // Create the Mapsnapshot peers
+ static auto constructor = javaClass.GetConstructor, jni::Array>(env);
+ auto nativePeer = std::make_unique(pixelRatio, pointForFn);
+ return javaClass.New(env, constructor, reinterpret_cast(nativePeer.release()), bitmap, jni::Make>(env, attributions));
+}
+
+jni::Class MapSnapshot::javaClass;
+
+void MapSnapshot::registerNative(jni::JNIEnv& env) {
+ // Lookup the class
+ MapSnapshot::javaClass = *jni::Class::Find(env).NewGlobalRef(env).release();
+
+#define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name)
+
+ // Register the peer
+ jni::RegisterNativePeer(env, MapSnapshot::javaClass,
+ "nativePtr",
+ std::make_unique,
+ "initialize",
+ "finalize",
+ METHOD(&MapSnapshot::pixelForLatLng, "pixelForLatLng")
+ );
+}
+
+} // namespace android
+} // namespace mbgl
diff --git a/platform/android/src/snapshotter/map_snapshot.hpp b/platform/android/src/snapshotter/map_snapshot.hpp
new file mode 100644
index 00000000000..64090bb48b3
--- /dev/null
+++ b/platform/android/src/snapshotter/map_snapshot.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+#include
+
+#include
+
+#include "../geometry/lat_lng.hpp"
+#include "../graphics/pointf.hpp"
+
+#include
+#include
+
+namespace mbgl {
+namespace android {
+
+class MapSnapshot {
+public:
+
+ using PointForFn = mbgl::MapSnapshotter::PointForFn;
+
+ static constexpr auto Name() { return "com/mapbox/mapboxsdk/snapshotter/MapSnapshot"; };
+
+ static void registerNative(jni::JNIEnv&);
+
+ static jni::Object New(JNIEnv& env,
+ PremultipliedImage&& image,
+ float pixelRatio,
+ std::vector attributions,
+ PointForFn pointForFn);
+
+ MapSnapshot(jni::JNIEnv&) {};
+ MapSnapshot(float pixelRatio, PointForFn);
+ ~MapSnapshot();
+
+ jni::Object pixelForLatLng(jni::JNIEnv&, jni::Object);
+
+private:
+ static jni::Class javaClass;
+
+ float pixelRatio;
+ mbgl::MapSnapshotter::PointForFn pointForFn;
+};
+
+} // namespace android
+} // namespace mbgl
\ No newline at end of file
diff --git a/platform/android/src/snapshotter/map_snapshotter.cpp b/platform/android/src/snapshotter/map_snapshotter.cpp
index d64218d11a8..3c6623af40a 100644
--- a/platform/android/src/snapshotter/map_snapshotter.cpp
+++ b/platform/android/src/snapshotter/map_snapshotter.cpp
@@ -8,7 +8,7 @@
#include
#include "../attach_env.hpp"
-#include "../bitmap.hpp"
+#include "map_snapshot.hpp"
namespace mbgl {
namespace android {
@@ -58,7 +58,9 @@ MapSnapshotter::~MapSnapshotter() = default;
void MapSnapshotter::start(JNIEnv&) {
MBGL_VERIFY_THREAD(tid);
- snapshotCallback = std::make_unique>(*Scheduler::GetCurrent(), [this](std::exception_ptr err, PremultipliedImage image) {
+ snapshotCallback = std::make_unique>(
+ *Scheduler::GetCurrent(),
+ [this](std::exception_ptr err, PremultipliedImage image, std::vector attributions, mbgl::MapSnapshotter::PointForFn pointForFn) {
MBGL_VERIFY_THREAD(tid);
android::UniqueEnv _env = android::AttachEnv();
@@ -67,12 +69,12 @@ void MapSnapshotter::start(JNIEnv&) {
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));
+ // Create the wrapper
+ auto mapSnapshot = android::MapSnapshot::New(*_env, std::move(image), pixelRatio, attributions, pointForFn);
// invoke callback
- static auto onSnapshotReady = javaClass.GetMethod)>(*_env, "onSnapshotReady");
- javaPeer->Call(*_env, onSnapshotReady, bitmap);
+ static auto onSnapshotReady = javaClass.GetMethod)>(*_env, "onSnapshotReady");
+ javaPeer->Call(*_env, onSnapshotReady, mapSnapshot);
}
});
@@ -81,9 +83,26 @@ void MapSnapshotter::start(JNIEnv&) {
void MapSnapshotter::cancel(JNIEnv&) {
MBGL_VERIFY_THREAD(tid);
-
snapshotCallback.reset();
- snapshotter.reset();
+}
+
+
+void MapSnapshotter::setStyleUrl(JNIEnv& env, jni::String styleURL) {
+ snapshotter->setStyleURL(jni::Make(env, styleURL));
+}
+
+void MapSnapshotter::setSize(JNIEnv&, jni::jint width, jni::jint height) {
+ auto size = mbgl::Size { static_cast(width), static_cast(height) };
+ snapshotter->setSize(size);
+}
+
+void MapSnapshotter::setCameraPosition(JNIEnv& env, jni::Object position) {
+ auto options = CameraPosition::getCameraOptions(env, position);
+ snapshotter->setCameraOptions(options);
+}
+
+void MapSnapshotter::setRegion(JNIEnv& env, jni::Object region) {
+ snapshotter->setRegion(LatLngBounds::getLatLngBounds(env, region));
}
// Static methods //
@@ -101,6 +120,10 @@ void MapSnapshotter::registerNative(jni::JNIEnv& env) {
std::make_unique, jni::Object, jni::jfloat, jni::jint, jni::jint, jni::String, jni::Object, jni::Object, jni::String>,
"nativeInitialize",
"finalize",
+ METHOD(&MapSnapshotter::setStyleUrl, "setStyleUrl"),
+ METHOD(&MapSnapshotter::setSize, "setSize"),
+ METHOD(&MapSnapshotter::setCameraPosition, "setCameraPosition"),
+ METHOD(&MapSnapshotter::setRegion, "setRegion"),
METHOD(&MapSnapshotter::start, "nativeStart"),
METHOD(&MapSnapshotter::cancel, "nativeCancel")
);
diff --git a/platform/android/src/snapshotter/map_snapshotter.hpp b/platform/android/src/snapshotter/map_snapshotter.hpp
index 093f589c059..fa8a2d7a5a9 100644
--- a/platform/android/src/snapshotter/map_snapshotter.hpp
+++ b/platform/android/src/snapshotter/map_snapshotter.hpp
@@ -40,6 +40,14 @@ class MapSnapshotter {
~MapSnapshotter();
+ void setStyleUrl(JNIEnv&, jni::String styleURL);
+
+ void setSize(JNIEnv&, jni::jint width, jni::jint height);
+
+ void setCameraPosition(JNIEnv&, jni::Object position);
+
+ void setRegion(JNIEnv&, jni::Object region);
+
void start(JNIEnv&);
void cancel(JNIEnv&);
diff --git a/platform/darwin/src/MGLMapSnapshotter.h b/platform/darwin/src/MGLMapSnapshotter.h
index 615d39bee4d..b6228269cf0 100644
--- a/platform/darwin/src/MGLMapSnapshotter.h
+++ b/platform/darwin/src/MGLMapSnapshotter.h
@@ -46,7 +46,7 @@ MGL_EXPORT
@property (nonatomic) MGLMapCamera *camera;
/**
- The cooordinate rectangle that encompasses the bounds to capture.
+ The coordinate rectangle that encompasses the bounds to capture.
If this property is non-empty and the camera property is non-nil, the camera’s
center coordinate and altitude are ignored in favor of this property’s value.
@@ -69,23 +69,42 @@ MGL_EXPORT
@end
+/**
+ An image generated by a snapshotter object.
+ */
+@interface MGLMapSnapshot : NSObject
+
#if TARGET_OS_IPHONE
/**
- A block to processes the result or error of a snapshot request.
-
- @param snapshot The `UIImage` that was generated or `nil` if an error occurred.
- @param error The error that occured or `nil` when successful.
+ Converts the specified map coordinate to a point in the coordinate space of the image.
*/
-typedef void (^MGLMapSnapshotCompletionHandler)(UIImage* _Nullable snapshot, NSError* _Nullable error);
+- (CGPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate;
+
+/**
+ The image of the map’s content.
+ */
+@property(nonatomic, readonly) UIImage *image;
#else
+/**
+ Converts the specified map coordinate to a point in the coordinate space of the image.
+ */
+- (NSPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate;
+
+/**
+ The image of the map’s content.
+ */
+@property(nonatomic, readonly) NSImage *image;
+#endif
+
+@end
+
/**
A block to processes the result or error of a snapshot request.
- @param snapshot The `NSImage` that was generated or `nil` if an error occurred.
- @param error The eror that occured or `nil` when succesful.
+ @param snapshot The `MGLMapSnapshot` that was generated or `nil` if an error occurred.
+ @param error The error that occured or `nil` when successful.
*/
-typedef void (^MGLMapSnapshotCompletionHandler)(NSImage* _Nullable snapshot, NSError* _Nullable error);
-#endif
+typedef void (^MGLMapSnapshotCompletionHandler)(MGLMapSnapshot* _Nullable snapshot, NSError* _Nullable error);
/**
An immutable utility object for capturing map-based images.
@@ -93,20 +112,18 @@ typedef void (^MGLMapSnapshotCompletionHandler)(NSImage* _Nullable snapshot, NSE
### Example
```swift
- var camera = MGLMapCamera()
- camera.centerCoordinate = CLLocationCoordinate2D(latitude: 37.7184, longitude: -122.4365)
- camera.pitch = 20
+ let camera = MGLMapCamera(lookingAtCenter: CLLocationCoordinate2D(latitude: 37.7184, longitude: -122.4365), fromDistance: 100, pitch: 20, heading: 0)
- var options = MGLMapSnapshotOptions(styleURL: MGLStyle.satelliteStreetsStyleURL(), camera: camera, size: CGSize(width: 320, height: 480))
+ let options = MGLMapSnapshotOptions(styleURL: MGLStyle.satelliteStreetsStyleURL(), camera: camera, size: CGSize(width: 320, height: 480))
options.zoomLevel = 10
- var snapshotter = MGLMapSnapshotter(options: options)
- snapshotter.start { (image, error) in
- if error {
- // error handler
- } else {
- // image handler
- }
+ let snapshotter = MGLMapSnapshotter(options: options)
+ snapshotter.start { (snapshot, error) in
+ if error != nil {
+ // error handler
+ } else {
+ // image handler
+ }
}
```
*/
@@ -138,6 +155,45 @@ MGL_EXPORT
*/
- (void)cancel;
+/**
+ The zoom level.
+
+ The default zoom level is 0. If this property is non-zero and the camera property
+ is non-nil, the camera’s altitude is ignored in favor of this property’s value.
+ */
+@property (nonatomic) double zoomLevel;
+
+/**
+ A camera representing the viewport visible in the snapshot.
+
+ If this property is non-nil and the `coordinateBounds` property is set to a non-empty
+ coordinate bounds, the camera’s center coordinate and altitude are ignored in favor
+ of the `coordinateBounds` property.
+ */
+@property (nonatomic) MGLMapCamera *camera;
+
+/**
+ The coordinate rectangle that encompasses the bounds to capture.
+
+ If this property is non-empty and the camera property is non-nil, the camera’s
+ center coordinate and altitude are ignored in favor of this property’s value.
+ */
+@property (nonatomic) MGLCoordinateBounds coordinateBounds;
+
+/**
+ URL of the map style to snapshot.
+
+ The URL may be a full HTTP or HTTPS URL, a Mapbox URL indicating the style’s
+ map ID (`mapbox://styles/{user}/{style`}), or a path to a local file relative
+ to the application’s resource path. Specify `nil` for the default style.
+ */
+@property (nonatomic, nullable) NSURL *styleURL;
+
+/**
+ The size of the output image, measured in points.
+ */
+@property (nonatomic) CGSize size;
+
/**
Indicates whether as snapshot is currently being generated.
*/
diff --git a/platform/darwin/src/MGLMapSnapshotter.mm b/platform/darwin/src/MGLMapSnapshotter.mm
index 835e1995f33..a00521a87a8 100644
--- a/platform/darwin/src/MGLMapSnapshotter.mm
+++ b/platform/darwin/src/MGLMapSnapshotter.mm
@@ -26,7 +26,7 @@
@implementation MGLMapSnapshotOptions
-- (instancetype _Nonnull)initWithStyleURL:(nullable NSURL*)styleURL camera:(MGLMapCamera*)camera size:(CGSize) size;
+- (instancetype _Nonnull)initWithStyleURL:(nullable NSURL *)styleURL camera:(MGLMapCamera *)camera size:(CGSize) size
{
self = [super init];
if (self) {
@@ -49,6 +49,33 @@ - (instancetype _Nonnull)initWithStyleURL:(nullable NSURL*)styleURL camera:(MGLM
@end
+@interface MGLMapSnapshot()
+- (instancetype)initWithImage:(nullable MGLImage *)image scale:(CGFloat)scale pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn;
+
+@property (nonatomic) CGFloat scale;
+@end
+
+@implementation MGLMapSnapshot {
+ mbgl::MapSnapshotter::PointForFn _pointForFn;
+}
+- (instancetype)initWithImage:(nullable MGLImage *)image scale:(CGFloat)scale pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn
+{
+ self = [super init];
+ if (self) {
+ _pointForFn = std::move(pointForFn);
+ _scale = scale;
+ _image = image;
+ }
+ return self;
+}
+
+- (CGPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate
+{
+ mbgl::ScreenCoordinate sc = _pointForFn(MGLLatLngFromLocationCoordinate2D(coordinate));
+ return CGPointMake(sc.x * self.scale, sc.y * self.scale);
+}
+@end
+
@interface MGLMapSnapshotter()
@property (nonatomic) MGLMapSnapshotOptions *options;
@end
@@ -60,7 +87,7 @@ @implementation MGLMapSnapshotter {
std::unique_ptr> _snapshotCallback;
}
-- (instancetype)initWithOptions:(MGLMapSnapshotOptions*)options;
+- (instancetype)initWithOptions:(MGLMapSnapshotOptions *)options
{
self = [super init];
if (self) {
@@ -102,12 +129,12 @@ - (instancetype)initWithOptions:(MGLMapSnapshotOptions*)options;
return self;
}
-- (void)startWithCompletionHandler:(MGLMapSnapshotCompletionHandler)completion;
+- (void)startWithCompletionHandler:(MGLMapSnapshotCompletionHandler)completion
{
[self startWithQueue:dispatch_get_main_queue() completionHandler:completion];
}
-- (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshotCompletionHandler)completion;
+- (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshotCompletionHandler)completion
{
if ([self isLoading]) {
[NSException raise:NSInternalInconsistencyException
@@ -117,7 +144,7 @@ - (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshot
_loading = true;
dispatch_async(queue, ^{
- _snapshotCallback = std::make_unique>(*mbgl::Scheduler::GetCurrent(), [=](std::exception_ptr mbglError, mbgl::PremultipliedImage image) {
+ _snapshotCallback = std::make_unique>(*mbgl::Scheduler::GetCurrent(), [=](std::exception_ptr mbglError, mbgl::PremultipliedImage image, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn) {
_loading = false;
if (mbglError) {
NSString *description = @(mbgl::util::toString(mbglError).c_str());
@@ -171,7 +198,8 @@ - (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshot
// Dispatch result to origin queue
dispatch_async(queue, ^{
- completion(compositedImage, nil);
+ MGLMapSnapshot* snapshot = [[MGLMapSnapshot alloc] initWithImage:compositedImage scale:self.options.scale pointForFn:pointForFn];
+ completion(snapshot, nil);
});
});
}
@@ -180,10 +208,87 @@ - (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshot
});
}
-- (void)cancel;
+- (void)cancel
{
_snapshotCallback.reset();
_mbglMapSnapshotter.reset();
}
+- (NSURL *)styleURL
+{
+ NSString *styleURLString = @(_mbglMapSnapshotter->getStyleURL().c_str());
+ return styleURLString.length ? [NSURL URLWithString:styleURLString] : nil;
+}
+
+- (void)setStyleURL:(NSURL *)url
+{
+ _mbglMapSnapshotter->setStyleURL(std::string([url.absoluteString UTF8String]));
+}
+
+- (CGSize)size
+{
+ mbgl::Size size = _mbglMapSnapshotter->getSize();
+ return CGSizeMake(size.width, size.height);
+}
+
+- (void)setSize:(CGSize)size
+{
+ _mbglMapSnapshotter->setSize({
+ static_cast(MAX(size.width, MGLSnapshotterMinimumPixelSize)),
+ static_cast(MAX(size.height, MGLSnapshotterMinimumPixelSize))
+ });
+}
+
+- (MGLMapCamera *)camera
+{
+ mbgl::CameraOptions cameraOptions = _mbglMapSnapshotter->getCameraOptions();
+ CGFloat pitch = *cameraOptions.pitch;
+ CLLocationDirection heading = mbgl::util::wrap(*cameraOptions.angle, 0., 360.);
+ CLLocationDistance distance = MGLAltitudeForZoomLevel(*cameraOptions.zoom, pitch, cameraOptions.center->latitude(), [self size]);
+ return [MGLMapCamera cameraLookingAtCenterCoordinate:MGLLocationCoordinate2DFromLatLng(*cameraOptions.center)
+ fromDistance:distance
+ pitch:pitch
+ heading:heading];
+}
+
+- (void)setCamera:(MGLMapCamera *)camera
+{
+ mbgl::CameraOptions cameraOptions;
+ CLLocationCoordinate2D center;
+ if (CLLocationCoordinate2DIsValid(camera.centerCoordinate)) {
+ cameraOptions.center = MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate);
+ center = camera.centerCoordinate;
+ } else {
+ // Center is optional, but always set.
+ center = MGLLocationCoordinate2DFromLatLng(*_mbglMapSnapshotter->getCameraOptions().center);
+ }
+
+ cameraOptions.angle = MAX(0, camera.heading) * mbgl::util::DEG2RAD;
+ cameraOptions.zoom = MAX(0, MGLZoomLevelForAltitude(camera.altitude, camera.pitch, center.latitude, [self size]));
+ cameraOptions.pitch = MAX(0, camera.pitch);
+}
+
+- (double)zoomLevel
+{
+ mbgl::CameraOptions cameraOptions = _mbglMapSnapshotter->getCameraOptions();
+ return MGLAltitudeForZoomLevel(*cameraOptions.zoom, *cameraOptions.pitch, cameraOptions.center->latitude(), [self size]);
+}
+
+- (void)setZoomLevel:(double)zoomLevel
+{
+ mbgl::CameraOptions cameraOptions = _mbglMapSnapshotter->getCameraOptions();
+ cameraOptions.zoom = zoomLevel;
+ _mbglMapSnapshotter->setCameraOptions(cameraOptions);
+}
+
+- (MGLCoordinateBounds)coordinateBounds
+{
+ return MGLCoordinateBoundsFromLatLngBounds(_mbglMapSnapshotter->getRegion());
+}
+
+- (void)setCoordinateBounds:(MGLCoordinateBounds)coordinateBounds
+{
+ _mbglMapSnapshotter->setRegion(MGLLatLngBoundsFromCoordinateBounds(coordinateBounds));
+}
+
@end
diff --git a/platform/darwin/test/MGLDocumentationExampleTests.swift b/platform/darwin/test/MGLDocumentationExampleTests.swift
index 42c656f203d..8762af9ba42 100644
--- a/platform/darwin/test/MGLDocumentationExampleTests.swift
+++ b/platform/darwin/test/MGLDocumentationExampleTests.swift
@@ -278,6 +278,24 @@ class MGLDocumentationExampleTests: XCTestCase, MGLMapViewDelegate {
//#-end-example-code
}
+ func testMGLMapSnapshotter() {
+ //#-example-code
+ let camera = MGLMapCamera(lookingAtCenter: CLLocationCoordinate2D(latitude: 37.7184, longitude: -122.4365), fromDistance: 100, pitch: 20, heading: 0)
+
+ let options = MGLMapSnapshotOptions(styleURL: MGLStyle.satelliteStreetsStyleURL(), camera: camera, size: CGSize(width: 320, height: 480))
+ options.zoomLevel = 10
+
+ let snapshotter = MGLMapSnapshotter(options: options)
+ snapshotter.start { (snapshot, error) in
+ if error != nil {
+ // error handler
+ } else {
+ // image handler
+ }
+ }
+ //#-end-example-code
+ }
+
// For testMGLMapView().
func myCustomFunction() {}
}
diff --git a/platform/default/mbgl/gl/headless_frontend.cpp b/platform/default/mbgl/gl/headless_frontend.cpp
index 5d2932258a3..2cbb624bd09 100644
--- a/platform/default/mbgl/gl/headless_frontend.cpp
+++ b/platform/default/mbgl/gl/headless_frontend.cpp
@@ -1,6 +1,8 @@
#include
#include
+#include
#include
+#include
#include
namespace mbgl {
@@ -83,4 +85,12 @@ PremultipliedImage HeadlessFrontend::render(Map& map) {
return result;
}
+optional HeadlessFrontend::getTransformState() const {
+ if (updateParameters) {
+ return updateParameters->transformState;
+ } else {
+ return {};
+ }
+}
+
} // namespace mbgl
diff --git a/platform/default/mbgl/gl/headless_frontend.hpp b/platform/default/mbgl/gl/headless_frontend.hpp
index 33503bc13b1..4d1116904ed 100644
--- a/platform/default/mbgl/gl/headless_frontend.hpp
+++ b/platform/default/mbgl/gl/headless_frontend.hpp
@@ -14,6 +14,7 @@ class Scheduler;
class Renderer;
class RendererBackend;
class Map;
+class TransformState;
class HeadlessFrontend : public RendererFrontend {
public:
@@ -34,6 +35,8 @@ class HeadlessFrontend : public RendererFrontend {
PremultipliedImage readStillImage();
PremultipliedImage render(Map&);
+ optional getTransformState() const;
+
private:
Size size;
float pixelRatio;
diff --git a/platform/default/mbgl/map/map_snapshotter.cpp b/platform/default/mbgl/map/map_snapshotter.cpp
index 95c46344fec..7b4ec5913bb 100644
--- a/platform/default/mbgl/map/map_snapshotter.cpp
+++ b/platform/default/mbgl/map/map_snapshotter.cpp
@@ -3,9 +3,11 @@
#include
#include
#include
+#include
#include
#include
#include
+#include
namespace mbgl {
@@ -20,6 +22,18 @@ class MapSnapshotter::Impl {
const optional region,
const optional programCacheDir);
+ void setStyleURL(std::string styleURL);
+ std::string getStyleURL() const;
+
+ void setSize(Size);
+ Size getSize() const;
+
+ void setCameraOptions(CameraOptions);
+ CameraOptions getCameraOptions() const;
+
+ void setRegion(LatLngBounds);
+ LatLngBounds getRegion() const;
+
void snapshot(ActorRef);
private:
@@ -44,18 +58,80 @@ MapSnapshotter::Impl::Impl(FileSource& fileSource,
// Set region, if specified
if (region) {
- mbgl::EdgeInsets insets = { 0, 0, 0, 0 };
- std::vector latLngs = { region->southwest(), region->northeast() };
- map.jumpTo(map.cameraForLatLngs(latLngs, insets));
+ this->setRegion(*region);
}
}
void MapSnapshotter::Impl::snapshot(ActorRef callback) {
map.renderStill([this, callback = std::move(callback)] (std::exception_ptr error) mutable {
- callback.invoke(&MapSnapshotter::Callback::operator(), error, error ? PremultipliedImage() : frontend.readStillImage());
+
+ // Create lambda that captures the current transform state
+ // and can be used to translate for geographic to screen
+ // coordinates
+ assert (frontend.getTransformState());
+ PointForFn pointForFn { [=, center=map.getLatLng(), transformState = *frontend.getTransformState()] (const LatLng& latLng) {
+ LatLng unwrappedLatLng = latLng.wrapped();
+ unwrappedLatLng.unwrapForShortestPath(center);
+ Transform transform { transformState };
+ return transform.latLngToScreenCoordinate(unwrappedLatLng);
+ }};
+
+ // Collect all source attributions
+ std::vector attributions;
+ for (auto source : map.getStyle().getSources()) {
+ auto attribution = source->getAttribution();
+ if (attribution) {
+ attributions.push_back(*attribution);
+ }
+ }
+
+ // Invoke callback
+ callback.invoke(
+ &MapSnapshotter::Callback::operator(),
+ error,
+ error ? PremultipliedImage() : frontend.readStillImage(),
+ std::move(attributions),
+ std::move(pointForFn)
+ );
});
}
+void MapSnapshotter::Impl::setStyleURL(std::string styleURL) {
+ map.getStyle().loadURL(styleURL);
+}
+
+std::string MapSnapshotter::Impl::getStyleURL() const {
+ return map.getStyle().getURL();
+}
+
+void MapSnapshotter::Impl::setSize(Size size) {
+ map.setSize(size);
+ frontend.setSize(size);
+}
+
+Size MapSnapshotter::Impl::getSize() const {
+ return map.getSize();
+}
+
+void MapSnapshotter::Impl::setCameraOptions(CameraOptions cameraOptions) {
+ map.jumpTo(cameraOptions);
+}
+
+CameraOptions MapSnapshotter::Impl::getCameraOptions() const {
+ EdgeInsets insets;
+ return map.getCameraOptions(insets);
+}
+
+void MapSnapshotter::Impl::setRegion(LatLngBounds region) {
+ mbgl::EdgeInsets insets = { 0, 0, 0, 0 };
+ std::vector latLngs = { region.southwest(), region.northeast() };
+ map.jumpTo(map.cameraForLatLngs(latLngs, insets));
+}
+
+LatLngBounds MapSnapshotter::Impl::getRegion() const {
+ return map.latLngBoundsForCamera(getCameraOptions());
+}
+
MapSnapshotter::MapSnapshotter(FileSource& fileSource,
Scheduler& scheduler,
const std::string& styleURL,
@@ -70,7 +146,39 @@ MapSnapshotter::MapSnapshotter(FileSource& fileSource,
MapSnapshotter::~MapSnapshotter() = default;
void MapSnapshotter::snapshot(ActorRef callback) {
- impl->actor().invoke(&Impl::snapshot, std::move(callback));
+ impl->actor().invoke(&Impl::snapshot, std::move(callback));
+}
+
+void MapSnapshotter::setStyleURL(const std::string& styleURL) {
+ impl->actor().invoke(&Impl::setStyleURL, styleURL);
+}
+
+std::string MapSnapshotter::getStyleURL() const {
+ return impl->actor().ask(&Impl::getStyleURL).get();
+}
+
+void MapSnapshotter::setSize(const Size& size) {
+ impl->actor().invoke(&Impl::setSize, size);
+}
+
+Size MapSnapshotter::getSize() const {
+ return impl->actor().ask(&Impl::getSize).get();
+}
+
+void MapSnapshotter::setCameraOptions(const CameraOptions& options) {
+ impl->actor().invoke(&Impl::setCameraOptions, options);
+}
+
+CameraOptions MapSnapshotter::getCameraOptions() const {
+ return impl->actor().ask(&Impl::getCameraOptions).get();
+}
+
+void MapSnapshotter::setRegion(const LatLngBounds& bounds) {
+ impl->actor().invoke(&Impl::setRegion, std::move(bounds));
+}
+
+LatLngBounds MapSnapshotter::getRegion() const {
+ return impl->actor().ask(&Impl::getRegion).get();
}
} // namespace mbgl
diff --git a/platform/default/mbgl/map/map_snapshotter.hpp b/platform/default/mbgl/map/map_snapshotter.hpp
index 0afa848fd8a..985396e5a36 100644
--- a/platform/default/mbgl/map/map_snapshotter.hpp
+++ b/platform/default/mbgl/map/map_snapshotter.hpp
@@ -3,9 +3,12 @@
#include
#include
#include
+#include
#include
+#include
#include
+#include
#include
namespace mbgl {
@@ -16,6 +19,10 @@ class FileSource;
class Size;
class LatLngBounds;
+namespace style {
+class Style;
+} // namespace style
+
class MapSnapshotter {
public:
MapSnapshotter(FileSource& fileSource,
@@ -29,7 +36,21 @@ class MapSnapshotter {
~MapSnapshotter();
- using Callback = std::function;
+ void setStyleURL(const std::string& styleURL);
+ std::string getStyleURL() const;
+
+ void setSize(const Size&);
+ Size getSize() const;
+
+ void setCameraOptions(const CameraOptions&);
+ CameraOptions getCameraOptions() const;
+
+ void setRegion(const LatLngBounds&);
+ LatLngBounds getRegion() const;
+
+ using PointForFn = std::function;
+ using Attributions = std::vector;
+ using Callback = std::function;
void snapshot(ActorRef);
private:
diff --git a/platform/ios/app/MBXSnapshotsViewController.m b/platform/ios/app/MBXSnapshotsViewController.m
index ab5ad97c904..3bf93d8721c 100644
--- a/platform/ios/app/MBXSnapshotsViewController.m
+++ b/platform/ios/app/MBXSnapshotsViewController.m
@@ -51,11 +51,11 @@ - (MGLMapSnapshotter*) startSnapshotterForImageView:(UIImageView*) imageView coo
// Create and start the snapshotter
MGLMapSnapshotter* snapshotter = [[MGLMapSnapshotter alloc] initWithOptions:options];
- [snapshotter startWithCompletionHandler: ^(UIImage *image, NSError *error) {
+ [snapshotter startWithCompletionHandler: ^(MGLMapSnapshot* snapshot, NSError *error) {
if (error) {
NSLog(@"Could not load snapshot: %@", [error localizedDescription]);
} else {
- imageView.image = image;
+ imageView.image = snapshot.image;
}
}];
diff --git a/platform/macos/app/MapDocument.m b/platform/macos/app/MapDocument.m
index feef53062b5..0df6b105184 100644
--- a/platform/macos/app/MapDocument.m
+++ b/platform/macos/app/MapDocument.m
@@ -167,7 +167,7 @@ - (IBAction)takeSnapshot:(id)sender {
// Create and start the snapshotter
snapshotter = [[MGLMapSnapshotter alloc] initWithOptions:options];
- [snapshotter startWithCompletionHandler:^(NSImage *image, NSError *error) {
+ [snapshotter startWithCompletionHandler:^(MGLMapSnapshot *snapshot, NSError *error) {
if (error) {
NSLog(@"Could not load snapshot: %@", error.localizedDescription);
} else {
@@ -182,7 +182,7 @@ - (IBAction)takeSnapshot:(id)sender {
NSURL *fileURL = panel.URL;
NSBitmapImageRep *bitmapRep;
- for (NSImageRep *imageRep in image.representations) {
+ for (NSImageRep *imageRep in snapshot.image.representations) {
if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
bitmapRep = (NSBitmapImageRep *)imageRep;
break; // stop on first bitmap rep we find
@@ -190,7 +190,7 @@ - (IBAction)takeSnapshot:(id)sender {
}
if (!bitmapRep) {
- bitmapRep = [NSBitmapImageRep imageRepWithData:image.TIFFRepresentation];
+ bitmapRep = [NSBitmapImageRep imageRepWithData:snapshot.image.TIFFRepresentation];
}
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileURL.pathExtension, NULL /* inConformingToUTI */);