Files
MapLibreParticleLab/platform/darwin/src/MLNMapSnapshotter.mm
mwilsnd 20d9e1028c Platform flattening overhaul (#1393)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bart Louwers <bart@emeel.net>
2023-07-26 17:50:34 +02:00

771 lines
32 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import "MLNMapSnapshotter.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/util/client_options.hpp>
#import <mbgl/util/string.hpp>
#import "MLNOfflineStorage_Private.h"
#import "MLNGeometry_Private.h"
#import "MLNStyle_Private.h"
#import "MLNAttributionInfo_Private.h"
#import "MLNLoggingConfiguration_Private.h"
#import "MLNRendererConfiguration.h"
#import "MLNMapSnapshotter_Private.h"
#import "MLNSettings_Private.h"
#if TARGET_OS_IPHONE
#import "UIImage+MLNAdditions.h"
#else
#import "NSImage+MLNAdditions.h"
#import <CoreGraphics/CoreGraphics.h>
#import <CoreImage/CIContext.h>
#import <CoreImage/CIFilter.h>
#import <QuartzCore/QuartzCore.h>
#endif
#import "NSBundle+MLNAdditions.h"
const CGPoint MLNLogoImagePosition = CGPointMake(8, 8);
const CGFloat MLNSnapshotterMinimumPixelSize = 64;
MLNImage *MLNAttributedSnapshot(mbgl::MapSnapshotter::Attributions attributions, MLNImage *mglImage, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MLNMapSnapshotOptions *options, MLNMapSnapshotOverlayHandler overlayHandler);
MLNMapSnapshot *MLNSnapshotWithDecoratedImage(MLNImage *mglImage, MLNMapSnapshotOptions *options, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MLNMapSnapshotOverlayHandler overlayHandler, NSError * _Nullable *outError);
NSArray<MLNAttributionInfo *> *MLNAttributionInfosFromAttributions(mbgl::MapSnapshotter::Attributions attributions);
class MLNMapSnapshotterDelegateHost: public mbgl::MapSnapshotterObserver {
public:
MLNMapSnapshotterDelegateHost(MLNMapSnapshotter *snapshotter_) : snapshotter(snapshotter_) {}
void onDidFailLoadingStyle(const std::string& errorMessage) {
MLNMapSnapshotter *strongSnapshotter = snapshotter;
if ([strongSnapshotter.delegate respondsToSelector:@selector(mapSnapshotterDidFail:withError:)]) {
NSString *description = @(errorMessage.c_str());
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedStringWithDefaultValue(@"SNAPSHOT_LOAD_STYLE_FAILED_DESC", nil, nil, @"The snapshot failed because the style can’t be loaded.", @"User-friendly error description"),
NSLocalizedFailureReasonErrorKey: description,
};
NSError *error = [NSError errorWithDomain:MLNErrorDomain code:MLNErrorCodeLoadStyleFailed userInfo:userInfo];
[strongSnapshotter.delegate mapSnapshotterDidFail:snapshotter withError:error];
}
}
void onDidFinishLoadingStyle() {
MLNMapSnapshotter *strongSnapshotter = snapshotter;
if ([strongSnapshotter.delegate respondsToSelector:@selector(mapSnapshotter:didFinishLoadingStyle:)]) {
[strongSnapshotter.delegate mapSnapshotter:snapshotter didFinishLoadingStyle:snapshotter.style];
}
}
void onStyleImageMissing(const std::string& imageName) {
MLNMapSnapshotter *strongSnapshotter = snapshotter;
if ([strongSnapshotter.delegate respondsToSelector:@selector(mapSnapshotter:didFailLoadingImageNamed:)]) {
[strongSnapshotter.delegate mapSnapshotter:snapshotter didFailLoadingImageNamed:@(imageName.c_str())];
}
}
private:
__weak MLNMapSnapshotter *snapshotter;
};
@interface MLNMapSnapshotOverlay() <MLNMapSnapshotProtocol>
@property (nonatomic, assign) CGFloat scale;
- (instancetype)initWithContext:(CGContextRef)context scale:(CGFloat)scale pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn;
@end
@implementation MLNMapSnapshotOverlay {
mbgl::MapSnapshotter::PointForFn _pointForFn;
mbgl::MapSnapshotter::LatLngForFn _latLngForFn;
}
- (instancetype) initWithContext:(CGContextRef)context scale:(CGFloat)scale pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn {
self = [super init];
if (self) {
_context = context;
_scale = scale;
_pointForFn = pointForFn;
_latLngForFn = latLngForFn;
}
return self;
}
#if TARGET_OS_IPHONE
- (CGPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate
{
mbgl::ScreenCoordinate sc = _pointForFn(MLNLatLngFromLocationCoordinate2D(coordinate));
return CGPointMake(sc.x, sc.y);
}
- (CLLocationCoordinate2D)coordinateForPoint:(CGPoint)point
{
mbgl::LatLng latLng = _latLngForFn(mbgl::ScreenCoordinate(point.x, point.y));
return MLNLocationCoordinate2DFromLatLng(latLng);
}
#else
- (NSPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate
{
mbgl::ScreenCoordinate sc = _pointForFn(MLNLatLngFromLocationCoordinate2D(coordinate));
CGFloat height = ((CGFloat)CGBitmapContextGetHeight(self.context))/self.scale;
return NSMakePoint(sc.x, height - sc.y);
}
- (CLLocationCoordinate2D)coordinateForPoint:(NSPoint)point
{
CGFloat height = ((CGFloat)CGBitmapContextGetHeight(self.context))/self.scale;
auto screenCoord = mbgl::ScreenCoordinate(point.x, height - point.y);
mbgl::LatLng latLng = _latLngForFn(screenCoord);
return MLNLocationCoordinate2DFromLatLng(latLng);
}
#endif
@end
@interface MLNMapSnapshotOptions ()
/**
:nodoc:
Whether the Mapbox wordmark is displayed.
@note The Mapbox terms of service, which governs the use of Mapbox-hosted
vector tiles and styles,
<a href="https://docs.mapbox.com/help/how-mapbox-works/attribution/">requires</a> most Mapbox
customers to display the Mapbox wordmark. If this applies to you, do not
hide the wordmark or change its contents.
*/
@property (nonatomic, readwrite) BOOL showsLogo;
@end
@implementation MLNMapSnapshotOptions
- (instancetype _Nonnull)initWithStyleURL:(nullable NSURL *)styleURL camera:(MLNMapCamera *)camera size:(CGSize)size
{
MLNLogDebug(@"Initializing withStyleURL: %@ camera: %@ size: %@", styleURL, camera, MLNStringFromSize(size));
self = [super init];
if (self)
{
if ( !styleURL)
{
styleURL = [MLNStyle defaultStyleURL];
}
_styleURL = styleURL;
_size = size;
_camera = camera;
_showsLogo = YES;
#if TARGET_OS_IPHONE
_scale = [UIScreen mainScreen].scale;
#else
_scale = [NSScreen mainScreen].backingScaleFactor;
#endif
}
return self;
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
__typeof__(self) copy = [[[self class] alloc] initWithStyleURL:_styleURL camera:_camera size:_size];
copy.zoomLevel = _zoomLevel;
copy.coordinateBounds = _coordinateBounds;
copy.scale = _scale;
copy.showsLogo = _showsLogo;
return copy;
}
@end
@interface MLNMapSnapshot() <MLNMapSnapshotProtocol>
- (instancetype)initWithImage:(nullable MLNImage *)image scale:(CGFloat)scale pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn;
@property (nonatomic) CGFloat scale;
@end
@implementation MLNMapSnapshot {
mbgl::MapSnapshotter::PointForFn _pointForFn;
mbgl::MapSnapshotter::LatLngForFn _latLngForFn;
}
- (instancetype)initWithImage:(nullable MLNImage *)image scale:(CGFloat)scale pointForFn:(mbgl::MapSnapshotter::PointForFn)pointForFn latLngForFn:(mbgl::MapSnapshotter::LatLngForFn)latLngForFn
{
self = [super init];
if (self) {
_pointForFn = pointForFn;
_latLngForFn = latLngForFn;
_scale = scale;
_image = image;
}
return self;
}
#if TARGET_OS_IPHONE
- (CGPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate
{
mbgl::ScreenCoordinate sc = _pointForFn(MLNLatLngFromLocationCoordinate2D(coordinate));
return CGPointMake(sc.x, sc.y);
}
- (CLLocationCoordinate2D)coordinateForPoint:(CGPoint)point
{
mbgl::LatLng latLng = _latLngForFn(mbgl::ScreenCoordinate(point.x, point.y));
return MLNLocationCoordinate2DFromLatLng(latLng);
}
#else
- (NSPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate
{
mbgl::ScreenCoordinate sc = _pointForFn(MLNLatLngFromLocationCoordinate2D(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 MLNLocationCoordinate2DFromLatLng(latLng);
}
#endif
@end
@interface MLNMapSnapshotter()
@property (nonatomic) BOOL loading;
@property (nonatomic) BOOL terminated;
@end
@implementation MLNMapSnapshotter {
std::unique_ptr<mbgl::MapSnapshotter> _mbglMapSnapshotter;
std::unique_ptr<MLNMapSnapshotterDelegateHost> _delegateHost;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self cancel];
}
- (instancetype)init {
NSAssert(NO, @"Please use -[MLNMapSnapshotter initWithOptions:]");
[super doesNotRecognizeSelector:_cmd];
return nil;
}
- (instancetype)initWithOptions:(MLNMapSnapshotOptions *)options
{
MLNLogDebug(@"Initializing withOptions: %@", options);
self = [super init];
if (self) {
self.options = 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
}
return self;
}
- (void)applicationWillTerminate:(NSNotification *)notification
{
[self cancel];
self.terminated = YES;
}
- (void)startWithCompletionHandler:(MLNMapSnapshotCompletionHandler)completion
{
MLNLogDebug(@"Starting withCompletionHandler: %@", completion);
[self startWithQueue:dispatch_get_main_queue() completionHandler:completion];
}
- (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MLNMapSnapshotCompletionHandler)completionHandler {
[self startWithQueue:queue overlayHandler:nil completionHandler:completionHandler];
}
- (void)startWithOverlayHandler:(MLNMapSnapshotOverlayHandler)overlayHandler completionHandler:(MLNMapSnapshotCompletionHandler)completion {
[self startWithQueue:dispatch_get_main_queue() overlayHandler:overlayHandler completionHandler:completion];
}
- (void)startWithQueue:(dispatch_queue_t)queue overlayHandler:(MLNMapSnapshotOverlayHandler)overlayHandler completionHandler:(MLNMapSnapshotCompletionHandler)completion
{
if (!completion) {
return;
}
// Ensure that offline storage has been initialized on the main thread, as MLNMapView and MLNOfflineStorage do when used directly.
// https://github.com/mapbox/mapbox-gl-native-ios/issues/227
if ([NSThread.currentThread isMainThread]) {
(void)[MLNOfflineStorage sharedOfflineStorage];
} else {
[NSException raise:NSInvalidArgumentException
format:@"-[MLNMapSnapshotter startWithQueue:completionHandler:] must be called from the main thread, not %@.", NSThread.currentThread];
}
if (self.loading) {
// Consider replacing this exception with an error passed to the completion block.
[NSException raise:NSInternalInconsistencyException
format:@"Already started this snapshotter."];
}
self.loading = YES;
if (self.terminated) {
[NSException raise:NSInternalInconsistencyException
format:@"Starting a snapshotter after application termination is not supported."];
}
MLNMapSnapshotOptions *options = [self.options copy];
[self configureWithOptions:options];
MLNLogDebug(@"Starting with options: %@", self.options);
// Temporarily capture the snapshotter until the completion handler finishes executing, to keep standalone local usage of the snapshotter from becoming a no-op.
// POSTCONDITION: Only refer to this variable in the final result queue.
// POSTCONDITION: It is important to nil out this variable at some point in the future to avoid a leak. In cases where the completion handler gets called, the variable should be nilled out explicitly. If -cancel is called, mbgl releases the snapshot block below, causing the only remaining references to the snapshotter to go out of scope.
__block MLNMapSnapshotter *strongSelf = self;
_mbglMapSnapshotter->snapshot(^(std::exception_ptr mbglError, mbgl::PremultipliedImage image, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn) {
if (mbglError) {
NSString *description = @(mbgl::util::toString(mbglError).c_str());
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description};
NSError *error = [NSError errorWithDomain:MLNErrorDomain code:MLNErrorCodeSnapshotFailed userInfo:userInfo];
// Dispatch to result queue
dispatch_async(queue, ^{
strongSelf.loading = NO;
completion(nil, error);
strongSelf = nil;
});
} else {
#if TARGET_OS_IPHONE
MLNImage *mglImage = [[MLNImage alloc] initWithMLNPremultipliedImage:std::move(image) scale:options.scale];
#else
MLNImage *mglImage = [[MLNImage alloc] initWithMLNPremultipliedImage:std::move(image)];
mglImage.size = NSMakeSize(mglImage.size.width / options.scale,
mglImage.size.height / options.scale);
#endif
// Process image watermark in a work queue
dispatch_queue_t workQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(workQueue, ^{
// Call a function that cannot accidentally capture self.
NSError *error;
MLNMapSnapshot *snapshot = MLNSnapshotWithDecoratedImage(mglImage, options, attributions, pointForFn, latLngForFn, overlayHandler, &error);
// Dispatch result to result queue
dispatch_async(queue, ^{
strongSelf.loading = NO;
completion(snapshot, error);
strongSelf = nil;
});
});
}
});
}
MLNImage *MLNAttributedSnapshot(mbgl::MapSnapshotter::Attributions attributions, MLNImage *mglImage, MLNMapSnapshotOptions *options, void (^overlayHandler)()) {
NSArray<MLNAttributionInfo *> *attributionInfo = MLNAttributionInfosFromAttributions(attributions);
#if TARGET_OS_IPHONE
MLNAttributionInfoStyle attributionInfoStyle = MLNAttributionInfoStyleLong;
for (NSUInteger styleValue = MLNAttributionInfoStyleLong; styleValue >= MLNAttributionInfoStyleShort; styleValue--) {
attributionInfoStyle = (MLNAttributionInfoStyle)styleValue;
CGSize attributionSize = [MLNMapSnapshotter attributionSizeWithLogoStyle:attributionInfoStyle sourceAttributionStyle:attributionInfoStyle attributionInfo:attributionInfo];
if (attributionSize.width <= mglImage.size.width) {
break;
}
}
UIImage *logoImage = [MLNMapSnapshotter logoImageWithStyle:attributionInfoStyle];
CGSize attributionBackgroundSize = [MLNMapSnapshotter attributionTextSizeWithStyle:attributionInfoStyle attributionInfo:attributionInfo];
CGRect logoImageRect = CGRectMake(MLNLogoImagePosition.x, mglImage.size.height - (MLNLogoImagePosition.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 = [MLNMapSnapshotter mapboxLongStyleLogo].size;
logoImageRect = CGRectMake(0, mglImage.size.height - (MLNLogoImagePosition.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, options.scale);
[mglImage drawInRect:CGRectMake(0, 0, mglImage.size.width, mglImage.size.height)];
overlayHandler();
CGContextRef currentContext = UIGraphicsGetCurrentContext();
if (!currentContext) {
// If the current context has been corrupted by the user,
// return nil so we can generate an error later.
return nil;
}
if (options.showsLogo) {
[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 = [MLNMapSnapshotter blurredAttributionBackground:ciAttributionImage];
[blurredAttributionBackground drawInRect:attributionBackgroundFrame];
[MLNMapSnapshotter drawAttributionTextWithStyle:attributionInfoStyle origin:attributionTextPosition attributionInfo:attributionInfo];
UIImage *compositedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return compositedImage;
#else
NSRect targetFrame = { .origin = NSZeroPoint, .size = options.size };
MLNAttributionInfoStyle attributionInfoStyle = MLNAttributionInfoStyleLong;
for (NSUInteger styleValue = MLNAttributionInfoStyleLong; styleValue >= MLNAttributionInfoStyleShort; styleValue--) {
attributionInfoStyle = (MLNAttributionInfoStyle)styleValue;
CGSize attributionSize = [MLNMapSnapshotter attributionSizeWithLogoStyle:attributionInfoStyle sourceAttributionStyle:attributionInfoStyle attributionInfo:attributionInfo];
if (attributionSize.width <= mglImage.size.width) {
break;
}
}
NSImage *logoImage = [MLNMapSnapshotter logoImageWithStyle:attributionInfoStyle];
CGSize attributionBackgroundSize = [MLNMapSnapshotter attributionTextSizeWithStyle:attributionInfoStyle attributionInfo:attributionInfo];
NSImage *sourceImage = mglImage;
CGRect logoImageRect = CGRectMake(MLNLogoImagePosition.x, MLNLogoImagePosition.y, logoImage.size.width, logoImage.size.height);
CGPoint attributionOrigin = CGPointMake(targetFrame.size.width - 10 - attributionBackgroundSize.width,
MLNLogoImagePosition.y + 1);
if (!logoImage) {
CGSize defaultLogoSize = [MLNMapSnapshotter mapboxLongStyleLogo].size;
logoImageRect = CGRectMake(0, MLNLogoImagePosition.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:targetFrame.size];
[compositedImage lockFocus];
[sourceImageRep drawInRect: targetFrame];
overlayHandler();
NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
if (!currentContext) {
// 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];
}
NSBitmapImageRep *attributionBackground = [[NSBitmapImageRep alloc] initWithFocusedViewRect:attributionBackgroundFrame];
CIImage *attributionBackgroundImage = [[CIImage alloc] initWithCGImage:[attributionBackground CGImage]];
NSImage *blurredAttributionBackground = [MLNMapSnapshotter blurredAttributionBackground:attributionBackgroundImage];
[blurredAttributionBackground drawInRect:attributionBackgroundFrame];
[MLNMapSnapshotter drawAttributionTextWithStyle:attributionInfoStyle origin:attributionTextPosition attributionInfo:attributionInfo];
[compositedImage unlockFocus];
return compositedImage;
#endif
}
MLNMapSnapshot *MLNSnapshotWithDecoratedImage(MLNImage *mglImage, MLNMapSnapshotOptions *options, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MLNMapSnapshotOverlayHandler overlayHandler, NSError * _Nullable *outError) {
MLNImage *compositedImage = MLNAttributedSnapshot(attributions, mglImage, options, ^{
if (!overlayHandler) {
return;
}
#if TARGET_OS_IPHONE
CGContextRef context = UIGraphicsGetCurrentContext();
if (!context) {
return;
}
MLNMapSnapshotOverlay *snapshotOverlay = [[MLNMapSnapshotOverlay alloc] initWithContext:context
scale:options.scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
CGContextSaveGState(context);
overlayHandler(snapshotOverlay);
CGContextRestoreGState(context);
#else
NSGraphicsContext *context = [NSGraphicsContext currentContext];
if (!context) {
return;
}
MLNMapSnapshotOverlay *snapshotOverlay = [[MLNMapSnapshotOverlay alloc] initWithContext:context.CGContext
scale:options.scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
[context saveGraphicsState];
overlayHandler(snapshotOverlay);
[context restoreGraphicsState];
#endif
});
if (compositedImage) {
return [[MLNMapSnapshot alloc] initWithImage:compositedImage
scale:options.scale
pointForFn:pointForFn
latLngForFn:latLngForFn];
} else {
if (outError) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to generate composited snapshot."};
*outError = [NSError errorWithDomain:MLNErrorDomain
code:MLNErrorCodeSnapshotFailed
userInfo:userInfo];
}
return nil;
}
}
NSArray<MLNAttributionInfo *> *MLNAttributionInfosFromAttributions(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:NSControlSizeMini];
NSColor *attributeFontColor = [NSColor blackColor];
#endif
for (auto attribution : attributions) {
NSString *attributionHTMLString = @(attribution.c_str());
NSArray *tileSetInfos = [MLNAttributionInfo attributionInfosFromHTMLString:attributionHTMLString
fontSize:fontSize
linkColor:attributeFontColor];
[infos growArrayByAddingAttributionInfosFromArray:tileSetInfos];
}
return infos;
}
+ (void)drawAttributionTextWithStyle:(MLNAttributionInfoStyle)attributionInfoStyle origin:(CGPoint)origin attributionInfo:(NSArray<MLNAttributionInfo *>*)attributionInfo
{
for (MLNAttributionInfo *info in attributionInfo) {
if (info.isFeedbackLink) {
continue;
}
NSAttributedString *attribution = [info titleWithStyle:attributionInfoStyle];
[attribution drawAtPoint:origin];
origin.x += [attribution size].width + 10;
}
}
+ (MLNImage *)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]];
MLNImage *image;
#if TARGET_OS_IPHONE
image = [UIImage imageWithCGImage:cgimg];
#else
image = [[NSImage alloc] initWithCGImage:cgimg size:[backgroundImage extent].size];
#endif
CGImageRelease(cgimg);
return image;
}
+ (MLNImage *)logoImageWithStyle:(MLNAttributionInfoStyle)style
{
MLNImage *logoImage;
switch (style) {
case MLNAttributionInfoStyleLong:
logoImage = [MLNMapSnapshotter mapboxLongStyleLogo];
break;
case MLNAttributionInfoStyleMedium:
#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 MLNAttributionInfoStyleShort:
logoImage = nil;
break;
}
return logoImage;
}
+ (MLNImage *)mapboxLongStyleLogo
{
MLNImage *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:(MLNAttributionInfoStyle)logoStyle sourceAttributionStyle:(MLNAttributionInfoStyle)attributionStyle attributionInfo:(NSArray<MLNAttributionInfo *>*)attributionInfo
{
MLNImage *logoImage = [self logoImageWithStyle:logoStyle];
CGSize attributionBackgroundSize = [MLNMapSnapshotter attributionTextSizeWithStyle:attributionStyle attributionInfo:attributionInfo];
CGSize attributionSize = CGSizeZero;
if (logoImage) {
attributionSize.width = MLNLogoImagePosition.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:(MLNAttributionInfoStyle)attributionStyle attributionInfo:(NSArray<MLNAttributionInfo *>*)attributionInfo
{
CGSize attributionBackgroundSize = CGSizeMake(10, 0);
for (MLNAttributionInfo *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
{
MLNLogInfo(@"Cancelling snapshotter.");
if (_mbglMapSnapshotter) {
_mbglMapSnapshotter->cancel();
}
_mbglMapSnapshotter.reset();
_delegateHost.reset();
}
- (void)configureWithOptions:(MLNMapSnapshotOptions *)options {
auto mbglFileSource = [[MLNOfflineStorage sharedOfflineStorage] mbglFileSource];
// Size; taking into account the minimum texture size for OpenGL ES
// For non retina screens the ratio is 1:1 MLNSnapshotterMinimumPixelSize
mbgl::Size size = {
static_cast<uint32_t>(MAX(options.size.width, MLNSnapshotterMinimumPixelSize)),
static_cast<uint32_t>(MAX(options.size.height, MLNSnapshotterMinimumPixelSize))
};
float pixelRatio = MAX(options.scale, 1);
// App-global configuration
MLNRendererConfiguration *config = [MLNRendererConfiguration currentConfiguration];
auto tileServerOptions = [[MLNSettings sharedSettings] tileServerOptionsInternal];
mbgl::ResourceOptions resourceOptions;
resourceOptions
.withTileServerOptions(*tileServerOptions)
.withCachePath(MLNOfflineStorage.sharedOfflineStorage.databasePath.UTF8String)
.withAssetPath(NSBundle.mainBundle.resourceURL.path.UTF8String);
mbgl::ClientOptions clientOptions;
auto apiKey = [[MLNSettings sharedSettings] apiKey];
if (apiKey) {
resourceOptions.withApiKey([apiKey UTF8String]);
}
// Create the snapshotter
auto localFontFamilyName = config.localFontFamilyName ? std::string(config.localFontFamilyName.UTF8String) : nullptr;
_delegateHost = std::make_unique<MLNMapSnapshotterDelegateHost>(self);
_mbglMapSnapshotter = std::make_unique<mbgl::MapSnapshotter>(
size, pixelRatio, resourceOptions, clientOptions, *_delegateHost, localFontFamilyName);
_mbglMapSnapshotter->setStyleURL(std::string(options.styleURL.absoluteString.UTF8String));
// Camera options
mbgl::CameraOptions cameraOptions;
if (CLLocationCoordinate2DIsValid(options.camera.centerCoordinate)) {
cameraOptions.center = MLNLatLngFromLocationCoordinate2D(options.camera.centerCoordinate);
}
if (options.camera.heading >= 0) {
cameraOptions.bearing = MAX(0, options.camera.heading);
}
if (options.zoomLevel >= 0) {
cameraOptions.zoom = MAX(0, options.zoomLevel);
}
if (options.camera.pitch >= 0) {
cameraOptions.pitch = MAX(0, options.camera.pitch);
}
if (cameraOptions != mbgl::CameraOptions()) {
_mbglMapSnapshotter->setCameraOptions(cameraOptions);
}
// Region
if (!MLNCoordinateBoundsIsEmpty(options.coordinateBounds)) {
_mbglMapSnapshotter->setRegion(MLNLatLngBoundsFromCoordinateBounds(options.coordinateBounds));
}
}
- (MLNStyle *)style {
if (!_mbglMapSnapshotter) {
return nil;
}
return [[MLNStyle alloc] initWithRawStyle:&_mbglMapSnapshotter->getStyle() stylable:self];
}
@end