diff --git a/platform/darwin/src/MGLMapSnapshotter.h b/platform/darwin/src/MGLMapSnapshotter.h index 98c8e8a37..ed7a8c2e5 100644 --- a/platform/darwin/src/MGLMapSnapshotter.h +++ b/platform/darwin/src/MGLMapSnapshotter.h @@ -58,7 +58,7 @@ typedef void (^MGLMapSnapshotOverlayHandler)(MGLMapSnapshotOverlay * snapshotOve The options to use when creating images with the `MGLMapSnapshotter`. */ MGL_EXPORT -@interface MGLMapSnapshotOptions : NSObject +@interface MGLMapSnapshotOptions : NSObject /** Creates a set of options with the minimum required information. @@ -252,7 +252,7 @@ MGL_EXPORT /** 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`. + resulting `MGLMapSnapshot`. @param overlayHandler The block to handle manipulation of the `MGLMapSnapshotter`'s `CGContext`. @param completionHandler The block to handle the result in. */ diff --git a/platform/darwin/src/MGLMapSnapshotter.mm b/platform/darwin/src/MGLMapSnapshotter.mm index 7c2385a5f..908181ef7 100644 --- a/platform/darwin/src/MGLMapSnapshotter.mm +++ b/platform/darwin/src/MGLMapSnapshotter.mm @@ -33,6 +33,9 @@ const CGPoint MGLLogoImagePosition = CGPointMake(8, 8); const CGFloat MGLSnapshotterMinimumPixelSize = 64; +MGLImage *MGLAttributedSnapshot(mbgl::MapSnapshotter::Attributions attributions, MGLImage *mglImage, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MGLMapSnapshotOptions *options, MGLMapSnapshotOverlayHandler overlayHandler); +MGLMapSnapshot *MGLSnapshotWithDecoratedImage(MGLImage *mglImage, MGLMapSnapshotOptions *options, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MGLMapSnapshotOverlayHandler overlayHandler, NSError * _Nullable *outError); +NSArray *MGLAttributionInfosFromAttributions(mbgl::MapSnapshotter::Attributions attributions); @interface MGLMapSnapshotOverlay() @property (nonatomic, assign) CGFloat scale; @@ -131,6 +134,15 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; 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 MGLMapSnapshot() @@ -190,32 +202,19 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; @end @interface MGLMapSnapshotter() -@property (nonatomic) BOOL cancelled; +@property (nonatomic) BOOL loading; @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 _mbglMapSnapshotter; - std::unique_ptr> _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]; - } + [self cancel]; } - - (instancetype)init { NSAssert(NO, @"Please use -[MGLMapSnapshotter initWithOptions:]"); [super doesNotRecognizeSelector:_cmd]; @@ -227,7 +226,7 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; MGLLogDebug(@"Initializing withOptions: %@", options); self = [super init]; if (self) { - [self setOptions:options]; + self.options = options; #if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; #else @@ -242,13 +241,7 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; - (void)applicationWillTerminate:(NSNotification *)notification { - if (self.completion) { - [self cancel]; - } - - _mbglMapSnapshotter.reset(); - _snapshotCallback.reset(); - + [self cancel]; self.terminated = YES; } @@ -273,36 +266,25 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; format:@"startWithQueue:completionHandler: must be called from a thread with an active run loop."]; } - if (self.completion) { + 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."]; } + + MGLMapSnapshotOptions *options = [self.options copy]; + [self configureWithOptions:options]; + MGLLogDebug(@"Starting with options: %@", self.options); - self.completion = completion; - self.resultQueue = queue; - self.cancelled = NO; - + // Capture weakSelf to avoid retain cycle if callback is never called (i.e., snapshot canceled). __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::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; - + _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}; @@ -312,34 +294,41 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; #endif // Dispatch to result queue dispatch_async(queue, ^{ - strongSelf.completion(nil, error); - strongSelf.completion = nil; + weakSelf.loading = NO; + completion(nil, error); }); - } else { #if TARGET_OS_IPHONE - MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image) scale:strongSelf.options.scale]; + MGLImage *mglImage = [[MGLImage alloc] initWithMGLPremultipliedImage:std::move(image) scale: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); + mglImage.size = NSMakeSize(mglImage.size.width / options.scale, + mglImage.size.height / options.scale); #endif - - [strongSelf drawAttributedSnapshot:attributions snapshotImage:mglImage pointForFn:pointForFn latLngForFn:latLngForFn overlayHandler:overlayHandler]; + // 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; + MGLMapSnapshot *snapshot = MGLSnapshotWithDecoratedImage(mglImage, options, attributions, pointForFn, latLngForFn, overlayHandler, &error); + + // Dispatch result to result queue + dispatch_async(queue, ^{ + __typeof__(self) strongSelf = weakSelf; + strongSelf.loading = NO; + if (completion) { + completion(snapshot, error); + } + }); + }); } - 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 showsLogo:(BOOL)showsLogo overlayHandler:(MGLMapSnapshotOverlayHandler)overlayHandler { +MGLImage *MGLAttributedSnapshot(mbgl::MapSnapshotter::Attributions attributions, MGLImage *mglImage, MGLMapSnapshotOptions *options, void (^overlayHandler)()) { - NSArray* attributionInfo = [MGLMapSnapshotter generateAttributionInfos:attributions]; + NSArray *attributionInfo = MGLAttributionInfosFromAttributions(attributions); #if TARGET_OS_IPHONE MGLAttributionInfoStyle attributionInfoStyle = MGLAttributionInfoStyleLong; @@ -375,31 +364,20 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; attributionBackgroundSize.width * mglImage.scale, attributionBackgroundSize.height * mglImage.scale); - - UIGraphicsBeginImageContextWithOptions(mglImage.size, NO, scale); + UIGraphicsBeginImageContextWithOptions(mglImage.size, NO, options.scale); [mglImage drawInRect:CGRectMake(0, 0, mglImage.size.width, mglImage.size.height)]; + overlayHandler(); CGContextRef currentContext = UIGraphicsGetCurrentContext(); - if (currentContext && overlayHandler) { - MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:currentContext - scale:scale - pointForFn:pointForFn - latLngForFn:latLngForFn]; - CGContextSaveGState(snapshotOverlay.context); - overlayHandler(snapshotOverlay); - CGContextRestoreGState(snapshotOverlay.context); - currentContext = UIGraphicsGetCurrentContext(); - } - - if (!currentContext && overlayHandler) { + if (!currentContext) { // If the current context has been corrupted by the user, // return nil so we can generate an error later. return nil; } - if (showsLogo) { + if (options.showsLogo) { [logoImage drawInRect:logoImageRect]; } @@ -423,9 +401,7 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; return compositedImage; #else - - NSSize targetSize = NSMakeSize(size.width, size.height); - NSRect targetFrame = NSMakeRect(0, 0, targetSize.width, targetSize.height); + NSRect targetFrame = { .origin = NSZeroPoint, .size = options.size }; MGLAttributionInfoStyle attributionInfoStyle = MGLAttributionInfoStyleLong; for (NSUInteger styleValue = MGLAttributionInfoStyleLong; styleValue >= MGLAttributionInfoStyleShort; styleValue--) { @@ -461,25 +437,14 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; NSImageRep *sourceImageRep = [sourceImage bestRepresentationForRect:targetFrame context:nil hints:nil]; - compositedImage = [[NSImage alloc] initWithSize:targetSize]; + compositedImage = [[NSImage alloc] initWithSize:targetFrame.size]; [compositedImage lockFocus]; [sourceImageRep drawInRect: targetFrame]; NSGraphicsContext *currentContext = [NSGraphicsContext currentContext]; - if (currentContext && overlayHandler) { - MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:currentContext.CGContext - scale:scale - pointForFn:pointForFn - latLngForFn:latLngForFn]; - [currentContext saveGraphicsState]; - overlayHandler(snapshotOverlay); - [currentContext restoreGraphicsState]; - currentContext = [NSGraphicsContext currentContext]; - } - - if (!currentContext && overlayHandler) { + if (!currentContext) { // If the current context has been corrupted by the user, // return nil so we can generate an error later. return nil; @@ -505,59 +470,55 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; #endif } -- (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; - - // Capture scale and size by value to avoid accessing self from another thread - CGFloat scale = self.options.scale; - CGSize size = self.options.size; - BOOL showsLogo = self.options.showsLogo; - - // 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 - showsLogo:showsLogo - overlayHandler:overlayHandler]; - - // Dispatch result to origin queue - dispatch_async(resultQueue, ^{ - __typeof__(self) strongself = weakself; - - if (strongself.completion) { - - 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; - } - } - }); +MGLMapSnapshot *MGLSnapshotWithDecoratedImage(MGLImage *mglImage, MGLMapSnapshotOptions *options, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn, MGLMapSnapshotOverlayHandler overlayHandler, NSError * _Nullable *outError) { + MGLImage *compositedImage = MGLAttributedSnapshot(attributions, mglImage, options, ^{ + if (!overlayHandler) { + return; + } +#if TARGET_OS_IPHONE + CGContextRef context = UIGraphicsGetCurrentContext(); + if (!context) { + return; + } + MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:context + scale:options.scale + pointForFn:pointForFn + latLngForFn:latLngForFn]; + CGContextSaveGState(context); + overlayHandler(snapshotOverlay); + CGContextRestoreGState(context); +#else + NSGraphicsContext *context = [NSGraphicsContext currentContext]; + if (!context) { + return; + } + MGLMapSnapshotOverlay *snapshotOverlay = [[MGLMapSnapshotOverlay alloc] initWithContext:context.CGContext + scale:options.scale + pointForFn:pointForFn + latLngForFn:latLngForFn]; + [context saveGraphicsState]; + overlayHandler(snapshotOverlay); + [context restoreGraphicsState]; +#endif }); + + if (compositedImage) { + return [[MGLMapSnapshot alloc] initWithImage:compositedImage + scale:options.scale + pointForFn:pointForFn + latLngForFn:latLngForFn]; + } else { + if (outError) { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Failed to generate composited snapshot."}; + *outError = [NSError errorWithDomain:MGLErrorDomain + code:MGLErrorCodeSnapshotFailed + userInfo:userInfo]; + } + return nil; + } } -+ (NSArray*) generateAttributionInfos:(mbgl::MapSnapshotter::Attributions)attributions { +NSArray *MGLAttributionInfosFromAttributions(mbgl::MapSnapshotter::Attributions attributions) { NSMutableArray *infos = [NSMutableArray array]; #if TARGET_OS_IPHONE @@ -567,8 +528,8 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; CGFloat fontSize = [NSFont systemFontSizeForControlSize:NSMiniControlSize]; NSColor *attributeFontColor = [NSColor blackColor]; #endif - for (auto attribution = attributions.begin(); attribution != attributions.end(); ++attribution) { - NSString *attributionHTMLString = @(attribution->c_str()); + for (auto attribution : attributions) { + NSString *attributionHTMLString = @(attribution.c_str()); NSArray *tileSetInfos = [MGLAttributionInfo attributionInfosFromHTMLString:attributionHTMLString fontSize:fontSize linkColor:attributeFontColor]; @@ -688,57 +649,16 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; - (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; + if (_mbglMapSnapshotter) { + _mbglMapSnapshotter->cancel(); } - - _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]; -#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR - [[MMEEventsManager sharedManager] reportError:error]; -#endif - 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; - +- (void)configureWithOptions:(MGLMapSnapshotOptions *)options { auto mbglFileSource = [[MGLOfflineStorage sharedOfflineStorage] mbglFileSource]; - std::string styleURL = std::string([options.styleURL.absoluteString UTF8String]); - std::pair 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 = { @@ -748,6 +668,19 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; float pixelRatio = MAX(options.scale, 1); + // 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( + size, pixelRatio, resourceOptions, mbgl::MapSnapshotterObserver::nullObserver(), config.localFontFamilyName); + + _mbglMapSnapshotter->setStyleURL(std::string(options.styleURL.absoluteString.UTF8String)); + // Camera options mbgl::CameraOptions cameraOptions; if (CLLocationCoordinate2DIsValid(options.camera.centerCoordinate)) { @@ -756,23 +689,12 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; cameraOptions.bearing = MAX(0, options.camera.heading); cameraOptions.zoom = MAX(0, options.zoomLevel); cameraOptions.pitch = MAX(0, options.camera.pitch); + _mbglMapSnapshotter->setCameraOptions(cameraOptions); // Region - mbgl::optional coordinateBounds; if (!MGLCoordinateBoundsIsEmpty(options.coordinateBounds)) { - coordinateBounds = MGLLatLngBoundsFromCoordinateBounds(options.coordinateBounds); + _mbglMapSnapshotter->setRegion(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( - style, size, pixelRatio, cameraOptions, coordinateBounds, config.localFontFamilyName, resourceOptions); } @end diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index bd3df88dc..b4552f8e4 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -19,6 +19,8 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * Added the `MGLMapView.minimumPitch` and `MGLMapView.maximumPitch` properties to further limit how much the user or your code can tilt the map. ([#208](https://github.com/mapbox/mapbox-gl-native-ios/pull/208)) * Fixed issues where an offline pack would stop downloading before completion. ([#16230](https://github.com/mapbox/mapbox-gl-native/pull/16230), [#16240](https://github.com/mapbox/mapbox-gl-native/pull/16240)) * When an offline pack encounters an HTTP 404 error, the `MGLOfflinePackUserInfoKeyError` user info key of the `MGLOfflinePackErrorNotification` now indicates the resource that could not be downloaded. ([#16240](https://github.com/mapbox/mapbox-gl-native/pull/16240)) +* The `-[MGLMapSnapshotter cancel]` method no longer calls the completion handler passed into `-[MGLMapSnapshotter startWithCompletionHandler:]`. ([#209](https://github.com/mapbox/mapbox-gl-native-ios/pull/210)) +* Fixed an issue where the `MGLMapSnapshotter.loading` property always returned `NO`, even while loading a snapshot. ([#209](https://github.com/mapbox/mapbox-gl-native-ios/pull/210)) * Fixed a memory leak when zooming with any options enabled in the `MGLMapView.debugMask` property. ([#15179](https://github.com/mapbox/mapbox-gl-native/issues/15179)) * Added the `-[MGLOfflineStorage preloadData:forURL:modificationDate:expirationDate:eTag:mustRevalidate:completionHandler:]` method for determining when the data is ready to retrieve from the cache. ([#188](https://github.com/mapbox/mapbox-gl-native-ios/pull/188)) diff --git a/platform/ios/Integration Tests/Snapshotter Tests/MGLMapSnapshotterTest.m b/platform/ios/Integration Tests/Snapshotter Tests/MGLMapSnapshotterTest.m index 9ef2054df..e016651de 100644 --- a/platform/ios/Integration Tests/Snapshotter Tests/MGLMapSnapshotterTest.m +++ b/platform/ios/Integration Tests/Snapshotter Tests/MGLMapSnapshotterTest.m @@ -72,6 +72,27 @@ MGLMapSnapshotter* snapshotterWithBounds(MGLCoordinateBounds bounds, CGSize size [self waitForExpectations:@[expectation] timeout:10.0]; } +- (void)testSnapshotterWithoutStrongReference🔒 { + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler shouldn’t be called if there’s no strong reference to the snapshotter and the snapshotter goes away."]; + expectation.inverted = YES; + + CGSize size = self.mapView.bounds.size; + CLLocationCoordinate2D coordinates = CLLocationCoordinate2DMake(30.0, 30.0); + __weak MGLMapSnapshotter *weakSnapshotter; + @autoreleasepool { + MGLMapSnapshotter *snapshotter = snapshotterWithCoordinates(coordinates, size); + weakSnapshotter = snapshotter; + [snapshotter startWithCompletionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) { + // This completion block should not be called. + [expectation fulfill]; + }]; + MGLTestAssertNotNil(self, weakSnapshotter, @"Locally scoped snapshotter should not go away while in scope."); + } + MGLTestAssertNil(self, weakSnapshotter, @"Snapshotter should go away on its own if there’s no strong reference to it."); + + [self waitForExpectations:@[expectation] timeout:5]; +} + - (void)testDeallocatingSnapshotterDuringSnapshot🔒 { // See also https://github.com/mapbox/mapbox-gl-native/issues/12336 @@ -93,20 +114,12 @@ MGLMapSnapshotter* snapshotterWithBounds(MGLCoordinateBounds bounds, CGSize size dispatch_async(dispatch_get_main_queue(), ^{ MGLMapSnapshotter *snapshotter = snapshotterWithCoordinates(coord, size); - __weak MGLMapSnapshotter *weakSnapshotter = snapshotter; - [snapshotter startWithCompletionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) { - // We expect this completion block to be called with an error + // This completion block should not be called. __typeof__(self) strongself = weakself; - - MGLTestAssertNil(strongself, snapshot); - MGLTestAssert(strongself, - ([error.domain isEqualToString:MGLErrorDomain] && error.code == MGLErrorCodeSnapshotFailed), - @"Should have errored"); - MGLTestAssertNil(strongself, weakSnapshotter, @"Snapshotter should have been deallocated"); - - dispatch_group_leave(dg); + MGLTestFail(strongself, @"Completion handler should not be called after snapshotter is deallocated."); }]; + dispatch_group_leave(dg); }); dispatch_group_notify(dg, dispatch_get_main_queue(), ^{ @@ -162,10 +175,6 @@ MGLMapSnapshotter* snapshotterWithBounds(MGLCoordinateBounds bounds, CGSize size } - (void)testCancellingSnapshot🔒 { - XCTestExpectation *expectation = [self expectationWithDescription:@"snapshots"]; - expectation.assertForOverFulfill = YES; - expectation.expectedFulfillmentCount = 1; - CGSize size = self.mapView.bounds.size; CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(30.0, 30.0); @@ -174,20 +183,11 @@ MGLMapSnapshotter* snapshotterWithBounds(MGLCoordinateBounds bounds, CGSize size __weak __typeof__(self) weakself = self; [snapshotter startWithCompletionHandler:^(MGLMapSnapshot * _Nullable snapshot, NSError * _Nullable error) { - // We expect this completion block to be called with an error __typeof__(self) strongself = weakself; - - MGLTestAssertNil(strongself, snapshot); - MGLTestAssert(strongself, - ([error.domain isEqualToString:MGLErrorDomain] && error.code == MGLErrorCodeSnapshotFailed), - @"Should have been cancelled"); - MGLTestAssert(strongself, snapshotter.cancelled, @"Should have been cancelled"); - [expectation fulfill]; + MGLTestFail(strongself, @"Completion handler should not be called when canceling."); }]; [snapshotter cancel]; - - [self waitForExpectations:@[expectation] timeout:5.0]; } - (void)testAllocatingSnapshotOnBackgroundQueue🔒 { diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md index 504c49d73..a7625208a 100644 --- a/platform/macos/CHANGELOG.md +++ b/platform/macos/CHANGELOG.md @@ -43,6 +43,12 @@ * Fixed an issue where `-[MGLMapView visibleFeaturesInRect:]` and `-[MGLShapeSource featuresMatchingPredicate:]` could return incorrect coordinates at zoom levels 20 and higher. ([#15560](https://github.com/mapbox/mapbox-gl-native/pull/15560)) * Improved feature querying performance. ([#14930](https://github.com/mapbox/mapbox-gl-native/pull/14930)) +### Snapshots + +* 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)) +* The `-[MGLMapSnapshotter cancel]` method no longer calls the completion handler passed into `-[MGLMapSnapshotter startWithCompletionHandler:]`. ([#209](https://github.com/mapbox/mapbox-gl-native-ios/pull/210)) +* Fixed an issue where the `MGLMapSnapshotter.loading` property always returned `NO`, even while loading a snapshot. ([#209](https://github.com/mapbox/mapbox-gl-native-ios/pull/210)) + ### Networking and storage * Ideographic glyphs from Chinese, Japanese, and Korean are no longer downloaded by default as part of offline packs; they are instead rendered on-device, saving bandwidth and storage while improving performance. ([#14176](https://github.com/mapbox/mapbox-gl-native/pull/14176)) @@ -55,7 +61,6 @@ ### Other changes -* 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)) * Fixed a memory leak when zooming with any options enabled in the `MGLMapView.debugMask` property. ([#15179](https://github.com/mapbox/mapbox-gl-native/issues/15179)) * `MGLLoggingLevel` has been updated to better match core log levels. You can now use `MGLLoggingConfiguration.loggingLevel` to filter logs from core. ([#15120](https://github.com/mapbox/mapbox-gl-native/pull/15120))