summaryrefslogtreecommitdiff
path: root/platform/darwin
diff options
context:
space:
mode:
Diffstat (limited to 'platform/darwin')
-rw-r--r--platform/darwin/src/MGLOfflinePack.mm21
-rw-r--r--platform/darwin/src/MGLOfflineRegion.h15
-rw-r--r--platform/darwin/src/MGLOfflineRegion_Private.h9
-rw-r--r--platform/darwin/src/MGLOfflineStorage.mm2
-rw-r--r--platform/darwin/src/MGLShapeOfflineRegion.h72
-rw-r--r--platform/darwin/src/MGLShapeOfflineRegion.mm120
-rw-r--r--platform/darwin/src/MGLShapeOfflineRegion_Private.h22
-rw-r--r--platform/darwin/src/MGLTilePyramidOfflineRegion.h14
-rw-r--r--platform/darwin/src/MGLTilePyramidOfflineRegion.mm5
-rw-r--r--platform/darwin/src/MGLTilePyramidOfflineRegion_Private.h22
-rw-r--r--platform/darwin/test/MGLOfflineRegionTests.m18
-rw-r--r--platform/darwin/test/MGLOfflineStorageTests.mm74
12 files changed, 365 insertions, 29 deletions
diff --git a/platform/darwin/src/MGLOfflinePack.mm b/platform/darwin/src/MGLOfflinePack.mm
index 7bbc681c88..bafb976585 100644
--- a/platform/darwin/src/MGLOfflinePack.mm
+++ b/platform/darwin/src/MGLOfflinePack.mm
@@ -3,6 +3,9 @@
#import "MGLOfflineStorage_Private.h"
#import "MGLOfflineRegion_Private.h"
#import "MGLTilePyramidOfflineRegion.h"
+#import "MGLTilePyramidOfflineRegion_Private.h"
+#import "MGLShapeOfflineRegion.h"
+#import "MGLShapeOfflineRegion_Private.h"
#import "NSValue+MGLAdditions.h"
@@ -27,6 +30,12 @@ const MGLExceptionName MGLInvalidOfflinePackException = @"MGLInvalidOfflinePackE
} \
} while (NO);
+@interface MGLTilePyramidOfflineRegion () <MGLOfflineRegion_Private, MGLTilePyramidOfflineRegion_Private>
+@end
+
+@interface MGLShapeOfflineRegion () <MGLOfflineRegion_Private, MGLShapeOfflineRegion_Private>
+@end
+
class MBGLOfflineRegionObserver : public mbgl::OfflineRegionObserver {
public:
MBGLOfflineRegionObserver(MGLOfflinePack *pack_) : pack(pack_) {}
@@ -78,7 +87,17 @@ private:
const mbgl::OfflineRegionDefinition &regionDefinition = _mbglOfflineRegion->getDefinition();
NSAssert([MGLTilePyramidOfflineRegion conformsToProtocol:@protocol(MGLOfflineRegion_Private)], @"MGLTilePyramidOfflineRegion should conform to MGLOfflineRegion_Private.");
- return [(id <MGLOfflineRegion_Private>)[MGLTilePyramidOfflineRegion alloc] initWithOfflineRegionDefinition:regionDefinition];
+ NSAssert([MGLShapeOfflineRegion conformsToProtocol:@protocol(MGLOfflineRegion_Private)], @"MGLShapeOfflineRegion should conform to MGLOfflineRegion_Private.");
+
+
+
+ return regionDefinition.match(
+ [&] (const mbgl::OfflineTilePyramidRegionDefinition def){
+ return (id <MGLOfflineRegion>)[[MGLTilePyramidOfflineRegion alloc] initWithOfflineRegionDefinition:def];
+ },
+ [&] (const mbgl::OfflineGeometryRegionDefinition& def){
+ return (id <MGLOfflineRegion>)[[MGLShapeOfflineRegion alloc] initWithOfflineRegionDefinition:def];
+ });
}
- (NSData *)context {
diff --git a/platform/darwin/src/MGLOfflineRegion.h b/platform/darwin/src/MGLOfflineRegion.h
index fe0ab6cb7f..3e0f485e2c 100644
--- a/platform/darwin/src/MGLOfflineRegion.h
+++ b/platform/darwin/src/MGLOfflineRegion.h
@@ -4,12 +4,21 @@ NS_ASSUME_NONNULL_BEGIN
/**
An object conforming to the `MGLOfflineRegion` protocol determines which
- resources are required by an `MGLOfflinePack` object. At present, only
- instances of `MGLTilePyramidOfflineRegion` may be used as `MGLOfflinePack`
- regions, but additional conforming implementations may be added in the future.
+ resources are required by an `MGLOfflinePack` object.
*/
@protocol MGLOfflineRegion <NSObject>
+/**
+ URL of the style whose resources are required for offline viewing.
+
+ In addition to the JSON stylesheet, different styles may require different font
+ glyphs, sprite sheets, and other resources.
+
+ The URL may be a full HTTP or HTTPS URL or a Mapbox URL indicating the style’s
+ map ID (`mapbox://styles/{user}/{style}`).
+ */
+@property (nonatomic, readonly) NSURL *styleURL;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/src/MGLOfflineRegion_Private.h b/platform/darwin/src/MGLOfflineRegion_Private.h
index b1dec8dd64..c1f3fd5200 100644
--- a/platform/darwin/src/MGLOfflineRegion_Private.h
+++ b/platform/darwin/src/MGLOfflineRegion_Private.h
@@ -9,15 +9,6 @@ NS_ASSUME_NONNULL_BEGIN
@protocol MGLOfflineRegion_Private <MGLOfflineRegion>
/**
- Initializes and returns an offline region backed by the given C++ region
- definition object.
-
- @param definition A reference to an offline region definition backing the
- offline region.
- */
-- (instancetype)initWithOfflineRegionDefinition:(const mbgl::OfflineRegionDefinition &)definition;
-
-/**
Creates and returns a C++ offline region definition corresponding to the
receiver.
*/
diff --git a/platform/darwin/src/MGLOfflineStorage.mm b/platform/darwin/src/MGLOfflineStorage.mm
index 05e1b06338..93a6da36c4 100644
--- a/platform/darwin/src/MGLOfflineStorage.mm
+++ b/platform/darwin/src/MGLOfflineStorage.mm
@@ -285,7 +285,7 @@ const MGLExceptionName MGLUnsupportedRegionTypeException = @"MGLUnsupportedRegio
return;
}
- const mbgl::OfflineTilePyramidRegionDefinition regionDefinition = [(id <MGLOfflineRegion_Private>)region offlineRegionDefinition];
+ const mbgl::OfflineRegionDefinition regionDefinition = [(id <MGLOfflineRegion_Private>)region offlineRegionDefinition];
mbgl::OfflineRegionMetadata metadata(context.length);
[context getBytes:&metadata[0] length:metadata.size()];
self.mbglFileSource->createOfflineRegion(regionDefinition, metadata, [&, completion](mbgl::expected<mbgl::OfflineRegion, std::exception_ptr> mbglOfflineRegion) {
diff --git a/platform/darwin/src/MGLShapeOfflineRegion.h b/platform/darwin/src/MGLShapeOfflineRegion.h
new file mode 100644
index 0000000000..ac54dc137b
--- /dev/null
+++ b/platform/darwin/src/MGLShapeOfflineRegion.h
@@ -0,0 +1,72 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLFoundation.h"
+#import "MGLOfflineRegion.h"
+#import "MGLShape.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ An offline region defined by a style URL, geographic shape, and
+ range of zoom levels.
+
+ This class requires fewer resources than MGLTilePyramidOfflineRegion
+ for irregularly shaped regions.
+ */
+MGL_EXPORT
+@interface MGLShapeOfflineRegion : NSObject <MGLOfflineRegion, NSSecureCoding, NSCopying>
+
+/**
+ The shape for the geographic region covered by the downloaded
+ tiles.
+ */
+@property (nonatomic, readonly) MGLShape *shape;
+
+/**
+ The minimum zoom level for which to download tiles and other resources.
+
+ For more information about zoom levels, `-[MGLMapView zoomLevel]`.
+ */
+@property (nonatomic, readonly) double minimumZoomLevel;
+
+/**
+ The maximum zoom level for which to download tiles and other resources.
+
+ For more information about zoom levels, `-[MGLMapView zoomLevel]`.
+ */
+@property (nonatomic, readonly) double maximumZoomLevel;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ Initializes a newly created offline region with the given style URL, geometry,
+ and range of zoom levels.
+
+ This is the designated initializer for `MGLShapeOfflineRegion`.
+
+ @param styleURL URL of the map style for which to download resources. The URL
+ may be a full HTTP or HTTPS URL or a Mapbox URL indicating the style’s map
+ ID (`mapbox://styles/{user}/{style}`). Specify `nil` for the default style.
+ Relative file URLs cannot be used as offline style URLs. To download the
+ online resources required by a local style, specify a URL to an online copy
+ of the style.
+ @param shape The shape of the geographic region to be covered by
+ the downloaded tiles.
+ @param minimumZoomLevel The minimum zoom level to be covered by the downloaded
+ tiles. This parameter should be set to at least 0 but no greater than the
+ value of the `maximumZoomLevel` parameter. For each required tile source, if
+ this parameter is set to a value less than the tile source’s minimum zoom
+ level, the download covers zoom levels down to the tile source’s minimum
+ zoom level.
+ @param maximumZoomLevel The maximum zoom level to be covered by the downloaded
+ tiles. This parameter should be set to at least the value of the
+ `minimumZoomLevel` parameter. For each required tile source, if this
+ parameter is set to a value greater than the tile source’s minimum zoom
+ level, the download covers zoom levels up to the tile source’s maximum zoom
+ level.
+ */
+- (instancetype)initWithStyleURL:(nullable NSURL *)styleURL shape:(MGLShape *)shape fromZoomLevel:(double)minimumZoomLevel toZoomLevel:(double)maximumZoomLevel NS_DESIGNATED_INITIALIZER;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/src/MGLShapeOfflineRegion.mm b/platform/darwin/src/MGLShapeOfflineRegion.mm
new file mode 100644
index 0000000000..e1393f1199
--- /dev/null
+++ b/platform/darwin/src/MGLShapeOfflineRegion.mm
@@ -0,0 +1,120 @@
+#import "MGLShapeOfflineRegion.h"
+
+#if !TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
+ #import <Cocoa/Cocoa.h>
+#else
+ #import <UIKit/UIKit.h>
+#endif
+
+#import "MGLOfflineRegion_Private.h"
+#import "MGLShapeOfflineRegion_Private.h"
+#import "MGLFeature_Private.h"
+#import "MGLShape_Private.h"
+#import "MGLStyle.h"
+
+@interface MGLShapeOfflineRegion () <MGLOfflineRegion_Private, MGLShapeOfflineRegion_Private>
+
+@end
+
+@implementation MGLShapeOfflineRegion {
+ NSURL *_styleURL;
+}
+
+@synthesize styleURL = _styleURL;
+
++ (BOOL)supportsSecureCoding {
+ return YES;
+}
+
+- (instancetype)init {
+ [NSException raise:@"Method unavailable"
+ format:
+ @"-[MGLShapeOfflineRegion init] is unavailable. "
+ @"Use -initWithStyleURL:shape:fromZoomLevel:toZoomLevel: instead."];
+ return nil;
+}
+
+- (instancetype)initWithStyleURL:(NSURL *)styleURL shape:(MGLShape *)shape fromZoomLevel:(double)minimumZoomLevel toZoomLevel:(double)maximumZoomLevel {
+ if (self = [super init]) {
+ if (!styleURL) {
+ styleURL = [MGLStyle streetsStyleURLWithVersion:MGLStyleDefaultVersion];
+ }
+
+ if (!styleURL.scheme) {
+ [NSException raise:@"Invalid style URL" format:
+ @"%@ does not support setting a relative file URL as the style URL. "
+ @"To download the online resources required by this style, "
+ @"specify a URL to an online copy of this style. "
+ @"For Mapbox-hosted styles, use the mapbox: scheme.",
+ NSStringFromClass([self class])];
+ }
+
+ _styleURL = styleURL;
+ _shape = shape;
+ _minimumZoomLevel = minimumZoomLevel;
+ _maximumZoomLevel = maximumZoomLevel;
+ }
+ return self;
+}
+
+- (instancetype)initWithOfflineRegionDefinition:(const mbgl::OfflineGeometryRegionDefinition &)definition {
+ NSURL *styleURL = [NSURL URLWithString:@(definition.styleURL.c_str())];
+ MGLShape *shape = MGLShapeFromGeoJSON(definition.geometry);
+ return [self initWithStyleURL:styleURL shape:shape 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::OfflineGeometryRegionDefinition(_styleURL.absoluteString.UTF8String,
+ _shape.geometryObject,
+ _minimumZoomLevel, _maximumZoomLevel,
+ scaleFactor);
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)coder {
+ NSURL *styleURL = [coder decodeObjectForKey:@"styleURL"];
+ MGLShape * shape = [coder decodeObjectForKey:@"shape"];
+ double minimumZoomLevel = [coder decodeDoubleForKey:@"minimumZoomLevel"];
+ double maximumZoomLevel = [coder decodeDoubleForKey:@"maximumZoomLevel"];
+
+ return [self initWithStyleURL:styleURL shape:shape fromZoomLevel:minimumZoomLevel toZoomLevel:maximumZoomLevel];
+}
+
+- (void)encodeWithCoder:(NSCoder *)coder
+{
+ [coder encodeObject:_styleURL forKey:@"styleURL"];
+ [coder encodeObject:_shape forKey:@"shape"];
+ [coder encodeDouble:_maximumZoomLevel forKey:@"maximumZoomLevel"];
+ [coder encodeDouble:_minimumZoomLevel forKey:@"minimumZoomLevel"];
+}
+
+- (id)copyWithZone:(nullable NSZone *)zone {
+ return [[[self class] allocWithZone:zone] initWithStyleURL:_styleURL shape:_shape fromZoomLevel:_minimumZoomLevel toZoomLevel:_maximumZoomLevel];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isKindOfClass:[self class]]) {
+ return NO;
+ }
+
+ MGLShapeOfflineRegion *otherRegion = other;
+ return (_minimumZoomLevel == otherRegion->_minimumZoomLevel
+ && _maximumZoomLevel == otherRegion->_maximumZoomLevel
+ && _shape.geometryObject == otherRegion->_shape.geometryObject
+ && [_styleURL isEqual:otherRegion->_styleURL]);
+}
+
+- (NSUInteger)hash {
+ return (_styleURL.hash
+ + _shape.hash
+ + @(_minimumZoomLevel).hash + @(_maximumZoomLevel).hash);
+}
+
+@end
diff --git a/platform/darwin/src/MGLShapeOfflineRegion_Private.h b/platform/darwin/src/MGLShapeOfflineRegion_Private.h
new file mode 100644
index 0000000000..2ab44ad405
--- /dev/null
+++ b/platform/darwin/src/MGLShapeOfflineRegion_Private.h
@@ -0,0 +1,22 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLOfflineRegion.h"
+
+#include <mbgl/storage/offline.hpp>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol MGLShapeOfflineRegion_Private <MGLOfflineRegion>
+
+/**
+ Initializes and returns an offline region backed by the given C++ region
+ definition object.
+
+ @param definition A reference to an offline region definition backing the
+ offline region.
+ */
+- (instancetype)initWithOfflineRegionDefinition:(const mbgl::OfflineGeometryRegionDefinition &)definition;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/src/MGLTilePyramidOfflineRegion.h b/platform/darwin/src/MGLTilePyramidOfflineRegion.h
index 31e5a41920..4fbb68dbc6 100644
--- a/platform/darwin/src/MGLTilePyramidOfflineRegion.h
+++ b/platform/darwin/src/MGLTilePyramidOfflineRegion.h
@@ -9,22 +9,14 @@ NS_ASSUME_NONNULL_BEGIN
/**
An offline region defined by a style URL, geographic coordinate bounds, and
range of zoom levels.
+
+ To minimize the resources required by an irregularly shaped offline region,
+ use the MGLShapeOfflineRegion class instead.
*/
MGL_EXPORT
@interface MGLTilePyramidOfflineRegion : NSObject <MGLOfflineRegion, NSSecureCoding, NSCopying>
/**
- URL of the style whose resources are required for offline viewing.
-
- In addition to the JSON stylesheet, different styles may require different font
- glyphs, sprite sheets, and other resources.
-
- The URL may be a full HTTP or HTTPS URL or a Mapbox URL indicating the style’s
- map ID (`mapbox://styles/{user}/{style}`).
- */
-@property (nonatomic, readonly) NSURL *styleURL;
-
-/**
The coordinate bounds for the geographic region covered by the downloaded
tiles.
*/
diff --git a/platform/darwin/src/MGLTilePyramidOfflineRegion.mm b/platform/darwin/src/MGLTilePyramidOfflineRegion.mm
index 7333703267..0766d224da 100644
--- a/platform/darwin/src/MGLTilePyramidOfflineRegion.mm
+++ b/platform/darwin/src/MGLTilePyramidOfflineRegion.mm
@@ -5,10 +5,11 @@
#endif
#import "MGLOfflineRegion_Private.h"
+#import "MGLTilePyramidOfflineRegion_Private.h"
#import "MGLGeometry_Private.h"
#import "MGLStyle.h"
-@interface MGLTilePyramidOfflineRegion () <MGLOfflineRegion_Private>
+@interface MGLTilePyramidOfflineRegion () <MGLOfflineRegion_Private, MGLTilePyramidOfflineRegion_Private>
@end
@@ -52,7 +53,7 @@
return self;
}
-- (instancetype)initWithOfflineRegionDefinition:(const mbgl::OfflineRegionDefinition &)definition {
+- (instancetype)initWithOfflineRegionDefinition:(const mbgl::OfflineTilePyramidRegionDefinition &)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];
diff --git a/platform/darwin/src/MGLTilePyramidOfflineRegion_Private.h b/platform/darwin/src/MGLTilePyramidOfflineRegion_Private.h
new file mode 100644
index 0000000000..90d8e05477
--- /dev/null
+++ b/platform/darwin/src/MGLTilePyramidOfflineRegion_Private.h
@@ -0,0 +1,22 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLOfflineRegion.h"
+
+#include <mbgl/storage/offline.hpp>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol MGLTilePyramidOfflineRegion_Private <MGLOfflineRegion>
+
+/**
+ Initializes and returns an offline region backed by the given C++ region
+ definition object.
+
+ @param definition A reference to an offline region definition backing the
+ offline region.
+ */
+- (instancetype)initWithOfflineRegionDefinition:(const mbgl::OfflineTilePyramidRegionDefinition &)definition;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/test/MGLOfflineRegionTests.m b/platform/darwin/test/MGLOfflineRegionTests.m
index da9928741b..eac6da9b54 100644
--- a/platform/darwin/test/MGLOfflineRegionTests.m
+++ b/platform/darwin/test/MGLOfflineRegionTests.m
@@ -17,7 +17,7 @@
XCTAssertThrowsSpecificNamed([[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:localURL bounds:bounds fromZoomLevel:0 toZoomLevel:DBL_MAX], NSException, MGLInvalidStyleURLException, @"No exception raised when initializing region with a local file URL as the style URL.");
}
-- (void)testEquality {
+- (void)testTilePyramidRegionEquality {
MGLCoordinateBounds bounds = MGLCoordinateBoundsMake(kCLLocationCoordinate2DInvalid, kCLLocationCoordinate2DInvalid);
MGLTilePyramidOfflineRegion *original = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:[MGLStyle lightStyleURLWithVersion:MGLStyleDefaultVersion] bounds:bounds fromZoomLevel:5 toZoomLevel:10];
MGLTilePyramidOfflineRegion *copy = [original copy];
@@ -29,4 +29,20 @@
XCTAssertEqual(original.maximumZoomLevel, original.maximumZoomLevel, @"Maximum zoom level has changed.");
}
+- (void)testGeometryRegionEquality {
+ NSString *geojson = @"{\"type\": \"Point\", \"coordinates\": [-3.8671874999999996, 52.482780222078226] }";
+ NSError *error;
+ MGLShape *shape = [MGLShape shapeWithData: [geojson dataUsingEncoding:NSUTF8StringEncoding] encoding: NSUTF8StringEncoding error:&error];
+ XCTAssertNil(error);
+
+ MGLShapeOfflineRegion *original = [[MGLShapeOfflineRegion alloc] initWithStyleURL:[MGLStyle lightStyleURLWithVersion:MGLStyleDefaultVersion] shape:shape fromZoomLevel:5 toZoomLevel:10];
+ MGLShapeOfflineRegion *copy = [original copy];
+ XCTAssertEqualObjects(original, copy, @"Shape region should be equal to its copy.");
+
+ XCTAssertEqualObjects(original.styleURL, copy.styleURL, @"Style URL has changed.");
+ XCTAssertEqualObjects(original.shape, copy.shape, @"Geometry has changed.");
+ XCTAssertEqual(original.minimumZoomLevel, original.minimumZoomLevel, @"Minimum zoom level has changed.");
+ XCTAssertEqual(original.maximumZoomLevel, original.maximumZoomLevel, @"Maximum zoom level has changed.");
+}
+
@end
diff --git a/platform/darwin/test/MGLOfflineStorageTests.mm b/platform/darwin/test/MGLOfflineStorageTests.mm
index 28c6633028..e9e2467f21 100644
--- a/platform/darwin/test/MGLOfflineStorageTests.mm
+++ b/platform/darwin/test/MGLOfflineStorageTests.mm
@@ -36,7 +36,7 @@
XCTAssertEqual([MGLOfflineStorage sharedOfflineStorage], [MGLOfflineStorage sharedOfflineStorage], @"There should only be one shared offline storage object.");
}
-- (void)testAddPack {
+- (void)testAddPackForBounds {
NSUInteger countOfPacks = [MGLOfflineStorage sharedOfflineStorage].packs.count;
NSURL *styleURL = [MGLStyle lightStyleURLWithVersion:8];
@@ -109,6 +109,78 @@
[self waitForExpectationsWithTimeout:1 handler:nil];
}
+- (void)testAddPackForGeometry {
+ NSUInteger countOfPacks = [MGLOfflineStorage sharedOfflineStorage].packs.count;
+
+ NSURL *styleURL = [MGLStyle lightStyleURLWithVersion:8];
+ double zoomLevel = 20;
+ NSString *geojson = @"{ \"type\": \"Polygon\", \"coordinates\": [ [ [ 5.1299285888671875, 52.10365839097971 ], [ 5.103063583374023, 52.110037078604236 ], [ 5.080232620239258, 52.09548601177304 ], [ 5.106925964355469, 52.07987524347506 ], [ 5.1299285888671875, 52.10365839097971 ] ] ]}";
+ NSError *error;
+ MGLShape *shape = [MGLShape shapeWithData: [geojson dataUsingEncoding:NSUTF8StringEncoding] encoding: NSUTF8StringEncoding error:&error];
+ XCTAssertNil(error);
+ MGLShapeOfflineRegion *region = [[MGLShapeOfflineRegion alloc] initWithStyleURL:styleURL shape:shape fromZoomLevel:zoomLevel toZoomLevel:zoomLevel];
+
+
+ NSString *nameKey = @"Name";
+ NSString *name = @"Utrecht centrum";
+
+ NSData *context = [NSKeyedArchiver archivedDataWithRootObject:@{nameKey: name}];
+
+ __block MGLOfflinePack *pack;
+ [self keyValueObservingExpectationForObject:[MGLOfflineStorage sharedOfflineStorage] keyPath:@"packs" handler:^BOOL(id _Nonnull observedObject, NSDictionary * _Nonnull change) {
+ const auto changeKind = static_cast<NSKeyValueChange>([change[NSKeyValueChangeKindKey] unsignedLongValue]);
+ NSIndexSet *indices = change[NSKeyValueChangeIndexesKey];
+ return changeKind == NSKeyValueChangeInsertion && indices.count == 1;
+ }];
+ XCTestExpectation *additionCompletionHandlerExpectation = [self expectationWithDescription:@"add pack completion handler"];
+ [[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack * _Nullable completionHandlerPack, NSError * _Nullable error) {
+ XCTAssertNotNil(completionHandlerPack, @"Added pack should exist.");
+ XCTAssertEqual(completionHandlerPack.state, MGLOfflinePackStateInactive, @"New pack should initially have inactive state.");
+ pack = completionHandlerPack;
+ [additionCompletionHandlerExpectation fulfill];
+ }];
+ [self waitForExpectationsWithTimeout:2 handler:nil];
+
+ XCTAssertEqual([MGLOfflineStorage sharedOfflineStorage].packs.count, countOfPacks + 1, @"Added pack should have been added to the canonical collection of packs owned by the shared offline storage object. This assertion can fail if this test is run before -testAAALoadPacks.");
+
+ XCTAssertEqual(pack, [MGLOfflineStorage sharedOfflineStorage].packs.lastObject, @"Pack should be appended to end of packs array.");
+
+ XCTAssertEqualObjects(pack.region, region, @"Added pack’s region has changed.");
+
+ NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:pack.context];
+ XCTAssert([userInfo isKindOfClass:[NSDictionary class]], @"Context of offline pack isn’t a dictionary.");
+ XCTAssert([userInfo[nameKey] isKindOfClass:[NSString class]], @"Name of offline pack isn’t a string.");
+ XCTAssertEqualObjects(userInfo[nameKey], name, @"Name of offline pack has changed.");
+
+ XCTAssertEqual(pack.state, MGLOfflinePackStateInactive, @"New pack should initially have inactive state.");
+
+ [self keyValueObservingExpectationForObject:pack keyPath:@"state" handler:^BOOL(id _Nonnull observedObject, NSDictionary * _Nonnull change) {
+ const auto changeKind = static_cast<NSKeyValueChange>([change[NSKeyValueChangeKindKey] unsignedLongValue]);
+ const auto state = static_cast<MGLOfflinePackState>([change[NSKeyValueChangeNewKey] longValue]);
+ return changeKind == NSKeyValueChangeSetting && state == MGLOfflinePackStateInactive;
+ }];
+ [self expectationForNotification:MGLOfflinePackProgressChangedNotification object:pack handler:^BOOL(NSNotification * _Nonnull notification) {
+ MGLOfflinePack *notificationPack = notification.object;
+ XCTAssert([notificationPack isKindOfClass:[MGLOfflinePack class]], @"Object of notification should be an MGLOfflinePack.");
+
+ NSDictionary *userInfo = notification.userInfo;
+ XCTAssertNotNil(userInfo, @"Progress change notification should have a userInfo dictionary.");
+
+ NSNumber *stateNumber = userInfo[MGLOfflinePackUserInfoKeyState];
+ XCTAssert([stateNumber isKindOfClass:[NSNumber class]], @"Progress change notification’s state should be an NSNumber.");
+ XCTAssertEqual(stateNumber.integerValue, pack.state, @"State in a progress change notification should match the pack’s state.");
+
+ NSValue *progressValue = userInfo[MGLOfflinePackUserInfoKeyProgress];
+ XCTAssert([progressValue isKindOfClass:[NSValue class]], @"Progress change notification’s progress should be an NSValue.");
+ XCTAssertEqualObjects(progressValue, [NSValue valueWithMGLOfflinePackProgress:pack.progress], @"Progress change notification’s progress should match pack’s progress.");
+
+ return notificationPack == pack && pack.state == MGLOfflinePackStateInactive;
+ }];
+ [pack requestProgress];
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+ pack = nil;
+}
+
- (void)testBackupExclusion {
NSURL *cacheDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory
inDomain:NSUserDomainMask