diff options
author | Minh Nguyễn <mxn@1ec5.org> | 2015-12-24 20:49:17 -0800 |
---|---|---|
committer | Minh Nguyễn <mxn@1ec5.org> | 2016-01-04 09:33:00 -0800 |
commit | 0712ed904b7cb6385e840eb0d9682d69a7306bda (patch) | |
tree | 3bd5547e0325194ed1e4072f5e95ef8709540632 /platform | |
parent | 6e4eb094bf2feb9be4f5d2d73b1186aed768539b (diff) | |
download | qtlocation-mapboxgl-0712ed904b7cb6385e840eb0d9682d69a7306bda.tar.gz |
[osx] Make osxapp document-based
Allows for multiple map windows open at a time.
Diffstat (limited to 'platform')
-rw-r--r-- | platform/osx/app/AppDelegate.h | 1 | ||||
-rw-r--r-- | platform/osx/app/AppDelegate.m | 540 | ||||
-rw-r--r-- | platform/osx/app/Info.plist | 15 | ||||
-rw-r--r-- | platform/osx/app/MainMenu.xib | 120 | ||||
-rw-r--r-- | platform/osx/app/MapDocument.h | 14 | ||||
-rw-r--r-- | platform/osx/app/MapDocument.m | 568 | ||||
-rw-r--r-- | platform/osx/app/MapDocument.xib | 129 | ||||
-rw-r--r-- | platform/osx/app/mapboxgl-app.gypi | 3 |
8 files changed, 746 insertions, 644 deletions
diff --git a/platform/osx/app/AppDelegate.h b/platform/osx/app/AppDelegate.h index 69b6e0f13a..9f76ee76f9 100644 --- a/platform/osx/app/AppDelegate.h +++ b/platform/osx/app/AppDelegate.h @@ -2,6 +2,7 @@ @interface AppDelegate : NSObject <NSApplicationDelegate> +@property (weak) IBOutlet NSWindow *preferencesWindow; @end diff --git a/platform/osx/app/AppDelegate.m b/platform/osx/app/AppDelegate.m index 6aaf965357..76c5222072 100644 --- a/platform/osx/app/AppDelegate.m +++ b/platform/osx/app/AppDelegate.m @@ -1,40 +1,18 @@ #import "AppDelegate.h" -#import "DroppedPinAnnotation.h" #import "LocationCoordinate2DTransformer.h" +#import "MapDocument.h" #import "NSValue+Additions.h" #import <Mapbox/Mapbox.h> static NSString * const MGLMapboxAccessTokenDefaultsKey = @"MGLMapboxAccessToken"; -static NSString * const MGLDroppedPinAnnotationImageIdentifier = @"dropped"; -static const CLLocationCoordinate2D WorldTourDestinations[] = { - { .latitude = 38.9131982, .longitude = -77.0325453144239 }, - { .latitude = 37.7757368, .longitude = -122.4135302 }, - { .latitude = 12.9810816, .longitude = 77.6368034 }, - { .latitude = -13.15589555, .longitude = -74.2178961777998 }, -}; - -@interface AppDelegate () <NSApplicationDelegate, NSSharingServicePickerDelegate, NSMenuDelegate, MGLMapViewDelegate> - -@property (weak) IBOutlet NSWindow *window; -@property (weak) IBOutlet MGLMapView *mapView; -@property (weak) IBOutlet NSMenu *mapViewContextMenu; - -@property (weak) IBOutlet NSWindow *preferencesWindow; +@interface AppDelegate () @end -@implementation AppDelegate { - NSPoint _mouseLocationForMapViewContextMenu; - NSUInteger _droppedPinCounter; - NSNumberFormatter *_spellOutNumberFormatter; - - BOOL _showsToolTipsOnDroppedPins; - BOOL _randomizesCursorsOnDroppedPins; - BOOL _isTouringWorld; -} +@implementation AppDelegate #pragma mark Lifecycle @@ -85,16 +63,6 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { [alert runModal]; [self showPreferences:nil]; } - - NSURL *savedURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"MBXCurrentStyleURL"]; - if (savedURL) { - self.mapView.styleURL = savedURL; - } - - _spellOutNumberFormatter = [[NSNumberFormatter alloc] init]; - - NSPressGestureRecognizer *pressGestureRecognizer = [[NSPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)]; - [self.mapView addGestureRecognizer:pressGestureRecognizer]; } - (void)applicationWillTerminate:(NSNotification *)notification { @@ -106,13 +74,20 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { NSString *accessToken = [userDefaults stringForKey:MGLMapboxAccessTokenDefaultsKey]; if (![accessToken isEqualToString:[MGLAccountManager accessToken]]) { [MGLAccountManager setAccessToken:accessToken]; - [self reload:self]; + [self.mainDocument reload:self]; } } +- (MapDocument *)mainDocument { + NSDocument *mainDocument = [NSApp mainWindow].windowController.document; + return [mainDocument isKindOfClass:[MapDocument class]] ? (MapDocument *)mainDocument : nil; +} + #pragma mark Services - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + [[NSDocumentController sharedDocumentController] newDocument:self]; + NSURL *url = [NSURL URLWithString:[event paramDescriptorForKeyword:keyDirectObject].stringValue]; NS_MUTABLE_DICTIONARY_OF(NSString *, NSString *) *params = [[NSMutableDictionary alloc] init]; for (NSString *param in [url.query componentsSeparatedByString:@"&"]) { @@ -126,218 +101,20 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { if (centerString) { NSArray *coordinateValues = [centerString componentsSeparatedByString:@","]; if (coordinateValues.count == 2) { - self.mapView.centerCoordinate = CLLocationCoordinate2DMake([coordinateValues[0] doubleValue], - [coordinateValues[1] doubleValue]); + self.mainDocument.mapView.centerCoordinate = CLLocationCoordinate2DMake([coordinateValues[0] doubleValue], + [coordinateValues[1] doubleValue]); } } NSString *zoomLevelString = params[@"zoom"]; if (zoomLevelString.length) { - self.mapView.zoomLevel = zoomLevelString.doubleValue; + self.mainDocument.mapView.zoomLevel = zoomLevelString.doubleValue; } NSString *directionString = params[@"bearing"]; if (directionString.length) { - self.mapView.direction = directionString.doubleValue; - } -} - -- (IBAction)showShareMenu:(id)sender { - NSSharingServicePicker *picker = [[NSSharingServicePicker alloc] initWithItems:@[self.shareURL]]; - picker.delegate = self; - [picker showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMinYEdge]; -} - -- (NSURL *)shareURL { - NSArray *components = self.mapView.styleURL.pathComponents; - CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; - return [NSURL URLWithString: - [NSString stringWithFormat:@"https://api.mapbox.com/styles/v1/%@/%@.html?access_token=%@#%.2f/%.5f/%.5f/%.f", - components[1], components[2], [MGLAccountManager accessToken], - self.mapView.zoomLevel, centerCoordinate.latitude, centerCoordinate.longitude, self.mapView.direction]]; -} - -#pragma mark View methods - -- (IBAction)setStyle:(id)sender { - NSInteger tag; - if ([sender isKindOfClass:[NSMenuItem class]]) { - tag = [sender tag]; - } else if ([sender isKindOfClass:[NSPopUpButton class]]) { - tag = [sender selectedTag]; - } - NSURL *styleURL; - switch (tag) { - case 1: - styleURL = [MGLStyle streetsStyleURL]; - break; - case 2: - styleURL = [MGLStyle emeraldStyleURL]; - break; - case 3: - styleURL = [MGLStyle lightStyleURL]; - break; - case 4: - styleURL = [MGLStyle darkStyleURL]; - break; - case 5: - styleURL = [MGLStyle satelliteStyleURL]; - break; - case 6: - styleURL = [MGLStyle hybridStyleURL]; - break; - default: - NSAssert(NO, @"Cannot set style from control with tag %li", (long)tag); - break; + self.mainDocument.mapView.direction = directionString.doubleValue; } - self.mapView.styleURL = styleURL; - [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCurrentStyleURL"]; - [self.window.toolbar validateVisibleItems]; -} - -- (IBAction)chooseCustomStyle:(id)sender { - NSAlert *alert = [[NSAlert alloc] init]; - alert.messageText = @"Apply custom style"; - alert.informativeText = @"Enter the URL to a JSON file that conforms to the Mapbox GL style specification, such as a style designed in Mapbox Studio:"; - NSTextField *textField = [[NSTextField alloc] initWithFrame:NSZeroRect]; - [textField sizeToFit]; - NSRect textFieldFrame = textField.frame; - textFieldFrame.size.width = 300; - textField.frame = textFieldFrame; - NSURL *savedURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"MBXCustomStyleURL"]; - if (savedURL) { - textField.stringValue = savedURL.absoluteString; - } - alert.accessoryView = textField; - [alert addButtonWithTitle:@"Apply"]; - [alert addButtonWithTitle:@"Cancel"]; - if ([alert runModal] == NSAlertFirstButtonReturn) { - self.mapView.styleURL = [NSURL URLWithString:textField.stringValue]; - [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCustomStyleURL"]; - [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCurrentStyleURL"]; - [self.window.toolbar validateVisibleItems]; - } -} - -- (IBAction)zoomIn:(id)sender { - [self.mapView setZoomLevel:self.mapView.zoomLevel + 1 animated:YES]; -} - -- (IBAction)zoomOut:(id)sender { - [self.mapView setZoomLevel:self.mapView.zoomLevel - 1 animated:YES]; -} - -- (IBAction)snapToNorth:(id)sender { - [self.mapView setDirection:0 animated:YES]; -} - -- (IBAction)reload:(id)sender { - [self.mapView reloadStyle:sender]; -} - -#pragma mark Debug methods - -- (IBAction)toggleTileBoundaries:(id)sender { - self.mapView.debugMask ^= MGLMapDebugTileBoundariesMask; -} - -- (IBAction)toggleTileInfo:(id)sender { - self.mapView.debugMask ^= MGLMapDebugTileInfoMask; -} - -- (IBAction)toggleTileTimestamps:(id)sender { - self.mapView.debugMask ^= MGLMapDebugTimestampsMask; -} - -- (IBAction)toggleCollisionBoxes:(id)sender { - self.mapView.debugMask ^= MGLMapDebugCollisionBoxesMask; -} - -- (IBAction)toggleShowsToolTipsOnDroppedPins:(id)sender { - _showsToolTipsOnDroppedPins = !_showsToolTipsOnDroppedPins; -} - -- (IBAction)toggleRandomizesCursorsOnDroppedPins:(id)sender { - _randomizesCursorsOnDroppedPins = !_randomizesCursorsOnDroppedPins; -} - -- (IBAction)dropManyPins:(id)sender { - [self.mapView removeAnnotations:self.mapView.annotations]; - - NSRect bounds = self.mapView.bounds; - NSMutableArray *annotations = [NSMutableArray array]; - for (CGFloat x = NSMinX(bounds); x < NSMaxX(bounds); x += arc4random_uniform(50)) { - for (CGFloat y = NSMaxY(bounds); y >= NSMinY(bounds); y -= arc4random_uniform(100)) { - [annotations addObject:[self pinAtPoint:NSMakePoint(x, y)]]; - } - } - - [NSTimer scheduledTimerWithTimeInterval:1/60 - target:self - selector:@selector(dropOneOfManyPins:) - userInfo:annotations - repeats:YES]; -} - -- (void)dropOneOfManyPins:(NSTimer *)timer { - NSMutableArray *annotations = timer.userInfo; - NSUInteger numberOfAnnotationsToAdd = 50; - if (annotations.count < numberOfAnnotationsToAdd) { - numberOfAnnotationsToAdd = annotations.count; - } - NSArray *annotationsToAdd = [annotations subarrayWithRange: - NSMakeRange(0, numberOfAnnotationsToAdd)]; - [self.mapView addAnnotations:annotationsToAdd]; - [annotations removeObjectsInRange:NSMakeRange(0, numberOfAnnotationsToAdd)]; - if (!annotations.count) { - [timer invalidate]; - } -} - -- (IBAction)removeAllPins:(id)sender { - [self.mapView removeAnnotations:self.mapView.annotations]; -} - -- (IBAction)startWorldTour:(id)sender { - _isTouringWorld = YES; - - [self removeAllPins:sender]; - NSUInteger numberOfAnnotations = sizeof(WorldTourDestinations) / sizeof(WorldTourDestinations[0]); - NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:numberOfAnnotations]; - for (NSUInteger i = 0; i < numberOfAnnotations; i++) { - MGLPointAnnotation *annotation = [[MGLPointAnnotation alloc] init]; - annotation.coordinate = WorldTourDestinations[i]; - [annotations addObject:annotation]; - } - [self.mapView addAnnotations:annotations]; - [self continueWorldTourWithRemainingAnnotations:annotations]; -} - -- (void)continueWorldTourWithRemainingAnnotations:(NS_MUTABLE_ARRAY_OF(MGLPointAnnotation *) *)annotations { - MGLPointAnnotation *nextAnnotation = annotations.firstObject; - if (!nextAnnotation || !_isTouringWorld) { - _isTouringWorld = NO; - return; - } - - [annotations removeObjectAtIndex:0]; - MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:nextAnnotation.coordinate - fromDistance:0 - pitch:arc4random_uniform(60) - heading:arc4random_uniform(360)]; - __weak AppDelegate *weakSelf = self; - [self.mapView flyToCamera:camera completionHandler:^{ - AppDelegate *strongSelf = weakSelf; - [strongSelf performSelector:@selector(continueWorldTourWithRemainingAnnotations:) - withObject:annotations - afterDelay:2]; - }]; -} - -- (IBAction)stopWorldTour:(id)sender { - _isTouringWorld = NO; - // Any programmatic viewpoint change cancels outstanding animations. - self.mapView.camera = self.mapView.camera; } #pragma mark Help methods @@ -355,13 +132,6 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { [alert runModal]; } -- (IBAction)giveFeedback:(id)sender { - CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; - NSURL *feedbackURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://www.mapbox.com/map-feedback/#/%.5f/%.5f/%.0f", - centerCoordinate.longitude, centerCoordinate.latitude, round(self.mapView.zoomLevel)]]; - [[NSWorkspace sharedWorkspace] openURL:feedbackURL]; -} - - (IBAction)showPreferences:(id)sender { [self.preferencesWindow makeKeyAndOrderFront:sender]; } @@ -370,296 +140,16 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://www.mapbox.com/studio/account/tokens/"]]; } -#pragma mark Mouse events - -- (void)handlePressGesture:(NSPressGestureRecognizer *)gestureRecognizer { - if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { - NSPoint location = [gestureRecognizer locationInView:self.mapView]; - [self dropPinAtPoint:location]; - } -} - -- (IBAction)dropPin:(NSMenuItem *)sender { - [self dropPinAtPoint:_mouseLocationForMapViewContextMenu]; -} - -- (void)dropPinAtPoint:(NSPoint)point { - DroppedPinAnnotation *annotation = [self pinAtPoint:point]; - [self.mapView addAnnotation:annotation]; - [self.mapView selectAnnotation:annotation]; -} - -- (DroppedPinAnnotation *)pinAtPoint:(NSPoint)point { - DroppedPinAnnotation *annotation = [[DroppedPinAnnotation alloc] init]; - annotation.coordinate = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; - annotation.title = @"Dropped Pin"; - _spellOutNumberFormatter.numberStyle = NSNumberFormatterSpellOutStyle; - if (_showsToolTipsOnDroppedPins) { - NSString *formattedNumber = [_spellOutNumberFormatter stringFromNumber:@(++_droppedPinCounter)]; - annotation.toolTip = formattedNumber; - } - return annotation; -} - -- (IBAction)removePin:(NSMenuItem *)sender { - [self removePinAtPoint:_mouseLocationForMapViewContextMenu]; -} - -- (void)removePinAtPoint:(NSPoint)point { - [self.mapView removeAnnotation:[self.mapView annotationAtPoint:point]]; -} - #pragma mark User interface validation - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { - if (menuItem.action == @selector(setStyle:)) { - NSURL *styleURL = self.mapView.styleURL; - NSCellStateValue state; - switch (menuItem.tag) { - case 1: - state = [styleURL isEqual:[MGLStyle streetsStyleURL]]; - break; - case 2: - state = [styleURL isEqual:[MGLStyle emeraldStyleURL]]; - break; - case 3: - state = [styleURL isEqual:[MGLStyle lightStyleURL]]; - break; - case 4: - state = [styleURL isEqual:[MGLStyle darkStyleURL]]; - break; - case 5: - state = [styleURL isEqual:[MGLStyle satelliteStyleURL]]; - break; - case 6: - state = [styleURL isEqual:[MGLStyle hybridStyleURL]]; - break; - default: - return NO; - } - menuItem.state = state; - return YES; - } - if (menuItem.action == @selector(chooseCustomStyle:)) { - menuItem.state = self.indexOfStyleInToolbarItem == NSNotFound; - return YES; - } - if (menuItem.action == @selector(zoomIn:)) { - return self.mapView.zoomLevel < self.mapView.maximumZoomLevel; - } - if (menuItem.action == @selector(zoomOut:)) { - return self.mapView.zoomLevel > self.mapView.minimumZoomLevel; - } - if (menuItem.action == @selector(snapToNorth:)) { - return self.mapView.direction != 0; - } - if (menuItem.action == @selector(reload:)) { - return YES; - } - if (menuItem.action == @selector(dropPin:)) { - BOOL isOverAnnotation = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; - menuItem.hidden = isOverAnnotation; - return YES; - } - if (menuItem.action == @selector(removePin:)) { - BOOL isOverAnnotation = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; - menuItem.hidden = !isOverAnnotation; - return YES; - } - if (menuItem.action == @selector(toggleTileBoundaries:)) { - BOOL isShown = self.mapView.debugMask & MGLMapDebugTileBoundariesMask; - menuItem.title = isShown ? @"Hide Tile Boundaries" : @"Show Tile Boundaries"; - return YES; - } - if (menuItem.action == @selector(toggleTileInfo:)) { - BOOL isShown = self.mapView.debugMask & MGLMapDebugTileInfoMask; - menuItem.title = isShown ? @"Hide Tile Info" : @"Show Tile Info"; - return YES; - } - if (menuItem.action == @selector(toggleTileTimestamps:)) { - BOOL isShown = self.mapView.debugMask & MGLMapDebugTimestampsMask; - menuItem.title = isShown ? @"Hide Tile Timestamps" : @"Show Tile Timestamps"; - return YES; - } - if (menuItem.action == @selector(toggleCollisionBoxes:)) { - BOOL isShown = self.mapView.debugMask & MGLMapDebugCollisionBoxesMask; - menuItem.title = isShown ? @"Hide Collision Boxes" : @"Show Collision Boxes"; - return YES; - } - if (menuItem.action == @selector(toggleShowsToolTipsOnDroppedPins:)) { - BOOL isShown = _showsToolTipsOnDroppedPins; - menuItem.title = isShown ? @"Hide Tooltips on Dropped Pins" : @"Show Tooltips on Dropped Pins"; - return YES; - } - if (menuItem.action == @selector(toggleRandomizesCursorsOnDroppedPins:)) { - BOOL isRandom = _randomizesCursorsOnDroppedPins; - menuItem.title = isRandom ? @"Use Default Cursor for Dropped Pins" : @"Use Random Cursors for Dropped Pins"; - return _showsToolTipsOnDroppedPins; - } - if (menuItem.action == @selector(dropManyPins:)) { - return YES; - } - if (menuItem.action == @selector(removeAllPins:)) { - return self.mapView.annotations.count; - } - if (menuItem.action == @selector(startWorldTour:)) { - return !_isTouringWorld; - } - if (menuItem.action == @selector(stopWorldTour:)) { - return _isTouringWorld; - } if (menuItem.action == @selector(showShortcuts:)) { return YES; } - if (menuItem.action == @selector(giveFeedback:)) { - return YES; - } if (menuItem.action == @selector(showPreferences:)) { return YES; } return NO; } -- (NSUInteger)indexOfStyleInToolbarItem { - if (![MGLAccountManager accessToken]) { - return NSNotFound; - } - - NSArray *styleURLs = @[ - [MGLStyle streetsStyleURL], - [MGLStyle emeraldStyleURL], - [MGLStyle lightStyleURL], - [MGLStyle darkStyleURL], - [MGLStyle satelliteStyleURL], - [MGLStyle hybridStyleURL], - ]; - return [styleURLs indexOfObject:self.mapView.styleURL]; -} - -- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem { - if (!self.mapView) { - return NO; - } - - if (toolbarItem.action == @selector(showShareMenu:)) { - [(NSButton *)toolbarItem.view sendActionOn:NSLeftMouseDownMask]; - if (![MGLAccountManager accessToken]) { - return NO; - } - NSURL *styleURL = self.mapView.styleURL; - return ([styleURL.scheme isEqualToString:@"mapbox"] - && [styleURL.pathComponents.firstObject isEqualToString:@"styles"]); - } - if (toolbarItem.action == @selector(setStyle:)) { - NSPopUpButton *popUpButton = (NSPopUpButton *)toolbarItem.view; - NSUInteger index = self.indexOfStyleInToolbarItem; - if (index == NSNotFound) { - [popUpButton addItemWithTitle:@"Custom"]; - index = [popUpButton numberOfItems] - 1; - } - [popUpButton selectItemAtIndex:index]; - } - return NO; -} - -#pragma mark NSApplicationDelegate methods - -- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { - return YES; -} - -#pragma mark NSSharingServicePickerDelegate methods - -- (NS_ARRAY_OF(NSSharingService *) *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items proposedSharingServices:(NS_ARRAY_OF(NSSharingService *) *)proposedServices { - NSURL *shareURL = self.shareURL; - NSURL *browserURL = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:shareURL]; - NSImage *browserIcon = [[NSWorkspace sharedWorkspace] iconForFile:browserURL.path]; - NSString *browserName = [[NSFileManager defaultManager] displayNameAtPath:browserURL.path]; - NSString *browserServiceName = [NSString stringWithFormat:@"Open in %@", browserName]; - - NSSharingService *browserService = [[NSSharingService alloc] initWithTitle:browserServiceName - image:browserIcon - alternateImage:nil - handler:^{ - [[NSWorkspace sharedWorkspace] openURL:self.shareURL]; - }]; - - NSMutableArray *sharingServices = [proposedServices mutableCopy]; - [sharingServices insertObject:browserService atIndex:0]; - return sharingServices; -} - -#pragma mark NSMenuDelegate methods - -- (void)menuWillOpen:(NSMenu *)menu { - if (menu == self.mapViewContextMenu) { - _mouseLocationForMapViewContextMenu = [self.window.contentView convertPoint:self.window.mouseLocationOutsideOfEventStream - toView:self.mapView]; - } -} - -#pragma mark MGLMapViewDelegate methods - -- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation { - return YES; -} - -- (MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id <MGLAnnotation>)annotation { - MGLAnnotationImage *annotationImage = [self.mapView dequeueReusableAnnotationImageWithIdentifier:MGLDroppedPinAnnotationImageIdentifier]; - if (!annotationImage) { - NSString *imagePath = [[NSBundle bundleForClass:[MGLMapView class]] - pathForResource:@"default_marker" ofType:@"pdf"]; - NSImage *image = [[NSImage alloc] initWithContentsOfFile:imagePath]; - NSRect alignmentRect = image.alignmentRect; - alignmentRect.origin.y = NSMidY(alignmentRect); - alignmentRect.size.height /= 2; - image.alignmentRect = alignmentRect; - annotationImage = [MGLAnnotationImage annotationImageWithImage:image - reuseIdentifier:MGLDroppedPinAnnotationImageIdentifier]; - } - if (_randomizesCursorsOnDroppedPins) { - NSArray *cursors = @[ - [NSCursor IBeamCursor], - [NSCursor crosshairCursor], - [NSCursor pointingHandCursor], - [NSCursor disappearingItemCursor], - [NSCursor IBeamCursorForVerticalLayout], - [NSCursor operationNotAllowedCursor], - [NSCursor dragLinkCursor], - [NSCursor dragCopyCursor], - [NSCursor contextualMenuCursor], - ]; - annotationImage.cursor = cursors[arc4random_uniform(cursors.count)]; - } else { - annotationImage.cursor = nil; - } - return annotationImage; -} - -- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id <MGLAnnotation>)annotation { - if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { - DroppedPinAnnotation *droppedPin = annotation; - [droppedPin resume]; - } -} - -- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id <MGLAnnotation>)annotation { - if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { - DroppedPinAnnotation *droppedPin = annotation; - [droppedPin pause]; - } -} - -@end - -@interface ValidatedToolbarItem : NSToolbarItem - -@end - -@implementation ValidatedToolbarItem - -- (void)validate { - [(AppDelegate *)self.toolbar.delegate validateToolbarItem:self]; -} - @end diff --git a/platform/osx/app/Info.plist b/platform/osx/app/Info.plist index 118f2c1b11..0ae973842b 100644 --- a/platform/osx/app/Info.plist +++ b/platform/osx/app/Info.plist @@ -4,6 +4,21 @@ <dict> <key>CFBundleDevelopmentRegion</key> <string>English</string> + <key>CFBundleDocumentTypes</key> + <array> + <dict> + <key>CFBundleTypeExtensions</key> + <array> + <string>mbx</string> + </array> + <key>CFBundleTypeName</key> + <string>Mapbox GL Map</string> + <key>CFBundleTypeRole</key> + <string>Editor</string> + <key>NSDocumentClass</key> + <string>MapDocument</string> + </dict> + </array> <key>CFBundleExecutable</key> <string>${EXECUTABLE_NAME}</string> <key>CFBundleGetInfoString</key> diff --git a/platform/osx/app/MainMenu.xib b/platform/osx/app/MainMenu.xib index 3f270c23fd..844af08f3e 100644 --- a/platform/osx/app/MainMenu.xib +++ b/platform/osx/app/MainMenu.xib @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> -<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9531" systemVersion="15B42" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <dependencies> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9531"/> </dependencies> @@ -17,10 +17,7 @@ </customObject> <customObject id="Voe-Tx-rLC" customClass="AppDelegate"> <connections> - <outlet property="mapView" destination="v1Z-oA-oHU" id="70S-xO-QIP"/> - <outlet property="mapViewContextMenu" destination="6rZ-M1-uTn" id="wl2-03-9Z8"/> <outlet property="preferencesWindow" destination="UWc-yQ-qda" id="Ota-aT-Mz2"/> - <outlet property="window" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/> </connections> </customObject> <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> @@ -544,100 +541,6 @@ </menuItem> </items> </menu> - <window title="Mapbox GL" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" frameAutosaveName="Mapbox" animationBehavior="default" id="QvC-M9-y7g"> - <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" fullSizeContentView="YES"/> - <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> - <rect key="contentRect" x="388" y="211" width="512" height="360"/> - <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/> - <view key="contentView" id="EiT-Mj-1SZ"> - <rect key="frame" x="0.0" y="0.0" width="512" height="360"/> - <autoresizingMask key="autoresizingMask"/> - <subviews> - <customView translatesAutoresizingMaskIntoConstraints="NO" id="v1Z-oA-oHU" customClass="MGLMapView"> - <rect key="frame" x="0.0" y="0.0" width="512" height="360"/> - <connections> - <outlet property="delegate" destination="Voe-Tx-rLC" id="qJ7-fL-iw1"/> - <outlet property="menu" destination="6rZ-M1-uTn" id="4sB-UN-zuc"/> - </connections> - </customView> - </subviews> - <constraints> - <constraint firstItem="v1Z-oA-oHU" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" id="Z3O-xb-fah"/> - <constraint firstAttribute="trailing" secondItem="v1Z-oA-oHU" secondAttribute="trailing" id="oYw-bk-P2l"/> - <constraint firstItem="v1Z-oA-oHU" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" id="r4Z-4m-bgA"/> - <constraint firstAttribute="bottom" secondItem="v1Z-oA-oHU" secondAttribute="bottom" id="w3h-H1-baQ"/> - </constraints> - </view> - <toolbar key="toolbar" implicitIdentifier="A3AC6577-4712-4628-813D-113498171A84" allowsUserCustomization="NO" displayMode="iconOnly" sizeMode="regular" id="8nY-8B-Om5"> - <allowedToolbarItems> - <toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="zzK-Oz-JV8"/> - <toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="SMs-8a-x2h"/> - <toolbarItem implicitItemIdentifier="2CB58C0A-7B95-4233-8DD3-F94BFE7D3061" label="Share" paletteLabel="Share" image="NSShareTemplate" id="HtT-hs-jWY" customClass="ValidatedToolbarItem"> - <nil key="toolTip"/> - <size key="minSize" width="40" height="32"/> - <size key="maxSize" width="40" height="32"/> - <button key="view" verticalHuggingPriority="750" id="l0U-ql-5hn"> - <rect key="frame" x="0.0" y="14" width="48" height="32"/> - <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> - <buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="NSShareTemplate" imagePosition="only" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vxZ-hj-hV2"> - <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> - <font key="font" metaFont="system"/> - </buttonCell> - </button> - <connections> - <action selector="showShareMenu:" target="-1" id="vDU-9Y-DkS"/> - </connections> - </toolbarItem> - <toolbarItem implicitItemIdentifier="BA3542AF-D63A-4893-9CC7-8F67EF2E82B0" label="Style" paletteLabel="Style" id="ge5-0H-wyr" customClass="ValidatedToolbarItem"> - <nil key="toolTip"/> - <size key="minSize" width="100" height="26"/> - <size key="maxSize" width="100" height="26"/> - <popUpButton key="view" verticalHuggingPriority="750" id="5Cd-Lw-cWm"> - <rect key="frame" x="0.0" y="14" width="100" height="26"/> - <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> - <popUpButtonCell key="cell" type="roundTextured" title="Streets" bezelStyle="texturedRounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="border" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="ZnV-nK-Bri" id="esr-2z-V4Y"> - <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> - <font key="font" metaFont="menu"/> - <menu key="menu" id="uxq-U6-EQW"> - <items> - <menuItem title="Streets" state="on" tag="1" id="ZnV-nK-Bri"/> - <menuItem title="Emerald" tag="2" id="LLa-jX-Dox"/> - <menuItem title="Light" tag="3" id="jDf-NE-hMn"/> - <menuItem title="Dark" tag="4" id="etJ-y7-M5R"> - <modifierMask key="keyEquivalentModifierMask"/> - </menuItem> - <menuItem title="Satellite" tag="5" id="xYa-J8-bLe"> - <modifierMask key="keyEquivalentModifierMask"/> - </menuItem> - <menuItem title="Hybrid" tag="6" id="1Uq-Nl-FSZ"> - <modifierMask key="keyEquivalentModifierMask"/> - </menuItem> - </items> - </menu> - </popUpButtonCell> - </popUpButton> - <connections> - <action selector="setStyle:" target="-1" id="qb2-E8-7pE"/> - </connections> - </toolbarItem> - </allowedToolbarItems> - <defaultToolbarItems> - <toolbarItem reference="HtT-hs-jWY"/> - <toolbarItem reference="SMs-8a-x2h"/> - <toolbarItem reference="ge5-0H-wyr"/> - </defaultToolbarItems> - <connections> - <outlet property="delegate" destination="Voe-Tx-rLC" id="ciT-Nf-Dtm"/> - </connections> - </toolbar> - <connections> - <binding destination="Voe-Tx-rLC" name="title" keyPath="mapView.centerCoordinate" id="XFo-sp-PtR"> - <dictionary key="options"> - <string key="NSValueTransformerName">LocationCoordinate2DTransformer</string> - </dictionary> - </binding> - </connections> - </window> <window title="Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="Preferences" animationBehavior="default" id="UWc-yQ-qda"> <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> @@ -700,29 +603,8 @@ <point key="canvasLocation" x="754" y="210"/> </window> <userDefaultsController representsSharedInstance="YES" id="45S-yT-WUN"/> - <menu title="Map View" id="6rZ-M1-uTn"> - <items> - <menuItem title="Drop Pin" id="fxQ-3P-E7l"> - <modifierMask key="keyEquivalentModifierMask"/> - <connections> - <action selector="dropPin:" target="Voe-Tx-rLC" id="eDR-iw-JUG"/> - </connections> - </menuItem> - <menuItem title="Remove Pin" id="rgF-cY-qgP"> - <modifierMask key="keyEquivalentModifierMask"/> - <connections> - <action selector="removePin:" target="Voe-Tx-rLC" id="38n-9m-8Wp"/> - </connections> - </menuItem> - </items> - <connections> - <outlet property="delegate" destination="Voe-Tx-rLC" id="MsV-YU-f4X"/> - </connections> - <point key="canvasLocation" x="820" y="254.5"/> - </menu> </objects> <resources> <image name="NSFollowLinkFreestandingTemplate" width="14" height="14"/> - <image name="NSShareTemplate" width="11" height="16"/> </resources> </document> diff --git a/platform/osx/app/MapDocument.h b/platform/osx/app/MapDocument.h new file mode 100644 index 0000000000..86ad05e6e2 --- /dev/null +++ b/platform/osx/app/MapDocument.h @@ -0,0 +1,14 @@ +#import <Cocoa/Cocoa.h> + +@class MGLMapView; + +@interface MapDocument : NSDocument + +@property (weak) IBOutlet MGLMapView *mapView; + +- (IBAction)setStyle:(id)sender; +- (IBAction)chooseCustomStyle:(id)sender; + +- (IBAction)reload:(id)sender; + +@end diff --git a/platform/osx/app/MapDocument.m b/platform/osx/app/MapDocument.m new file mode 100644 index 0000000000..b7d16e5c46 --- /dev/null +++ b/platform/osx/app/MapDocument.m @@ -0,0 +1,568 @@ +#import "MapDocument.h" + +#import "DroppedPinAnnotation.h" + +#import <Mapbox/Mapbox.h> + +static NSString * const MGLDroppedPinAnnotationImageIdentifier = @"dropped"; + +static const CLLocationCoordinate2D WorldTourDestinations[] = { + { .latitude = 38.9131982, .longitude = -77.0325453144239 }, + { .latitude = 37.7757368, .longitude = -122.4135302 }, + { .latitude = 12.9810816, .longitude = 77.6368034 }, + { .latitude = -13.15589555, .longitude = -74.2178961777998 }, +}; + +@interface MapDocument () <NSSharingServicePickerDelegate, NSMenuDelegate, MGLMapViewDelegate> + +@property (weak) IBOutlet NSMenu *mapViewContextMenu; + +@end + +@implementation MapDocument { + NSPoint _mouseLocationForMapViewContextMenu; + NSUInteger _droppedPinCounter; + NSNumberFormatter *_spellOutNumberFormatter; + + BOOL _showsToolTipsOnDroppedPins; + BOOL _randomizesCursorsOnDroppedPins; + BOOL _isTouringWorld; +} + +#pragma mark Lifecycle + +- (NSString *)windowNibName { + return @"MapDocument"; +} + +- (void)windowControllerDidLoadNib:(NSWindowController *)aController { + [super windowControllerDidLoadNib:aController]; + + NSURL *savedURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"MBXCurrentStyleURL"]; + if (savedURL) { + self.mapView.styleURL = savedURL; + } + + _spellOutNumberFormatter = [[NSNumberFormatter alloc] init]; + + NSPressGestureRecognizer *pressGestureRecognizer = [[NSPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)]; + [self.mapView addGestureRecognizer:pressGestureRecognizer]; +} + +- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { + // Insert code here to write your document to data of the specified type. If outError != NULL, ensure that you create and set an appropriate error when returning nil. + // You can also choose to override -fileWrapperOfType:error:, -writeToURL:ofType:error:, or -writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead. + if (outError) { + *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:nil]; + } + return nil; +} + +- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError { + // Insert code here to read your document from the given data of the specified type. If outError != NULL, ensure that you create and set an appropriate error when returning NO. + // You can also choose to override -readFromFileWrapper:ofType:error: or -readFromURL:ofType:error: instead. + // If you override either of these, you should also override -isEntireFileLoaded to return NO if the contents are lazily loaded. + if (outError) { + *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:nil]; + } + return NO; +} + ++ (BOOL)autosavesInPlace { + return YES; +} + +- (NSWindow *)window { + return self.windowControllers.firstObject.window; +} + +#pragma mark Services + +- (IBAction)showShareMenu:(id)sender { + NSSharingServicePicker *picker = [[NSSharingServicePicker alloc] initWithItems:@[self.shareURL]]; + picker.delegate = self; + [picker showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMinYEdge]; +} + +- (NSURL *)shareURL { + NSArray *components = self.mapView.styleURL.pathComponents; + CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; + return [NSURL URLWithString: + [NSString stringWithFormat:@"https://api.mapbox.com/styles/v1/%@/%@.html?access_token=%@#%.2f/%.5f/%.5f/%.f", + components[1], components[2], [MGLAccountManager accessToken], + self.mapView.zoomLevel, centerCoordinate.latitude, centerCoordinate.longitude, self.mapView.direction]]; +} + +#pragma mark View methods + +- (IBAction)setStyle:(id)sender { + NSInteger tag; + if ([sender isKindOfClass:[NSMenuItem class]]) { + tag = [sender tag]; + } else if ([sender isKindOfClass:[NSPopUpButton class]]) { + tag = [sender selectedTag]; + } + NSURL *styleURL; + switch (tag) { + case 1: + styleURL = [MGLStyle streetsStyleURL]; + break; + case 2: + styleURL = [MGLStyle emeraldStyleURL]; + break; + case 3: + styleURL = [MGLStyle lightStyleURL]; + break; + case 4: + styleURL = [MGLStyle darkStyleURL]; + break; + case 5: + styleURL = [MGLStyle satelliteStyleURL]; + break; + case 6: + styleURL = [MGLStyle hybridStyleURL]; + break; + default: + NSAssert(NO, @"Cannot set style from control with tag %li", (long)tag); + break; + } + self.mapView.styleURL = styleURL; + [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCurrentStyleURL"]; + [self.window.toolbar validateVisibleItems]; +} + +- (IBAction)chooseCustomStyle:(id)sender { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = @"Apply custom style"; + alert.informativeText = @"Enter the URL to a JSON file that conforms to the Mapbox GL style specification, such as a style designed in Mapbox Studio:"; + NSTextField *textField = [[NSTextField alloc] initWithFrame:NSZeroRect]; + [textField sizeToFit]; + NSRect textFieldFrame = textField.frame; + textFieldFrame.size.width = 300; + textField.frame = textFieldFrame; + NSURL *savedURL = [[NSUserDefaults standardUserDefaults] URLForKey:@"MBXCustomStyleURL"]; + if (savedURL) { + textField.stringValue = savedURL.absoluteString; + } + alert.accessoryView = textField; + [alert addButtonWithTitle:@"Apply"]; + [alert addButtonWithTitle:@"Cancel"]; + if ([alert runModal] == NSAlertFirstButtonReturn) { + self.mapView.styleURL = [NSURL URLWithString:textField.stringValue]; + [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCustomStyleURL"]; + [[NSUserDefaults standardUserDefaults] setURL:self.mapView.styleURL forKey:@"MBXCurrentStyleURL"]; + [self.window.toolbar validateVisibleItems]; + } +} + +- (IBAction)zoomIn:(id)sender { + [self.mapView setZoomLevel:self.mapView.zoomLevel + 1 animated:YES]; +} + +- (IBAction)zoomOut:(id)sender { + [self.mapView setZoomLevel:self.mapView.zoomLevel - 1 animated:YES]; +} + +- (IBAction)snapToNorth:(id)sender { + [self.mapView setDirection:0 animated:YES]; +} + +- (IBAction)reload:(id)sender { + [self.mapView reloadStyle:sender]; +} + +#pragma mark Debug methods + +- (IBAction)toggleTileBoundaries:(id)sender { + self.mapView.debugMask ^= MGLMapDebugTileBoundariesMask; +} + +- (IBAction)toggleTileInfo:(id)sender { + self.mapView.debugMask ^= MGLMapDebugTileInfoMask; +} + +- (IBAction)toggleTileTimestamps:(id)sender { + self.mapView.debugMask ^= MGLMapDebugTimestampsMask; +} + +- (IBAction)toggleCollisionBoxes:(id)sender { + self.mapView.debugMask ^= MGLMapDebugCollisionBoxesMask; +} + +- (IBAction)toggleShowsToolTipsOnDroppedPins:(id)sender { + _showsToolTipsOnDroppedPins = !_showsToolTipsOnDroppedPins; +} + +- (IBAction)toggleRandomizesCursorsOnDroppedPins:(id)sender { + _randomizesCursorsOnDroppedPins = !_randomizesCursorsOnDroppedPins; +} + +- (IBAction)dropManyPins:(id)sender { + [self.mapView removeAnnotations:self.mapView.annotations]; + + NSRect bounds = self.mapView.bounds; + NSMutableArray *annotations = [NSMutableArray array]; + for (CGFloat x = NSMinX(bounds); x < NSMaxX(bounds); x += arc4random_uniform(50)) { + for (CGFloat y = NSMaxY(bounds); y >= NSMinY(bounds); y -= arc4random_uniform(100)) { + [annotations addObject:[self pinAtPoint:NSMakePoint(x, y)]]; + } + } + + [NSTimer scheduledTimerWithTimeInterval:1/60 + target:self + selector:@selector(dropOneOfManyPins:) + userInfo:annotations + repeats:YES]; +} + +- (void)dropOneOfManyPins:(NSTimer *)timer { + NSMutableArray *annotations = timer.userInfo; + NSUInteger numberOfAnnotationsToAdd = 50; + if (annotations.count < numberOfAnnotationsToAdd) { + numberOfAnnotationsToAdd = annotations.count; + } + NSArray *annotationsToAdd = [annotations subarrayWithRange: + NSMakeRange(0, numberOfAnnotationsToAdd)]; + [self.mapView addAnnotations:annotationsToAdd]; + [annotations removeObjectsInRange:NSMakeRange(0, numberOfAnnotationsToAdd)]; + if (!annotations.count) { + [timer invalidate]; + } +} + +- (IBAction)removeAllPins:(id)sender { + [self.mapView removeAnnotations:self.mapView.annotations]; +} + +- (IBAction)startWorldTour:(id)sender { + _isTouringWorld = YES; + + [self removeAllPins:sender]; + NSUInteger numberOfAnnotations = sizeof(WorldTourDestinations) / sizeof(WorldTourDestinations[0]); + NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:numberOfAnnotations]; + for (NSUInteger i = 0; i < numberOfAnnotations; i++) { + MGLPointAnnotation *annotation = [[MGLPointAnnotation alloc] init]; + annotation.coordinate = WorldTourDestinations[i]; + [annotations addObject:annotation]; + } + [self.mapView addAnnotations:annotations]; + [self continueWorldTourWithRemainingAnnotations:annotations]; +} + +- (void)continueWorldTourWithRemainingAnnotations:(NS_MUTABLE_ARRAY_OF(MGLPointAnnotation *) *)annotations { + MGLPointAnnotation *nextAnnotation = annotations.firstObject; + if (!nextAnnotation || !_isTouringWorld) { + _isTouringWorld = NO; + return; + } + + [annotations removeObjectAtIndex:0]; + MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:nextAnnotation.coordinate + fromDistance:0 + pitch:arc4random_uniform(60) + heading:arc4random_uniform(360)]; + __weak MapDocument *weakSelf = self; + [self.mapView flyToCamera:camera completionHandler:^{ + MapDocument *strongSelf = weakSelf; + [strongSelf performSelector:@selector(continueWorldTourWithRemainingAnnotations:) + withObject:annotations + afterDelay:2]; + }]; +} + +- (IBAction)stopWorldTour:(id)sender { + _isTouringWorld = NO; + // Any programmatic viewpoint change cancels outstanding animations. + self.mapView.camera = self.mapView.camera; +} + +#pragma mark Help methods + +- (IBAction)giveFeedback:(id)sender { + CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; + NSURL *feedbackURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://www.mapbox.com/map-feedback/#/%.5f/%.5f/%.0f", + centerCoordinate.longitude, centerCoordinate.latitude, round(self.mapView.zoomLevel)]]; + [[NSWorkspace sharedWorkspace] openURL:feedbackURL]; +} + +#pragma mark Mouse events + +- (void)handlePressGesture:(NSPressGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { + NSPoint location = [gestureRecognizer locationInView:self.mapView]; + [self dropPinAtPoint:location]; + } +} + +- (IBAction)dropPin:(NSMenuItem *)sender { + [self dropPinAtPoint:_mouseLocationForMapViewContextMenu]; +} + +- (void)dropPinAtPoint:(NSPoint)point { + DroppedPinAnnotation *annotation = [self pinAtPoint:point]; + [self.mapView addAnnotation:annotation]; + [self.mapView selectAnnotation:annotation]; +} + +- (DroppedPinAnnotation *)pinAtPoint:(NSPoint)point { + DroppedPinAnnotation *annotation = [[DroppedPinAnnotation alloc] init]; + annotation.coordinate = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; + annotation.title = @"Dropped Pin"; + _spellOutNumberFormatter.numberStyle = NSNumberFormatterSpellOutStyle; + if (_showsToolTipsOnDroppedPins) { + NSString *formattedNumber = [_spellOutNumberFormatter stringFromNumber:@(++_droppedPinCounter)]; + annotation.toolTip = formattedNumber; + } + return annotation; +} + +- (IBAction)removePin:(NSMenuItem *)sender { + [self removePinAtPoint:_mouseLocationForMapViewContextMenu]; +} + +- (void)removePinAtPoint:(NSPoint)point { + [self.mapView removeAnnotation:[self.mapView annotationAtPoint:point]]; +} + +#pragma mark User interface validation + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem { + if (menuItem.action == @selector(setStyle:)) { + NSURL *styleURL = self.mapView.styleURL; + NSCellStateValue state; + switch (menuItem.tag) { + case 1: + state = [styleURL isEqual:[MGLStyle streetsStyleURL]]; + break; + case 2: + state = [styleURL isEqual:[MGLStyle emeraldStyleURL]]; + break; + case 3: + state = [styleURL isEqual:[MGLStyle lightStyleURL]]; + break; + case 4: + state = [styleURL isEqual:[MGLStyle darkStyleURL]]; + break; + case 5: + state = [styleURL isEqual:[MGLStyle satelliteStyleURL]]; + break; + case 6: + state = [styleURL isEqual:[MGLStyle hybridStyleURL]]; + break; + default: + return NO; + } + menuItem.state = state; + return YES; + } + if (menuItem.action == @selector(chooseCustomStyle:)) { + menuItem.state = self.indexOfStyleInToolbarItem == NSNotFound; + return YES; + } + if (menuItem.action == @selector(zoomIn:)) { + return self.mapView.zoomLevel < self.mapView.maximumZoomLevel; + } + if (menuItem.action == @selector(zoomOut:)) { + return self.mapView.zoomLevel > self.mapView.minimumZoomLevel; + } + if (menuItem.action == @selector(snapToNorth:)) { + return self.mapView.direction != 0; + } + if (menuItem.action == @selector(reload:)) { + return YES; + } + if (menuItem.action == @selector(dropPin:)) { + BOOL isOverAnnotation = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; + menuItem.hidden = isOverAnnotation; + return YES; + } + if (menuItem.action == @selector(removePin:)) { + BOOL isOverAnnotation = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu]; + menuItem.hidden = !isOverAnnotation; + return YES; + } + if (menuItem.action == @selector(toggleTileBoundaries:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugTileBoundariesMask; + menuItem.title = isShown ? @"Hide Tile Boundaries" : @"Show Tile Boundaries"; + return YES; + } + if (menuItem.action == @selector(toggleTileInfo:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugTileInfoMask; + menuItem.title = isShown ? @"Hide Tile Info" : @"Show Tile Info"; + return YES; + } + if (menuItem.action == @selector(toggleTileTimestamps:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugTimestampsMask; + menuItem.title = isShown ? @"Hide Tile Timestamps" : @"Show Tile Timestamps"; + return YES; + } + if (menuItem.action == @selector(toggleCollisionBoxes:)) { + BOOL isShown = self.mapView.debugMask & MGLMapDebugCollisionBoxesMask; + menuItem.title = isShown ? @"Hide Collision Boxes" : @"Show Collision Boxes"; + return YES; + } + if (menuItem.action == @selector(toggleShowsToolTipsOnDroppedPins:)) { + BOOL isShown = _showsToolTipsOnDroppedPins; + menuItem.title = isShown ? @"Hide Tooltips on Dropped Pins" : @"Show Tooltips on Dropped Pins"; + return YES; + } + if (menuItem.action == @selector(toggleRandomizesCursorsOnDroppedPins:)) { + BOOL isRandom = _randomizesCursorsOnDroppedPins; + menuItem.title = isRandom ? @"Use Default Cursor for Dropped Pins" : @"Use Random Cursors for Dropped Pins"; + return _showsToolTipsOnDroppedPins; + } + if (menuItem.action == @selector(dropManyPins:)) { + return YES; + } + if (menuItem.action == @selector(removeAllPins:)) { + return self.mapView.annotations.count; + } + if (menuItem.action == @selector(startWorldTour:)) { + return !_isTouringWorld; + } + if (menuItem.action == @selector(stopWorldTour:)) { + return _isTouringWorld; + } + if (menuItem.action == @selector(giveFeedback:)) { + return YES; + } + return NO; +} + +- (NSUInteger)indexOfStyleInToolbarItem { + if (![MGLAccountManager accessToken]) { + return NSNotFound; + } + + NSArray *styleURLs = @[ + [MGLStyle streetsStyleURL], + [MGLStyle emeraldStyleURL], + [MGLStyle lightStyleURL], + [MGLStyle darkStyleURL], + [MGLStyle satelliteStyleURL], + [MGLStyle hybridStyleURL], + ]; + return [styleURLs indexOfObject:self.mapView.styleURL]; +} + +- (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem { + if (!self.mapView) { + return NO; + } + + if (toolbarItem.action == @selector(showShareMenu:)) { + [(NSButton *)toolbarItem.view sendActionOn:NSLeftMouseDownMask]; + if (![MGLAccountManager accessToken]) { + return NO; + } + NSURL *styleURL = self.mapView.styleURL; + return ([styleURL.scheme isEqualToString:@"mapbox"] + && [styleURL.pathComponents.firstObject isEqualToString:@"styles"]); + } + if (toolbarItem.action == @selector(setStyle:)) { + NSPopUpButton *popUpButton = (NSPopUpButton *)toolbarItem.view; + NSUInteger index = self.indexOfStyleInToolbarItem; + if (index == NSNotFound) { + [popUpButton addItemWithTitle:@"Custom"]; + index = [popUpButton numberOfItems] - 1; + } + [popUpButton selectItemAtIndex:index]; + } + return NO; +} + +#pragma mark NSSharingServicePickerDelegate methods + +- (NS_ARRAY_OF(NSSharingService *) *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items proposedSharingServices:(NS_ARRAY_OF(NSSharingService *) *)proposedServices { + NSURL *shareURL = self.shareURL; + NSURL *browserURL = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:shareURL]; + NSImage *browserIcon = [[NSWorkspace sharedWorkspace] iconForFile:browserURL.path]; + NSString *browserName = [[NSFileManager defaultManager] displayNameAtPath:browserURL.path]; + NSString *browserServiceName = [NSString stringWithFormat:@"Open in %@", browserName]; + + NSSharingService *browserService = [[NSSharingService alloc] initWithTitle:browserServiceName + image:browserIcon + alternateImage:nil + handler:^{ + [[NSWorkspace sharedWorkspace] openURL:self.shareURL]; + }]; + + NSMutableArray *sharingServices = [proposedServices mutableCopy]; + [sharingServices insertObject:browserService atIndex:0]; + return sharingServices; +} + +#pragma mark NSMenuDelegate methods + +- (void)menuWillOpen:(NSMenu *)menu { + if (menu == self.mapViewContextMenu) { + _mouseLocationForMapViewContextMenu = [self.window.contentView convertPoint:self.window.mouseLocationOutsideOfEventStream + toView:self.mapView]; + } +} + +#pragma mark MGLMapViewDelegate methods + +- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation { + return YES; +} + +- (MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id <MGLAnnotation>)annotation { + MGLAnnotationImage *annotationImage = [self.mapView dequeueReusableAnnotationImageWithIdentifier:MGLDroppedPinAnnotationImageIdentifier]; + if (!annotationImage) { + NSString *imagePath = [[NSBundle bundleForClass:[MGLMapView class]] + pathForResource:@"default_marker" ofType:@"pdf"]; + NSImage *image = [[NSImage alloc] initWithContentsOfFile:imagePath]; + NSRect alignmentRect = image.alignmentRect; + alignmentRect.origin.y = NSMidY(alignmentRect); + alignmentRect.size.height /= 2; + image.alignmentRect = alignmentRect; + annotationImage = [MGLAnnotationImage annotationImageWithImage:image + reuseIdentifier:MGLDroppedPinAnnotationImageIdentifier]; + } + if (_randomizesCursorsOnDroppedPins) { + NSArray *cursors = @[ + [NSCursor IBeamCursor], + [NSCursor crosshairCursor], + [NSCursor pointingHandCursor], + [NSCursor disappearingItemCursor], + [NSCursor IBeamCursorForVerticalLayout], + [NSCursor operationNotAllowedCursor], + [NSCursor dragLinkCursor], + [NSCursor dragCopyCursor], + [NSCursor contextualMenuCursor], + ]; + annotationImage.cursor = cursors[arc4random_uniform(cursors.count)]; + } else { + annotationImage.cursor = nil; + } + return annotationImage; +} + +- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id <MGLAnnotation>)annotation { + if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { + DroppedPinAnnotation *droppedPin = annotation; + [droppedPin resume]; + } +} + +- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id <MGLAnnotation>)annotation { + if ([annotation isKindOfClass:[DroppedPinAnnotation class]]) { + DroppedPinAnnotation *droppedPin = annotation; + [droppedPin pause]; + } +} + +@end + +@interface ValidatedToolbarItem : NSToolbarItem + +@end + +@implementation ValidatedToolbarItem + +- (void)validate { + [(MapDocument *)self.toolbar.delegate validateToolbarItem:self]; +} + +@end diff --git a/platform/osx/app/MapDocument.xib b/platform/osx/app/MapDocument.xib new file mode 100644 index 0000000000..3adbba8c10 --- /dev/null +++ b/platform/osx/app/MapDocument.xib @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9531"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="MapDocument"> + <connections> + <outlet property="mapView" destination="q4d-kF-8Hi" id="7hI-dS-A5R"/> + <outlet property="mapViewContextMenu" destination="XbX-6a-Mgy" id="YD0-1r-5N2"/> + <outlet property="window" destination="cSv-fg-MAQ" id="TBu-Mu-79N"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <window title="Mapbox GL" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" frameAutosaveName="Mapbox" animationBehavior="default" id="cSv-fg-MAQ"> + <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" fullSizeContentView="YES"/> + <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> + <rect key="contentRect" x="388" y="211" width="512" height="480"/> + <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/> + <view key="contentView" id="TuG-C5-zLS"> + <rect key="frame" x="0.0" y="0.0" width="512" height="480"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <customView translatesAutoresizingMaskIntoConstraints="NO" id="q4d-kF-8Hi" customClass="MGLMapView"> + <rect key="frame" x="0.0" y="0.0" width="512" height="480"/> + <connections> + <outlet property="delegate" destination="-2" id="dh2-0H-jFZ"/> + </connections> + </customView> + </subviews> + <constraints> + <constraint firstAttribute="bottom" secondItem="q4d-kF-8Hi" secondAttribute="bottom" id="L2t-Be-qWL"/> + <constraint firstItem="q4d-kF-8Hi" firstAttribute="top" secondItem="TuG-C5-zLS" secondAttribute="top" id="T8A-o3-Bhq"/> + <constraint firstItem="q4d-kF-8Hi" firstAttribute="leading" secondItem="TuG-C5-zLS" secondAttribute="leading" id="fGH-YW-Qd3"/> + <constraint firstAttribute="trailing" secondItem="q4d-kF-8Hi" secondAttribute="trailing" id="yfG-iG-K4C"/> + </constraints> + </view> + <toolbar key="toolbar" implicitIdentifier="A3AC6577-4712-4628-813D-113498171A84" allowsUserCustomization="NO" displayMode="iconOnly" sizeMode="regular" id="DTc-AP-Bah"> + <allowedToolbarItems> + <toolbarItem implicitItemIdentifier="NSToolbarSpaceItem" id="bld-8W-Wgg"/> + <toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="z4l-5x-MzK"/> + <toolbarItem implicitItemIdentifier="2CB58C0A-7B95-4233-8DD3-F94BFE7D3061" label="Share" paletteLabel="Share" image="NSShareTemplate" id="XJT-Ho-tuZ" customClass="ValidatedToolbarItem"> + <nil key="toolTip"/> + <size key="minSize" width="40" height="32"/> + <size key="maxSize" width="48" height="32"/> + <button key="view" verticalHuggingPriority="750" id="y6e-ev-rVL"> + <rect key="frame" x="0.0" y="14" width="48" height="32"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="NSShareTemplate" imagePosition="only" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="TBK-Ra-XzZ"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + </button> + <connections> + <action selector="showShareMenu:" target="-1" id="fCB-HP-iou"/> + </connections> + </toolbarItem> + <toolbarItem implicitItemIdentifier="BA3542AF-D63A-4893-9CC7-8F67EF2E82B0" label="Style" paletteLabel="Style" id="u23-0z-Otl" customClass="ValidatedToolbarItem"> + <nil key="toolTip"/> + <size key="minSize" width="100" height="26"/> + <size key="maxSize" width="100" height="26"/> + <popUpButton key="view" verticalHuggingPriority="750" id="Tzm-Cy-dQg"> + <rect key="frame" x="0.0" y="14" width="100" height="26"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> + <popUpButtonCell key="cell" type="roundTextured" title="Streets" bezelStyle="texturedRounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="border" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="wvt-tP-O3a" id="3PJ-qK-Oh3"> + <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="menu"/> + <menu key="menu" id="xf3-qk-IhF"> + <items> + <menuItem title="Streets" state="on" tag="1" id="wvt-tP-O3a"/> + <menuItem title="Emerald" tag="2" id="RkE-lp-fL9"/> + <menuItem title="Light" tag="3" id="R4X-kt-HHb"/> + <menuItem title="Dark" tag="4" id="jUC-5X-0Zx"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Satellite" tag="5" id="CTe-e2-o42"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Hybrid" tag="6" id="7ly-oA-0ND"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + </menu> + </popUpButtonCell> + </popUpButton> + <connections> + <action selector="setStyle:" target="-1" id="2Kw-9i-a3G"/> + </connections> + </toolbarItem> + </allowedToolbarItems> + <defaultToolbarItems> + <toolbarItem reference="XJT-Ho-tuZ"/> + <toolbarItem reference="z4l-5x-MzK"/> + <toolbarItem reference="u23-0z-Otl"/> + </defaultToolbarItems> + <connections> + <outlet property="delegate" destination="-2" id="V9D-gS-Tvu"/> + </connections> + </toolbar> + <connections> + <binding destination="-2" name="displayPatternTitle1" keyPath="mapView.centerCoordinate" id="wtz-AV-bG1"> + <dictionary key="options"> + <string key="NSDisplayPattern">%{title1}@</string> + <string key="NSValueTransformerName">LocationCoordinate2DTransformer</string> + </dictionary> + </binding> + <outlet property="delegate" destination="-2" id="HEo-Qf-o6o"/> + </connections> + </window> + <menu title="Map View" id="XbX-6a-Mgy"> + <items> + <menuItem title="Drop Pin" id="qZJ-mM-bLj"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + <menuItem title="Remove Pin" id="Zhx-30-VmE"> + <modifierMask key="keyEquivalentModifierMask"/> + </menuItem> + </items> + <connections> + <outlet property="delegate" destination="-2" id="oHe-ZP-lyc"/> + </connections> + <point key="canvasLocation" x="820" y="254.5"/> + </menu> + </objects> + <resources> + <image name="NSShareTemplate" width="11" height="16"/> + </resources> +</document> diff --git a/platform/osx/app/mapboxgl-app.gypi b/platform/osx/app/mapboxgl-app.gypi index ca096143c2..7b39f5d9eb 100644 --- a/platform/osx/app/mapboxgl-app.gypi +++ b/platform/osx/app/mapboxgl-app.gypi @@ -13,6 +13,7 @@ 'Credits.rtf', 'Icon.icns', 'MainMenu.xib', + 'MapDocument.xib', ], 'dependencies': [ @@ -26,6 +27,8 @@ './DroppedPinAnnotation.m', './LocationCoordinate2DTransformer.h', './LocationCoordinate2DTransformer.m', + './MapDocument.h', + './MapDocument.m', './TimeIntervalTransformer.h', './TimeIntervalTransformer.m', './NSValue+Additions.h', |