From f01588cac78b5e5411385faa451080a74320500b Mon Sep 17 00:00:00 2001 From: Fabian Guerra Soto Date: Wed, 4 Oct 2017 10:03:03 -0400 Subject: [ios, macos] Improve snap shotter documentation. (#10020) * [ios, macos] Improve snap shotter documentation. * [macos] Save snapshots in correct format * [macos] Renamed snapshot item to Export Image * [ios, macos] Clarify Snapshotter documentation. * [ios] Fix snapshot scale * [macOS] Fix snapshotter 4x scaling. * [ios] Fix snapshotter final image scale. * [ios, macos] Update snapshotter size documentation. * [ios, macos] Throw an exception when the snapshotter has already started. * [ios, macos] Add snapshotter header example. * [ios, macos] Use one of the predefined Foundation's exception names. --- platform/darwin/src/MGLMapSnapshotter.h | 85 ++++++++++++++++++++------- platform/darwin/src/MGLMapSnapshotter.mm | 78 +++++++++++++++--------- platform/darwin/src/MGLTypes.h | 2 + platform/ios/app/MBXSnapshotsViewController.m | 28 ++++----- platform/ios/src/UIImage+MGLAdditions.h | 2 +- platform/ios/src/UIImage+MGLAdditions.mm | 4 +- platform/macos/app/Base.lproj/MainMenu.xib | 2 +- platform/macos/app/MapDocument.m | 51 ++++++++-------- 8 files changed, 160 insertions(+), 92 deletions(-) diff --git a/platform/darwin/src/MGLMapSnapshotter.h b/platform/darwin/src/MGLMapSnapshotter.h index a2a4f1b331..615d39bee4 100644 --- a/platform/darwin/src/MGLMapSnapshotter.h +++ b/platform/darwin/src/MGLMapSnapshotter.h @@ -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. + + 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; /** - 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,46 @@ 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. + + ### Example + + ```swift + var camera = MGLMapCamera() + camera.centerCoordinate = CLLocationCoordinate2D(latitude: 37.7184, longitude: -122.4365) + camera.pitch = 20 + + var 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 + } + } + ``` */ MGL_EXPORT @interface MGLMapSnapshotter : NSObject @@ -92,6 +132,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; diff --git a/platform/darwin/src/MGLMapSnapshotter.mm b/platform/darwin/src/MGLMapSnapshotter.mm index c81fd39c4a..12b932daa3 100644 --- a/platform/darwin/src/MGLMapSnapshotter.mm +++ b/platform/darwin/src/MGLMapSnapshotter.mm @@ -13,6 +13,7 @@ #import "MGLOfflineStorage_Private.h" #import "MGLGeometry_Private.h" #import "NSBundle+MGLAdditions.h" +#import "MGLStyle.h" #if TARGET_OS_IPHONE #import "UIImage+MGLAdditions.h" @@ -20,12 +21,19 @@ #import "NSImage+MGLAdditions.h" #endif +const CGPoint MGLLogoImagePosition = CGPointMake(8, 8); +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 @@ @end +@interface MGLMapSnapshotter() +@property (nonatomic) MGLMapSnapshotOptions *options; +@end + @implementation MGLMapSnapshotter { - std::shared_ptr mbglThreadPool; - std::unique_ptr mbglMapSnapshotter; - std::unique_ptr> snapshotCallback; + std::shared_ptr _mbglThreadPool; + std::unique_ptr _mbglMapSnapshotter; + std::unique_ptr> _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(MAX(options.size.width, 64)), - static_cast(MAX(options.size.height, 64)) + static_cast(MAX(options.size.width, MGLSnapshotterMinimumPixelSize)), + static_cast(MAX(options.size.height, MGLSnapshotterMinimumPixelSize)) }; float pixelRatio = MAX(options.scale, 1); @@ -73,17 +87,17 @@ 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 region; - if (!MGLCoordinateBoundsIsEmpty(options.region)) { - region = MGLLatLngBoundsFromCoordinateBounds(options.region); + mbgl::optional coordinateBounds; + if (!MGLCoordinateBoundsIsEmpty(options.coordinateBounds)) { + coordinateBounds = MGLLatLngBoundsFromCoordinateBounds(options.coordinateBounds); } // Create the snapshotter - mbglMapSnapshotter = std::make_unique(*mbglFileSource, *mbglThreadPool, styleURL, size, pixelRatio, cameraOptions, region); + _mbglMapSnapshotter = std::make_unique(*mbglFileSource, *_mbglThreadPool, styleURL, size, pixelRatio, cameraOptions, coordinateBounds); } return self; } @@ -96,30 +110,30 @@ - (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:NSInternalInconsistencyException + format:@"Already started this snapshotter."]; } _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) { _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]; // 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); @@ -127,20 +141,30 @@ #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 @@ }); } }); - mbglMapSnapshotter->snapshot(snapshotCallback->self()); + _mbglMapSnapshotter->snapshot(_snapshotCallback->self()); }); } - (void)cancel; { - snapshotCallback.reset(); - mbglMapSnapshotter.reset(); + _snapshotCallback.reset(); + _mbglMapSnapshotter.reset(); } @end diff --git a/platform/darwin/src/MGLTypes.h b/platform/darwin/src/MGLTypes.h index b3227e1cdf..5c32791c2f 100644 --- a/platform/darwin/src/MGLTypes.h +++ b/platform/darwin/src/MGLTypes.h @@ -47,6 +47,8 @@ typedef NS_ENUM(NSInteger, MGLErrorCode) { MGLErrorCodeParseStyleFailed = 4, /** An attempt to load the style failed. */ MGLErrorCodeLoadStyleFailed = 5, + /** An error occurred while snapshotting the map. */ + MGLErrorCodeSnapshotFailed = 6, }; /** Options for enabling debugging features in an `MGLMapView` instance. */ diff --git a/platform/ios/app/MBXSnapshotsViewController.m b/platform/ios/app/MBXSnapshotsViewController.m index d26479f085..ab5ad97c90 100644 --- a/platform/ios/app/MBXSnapshotsViewController.m +++ b/platform/ios/app/MBXSnapshotsViewController.m @@ -18,27 +18,27 @@ @implementation MBXSnapshotsViewController { // Top row - MGLMapSnapshotter* snapshotterTL; - MGLMapSnapshotter* snapshotterTM; - MGLMapSnapshotter* snapshotterTR; + MGLMapSnapshotter* topLeftSnapshotter; + MGLMapSnapshotter* topCenterSnapshotter; + MGLMapSnapshotter* topRightSnapshotter; // Bottom row - MGLMapSnapshotter* snapshotterBL; - MGLMapSnapshotter* snapshotterBM; - MGLMapSnapshotter* snapshotterBR; + MGLMapSnapshotter* bottomLeftSnapshotter; + MGLMapSnapshotter* bottomCenterSnapshotter; + MGLMapSnapshotter* bottomRightSnapshotter; } - (void)viewDidLoad { [super viewDidLoad]; // Start snapshotters - snapshotterTL = [self startSnapshotterForImageView:_snapshotImageViewTL coordinates:CLLocationCoordinate2DMake(37.7184, -122.4365)]; - snapshotterTM = [self startSnapshotterForImageView:_snapshotImageViewTM coordinates:CLLocationCoordinate2DMake(38.8936, -77.0146)]; - snapshotterTR = [self startSnapshotterForImageView:_snapshotImageViewTR coordinates:CLLocationCoordinate2DMake(-13.1356, -74.2442)]; + topLeftSnapshotter = [self startSnapshotterForImageView:_snapshotImageViewTL coordinates:CLLocationCoordinate2DMake(37.7184, -122.4365)]; + topCenterSnapshotter = [self startSnapshotterForImageView:_snapshotImageViewTM coordinates:CLLocationCoordinate2DMake(38.8936, -77.0146)]; + topRightSnapshotter = [self startSnapshotterForImageView:_snapshotImageViewTR coordinates:CLLocationCoordinate2DMake(-13.1356, -74.2442)]; - snapshotterBL = [self startSnapshotterForImageView:_snapshotImageViewBL coordinates:CLLocationCoordinate2DMake(52.5072, 13.4247)]; - snapshotterBM = [self startSnapshotterForImageView:_snapshotImageViewBM coordinates:CLLocationCoordinate2DMake(60.2118, 24.6754)]; - snapshotterBR = [self startSnapshotterForImageView:_snapshotImageViewBR coordinates:CLLocationCoordinate2DMake(31.2780, 121.4286)]; + bottomLeftSnapshotter = [self startSnapshotterForImageView:_snapshotImageViewBL coordinates:CLLocationCoordinate2DMake(52.5072, 13.4247)]; + bottomCenterSnapshotter = [self startSnapshotterForImageView:_snapshotImageViewBM coordinates:CLLocationCoordinate2DMake(60.2118, 24.6754)]; + bottomRightSnapshotter = [self startSnapshotterForImageView:_snapshotImageViewBR coordinates:CLLocationCoordinate2DMake(31.2780, 121.4286)]; } - (MGLMapSnapshotter*) startSnapshotterForImageView:(UIImageView*) imageView coordinates:(CLLocationCoordinate2D) coordinates { @@ -46,8 +46,8 @@ MGLMapCamera* mapCamera = [[MGLMapCamera alloc] init]; mapCamera.pitch = 20; mapCamera.centerCoordinate = coordinates; - MGLMapSnapshotOptions* options = [[MGLMapSnapshotOptions alloc] initWithStyleURL:[NSURL URLWithString:@"mapbox://styles/mapbox/traffic-day-v2"] camera:mapCamera size:CGSizeMake(imageView.frame.size.width, imageView.frame.size.height)]; - options.zoom = 10; + MGLMapSnapshotOptions* options = [[MGLMapSnapshotOptions alloc] initWithStyleURL:[MGLStyle satelliteStreetsStyleURL] camera:mapCamera size:CGSizeMake(imageView.frame.size.width, imageView.frame.size.height)]; + options.zoomLevel = 10; // Create and start the snapshotter MGLMapSnapshotter* snapshotter = [[MGLMapSnapshotter alloc] initWithOptions:options]; diff --git a/platform/ios/src/UIImage+MGLAdditions.h b/platform/ios/src/UIImage+MGLAdditions.h index 3c179d6324..22bb740242 100644 --- a/platform/ios/src/UIImage+MGLAdditions.h +++ b/platform/ios/src/UIImage+MGLAdditions.h @@ -8,7 +8,7 @@ NS_ASSUME_NONNULL_BEGIN - (nullable instancetype)initWithMGLStyleImage:(const mbgl::style::Image *)styleImage; -- (nullable instancetype)initWithMGLPremultipliedImage:(const mbgl::PremultipliedImage&&)mbglImage; +- (nullable instancetype)initWithMGLPremultipliedImage:(const mbgl::PremultipliedImage&&)mbglImage scale:(CGFloat)scale; - (std::unique_ptr)mgl_styleImageWithIdentifier:(NSString *)identifier; diff --git a/platform/ios/src/UIImage+MGLAdditions.mm b/platform/ios/src/UIImage+MGLAdditions.mm index 7cf1ed9bcc..8ab1d5c259 100644 --- a/platform/ios/src/UIImage+MGLAdditions.mm +++ b/platform/ios/src/UIImage+MGLAdditions.mm @@ -22,14 +22,14 @@ return self; } -- (nullable instancetype)initWithMGLPremultipliedImage:(const mbgl::PremultipliedImage&&)mbglImage +- (nullable instancetype)initWithMGLPremultipliedImage:(const mbgl::PremultipliedImage&&)mbglImage scale:(CGFloat)scale { CGImageRef image = CGImageFromMGLPremultipliedImage(mbglImage.clone()); if (!image) { return nil; } - self = [self initWithCGImage:image scale:1.0 orientation:UIImageOrientationUp]; + self = [self initWithCGImage:image scale:scale orientation:UIImageOrientationUp]; CGImageRelease(image); return self; diff --git a/platform/macos/app/Base.lproj/MainMenu.xib b/platform/macos/app/Base.lproj/MainMenu.xib index 9a53ba9d4b..3243838848 100644 --- a/platform/macos/app/Base.lproj/MainMenu.xib +++ b/platform/macos/app/Base.lproj/MainMenu.xib @@ -128,7 +128,7 @@ - + diff --git a/platform/macos/app/MapDocument.m b/platform/macos/app/MapDocument.m index 36ca4ad228..feef53062b 100644 --- a/platform/macos/app/MapDocument.m +++ b/platform/macos/app/MapDocument.m @@ -162,50 +162,49 @@ NS_ARRAY_OF(id ) *MBXFlattenedShapes(NS_ARRAY_OF(id