diff --git a/platform/darwin/src/MGLMapSnapshotter.h b/platform/darwin/src/MGLMapSnapshotter.h new file mode 100644 index 00000000000..a2a4f1b3311 --- /dev/null +++ b/platform/darwin/src/MGLMapSnapshotter.h @@ -0,0 +1,105 @@ +#import +#import "MGLTypes.h" +#import "MGLGeometry.h" +#import "MGLMapCamera.h" + +NS_ASSUME_NONNULL_BEGIN + +MGL_EXPORT +/** + The options to use when creating images with the `MGLMapsnapshotter`. + */ +@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 + */ +- (instancetype)initWithStyleURL:(NSURL*)styleURL camera:(MGLMapCamera*)camera size:(CGSize)size; + +#pragma mark - Configuring the map + +/** + The style URL for these options. + */ +@property (nonatomic, readonly) NSURL* styleURL; + +/** + The zoom. Default is 0. + */ +@property (nonatomic) double zoom; + +/** + The `MGLMapcamera` options to use. + */ +@property (nonatomic) MGLMapCamera* camera; + +/** + A region to capture. Overrides the center coordinate + in the mapCamera options if set + */ +@property (nonatomic) MGLCoordinateBounds region; + +#pragma mark - Configuring the image + +/** + The size of the output image. Minimum is 64x64 + */ +@property (nonatomic, readonly) CGSize size; + +/** + The scale of the output image. Defaults to the main screen scale. + Minimum is 1. + */ +@property (nonatomic) CGFloat scale; + +@end + +/** + 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 image 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); + +/** + A utility object for capturing map-based images. + */ +MGL_EXPORT +@interface MGLMapSnapshotter : NSObject + +- (instancetype)initWithOptions:(MGLMapSnapshotOptions*)options; + +/** + Starts the snapshot creation and executes the specified block with the result. + + @param completionHandler The block to handle the result in. + */ +- (void)startWithCompletionHandler:(MGLMapSnapshotCompletionHandler)completionHandler; + +/** + Starts the snapshot creation and executes the specified block with the result on the specified queue. + + @param queue The queue to handle the result on. + @param completionHandler The block to handle the result in. + */ +- (void)startWithQueue:(dispatch_queue_t)queue completionHandler:(MGLMapSnapshotCompletionHandler)completionHandler; + +/** + Cancels the snapshot creation request, if any. + */ +- (void)cancel; + +/** + Indicates whether as snapshot is currently being generated. + */ +@property (nonatomic, readonly, getter=isLoading) BOOL loading; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/darwin/src/MGLMapSnapshotter.mm b/platform/darwin/src/MGLMapSnapshotter.mm new file mode 100644 index 00000000000..c81fd39c4aa --- /dev/null +++ b/platform/darwin/src/MGLMapSnapshotter.mm @@ -0,0 +1,163 @@ +#import "MGLMapSnapshotter.h" + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import "MGLOfflineStorage_Private.h" +#import "MGLGeometry_Private.h" +#import "NSBundle+MGLAdditions.h" + +#if TARGET_OS_IPHONE +#import "UIImage+MGLAdditions.h" +#else +#import "NSImage+MGLAdditions.h" +#endif + +@implementation MGLMapSnapshotOptions + +- (instancetype _Nonnull)initWithStyleURL:(NSURL* _Nonnull)styleURL camera:(MGLMapCamera*)camera size:(CGSize) size; +{ + self = [super init]; + if (self) { + _styleURL = styleURL; + _size = size; + _camera = camera; +#if TARGET_OS_IPHONE + _scale = [UIScreen mainScreen].scale; +#else + _scale = [NSScreen mainScreen].backingScaleFactor; +#endif + + } + return self; +} + +@end + +@implementation MGLMapSnapshotter { + + std::shared_ptr mbglThreadPool; + std::unique_ptr mbglMapSnapshotter; + std::unique_ptr> snapshotCallback; +} + +- (instancetype)initWithOptions:(MGLMapSnapshotOptions*)options; +{ + self = [super init]; + if (self) { + _loading = false; + + mbgl::DefaultFileSource *mbglFileSource = [MGLOfflineStorage sharedOfflineStorage].mbglFileSource; + mbglThreadPool = mbgl::sharedThreadPool(); + + std::string styleURL = std::string([options.styleURL.absoluteString UTF8String]); + + // Size; taking into account the minimum texture size for OpenGL ES + mbgl::Size size = { + static_cast(MAX(options.size.width, 64)), + static_cast(MAX(options.size.height, 64)) + }; + + float pixelRatio = MAX(options.scale, 1); + + // Camera options + mbgl::CameraOptions cameraOptions; + if (CLLocationCoordinate2DIsValid(options.camera.centerCoordinate)) { + cameraOptions.center = MGLLatLngFromLocationCoordinate2D(options.camera.centerCoordinate); + } + cameraOptions.angle = MAX(0, options.camera.heading) * mbgl::util::DEG2RAD; + cameraOptions.zoom = MAX(0, options.zoom); + cameraOptions.pitch = MAX(0, options.camera.pitch); + + // Region + mbgl::optional region; + if (!MGLCoordinateBoundsIsEmpty(options.region)) { + region = MGLLatLngBoundsFromCoordinateBounds(options.region); + } + + // Create the snapshotter + mbglMapSnapshotter = std::make_unique(*mbglFileSource, *mbglThreadPool, styleURL, size, pixelRatio, cameraOptions, region); + } + return self; +} + +- (void)startWithCompletionHandler:(MGLMapSnapshotCompletionHandler)completion; +{ + [self startWithQueue:dispatch_get_main_queue() completionHandler:completion]; +} + +- (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; + } + + _loading = true; + + dispatch_async(queue, ^{ + 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]; + + // Dispatch result to origin queue + dispatch_async(queue, ^{ + completion(nil, error); + }); + } else { + MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image)]; + + // 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); + + [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)]; + UIImage *compositedImage = UIGraphicsGetImageFromCurrentImageContext(); + + UIGraphicsEndImageContext(); +#else + NSImage *logoImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mgl_frameworkBundle] pathForResource:@"mapbox" ofType:@"pdf"]]; + NSImage *compositedImage = mglImage; + + [compositedImage lockFocus]; + [logoImage drawInRect:CGRectMake(8, 8, logoImage.size.width,logoImage.size.height)]; + [compositedImage unlockFocus]; +#endif + + // Dispatch result to origin queue + dispatch_async(queue, ^{ + completion(compositedImage, nil); + }); + }); + } + }); + mbglMapSnapshotter->snapshot(snapshotCallback->self()); + }); +} + +- (void)cancel; +{ + snapshotCallback.reset(); + mbglMapSnapshotter.reset(); +} + +@end diff --git a/platform/ios/app/MBXSnapshotsViewController.h b/platform/ios/app/MBXSnapshotsViewController.h new file mode 100644 index 00000000000..f791602e98a --- /dev/null +++ b/platform/ios/app/MBXSnapshotsViewController.h @@ -0,0 +1,5 @@ +#import + +@interface MBXSnapshotsViewController : UIViewController + +@end diff --git a/platform/ios/app/MBXSnapshotsViewController.m b/platform/ios/app/MBXSnapshotsViewController.m new file mode 100644 index 00000000000..d26479f0854 --- /dev/null +++ b/platform/ios/app/MBXSnapshotsViewController.m @@ -0,0 +1,66 @@ +#import "MBXSnapshotsViewController.h" + +#import + +@interface MBXSnapshotsViewController () + +// Top row +@property (weak, nonatomic) IBOutlet UIImageView *snapshotImageViewTL; +@property (weak, nonatomic) IBOutlet UIImageView *snapshotImageViewTM; +@property (weak, nonatomic) IBOutlet UIImageView *snapshotImageViewTR; + +// Bottom row +@property (weak, nonatomic) IBOutlet UIImageView *snapshotImageViewBL; +@property (weak, nonatomic) IBOutlet UIImageView *snapshotImageViewBM; +@property (weak, nonatomic) IBOutlet UIImageView *snapshotImageViewBR; + +@end + +@implementation MBXSnapshotsViewController { + // Top row + MGLMapSnapshotter* snapshotterTL; + MGLMapSnapshotter* snapshotterTM; + MGLMapSnapshotter* snapshotterTR; + + // Bottom row + MGLMapSnapshotter* snapshotterBL; + MGLMapSnapshotter* snapshotterBM; + MGLMapSnapshotter* snapshotterBR; +} + +- (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)]; + + 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)]; +} + +- (MGLMapSnapshotter*) startSnapshotterForImageView:(UIImageView*) imageView coordinates:(CLLocationCoordinate2D) coordinates { + // Create snapshot options + 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; + + // Create and start the snapshotter + MGLMapSnapshotter* snapshotter = [[MGLMapSnapshotter alloc] initWithOptions:options]; + [snapshotter startWithCompletionHandler: ^(UIImage *image, NSError *error) { + if (error) { + NSLog(@"Could not load snapshot: %@", [error localizedDescription]); + } else { + imageView.image = image; + } + }]; + + return snapshotter; +} + + +@end diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 740e52b33bf..e7825fac9dd 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -84,6 +84,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { MBXSettingsMiscellaneousScrollView, MBXSettingsMiscellaneousToggleTwoMaps, MBXSettingsMiscellaneousCountryLabels, + MBXSettingsMiscellaneousShowSnapshots, MBXSettingsMiscellaneousPrintLogFile, MBXSettingsMiscellaneousDeleteLogFile, }; @@ -358,6 +359,7 @@ - (void)dismissSettings:(__unused id)sender @"Embedded Map View", [NSString stringWithFormat:@"%@ Second Map", ([self.view viewWithTag:2] == nil ? @"Show" : @"Hide")], [NSString stringWithFormat:@"Show Labels in %@", (_usingLocaleBasedCountryLabels ? @"Default Language" : [[NSLocale currentLocale] displayNameForKey:NSLocaleIdentifier value:[self bestLanguageForUser]])], + @"Show Snapshots" ]]; if (self.debugLoggingEnabled) @@ -639,6 +641,11 @@ - (void)performActionForSettingAtIndexPath:(NSIndexPath *)indexPath constant:0]]; } break; + case MBXSettingsMiscellaneousShowSnapshots: + { + [self performSegueWithIdentifier:@"ShowSnapshots" sender:nil]; + break; + } default: NSAssert(NO, @"All miscellaneous setting rows should be implemented"); break; diff --git a/platform/ios/app/Main.storyboard b/platform/ios/app/Main.storyboard index 40198146abf..c7bcd6e0f0e 100644 --- a/platform/ios/app/Main.storyboard +++ b/platform/ios/app/Main.storyboard @@ -1,11 +1,11 @@ - + - + @@ -96,6 +96,7 @@ + @@ -352,6 +353,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/ios/config.cmake b/platform/ios/config.cmake index 7ea1dddef76..4e873e73ea6 100644 --- a/platform/ios/config.cmake +++ b/platform/ios/config.cmake @@ -57,6 +57,10 @@ macro(mbgl_platform_core) PRIVATE platform/default/mbgl/gl/headless_display.cpp PRIVATE platform/default/mbgl/gl/headless_display.hpp + # Snapshotting + PRIVATE platform/default/mbgl/map/map_snapshotter.cpp + PRIVATE platform/default/mbgl/map/map_snapshotter.hpp + # Thread pool PRIVATE platform/default/mbgl/util/shared_thread_pool.cpp PRIVATE platform/default/mbgl/util/shared_thread_pool.hpp diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 31605f4ea19..060fb45d812 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -153,7 +153,6 @@ 400533011DB0862B0069F638 /* NSArray+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 400532FF1DB0862B0069F638 /* NSArray+MGLAdditions.h */; }; 400533021DB0862B0069F638 /* NSArray+MGLAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 400533001DB0862B0069F638 /* NSArray+MGLAdditions.mm */; }; 400533031DB086490069F638 /* NSArray+MGLAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 400533001DB0862B0069F638 /* NSArray+MGLAdditions.mm */; }; - 4018B1C71CDC287F00F666AF /* MGLAnnotationView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.mm */; }; 4018B1C81CDC287F00F666AF /* MGLAnnotationView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.mm */; }; 4018B1C91CDC288A00F666AF /* MGLAnnotationView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */; }; 4018B1CA1CDC288E00F666AF /* MGLAnnotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -217,6 +216,12 @@ 7E016D861D9E890300A29A21 /* MGLPolygon+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E016D831D9E890300A29A21 /* MGLPolygon+MGLAdditions.m */; }; 7E016D871D9E890300A29A21 /* MGLPolygon+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E016D831D9E890300A29A21 /* MGLPolygon+MGLAdditions.m */; }; 920A3E5D1E6F995200C16EFC /* MGLSourceQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 920A3E5C1E6F995200C16EFC /* MGLSourceQueryTests.m */; }; + 927FBCFC1F4DAA8300F8BF1F /* MBXSnapshotsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 927FBCFB1F4DAA8300F8BF1F /* MBXSnapshotsViewController.m */; }; + 927FBCFF1F4DB05500F8BF1F /* MGLMapSnapshotter.h in Headers */ = {isa = PBXBuildFile; fileRef = 927FBCFD1F4DB05500F8BF1F /* MGLMapSnapshotter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 927FBD001F4DB05500F8BF1F /* MGLMapSnapshotter.h in Headers */ = {isa = PBXBuildFile; fileRef = 927FBCFD1F4DB05500F8BF1F /* MGLMapSnapshotter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 927FBD011F4DB05500F8BF1F /* MGLMapSnapshotter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 927FBCFE1F4DB05500F8BF1F /* MGLMapSnapshotter.mm */; }; + 927FBD021F4DB05500F8BF1F /* MGLMapSnapshotter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 927FBCFE1F4DB05500F8BF1F /* MGLMapSnapshotter.mm */; }; + 929EFFAB1F56DCD4003A77D5 /* MGLAnnotationView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.mm */; }; 92F2C3ED1F0E3C3A00268EC0 /* MGLRendererFrontend.h in Headers */ = {isa = PBXBuildFile; fileRef = 92F2C3EC1F0E3C3A00268EC0 /* MGLRendererFrontend.h */; }; 960D0C361ECF5AAF008E151F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 960D0C351ECF5AAF008E151F /* Images.xcassets */; }; 960D0C371ECF5AAF008E151F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 960D0C351ECF5AAF008E151F /* Images.xcassets */; }; @@ -689,6 +694,10 @@ 7E016D821D9E890300A29A21 /* MGLPolygon+MGLAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MGLPolygon+MGLAdditions.h"; sourceTree = ""; }; 7E016D831D9E890300A29A21 /* MGLPolygon+MGLAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MGLPolygon+MGLAdditions.m"; sourceTree = ""; }; 920A3E5C1E6F995200C16EFC /* MGLSourceQueryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLSourceQueryTests.m; path = ../../darwin/test/MGLSourceQueryTests.m; sourceTree = ""; }; + 927FBCFA1F4DAA8300F8BF1F /* MBXSnapshotsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBXSnapshotsViewController.h; sourceTree = ""; }; + 927FBCFB1F4DAA8300F8BF1F /* MBXSnapshotsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBXSnapshotsViewController.m; sourceTree = ""; }; + 927FBCFD1F4DB05500F8BF1F /* MGLMapSnapshotter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapSnapshotter.h; sourceTree = ""; }; + 927FBCFE1F4DB05500F8BF1F /* MGLMapSnapshotter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapSnapshotter.mm; sourceTree = ""; }; 92F2C3EC1F0E3C3A00268EC0 /* MGLRendererFrontend.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLRendererFrontend.h; sourceTree = ""; }; 960D0C351ECF5AAF008E151F /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 9620BB361E69FE1700705A1D /* MGLSDKUpdateChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLSDKUpdateChecker.h; sourceTree = ""; }; @@ -1224,6 +1233,8 @@ 354B839B1D2E9B48005D9406 /* MBXUserLocationAnnotationView.m */, DA1DC9681CB6C6B7006E619F /* MBXOfflinePacksTableViewController.h */, DA1DC9691CB6C6B7006E619F /* MBXOfflinePacksTableViewController.m */, + 927FBCFA1F4DAA8300F8BF1F /* MBXSnapshotsViewController.h */, + 927FBCFB1F4DAA8300F8BF1F /* MBXSnapshotsViewController.m */, DA1DC9531CB6C1C2006E619F /* MBXViewController.h */, DA1DC99A1CB6E064006E619F /* MBXViewController.m */, 632281DD1E6F855900D75A5D /* MBXEmbeddedMapViewController.h */, @@ -1347,6 +1358,8 @@ 558DE79F1E5615E400C7916D /* MGLFoundation.mm */, DA8847E21CBAFA5100AB86E3 /* MGLMapCamera.h */, DA8848031CBAFA6200AB86E3 /* MGLMapCamera.mm */, + 927FBCFD1F4DB05500F8BF1F /* MGLMapSnapshotter.h */, + 927FBCFE1F4DB05500F8BF1F /* MGLMapSnapshotter.mm */, DD0902A41DB18F1B00C5BDCE /* MGLNetworkConfiguration.h */, DD0902A21DB18DE700C5BDCE /* MGLNetworkConfiguration.m */, 92F2C3EC1F0E3C3A00268EC0 /* MGLRendererFrontend.h */, @@ -1724,6 +1737,7 @@ 3566C7661D4A77BA008152BC /* MGLShapeSource.h in Headers */, 35CE61821D4165D9004F2359 /* UIColor+MGLAdditions.h in Headers */, 35B82BF81D6C5F8400B1B721 /* NSPredicate+MGLAdditions.h in Headers */, + 927FBCFF1F4DB05500F8BF1F /* MGLMapSnapshotter.h in Headers */, DA35A29E1CC9E94C00E826B2 /* MGLCoordinateFormatter.h in Headers */, DAF0D8181DFE6B2800B28378 /* MGLAttributionInfo_Private.h in Headers */, DAAF722B1DA903C700312FA4 /* MGLStyleValue.h in Headers */, @@ -1800,6 +1814,7 @@ 35E0CFE71D3E501500188327 /* MGLStyle_Private.h in Headers */, DABFB86D1CBE9A0F00D62B32 /* MGLAnnotationImage.h in Headers */, DABFB8721CBE9A0F00D62B32 /* MGLUserLocation.h in Headers */, + 927FBD001F4DB05500F8BF1F /* MGLMapSnapshotter.h in Headers */, 3566C7721D4A9198008152BC /* MGLSource_Private.h in Headers */, 353933FF1D3FB7DD003F57D7 /* MGLSymbolStyleLayer.h in Headers */, DAAF722E1DA903C700312FA4 /* MGLStyleValue_Private.h in Headers */, @@ -1808,7 +1823,6 @@ DABFB8621CBE99E500D62B32 /* MGLOfflinePack.h in Headers */, DAD1656D1CF41981001FF4B9 /* MGLFeature.h in Headers */, DA17BE311CC4BDAA00402C41 /* MGLMapView_Private.h in Headers */, - 92F2C3EE1F0E3DC600268EC0 /* MGLRendererFrontend.h in Headers */, DABFB86C1CBE99E500D62B32 /* MGLTypes.h in Headers */, DABFB8691CBE99E500D62B32 /* MGLShape.h in Headers */, 9620BB391E69FE1700705A1D /* MGLSDKUpdateChecker.h in Headers */, @@ -2170,6 +2184,7 @@ DA1DC9991CB6E054006E619F /* MBXAppDelegate.m in Sources */, DA1DC96B1CB6C6B7006E619F /* MBXOfflinePacksTableViewController.m in Sources */, DA1DC96A1CB6C6B7006E619F /* MBXCustomCalloutView.m in Sources */, + 927FBCFC1F4DAA8300F8BF1F /* MBXSnapshotsViewController.m in Sources */, DA1DC99B1CB6E064006E619F /* MBXViewController.m in Sources */, 40FDA76B1CCAAA6800442548 /* MBXAnnotationView.m in Sources */, 632281DF1E6F855900D75A5D /* MBXEmbeddedMapViewController.m in Sources */, @@ -2239,6 +2254,7 @@ 40EDA1C11CFE0E0500D9EA68 /* MGLAnnotationContainerView.m in Sources */, DA8848541CBAFB9800AB86E3 /* MGLCompactCalloutView.m in Sources */, DA8848251CBAFA6200AB86E3 /* MGLPointAnnotation.mm in Sources */, + 929EFFAB1F56DCD4003A77D5 /* MGLAnnotationView.mm in Sources */, 35136D3C1D42272500C20EFD /* MGLCircleStyleLayer.mm in Sources */, DD9BE4F81EB263C50079A3AF /* UIViewController+MGLAdditions.m in Sources */, 350098DE1D484E60004B2AF0 /* NSValue+MGLStyleAttributeAdditions.mm in Sources */, @@ -2250,6 +2266,7 @@ DA00FC901D5EEB0D009AABC8 /* MGLAttributionInfo.mm in Sources */, DA88482D1CBAFA6200AB86E3 /* NSBundle+MGLAdditions.m in Sources */, DA88485B1CBAFB9800AB86E3 /* MGLUserLocation.m in Sources */, + 927FBD011F4DB05500F8BF1F /* MGLMapSnapshotter.mm in Sources */, 350098BD1D480108004B2AF0 /* MGLVectorSource.mm in Sources */, 3566C76E1D4A8DFA008152BC /* MGLRasterSource.mm in Sources */, DA88488C1CBB037E00AB86E3 /* SMCalloutView.m in Sources */, @@ -2280,7 +2297,6 @@ 3510FFF21D6D9D8C00F413B2 /* NSExpression+MGLAdditions.mm in Sources */, DA88481F1CBAFA6200AB86E3 /* MGLMultiPoint.mm in Sources */, DA88482B1CBAFA6200AB86E3 /* MGLTypes.m in Sources */, - 4018B1C71CDC287F00F666AF /* MGLAnnotationView.mm in Sources */, FA68F14D1E9D656600F9F6C2 /* MGLFillExtrusionStyleLayer.mm in Sources */, 404C26E41D89B877000AA13D /* MGLTileSource.mm in Sources */, 355AE0011E9281DA00F3939D /* MGLScaleBar.mm in Sources */, @@ -2334,6 +2350,7 @@ DA00FC911D5EEB0D009AABC8 /* MGLAttributionInfo.mm in Sources */, DAA4E4201CBB730400178DFB /* MGLOfflinePack.mm in Sources */, DAA4E4331CBB730400178DFB /* MGLUserLocation.m in Sources */, + 927FBD021F4DB05500F8BF1F /* MGLMapSnapshotter.mm in Sources */, 350098BE1D480108004B2AF0 /* MGLVectorSource.mm in Sources */, 3566C76F1D4A8DFA008152BC /* MGLRasterSource.mm in Sources */, DAA4E4351CBB730400178DFB /* SMCalloutView.m in Sources */, diff --git a/platform/ios/src/Mapbox.h b/platform/ios/src/Mapbox.h index abe16cc3eee..9b2c472cf64 100644 --- a/platform/ios/src/Mapbox.h +++ b/platform/ios/src/Mapbox.h @@ -60,3 +60,4 @@ FOUNDATION_EXPORT MGL_EXPORT const unsigned char MapboxVersionString[]; #import "NSValue+MGLAdditions.h" #import "MGLStyleValue.h" #import "MGLAttributionInfo.h" +#import "MGLMapSnapshotter.h" diff --git a/platform/ios/src/UIImage+MGLAdditions.h b/platform/ios/src/UIImage+MGLAdditions.h index 6e15e07cb55..3c179d63246 100644 --- a/platform/ios/src/UIImage+MGLAdditions.h +++ b/platform/ios/src/UIImage+MGLAdditions.h @@ -8,6 +8,8 @@ NS_ASSUME_NONNULL_BEGIN - (nullable instancetype)initWithMGLStyleImage:(const mbgl::style::Image *)styleImage; +- (nullable instancetype)initWithMGLPremultipliedImage:(const mbgl::PremultipliedImage&&)mbglImage; + - (std::unique_ptr)mgl_styleImageWithIdentifier:(NSString *)identifier; - (mbgl::PremultipliedImage)mgl_premultipliedImage; diff --git a/platform/ios/src/UIImage+MGLAdditions.mm b/platform/ios/src/UIImage+MGLAdditions.mm index 5e28d181901..7cf1ed9bccc 100644 --- a/platform/ios/src/UIImage+MGLAdditions.mm +++ b/platform/ios/src/UIImage+MGLAdditions.mm @@ -22,6 +22,19 @@ - (nullable instancetype)initWithMGLStyleImage:(const mbgl::style::Image *)style return self; } +- (nullable instancetype)initWithMGLPremultipliedImage:(const mbgl::PremultipliedImage&&)mbglImage +{ + CGImageRef image = CGImageFromMGLPremultipliedImage(mbglImage.clone()); + if (!image) { + return nil; + } + + self = [self initWithCGImage:image scale:1.0 orientation:UIImageOrientationUp]; + + CGImageRelease(image); + return self; +} + - (std::unique_ptr)mgl_styleImageWithIdentifier:(NSString *)identifier { BOOL isTemplate = self.renderingMode == UIImageRenderingModeAlwaysTemplate; return std::make_unique([identifier UTF8String], diff --git a/platform/macos/app/Base.lproj/MainMenu.xib b/platform/macos/app/Base.lproj/MainMenu.xib index 9a8cf05c166..9a53ba9d4b8 100644 --- a/platform/macos/app/Base.lproj/MainMenu.xib +++ b/platform/macos/app/Base.lproj/MainMenu.xib @@ -1,7 +1,7 @@ - + - + @@ -128,6 +128,12 @@ + + + + + + @@ -654,7 +660,7 @@ CA - + @@ -730,7 +736,7 @@ CA - + @@ -739,7 +745,7 @@ CA - + diff --git a/platform/macos/app/Base.lproj/MapDocument.xib b/platform/macos/app/Base.lproj/MapDocument.xib index d95f21b2e95..0394f38533a 100644 --- a/platform/macos/app/Base.lproj/MapDocument.xib +++ b/platform/macos/app/Base.lproj/MapDocument.xib @@ -1,7 +1,7 @@ - + - + @@ -48,7 +48,7 @@ - + diff --git a/platform/macos/app/MapDocument.m b/platform/macos/app/MapDocument.m index d6855d3ff27..406ff0ca9d7 100644 --- a/platform/macos/app/MapDocument.m +++ b/platform/macos/app/MapDocument.m @@ -3,6 +3,7 @@ #import "AppDelegate.h" #import "LimeGreenStyleLayer.h" #import "DroppedPinAnnotation.h" +#import "MGLMapsnapshotter.h" #import "MGLStyle+MBXAdditions.h" #import "MGLVectorSource+MGLAdditions.h" @@ -73,6 +74,9 @@ @implementation MapDocument { BOOL _isTouringWorld; BOOL _isShowingPolygonAndPolylineAnnotations; BOOL _isShowingAnimatedAnnotation; + + // Snapshotter + MGLMapSnapshotter* snapshotter; } #pragma mark Lifecycle @@ -153,6 +157,67 @@ - (NSURL *)shareURL { camera.heading, camera.pitch]]; } +#pragma mark File methods + +- (IBAction)takeSnapshot:(id)sender { + MGLMapCamera *camera = self.mapView.camera; + + MGLMapSnapshotOptions* options = [[MGLMapSnapshotOptions alloc] initWithStyleURL:self.mapView.styleURL camera:camera size:self.mapView.bounds.size]; + options.zoom = self.mapView.zoomLevel; + + // Create and start the snapshotter + snapshotter = [[MGLMapSnapshotter alloc] initWithOptions:options]; + [snapshotter startWithCompletionHandler: ^(NSImage *image, NSError *error) { + if (error) { + NSLog(@"Could not load snapshot: %@", [error localizedDescription]); + } else { + NSWindow* window = [[[self windowControllers] objectAtIndex:0] window]; + + NSString* newName = [[@"snapshot" stringByDeletingPathExtension] stringByAppendingPathExtension:@"png"]; + + // Set the default name for the file and show the panel. + NSSavePanel* panel = [NSSavePanel savePanel]; + [panel setNameFieldStringValue:newName]; + [panel beginSheetModalForWindow:window completionHandler:^(NSInteger result){ + if (result == NSFileHandlingPanelOKButton) { + // Write the contents in the new format. + NSURL* fileURL = [panel URL]; + + NSBitmapImageRep *bitmapRep = nil; + for (NSImageRep *imageRep in [image representations]) { + if ([imageRep isKindOfClass:[NSBitmapImageRep class]]){ + bitmapRep = (NSBitmapImageRep *)imageRep; + break; // stop on first bitmap rep we find + } + } + + if (!bitmapRep) { + bitmapRep = [NSBitmapImageRep imageRepWithData:[image TIFFRepresentation]]; + + } + + NSString *extension = [[fileURL pathExtension] lowercaseString]; + NSBitmapImageFileType fileType; + if ([extension isEqualToString:@"png"]) { + fileType = NSPNGFileType; + } else if ([extension isEqualToString:@"gif"]) { + fileType = NSGIFFileType; + } else if ([extension isEqualToString:@"jpg"] || [extension isEqualToString:@"jpeg"]) { + fileType = NSJPEGFileType; + } else { + fileType = NSTIFFFileType; + } + + NSData *imageData = [bitmapRep representationUsingType:fileType properties:@{}]; + [imageData writeToURL:fileURL atomically:NO]; + } + }]; + + } + snapshotter = nil; + }]; +} + #pragma mark View methods - (IBAction)showStyle:(id)sender { @@ -963,6 +1028,9 @@ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { if (menuItem.action == @selector(giveFeedback:)) { return YES; } + if (menuItem.action == @selector(takeSnapshot:)) { + return !(snapshotter && [snapshotter isLoading]); + } return NO; } diff --git a/platform/macos/config.cmake b/platform/macos/config.cmake index 1d471e1a8b2..bb2cc9ac1c7 100644 --- a/platform/macos/config.cmake +++ b/platform/macos/config.cmake @@ -53,6 +53,10 @@ macro(mbgl_platform_core) PRIVATE platform/default/mbgl/gl/headless_display.hpp PRIVATE platform/darwin/src/headless_display_cgl.cpp + # Snapshotting + PRIVATE platform/default/mbgl/map/map_snapshotter.cpp + PRIVATE platform/default/mbgl/map/map_snapshotter.hpp + # Thread pool PRIVATE platform/default/mbgl/util/shared_thread_pool.cpp PRIVATE platform/default/mbgl/util/shared_thread_pool.hpp diff --git a/platform/macos/macos.xcodeproj/project.pbxproj b/platform/macos/macos.xcodeproj/project.pbxproj index 34b08c00359..40bbb07e8b2 100644 --- a/platform/macos/macos.xcodeproj/project.pbxproj +++ b/platform/macos/macos.xcodeproj/project.pbxproj @@ -81,6 +81,8 @@ 558DE7A61E56161C00C7916D /* MGLFoundation_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 558DE7A41E56161C00C7916D /* MGLFoundation_Private.h */; }; 558DE7A71E56161C00C7916D /* MGLFoundation.mm in Sources */ = {isa = PBXBuildFile; fileRef = 558DE7A51E56161C00C7916D /* MGLFoundation.mm */; }; 55E2AD111E5B0A6900E8C587 /* MGLOfflineStorageTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 55E2AD101E5B0A6900E8C587 /* MGLOfflineStorageTests.mm */; }; + 92092EF01F5EB10E00AF5130 /* MGLMapSnapshotter.h in Headers */ = {isa = PBXBuildFile; fileRef = 92092EEE1F5EB10E00AF5130 /* MGLMapSnapshotter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 92092EF11F5EB10E00AF5130 /* MGLMapSnapshotter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 92092EEF1F5EB10E00AF5130 /* MGLMapSnapshotter.mm */; }; 920A3E591E6F859D00C16EFC /* MGLSourceQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 920A3E581E6F859D00C16EFC /* MGLSourceQueryTests.m */; }; 92F2C3EB1F0E3A1900268EC0 /* MGLRendererFrontend.h in Headers */ = {isa = PBXBuildFile; fileRef = 92F2C3EA1F0E3A1900268EC0 /* MGLRendererFrontend.h */; }; 96E027311E57C9A7004B8E66 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 96E027331E57C9A7004B8E66 /* Localizable.strings */; }; @@ -354,6 +356,8 @@ 55D9B4B01D005D3900C1CCE2 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 55E2AD101E5B0A6900E8C587 /* MGLOfflineStorageTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLOfflineStorageTests.mm; path = ../../darwin/test/MGLOfflineStorageTests.mm; sourceTree = ""; }; 55FE0E8D1D100A0900FD240B /* config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = config.xcconfig; path = ../../build/macos/config.xcconfig; sourceTree = ""; }; + 92092EEE1F5EB10E00AF5130 /* MGLMapSnapshotter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapSnapshotter.h; sourceTree = ""; }; + 92092EEF1F5EB10E00AF5130 /* MGLMapSnapshotter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapSnapshotter.mm; sourceTree = ""; }; 920A3E581E6F859D00C16EFC /* MGLSourceQueryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLSourceQueryTests.m; sourceTree = ""; }; 92F2C3EA1F0E3A1900268EC0 /* MGLRendererFrontend.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLRendererFrontend.h; sourceTree = ""; }; 966091701E5BBFF700A9A03B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; @@ -1048,6 +1052,8 @@ 558DE7A51E56161C00C7916D /* MGLFoundation.mm */, DAE6C34D1CC31E0400DB3429 /* MGLMapCamera.h */, DAE6C36E1CC31E2A00DB3429 /* MGLMapCamera.mm */, + 92092EEE1F5EB10E00AF5130 /* MGLMapSnapshotter.h */, + 92092EEF1F5EB10E00AF5130 /* MGLMapSnapshotter.mm */, DD0902B01DB1AC6400C5BDCE /* MGLNetworkConfiguration.h */, DD0902AF1DB1AC6400C5BDCE /* MGLNetworkConfiguration.m */, 92F2C3EA1F0E3A1900268EC0 /* MGLRendererFrontend.h */, @@ -1111,6 +1117,7 @@ DA8F259C1D51CB000010E6B5 /* MGLStyleValue_Private.h in Headers */, DAE6C35B1CC31E0400DB3429 /* MGLAnnotation.h in Headers */, DAE6C3B61CC31EF300DB3429 /* MGLMapView_Private.h in Headers */, + 92092EF01F5EB10E00AF5130 /* MGLMapSnapshotter.h in Headers */, 3527428D1D4C24AB00A1ECE6 /* MGLCircleStyleLayer.h in Headers */, DA00FC8A1D5EEAC3009AABC8 /* MGLAttributionInfo.h in Headers */, DAE6C3B21CC31EF300DB3429 /* MGLAttributionButton.h in Headers */, @@ -1403,6 +1410,7 @@ buildActionMask = 2147483647; files = ( 07A019EF1ED665CD00ACD43E /* MGLImageSource.mm in Sources */, + 92092EF11F5EB10E00AF5130 /* MGLMapSnapshotter.mm in Sources */, 40ABDB561DB0022100372083 /* NSImage+MGLAdditions.mm in Sources */, DAE6C3901CC31E2A00DB3429 /* MGLPointAnnotation.mm in Sources */, DAE6C3981CC31E2A00DB3429 /* NSBundle+MGLAdditions.m in Sources */, diff --git a/platform/macos/src/Mapbox.h b/platform/macos/src/Mapbox.h index e4ad258b6ea..a082a4771e6 100644 --- a/platform/macos/src/Mapbox.h +++ b/platform/macos/src/Mapbox.h @@ -56,3 +56,4 @@ FOUNDATION_EXPORT MGL_EXPORT const unsigned char MapboxVersionString[]; #import "NSValue+MGLAdditions.h" #import "MGLStyleValue.h" #import "MGLAttributionInfo.h" +#import "MGLMapSnapshotter.h"