659 lines
26 KiB
Objective-C
659 lines
26 KiB
Objective-C
#import "AppDelegate.h"
|
|
|
|
#import "DroppedPinAnnotation.h"
|
|
#import "LocationCoordinate2DTransformer.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;
|
|
|
|
@end
|
|
|
|
@implementation AppDelegate {
|
|
NSPoint _mouseLocationForMapViewContextMenu;
|
|
NSUInteger _droppedPinCounter;
|
|
NSNumberFormatter *_spellOutNumberFormatter;
|
|
|
|
BOOL _showsToolTipsOnDroppedPins;
|
|
BOOL _randomizesCursorsOnDroppedPins;
|
|
BOOL _isTouringWorld;
|
|
}
|
|
|
|
#pragma mark Lifecycle
|
|
|
|
+ (void)load {
|
|
// Set access token, unless MGLAccountManager already read it in from Info.plist.
|
|
if (![MGLAccountManager accessToken]) {
|
|
NSString *accessToken = [NSProcessInfo processInfo].environment[@"MAPBOX_ACCESS_TOKEN"];
|
|
if (accessToken) {
|
|
// Store to preferences so that we can launch the app later on without having to specify
|
|
// token.
|
|
[[NSUserDefaults standardUserDefaults] setObject:accessToken forKey:MGLMapboxAccessTokenDefaultsKey];
|
|
} else {
|
|
// Try to retrieve from preferences, maybe we've stored them there previously and can reuse
|
|
// the token.
|
|
accessToken = [[NSUserDefaults standardUserDefaults] stringForKey:MGLMapboxAccessTokenDefaultsKey];
|
|
}
|
|
[MGLAccountManager setAccessToken:accessToken];
|
|
}
|
|
}
|
|
|
|
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(userDefaultsDidChange:)
|
|
name:NSUserDefaultsDidChangeNotification
|
|
object:nil];
|
|
|
|
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self
|
|
andSelector:@selector(handleGetURLEvent:withReplyEvent:)
|
|
forEventClass:kInternetEventClass
|
|
andEventID:kAEGetURL];
|
|
|
|
// Normally, an application should respect the “Close windows when quitting
|
|
// an application” setting in the General pane of System Preferences. But
|
|
// the map would only be restored to its last opened location if the user
|
|
// quits the application using Quit and Keep Windows. An application that
|
|
// displays only a map should restore the last viewed map, like Maps.app
|
|
// does.
|
|
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSQuitAlwaysKeepsWindows"];
|
|
}
|
|
|
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
|
// Set access token, unless MGLAccountManager already read it in from Info.plist.
|
|
if (![MGLAccountManager accessToken]) {
|
|
NSAlert *alert = [[NSAlert alloc] init];
|
|
alert.messageText = @"Access token required";
|
|
alert.informativeText = @"To load Mapbox-hosted tiles and styles, enter your Mapbox access token in Preferences.";
|
|
[alert addButtonWithTitle:@"Open Preferences"];
|
|
[alert runModal];
|
|
[self showPreferences:nil];
|
|
}
|
|
|
|
_spellOutNumberFormatter = [[NSNumberFormatter alloc] init];
|
|
|
|
NSPressGestureRecognizer *pressGestureRecognizer = [[NSPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)];
|
|
[self.mapView addGestureRecognizer:pressGestureRecognizer];
|
|
}
|
|
|
|
- (void)applicationWillTerminate:(NSNotification *)notification {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)userDefaultsDidChange:(NSNotification *)notification {
|
|
NSUserDefaults *userDefaults = notification.object;
|
|
NSString *accessToken = [userDefaults stringForKey:MGLMapboxAccessTokenDefaultsKey];
|
|
if (![accessToken isEqualToString:[MGLAccountManager accessToken]]) {
|
|
[MGLAccountManager setAccessToken:accessToken];
|
|
[self reload:self];
|
|
}
|
|
}
|
|
|
|
#pragma mark Services
|
|
|
|
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
|
|
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:@"&"]) {
|
|
NSArray *parts = [param componentsSeparatedByString:@"="];
|
|
if (parts.count >= 2) {
|
|
params[parts[0]] = [parts[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
|
}
|
|
}
|
|
|
|
NSString *centerString = params[@"center"];
|
|
if (centerString) {
|
|
NSArray *coordinateValues = [centerString componentsSeparatedByString:@","];
|
|
if (coordinateValues.count == 2) {
|
|
self.mapView.centerCoordinate = CLLocationCoordinate2DMake([coordinateValues[0] doubleValue],
|
|
[coordinateValues[1] doubleValue]);
|
|
}
|
|
}
|
|
|
|
NSString *zoomLevelString = params[@"zoom"];
|
|
if (zoomLevelString.length) {
|
|
self.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.mapView.styleURL = styleURL;
|
|
[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:";
|
|
NSTextField *textField = [[NSTextField alloc] initWithFrame:NSZeroRect];
|
|
[textField sizeToFit];
|
|
NSRect textFieldFrame = textField.frame;
|
|
textFieldFrame.size.width = 300;
|
|
textField.frame = textFieldFrame;
|
|
NSString *savedURLString = [[NSUserDefaults standardUserDefaults] stringForKey:@"MBXCustomStyleURL"];
|
|
if (savedURLString) {
|
|
textField.stringValue = savedURLString;
|
|
}
|
|
alert.accessoryView = textField;
|
|
[alert addButtonWithTitle:@"Apply"];
|
|
[alert addButtonWithTitle:@"Cancel"];
|
|
if ([alert runModal] == NSAlertFirstButtonReturn) {
|
|
[[NSUserDefaults standardUserDefaults] setObject:textField.stringValue forKey:@"MBXCustomStyleURL"];
|
|
self.mapView.styleURL = [NSURL URLWithString:textField.stringValue];
|
|
[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
|
|
|
|
- (IBAction)showShortcuts:(id)sender {
|
|
NSAlert *alert = [[NSAlert alloc] init];
|
|
alert.messageText = @"Mapbox GL Help";
|
|
alert.informativeText = @"\
|
|
• To scroll, swipe with two fingers, drag the cursor, or press the arrow keys.\n\
|
|
• To zoom, pinch with two fingers, or hold down Shift while dragging the cursor up and down, or hold down Option while pressing the up and down arrow keys.\n\
|
|
• To rotate, move two fingers opposite each other in a circle, or hold down Option while dragging the cursor left and right, or hold down Option while pressing the left and right arrow keys.\n\
|
|
• To tilt, hold down Option while dragging the cursor up and down.\n\
|
|
• To drop a pin, click and hold.\
|
|
";
|
|
[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];
|
|
}
|
|
|
|
- (IBAction)openAccessTokenManager:(id)sender {
|
|
[[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
|