diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index dc1c082edd3..fc89ebcc433 100644 --- a/gyp/platform-ios.gypi +++ b/gyp/platform-ios.gypi @@ -44,6 +44,12 @@ '../platform/darwin/src/MGLPolyline.mm', '../platform/darwin/src/MGLPolygon.mm', '../platform/darwin/src/MGLMapCamera.mm', + '../platform/darwin/src/MGLDownloadable.mm', + '../platform/darwin/src/MGLDownloadable_Private.h', + '../platform/darwin/src/MGLDownloadController.mm', + '../platform/darwin/src/MGLDownloadController_Private.h', + '../platform/darwin/src/MGLDownloadRegion_Private.h', + '../platform/darwin/src/MGLTilePyramidDownloadRegion.mm', '../platform/ios/src/MGLMapboxEvents.h', '../platform/ios/src/MGLMapboxEvents.m', '../platform/ios/src/MGLAPIClient.h', @@ -59,7 +65,6 @@ '../platform/ios/src/MGLUserLocationAnnotationView.m', '../platform/ios/src/MGLAnnotationImage_Private.h', '../platform/ios/src/MGLAnnotationImage.m', - '../platform/ios/include/MGLCalloutView.h', '../platform/ios/src/MGLCompactCalloutView.h', '../platform/ios/src/MGLCompactCalloutView.m', '../platform/ios/src/NSBundle+MGLAdditions.h', diff --git a/platform/darwin/include/MGLDownloadController.h b/platform/darwin/include/MGLDownloadController.h new file mode 100644 index 00000000000..415ba0bffe3 --- /dev/null +++ b/platform/darwin/include/MGLDownloadController.h @@ -0,0 +1,27 @@ +#import + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MGLDownloadable; +@protocol MGLDownloadRegion; + +typedef void (^MGLDownloadableRegistrationCompletionHandler)(MGLDownloadable *downloadable, NSError *error); +typedef void (^MGLDownloadablesRequestCompletionHandler)(NS_ARRAY_OF(MGLDownloadable *) *downloadables, NSError *error); + +@interface MGLDownloadController : NSObject + ++ (instancetype)sharedController; + +- (instancetype)init NS_UNAVAILABLE; + +- (void)addDownloadableForRegion:(id )downloadRegion withContext:(NSData *)context completionHandler:(MGLDownloadableRegistrationCompletionHandler)completion; + +- (void)requestDownloadablesWithCompletionHandler:(MGLDownloadablesRequestCompletionHandler)completion; + +- (void)setMaximumAllowedMapboxTiles:(uint64_t)maximumCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/darwin/include/MGLDownloadRegion.h b/platform/darwin/include/MGLDownloadRegion.h new file mode 100644 index 00000000000..5910c2117bc --- /dev/null +++ b/platform/darwin/include/MGLDownloadRegion.h @@ -0,0 +1,13 @@ +#import + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol MGLDownloadRegion + +@property (nonatomic, readonly) NSURL *styleURL; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/darwin/include/MGLDownloadable.h b/platform/darwin/include/MGLDownloadable.h new file mode 100644 index 00000000000..b3ce09981aa --- /dev/null +++ b/platform/darwin/include/MGLDownloadable.h @@ -0,0 +1,47 @@ +#import + +#import "MGLDownloadRegion.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol MGLDownloadableDelegate; + +typedef NS_ENUM (NSInteger, MGLDownloadableState) { + MGLDownloadableStateInactive = 0, + MGLDownloadableStateActive = 1, + MGLDownloadableStateComplete = 1, +}; + +typedef struct MGLDownloadableProgress { + uint64_t countOfResourcesCompleted; + uint64_t countOfBytesCompleted; + uint64_t countOfResourcesExpected; + uint64_t maximumResourcesExpected; +} MGLDownloadableProgress; + +@interface MGLDownloadable : NSObject + +@property (nonatomic, readonly) id region; +@property (nonatomic, readonly) NSData *context; +@property (nonatomic, readonly) MGLDownloadableState state; +@property (nonatomic, readonly) MGLDownloadableProgress progress; +@property (nonatomic, weak, nullable) id delegate; + +- (instancetype)init NS_UNAVAILABLE; + +- (void)resume; +- (void)suspend; + +@end + +@protocol MGLDownloadableDelegate + +@optional + +- (void)downloadable:(MGLDownloadable *)downloadable progressDidChange:(MGLDownloadableProgress)progress; +- (void)downloadable:(MGLDownloadable *)downloadable didReceiveError:(NSError *)error; +- (void)downloadable:(MGLDownloadable *)downloadable didReceiveMaximumAllowedMapboxTiles:(uint64_t)maximumCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/darwin/include/MGLTilePyramidDownloadRegion.h b/platform/darwin/include/MGLTilePyramidDownloadRegion.h new file mode 100644 index 00000000000..e08f56f73e7 --- /dev/null +++ b/platform/darwin/include/MGLTilePyramidDownloadRegion.h @@ -0,0 +1,20 @@ +#import + +#import "MGLDownloadRegion.h" +#import "MGLGeometry.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MGLTilePyramidDownloadRegion : NSObject + +@property (nonatomic, readonly) NSURL *styleURL; +@property (nonatomic, readonly) MGLCoordinateBounds bounds; +@property (nonatomic, readonly) double minimumZoomLevel; +@property (nonatomic, readonly) double maximumZoomLevel; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithStyleURL:(nullable NSURL *)styleURL bounds:(MGLCoordinateBounds)bounds fromZoomLevel:(double)minimumZoomLevel toZoomLevel:(double)maximumZoomLevel NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/darwin/include/MGLTypes.h b/platform/darwin/include/MGLTypes.h index c107055dbbd..213b90670ec 100644 --- a/platform/darwin/include/MGLTypes.h +++ b/platform/darwin/include/MGLTypes.h @@ -15,6 +15,13 @@ NS_ASSUME_NONNULL_BEGIN /** Indicates an error occurred in the Mapbox SDK. */ extern NSString * const MGLErrorDomain; +typedef NS_ENUM(NSInteger, MGLErrorCode) { + MGLErrorCodeUnknown = -1, + MGLErrorCodeNotFound = 1, + MGLErrorCodeBadServerResponse = 2, + MGLErrorCodeConnectionFailed = 3, +}; + /** The mode used to track the user location on the map. */ typedef NS_ENUM(NSUInteger, MGLUserTrackingMode) { /** The map does not follow the user location. */ diff --git a/platform/darwin/src/MGLDownloadController.mm b/platform/darwin/src/MGLDownloadController.mm new file mode 100644 index 00000000000..51fe8df7eff --- /dev/null +++ b/platform/darwin/src/MGLDownloadController.mm @@ -0,0 +1,129 @@ +#import "MGLDownloadController.h" + +#import "MGLAccountManager_Private.h" +#import "MGLGeometry_Private.h" +#import "MGLDownloadable_Private.h" +#import "MGLDownloadRegion_Private.h" +#import "MGLTilePyramidDownloadRegion.h" + +#include +#include + +@interface MGLDownloadController () + +- (instancetype)initWithFileName:(NSString *)fileName NS_DESIGNATED_INITIALIZER; + +@end + +@implementation MGLDownloadController { + mbgl::DefaultFileSource *_mbglFileSource; +} + ++ (instancetype)sharedController { + static dispatch_once_t onceToken; + static MGLDownloadController *sharedController; + dispatch_once(&onceToken, ^{ + sharedController = [[self alloc] initWithName:@"offline.db"]; + }); + return sharedController; +} + +- (instancetype)initWithFileName:(NSString *)fileName { + if (self = [super init]) { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *fileCachePath = [paths.firstObject stringByAppendingPathComponent:fileName]; + _mbglFileSource = new mbgl::DefaultFileSource(fileCachePath.UTF8String, [NSBundle mainBundle].resourceURL.path.UTF8String); + + // Observe for changes to the global access token (and find out the current one). + [[MGLAccountManager sharedManager] addObserver:self + forKeyPath:@"accessToken" + options:(NSKeyValueObservingOptionInitial | + NSKeyValueObservingOptionNew) + context:NULL]; + } + return self; +} + +- (void)dealloc { + [[MGLAccountManager sharedManager] removeObserver:self forKeyPath:@"accessToken"]; + + delete _mbglFileSource; + _mbglFileSource = nullptr; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NS_DICTIONARY_OF(NSString *, id) *)change context:(__unused void *)context { + // Synchronize mbgl::Map’s access token with the global one in MGLAccountManager. + if ([keyPath isEqualToString:@"accessToken"] && object == [MGLAccountManager sharedManager]) { + NSString *accessToken = change[NSKeyValueChangeNewKey]; + if (![accessToken isKindOfClass:[NSNull class]]) { + _mbglFileSource->setAccessToken(accessToken.UTF8String); + } + } +} + +- (void)addDownloadableForRegion:(id )downloadRegion withContext:(NSData *)context completionHandler:(MGLDownloadableRegistrationCompletionHandler)completion { + if (![downloadRegion conformsToProtocol:@protocol(MGLDownloadRegion_Private)]) { + [NSException raise:@"Unsupported region type" format: + @"Regions of type %@ are unsupported.", NSStringFromClass(downloadRegion.class)]; + return; + } + + const mbgl::OfflineRegionDefinition regionDefinition = [(id )downloadRegion offlineRegionDefinition]; + mbgl::OfflineRegionMetadata metadata; + metadata.reserve(context.length); + [context getBytes:&metadata length:metadata.capacity()]; + _mbglFileSource->createOfflineRegion(regionDefinition, metadata, [&](std::exception_ptr exception, mbgl::optional region) { + dispatch_async(dispatch_get_main_queue(), [&](void) { + NSError *error; + if (exception) { + error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{ + NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()), + }]; + } + if (completion) { + MGLDownloadable *downloadable = [[MGLDownloadable alloc] initWithMBGLRegion:new mbgl::OfflineRegion(std::move(*region))]; + completion(downloadable, error); + } + }); + }); +} + +- (void)resumeDownloadable:(MGLDownloadable *)downloadable { + _mbglFileSource->setOfflineRegionDownloadState(*downloadable.mbglOfflineRegion, mbgl::OfflineRegionDownloadState::Active); +} + +- (void)suspendDownloadable:(MGLDownloadable *)downloadable { + _mbglFileSource->setOfflineRegionDownloadState(*downloadable.mbglOfflineRegion, mbgl::OfflineRegionDownloadState::Inactive); +} + +- (void)requestDownloadablesWithCompletionHandler:(MGLDownloadablesRequestCompletionHandler)completion { + _mbglFileSource->listOfflineRegions([&](std::exception_ptr exception, mbgl::optional> regions) { + dispatch_async(dispatch_get_main_queue(), [&](void) { + NSError *error; + if (exception) { + error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{ + NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()), + }]; + } + if (completion) { + NSMutableArray *downloadables; + if (regions) { + downloadables = [NSMutableArray arrayWithCapacity:regions->size()]; + for (mbgl::OfflineRegion ®ion : *regions) { + MGLDownloadable *downloadable = [[MGLDownloadable alloc] initWithMBGLRegion:new mbgl::OfflineRegion(std::move(region))]; + mbgl::OfflineRegionObserver *observer = downloadable.mbglOfflineRegionObserver; + _mbglFileSource->setOfflineRegionObserver(*downloadable.mbglOfflineRegion, std::make_unique(*observer)); + [downloadables addObject:downloadable]; + } + } + completion(downloadables, error); + } + }); + }); +} + +- (void)setMaximumAllowedMapboxTiles:(uint64_t)maximumCount { + _mbglFileSource->setOfflineMapboxTileCountLimit(maximumCount); +} + +@end diff --git a/platform/darwin/src/MGLDownloadController_Private.h b/platform/darwin/src/MGLDownloadController_Private.h new file mode 100644 index 00000000000..9d844fda11c --- /dev/null +++ b/platform/darwin/src/MGLDownloadController_Private.h @@ -0,0 +1,10 @@ +#import "MGLDownloadController.h" + +#import "MGLDownloadable.h" + +@interface MGLDownloadController (Private) + +- (void)resumeDownloadable:(MGLDownloadable *)downloadable; +- (void)suspendDownloadable:(MGLDownloadable *)downloadable; + +@end diff --git a/platform/darwin/src/MGLDownloadRegion_Private.h b/platform/darwin/src/MGLDownloadRegion_Private.h new file mode 100644 index 00000000000..785775198af --- /dev/null +++ b/platform/darwin/src/MGLDownloadRegion_Private.h @@ -0,0 +1,17 @@ +#import + +#import "MGLDownloadRegion.h" + +#include + +NS_ASSUME_NONNULL_BEGIN + +@protocol MGLDownloadRegion_Private + +- (instancetype)initWithOfflineRegionDefinition:(const mbgl::OfflineRegionDefinition &)definition; + +- (const mbgl::OfflineRegionDefinition)offlineRegionDefinition; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/darwin/src/MGLDownloadable.mm b/platform/darwin/src/MGLDownloadable.mm new file mode 100644 index 00000000000..0bfdec691c5 --- /dev/null +++ b/platform/darwin/src/MGLDownloadable.mm @@ -0,0 +1,145 @@ +#import "MGLDownloadable_Private.h" + +#import "MGLDownloadController_Private.h" +#import "MGLDownloadRegion_Private.h" +#import "MGLTilePyramidDownloadRegion.h" + +#include + +class MBGLOfflineRegionObserver; + +@interface MGLDownloadable () + +@property (nonatomic, readwrite) mbgl::OfflineRegion *mbglOfflineRegion; +@property (nonatomic, readwrite) MBGLOfflineRegionObserver *mbglOfflineRegionObserver; +@property (nonatomic, readwrite) MGLDownloadableState state; +@property (nonatomic, readwrite) MGLDownloadableProgress progress; + +@end + +@implementation MGLDownloadable + +- (instancetype)init { + [NSException raise:@"Method unavailable" + format: + @"-[MGLDownloadable init] is unavailable. " + @"Use +[MGLDownloadController addDownloadRegion:context:completionHandler:] instead."]; + return nil; +} + +- (instancetype)initWithMBGLRegion:(mbgl::OfflineRegion *)region { + if (self = [super init]) { + _mbglOfflineRegion = region; + _state = MGLDownloadableStateInactive; + _mbglOfflineRegionObserver = new MBGLOfflineRegionObserver(self); + } + return self; +} + +- (void)dealloc { + delete _mbglOfflineRegionObserver; + _mbglOfflineRegionObserver = nullptr; +} + +- (id )region { + const mbgl::OfflineRegionDefinition ®ionDefinition = _mbglOfflineRegion->getDefinition(); + NSAssert([MGLTilePyramidDownloadRegion conformsToProtocol:@protocol(MGLDownloadRegion_Private)], @"MGLTilePyramidDownloadRegion should conform to MGLDownloadRegion_Private."); + return [(id )[MGLTilePyramidDownloadRegion alloc] initWithOfflineRegionDefinition:regionDefinition]; +} + +- (NSData *)context { + auto &metadata = _mbglOfflineRegion->getMetadata(); + return [NSData dataWithBytes:&metadata length:metadata.size()]; +} + +- (void)resume { + [[MGLDownloadController sharedController] resumeDownloadable:self]; + self.state = MGLDownloadableStateActive; +} + +- (void)suspend { + [[MGLDownloadController sharedController] suspendDownloadable:self]; + self.state = MGLDownloadableStateInactive; +} + +MGLDownloadableState MGLDownloadableStateFromOfflineRegionDownloadState(mbgl::OfflineRegionDownloadState offlineRegionDownloadState) { + switch (offlineRegionDownloadState) { + case mbgl::OfflineRegionDownloadState::Inactive: + return MGLDownloadableStateInactive; + + case mbgl::OfflineRegionDownloadState::Active: + return MGLDownloadableStateActive; + } +} + +NSError *MGLErrorFromResponseError(mbgl::Response::Error error) { + NSInteger errorCode = MGLErrorCodeUnknown; + switch (error.reason) { + case mbgl::Response::Error::Reason::NotFound: + errorCode = MGLErrorCodeNotFound; + break; + + case mbgl::Response::Error::Reason::Server: + errorCode = MGLErrorCodeBadServerResponse; + break; + + case mbgl::Response::Error::Reason::Connection: + errorCode = MGLErrorCodeConnectionFailed; + break; + + default: + break; + } + return [NSError errorWithDomain:MGLErrorDomain code:errorCode userInfo:@{ + NSLocalizedFailureReasonErrorKey: @(error.message.c_str()) + }]; +} + +void MBGLOfflineRegionObserver::statusChanged(mbgl::OfflineRegionStatus status) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSCAssert(downloadable, @"MBGLOfflineRegionObserver is dangling without an associated MGLDownloadable."); + + switch (status.downloadState) { + case mbgl::OfflineRegionDownloadState::Inactive: + downloadable.state = status.complete() ? MGLDownloadableStateComplete : MGLDownloadableStateInactive; + + case mbgl::OfflineRegionDownloadState::Active: + downloadable.state = MGLDownloadableStateActive; + } + + if ([downloadable.delegate respondsToSelector:@selector(downloadable:progressDidChange:)]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc99-extensions" + downloadable.progress = { + .countOfResourcesCompleted = status.completedResourceCount, + .countOfBytesCompleted = status.completedResourceSize, + .countOfResourcesExpected = status.requiredResourceCount, + .maximumResourcesExpected = status.requiredResourceCountIsPrecise ? status.requiredResourceCount : UINT64_MAX, + }; +#pragma clang diagnostic pop + [downloadable.delegate downloadable:downloadable progressDidChange:downloadable.progress]; + } + }); +} + +void MBGLOfflineRegionObserver::responseError(mbgl::Response::Error error) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSCAssert(downloadable, @"MBGLOfflineRegionObserver is dangling without an associated MGLDownloadable."); + + if ([downloadable.delegate respondsToSelector:@selector(downloadable:didReceiveError:)]) { + [downloadable.delegate downloadable:downloadable didReceiveError:MGLErrorFromResponseError(error)]; + } + }); +} + +void MBGLOfflineRegionObserver::mapboxTileCountLimitExceeded(uint64_t limit) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSCAssert(downloadable, @"MBGLOfflineRegionObserver is dangling without an associated MGLDownloadable."); + + if ([downloadable.delegate respondsToSelector:@selector(downloadable:didReceiveMaximumAllowedMapboxTiles:)]) { + [downloadable.delegate downloadable:downloadable didReceiveMaximumAllowedMapboxTiles:limit]; + } + }); +} + +@end diff --git a/platform/darwin/src/MGLDownloadable_Private.h b/platform/darwin/src/MGLDownloadable_Private.h new file mode 100644 index 00000000000..b7594495d6b --- /dev/null +++ b/platform/darwin/src/MGLDownloadable_Private.h @@ -0,0 +1,26 @@ +#import "MGLDownloadable.h" + +#include + +class MBGLOfflineRegionObserver; + +@interface MGLDownloadable (Private) + +@property (nonatomic, readonly) mbgl::OfflineRegion *mbglOfflineRegion; +@property (nonatomic, readonly) MBGLOfflineRegionObserver *mbglOfflineRegionObserver; + +- (instancetype)initWithMBGLRegion:(mbgl::OfflineRegion *)region; + +@end + +class MBGLOfflineRegionObserver : public mbgl::OfflineRegionObserver { +public: + MBGLOfflineRegionObserver(MGLDownloadable *downloadable_) : downloadable(downloadable_) {} + + void statusChanged(mbgl::OfflineRegionStatus status) override; + void responseError(mbgl::Response::Error error) override; + void mapboxTileCountLimitExceeded(uint64_t limit) override; + +private: + __weak MGLDownloadable *downloadable = nullptr; +}; diff --git a/platform/darwin/src/MGLTilePyramidDownloadRegion.mm b/platform/darwin/src/MGLTilePyramidDownloadRegion.mm new file mode 100644 index 00000000000..b8f0de51070 --- /dev/null +++ b/platform/darwin/src/MGLTilePyramidDownloadRegion.mm @@ -0,0 +1,64 @@ +#import "MGLTilePyramidDownloadRegion.h" + +#import "MGLDownloadRegion_Private.h" +#import "MGLGeometry_Private.h" +#import "MGLStyle.h" + +@interface MGLTilePyramidDownloadRegion () + +@property (nonatomic, readwrite, null_resettable) NSURL *styleURL; + +@end + +@implementation MGLTilePyramidDownloadRegion + +- (instancetype)init { + [NSException raise:@"Method unavailable" + format: + @"-[MGLTilePyramidDownloadRegion init] is unavailable. " + @"Use -initWithStyleURL:bounds:fromZoomLevel:toZoomLevel: instead."]; + return nil; +} + +- (instancetype)initWithStyleURL:(NSURL *)styleURL bounds:(MGLCoordinateBounds)bounds fromZoomLevel:(double)minimumZoomLevel toZoomLevel:(double)maximumZoomLevel { + if (self = [super init]) { + self.styleURL = styleURL; + _bounds = bounds; + _minimumZoomLevel = minimumZoomLevel; + _maximumZoomLevel = maximumZoomLevel; + } + return self; +} + +- (instancetype)initWithOfflineRegionDefinition:(const mbgl::OfflineRegionDefinition &)definition { + NSURL *styleURL = [NSURL URLWithString:@(definition.styleURL.c_str())]; + MGLCoordinateBounds bounds = MGLCoordinateBoundsFromLatLngBounds(definition.bounds); + return [self initWithStyleURL:styleURL bounds:bounds fromZoomLevel:definition.minZoom toZoomLevel:definition.maxZoom]; +} + +- (void)setStyleURL:(NSURL *)styleURL { + if (!styleURL) { + styleURL = [MGLStyle streetsStyleURL]; + } + + if (!styleURL.scheme) { + // Assume a relative path into the application bundle. + styleURL = [NSURL URLWithString:[@"asset://" stringByAppendingString:styleURL.absoluteString]]; + } + + _styleURL = styleURL; +} + +- (const mbgl::OfflineRegionDefinition)offlineRegionDefinition { +#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR + const float scaleFactor = [UIScreen instancesRespondToSelector:@selector(nativeScale)] ? [[UIScreen mainScreen] nativeScale] : [[UIScreen mainScreen] scale]; +#elif TARGET_OS_MAC + const float scaleFactor = [NSScreen mainScreen].backingScaleFactor; +#endif + return mbgl::OfflineTilePyramidRegionDefinition(_styleURL.absoluteString.UTF8String, + MGLLatLngBoundsFromCoordinateBounds(_bounds), + _minimumZoomLevel, _maximumZoomLevel, + scaleFactor); +} + +@end diff --git a/platform/ios/framework/Mapbox.h b/platform/ios/framework/Mapbox.h index 11f163376a6..f13a542ac20 100644 --- a/platform/ios/framework/Mapbox.h +++ b/platform/ios/framework/Mapbox.h @@ -12,6 +12,9 @@ FOUNDATION_EXPORT const unsigned char MapboxVersionString[]; #import "MGLCalloutView.h" #import "MGLMapCamera.h" #import "MGLGeometry.h" +#import "MGLDownloadable.h" +#import "MGLDownloadRegion.h" +#import "MGLDownloadController.h" #import "MGLMapView.h" #import "MGLMapView+IBAdditions.h" #import "MGLMapView+MGLCustomStyleLayerAdditions.h" @@ -22,5 +25,6 @@ FOUNDATION_EXPORT const unsigned char MapboxVersionString[]; #import "MGLPolyline.h" #import "MGLShape.h" #import "MGLStyle.h" +#import "MGLTilePyramidDownloadRegion.h" #import "MGLTypes.h" #import "MGLUserLocation.h"