-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[ios, macos] Improve snap shotter documentation. #10020
Changes from 9 commits
5213133
64e0cc2
d16aea7
dff1bc0
b767c21
6ea1e41
6beeb87
9eaed01
21fe811
a072ecc
73f84e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,40 +12,52 @@ MGL_EXPORT | |
@interface MGLMapSnapshotOptions : NSObject | ||
|
||
/** | ||
Creates a set of options with the minimum required information | ||
@param styleURL the style url to use | ||
@param camera the camera settings | ||
@param size the image size | ||
Creates a set of options with the minimum required information. | ||
|
||
@param styleURL 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. | ||
@param size The image size. | ||
*/ | ||
- (instancetype)initWithStyleURL:(NSURL*)styleURL camera:(MGLMapCamera*)camera size:(CGSize)size; | ||
- (instancetype)initWithStyleURL:(nullable NSURL *)styleURL camera:(MGLMapCamera *)camera size:(CGSize)size; | ||
|
||
#pragma mark - Configuring the map | ||
#pragma mark - Configuring the Map | ||
|
||
/** | ||
The style URL for these options. | ||
URL of the map style to snapshot. | ||
*/ | ||
@property (nonatomic, readonly) NSURL* styleURL; | ||
@property (nonatomic, readonly) NSURL *styleURL; | ||
|
||
/** | ||
The zoom. Default is 0. | ||
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 zoom; | ||
@property (nonatomic) double zoomLevel; | ||
|
||
/** | ||
The `MGLMapcamera` options to use. | ||
A camera representing the viewport visible in the snapshot. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
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; | ||
@property (nonatomic) MGLMapCamera *camera; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is |
||
|
||
/** | ||
A region to capture. Overrides the center coordinate | ||
in the mapCamera options if set | ||
The cooordinate 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 region; | ||
@property (nonatomic) MGLCoordinateBounds coordinateBounds; | ||
|
||
#pragma mark - Configuring the image | ||
#pragma mark - Configuring the Image | ||
|
||
/** | ||
The size of the output image. Minimum is 64x64 | ||
The size of the output image, measured in points. | ||
|
||
*/ | ||
@property (nonatomic, readonly) CGSize size; | ||
|
||
|
@@ -57,18 +69,26 @@ MGL_EXPORT | |
|
||
@end | ||
|
||
#if TARGET_OS_IPHONE | ||
/** | ||
A block to processes the result or error of a snapshot request. | ||
|
||
The result will be either an `MGLImage` or a `NSError` | ||
@param snapshot The `UIImage` that was generated or `nil` if an error occurred. | ||
@param error The error that occured or `nil` when successful. | ||
*/ | ||
typedef void (^MGLMapSnapshotCompletionHandler)(UIImage* _Nullable snapshot, NSError* _Nullable error); | ||
#else | ||
/** | ||
A block to processes the result or error of a snapshot request. | ||
|
||
@param snapshot The image that was generated or `nil` if an error occurred. | ||
@param snapshot The `NSImage` that was generated or `nil` if an error occurred. | ||
@param error The eror that occured or `nil` when succesful. | ||
*/ | ||
typedef void (^MGLMapSnapshotCompletionHandler)(MGLImage* _Nullable snapshot, NSError* _Nullable error); | ||
typedef void (^MGLMapSnapshotCompletionHandler)(NSImage* _Nullable snapshot, NSError* _Nullable error); | ||
#endif | ||
|
||
/** | ||
A utility object for capturing map-based images. | ||
An immutable utility object for capturing map-based images. | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As tail work, it would be helpful to add a small code example of working with MGLMapSnapshotter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tail work ticket in our ios-sdk repo https://github.com/mapbox/ios-sdk/issues/332 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was referring to a short code example here inside the header, similar to what we do for classes like MGLShapeSource and MGLMapView, so that the example shows up at the beginning of the MGLMapSnapshotter class reference. |
||
MGL_EXPORT | ||
@interface MGLMapSnapshotter : NSObject | ||
|
@@ -92,6 +112,9 @@ MGL_EXPORT | |
|
||
/** | ||
Cancels the snapshot creation request, if any. | ||
|
||
Once you call this method, you cannot resume the snapshot. In order to obtain the | ||
snapshot, create a new `MGLMapSnapshotter` object. | ||
*/ | ||
- (void)cancel; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,19 +13,27 @@ | |
#import "MGLOfflineStorage_Private.h" | ||
#import "MGLGeometry_Private.h" | ||
#import "NSBundle+MGLAdditions.h" | ||
#import "MGLStyle.h" | ||
|
||
#if TARGET_OS_IPHONE | ||
#import "UIImage+MGLAdditions.h" | ||
#else | ||
#import "NSImage+MGLAdditions.h" | ||
#endif | ||
|
||
const CGPoint MGLLogoImagePosition = CGPointMake(8, 8); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be interesting to make this position configurable as tail work. |
||
const CGFloat MGLSnapshotterMinimumPixelSize = 64; | ||
|
||
@implementation MGLMapSnapshotOptions | ||
|
||
- (instancetype _Nonnull)initWithStyleURL:(NSURL* _Nonnull)styleURL camera:(MGLMapCamera*)camera size:(CGSize) size; | ||
- (instancetype _Nonnull)initWithStyleURL:(nullable NSURL*)styleURL camera:(MGLMapCamera*)camera size:(CGSize) size; | ||
{ | ||
self = [super init]; | ||
if (self) { | ||
if ( !styleURL) | ||
{ | ||
styleURL = [MGLStyle streetsStyleURLWithVersion:MGLStyleDefaultVersion]; | ||
} | ||
_styleURL = styleURL; | ||
_size = size; | ||
_camera = camera; | ||
|
@@ -41,28 +49,34 @@ - (instancetype _Nonnull)initWithStyleURL:(NSURL* _Nonnull)styleURL camera:(MGLM | |
|
||
@end | ||
|
||
@interface MGLMapSnapshotter() | ||
@property (nonatomic) MGLMapSnapshotOptions *options; | ||
@end | ||
|
||
@implementation MGLMapSnapshotter { | ||
|
||
std::shared_ptr<mbgl::ThreadPool> mbglThreadPool; | ||
std::unique_ptr<mbgl::MapSnapshotter> mbglMapSnapshotter; | ||
std::unique_ptr<mbgl::Actor<mbgl::MapSnapshotter::Callback>> snapshotCallback; | ||
std::shared_ptr<mbgl::ThreadPool> _mbglThreadPool; | ||
std::unique_ptr<mbgl::MapSnapshotter> _mbglMapSnapshotter; | ||
std::unique_ptr<mbgl::Actor<mbgl::MapSnapshotter::Callback>> _snapshotCallback; | ||
} | ||
|
||
- (instancetype)initWithOptions:(MGLMapSnapshotOptions*)options; | ||
{ | ||
self = [super init]; | ||
if (self) { | ||
_options = options; | ||
_loading = false; | ||
|
||
mbgl::DefaultFileSource *mbglFileSource = [MGLOfflineStorage sharedOfflineStorage].mbglFileSource; | ||
mbglThreadPool = mbgl::sharedThreadPool(); | ||
_mbglThreadPool = mbgl::sharedThreadPool(); | ||
|
||
std::string styleURL = std::string([options.styleURL.absoluteString UTF8String]); | ||
|
||
// Size; taking into account the minimum texture size for OpenGL ES | ||
// For non retina screens the ratio is 1:1 MGLSnapshotterMinimumPixelSize | ||
mbgl::Size size = { | ||
static_cast<uint32_t>(MAX(options.size.width, 64)), | ||
static_cast<uint32_t>(MAX(options.size.height, 64)) | ||
static_cast<uint32_t>(MAX(options.size.width, MGLSnapshotterMinimumPixelSize)), | ||
static_cast<uint32_t>(MAX(options.size.height, MGLSnapshotterMinimumPixelSize)) | ||
}; | ||
|
||
float pixelRatio = MAX(options.scale, 1); | ||
|
@@ -73,17 +87,17 @@ - (instancetype)initWithOptions:(MGLMapSnapshotOptions*)options; | |
cameraOptions.center = MGLLatLngFromLocationCoordinate2D(options.camera.centerCoordinate); | ||
} | ||
cameraOptions.angle = MAX(0, options.camera.heading) * mbgl::util::DEG2RAD; | ||
cameraOptions.zoom = MAX(0, options.zoom); | ||
cameraOptions.zoom = MAX(0, options.zoomLevel); | ||
cameraOptions.pitch = MAX(0, options.camera.pitch); | ||
|
||
// Region | ||
mbgl::optional<mbgl::LatLngBounds> region; | ||
if (!MGLCoordinateBoundsIsEmpty(options.region)) { | ||
region = MGLLatLngBoundsFromCoordinateBounds(options.region); | ||
mbgl::optional<mbgl::LatLngBounds> coordinateBounds; | ||
if (!MGLCoordinateBoundsIsEmpty(options.coordinateBounds)) { | ||
coordinateBounds = MGLLatLngBoundsFromCoordinateBounds(options.coordinateBounds); | ||
} | ||
|
||
// Create the snapshotter | ||
mbglMapSnapshotter = std::make_unique<mbgl::MapSnapshotter>(*mbglFileSource, *mbglThreadPool, styleURL, size, pixelRatio, cameraOptions, region); | ||
_mbglMapSnapshotter = std::make_unique<mbgl::MapSnapshotter>(*mbglFileSource, *_mbglThreadPool, styleURL, size, pixelRatio, cameraOptions, coordinateBounds); | ||
} | ||
return self; | ||
} | ||
|
@@ -96,51 +110,61 @@ - (void)startWithCompletionHandler:(MGLMapSnapshotCompletionHandler)completion; | |
- (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshotCompletionHandler)completion; | ||
{ | ||
if ([self isLoading]) { | ||
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Already started this snapshotter"}; | ||
NSError *error = [NSError errorWithDomain:MGLErrorDomain code:1 userInfo:userInfo]; | ||
dispatch_async(queue, ^{ | ||
completion(nil, error); | ||
}); | ||
return; | ||
[NSException raise:@"MGLAlreadyStartedSnapshotterException" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per #7258, use one of Foundation's predefined exception names or pull this string out into a public constant. |
||
format:@"Already started this snapshotter."]; | ||
} | ||
|
||
_loading = true; | ||
|
||
dispatch_async(queue, ^{ | ||
snapshotCallback = std::make_unique<mbgl::Actor<mbgl::MapSnapshotter::Callback>>(*mbgl::Scheduler::GetCurrent(), [=](std::exception_ptr mbglError, mbgl::PremultipliedImage image) { | ||
_snapshotCallback = std::make_unique<mbgl::Actor<mbgl::MapSnapshotter::Callback>>(*mbgl::Scheduler::GetCurrent(), [=](std::exception_ptr mbglError, mbgl::PremultipliedImage image) { | ||
_loading = false; | ||
if (mbglError) { | ||
NSString *description = @(mbgl::util::toString(mbglError).c_str()); | ||
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description}; | ||
NSError *error = [NSError errorWithDomain:MGLErrorDomain code:1 userInfo:userInfo]; | ||
NSError *error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeSnapshotFailed userInfo:userInfo]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per #10020 (comment), should we remove this error or replace it with an NSException? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes sense let the developer know that it has a snapshotter task running it's easy to forget this. A NSException is too drastic, I would like to continue with the NSError for the time being and evaluate this again once we made the snapshotter reusable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To elaborate, NSError is intended to problems outside the developer’s control, for issues that can’t be fixed in the developer’s own code. If the developer is reusing a snapshotter in their own code, I think that would be an error on their part and an appropriate use of NSException. But if MapKit simply sets the NSError in this case, then I guess it’s OK to follow their lead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with you NSError is intended to problems outside the developer's control. NSException is a better choice. Making the changes. |
||
|
||
// Dispatch result to origin queue | ||
dispatch_async(queue, ^{ | ||
completion(nil, error); | ||
}); | ||
} else { | ||
#if TARGET_OS_IPHONE | ||
MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image) scale:self.options.scale]; | ||
#else | ||
MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image)]; | ||
#endif | ||
|
||
// Process image watermark in a work queue | ||
dispatch_queue_t workQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); | ||
dispatch_async(workQueue, ^{ | ||
#if TARGET_OS_IPHONE | ||
UIImage *logoImage = [UIImage imageNamed:@"mapbox" inBundle:[NSBundle mgl_frameworkBundle] compatibleWithTraitCollection:nil]; | ||
|
||
UIGraphicsBeginImageContext(mglImage.size); | ||
UIGraphicsBeginImageContextWithOptions(mglImage.size, NO, self.options.scale); | ||
|
||
[mglImage drawInRect:CGRectMake(0, 0, mglImage.size.width, mglImage.size.height)]; | ||
[logoImage drawInRect:CGRectMake(8, mglImage.size.height - (8 + logoImage.size.height), logoImage.size.width,logoImage.size.height)]; | ||
[logoImage drawInRect:CGRectMake(MGLLogoImagePosition.x, mglImage.size.height - (MGLLogoImagePosition.y + logoImage.size.height), logoImage.size.width,logoImage.size.height)]; | ||
UIImage *compositedImage = UIGraphicsGetImageFromCurrentImageContext(); | ||
|
||
UIGraphicsEndImageContext(); | ||
#else | ||
NSImage *logoImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mgl_frameworkBundle] pathForResource:@"mapbox" ofType:@"pdf"]]; | ||
NSImage *compositedImage = mglImage; | ||
NSImage *sourceImage = mglImage; | ||
|
||
NSSize targetSize = NSMakeSize(self.options.size.width, self.options.size.height); | ||
NSRect targetFrame = NSMakeRect(0, 0, targetSize.width, targetSize.height); | ||
NSImage *compositedImage = nil; | ||
NSImageRep *sourceImageRep = [sourceImage bestRepresentationForRect:targetFrame | ||
context:nil | ||
hints:nil]; | ||
compositedImage = [[NSImage alloc] initWithSize:targetSize]; | ||
|
||
[compositedImage lockFocus]; | ||
[logoImage drawInRect:CGRectMake(8, 8, logoImage.size.width,logoImage.size.height)]; | ||
[sourceImageRep drawInRect: targetFrame]; | ||
[logoImage drawInRect:CGRectMake(MGLLogoImagePosition.x, MGLLogoImagePosition.y, logoImage.size.width,logoImage.size.height)]; | ||
[compositedImage unlockFocus]; | ||
|
||
#endif | ||
|
||
// Dispatch result to origin queue | ||
|
@@ -150,14 +174,14 @@ - (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshot | |
}); | ||
} | ||
}); | ||
mbglMapSnapshotter->snapshot(snapshotCallback->self()); | ||
_mbglMapSnapshotter->snapshot(_snapshotCallback->self()); | ||
}); | ||
} | ||
|
||
- (void)cancel; | ||
{ | ||
snapshotCallback.reset(); | ||
mbglMapSnapshotter.reset(); | ||
_snapshotCallback.reset(); | ||
_mbglMapSnapshotter.reset(); | ||
} | ||
|
||
@end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sure would be nice to implement #5583 instead of offering three different ways to set the viewport on a snapshot that interact in unintuitive ways.