diff --git a/platform/osx/app/AppDelegate.h b/platform/osx/app/AppDelegate.h index 45d389f54..ca9edab77 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 b54ab4fe7..186a3a1ab 100644 --- a/platform/osx/app/AppDelegate.m +++ b/platform/osx/app/AppDelegate.m @@ -7,8 +7,58 @@ 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; + @end @implementation AppDelegate @@ -63,6 +113,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 +131,8 @@ NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask"; [[NSUserDefaults standardUserDefaults] setInteger:mapView.debugMask forKey:MGLLastMapDebugMaskDefaultsKey]; } } + + [self.offlinePacksArrayController unbind:@"content"]; } #pragma mark Services @@ -121,6 +175,45 @@ NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask"; [[NSDocumentController sharedDocumentController] openUntitledDocumentAndDisplay:YES error:NULL]; } +#pragma mark Offline pack management + +- (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 +247,9 @@ NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask"; if (menuItem.action == @selector(showPreferences:)) { 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 64ff4e550..c408bdc56 100644 --- a/platform/osx/app/MainMenu.xib +++ b/platform/osx/app/MainMenu.xib @@ -1,7 +1,7 @@ - + - + @@ -17,6 +17,7 @@ + @@ -113,6 +114,12 @@ + + + + + + @@ -516,6 +523,13 @@ + + + + + + + @@ -551,7 +565,7 @@ - + @@ -609,8 +623,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OfflinePackNameValueTransformer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + context + countOfResourcesCompleted + countOfResourcesExpected + countOfBytesCompleted + stateImage + + + + diff --git a/platform/osx/app/MapDocument.m b/platform/osx/app/MapDocument.m index 7c42bc980..16d15c4ff 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 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 000000000..11fe3ff44 --- /dev/null +++ b/platform/osx/app/OfflinePackNameValueTransformer.h @@ -0,0 +1,5 @@ +#import + +@interface OfflinePackNameValueTransformer : NSValueTransformer + +@end diff --git a/platform/osx/app/OfflinePackNameValueTransformer.m b/platform/osx/app/OfflinePackNameValueTransformer.m new file mode 100644 index 000000000..2825e48ed --- /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 7b39f5d9e..8b8cc1727 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',