summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
authorMinh Nguyễn <mxn@1ec5.org>2016-03-03 16:28:51 -0800
committerMinh Nguyễn <mxn@1ec5.org>2016-03-10 17:08:58 -0800
commit77657b38969a55377117dfde5fc9d5e3478208d1 (patch)
tree612ffa47975015efaa300a617e4d916a14e72688 /platform
parenta65730803e4185600ff7251cf0a18a4835f4f41b (diff)
downloadqtlocation-mapboxgl-77657b38969a55377117dfde5fc9d5e3478208d1.tar.gz
[ios] Implemented offline API in iOS SDK
Fixes #3892.
Diffstat (limited to 'platform')
-rw-r--r--platform/darwin/include/MGLDownloadController.h30
-rw-r--r--platform/darwin/include/MGLDownloadRegion.h13
-rw-r--r--platform/darwin/include/MGLDownloadable.h49
-rw-r--r--platform/darwin/include/MGLTilePyramidDownloadRegion.h19
-rw-r--r--platform/darwin/include/MGLTypes.h7
-rw-r--r--platform/darwin/src/MGLDownloadController.mm134
-rw-r--r--platform/darwin/src/MGLDownloadController_Private.h15
-rw-r--r--platform/darwin/src/MGLDownloadRegion_Private.h17
-rw-r--r--platform/darwin/src/MGLDownloadable.mm160
-rw-r--r--platform/darwin/src/MGLDownloadable_Private.h26
-rw-r--r--platform/darwin/src/MGLTilePyramidDownloadRegion.mm62
-rw-r--r--platform/ios/framework/Mapbox.h4
-rw-r--r--platform/ios/framework/Mapbox.m3
13 files changed, 539 insertions, 0 deletions
diff --git a/platform/darwin/include/MGLDownloadController.h b/platform/darwin/include/MGLDownloadController.h
new file mode 100644
index 0000000000..86bd1e6573
--- /dev/null
+++ b/platform/darwin/include/MGLDownloadController.h
@@ -0,0 +1,30 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class MGLDownloadable;
+@protocol MGLDownloadRegion;
+
+typedef void (^MGLDownloadableRegistrationCompletionHandler)(MGLDownloadable *downloadable, NSError *error);
+typedef void (^MGLDownloadableRemovalCompletionHandler)(NSError *error);
+typedef void (^MGLDownloadablesRequestCompletionHandler)(NS_ARRAY_OF(MGLDownloadable *) *downloadables, NSError *error);
+
+@interface MGLDownloadController : NSObject
+
++ (instancetype)sharedController;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (void)addDownloadableForRegion:(id <MGLDownloadRegion>)downloadRegion withContext:(NSData *)context completionHandler:(MGLDownloadableRegistrationCompletionHandler)completion;
+
+- (void)removeDownloadable:(MGLDownloadable *)downloadable withCompletionHandler:(MGLDownloadableRemovalCompletionHandler)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 0000000000..5910c2117b
--- /dev/null
+++ b/platform/darwin/include/MGLDownloadRegion.h
@@ -0,0 +1,13 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol MGLDownloadRegion <NSObject>
+
+@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 0000000000..522a3c37ab
--- /dev/null
+++ b/platform/darwin/include/MGLDownloadable.h
@@ -0,0 +1,49 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLDownloadRegion.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol MGLDownloadableDelegate;
+
+typedef NS_ENUM (NSInteger, MGLDownloadableState) {
+ MGLDownloadableStateInactive = 0,
+ MGLDownloadableStateActive = 1,
+ MGLDownloadableStateComplete = 2,
+};
+
+typedef struct MGLDownloadableProgress {
+ uint64_t countOfResourcesCompleted;
+ uint64_t countOfBytesCompleted;
+ uint64_t countOfResourcesExpected;
+ uint64_t maximumResourcesExpected;
+} MGLDownloadableProgress;
+
+@interface MGLDownloadable : NSObject
+
+@property (nonatomic, readonly) id <MGLDownloadRegion> region;
+@property (nonatomic, readonly) NSData *context;
+@property (nonatomic, readonly) MGLDownloadableState state;
+@property (nonatomic, readonly) MGLDownloadableProgress progress;
+@property (nonatomic, weak, nullable) id <MGLDownloadableDelegate> delegate;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (void)resume;
+- (void)suspend;
+
+- (void)requestProgress;
+
+@end
+
+@protocol MGLDownloadableDelegate <NSObject>
+
+@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 0000000000..6269775f19
--- /dev/null
+++ b/platform/darwin/include/MGLTilePyramidDownloadRegion.h
@@ -0,0 +1,19 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLDownloadRegion.h"
+#import "MGLGeometry.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MGLTilePyramidDownloadRegion : NSObject <MGLDownloadRegion>
+
+@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 c107055dbb..213b90670e 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 0000000000..e4dd273032
--- /dev/null
+++ b/platform/darwin/src/MGLDownloadController.mm
@@ -0,0 +1,134 @@
+#import "MGLDownloadController_Private.h"
+
+#import "MGLAccountManager_Private.h"
+#import "MGLGeometry_Private.h"
+#import "MGLDownloadable_Private.h"
+#import "MGLDownloadRegion_Private.h"
+#import "MGLTilePyramidDownloadRegion.h"
+
+#include <mbgl/util/string.hpp>
+
+@interface MGLDownloadController ()
+
+@property (nonatomic) mbgl::DefaultFileSource *mbglFileSource;
+
+- (instancetype)initWithFileName:(NSString *)fileName NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@implementation MGLDownloadController
+
++ (instancetype)sharedController {
+ static dispatch_once_t onceToken;
+ static MGLDownloadController *sharedController;
+ dispatch_once(&onceToken, ^{
+ sharedController = [[self alloc] initWithFileName:@"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 the file source’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]]) {
+ self.mbglFileSource->setAccessToken(accessToken.UTF8String);
+ }
+ }
+}
+
+- (void)addDownloadableForRegion:(id <MGLDownloadRegion>)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::OfflineTilePyramidRegionDefinition regionDefinition = [(id <MGLDownloadRegion_Private>)downloadRegion offlineRegionDefinition];
+ mbgl::OfflineRegionMetadata metadata(context.length);
+ [context getBytes:&metadata[0] length:metadata.size()];
+ self.mbglFileSource->createOfflineRegion(regionDefinition, metadata, [&, completion](std::exception_ptr exception, mbgl::optional<mbgl::OfflineRegion> region) {
+ NSError *error;
+ if (exception) {
+ NSString *errorDescription = @(mbgl::util::toString(exception).c_str());
+ error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:errorDescription ? @{
+ NSLocalizedDescriptionKey: errorDescription,
+ } : nil];
+ }
+ if (completion) {
+ MGLDownloadable *downloadable = [[MGLDownloadable alloc] initWithMBGLRegion:new mbgl::OfflineRegion(std::move(*region))];
+ dispatch_async(dispatch_get_main_queue(), [&, completion, error, downloadable](void) {
+ completion(downloadable, error);
+ });
+ }
+ });
+}
+
+- (void)removeDownloadable:(MGLDownloadable *)downloadable withCompletionHandler:(MGLDownloadableRemovalCompletionHandler)completion {
+ self.mbglFileSource->deleteOfflineRegion(std::move(*downloadable.mbglOfflineRegion), [&, completion](std::exception_ptr exception) {
+ NSError *error;
+ if (exception) {
+ error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{
+ NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()),
+ }];
+ }
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), [&, completion, error](void) {
+ completion(error);
+ });
+ }
+ });
+}
+
+- (void)requestDownloadablesWithCompletionHandler:(MGLDownloadablesRequestCompletionHandler)completion {
+ self.mbglFileSource->listOfflineRegions([&, completion](std::exception_ptr exception, mbgl::optional<std::vector<mbgl::OfflineRegion>> regions) {
+ NSError *error;
+ if (exception) {
+ error = [NSError errorWithDomain:MGLErrorDomain code:-1 userInfo:@{
+ NSLocalizedDescriptionKey: @(mbgl::util::toString(exception).c_str()),
+ }];
+ }
+ NSMutableArray *downloadables;
+ if (regions) {
+ downloadables = [NSMutableArray arrayWithCapacity:regions->size()];
+ for (mbgl::OfflineRegion &region : *regions) {
+ MGLDownloadable *downloadable = [[MGLDownloadable alloc] initWithMBGLRegion:new mbgl::OfflineRegion(std::move(region))];
+ [downloadables addObject:downloadable];
+ }
+ }
+ if (completion) {
+ dispatch_async(dispatch_get_main_queue(), [&, completion, error, downloadables](void) {
+ 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 0000000000..2e26ab90cb
--- /dev/null
+++ b/platform/darwin/src/MGLDownloadController_Private.h
@@ -0,0 +1,15 @@
+#import "MGLDownloadController.h"
+
+#import "MGLDownloadable.h"
+
+#include <mbgl/storage/default_file_source.hpp>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MGLDownloadController (Private)
+
+@property (nonatomic) mbgl::DefaultFileSource *mbglFileSource;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/src/MGLDownloadRegion_Private.h b/platform/darwin/src/MGLDownloadRegion_Private.h
new file mode 100644
index 0000000000..785775198a
--- /dev/null
+++ b/platform/darwin/src/MGLDownloadRegion_Private.h
@@ -0,0 +1,17 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLDownloadRegion.h"
+
+#include <mbgl/storage/offline.hpp>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol MGLDownloadRegion_Private <MGLDownloadRegion>
+
+- (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 0000000000..507461b4b7
--- /dev/null
+++ b/platform/darwin/src/MGLDownloadable.mm
@@ -0,0 +1,160 @@
+#import "MGLDownloadable_Private.h"
+
+#import "MGLDownloadController_Private.h"
+#import "MGLDownloadRegion_Private.h"
+#import "MGLTilePyramidDownloadRegion.h"
+
+#include <mbgl/storage/default_file_source.hpp>
+#include <mbgl/util/string.hpp>
+
+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);
+
+ mbgl::DefaultFileSource *mbglFileSource = [[MGLDownloadController sharedController] mbglFileSource];
+ mbglFileSource->setOfflineRegionObserver(*_mbglOfflineRegion, std::make_unique<MBGLOfflineRegionObserver>(*_mbglOfflineRegionObserver));
+ }
+ return self;
+}
+
+- (void)dealloc {
+ delete _mbglOfflineRegionObserver;
+ _mbglOfflineRegionObserver = nullptr;
+}
+
+- (id <MGLDownloadRegion>)region {
+ const mbgl::OfflineRegionDefinition &regionDefinition = _mbglOfflineRegion->getDefinition();
+ NSAssert([MGLTilePyramidDownloadRegion conformsToProtocol:@protocol(MGLDownloadRegion_Private)], @"MGLTilePyramidDownloadRegion should conform to MGLDownloadRegion_Private.");
+ return [(id <MGLDownloadRegion_Private>)[MGLTilePyramidDownloadRegion alloc] initWithOfflineRegionDefinition:regionDefinition];
+}
+
+- (NSData *)context {
+ const mbgl::OfflineRegionMetadata &metadata = _mbglOfflineRegion->getMetadata();
+ return [NSData dataWithBytes:&metadata[0] length:metadata.size()];
+}
+
+- (void)resume {
+ mbgl::DefaultFileSource *mbglFileSource = [[MGLDownloadController sharedController] mbglFileSource];
+ mbglFileSource->setOfflineRegionDownloadState(*_mbglOfflineRegion, mbgl::OfflineRegionDownloadState::Active);
+ self.state = MGLDownloadableStateActive;
+}
+
+- (void)suspend {
+ mbgl::DefaultFileSource *mbglFileSource = [[MGLDownloadController sharedController] mbglFileSource];
+ mbglFileSource->setOfflineRegionDownloadState(*_mbglOfflineRegion, mbgl::OfflineRegionDownloadState::Inactive);
+ self.state = MGLDownloadableStateInactive;
+}
+
+- (void)requestProgress {
+ mbgl::DefaultFileSource *mbglFileSource = [[MGLDownloadController sharedController] mbglFileSource];
+
+ __weak MGLDownloadable *weakSelf = self;
+ mbglFileSource->getOfflineRegionStatus(*_mbglOfflineRegion, [&, weakSelf](__unused std::exception_ptr exception, mbgl::optional<mbgl::OfflineRegionStatus> status) {
+ if (status) {
+ mbgl::OfflineRegionStatus checkedStatus = *status;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ MGLDownloadable *strongSelf = weakSelf;
+ [strongSelf offlineRegionStatusDidChange:checkedStatus];
+ });
+ }
+ });
+}
+
+- (void)offlineRegionStatusDidChange:(mbgl::OfflineRegionStatus)status {
+ switch (status.downloadState) {
+ case mbgl::OfflineRegionDownloadState::Inactive:
+ self.state = status.complete() ? MGLDownloadableStateComplete : MGLDownloadableStateInactive;
+ break;
+
+ case mbgl::OfflineRegionDownloadState::Active:
+ self.state = MGLDownloadableStateActive;
+ break;
+ }
+
+ MGLDownloadableProgress progress;
+ progress.countOfResourcesCompleted = status.completedResourceCount;
+ progress.countOfBytesCompleted = status.completedResourceSize;
+ progress.countOfResourcesExpected = status.requiredResourceCount;
+ progress.maximumResourcesExpected = status.requiredResourceCountIsPrecise ? status.requiredResourceCount : UINT64_MAX;
+ self.progress = progress;
+
+ if ([self.delegate respondsToSelector:@selector(downloadable:progressDidChange:)]) {
+ [self.delegate downloadable:self progressDidChange:progress];
+ }
+}
+
+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.");
+
+ [downloadable offlineRegionStatusDidChange:status];
+ });
+}
+
+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 0000000000..a9b2460c19
--- /dev/null
+++ b/platform/darwin/src/MGLDownloadable_Private.h
@@ -0,0 +1,26 @@
+#import "MGLDownloadable.h"
+
+#include <mbgl/storage/default_file_source.hpp>
+
+class MBGLOfflineRegionObserver;
+
+@interface MGLDownloadable (Private)
+
+@property (nonatomic) 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 0000000000..942450c0c2
--- /dev/null
+++ b/platform/darwin/src/MGLTilePyramidDownloadRegion.mm
@@ -0,0 +1,62 @@
+#import "MGLTilePyramidDownloadRegion.h"
+
+#import "MGLDownloadRegion_Private.h"
+#import "MGLGeometry_Private.h"
+#import "MGLStyle.h"
+
+@interface MGLTilePyramidDownloadRegion () <MGLDownloadRegion_Private>
+
+@end
+
+@implementation MGLTilePyramidDownloadRegion {
+ NSURL *_styleURL;
+}
+
+@synthesize styleURL = _styleURL;
+
+- (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]) {
+ 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;
+ _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];
+}
+
+- (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 11f163376a..f13a542ac2 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"
diff --git a/platform/ios/framework/Mapbox.m b/platform/ios/framework/Mapbox.m
index 93f58b9be6..cc3be1e2b2 100644
--- a/platform/ios/framework/Mapbox.m
+++ b/platform/ios/framework/Mapbox.m
@@ -17,6 +17,8 @@ static void InitializeMapbox() {
[MGLAccountManager class];
[MGLAnnotationImage class];
+ [MGLDownloadable class];
+ [MGLDownloadController class];
[MGLMapCamera class];
[MGLMapView class];
[MGLMultiPoint class];
@@ -25,5 +27,6 @@ static void InitializeMapbox() {
[MGLPolyline class];
[MGLShape class];
[MGLStyle class];
+ [MGLTilePyramidDownloadRegion class];
[MGLUserLocation class];
}