diff options
author | John Firebaugh <john.firebaugh@gmail.com> | 2016-03-30 17:01:54 -0700 |
---|---|---|
committer | John Firebaugh <john.firebaugh@gmail.com> | 2016-03-30 17:43:37 -0700 |
commit | 5eda74a514964d1cac684483bafa08d458175f9a (patch) | |
tree | a38f886f5742d52a915c3c72959a5eba50b9fa0e /platform/osx | |
parent | b6a181097c3e7c3168be3575d5d7e95820fc74ba (diff) | |
parent | 7b5a1ca1670a0346cdbf2af689fabde4e70ed561 (diff) | |
download | qtlocation-mapboxgl-5eda74a514964d1cac684483bafa08d458175f9a.tar.gz |
Merge branch 'release-ios-3.2.0-android-4.0.0'
Diffstat (limited to 'platform/osx')
-rw-r--r-- | platform/osx/app/AppDelegate.h | 2 | ||||
-rw-r--r-- | platform/osx/app/AppDelegate.m | 108 | ||||
-rw-r--r-- | platform/osx/app/MainMenu.xib | 203 | ||||
-rw-r--r-- | platform/osx/app/MapDocument.m | 38 | ||||
-rw-r--r-- | platform/osx/app/OfflinePackNameValueTransformer.h | 5 | ||||
-rw-r--r-- | platform/osx/app/OfflinePackNameValueTransformer.m | 33 | ||||
-rw-r--r-- | platform/osx/app/mapboxgl-app.gypi | 2 | ||||
-rw-r--r-- | platform/osx/src/MGLMapView.mm | 62 | ||||
-rw-r--r-- | platform/osx/test/MGLOfflinePackTests.m | 40 | ||||
-rw-r--r-- | platform/osx/test/MGLOfflineRegionTests.m | 35 | ||||
-rw-r--r-- | platform/osx/test/MGLOfflineStorageTests.m | 126 | ||||
-rw-r--r-- | platform/osx/test/osxtest.gypi | 3 |
12 files changed, 611 insertions, 46 deletions
diff --git a/platform/osx/app/AppDelegate.h b/platform/osx/app/AppDelegate.h index 45d389f546..ca9edab773 100644 --- a/platform/osx/app/AppDelegate.h +++ b/platform/osx/app/AppDelegate.h @@ -15,8 +15,8 @@ extern NSString * const MGLMapboxAccessTokenDefaultsKey; @property (assign) double pendingZoomLevel; @property (copy) MGLMapCamera *pendingCamera; +@property (assign) MGLCoordinateBounds pendingVisibleCoordinateBounds; @property (copy) NSURL *pendingStyleURL; @property (assign) MGLMapDebugMaskOptions pendingDebugMask; @end - diff --git a/platform/osx/app/AppDelegate.m b/platform/osx/app/AppDelegate.m index b54ab4fe74..d9fd967410 100644 --- a/platform/osx/app/AppDelegate.m +++ b/platform/osx/app/AppDelegate.m @@ -7,8 +7,59 @@ NSString * const MGLLastMapCameraDefaultsKey = @"MGLLastMapCamera"; NSString * const MGLLastMapStyleURLDefaultsKey = @"MGLLastMapStyleURL"; NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask"; +/** + Some convenience methods to make offline pack properties easier to bind to. + */ +@implementation MGLOfflinePack (Additions) + ++ (NSSet *)keyPathsForValuesAffectingStateImage { + return [NSSet setWithObjects:@"state", nil]; +} + +- (NSImage *)stateImage { + switch (self.state) { + case MGLOfflinePackStateComplete: + return [NSImage imageNamed:@"NSMenuOnStateTemplate"]; + + case MGLOfflinePackStateActive: + return [NSImage imageNamed:@"NSFollowLinkFreestandingTemplate"]; + + default: + return nil; + } +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfResourcesCompleted { + return [NSSet setWithObjects:@"progress", nil]; +} + +- (uint64_t)countOfResourcesCompleted { + return self.progress.countOfResourcesCompleted; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfResourcesExpected { + return [NSSet setWithObjects:@"progress", nil]; +} + +- (uint64_t)countOfResourcesExpected { + return self.progress.countOfResourcesExpected; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfBytesCompleted { + return [NSSet setWithObjects:@"progress", nil]; +} + +- (uint64_t)countOfBytesCompleted { + return self.progress.countOfBytesCompleted; +} + +@end + @interface AppDelegate () +@property (weak) IBOutlet NSArrayController *offlinePacksArrayController; +@property (weak) IBOutlet NSPanel *offlinePacksPanel; + @end @implementation AppDelegate @@ -63,6 +114,8 @@ NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask"; [alert runModal]; [self showPreferences:nil]; } + + [self.offlinePacksArrayController bind:@"content" toObject:[MGLOfflineStorage sharedOfflineStorage] withKeyPath:@"packs" options:nil]; } - (void)applicationWillTerminate:(NSNotification *)notification { @@ -79,6 +132,8 @@ NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask"; [[NSUserDefaults standardUserDefaults] setInteger:mapView.debugMask forKey:MGLLastMapDebugMaskDefaultsKey]; } } + + [self.offlinePacksArrayController unbind:@"content"]; } #pragma mark Services @@ -121,6 +176,53 @@ NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask"; [[NSDocumentController sharedDocumentController] openUntitledDocumentAndDisplay:YES error:NULL]; } +#pragma mark Offline pack management + +- (IBAction)showOfflinePacksPanel:(id)sender { + [self.offlinePacksPanel makeKeyAndOrderFront:sender]; + + for (MGLOfflinePack *pack in self.offlinePacksArrayController.arrangedObjects) { + [pack requestProgress]; + } +} + +- (IBAction)delete:(id)sender { + for (MGLOfflinePack *pack in self.offlinePacksArrayController.selectedObjects) { + [[MGLOfflineStorage sharedOfflineStorage] removePack:pack withCompletionHandler:^(NSError * _Nullable error) { + if (error) { + [[NSAlert alertWithError:error] runModal]; + } + }]; + } +} + +- (IBAction)chooseOfflinePack:(id)sender { + for (MGLOfflinePack *pack in self.offlinePacksArrayController.selectedObjects) { + switch (pack.state) { + case MGLOfflinePackStateComplete: + { + if ([pack.region isKindOfClass:[MGLTilePyramidOfflineRegion class]]) { + MGLTilePyramidOfflineRegion *region = (MGLTilePyramidOfflineRegion *)pack.region; + self.pendingVisibleCoordinateBounds = region.bounds; + [[NSDocumentController sharedDocumentController] openUntitledDocumentAndDisplay:YES error:NULL]; + } + break; + } + + case MGLOfflinePackStateInactive: + [pack resume]; + break; + + case MGLOfflinePackStateActive: + [pack suspend]; + break; + + default: + break; + } + } +} + #pragma mark Help methods - (IBAction)showShortcuts:(id)sender { @@ -154,6 +256,12 @@ NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask"; if (menuItem.action == @selector(showPreferences:)) { return YES; } + if (menuItem.action == @selector(showOfflinePacksPanel:)) { + return YES; + } + if (menuItem.action == @selector(delete:)) { + return self.offlinePacksArrayController.selectedObjects.count; + } return NO; } diff --git a/platform/osx/app/MainMenu.xib b/platform/osx/app/MainMenu.xib index 64ff4e550d..646c4ae40d 100644 --- a/platform/osx/app/MainMenu.xib +++ b/platform/osx/app/MainMenu.xib @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> -<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10109" systemVersion="15E39d" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10116" systemVersion="15E65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <dependencies> - <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10109"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10116"/> </dependencies> <objects> <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> @@ -17,6 +17,8 @@ </customObject> <customObject id="Voe-Tx-rLC" customClass="AppDelegate"> <connections> + <outlet property="offlinePacksArrayController" destination="dWe-R6-sRz" id="Ar5-xu-ABm"/> + <outlet property="offlinePacksPanel" destination="Jjv-gs-Tx6" id="0vK-rR-3ZX"/> <outlet property="preferencesWindow" destination="UWc-yQ-qda" id="Ota-aT-Mz2"/> </connections> </customObject> @@ -113,6 +115,12 @@ <action selector="saveDocumentAs:" target="-1" id="mDf-zr-I0C"/> </connections> </menuItem> + <menuItem title="Save Offline Pack…" id="UXB-sj-C7i"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="addOfflinePack:" target="-1" id="Usu-xO-QEx"/> + </connections> + </menuItem> <menuItem title="Revert to Saved" id="KaW-ft-85H"> <modifierMask key="keyEquivalentModifierMask"/> <connections> @@ -516,6 +524,13 @@ <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/> </connections> </menuItem> + <menuItem isSeparatorItem="YES" id="Uix-g7-fAt"/> + <menuItem title="Offline Packs" id="YW3-jR-knj"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="showOfflinePacksPanel:" target="Voe-Tx-rLC" id="kj9-ht-KmF"/> + </connections> + </menuItem> <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/> <menuItem title="Bring All to Front" id="LE2-aR-0XJ"> <modifierMask key="keyEquivalentModifierMask"/> @@ -551,7 +566,7 @@ <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> <rect key="contentRect" x="109" y="131" width="350" height="62"/> - <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1050"/> + <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/> <view key="contentView" id="eA4-n3-qPe"> <rect key="frame" x="0.0" y="0.0" width="350" height="62"/> <autoresizingMask key="autoresizingMask"/> @@ -609,8 +624,190 @@ <point key="canvasLocation" x="754" y="210"/> </window> <userDefaultsController representsSharedInstance="YES" id="45S-yT-WUN"/> + <window title="Offline Packs" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="MBXOfflinePacksPanel" animationBehavior="default" id="Jjv-gs-Tx6" customClass="NSPanel"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" utility="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> + <rect key="contentRect" x="830" y="430" width="400" height="300"/> + <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/> + <view key="contentView" id="8ha-hw-zOD"> + <rect key="frame" x="0.0" y="0.0" width="400" height="300"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <scrollView autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Q8b-0e-dLv"> + <rect key="frame" x="-1" y="20" width="402" height="281"/> + <clipView key="contentView" id="J9U-Yx-o2S"> + <rect key="frame" x="1" y="0.0" width="400" height="280"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" autosaveColumns="NO" headerView="MAZ-Iq-hBi" id="Ato-Vu-HYT"> + <rect key="frame" x="0.0" y="0.0" width="400" height="257"/> + <autoresizingMask key="autoresizingMask"/> + <size key="intercellSpacing" width="3" height="2"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/> + <tableColumns> + <tableColumn identifier="" editable="NO" width="16" minWidth="10" maxWidth="3.4028234663852886e+38" id="xtw-hQ-8C5" userLabel="State"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + </tableHeaderCell> + <imageCell key="dataCell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="edU-Yw-20f"/> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.stateImage" id="2wd-1J-TZt"/> + </connections> + </tableColumn> + <tableColumn editable="NO" width="116" minWidth="40" maxWidth="1000" id="2hD-LN-h0L"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Name"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="oys-QZ-34I"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.context" id="NtD-s5-ZUq"> + <dictionary key="options"> + <string key="NSValueTransformerName">OfflinePackNameValueTransformer</string> + </dictionary> + </binding> + </connections> + </tableColumn> + <tableColumn editable="NO" width="50" minWidth="40" maxWidth="1000" id="pkI-c7-xoD"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Downloaded"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="WfC-qb-HsW"> + <numberFormatter key="formatter" formatterBehavior="default10_4" numberStyle="decimal" usesGroupingSeparator="NO" groupingSize="0" minimumIntegerDigits="0" maximumIntegerDigits="42" id="sNm-Qn-ne6"/> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfResourcesCompleted" id="mu6-Jg-GiU"/> + </connections> + </tableColumn> + <tableColumn identifier="" editable="NO" width="50" minWidth="10" maxWidth="3.4028234663852886e+38" id="Rrd-A9-jqc"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Total"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="mHy-qJ-rOA"> + <numberFormatter key="formatter" formatterBehavior="default10_4" numberStyle="decimal" usesGroupingSeparator="NO" groupingSize="0" minimumIntegerDigits="0" maximumIntegerDigits="42" id="kyx-ZP-OBH"/> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfResourcesExpected" id="mh2-k0-vvB"/> + </connections> + </tableColumn> + <tableColumn identifier="" editable="NO" width="60" minWidth="10" maxWidth="3.4028234663852886e+38" id="h7m-6l-KaS"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Size"> + <font key="font" metaFont="smallSystem"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="701-bg-k6L"> + <byteCountFormatter key="formatter" allowsNonnumericFormatting="NO" id="IXV-J9-sP3"/> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + <connections> + <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfBytesCompleted" id="Zsa-Na-yFN"/> + </connections> + </tableColumn> + </tableColumns> + <connections> + <action trigger="doubleAction" selector="chooseOfflinePack:" target="-1" id="pUN-eT-zRT"/> + </connections> + </tableView> + </subviews> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </clipView> + <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="QLr-6P-Ogs"> + <rect key="frame" x="1" y="7" width="0.0" height="16"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="q0K-eE-mzL"> + <rect key="frame" x="224" y="17" width="15" height="102"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <tableHeaderView key="headerView" id="MAZ-Iq-hBi"> + <rect key="frame" x="0.0" y="0.0" width="400" height="23"/> + <autoresizingMask key="autoresizingMask"/> + </tableHeaderView> + </scrollView> + <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wzf-ce-Spm"> + <rect key="frame" x="0.0" y="-1" width="21" height="21"/> + <constraints> + <constraint firstAttribute="width" constant="21" id="5ST-tY-8Ph"/> + </constraints> + <buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSAddTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="sew-F7-i5T"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <connections> + <action selector="addOfflinePack:" target="-1" id="SN0-PM-HoU"/> + </connections> + </button> + <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7L7-hr-zId"> + <rect key="frame" x="20" y="0.0" width="21" height="19"/> + <constraints> + <constraint firstAttribute="width" constant="21" id="JYb-AF-8gZ"/> + </constraints> + <buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSRemoveTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="oTF-3m-6qT"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + <string key="keyEquivalent" base64-UTF8="YES"> +CA +</string> + </buttonCell> + <connections> + <action selector="delete:" target="-1" id="EGL-bf-yUD"/> + </connections> + </button> + </subviews> + <constraints> + <constraint firstItem="7L7-hr-zId" firstAttribute="centerY" secondItem="wzf-ce-Spm" secondAttribute="centerY" id="7TI-6w-bf1"/> + <constraint firstAttribute="bottom" secondItem="Q8b-0e-dLv" secondAttribute="bottom" constant="20" symbolic="YES" id="DZa-ly-bhV"/> + <constraint firstItem="wzf-ce-Spm" firstAttribute="top" secondItem="Q8b-0e-dLv" secondAttribute="bottom" id="LhK-5z-CQA"/> + <constraint firstItem="Q8b-0e-dLv" firstAttribute="leading" secondItem="8ha-hw-zOD" secondAttribute="leading" constant="-1" id="Oyo-ch-rZo"/> + <constraint firstAttribute="bottom" secondItem="7L7-hr-zId" secondAttribute="bottom" id="TtY-j1-T5h"/> + <constraint firstItem="Q8b-0e-dLv" firstAttribute="top" secondItem="8ha-hw-zOD" secondAttribute="top" constant="-1" id="WDk-Ig-Grr"/> + <constraint firstAttribute="trailing" secondItem="Q8b-0e-dLv" secondAttribute="trailing" constant="-1" id="hHf-rd-Wcv"/> + <constraint firstItem="7L7-hr-zId" firstAttribute="leading" secondItem="8ha-hw-zOD" secondAttribute="leading" constant="20" symbolic="YES" id="iKJ-ph-ACS"/> + <constraint firstAttribute="bottom" secondItem="wzf-ce-Spm" secondAttribute="bottom" constant="-1" id="jFV-Xi-fWr"/> + <constraint firstItem="wzf-ce-Spm" firstAttribute="leading" secondItem="8ha-hw-zOD" secondAttribute="leading" id="kJt-oJ-72R"/> + </constraints> + </view> + <point key="canvasLocation" x="720" y="317"/> + </window> + <arrayController objectClassName="MGLOfflinePack" editable="NO" avoidsEmptySelection="NO" id="dWe-R6-sRz" userLabel="Offline Packs Array Controller"> + <declaredKeys> + <string>context</string> + <string>countOfResourcesCompleted</string> + <string>countOfResourcesExpected</string> + <string>countOfBytesCompleted</string> + <string>stateImage</string> + </declaredKeys> + </arrayController> </objects> <resources> + <image name="NSAddTemplate" width="11" height="11"/> <image name="NSFollowLinkFreestandingTemplate" width="14" height="14"/> + <image name="NSRemoveTemplate" width="11" height="11"/> </resources> </document> diff --git a/platform/osx/app/MapDocument.m b/platform/osx/app/MapDocument.m index 7c42bc9802..16d15c4ffd 100644 --- a/platform/osx/app/MapDocument.m +++ b/platform/osx/app/MapDocument.m @@ -204,6 +204,10 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { appDelegate.pendingZoomLevel = -1; appDelegate.pendingCamera = nil; } + if (!MGLCoordinateBoundsIsEmpty(appDelegate.pendingVisibleCoordinateBounds)) { + self.mapView.visibleCoordinateBounds = appDelegate.pendingVisibleCoordinateBounds; + appDelegate.pendingVisibleCoordinateBounds = (MGLCoordinateBounds){ { 0, 0 }, { 0, 0 } }; + } if (appDelegate.pendingDebugMask) { self.mapView.debugMask = appDelegate.pendingDebugMask; } @@ -350,6 +354,36 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { [self.mapView addAnnotation:line]; } +#pragma mark Offline packs + +- (IBAction)addOfflinePack:(id)sender { + NSAlert *namePrompt = [[NSAlert alloc] init]; + namePrompt.messageText = @"Add offline pack"; + namePrompt.informativeText = @"Choose a name for the pack:"; + NSTextField *nameTextField = [[NSTextField alloc] initWithFrame:NSZeroRect]; + nameTextField.placeholderString = MGLStringFromCoordinateBounds(self.mapView.visibleCoordinateBounds); + [nameTextField sizeToFit]; + NSRect textFieldFrame = nameTextField.frame; + textFieldFrame.size.width = 300; + nameTextField.frame = textFieldFrame; + namePrompt.accessoryView = nameTextField; + [namePrompt addButtonWithTitle:@"Add"]; + [namePrompt addButtonWithTitle:@"Cancel"]; + if ([namePrompt runModal] != NSAlertFirstButtonReturn) { + return; + } + + id <MGLOfflineRegion> region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:self.mapView.styleURL bounds:self.mapView.visibleCoordinateBounds fromZoomLevel:self.mapView.zoomLevel toZoomLevel:self.mapView.maximumZoomLevel]; + NSData *context = [[NSValueTransformer valueTransformerForName:@"OfflinePackNameValueTransformer"] reverseTransformedValue:nameTextField.stringValue]; + [[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack * _Nullable pack, NSError * _Nullable error) { + if (error) { + [[NSAlert alertWithError:error] runModal]; + } else { + [pack resume]; + } + }]; +} + #pragma mark Help methods - (IBAction)giveFeedback:(id)sender { @@ -500,6 +534,10 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { if (menuItem.action == @selector(drawPolygonAndPolyLineAnnotations:)) { return !_isShowingPolygonAndPolylineAnnotations; } + if (menuItem.action == @selector(addOfflinePack:)) { + NSURL *styleURL = self.mapView.styleURL; + return !styleURL.isFileURL; + } if (menuItem.action == @selector(giveFeedback:)) { return YES; } diff --git a/platform/osx/app/OfflinePackNameValueTransformer.h b/platform/osx/app/OfflinePackNameValueTransformer.h new file mode 100644 index 0000000000..11fe3ff441 --- /dev/null +++ b/platform/osx/app/OfflinePackNameValueTransformer.h @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> + +@interface OfflinePackNameValueTransformer : NSValueTransformer + +@end diff --git a/platform/osx/app/OfflinePackNameValueTransformer.m b/platform/osx/app/OfflinePackNameValueTransformer.m new file mode 100644 index 0000000000..2825e48ed3 --- /dev/null +++ b/platform/osx/app/OfflinePackNameValueTransformer.m @@ -0,0 +1,33 @@ +#import "OfflinePackNameValueTransformer.h" + +static NSString * const MBXOfflinePackContextNameKey = @"Name"; + +@implementation OfflinePackNameValueTransformer + ++ (Class)transformedValueClass { + return [NSString class]; +} + ++ (BOOL)allowsReverseTransformation { + return YES; +} + +- (NSString *)transformedValue:(NSData *)context { + NSAssert([context isKindOfClass:[NSData class]], @"Context should be NSData."); + + NSDictionary *userInfo = [NSKeyedUnarchiver unarchiveObjectWithData:context]; + NSAssert([userInfo isKindOfClass:[NSDictionary class]], @"Context of offline pack isn’t a dictionary."); + NSString *name = userInfo[MBXOfflinePackContextNameKey]; + NSAssert([name isKindOfClass:[NSString class]], @"Name of offline pack isn’t a string."); + return name; +} + +- (NSData *)reverseTransformedValue:(NSString *)name { + NSAssert([name isKindOfClass:[NSString class]], @"Name should be a string."); + + return [NSKeyedArchiver archivedDataWithRootObject:@{ + MBXOfflinePackContextNameKey: name, + }]; +} + +@end diff --git a/platform/osx/app/mapboxgl-app.gypi b/platform/osx/app/mapboxgl-app.gypi index 7b39f5d9eb..8b8cc17276 100644 --- a/platform/osx/app/mapboxgl-app.gypi +++ b/platform/osx/app/mapboxgl-app.gypi @@ -29,6 +29,8 @@ './LocationCoordinate2DTransformer.m', './MapDocument.h', './MapDocument.m', + './OfflinePackNameValueTransformer.h', + './OfflinePackNameValueTransformer.m', './TimeIntervalTransformer.h', './TimeIntervalTransformer.m', './NSValue+Additions.h', diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index 6f3cd8e329..f8bbb61d05 100644 --- a/platform/osx/src/MGLMapView.mm +++ b/platform/osx/src/MGLMapView.mm @@ -4,10 +4,11 @@ #import "MGLOpenGLLayer.h" #import "MGLStyle.h" -#import "../../darwin/src/MGLAccountManager_Private.h" #import "../../darwin/src/MGLGeometry_Private.h" #import "../../darwin/src/MGLMultiPoint_Private.h" +#import "../../darwin/src/MGLOfflineStorage_Private.h" +#import "MGLAccountManager.h" #import "MGLMapCamera.h" #import "MGLPolygon.h" #import "MGLPolyline.h" @@ -151,7 +152,6 @@ public: /// Cross-platform map view controller. mbgl::Map *_mbglMap; MGLMapViewImpl *_mbglView; - mbgl::DefaultFileSource *_mbglFileSource; NSPanGestureRecognizer *_panGestureRecognizer; NSMagnificationGestureRecognizer *_magnificationGestureRecognizer; @@ -232,36 +232,25 @@ public: // Set up cross-platform controllers and resources. _mbglView = new MGLMapViewImpl(self, [NSScreen mainScreen].backingScaleFactor); - // Place the cache in a location that can be shared among all the - // applications that embed the Mapbox OS X SDK. - NSURL *cacheDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory - inDomain:NSUserDomainMask - appropriateForURL:nil - create:YES - error:nil]; - cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent: - [[NSBundle mgl_frameworkBundle] bundleIdentifier]]; - [[NSFileManager defaultManager] createDirectoryAtURL:cacheDirectoryURL - withIntermediateDirectories:YES - attributes:nil - error:nil]; - NSURL *cacheURL = [cacheDirectoryURL URLByAppendingPathComponent:@"cache.db"]; - NSString *cachePath = cacheURL ? cacheURL.path : @""; - _mbglFileSource = new mbgl::DefaultFileSource(cachePath.UTF8String, [[[[NSBundle mainBundle] resourceURL] path] UTF8String]); - - _mbglMap = new mbgl::Map(*_mbglView, *_mbglFileSource, mbgl::MapMode::Continuous, mbgl::GLContextMode::Unique, mbgl::ConstrainMode::None); + // Delete the pre-offline ambient cache at + // ~/Library/Caches/com.mapbox.sdk.ios/cache.db. + NSURL *cachesDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory + inDomain:NSUserDomainMask + appropriateForURL:nil + create:NO + error:nil]; + cachesDirectoryURL = [cachesDirectoryURL URLByAppendingPathComponent: + [NSBundle mgl_frameworkBundle].bundleIdentifier]; + NSURL *legacyCacheURL = [cachesDirectoryURL URLByAppendingPathComponent:@"cache.db"]; + [[NSFileManager defaultManager] removeItemAtURL:legacyCacheURL error:NULL]; + + mbgl::DefaultFileSource *mbglFileSource = [MGLOfflineStorage sharedOfflineStorage].mbglFileSource; + _mbglMap = new mbgl::Map(*_mbglView, *mbglFileSource, mbgl::MapMode::Continuous, mbgl::GLContextMode::Unique, mbgl::ConstrainMode::None); // Install the OpenGL layer. Interface Builder’s synchronous drawing means // we can’t display a map, so don’t even bother to have a map layer. self.layer = _isTargetingInterfaceBuilder ? [CALayer layer] : [MGLOpenGLLayer layer]; - // 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]; - // Notify map object when network reachability status changes. MGLReachability *reachability = [MGLReachability reachabilityForInternetConnection]; reachability.reachableBlock = ^(MGLReachability *) { @@ -443,7 +432,6 @@ public: } - (void)dealloc { - [[MGLAccountManager sharedManager] removeObserver:self forKeyPath:@"accessToken"]; [self.window removeObserver:self forKeyPath:@"contentLayoutRect"]; [self.window removeObserver:self forKeyPath:@"titlebarAppearsTransparent"]; @@ -455,25 +443,15 @@ public: delete _mbglMap; _mbglMap = nullptr; } - if (_mbglFileSource) { - delete _mbglFileSource; - _mbglFileSource = nullptr; - } if (_mbglView) { delete _mbglView; _mbglView = nullptr; } } -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)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((std::string)accessToken.UTF8String); - } - } else if ([keyPath isEqualToString:@"contentLayoutRect"] || - [keyPath isEqualToString:@"titlebarAppearsTransparent"]) { +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(__unused id)object change:(__unused NSDictionary *)change context:(__unused void *)context { + if ([keyPath isEqualToString:@"contentLayoutRect"] || + [keyPath isEqualToString:@"titlebarAppearsTransparent"]) { [self adjustContentInsets]; } } @@ -2182,7 +2160,7 @@ public: convertedPoint.x, // mbgl origin is at the top-left corner. NSHeight(self.bounds) - convertedPoint.y, - }); + }).wrapped(); } - (NSRect)convertCoordinateBounds:(MGLCoordinateBounds)bounds toRectToView:(nullable NSView *)view { diff --git a/platform/osx/test/MGLOfflinePackTests.m b/platform/osx/test/MGLOfflinePackTests.m new file mode 100644 index 0000000000..41262d16c7 --- /dev/null +++ b/platform/osx/test/MGLOfflinePackTests.m @@ -0,0 +1,40 @@ +#import <Mapbox/Mapbox.h> + +#pragma clang diagnostic ignored "-Wgnu-statement-expression" +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" + +#import <XCTest/XCTest.h> + +@interface MGLOfflinePackTests : XCTestCase + +@end + +@implementation MGLOfflinePackTests + +- (void)testInvalidation { + MGLOfflinePack *invalidPack = [[MGLOfflinePack alloc] init]; + + XCTAssertEqual(invalidPack.state, MGLOfflinePackStateInvalid, @"Offline pack should be invalid when initialized independently of MGLOfflineStorage."); + + XCTAssertThrowsSpecificNamed(invalidPack.region, NSException, @"Invalid offline pack", @"Invalid offline pack should raise an exception when accessing its region."); + XCTAssertThrowsSpecificNamed(invalidPack.context, NSException, @"Invalid offline pack", @"Invalid offline pack should raise an exception when accessing its context."); + XCTAssertThrowsSpecificNamed([invalidPack resume], NSException, @"Invalid offline pack", @"Invalid offline pack should raise an exception when being resumed."); + XCTAssertThrowsSpecificNamed([invalidPack suspend], NSException, @"Invalid offline pack", @"Invalid offline pack should raise an exception when being suspended."); +} + +- (void)testProgressBoxing { + MGLOfflinePackProgress progress = { + .countOfResourcesCompleted = 1, + .countOfResourcesExpected = 2, + .countOfBytesCompleted = 7, + .maximumResourcesExpected = UINT64_MAX, + }; + MGLOfflinePackProgress roundTrippedProgress = [NSValue valueWithMGLOfflinePackProgress:progress].MGLOfflinePackProgressValue; + + XCTAssertEqual(progress.countOfResourcesCompleted, roundTrippedProgress.countOfResourcesCompleted, @"Completed resources should round-trip."); + XCTAssertEqual(progress.countOfResourcesExpected, roundTrippedProgress.countOfResourcesExpected, @"Expected resources should round-trip."); + XCTAssertEqual(progress.countOfBytesCompleted, roundTrippedProgress.countOfBytesCompleted, @"Completed bytes should round-trip."); + XCTAssertEqual(progress.maximumResourcesExpected, roundTrippedProgress.maximumResourcesExpected, @"Maximum expected resources should round-trip."); +} + +@end diff --git a/platform/osx/test/MGLOfflineRegionTests.m b/platform/osx/test/MGLOfflineRegionTests.m new file mode 100644 index 0000000000..63befdf14c --- /dev/null +++ b/platform/osx/test/MGLOfflineRegionTests.m @@ -0,0 +1,35 @@ +#import <Mapbox/Mapbox.h> + +#pragma clang diagnostic ignored "-Wgnu-statement-expression" +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" + +#import <XCTest/XCTest.h> + +@interface MGLOfflineRegionTests : XCTestCase + +@end + +@implementation MGLOfflineRegionTests + +- (void)testStyleURLs { + MGLCoordinateBounds bounds = MGLCoordinateBoundsMake(kCLLocationCoordinate2DInvalid, kCLLocationCoordinate2DInvalid); + MGLTilePyramidOfflineRegion *region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:nil bounds:bounds fromZoomLevel:0 toZoomLevel:DBL_MAX]; + XCTAssertEqualObjects(region.styleURL, [MGLStyle streetsStyleURL], @"Streets isn’t the default style."); + + NSURL *localURL = [NSURL URLWithString:@"beautiful.style"]; + XCTAssertThrowsSpecificNamed([[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:localURL bounds:bounds fromZoomLevel:0 toZoomLevel:DBL_MAX], NSException, @"Invalid style URL", @"No exception raised when initializing region with a local file URL as the style URL."); +} + +- (void)testEquality { + MGLCoordinateBounds bounds = MGLCoordinateBoundsMake(kCLLocationCoordinate2DInvalid, kCLLocationCoordinate2DInvalid); + MGLTilePyramidOfflineRegion *original = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:[MGLStyle lightStyleURL] bounds:bounds fromZoomLevel:5 toZoomLevel:10]; + MGLTilePyramidOfflineRegion *copy = [original copy]; + XCTAssertEqualObjects(original, copy, @"Tile pyramid region should be equal to its copy."); + + XCTAssertEqualObjects(original.styleURL, copy.styleURL, @"Style URL has changed."); + XCTAssert(MGLCoordinateBoundsEqualToCoordinateBounds(original.bounds, copy.bounds), @"Bounds have 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/osx/test/MGLOfflineStorageTests.m b/platform/osx/test/MGLOfflineStorageTests.m new file mode 100644 index 0000000000..8ffa1207ce --- /dev/null +++ b/platform/osx/test/MGLOfflineStorageTests.m @@ -0,0 +1,126 @@ +#import <Mapbox/Mapbox.h> + +#pragma clang diagnostic ignored "-Wgnu-statement-expression" +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" + +#import <XCTest/XCTest.h> + +@interface MGLOfflineStorageTests : XCTestCase + +@end + +@implementation MGLOfflineStorageTests + +- (void)testSharedObject { + XCTAssertEqual([MGLOfflineStorage sharedOfflineStorage], [MGLOfflineStorage sharedOfflineStorage], @"There should only be one shared offline storage object."); +} + +// This test needs to come first so it can test the initial loading of packs. +- (void)testAAALoadPacks { + XCTestExpectation *kvoExpectation = [self keyValueObservingExpectationForObject:[MGLOfflineStorage sharedOfflineStorage] keyPath:@"packs" handler:^BOOL(id _Nonnull observedObject, NSDictionary * _Nonnull change) { + NSKeyValueChange changeKind = [change[NSKeyValueChangeKindKey] unsignedIntegerValue]; + return changeKind = NSKeyValueChangeSetting; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + + XCTAssertNotNil([MGLOfflineStorage sharedOfflineStorage].packs, @"Shared offline storage object should have a non-nil collection of packs by this point."); +} + +- (void)testAddPack { + NSUInteger countOfPacks = [MGLOfflineStorage sharedOfflineStorage].packs.count; + + NSURL *styleURL = [MGLStyle lightStyleURL]; + /// Somewhere near Grape Grove, Ohio, United States. + MGLCoordinateBounds bounds = { + { 39.70358155855172, -83.69506472545841 }, + { 39.703818870225376, -83.69420641857361 }, + }; + double zoomLevel = 20; + MGLTilePyramidOfflineRegion *region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:styleURL bounds:bounds fromZoomLevel:zoomLevel toZoomLevel:zoomLevel]; + + NSString *nameKey = @"Name"; + NSString *name = @"🍇 Grape Grove"; + + NSData *context = [NSKeyedArchiver archivedDataWithRootObject:@{ + nameKey: name, + }]; + + __block MGLOfflinePack *pack; + [self keyValueObservingExpectationForObject:[MGLOfflineStorage sharedOfflineStorage] keyPath:@"packs" handler:^BOOL(id _Nonnull observedObject, NSDictionary * _Nonnull change) { + NSKeyValueChange changeKind = [change[NSKeyValueChangeKindKey] unsignedIntegerValue]; + 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:1 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) { + NSKeyValueChange changeKind = [change[NSKeyValueChangeKindKey] unsignedIntegerValue]; + MGLOfflinePackState state = [change[NSKeyValueChangeNewKey] integerValue]; + 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[MGLOfflinePackStateUserInfoKey]; + 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[MGLOfflinePackProgressUserInfoKey]; + 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]; +} + +- (void)testRemovePack { + NSUInteger countOfPacks = [MGLOfflineStorage sharedOfflineStorage].packs.count; + + MGLOfflinePack *pack = [MGLOfflineStorage sharedOfflineStorage].packs.lastObject; + XCTAssertNotNil(pack, @"Added pack should still exist."); + + XCTestExpectation *kvoExpectation = [self keyValueObservingExpectationForObject:[MGLOfflineStorage sharedOfflineStorage] keyPath:@"packs" handler:^BOOL(id _Nonnull observedObject, NSDictionary * _Nonnull change) { + NSKeyValueChange changeKind = [change[NSKeyValueChangeKindKey] unsignedIntegerValue]; + NSIndexSet *indices = change[NSKeyValueChangeIndexesKey]; + return changeKind = NSKeyValueChangeRemoval && indices.count == 1; + }]; + XCTestExpectation *completionHandlerExpectation = [self expectationWithDescription:@"remove pack completion handler"]; + [[MGLOfflineStorage sharedOfflineStorage] removePack:pack withCompletionHandler:^(NSError * _Nullable error) { + XCTAssertEqual(pack.state, MGLOfflinePackStateInvalid, @"Removed pack should be invalid in the completion handler."); + [completionHandlerExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + + XCTAssertEqual(pack.state, MGLOfflinePackStateInvalid, @"Removed pack should have been invalidated synchronously."); + + XCTAssertEqual([MGLOfflineStorage sharedOfflineStorage].packs.count, countOfPacks - 1, @"Removed pack should have been removed from the canonical collection of packs owned by the shared offline storage object. This assertion can fail if this test is run before -testAAALoadPacks or -testAddPack."); +} + +@end diff --git a/platform/osx/test/osxtest.gypi b/platform/osx/test/osxtest.gypi index 30bced31c4..6165b6fa88 100644 --- a/platform/osx/test/osxtest.gypi +++ b/platform/osx/test/osxtest.gypi @@ -44,6 +44,9 @@ 'sources': [ './MGLGeometryTests.mm', + './MGLOfflinePackTests.m', + './MGLOfflineRegionTests.m', + './MGLOfflineStorageTests.m', './MGLStyleTests.mm', ], |