Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

[ios, macos] Improve snap shotter documentation. #10020

Merged
merged 11 commits into from
Oct 4, 2017

Conversation

fabian-guerra
Copy link
Contributor

@fabian-guerra fabian-guerra commented Sep 18, 2017

Fixes #9920

  • Update documentation.
  • Improve snapshoter code (localization, error handling, naming).


/**
The size of the output image. Minimum is 64x64
The size of the output image. Minimum is 64x64.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#9891 (comment) asks if this is points or pixels. My reading of the implementation is that it is pixels. We should add that context here.

Copy link
Contributor

@boundsj boundsj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#9891 (comment) asks if the cancel method can be called more than once on the same instance of snapshotter. It appears to me that it can be and that N+1 calls would not cause any problem but I've not actually tested. I agree we should note that multiple calls are/are not a problem.

@fabian-guerra
Copy link
Contributor Author

Tail work #10038 for making snapshotter reusable.

@fabian-guerra fabian-guerra self-assigned this Sep 21, 2017
Copy link
Contributor

@boundsj boundsj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lgtm 👍

@1ec5 do you think there is anything else here we must do for v3.7.0?

@fabian-guerra fabian-guerra added this to the ios-v3.7.0 milestone Sep 22, 2017
@@ -12,40 +12,41 @@ MGL_EXPORT
@interface MGLMapSnapshotOptions : NSObject

/**
Creates a set of options with the minimum required information
Creates a set of options with the minimum required information.

@param styleURL the style url to use
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameter documentation should start with a capital letter and end with a period. URL should be capitalized. Note that this differs from the prevailing code style in the Android SDK.

This is based on the documentation for -[MGLMapView initWithFrame: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.

(Can local styles be snapshotted?)

/ref #9891 (comment)

@@ -12,40 +12,41 @@ MGL_EXPORT
@interface MGLMapSnapshotOptions : NSObject

/**
Creates a set of options with the minimum required information
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
*/
- (instancetype)initWithStyleURL:(NSURL*)styleURL camera:(MGLMapCamera*)camera size:(CGSize)size;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styleURL should be marked nullable, given the change to the implementation.

/ref #9891 (comment)


/**
The zoom. Default is 0.
The zoom level. Default is 0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate paragraph:

The default zoom level is 0.

*/
@property (nonatomic) double zoom;
@property (nonatomic) double zoomLevel;

/**
The `MGLMapcamera` options to use.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: MGLMapCamera. In any case, this description would make more sense:

A camera representing the viewport visible in the snapshot.

*/
@property (nonatomic) double zoom;
@property (nonatomic) double zoomLevel;

/**
The `MGLMapcamera` options to use.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the coordinateBounds documentation, this comment should explain what happens if both it and coordinateBounds are set.

What happens if the camera specifies an altitude that differs from the zoomLevel property? What value ends up being preferred?

@@ -96,7 +103,8 @@ - (void)startWithCompletionHandler:(MGLMapSnapshotCompletionHandler)completion;
- (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshotCompletionHandler)completion;
{
if ([self isLoading]) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Already started this snapshotter"};
NSString *errorMessage = NSLocalizedStringWithDefaultValue(@"ALREADY_STARTED_SNAPSHOTER", nil, nil, @"Already started this snapshotter", "User-friendly error description");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NSError is meant to be used for errors that are outside the developer’s control, and its localized error description may be displayed to the end user. But if a snapshotter can’t be reused and the developer tries to reuse one, then that’s avoidable programmer error, and we should assert instead of raising an error. If a snapshotter can be reused, then this error should be removed.

Either way, it does seem useful to retain an error parameter just in case, even if it goes unused for the time being.

@@ -47,6 +47,8 @@ typedef NS_ENUM(NSInteger, MGLErrorCode) {
MGLErrorCodeParseStyleFailed = 4,
/** An attempt to load the style failed. */
MGLErrorCodeLoadStyleFailed = 5,
/** An error occurred while attempting to get the callback. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An error occurred while snapshotting the map.

@@ -47,6 +47,8 @@ typedef NS_ENUM(NSInteger, MGLErrorCode) {
MGLErrorCodeParseStyleFailed = 4,
/** An attempt to load the style failed. */
MGLErrorCodeLoadStyleFailed = 5,
/** An error occurred while attempting to get the callback. */
MGLErrorCodeSnapShotterCallbackFailed = 6,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MGLErrorCodeSnapshotFailed


