From 78f93708d66d99da71e3292c32f34cc4e46ecc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 28 Mar 2016 00:05:58 -0700 Subject: [PATCH] [osx] Offline pack manager in osxapp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added an offline pack download manager to osxapp to demonstrate the use of MGLOfflineStorage with Cocoa Bindings. Like the Offline Packs view controller in iosapp, new packs are added based on the foremost map view’s current state. Double-clicking a complete offline pack opens a new map window to the pack’s bounds. --- platform/osx/app/AppDelegate.h | 2 +- platform/osx/app/AppDelegate.m | 96 +++++++++ platform/osx/app/MainMenu.xib | 202 +++++++++++++++++- platform/osx/app/MapDocument.m | 38 ++++ .../osx/app/OfflinePackNameValueTransformer.h | 5 + .../osx/app/OfflinePackNameValueTransformer.m | 33 +++ platform/osx/app/mapboxgl-app.gypi | 2 + 7 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 platform/osx/app/OfflinePackNameValueTransformer.h create mode 100644 platform/osx/app/OfflinePackNameValueTransformer.m 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',