[ios] Implemented offline API in iOS SDK

Fixes #3892.
This commit is contained in:
Minh Nguyễn
2016-03-03 16:28:51 -08:00
committed by Minh Nguyễn
parent a65730803e
commit 77657b3896
14 changed files with 545 additions and 1 deletions

View File

@@ -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',

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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. */

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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"

View File

@@ -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];
}