- Do not carry it over everywhere as parameter, it is a shared instance anyway and the lifecycle is pretty much the app lifecycle from the moment we instantiate a map. - Rename to BackgroundScheduler because it is a Scheduler that will do tasks in the background, we don't make assumptions if it is a thread pool or a single thread. - Most importantly, remove the dependency from `core` on `platform`.
631 lines
26 KiB
Plaintext
631 lines
26 KiB
Plaintext
#import "MGLMapSnapshotter.h"
|
|
|
|
#import <mbgl/actor/actor.hpp>
|
|
#import <mbgl/actor/scheduler.hpp>
|
|
#import <mbgl/util/geo.hpp>
|
|
#import <mbgl/map/map_options.hpp>
|
|
#import <mbgl/map/map_snapshotter.hpp>
|
|
#import <mbgl/map/camera.hpp>
|
|
#import <mbgl/storage/resource_options.hpp>
|
|
#import <mbgl/storage/default_file_source.hpp>
|
|
#import <mbgl/util/string.hpp>
|
|
|
|
#import "MGLOfflineStorage_Private.h"
|
|
#import "MGLGeometry_Private.h"
|
|
#import "NSBundle+MGLAdditions.h"
|
|
#import "MGLStyle.h"
|
|
#import "MGLAttributionInfo_Private.h"
|
|
#import "MGLLoggingConfiguration_Private.h"
|
|
#import "MGLRendererConfiguration.h"
|
|
#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR
|
|
#import "MGLMapboxEvents.h"
|
|
#endif
|
|
|
|
#if TARGET_OS_IPHONE
|
|
#import "UIImage+MGLAdditions.h"
|
|
#else
|
|
#import "NSImage+MGLAdditions.h"
|
|
#import <CoreGraphics/CoreGraphics.h>
|
|
#import <QuartzCore/QuartzCore.h>
|
|
#endif
|
|
|
|
const CGPoint MGLLogoImagePosition = CGPointMake(8, 8);
|
|
const CGFloat MGLSnapshotterMinimumPixelSize = 64;
|
|
|
|
@implementation MGLMapSnapshotOptions
|
|
|
|
- (instancetype _Nonnull)initWithStyleURL:(nullable NSURL *)styleURL camera:(MGLMapCamera *)camera size:(CGSize)size
|
|
{
|
|
MGLLogDebug(@"Initializing withStyleURL: %@ camera: %@ size: %@", styleURL, camera, MGLStringFromSize(size));
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
if ( !styleURL)
|
|
{
|
|
styleURL = [MGLStyle streetsStyleURLWithVersion:MGLStyleDefaultVersion];
|
|
}
|
|
_styleURL = styleURL;
|
|
_size = size;
|
|
_camera = camera;
|
|
#if TARGET_OS_IPHONE
|
|
_scale = [UIScreen mainScreen].scale;
|
|
#else
|
|
_scale = [NSScreen mainScreen].backingScaleFactor;
|
|
#endif
|
|
}
|
|
return self;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface MGLMapSnapshot()
|
|
- (instancetype)initWithImage:(nullable MGLImage *)image scale:(CGFloat)scale pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn;
|
|
|
|
@property (nonatomic) CGFloat scale;
|
|
@end
|
|
|
|
@implementation MGLMapSnapshot {
|
|
mbgl::MapSnapshotter::PointForFn _pointForFn;
|
|
mbgl::MapSnapshotter::LatLngForFn _latLngForFn;
|
|
}
|
|
|
|
- (instancetype)initWithImage:(nullable MGLImage *)image scale:(CGFloat)scale pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_pointForFn = std::move(pointForFn);
|
|
_latLngForFn = std::move(latLngForFn);
|
|
_scale = scale;
|
|
_image = image;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#if TARGET_OS_IPHONE
|
|
|
|
- (CGPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate
|
|
{
|
|
mbgl::ScreenCoordinate sc = _pointForFn(MGLLatLngFromLocationCoordinate2D(coordinate));
|
|
return CGPointMake(sc.x, sc.y);
|
|
}
|
|
|
|
- (CLLocationCoordinate2D)coordinateForPoint:(CGPoint)point
|
|
{
|
|
mbgl::LatLng latLng = _latLngForFn(mbgl::ScreenCoordinate(point.x, point.y));
|
|
return MGLLocationCoordinate2DFromLatLng(latLng);
|
|
}
|
|
|
|
#else
|
|
|
|
- (NSPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate
|
|
{
|
|
mbgl::ScreenCoordinate sc = _pointForFn(MGLLatLngFromLocationCoordinate2D(coordinate));
|
|
return NSMakePoint(sc.x, self.image.size.height - sc.y);
|
|
}
|
|
|
|
- (CLLocationCoordinate2D)coordinateForPoint:(NSPoint)point
|
|
{
|
|
auto screenCoord = mbgl::ScreenCoordinate(point.x, self.image.size.height - point.y);
|
|
mbgl::LatLng latLng = _latLngForFn(screenCoord);
|
|
return MGLLocationCoordinate2DFromLatLng(latLng);
|
|
}
|
|
|
|
#endif
|
|
|
|
@end
|
|
|
|
@interface MGLMapSnapshotter()
|
|
@property (nonatomic) BOOL cancelled;
|
|
@property (nonatomic) BOOL terminated;
|
|
@property (nonatomic) dispatch_queue_t resultQueue;
|
|
@property (nonatomic, copy) MGLMapSnapshotCompletionHandler completion;
|
|
+ (void)completeWithErrorCode:(MGLErrorCode)errorCode description:(nonnull NSString*)description onQueue:(dispatch_queue_t)queue completion:(MGLMapSnapshotCompletionHandler)completion;
|
|
@end
|
|
|
|
@implementation MGLMapSnapshotter {
|
|
std::unique_ptr<mbgl::MapSnapshotter> _mbglMapSnapshotter;
|
|
std::unique_ptr<mbgl::Actor<mbgl::MapSnapshotter::Callback>> _snapshotCallback;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
|
|
if (_completion) {
|
|
MGLAssert(_snapshotCallback, @"Snapshot in progress - there should be a valid callback");
|
|
|
|
[MGLMapSnapshotter completeWithErrorCode:MGLErrorCodeSnapshotFailed
|
|
description:@"MGLMapSnapshotter deallocated prior to snapshot completion."
|
|
onQueue:_resultQueue
|
|
completion:_completion];
|
|
}
|
|
}
|
|
|
|
|
|
- (instancetype)init {
|
|
NSAssert(NO, @"Please use -[MGLMapSnapshotter initWithOptions:]");
|
|
[super doesNotRecognizeSelector:_cmd];
|
|
return nil;
|
|
}
|
|
|
|
- (instancetype)initWithOptions:(MGLMapSnapshotOptions *)options
|
|
{
|
|
MGLLogDebug(@"Initializing withOptions: %@", options);
|
|
self = [super init];
|
|
if (self) {
|
|
[self setOptions:options];
|
|
#if TARGET_OS_IPHONE
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil];
|
|
#else
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil];
|
|
#endif
|
|
#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR
|
|
[MGLMapboxEvents pushTurnstileEvent];
|
|
#endif
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)applicationWillTerminate:(NSNotification *)notification
|
|
{
|
|
if (self.completion) {
|
|
[self cancel];
|
|
}
|
|
|
|
_mbglMapSnapshotter.reset();
|
|
_snapshotCallback.reset();
|
|
|
|
self.terminated = YES;
|
|
}
|
|
|
|
- (void)startWithCompletionHandler:(MGLMapSnapshotCompletionHandler)completion
|
|
{
|
|
MGLLogDebug(@"Starting withCompletionHandler: %@", completion);
|
|
[self startWithQueue:dispatch_get_main_queue() completionHandler:completion];
|
|
}
|
|
|
|
- (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshotCompletionHandler)completion
|
|
{
|
|
if (!mbgl::Scheduler::GetCurrent()) {
|
|
[NSException raise:NSInvalidArgumentException
|
|
format:@"startWithQueue:completionHandler: must be called from a thread with an active run loop."];
|
|
}
|
|
|
|
if (self.completion) {
|
|
// Consider replacing this exception with an error passed to the completion block.
|
|
[NSException raise:NSInternalInconsistencyException
|
|
format:@"Already started this snapshotter."];
|
|
}
|
|
|
|
if (self.terminated) {
|
|
[NSException raise:NSInternalInconsistencyException
|
|
format:@"Starting a snapshotter after application termination is not supported."];
|
|
}
|
|
|
|
self.completion = completion;
|
|
self.resultQueue = queue;
|
|
self.cancelled = NO;
|
|
|
|
__weak __typeof__(self) weakSelf = self;
|
|
// mbgl::Scheduler::GetCurrent() scheduler means "run callback on current (ie UI/main) thread"
|
|
// 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) {
|
|
|
|
__typeof__(self) strongSelf = weakSelf;
|
|
// If self had died, _snapshotCallback would have been destroyed and this block would not be executed
|
|
MGLCAssert(strongSelf, @"Snapshot callback executed after being destroyed.");
|
|
|
|
if (!strongSelf.completion)
|
|
return;
|
|
|
|
if (mbglError) {
|
|
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);
|
|
strongSelf.completion = nil;
|
|
});
|
|
} else {
|
|
#if TARGET_OS_IPHONE
|
|
MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image) scale:strongSelf.options.scale];
|
|
#else
|
|
MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image)];
|
|
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->_snapshotCallback = NULL;
|
|
|
|
});
|
|
|
|
// Launches snapshot on background Thread owned by mbglMapSnapshotter
|
|
// _snapshotCallback->self() is an ActorRef: if the callback is destroyed, further messages
|
|
// to the callback are just no-ops
|
|
_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 {
|
|
|
|
NSArray<MGLAttributionInfo *>* attributionInfo = [MGLMapSnapshotter generateAttributionInfos:attributions];
|
|
|
|
#if TARGET_OS_IPHONE
|
|
MGLAttributionInfoStyle attributionInfoStyle = MGLAttributionInfoStyleLong;
|
|
for (NSUInteger styleValue = MGLAttributionInfoStyleLong; styleValue >= MGLAttributionInfoStyleShort; styleValue--) {
|
|
attributionInfoStyle = (MGLAttributionInfoStyle)styleValue;
|
|
CGSize attributionSize = [MGLMapSnapshotter attributionSizeWithLogoStyle:attributionInfoStyle sourceAttributionStyle:attributionInfoStyle attributionInfo:attributionInfo];
|
|
if (attributionSize.width <= mglImage.size.width) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
UIImage *logoImage = [MGLMapSnapshotter logoImageWithStyle:attributionInfoStyle];
|
|
CGSize attributionBackgroundSize = [MGLMapSnapshotter attributionTextSizeWithStyle:attributionInfoStyle attributionInfo:attributionInfo];
|
|
|
|
CGRect logoImageRect = CGRectMake(MGLLogoImagePosition.x, mglImage.size.height - (MGLLogoImagePosition.y + logoImage.size.height), logoImage.size.width, logoImage.size.height);
|
|
CGPoint attributionOrigin = CGPointMake(mglImage.size.width - 10 - attributionBackgroundSize.width,
|
|
logoImageRect.origin.y + (logoImageRect.size.height / 2) - (attributionBackgroundSize.height / 2) + 1);
|
|
if (!logoImage) {
|
|
CGSize defaultLogoSize = [MGLMapSnapshotter mapboxLongStyleLogo].size;
|
|
logoImageRect = CGRectMake(0, mglImage.size.height - (MGLLogoImagePosition.y + defaultLogoSize.height), 0, defaultLogoSize.height);
|
|
attributionOrigin = CGPointMake(10, logoImageRect.origin.y + (logoImageRect.size.height / 2) - (attributionBackgroundSize.height / 2) + 1);
|
|
}
|
|
|
|
CGRect attributionBackgroundFrame = CGRectMake(attributionOrigin.x,
|
|
attributionOrigin.y,
|
|
attributionBackgroundSize.width,
|
|
attributionBackgroundSize.height);
|
|
CGPoint attributionTextPosition = CGPointMake(attributionBackgroundFrame.origin.x + 10,
|
|
attributionBackgroundFrame.origin.y - 1);
|
|
|
|
CGRect cropRect = CGRectMake(attributionBackgroundFrame.origin.x * mglImage.scale,
|
|
attributionBackgroundFrame.origin.y * mglImage.scale,
|
|
attributionBackgroundSize.width * mglImage.scale,
|
|
attributionBackgroundSize.height * mglImage.scale);
|
|
|
|
|
|
UIGraphicsBeginImageContextWithOptions(mglImage.size, NO, scale);
|
|
|
|
[mglImage drawInRect:CGRectMake(0, 0, mglImage.size.width, mglImage.size.height)];
|
|
|
|
[logoImage drawInRect:logoImageRect];
|
|
|
|
UIImage *currentImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
CGImageRef attributionImageRef = CGImageCreateWithImageInRect([currentImage CGImage], cropRect);
|
|
UIImage *attributionImage = [UIImage imageWithCGImage:attributionImageRef];
|
|
CGImageRelease(attributionImageRef);
|
|
|
|
CIImage *ciAttributionImage = [[CIImage alloc] initWithCGImage:attributionImage.CGImage];
|
|
|
|
UIImage *blurredAttributionBackground = [MGLMapSnapshotter blurredAttributionBackground:ciAttributionImage];
|
|
|
|
[blurredAttributionBackground drawInRect:attributionBackgroundFrame];
|
|
|
|
[MGLMapSnapshotter drawAttributionTextWithStyle:attributionInfoStyle origin:attributionTextPosition attributionInfo:attributionInfo];
|
|
|
|
UIImage *compositedImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
|
|
UIGraphicsEndImageContext();
|
|
|
|
return compositedImage;
|
|
|
|
#else
|
|
|
|
NSSize targetSize = NSMakeSize(size.width, size.height);
|
|
NSRect targetFrame = NSMakeRect(0, 0, targetSize.width, targetSize.height);
|
|
|
|
MGLAttributionInfoStyle attributionInfoStyle = MGLAttributionInfoStyleLong;
|
|
for (NSUInteger styleValue = MGLAttributionInfoStyleLong; styleValue >= MGLAttributionInfoStyleShort; styleValue--) {
|
|
attributionInfoStyle = (MGLAttributionInfoStyle)styleValue;
|
|
CGSize attributionSize = [MGLMapSnapshotter attributionSizeWithLogoStyle:attributionInfoStyle sourceAttributionStyle:attributionInfoStyle attributionInfo:attributionInfo];
|
|
if (attributionSize.width <= mglImage.size.width) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
NSImage *logoImage = [MGLMapSnapshotter logoImageWithStyle:attributionInfoStyle];
|
|
CGSize attributionBackgroundSize = [MGLMapSnapshotter attributionTextSizeWithStyle:attributionInfoStyle attributionInfo:attributionInfo];
|
|
NSImage *sourceImage = mglImage;
|
|
|
|
CGRect logoImageRect = CGRectMake(MGLLogoImagePosition.x, MGLLogoImagePosition.y, logoImage.size.width, logoImage.size.height);
|
|
CGPoint attributionOrigin = CGPointMake(targetFrame.size.width - 10 - attributionBackgroundSize.width,
|
|
MGLLogoImagePosition.y + 1);
|
|
if (!logoImage) {
|
|
CGSize defaultLogoSize = [MGLMapSnapshotter mapboxLongStyleLogo].size;
|
|
logoImageRect = CGRectMake(0, MGLLogoImagePosition.y, 0, defaultLogoSize.height);
|
|
attributionOrigin = CGPointMake(10, attributionOrigin.y);
|
|
}
|
|
|
|
CGRect attributionBackgroundFrame = CGRectMake(attributionOrigin.x,
|
|
attributionOrigin.y,
|
|
attributionBackgroundSize.width,
|
|
attributionBackgroundSize.height);
|
|
CGPoint attributionTextPosition = CGPointMake(attributionBackgroundFrame.origin.x + 10,
|
|
logoImageRect.origin.y + (logoImageRect.size.height / 2) - (attributionBackgroundSize.height / 2));
|
|
|
|
|
|
NSImage *compositedImage = nil;
|
|
NSImageRep *sourceImageRep = [sourceImage bestRepresentationForRect:targetFrame
|
|
context:nil
|
|
hints:nil];
|
|
compositedImage = [[NSImage alloc] initWithSize:targetSize];
|
|
|
|
[compositedImage lockFocus];
|
|
|
|
[sourceImageRep drawInRect: targetFrame];
|
|
|
|
if (logoImage) {
|
|
[logoImage drawInRect:logoImageRect];
|
|
}
|
|
|
|
NSBitmapImageRep *attributionBackground = [[NSBitmapImageRep alloc] initWithFocusedViewRect:attributionBackgroundFrame];
|
|
|
|
CIImage *attributionBackgroundImage = [[CIImage alloc] initWithCGImage:[attributionBackground CGImage]];
|
|
|
|
NSImage *blurredAttributionBackground = [MGLMapSnapshotter blurredAttributionBackground:attributionBackgroundImage];
|
|
|
|
[blurredAttributionBackground drawInRect:attributionBackgroundFrame];
|
|
|
|
[MGLMapSnapshotter drawAttributionTextWithStyle:attributionInfoStyle origin:attributionTextPosition attributionInfo:attributionInfo];
|
|
|
|
[compositedImage unlockFocus];
|
|
|
|
return compositedImage;
|
|
#endif
|
|
}
|
|
|
|
- (void)drawAttributedSnapshot:(mbgl::MapSnapshotter::Attributions)attributions snapshotImage:(MGLImage *)mglImage pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn {
|
|
|
|
// 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;
|
|
|
|
// Capture scale and size by value to avoid accessing self from another thread
|
|
CGFloat scale = self.options.scale;
|
|
CGSize size = self.options.size;
|
|
|
|
// pointForFn is a copyable std::function that captures state by value: see MapSnapshotter::Impl::snapshot
|
|
__weak __typeof__(self) weakself = self;
|
|
|
|
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];
|
|
|
|
// 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;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
+ (NSArray<MGLAttributionInfo *>*) generateAttributionInfos:(mbgl::MapSnapshotter::Attributions)attributions {
|
|
NSMutableArray *infos = [NSMutableArray array];
|
|
|
|
#if TARGET_OS_IPHONE
|
|
CGFloat fontSize = [UIFont smallSystemFontSize];
|
|
UIColor *attributeFontColor = [UIColor blackColor];
|
|
#else
|
|
CGFloat fontSize = [NSFont systemFontSizeForControlSize:NSMiniControlSize];
|
|
NSColor *attributeFontColor = [NSColor blackColor];
|
|
#endif
|
|
for (auto attribution = attributions.begin(); attribution != attributions.end(); ++attribution) {
|
|
NSString *attributionHTMLString = @(attribution->c_str());
|
|
NSArray *tileSetInfos = [MGLAttributionInfo attributionInfosFromHTMLString:attributionHTMLString
|
|
fontSize:fontSize
|
|
linkColor:attributeFontColor];
|
|
[infos growArrayByAddingAttributionInfosFromArray:tileSetInfos];
|
|
}
|
|
return infos;
|
|
}
|
|
|
|
+ (void)drawAttributionTextWithStyle:(MGLAttributionInfoStyle)attributionInfoStyle origin:(CGPoint)origin attributionInfo:(NSArray<MGLAttributionInfo *>*)attributionInfo
|
|
{
|
|
for (MGLAttributionInfo *info in attributionInfo) {
|
|
if (info.isFeedbackLink) {
|
|
continue;
|
|
}
|
|
NSAttributedString *attribution = [info titleWithStyle:attributionInfoStyle];
|
|
[attribution drawAtPoint:origin];
|
|
|
|
origin.x += [attribution size].width + 10;
|
|
}
|
|
}
|
|
|
|
+ (MGLImage *)blurredAttributionBackground:(CIImage *)backgroundImage
|
|
{
|
|
CGAffineTransform transform = CGAffineTransformIdentity;
|
|
CIFilter *clamp = [CIFilter filterWithName:@"CIAffineClamp"];
|
|
[clamp setValue:backgroundImage forKey:kCIInputImageKey];
|
|
[clamp setValue:[NSValue valueWithBytes:&transform objCType:@encode(CGAffineTransform)] forKey:@"inputTransform"];
|
|
|
|
CIFilter *attributionBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
|
|
[attributionBlurFilter setValue:[clamp outputImage] forKey:kCIInputImageKey];
|
|
[attributionBlurFilter setValue:@10 forKey:kCIInputRadiusKey];
|
|
|
|
CIFilter *attributionColorFilter = [CIFilter filterWithName:@"CIColorControls"];
|
|
[attributionColorFilter setValue:[attributionBlurFilter outputImage] forKey:kCIInputImageKey];
|
|
[attributionColorFilter setValue:@(0.1) forKey:kCIInputBrightnessKey];
|
|
|
|
CIImage *blurredImage = attributionColorFilter.outputImage;
|
|
|
|
CIContext *ctx = [CIContext contextWithOptions:nil];
|
|
CGImageRef cgimg = [ctx createCGImage:blurredImage fromRect:[backgroundImage extent]];
|
|
MGLImage *image;
|
|
|
|
#if TARGET_OS_IPHONE
|
|
image = [UIImage imageWithCGImage:cgimg];
|
|
#else
|
|
image = [[NSImage alloc] initWithCGImage:cgimg size:[backgroundImage extent].size];
|
|
#endif
|
|
|
|
CGImageRelease(cgimg);
|
|
return image;
|
|
}
|
|
|
|
+ (MGLImage *)logoImageWithStyle:(MGLAttributionInfoStyle)style
|
|
{
|
|
MGLImage *logoImage;
|
|
switch (style) {
|
|
case MGLAttributionInfoStyleLong:
|
|
logoImage = [MGLMapSnapshotter mapboxLongStyleLogo];
|
|
break;
|
|
case MGLAttributionInfoStyleMedium:
|
|
#if TARGET_OS_IPHONE
|
|
logoImage = [UIImage imageNamed:@"mapbox_helmet" inBundle:[NSBundle mgl_frameworkBundle] compatibleWithTraitCollection:nil];
|
|
#else
|
|
logoImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mgl_frameworkBundle] pathForResource:@"mapbox_helmet" ofType:@"pdf"]];
|
|
#endif
|
|
break;
|
|
case MGLAttributionInfoStyleShort:
|
|
logoImage = nil;
|
|
break;
|
|
}
|
|
return logoImage;
|
|
}
|
|
|
|
+ (MGLImage *)mapboxLongStyleLogo
|
|
{
|
|
MGLImage *logoImage;
|
|
#if TARGET_OS_IPHONE
|
|
logoImage =[UIImage imageNamed:@"mapbox" inBundle:[NSBundle mgl_frameworkBundle] compatibleWithTraitCollection:nil];
|
|
#else
|
|
logoImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mgl_frameworkBundle] pathForResource:@"mapbox" ofType:@"pdf"]];
|
|
#endif
|
|
return logoImage;
|
|
}
|
|
|
|
+ (CGSize)attributionSizeWithLogoStyle:(MGLAttributionInfoStyle)logoStyle sourceAttributionStyle:(MGLAttributionInfoStyle)attributionStyle attributionInfo:(NSArray<MGLAttributionInfo *>*)attributionInfo
|
|
{
|
|
MGLImage *logoImage = [self logoImageWithStyle:logoStyle];
|
|
|
|
CGSize attributionBackgroundSize = [MGLMapSnapshotter attributionTextSizeWithStyle:attributionStyle attributionInfo:attributionInfo];
|
|
|
|
CGSize attributionSize = CGSizeZero;
|
|
|
|
if (logoImage) {
|
|
attributionSize.width = MGLLogoImagePosition.x + logoImage.size.width + 10;
|
|
}
|
|
attributionSize.width = attributionSize.width + 10 + attributionBackgroundSize.width + 10;
|
|
attributionSize.height = MAX(logoImage.size.height, attributionBackgroundSize.height);
|
|
|
|
return attributionSize;
|
|
}
|
|
|
|
+ (CGSize)attributionTextSizeWithStyle:(MGLAttributionInfoStyle)attributionStyle attributionInfo:(NSArray<MGLAttributionInfo *>*)attributionInfo
|
|
{
|
|
CGSize attributionBackgroundSize = CGSizeMake(10, 0);
|
|
for (MGLAttributionInfo *info in attributionInfo) {
|
|
if (info.isFeedbackLink) {
|
|
continue;
|
|
}
|
|
CGSize attributionSize = [info titleWithStyle:attributionStyle].size;
|
|
attributionBackgroundSize.width += attributionSize.width + 10;
|
|
attributionBackgroundSize.height = MAX(attributionSize.height, attributionBackgroundSize.height);
|
|
}
|
|
|
|
return attributionBackgroundSize;
|
|
}
|
|
|
|
- (void)cancel
|
|
{
|
|
MGLLogInfo(@"Cancelling snapshotter.");
|
|
self.cancelled = YES;
|
|
|
|
if (_snapshotCallback) {
|
|
[MGLMapSnapshotter completeWithErrorCode:MGLErrorCodeSnapshotFailed
|
|
description:[NSString stringWithFormat:@"MGLMapSnapshotter cancelled from %s", __PRETTY_FUNCTION__]
|
|
onQueue:self.resultQueue
|
|
completion:self.completion];
|
|
self.completion = nil;
|
|
}
|
|
|
|
_snapshotCallback.reset();
|
|
_mbglMapSnapshotter.reset();
|
|
}
|
|
|
|
+ (void)completeWithErrorCode:(MGLErrorCode)errorCode description:(nonnull NSString*)description onQueue:(dispatch_queue_t)queue completion:(MGLMapSnapshotCompletionHandler)completion {
|
|
// The snapshot hasn't completed, so we should alert the caller
|
|
if (completion && queue) {
|
|
dispatch_async(queue, ^{
|
|
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description};
|
|
NSError *error = [NSError errorWithDomain:MGLErrorDomain
|
|
code:errorCode
|
|
userInfo:userInfo];
|
|
completion(NULL, error);
|
|
});
|
|
}
|
|
}
|
|
|
|
- (void)setOptions:(MGLMapSnapshotOptions *)options
|
|
{
|
|
if (_terminated) {
|
|
[NSException raise:NSInternalInconsistencyException
|
|
format:@"Calling MGLMapSnapshotter.options after application termination is not supported."];
|
|
}
|
|
|
|
MGLLogDebug(@"Setting options: %@", options);
|
|
|
|
if (_completion) {
|
|
[self cancel];
|
|
}
|
|
|
|
_cancelled = NO;
|
|
_options = options;
|
|
|
|
auto mbglFileSource = [[MGLOfflineStorage sharedOfflineStorage] mbglFileSource];
|
|
|
|
std::string styleURL = std::string([options.styleURL.absoluteString UTF8String]);
|
|
std::pair<bool, std::string> style = std::make_pair(false, styleURL);
|
|
|
|
// 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, MGLSnapshotterMinimumPixelSize)),
|
|
static_cast<uint32_t>(MAX(options.size.height, MGLSnapshotterMinimumPixelSize))
|
|
};
|
|
|
|
float pixelRatio = MAX(options.scale, 1);
|
|
|
|
// Camera options
|
|
mbgl::CameraOptions cameraOptions;
|
|
if (CLLocationCoordinate2DIsValid(options.camera.centerCoordinate)) {
|
|
cameraOptions.center = MGLLatLngFromLocationCoordinate2D(options.camera.centerCoordinate);
|
|
}
|
|
cameraOptions.bearing = MAX(0, options.camera.heading);
|
|
cameraOptions.zoom = MAX(0, options.zoomLevel);
|
|
cameraOptions.pitch = MAX(0, options.camera.pitch);
|
|
|
|
// Region
|
|
mbgl::optional<mbgl::LatLngBounds> coordinateBounds;
|
|
if (!MGLCoordinateBoundsIsEmpty(options.coordinateBounds)) {
|
|
coordinateBounds = MGLLatLngBoundsFromCoordinateBounds(options.coordinateBounds);
|
|
}
|
|
|
|
// App-global configuration
|
|
MGLRendererConfiguration* config = [MGLRendererConfiguration currentConfiguration];
|
|
|
|
mbgl::ResourceOptions resourceOptions;
|
|
resourceOptions.withCachePath([[MGLOfflineStorage sharedOfflineStorage] mbglCachePath])
|
|
.withAssetPath([NSBundle mainBundle].resourceURL.path.UTF8String);
|
|
|
|
// Create the snapshotter
|
|
_mbglMapSnapshotter = std::make_unique<mbgl::MapSnapshotter>(
|
|
style, size, pixelRatio, cameraOptions, coordinateBounds, config.cacheDir, config.localFontFamilyName, resourceOptions);
|
|
}
|
|
|
|
@end
|