[ios, macos] Introduce custom drawing overlays for MGLMapSnapshotter

This commit is contained in:
Nadia Barbosa
2019-09-13 12:35:47 -07:00
committed by Nadia Barbosa
parent 40b0cb74cb
commit 8331b7adfe
5 changed files with 189 additions and 17 deletions

View File

@@ -5,6 +5,30 @@
NS_ASSUME_NONNULL_BEGIN
/**
An overlay that is placed within a `MGLMapSnapshot`.
To access this object, use `-[MGLMapSnapshotter startWithOverlayHandler:completionHandler:]`.
*/
MGL_EXPORT
@interface MGLMapSnapshotOverlay : NSObject
/**
The current `CGContext` that snapshot is drawing within. You may use this context
to perform additional custom drawing.
*/
@property (nonatomic, readonly) CGContextRef context;
@end
/**
A block provided during the snapshot drawing process, enabling the ability to
draw custom overlays rendered with Core Graphics.
@param snapshotOverlay The `MGLMapSnapshotOverlay` provided during snapshot drawing.
*/
typedef void (^MGLMapSnapshotOverlayHandler)(MGLMapSnapshotOverlay * snapshotOverlay);
/**
The options to use when creating images with the `MGLMapSnapshotter`.
*/
@@ -200,6 +224,16 @@ MGL_EXPORT
*/
- (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshotCompletionHandler)completionHandler;
/**
Starts the snapshot creation and executes the specified blocks with the result
on the specified queue. Use this option if you want to add custom drawing on top of the
resulting `MGLMapSnapShot`.
@param queue The queue to handle the result on.
@param overlayHandler The block to handle manipulation of the `MGLMapSnapshotter`'s `CGContext`.
@param completionHandler The block to handle the result in.
*/
- (void)startWithOverlayHandler:(MGLMapSnapshotOverlayHandler)overlayHandler completionHandler:(MGLMapSnapshotCompletionHandler)completionHandler;
/**
Cancels the snapshot creation request, if any.

View File

@@ -32,6 +32,26 @@
const CGPoint MGLLogoImagePosition = CGPointMake(8, 8);
const CGFloat MGLSnapshotterMinimumPixelSize = 64;
@interface MGLMapSnapshotOverlay()
- (instancetype)initWithContext:(CGContextRef)context;
@end
@implementation MGLMapSnapshotOverlay
- (instancetype) initWithContext:(CGContextRef)context {
self = [super init];
if (self) {
_context = context;
}
return self;
}
@end
@implementation MGLMapSnapshotOptions
- (instancetype _Nonnull)initWithStyleURL:(nullable NSURL *)styleURL camera:(MGLMapCamera *)camera size:(CGSize)size
@@ -183,7 +203,15 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
[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)completionHandler {
[self startWithQueue:queue overlayHandler:nil completionHandler:completionHandler];
}
- (void)startWithOverlayHandler:(MGLMapSnapshotOverlayHandler)overlayHandler completionHandler:(MGLMapSnapshotCompletionHandler)completion {
[self startWithQueue:dispatch_get_main_queue() overlayHandler:overlayHandler completionHandler:completion];
}
- (void)startWithQueue:(dispatch_queue_t)queue overlayHandler:(MGLMapSnapshotOverlayHandler)overlayHandler completionHandler:(MGLMapSnapshotCompletionHandler)completion
{
if (!mbgl::Scheduler::GetCurrent()) {
[NSException raise:NSInvalidArgumentException
@@ -210,8 +238,8 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
// capture weakSelf to avoid retain cycle if callback is never called (ie snapshot cancelled)
_snapshotCallback = std::make_unique<mbgl::Actor<mbgl::MapSnapshotter::Callback>>(
*mbgl::Scheduler::GetCurrent(),
[=](std::exception_ptr mbglError, mbgl::PremultipliedImage image, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn) {
*mbgl::Scheduler::GetCurrent(),
[=](std::exception_ptr mbglError, mbgl::PremultipliedImage image, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn) {
__typeof__(self) strongSelf = weakSelf;
// If self had died, _snapshotCallback would have been destroyed and this block would not be executed
@@ -224,7 +252,7 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
NSString *description = @(mbgl::util::toString(mbglError).c_str());
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description};
NSError *error = [NSError errorWithDomain:MGLErrorDomain code:MGLErrorCodeSnapshotFailed userInfo:userInfo];
// Dispatch to result queue
dispatch_async(queue, ^{
strongSelf.completion(nil, error);
@@ -238,11 +266,12 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
mglImage.size = NSMakeSize(mglImage.size.width / strongSelf.options.scale,
mglImage.size.height / strongSelf.options.scale);
#endif
[strongSelf drawAttributedSnapshot:attributions snapshotImage:mglImage pointForFn:pointForFn latLngForFn:latLngForFn];
[strongSelf drawAttributedSnapshot:attributions snapshotImage:mglImage pointForFn:pointForFn latLngForFn:latLngForFn overlayHandler:overlayHandler];
}
strongSelf->_snapshotCallback = NULL;
});
});
// Launches snapshot on background Thread owned by mbglMapSnapshotter
// _snapshotCallback->self() is an ActorRef: if the callback is destroyed, further messages
@@ -250,7 +279,7 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
_mbglMapSnapshotter->snapshot(_snapshotCallback->self());
}
+ (MGLImage*)drawAttributedSnapshotWorker:(mbgl::MapSnapshotter::Attributions)attributions snapshotImage:(MGLImage *)mglImage pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn scale:(CGFloat)scale size:(CGSize)size {
+ (MGLImage*)drawAttributedSnapshotWorker:(mbgl::MapSnapshotter::Attributions)attributions snapshotImage:(MGLImage *)mglImage pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn scale:(CGFloat)scale size:(CGSize)size overlayHandler:(MGLMapSnapshotOverlayHandler)overlayHandler {
NSArray<MGLAttributionInfo *>* attributionInfo = [MGLMapSnapshotter generateAttributionInfos:attributions];
@@ -292,7 +321,23 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
UIGraphicsBeginImageContextWithOptions(mglImage.size, NO, scale);
[mglImage drawInRect:CGRectMake(0, 0, mglImage.size.width, mglImage.size.height)];
CGContextRef currentContext = UIGraphicsGetCurrentContext();
if (currentContext && overlayHandler) {
MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:currentContext];
CGContextSaveGState(snapshotOverlay.context);
overlayHandler(snapshotOverlay);
CGContextRestoreGState(snapshotOverlay.context);
currentContext = UIGraphicsGetCurrentContext();
}
if (!currentContext && overlayHandler) {
// If the current context has been corrupted by the user,
// return nil so we can generate an error later.
return nil;
}
[logoImage drawInRect:logoImageRect];
UIImage *currentImage = UIGraphicsGetImageFromCurrentImageContext();
@@ -359,6 +404,21 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
[sourceImageRep drawInRect: targetFrame];
NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
if (currentContext && overlayHandler) {
MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:currentContext.CGContext];
[currentContext saveGraphicsState];
overlayHandler(snapshotOverlay);
[currentContext restoreGraphicsState];
currentContext = [NSGraphicsContext currentContext];
}
if (!currentContext && overlayHandler) {
// If the current context has been corrupted by the user,
// return nil so we can generate an error later.
return nil;
}
if (logoImage) {
[logoImage drawInRect:logoImageRect];
}
@@ -379,8 +439,8 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
#endif
}
- (void)drawAttributedSnapshot:(mbgl::MapSnapshotter::Attributions)attributions snapshotImage:(MGLImage *)mglImage pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn {
- (void)drawAttributedSnapshot:(mbgl::MapSnapshotter::Attributions)attributions snapshotImage:(MGLImage *)mglImage pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn overlayHandler:(MGLMapSnapshotOverlayHandler)overlayHandler {
// Process image watermark in a work queue
dispatch_queue_t workQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t resultQueue = self.resultQueue;
@@ -394,19 +454,30 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64;
dispatch_async(workQueue, ^{
// Call a class method to ensure we're not accidentally capturing self
MGLImage *compositedImage = [MGLMapSnapshotter drawAttributedSnapshotWorker:attributions snapshotImage:mglImage pointForFn:pointForFn latLngForFn:latLngForFn scale:scale size:size];
MGLImage *compositedImage = [MGLMapSnapshotter drawAttributedSnapshotWorker:attributions snapshotImage:mglImage pointForFn:pointForFn latLngForFn:latLngForFn scale:scale size:size overlayHandler:overlayHandler];
// Dispatch result to origin queue
dispatch_async(resultQueue, ^{
__typeof__(self) strongself = weakself;
if (strongself.completion) {
MGLMapSnapshot* snapshot = [[MGLMapSnapshot alloc] initWithImage:compositedImage
scale:scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
strongself.completion(snapshot, nil);
strongself.completion = nil;
if (!compositedImage) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to generate composited snapshot."};
NSError *error = [NSError errorWithDomain:MGLErrorDomain
code:MGLErrorCodeSnapshotFailed
userInfo:userInfo];
strongself.completion(nil, error);
strongself.completion = nil;
} else {
MGLMapSnapshot* snapshot = [[MGLMapSnapshot alloc] initWithImage:compositedImage
scale:scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
strongself.completion(snapshot, nil);
strongself.completion = nil;
}
}
});
});

View File

@@ -2,6 +2,15 @@
Mapbox welcomes participation and contributions from everyone. Please read [CONTRIBUTING.md](../../CONTRIBUTING.md) to get started.
## master
### Styles and rendering
* Added an `-[MGLMapSnapshotter startWithOverlayHandler:completionHandler:]` method to provide the snapshot's current `CGContext` in order to perform custom drawing on `MGLMapSnapShot` objects. ([#15530](https://github.com/mapbox/mapbox-gl-native/pull/15530))
### Performance improvements
* Newly loaded labels appear faster on the screen. ([#15308](https://github.com/mapbox/mapbox-gl-native/pull/15308))
## 5.4.0
### Styles and rendering

View File

@@ -393,5 +393,62 @@ MGLMapSnapshotter* snapshotterWithCoordinates(CLLocationCoordinate2D coordinates
[self waitForExpectations:@[expectation] timeout:10.0];
}
- (void)testSnapshotWithOverlayHandlerFailure {
if (![self validAccessToken]) {
return;
}
CGSize size = self.mapView.bounds.size;
XCTestExpectation *expectation = [self expectationWithDescription:@"snapshot with overlay fails"];
expectation.expectedFulfillmentCount = 2;
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(30.0, 30.0);
MGLMapSnapshotter *snapshotter = snapshotterWithCoordinates(coord, size);
XCTAssertNotNil(snapshotter);
[snapshotter startWithOverlayHandler:^(MGLMapSnapshotOverlay * _Nullable snapshotOverlay) {
UIGraphicsEndImageContext();
[expectation fulfill];
} completionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) {
XCTAssertNil(snapshot);
XCTAssertNotNil(error);
[expectation fulfill];
}];
[self waitForExpectations:@[expectation] timeout:10.0];
}
- (void)testSnapshotWithOverlayHandlerSuccess {
if (![self validAccessToken]) {
return;
}
CGSize size = self.mapView.bounds.size;
CGRect snapshotRect = CGRectMake(0, 0, size.width, size.height);
XCTestExpectation *expectation = [self expectationWithDescription:@"snapshot with overlay succeeds"];
expectation.expectedFulfillmentCount = 2;
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(30.0, 30.0);
MGLMapSnapshotter *snapshotter = snapshotterWithCoordinates(coord, size);
XCTAssertNotNil(snapshotter);
[snapshotter startWithOverlayHandler:^(MGLMapSnapshotOverlay * _Nullable snapshotOverlay) {
CGContextSetFillColorWithColor(snapshotOverlay.context, [UIColor.greenColor CGColor]);
CGContextSetAlpha(snapshotOverlay.context, 0.2);
CGContextAddRect(snapshotOverlay.context, snapshotRect);
CGContextFillRect(snapshotOverlay.context, snapshotRect);
[expectation fulfill];
} completionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) {
XCTAssertNil(error);
XCTAssertNotNil(snapshot);
[expectation fulfill];
}];
[self waitForExpectations:@[expectation] timeout:10.0];
}
@end

View File

@@ -17,6 +17,7 @@
* Fixed a rendering issue that non-SDF icon would be treated as SDF icon if they are in the same layer. ([#15456](https://github.com/mapbox/mapbox-gl-native/pull/15456))
* Fixed a rendering issue of `collisionBox` when `text-translate` or `icon-translate` is enabled. ([#15467](https://github.com/mapbox/mapbox-gl-native/pull/15467))
* Fixed an issue of integer overflow when converting `tileCoordinates` to `LatLon`, which caused issues such as `queryRenderedFeatures` and `querySourceFeatures` returning incorrect coordinates at zoom levels 20 and higher. ([#15560](https://github.com/mapbox/mapbox-gl-native/pull/15560))
* Added an `-[MGLMapSnapshotter startWithOverlayHandler:completionHandler:]` method to provide the snapshot's current `CGContext` in order to perform custom drawing on `MGLMapSnapShot` objects. ([#15530](https://github.com/mapbox/mapbox-gl-native/pull/15530))
### Styles and rendering