for (NSString *imageType in [NSImage imageTypes]) {
if ([uti isEqualToString:imageType]) {
if ([uti containsString:@"png"]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public.png isn’t the only UTI that contains the substring png, and UTIs that don’t contain png may still be PNGs. Use UTTypeConformsTo() from the Core Services framework to perform this check:

UTTypeConformsTo((__bridge CFStringRef)uti, kUTTypePNG)

} else if ([uti containsString:@"gif"]) {
fileType = NSGIFFileType;
} else if ([uti containsString:@"jpeg"]) {
fileType = NSJPEGFileType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NSBitmapImageFileTypeBMP and NSBitmapImageFileTypeJPEG2000 are also available.

@fabian-guerra fabian-guerra force-pushed the fabian-snapshoter-9920 branch 3 times, most recently from 7cc01ab to 0895f64 Compare September 28, 2017 16:49
Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the below, #9891 (comment) points out that the menu item should be in title case, not sentence case.

The size of the output image. Minimum is 64x64
The size of the output image.

The image may be no less than 64 pixels wide and no less than 64 pixels tall.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let’s clarify that we really mean pixels here, because most developers will read this thinking it’s a typo:

The image may be no less than 64 pixels wide (32 points wide on a 2× display) and no less than 64 pixels tall.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately we should handle the conversion ourselves so that developers don’t have to multiply by the backing scale factor, which is inconvenient and inconsistent with MapKit: #10088.

Copy link
Contributor Author

@fabian-guerra fabian-guerra Sep 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internally we handle the scale factor so size is actually in points instead of pixels in the end. Do you think this clarifies the issue?

The size of the output image may be no less than the following dimensions:

Non-retina 64 points wide and tall.
2× 32 points wide and tall.
3× 22 points wide and tall.

A region to capture. Overrides the center coordinate
in the mapCamera options if set
The cooordinate rectangle that encompasses the bounds to capture. Overrides the center coordinate
in the mapCamera options if set.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


/**
The `MGLMapcamera` options to use.
A camera representing the viewport visible in the snapshot.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


/**
The zoom. Default is 0.
The default zoom level is 0. Overrides the altitude in the mapCamera options if set.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Contributor

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.

/**
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 succesful.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: successful. (×2)


/**
A utility object for capturing map-based images.
An immutable utility object for capturing map-based images.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

@@ -112,7 +120,7 @@ - (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshot
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];
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

fileType = NSJPEGFileType;
}
if (UTTypeConformsTo((__bridge CFStringRef)uti, kUTTypeJPEG2000)) {
fileType = NSJPEGFileType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kUTTypeJPEG2000 translates to NSJPEG2000FileType, not NSJPEGFileType.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #10090.

}

// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mbglMapSnapshotter and the other ivars on MGLMapSnapshotter should begin with underscores.

NSBitmapImageFileType fileType;
if ([extension isEqualToString:@"png"]) {
NSString *uti;
[fileURL getResourceValue:&uti forKey:NSURLTypeIdentifierKey error:nil];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out this doesn’t work because no file has been laid down by this point. This code ends up producing a TIFF no matter what extension is specified.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #10090.

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/options.scale)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the developer sets a scale other than the device’s native scale, does the effective minimum pixel size change?


// 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, [UIScreen mainScreen].scale);
Copy link
Contributor

@frederoni frederoni Oct 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't expose a scale property then override it with [UIScreen mainScreen].scale. Either remove the scale or grab the scale from the options.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


/**
The size of the output image. Minimum is 64x64
The size of the output image.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per #10088 (comment), this property is actually intended to be measured in points (even though the minimum is in pixels).

The size of the output image, measured in points.

@friedbunny friedbunny added iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS labels Oct 3, 2017
Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good to go, other than the exception name.

completion(nil, error);
});
return;
[NSException raise:@"MGLAlreadyStartedSnapshotterException"
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

@fabian-guerra fabian-guerra merged commit f01588c into master Oct 4, 2017
@fabian-guerra fabian-guerra deleted the fabian-snapshoter-9920 branch October 4, 2017 14:03
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants