summaryrefslogtreecommitdiff
path: root/platform/macos/app
diff options
context:
space:
mode:
Diffstat (limited to 'platform/macos/app')
-rw-r--r--platform/macos/app/AppDelegate.h24
-rw-r--r--platform/macos/app/AppDelegate.m270
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon128x128.pngbin0 -> 3668 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon16x16.pngbin0 -> 713 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256-1.pngbin0 -> 8495 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256.pngbin0 -> 8495 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32-1.pngbin0 -> 1213 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32.pngbin0 -> 1213 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512-1.pngbin0 -> 20280 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512.pngbin0 -> 20280 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/Contents.json68
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon-1.pngbin0 -> 2205 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon.pngbin0 -> 9293 bytes
-rw-r--r--platform/macos/app/Assets.xcassets/Contents.json6
-rw-r--r--platform/macos/app/Base.lproj/MainMenu.xib845
-rw-r--r--platform/macos/app/Base.lproj/MapDocument.xib136
-rw-r--r--platform/macos/app/Credits.rtf9
-rw-r--r--platform/macos/app/DroppedPinAnnotation.h10
-rw-r--r--platform/macos/app/DroppedPinAnnotation.m68
-rw-r--r--platform/macos/app/Info.plist56
-rw-r--r--platform/macos/app/LocationCoordinate2DTransformer.h5
-rw-r--r--platform/macos/app/LocationCoordinate2DTransformer.m31
-rw-r--r--platform/macos/app/MapDocument.h14
-rw-r--r--platform/macos/app/MapDocument.m721
-rw-r--r--platform/macos/app/OfflinePackNameValueTransformer.h5
-rw-r--r--platform/macos/app/OfflinePackNameValueTransformer.m33
-rw-r--r--platform/macos/app/TimeIntervalTransformer.h5
-rw-r--r--platform/macos/app/TimeIntervalTransformer.m53
-rw-r--r--platform/macos/app/main.m5
29 files changed, 2364 insertions, 0 deletions
diff --git a/platform/macos/app/AppDelegate.h b/platform/macos/app/AppDelegate.h
new file mode 100644
index 0000000000..a1d9297b2f
--- /dev/null
+++ b/platform/macos/app/AppDelegate.h
@@ -0,0 +1,24 @@
+#import <Mapbox/Mapbox.h>
+
+extern NSString * const MGLMapboxAccessTokenDefaultsKey;
+
+@interface AppDelegate : NSObject <NSApplicationDelegate>
+
+@property (weak) IBOutlet NSWindow *preferencesWindow;
+
+// 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. These properties
+// temporarily hold state for the next map window to be opened.
+
+@property (assign) double pendingZoomLevel;
+@property (copy) MGLMapCamera *pendingCamera;
+@property (assign) MGLCoordinateBounds pendingVisibleCoordinateBounds;
+@property (assign) double pendingMinimumZoomLevel;
+@property (assign) double pendingMaximumZoomLevel;
+@property (copy) NSURL *pendingStyleURL;
+@property (assign) MGLMapDebugMaskOptions pendingDebugMask;
+
+@end
diff --git a/platform/macos/app/AppDelegate.m b/platform/macos/app/AppDelegate.m
new file mode 100644
index 0000000000..b7860cf130
--- /dev/null
+++ b/platform/macos/app/AppDelegate.m
@@ -0,0 +1,270 @@
+#import "AppDelegate.h"
+
+#import "MapDocument.h"
+
+NSString * const MGLMapboxAccessTokenDefaultsKey = @"MGLMapboxAccessToken";
+NSString * const MGLLastMapCameraDefaultsKey = @"MGLLastMapCamera";
+NSString * const MGLLastMapStyleURLDefaultsKey = @"MGLLastMapStyleURL";
+NSString * const MGLLastMapDebugMaskDefaultsKey = @"MGLLastMapDebugMask";
+
+/**
+ Some convenience methods to make offline pack properties easier to bind to.
+ */
+@implementation MGLOfflinePack (Additions)
+
++ (NSSet *)keyPathsForValuesAffectingStateImage {
+ return [NSSet setWithObjects:@"state", nil];
+}
+
+- (NSImage *)stateImage {
+ switch (self.state) {
+ case MGLOfflinePackStateComplete:
+ return [NSImage imageNamed:@"NSMenuOnStateTemplate"];
+
+ case MGLOfflinePackStateActive:
+ return [NSImage imageNamed:@"NSFollowLinkFreestandingTemplate"];
+
+ default:
+ return nil;
+ }
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfResourcesCompleted {
+ return [NSSet setWithObjects:@"progress", nil];
+}
+
+- (uint64_t)countOfResourcesCompleted {
+ return self.progress.countOfResourcesCompleted;
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfResourcesExpected {
+ return [NSSet setWithObjects:@"progress", nil];
+}
+
+- (uint64_t)countOfResourcesExpected {
+ return self.progress.countOfResourcesExpected;
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCountOfBytesCompleted {
+ return [NSSet setWithObjects:@"progress", nil];
+}
+
+- (uint64_t)countOfBytesCompleted {
+ return self.progress.countOfBytesCompleted;
+}
+
+@end
+
+@interface AppDelegate ()
+
+@property (weak) IBOutlet NSArrayController *offlinePacksArrayController;
+@property (weak) IBOutlet NSPanel *offlinePacksPanel;
+
+@end
+
+@implementation AppDelegate
+
+#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 {
+ [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self
+ andSelector:@selector(handleGetURLEvent:withReplyEvent:)
+ forEventClass:kInternetEventClass
+ andEventID:kAEGetURL];
+
+ if (![[NSUserDefaults standardUserDefaults] boolForKey:@"NSQuitAlwaysKeepsWindows"]) {
+ NSData *cameraData = [[NSUserDefaults standardUserDefaults] objectForKey:MGLLastMapCameraDefaultsKey];
+ if (cameraData) {
+ NSKeyedUnarchiver *coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:cameraData];
+ self.pendingZoomLevel = -1;
+ self.pendingCamera = [[MGLMapCamera alloc] initWithCoder:coder];
+ }
+ NSString *styleURLString = [[NSUserDefaults standardUserDefaults] objectForKey:MGLLastMapStyleURLDefaultsKey];
+ if (styleURLString) {
+ self.pendingStyleURL = [NSURL URLWithString:styleURLString];
+ }
+ self.pendingDebugMask = [[NSUserDefaults standardUserDefaults] integerForKey:MGLLastMapDebugMaskDefaultsKey];
+ }
+}
+
+- (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];
+ }
+
+ [self.offlinePacksArrayController bind:@"content" toObject:[MGLOfflineStorage sharedOfflineStorage] withKeyPath:@"packs" options:nil];
+}
+
+- (void)applicationWillTerminate:(NSNotification *)notification {
+ if (![[NSUserDefaults standardUserDefaults] boolForKey:@"NSQuitAlwaysKeepsWindows"]) {
+ NSDocument *currentDocument = [NSDocumentController sharedDocumentController].currentDocument;
+ if ([currentDocument isKindOfClass:[MapDocument class]]) {
+ MGLMapView *mapView = [(MapDocument *)currentDocument mapView];
+ NSMutableData *cameraData = [NSMutableData data];
+ NSKeyedArchiver *coder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:cameraData];
+ [mapView.camera encodeWithCoder:coder];
+ [coder finishEncoding];
+ [[NSUserDefaults standardUserDefaults] setObject:cameraData forKey:MGLLastMapCameraDefaultsKey];
+ [[NSUserDefaults standardUserDefaults] setObject:mapView.styleURL.absoluteString forKey:MGLLastMapStyleURLDefaultsKey];
+ [[NSUserDefaults standardUserDefaults] setInteger:mapView.debugMask forKey:MGLLastMapDebugMaskDefaultsKey];
+ }
+ }
+
+ [self.offlinePacksArrayController unbind:@"content"];
+}
+
+#pragma mark Services
+
+- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
+ // mapboxgl://?center=29.95,-90.066667&zoom=14&bearing=45&pitch=30
+ 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];
+ }
+ }
+
+ MGLMapCamera *camera = [MGLMapCamera camera];
+ NSString *zoomLevelString = params[@"zoom"];
+ self.pendingZoomLevel = zoomLevelString.length ? zoomLevelString.doubleValue : -1;
+
+ NSString *directionString = params[@"bearing"];
+ if (directionString.length) {
+ camera.heading = directionString.doubleValue;
+ }
+
+ NSString *centerString = params[@"center"];
+ if (centerString) {
+ NSArray *coordinateValues = [centerString componentsSeparatedByString:@","];
+ if (coordinateValues.count == 2) {
+ camera.centerCoordinate = CLLocationCoordinate2DMake([coordinateValues[0] doubleValue],
+ [coordinateValues[1] doubleValue]);
+ }
+ }
+
+ NSString *pitchString = params[@"pitch"];
+ if (pitchString.length) {
+ camera.pitch = pitchString.doubleValue;
+ }
+
+ self.pendingCamera = camera;
+ [[NSDocumentController sharedDocumentController] openUntitledDocumentAndDisplay:YES error:NULL];
+}
+
+#pragma mark Offline pack management
+
+- (IBAction)showOfflinePacksPanel:(id)sender {
+ [self.offlinePacksPanel makeKeyAndOrderFront:sender];
+
+ for (MGLOfflinePack *pack in self.offlinePacksArrayController.arrangedObjects) {
+ [pack requestProgress];
+ }
+}
+
+- (IBAction)delete:(id)sender {
+ for (MGLOfflinePack *pack in self.offlinePacksArrayController.selectedObjects) {
+ [[MGLOfflineStorage sharedOfflineStorage] removePack:pack withCompletionHandler:^(NSError * _Nullable error) {
+ if (error) {
+ [[NSAlert alertWithError:error] runModal];
+ }
+ }];
+ }
+}
+
+- (IBAction)chooseOfflinePack:(id)sender {
+ for (MGLOfflinePack *pack in self.offlinePacksArrayController.selectedObjects) {
+ switch (pack.state) {
+ case MGLOfflinePackStateComplete:
+ {
+ if ([pack.region isKindOfClass:[MGLTilePyramidOfflineRegion class]]) {
+ MGLTilePyramidOfflineRegion *region = (MGLTilePyramidOfflineRegion *)pack.region;
+ self.pendingVisibleCoordinateBounds = region.bounds;
+ self.pendingMinimumZoomLevel = region.minimumZoomLevel;
+ self.pendingMaximumZoomLevel = region.maximumZoomLevel;
+ [[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 {
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.messageText = @"Mapbox GL Help";
+ alert.informativeText = @"\
+• To scroll, swipe with two fingers on a trackpad, or drag the cursor, or press the arrow keys.\n\
+• To zoom in, pinch two fingers apart on a trackpad, or double-click, or hold down Shift while dragging the cursor down, or hold down Option while pressing the up key.\n\
+• To zoom out, pinch two fingers together on a trackpad, or double-tap on a mouse, or hold down Shift while dragging the cursor up, or hold down Option while pressing the down key.\n\
+• To rotate, move two fingers opposite each other in a circle on a trackpad, 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)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 User interface validation
+
+- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
+ if (menuItem.action == @selector(showShortcuts:)) {
+ return YES;
+ }
+ if (menuItem.action == @selector(showPreferences:)) {
+ return YES;
+ }
+ if (menuItem.action == @selector(showOfflinePacksPanel:)) {
+ return YES;
+ }
+ if (menuItem.action == @selector(delete:)) {
+ return self.offlinePacksArrayController.selectedObjects.count;
+ }
+ return NO;
+}
+
+@end
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon128x128.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon128x128.png
new file mode 100644
index 0000000000..145d5a7d85
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon128x128.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon16x16.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon16x16.png
new file mode 100644
index 0000000000..fa2588dec3
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon16x16.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256-1.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256-1.png
new file mode 100644
index 0000000000..18fec77f84
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256-1.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256.png
new file mode 100644
index 0000000000..18fec77f84
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon256x256.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32-1.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32-1.png
new file mode 100644
index 0000000000..bf3acc1282
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32-1.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32.png
new file mode 100644
index 0000000000..bf3acc1282
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon32x32.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512-1.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512-1.png
new file mode 100644
index 0000000000..1ea7683696
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512-1.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512.png
new file mode 100644
index 0000000000..1ea7683696
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/AppIcon512x512.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/Contents.json b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..58e739d056
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "AppIcon16x16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "AppIcon32x32-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "AppIcon32x32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "icon-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "AppIcon128x128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "AppIcon256x256-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "AppIcon256x256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "AppIcon512x512-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "AppIcon512x512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "icon.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon-1.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon-1.png
new file mode 100644
index 0000000000..36dd7acf90
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon-1.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon.png b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon.png
new file mode 100644
index 0000000000..fdee900aa4
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/AppIcon.appiconset/icon.png
Binary files differ
diff --git a/platform/macos/app/Assets.xcassets/Contents.json b/platform/macos/app/Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..da4a164c91
--- /dev/null
+++ b/platform/macos/app/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/platform/macos/app/Base.lproj/MainMenu.xib b/platform/macos/app/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000000..4afb3b244e
--- /dev/null
+++ b/platform/macos/app/Base.lproj/MainMenu.xib
@@ -0,0 +1,845 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15E65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/>
+ </dependencies>
+ <objects>
+ <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
+ <connections>
+ <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
+ </connections>
+ </customObject>
+ <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+ <customObject id="-3" userLabel="Application" customClass="NSObject">
+ <connections>
+ <outlet property="delegate" destination="Voe-Tx-rLC" id="z9N-Fm-MUP"/>
+ </connections>
+ </customObject>
+ <customObject id="Voe-Tx-rLC" customClass="AppDelegate">
+ <connections>
+ <outlet property="offlinePacksArrayController" destination="dWe-R6-sRz" id="Ar5-xu-ABm"/>
+ <outlet property="offlinePacksPanel" destination="Jjv-gs-Tx6" id="0vK-rR-3ZX"/>
+ <outlet property="preferencesWindow" destination="UWc-yQ-qda" id="Ota-aT-Mz2"/>
+ </connections>
+ </customObject>
+ <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
+ <items>
+ <menuItem title="Mapbox GL" id="1Xt-HY-uBw">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Mapbox GL" systemMenu="apple" id="uQy-DD-JDr">
+ <items>
+ <menuItem title="About Mapbox GL" id="5kV-Vb-QxS">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
+ <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW">
+ <connections>
+ <action selector="showPreferences:" target="-1" id="Llx-Uy-HTS"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
+ <menuItem title="Services" id="NMo-om-nkz">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
+ <menuItem title="Hide Mapbox GL" keyEquivalent="h" id="Olw-nP-bQN">
+ <connections>
+ <action selector="hide:" target="-1" id="PnN-Uc-m68"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Show All" id="Kd2-mp-pUS">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
+ <menuItem title="Quit Mapbox GL" keyEquivalent="q" id="4sb-4s-VLi">
+ <connections>
+ <action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="File" id="dMs-cI-mzQ">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="File" id="bib-Uj-vzu">
+ <items>
+ <menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
+ <connections>
+ <action selector="newDocument:" target="-1" id="4Si-XN-c54"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9">
+ <connections>
+ <action selector="openDocument:" target="-1" id="bVn-NM-KNZ"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Open Recent" id="tXI-mr-wws">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ">
+ <items>
+ <menuItem title="Clear Menu" id="vNY-rz-j42">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="clearRecentDocuments:" target="-1" id="Daa-9d-B3U"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
+ <menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
+ <connections>
+ <action selector="performClose:" target="-1" id="HmO-Ls-i7Q"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV">
+ <connections>
+ <action selector="saveDocument:" target="-1" id="teZ-XB-qJY"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A">
+ <connections>
+ <action selector="saveDocumentAs:" target="-1" id="mDf-zr-I0C"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Save Offline Pack…" id="UXB-sj-C7i">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="addOfflinePack:" target="-1" id="Usu-xO-QEx"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Revert to Saved" id="KaW-ft-85H">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="revertDocumentToSaved:" target="-1" id="iJ3-Pv-kwq"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
+ <menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
+ <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
+ <connections>
+ <action selector="runPageLayout:" target="-1" id="Din-rz-gC5"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
+ <connections>
+ <action selector="print:" target="-1" id="qaZ-4w-aoO"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Edit" id="5QF-Oa-p0T">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Edit" id="W48-6f-4Dl">
+ <items>
+ <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
+ <connections>
+ <action selector="undo:" target="-1" id="M6e-cu-g7V"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
+ <connections>
+ <action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
+ <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
+ <connections>
+ <action selector="cut:" target="-1" id="YJe-68-I9s"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
+ <connections>
+ <action selector="copy:" target="-1" id="G1f-GL-Joy"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
+ <connections>
+ <action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Delete" id="pa3-QI-u2k">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
+ <connections>
+ <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
+ <menuItem title="Find" id="4EN-yA-p0u">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Find" id="1b7-l0-nxx">
+ <items>
+ <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
+ <connections>
+ <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
+ <items>
+ <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
+ <connections>
+ <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
+ <connections>
+ <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
+ <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Substitutions" id="9ic-FL-obx">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
+ <items>
+ <menuItem title="Show Substitutions" id="z6F-FW-3nz">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
+ <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smart Quotes" id="hQb-2v-fYv">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smart Dashes" id="rgM-f4-ycn">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smart Links" id="cwL-P1-jid">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Data Detectors" id="tRr-pd-1PS">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Text Replacement" id="HFQ-gK-NFA">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Transformations" id="2oI-Rn-ZJC">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Transformations" id="c8a-y6-VQd">
+ <items>
+ <menuItem title="Make Upper Case" id="vmV-6d-7jI">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Make Lower Case" id="d9M-CD-aMd">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Capitalize" id="UEZ-Bs-lqG">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Speech" id="xrE-MZ-jX0">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Speech" id="3rS-ZA-NoH">
+ <items>
+ <menuItem title="Start Speaking" id="Ynk-f8-cLZ">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Stop Speaking" id="Oyz-dy-DGm">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="View" id="H8h-7b-M4v">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="View" id="HyV-fh-RgO">
+ <items>
+ <menuItem title="Streets" state="on" tag="1" keyEquivalent="1" id="17N-yz-NNo">
+ <connections>
+ <action selector="setStyle:" target="-1" id="I4L-Wx-UXA"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Outdoors" tag="2" keyEquivalent="2" id="BBa-Qa-SQr">
+ <connections>
+ <action selector="setStyle:" target="-1" id="rM1-yG-t5u"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Light" tag="3" keyEquivalent="3" id="HWe-7u-UVJ">
+ <connections>
+ <action selector="setStyle:" target="-1" id="Q9V-O1-oRz"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Dark" tag="4" keyEquivalent="4" id="6HI-q6-AeV">
+ <connections>
+ <action selector="setStyle:" target="-1" id="YfH-1I-G50"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Satellite" tag="5" keyEquivalent="5" id="h0J-5X-kgF">
+ <connections>
+ <action selector="setStyle:" target="-1" id="GXt-oK-Hy1"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Satellite Streets" tag="6" keyEquivalent="6" id="9BL-00-HFt">
+ <connections>
+ <action selector="setStyle:" target="-1" id="oL4-AC-waq"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Custom Style…" id="L0h-86-2cU">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="chooseCustomStyle:" target="-1" id="QJF-fM-Ty3"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="BMF-ml-0Bd"/>
+ <menuItem title="Zoom In" keyEquivalent="+" id="W82-WO-xvB">
+ <connections>
+ <action selector="zoomIn:" target="-1" id="g33-vK-zUu"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Zoom Out" keyEquivalent="-" id="j7h-PY-edM">
+ <connections>
+ <action selector="zoomOut:" target="-1" id="0pP-tO-9ex"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Snap to North" keyEquivalent="" id="Zss-3w-wkz">
+ <connections>
+ <action selector="snapToNorth:" target="-1" id="Ayq-GE-Lb5"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="mkP-YN-G0w"/>
+ <menuItem title="Reload" keyEquivalent="r" id="JvI-nv-KaE">
+ <connections>
+ <action selector="reload:" target="-1" id="xkh-9F-mOe"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="CyM-Wv-Bnc"/>
+ <menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="toggleToolbarShown:" target="-1" id="BXY-wc-z0C"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Debug" id="ZNC-5r-eBw">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Debug" id="McE-ka-r79">
+ <items>
+ <menuItem title="Show Tile Boundaries" id="rDE-dG-rTR">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleTileBoundaries:" target="-1" id="LAO-88-F7h"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Show Tile Info" id="LoH-qD-kb0">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleTileInfo:" target="-1" id="KCn-0G-V87"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Show Tile Timestamps" id="bY0-2E-LZ7">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleTileTimestamps:" target="-1" id="tBs-2N-KEG"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Show Collision Boxes" id="Y0b-3K-mJE">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleCollisionBoxes:" target="-1" id="EYa-7n-iWZ"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Show Wireframes" id="hSX-Be-8xC">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleWireframes:" target="-1" id="usj-ug-upt"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="2EG-Hp-4FA"/>
+ <menuItem title="Show Color Buffer" id="Eao-WE-BWz">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="showColorBuffer:" target="-1" id="Nuq-Qs-98g"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Show Stencil Buffer" id="LlS-Yh-RkN">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="showStencilBuffer:" target="-1" id="WkN-t9-Mpv"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="dYw-bb-tr1"/>
+ <menuItem title="Show Tooltips on Dropped Pins" id="uir-Rx-zmw">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleShowsToolTipsOnDroppedPins:" target="-1" id="1YC-Co-QQ6"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Use Random Cursors for Dropped Pins" id="ZTk-lc-Jgu">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleRandomizesCursorsOnDroppedPins:" target="-1" id="Mpw-b8-oub"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="Sl5-nE-kHd"/>
+ <menuItem title="Blanket Map With Pins" id="LMZ-oe-Ngh">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="dropManyPins:" target="-1" id="Rtv-8N-3Z8"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Add Polygon and Polyline" id="DVr-vT-lpe">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="drawPolygonAndPolyLineAnnotations:" target="-1" id="EhT-CB-gee"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Remove All Annotations" id="6rC-68-vk0">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="removeAllAnnotations:" target="-1" id="6v3-0E-LsR"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="wQq-Mx-QY0"/>
+ <menuItem title="Start World Tour" id="VFo-Jh-2sw">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="startWorldTour:" target="-1" id="66Y-Gm-Yn1"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Stop World Tour" id="Pa8-qU-xfr">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="stopWorldTour:" target="-1" id="aq0-7t-AGi"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Window" id="aUF-d1-5bR">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
+ <items>
+ <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
+ <connections>
+ <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Zoom" keyEquivalent="z" id="R4o-n2-Eq4">
+ <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
+ <connections>
+ <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="Uix-g7-fAt"/>
+ <menuItem title="Offline Packs" id="YW3-jR-knj">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="showOfflinePacksPanel:" target="Voe-Tx-rLC" id="kj9-ht-KmF"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
+ <menuItem title="Bring All to Front" id="LE2-aR-0XJ">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Help" id="wpr-3q-Mcd">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
+ <items>
+ <menuItem title="Mapbox GL Help" keyEquivalent="?" id="FKE-Sm-Kum">
+ <connections>
+ <action selector="showShortcuts:" target="-1" id="hNZ-sm-X2q"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="EpY-wQ-SjH"/>
+ <menuItem title="Improve This Map" id="xu5-WN-qYK">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="giveFeedback:" target="-1" id="cil-i9-r39"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ </items>
+ </menu>
+ <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"/>
+ <rect key="contentRect" x="109" y="131" width="350" height="84"/>
+ <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/>
+ <view key="contentView" id="eA4-n3-qPe">
+ <rect key="frame" x="0.0" y="0.0" width="350" height="84"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0IK-AW-Gg3">
+ <rect key="frame" x="18" y="45" width="89" height="17"/>
+ <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Access token:" id="Ptd-FI-M5A">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ <connections>
+ <accessibilityConnection property="link" destination="7sb-sf-oJU" id="U0t-jC-oQ7"/>
+ </connections>
+ </textField>
+ <textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7sb-sf-oJU">
+ <rect key="frame" x="113" y="42" width="197" height="22"/>
+ <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" id="jlV-TC-NUv">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ <connections>
+ <binding destination="45S-yT-WUN" name="value" keyPath="values.MGLMapboxAccessToken" id="iJE-S2-ALY"/>
+ </connections>
+ </textField>
+ <button translatesAutoresizingMaskIntoConstraints="NO" id="c3S-LC-PoX">
+ <rect key="frame" x="318" y="47" width="12" height="12"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="12" id="M3J-pU-gKn"/>
+ </constraints>
+ <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSFollowLinkFreestandingTemplate" imagePosition="only" alignment="center" controlSize="small" imageScaling="proportionallyUpOrDown" inset="2" id="38x-37-Ay0">
+ <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
+ <font key="font" metaFont="smallSystem"/>
+ </buttonCell>
+ <connections>
+ <action selector="openAccessTokenManager:" target="-1" id="1LX-4G-roC"/>
+ </connections>
+ </button>
+ <button translatesAutoresizingMaskIntoConstraints="NO" id="7IZ-zl-iT1">
+ <rect key="frame" x="18" y="18" width="109" height="18"/>
+ <buttonCell key="cell" type="check" title="Scroll to zoom" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="hVR-66-JSh">
+ <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
+ <font key="font" metaFont="system"/>
+ </buttonCell>
+ <connections>
+ <binding destination="45S-yT-WUN" name="value" keyPath="values.MGLScrollWheelZoomsMapView" id="2AZ-bk-DM5"/>
+ </connections>
+ </button>
+ </subviews>
+ <constraints>
+ <constraint firstAttribute="trailing" secondItem="c3S-LC-PoX" secondAttribute="trailing" constant="20" symbolic="YES" id="7QU-Jd-Rg6"/>
+ <constraint firstItem="c3S-LC-PoX" firstAttribute="top" secondItem="eA4-n3-qPe" secondAttribute="top" constant="25" id="JOS-HU-27c"/>
+ <constraint firstItem="7sb-sf-oJU" firstAttribute="leading" secondItem="0IK-AW-Gg3" secondAttribute="trailing" constant="8" symbolic="YES" id="SS6-VQ-sLK"/>
+ <constraint firstItem="0IK-AW-Gg3" firstAttribute="leading" secondItem="eA4-n3-qPe" secondAttribute="leading" constant="20" symbolic="YES" id="TYG-io-qfV"/>
+ <constraint firstItem="7sb-sf-oJU" firstAttribute="top" secondItem="eA4-n3-qPe" secondAttribute="top" constant="20" symbolic="YES" id="Vzb-q8-ecP"/>
+ <constraint firstItem="7IZ-zl-iT1" firstAttribute="leading" secondItem="0IK-AW-Gg3" secondAttribute="leading" id="aIY-WX-AW9"/>
+ <constraint firstItem="7IZ-zl-iT1" firstAttribute="top" secondItem="7sb-sf-oJU" secondAttribute="bottom" constant="8" symbolic="YES" id="ide-24-GqL"/>
+ <constraint firstItem="c3S-LC-PoX" firstAttribute="leading" secondItem="7sb-sf-oJU" secondAttribute="trailing" constant="8" symbolic="YES" id="pjl-9u-IgM"/>
+ <constraint firstItem="7sb-sf-oJU" firstAttribute="baseline" secondItem="0IK-AW-Gg3" secondAttribute="baseline" id="qIY-Jr-9Ws"/>
+ <constraint firstAttribute="bottom" secondItem="7IZ-zl-iT1" secondAttribute="bottom" constant="20" symbolic="YES" id="wng-pn-VIz"/>
+ <constraint firstItem="7sb-sf-oJU" firstAttribute="centerY" secondItem="c3S-LC-PoX" secondAttribute="centerY" id="zej-gw-fC0"/>
+ </constraints>
+ </view>
+ <connections>
+ <outlet property="initialFirstResponder" destination="7sb-sf-oJU" id="UZe-di-dnA"/>
+ </connections>
+ <point key="canvasLocation" x="754" y="221"/>
+ </window>
+ <userDefaultsController representsSharedInstance="YES" id="45S-yT-WUN"/>
+ <window title="Offline Packs" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="MBXOfflinePacksPanel" animationBehavior="default" id="Jjv-gs-Tx6" customClass="NSPanel">
+ <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" utility="YES"/>
+ <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
+ <rect key="contentRect" x="830" y="430" width="400" height="300"/>
+ <rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/>
+ <view key="contentView" id="8ha-hw-zOD">
+ <rect key="frame" x="0.0" y="0.0" width="400" height="300"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <scrollView autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Q8b-0e-dLv">
+ <rect key="frame" x="-1" y="20" width="402" height="281"/>
+ <clipView key="contentView" id="J9U-Yx-o2S">
+ <rect key="frame" x="1" y="0.0" width="400" height="280"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" autosaveColumns="NO" headerView="MAZ-Iq-hBi" id="Ato-Vu-HYT">
+ <rect key="frame" x="0.0" y="0.0" width="400" height="257"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <size key="intercellSpacing" width="3" height="2"/>
+ <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
+ <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
+ <tableColumns>
+ <tableColumn identifier="" editable="NO" width="16" minWidth="10" maxWidth="3.4028234663852886e+38" id="xtw-hQ-8C5" userLabel="State">
+ <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
+ <font key="font" metaFont="smallSystem"/>
+ <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
+ </tableHeaderCell>
+ <imageCell key="dataCell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="edU-Yw-20f"/>
+ <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
+ <connections>
+ <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.stateImage" id="2wd-1J-TZt"/>
+ </connections>
+ </tableColumn>
+ <tableColumn editable="NO" width="116" minWidth="40" maxWidth="1000" id="2hD-LN-h0L">
+ <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Name">
+ <font key="font" metaFont="smallSystem"/>
+ <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
+ </tableHeaderCell>
+ <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="oys-QZ-34I">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
+ <connections>
+ <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.context" id="NtD-s5-ZUq">
+ <dictionary key="options">
+ <string key="NSValueTransformerName">OfflinePackNameValueTransformer</string>
+ </dictionary>
+ </binding>
+ </connections>
+ </tableColumn>
+ <tableColumn editable="NO" width="50" minWidth="40" maxWidth="1000" id="pkI-c7-xoD">
+ <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" title="Downloaded">
+ <font key="font" metaFont="smallSystem"/>
+ <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
+ </tableHeaderCell>
+ <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="WfC-qb-HsW">
+ <numberFormatter key="formatter" formatterBehavior="default10_4" numberStyle="decimal" usesGroupingSeparator="NO" groupingSize="0" minimumIntegerDigits="0" maximumIntegerDigits="42" id="sNm-Qn-ne6"/>
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
+ <connections>
+ <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfResourcesCompleted" id="mu6-Jg-GiU"/>
+ </connections>
+ </tableColumn>
+ <tableColumn identifier="" editable="NO" width="50" minWidth="10" maxWidth="3.4028234663852886e+38" id="Rrd-A9-jqc">
+ <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Total">
+ <font key="font" metaFont="smallSystem"/>
+ <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
+ </tableHeaderCell>
+ <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="mHy-qJ-rOA">
+ <numberFormatter key="formatter" formatterBehavior="default10_4" numberStyle="decimal" usesGroupingSeparator="NO" groupingSize="0" minimumIntegerDigits="0" maximumIntegerDigits="42" id="kyx-ZP-OBH"/>
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
+ <connections>
+ <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfResourcesExpected" id="mh2-k0-vvB"/>
+ </connections>
+ </tableColumn>
+ <tableColumn identifier="" editable="NO" width="60" minWidth="10" maxWidth="3.4028234663852886e+38" id="h7m-6l-KaS">
+ <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Size">
+ <font key="font" metaFont="smallSystem"/>
+ <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
+ </tableHeaderCell>
+ <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="701-bg-k6L">
+ <byteCountFormatter key="formatter" allowsNonnumericFormatting="NO" id="IXV-J9-sP3"/>
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
+ <connections>
+ <binding destination="dWe-R6-sRz" name="value" keyPath="arrangedObjects.countOfBytesCompleted" id="Zsa-Na-yFN"/>
+ </connections>
+ </tableColumn>
+ </tableColumns>
+ <connections>
+ <action trigger="doubleAction" selector="chooseOfflinePack:" target="-1" id="pUN-eT-zRT"/>
+ </connections>
+ </tableView>
+ </subviews>
+ <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
+ </clipView>
+ <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="QLr-6P-Ogs">
+ <rect key="frame" x="1" y="7" width="0.0" height="16"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </scroller>
+ <scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="q0K-eE-mzL">
+ <rect key="frame" x="224" y="17" width="15" height="102"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </scroller>
+ <tableHeaderView key="headerView" id="MAZ-Iq-hBi">
+ <rect key="frame" x="0.0" y="0.0" width="400" height="23"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </tableHeaderView>
+ </scrollView>
+ <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wzf-ce-Spm">
+ <rect key="frame" x="0.0" y="-1" width="21" height="21"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="21" id="5ST-tY-8Ph"/>
+ </constraints>
+ <buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSAddTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="sew-F7-i5T">
+ <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
+ <font key="font" metaFont="system"/>
+ </buttonCell>
+ <connections>
+ <action selector="addOfflinePack:" target="-1" id="SN0-PM-HoU"/>
+ </connections>
+ </button>
+ <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7L7-hr-zId">
+ <rect key="frame" x="20" y="0.0" width="21" height="19"/>
+ <constraints>
+ <constraint firstAttribute="width" constant="21" id="JYb-AF-8gZ"/>
+ </constraints>
+ <buttonCell key="cell" type="smallSquare" bezelStyle="smallSquare" image="NSRemoveTemplate" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="oTF-3m-6qT">
+ <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
+ <font key="font" metaFont="system"/>
+ <string key="keyEquivalent" base64-UTF8="YES">
+CA
+</string>
+ </buttonCell>
+ <connections>
+ <action selector="delete:" target="-1" id="EGL-bf-yUD"/>
+ </connections>
+ </button>
+ </subviews>
+ <constraints>
+ <constraint firstItem="7L7-hr-zId" firstAttribute="centerY" secondItem="wzf-ce-Spm" secondAttribute="centerY" id="7TI-6w-bf1"/>
+ <constraint firstAttribute="bottom" secondItem="Q8b-0e-dLv" secondAttribute="bottom" constant="20" symbolic="YES" id="DZa-ly-bhV"/>
+ <constraint firstItem="wzf-ce-Spm" firstAttribute="top" secondItem="Q8b-0e-dLv" secondAttribute="bottom" id="LhK-5z-CQA"/>
+ <constraint firstItem="Q8b-0e-dLv" firstAttribute="leading" secondItem="8ha-hw-zOD" secondAttribute="leading" constant="-1" id="Oyo-ch-rZo"/>
+ <constraint firstAttribute="bottom" secondItem="7L7-hr-zId" secondAttribute="bottom" id="TtY-j1-T5h"/>
+ <constraint firstItem="Q8b-0e-dLv" firstAttribute="top" secondItem="8ha-hw-zOD" secondAttribute="top" constant="-1" id="WDk-Ig-Grr"/>
+ <constraint firstAttribute="trailing" secondItem="Q8b-0e-dLv" secondAttribute="trailing" constant="-1" id="hHf-rd-Wcv"/>
+ <constraint firstItem="7L7-hr-zId" firstAttribute="leading" secondItem="8ha-hw-zOD" secondAttribute="leading" constant="20" symbolic="YES" id="iKJ-ph-ACS"/>
+ <constraint firstAttribute="bottom" secondItem="wzf-ce-Spm" secondAttribute="bottom" constant="-1" id="jFV-Xi-fWr"/>
+ <constraint firstItem="wzf-ce-Spm" firstAttribute="leading" secondItem="8ha-hw-zOD" secondAttribute="leading" id="kJt-oJ-72R"/>
+ </constraints>
+ </view>
+ <point key="canvasLocation" x="720" y="317"/>
+ </window>
+ <arrayController objectClassName="MGLOfflinePack" editable="NO" avoidsEmptySelection="NO" id="dWe-R6-sRz" userLabel="Offline Packs Array Controller">
+ <declaredKeys>
+ <string>context</string>
+ <string>countOfResourcesCompleted</string>
+ <string>countOfResourcesExpected</string>
+ <string>countOfBytesCompleted</string>
+ <string>stateImage</string>
+ </declaredKeys>
+ </arrayController>
+ </objects>
+ <resources>
+ <image name="NSAddTemplate" width="11" height="11"/>
+ <image name="NSFollowLinkFreestandingTemplate" width="14" height="14"/>
+ <image name="NSRemoveTemplate" width="11" height="11"/>
+ </resources>
+</document>
diff --git a/platform/macos/app/Base.lproj/MapDocument.xib b/platform/macos/app/Base.lproj/MapDocument.xib
new file mode 100644
index 0000000000..9a3db47df6
--- /dev/null
+++ b/platform/macos/app/Base.lproj/MapDocument.xib
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15E65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/>
+ </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 allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" frameAutosaveName="MBXMapWindow" 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"/>
+ <outlet property="menu" destination="XbX-6a-Mgy" id="dSu-HR-Kq2"/>
+ </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="120" height="26"/>
+ <popUpButton key="view" verticalHuggingPriority="750" id="Tzm-Cy-dQg">
+ <rect key="frame" x="0.0" y="14" width="120" 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="Outdoors" 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="Satellite Streets" 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"/>
+ <connections>
+ <action selector="dropPin:" target="-1" id="hxx-eC-kqU"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Remove Pin" id="Zhx-30-VmE">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="removePin:" target="-1" id="w0R-0B-7mG"/>
+ </connections>
+ </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/macos/app/Credits.rtf b/platform/macos/app/Credits.rtf
new file mode 100644
index 0000000000..6b17eb34b2
--- /dev/null
+++ b/platform/macos/app/Credits.rtf
@@ -0,0 +1,9 @@
+{\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf130
+{\fonttbl\f0\fnil\fcharset0 SFUIText-Regular;}
+{\colortbl;\red255\green255\blue255;}
+\margl1440\margr1440\vieww10800\viewh8400\viewkind0
+\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0
+
+\f0\fs20 \cf0 Copyright \'a9 {\field{\*\fldinst{HYPERLINK "https://www.mapbox.com/about/maps/"}}{\fldrslt Mapbox}}.\
+\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0
+\cf0 Copyright \'a9 {\field{\*\fldinst{HYPERLINK "http://www.openstreetmap.org/about/"}}{\fldrslt OpenStreetMap contributors}}.} \ No newline at end of file
diff --git a/platform/macos/app/DroppedPinAnnotation.h b/platform/macos/app/DroppedPinAnnotation.h
new file mode 100644
index 0000000000..435a56738b
--- /dev/null
+++ b/platform/macos/app/DroppedPinAnnotation.h
@@ -0,0 +1,10 @@
+#import <Mapbox/Mapbox.h>
+
+@interface DroppedPinAnnotation : MGLPointAnnotation
+
+@property (nonatomic, readonly) NSTimeInterval elapsedShownTime;
+
+- (void)resume;
+- (void)pause;
+
+@end
diff --git a/platform/macos/app/DroppedPinAnnotation.m b/platform/macos/app/DroppedPinAnnotation.m
new file mode 100644
index 0000000000..5b19fd7401
--- /dev/null
+++ b/platform/macos/app/DroppedPinAnnotation.m
@@ -0,0 +1,68 @@
+#import "DroppedPinAnnotation.h"
+
+#import "LocationCoordinate2DTransformer.h"
+#import "TimeIntervalTransformer.h"
+
+#import <Mapbox/Mapbox.h>
+
+static MGLCoordinateFormatter *DroppedPinCoordinateFormatter;
+
+@implementation DroppedPinAnnotation {
+ NSTimer *_timer;
+ NSTimeInterval _priorShownTimeInterval;
+ NSDate *_dateShown;
+
+ NSValueTransformer *_timeIntervalTransformer;
+}
+
++ (void)initialize {
+ if (self == [DroppedPinAnnotation class]) {
+ DroppedPinCoordinateFormatter = [[MGLCoordinateFormatter alloc] init];
+ }
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _timeIntervalTransformer = [NSValueTransformer valueTransformerForName:
+ NSStringFromClass([TimeIntervalTransformer class])];
+ [self update:nil];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self pause];
+}
+
+- (void)setCoordinate:(CLLocationCoordinate2D)coordinate {
+ super.coordinate = coordinate;
+ [self update:nil];
+}
+
+- (NSTimeInterval)elapsedShownTime {
+ return _priorShownTimeInterval - _dateShown.timeIntervalSinceNow;
+}
+
+- (void)resume {
+ _dateShown = [NSDate date];
+ _timer = [NSTimer scheduledTimerWithTimeInterval:1
+ target:self
+ selector:@selector(update:)
+ userInfo:nil
+ repeats:YES];
+}
+
+- (void)pause {
+ [_timer invalidate];
+ _timer = nil;
+ _priorShownTimeInterval -= _dateShown.timeIntervalSinceNow;
+ _dateShown = nil;
+}
+
+- (void)update:(NSTimer *)timer {
+ NSString *coordinate = [DroppedPinCoordinateFormatter stringFromCoordinate:self.coordinate];
+ NSString *elapsedTime = [_timeIntervalTransformer transformedValue:@(self.elapsedShownTime)];
+ self.subtitle = [NSString stringWithFormat:@"%@\nSelected for %@", coordinate, elapsedTime];
+}
+
+@end
diff --git a/platform/macos/app/Info.plist b/platform/macos/app/Info.plist
new file mode 100644
index 0000000000..cc7037f589
--- /dev/null
+++ b/platform/macos/app/Info.plist
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</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>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>0.1.0</string>
+ <key>CFBundleSignature</key>
+ <string>MBGL</string>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleURLName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>mapboxgl</string>
+ </array>
+ </dict>
+ </array>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>
diff --git a/platform/macos/app/LocationCoordinate2DTransformer.h b/platform/macos/app/LocationCoordinate2DTransformer.h
new file mode 100644
index 0000000000..162325fbad
--- /dev/null
+++ b/platform/macos/app/LocationCoordinate2DTransformer.h
@@ -0,0 +1,5 @@
+#import <Foundation/Foundation.h>
+
+@interface LocationCoordinate2DTransformer : NSValueTransformer
+
+@end
diff --git a/platform/macos/app/LocationCoordinate2DTransformer.m b/platform/macos/app/LocationCoordinate2DTransformer.m
new file mode 100644
index 0000000000..59654f1676
--- /dev/null
+++ b/platform/macos/app/LocationCoordinate2DTransformer.m
@@ -0,0 +1,31 @@
+#import "LocationCoordinate2DTransformer.h"
+
+#import <Mapbox/Mapbox.h>
+
+@implementation LocationCoordinate2DTransformer {
+ MGLCoordinateFormatter *_coordinateFormatter;
+}
+
++ (Class)transformedValueClass {
+ return [NSString class];
+}
+
++ (BOOL)allowsReverseTransformation {
+ return NO;
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _coordinateFormatter = [[MGLCoordinateFormatter alloc] init];
+ }
+ return self;
+}
+
+- (id)transformedValue:(id)value {
+ if (![value isKindOfClass:[NSValue class]]) {
+ return nil;
+ }
+ return [_coordinateFormatter stringForObjectValue:value].capitalizedString;
+}
+
+@end
diff --git a/platform/macos/app/MapDocument.h b/platform/macos/app/MapDocument.h
new file mode 100644
index 0000000000..86ad05e6e2
--- /dev/null
+++ b/platform/macos/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/macos/app/MapDocument.m b/platform/macos/app/MapDocument.m
new file mode 100644
index 0000000000..9bff4603e4
--- /dev/null
+++ b/platform/macos/app/MapDocument.m
@@ -0,0 +1,721 @@
+#import "MapDocument.h"
+
+#import "AppDelegate.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 () <NSWindowDelegate, NSSharingServicePickerDelegate, NSMenuDelegate, MGLMapViewDelegate>
+
+@property (weak) IBOutlet NSMenu *mapViewContextMenu;
+
+@end
+
+@implementation MapDocument {
+ /// Style URL inherited from an existing document at the time this document
+ /// was created.
+ NSURL *_inheritedStyleURL;
+
+ NSPoint _mouseLocationForMapViewContextMenu;
+ NSUInteger _droppedPinCounter;
+ NSNumberFormatter *_spellOutNumberFormatter;
+
+ BOOL _showsToolTipsOnDroppedPins;
+ BOOL _randomizesCursorsOnDroppedPins;
+ BOOL _isTouringWorld;
+ BOOL _isShowingPolygonAndPolylineAnnotations;
+}
+
+#pragma mark Lifecycle
+
+- (NSString *)windowNibName {
+ return @"MapDocument";
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)windowControllerWillLoadNib:(NSWindowController *)windowController {
+ NSDocument *currentDocument = [NSDocumentController sharedDocumentController].currentDocument;
+ if ([currentDocument isKindOfClass:[MapDocument class]]) {
+ _inheritedStyleURL = [(MapDocument *)currentDocument mapView].styleURL;
+ }
+}
+
+- (void)windowControllerDidLoadNib:(NSWindowController *)controller {
+ [super windowControllerDidLoadNib:controller];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(userDefaultsDidChange:)
+ name:NSUserDefaultsDidChangeNotification
+ object:nil];
+
+ _spellOutNumberFormatter = [[NSNumberFormatter alloc] init];
+
+ NSPressGestureRecognizer *pressGestureRecognizer = [[NSPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)];
+ [self.mapView addGestureRecognizer:pressGestureRecognizer];
+
+ [self applyPendingState];
+}
+
+- (NSWindow *)window {
+ return self.windowControllers.firstObject.window;
+}
+
+- (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 NSWindowDelegate methods
+
+- (void)window:(NSWindow *)window willEncodeRestorableState:(NSCoder *)state {
+ [state encodeObject:self.mapView.styleURL forKey:@"MBXMapViewStyleURL"];
+}
+
+- (void)window:(NSWindow *)window didDecodeRestorableState:(NSCoder *)state {
+ self.mapView.styleURL = [state decodeObjectForKey:@"MBXMapViewStyleURL"];
+}
+
+#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 streetsStyleURLWithVersion:MGLStyleDefaultVersion];
+ break;
+ case 2:
+ styleURL = [MGLStyle outdoorsStyleURLWithVersion:MGLStyleDefaultVersion];
+ break;
+ case 3:
+ styleURL = [MGLStyle lightStyleURLWithVersion:MGLStyleDefaultVersion];
+ break;
+ case 4:
+ styleURL = [MGLStyle darkStyleURLWithVersion:MGLStyleDefaultVersion];
+ break;
+ case 5:
+ styleURL = [MGLStyle satelliteStyleURLWithVersion:MGLStyleDefaultVersion];
+ break;
+ case 6:
+ styleURL = [MGLStyle satelliteStreetsStyleURLWithVersion:MGLStyleDefaultVersion];
+ 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, 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"];
+ [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];
+}
+
+- (void)applyPendingState {
+ if (_inheritedStyleURL) {
+ self.mapView.styleURL = _inheritedStyleURL;
+ _inheritedStyleURL = nil;
+ }
+
+ AppDelegate *appDelegate = (AppDelegate *)NSApp.delegate;
+ if (appDelegate.pendingStyleURL) {
+ self.mapView.styleURL = appDelegate.pendingStyleURL;
+ }
+ if (appDelegate.pendingCamera) {
+ if (appDelegate.pendingZoomLevel >= 0) {
+ self.mapView.zoomLevel = appDelegate.pendingZoomLevel;
+ appDelegate.pendingCamera.altitude = self.mapView.camera.altitude;
+ }
+ self.mapView.camera = appDelegate.pendingCamera;
+ 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;
+ }
+ if (appDelegate.pendingMinimumZoomLevel >= 0) {
+ self.mapView.zoomLevel = MAX(appDelegate.pendingMinimumZoomLevel, self.mapView.zoomLevel);
+ appDelegate.pendingMaximumZoomLevel = -1;
+ }
+ if (appDelegate.pendingMaximumZoomLevel >= 0) {
+ self.mapView.zoomLevel = MIN(appDelegate.pendingMaximumZoomLevel, self.mapView.zoomLevel);
+ appDelegate.pendingMaximumZoomLevel = -1;
+ }
+
+ // Temporarily set the display name to the default center coordinate instead
+ // of “Untitled” until the binding kicks in.
+ NSValue *coordinateValue = [NSValue valueWithMGLCoordinate:self.mapView.centerCoordinate];
+ self.displayName = [[NSValueTransformer valueTransformerForName:@"LocationCoordinate2DTransformer"]
+ transformedValue:coordinateValue];
+}
+
+#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)toggleWireframes:(id)sender {
+ self.mapView.debugMask ^= MGLMapDebugWireframesMask;
+}
+
+- (IBAction)showColorBuffer:(id)sender {
+ self.mapView.debugMask &= ~MGLMapDebugStencilBufferMask;
+}
+
+- (IBAction)showStencilBuffer:(id)sender {
+ self.mapView.debugMask |= MGLMapDebugStencilBufferMask;
+}
+
+- (IBAction)toggleShowsToolTipsOnDroppedPins:(id)sender {
+ _showsToolTipsOnDroppedPins = !_showsToolTipsOnDroppedPins;
+}
+
+- (IBAction)toggleRandomizesCursorsOnDroppedPins:(id)sender {
+ _randomizesCursorsOnDroppedPins = !_randomizesCursorsOnDroppedPins;
+}
+
+- (IBAction)dropManyPins:(id)sender {
+ [self removeAllAnnotations:sender];
+
+ 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)removeAllAnnotations:(id)sender {
+ [self.mapView removeAnnotations:self.mapView.annotations];
+ _isShowingPolygonAndPolylineAnnotations = NO;
+}
+
+- (IBAction)startWorldTour:(id)sender {
+ _isTouringWorld = YES;
+
+ [self removeAllAnnotations: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;
+}
+
+- (IBAction)drawPolygonAndPolyLineAnnotations:(id)sender {
+
+ if (_isShowingPolygonAndPolylineAnnotations) {
+ [self removeAllAnnotations:sender];
+ return;
+ }
+
+ _isShowingPolygonAndPolylineAnnotations = YES;
+
+ // Pacific Northwest triangle
+ CLLocationCoordinate2D triangleCoordinates[3] = {
+ CLLocationCoordinate2DMake(44, -122),
+ CLLocationCoordinate2DMake(46, -122),
+ CLLocationCoordinate2DMake(46, -121)
+ };
+ MGLPolygon *triangle = [MGLPolygon polygonWithCoordinates:triangleCoordinates count:3];
+ [self.mapView addAnnotation:triangle];
+
+ // West coast line
+ CLLocationCoordinate2D lineCoordinates[4] = {
+ CLLocationCoordinate2DMake(47.6025, -122.3327),
+ CLLocationCoordinate2DMake(45.5189, -122.6726),
+ CLLocationCoordinate2DMake(37.7790, -122.4177),
+ CLLocationCoordinate2DMake(34.0532, -118.2349)
+ };
+ MGLPolyline *line = [MGLPolyline polylineWithCoordinates:lineCoordinates count:4];
+ [self.mapView addAnnotation:line];
+}
+
+#pragma mark Offline packs
+
+- (IBAction)addOfflinePack:(id)sender {
+ NSAlert *namePrompt = [[NSAlert alloc] init];
+ namePrompt.messageText = @"Add offline pack";
+ namePrompt.informativeText = @"Choose a name for the pack:";
+ NSTextField *nameTextField = [[NSTextField alloc] initWithFrame:NSZeroRect];
+ nameTextField.placeholderString = MGLStringFromCoordinateBounds(self.mapView.visibleCoordinateBounds);
+ [nameTextField sizeToFit];
+ NSRect textFieldFrame = nameTextField.frame;
+ textFieldFrame.size.width = 300;
+ nameTextField.frame = textFieldFrame;
+ namePrompt.accessoryView = nameTextField;
+ [namePrompt addButtonWithTitle:@"Add"];
+ [namePrompt addButtonWithTitle:@"Cancel"];
+ if ([namePrompt runModal] != NSAlertFirstButtonReturn) {
+ return;
+ }
+
+ id <MGLOfflineRegion> region = [[MGLTilePyramidOfflineRegion alloc] initWithStyleURL:self.mapView.styleURL bounds:self.mapView.visibleCoordinateBounds fromZoomLevel:self.mapView.zoomLevel toZoomLevel:self.mapView.maximumZoomLevel];
+ NSData *context = [[NSValueTransformer valueTransformerForName:@"OfflinePackNameValueTransformer"] reverseTransformedValue:nameTextField.stringValue];
+ [[MGLOfflineStorage sharedOfflineStorage] addPackForRegion:region withContext:context completionHandler:^(MGLOfflinePack * _Nullable pack, NSError * _Nullable error) {
+ if (error) {
+ [[NSAlert alertWithError:error] runModal];
+ } else {
+ [pack resume];
+ }
+ }];
+}
+
+#pragma mark Help methods
+
+- (IBAction)giveFeedback:(id)sender {
+ 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 + 1)]];
+ [[NSWorkspace sharedWorkspace] openURL:feedbackURL];
+}
+
+#pragma mark Mouse events
+
+- (void)handlePressGesture:(NSPressGestureRecognizer *)gestureRecognizer {
+ if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
+ NSPoint location = [gestureRecognizer locationInView:self.mapView];
+ if (!NSPointInRect([gestureRecognizer locationInView:self.mapView.compass], self.mapView.compass.bounds)
+ && !NSPointInRect([gestureRecognizer locationInView:self.mapView.zoomControls], self.mapView.zoomControls.bounds)
+ && !NSPointInRect([gestureRecognizer locationInView:self.mapView.attributionView], self.mapView.attributionView.bounds)) {
+ [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 streetsStyleURLWithVersion:MGLStyleDefaultVersion]];
+ break;
+ case 2:
+ state = [styleURL isEqual:[MGLStyle outdoorsStyleURLWithVersion:MGLStyleDefaultVersion]];
+ break;
+ case 3:
+ state = [styleURL isEqual:[MGLStyle lightStyleURLWithVersion:MGLStyleDefaultVersion]];
+ break;
+ case 4:
+ state = [styleURL isEqual:[MGLStyle darkStyleURLWithVersion:MGLStyleDefaultVersion]];
+ break;
+ case 5:
+ state = [styleURL isEqual:[MGLStyle satelliteStyleURLWithVersion:MGLStyleDefaultVersion]];
+ break;
+ case 6:
+ state = [styleURL isEqual:[MGLStyle satelliteStreetsStyleURLWithVersion:MGLStyleDefaultVersion]];
+ 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:)) {
+ id <MGLAnnotation> annotationUnderCursor = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu];
+ menuItem.hidden = annotationUnderCursor != nil;
+ return YES;
+ }
+ if (menuItem.action == @selector(removePin:)) {
+ id <MGLAnnotation> annotationUnderCursor = [self.mapView annotationAtPoint:_mouseLocationForMapViewContextMenu];
+ menuItem.hidden = annotationUnderCursor == nil;
+ 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(toggleWireframes:)) {
+ BOOL isShown = self.mapView.debugMask & MGLMapDebugWireframesMask;
+ menuItem.title = isShown ? @"Hide Wireframes" : @"Show Wireframes";
+ return YES;
+ }
+ if (menuItem.action == @selector(showColorBuffer:)) {
+ BOOL enabled = self.mapView.debugMask & MGLMapDebugStencilBufferMask;
+ menuItem.state = enabled ? NSOffState : NSOnState;
+ return YES;
+ }
+ if (menuItem.action == @selector(showStencilBuffer:)) {
+ BOOL enabled = self.mapView.debugMask & MGLMapDebugStencilBufferMask;
+ menuItem.state = enabled ? NSOnState : NSOffState;
+ 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(removeAllAnnotations:)) {
+ return self.mapView.annotations.count > 0;
+ }
+ if (menuItem.action == @selector(startWorldTour:)) {
+ return !_isTouringWorld;
+ }
+ if (menuItem.action == @selector(stopWorldTour:)) {
+ return _isTouringWorld;
+ }
+ 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;
+ }
+ return NO;
+}
+
+- (NSUInteger)indexOfStyleInToolbarItem {
+ if (![MGLAccountManager accessToken]) {
+ return NSNotFound;
+ }
+
+ NSArray *styleURLs = @[
+ [MGLStyle streetsStyleURLWithVersion:MGLStyleDefaultVersion],
+ [MGLStyle outdoorsStyleURLWithVersion:MGLStyleDefaultVersion],
+ [MGLStyle lightStyleURLWithVersion:MGLStyleDefaultVersion],
+ [MGLStyle darkStyleURLWithVersion:MGLStyleDefaultVersion],
+ [MGLStyle satelliteStyleURLWithVersion:MGLStyleDefaultVersion],
+ [MGLStyle satelliteStreetsStyleURLWithVersion:MGLStyleDefaultVersion],
+ ];
+ 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((uint32_t)cursors.count) % 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/macos/app/OfflinePackNameValueTransformer.h b/platform/macos/app/OfflinePackNameValueTransformer.h
new file mode 100644
index 0000000000..11fe3ff441
--- /dev/null
+++ b/platform/macos/app/OfflinePackNameValueTransformer.h
@@ -0,0 +1,5 @@
+#import <Foundation/Foundation.h>
+
+@interface OfflinePackNameValueTransformer : NSValueTransformer
+
+@end
diff --git a/platform/macos/app/OfflinePackNameValueTransformer.m b/platform/macos/app/OfflinePackNameValueTransformer.m
new file mode 100644
index 0000000000..2825e48ed3
--- /dev/null
+++ b/platform/macos/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/macos/app/TimeIntervalTransformer.h b/platform/macos/app/TimeIntervalTransformer.h
new file mode 100644
index 0000000000..ca88ad2cd1
--- /dev/null
+++ b/platform/macos/app/TimeIntervalTransformer.h
@@ -0,0 +1,5 @@
+#import <Foundation/Foundation.h>
+
+@interface TimeIntervalTransformer : NSValueTransformer
+
+@end
diff --git a/platform/macos/app/TimeIntervalTransformer.m b/platform/macos/app/TimeIntervalTransformer.m
new file mode 100644
index 0000000000..39177dc5bc
--- /dev/null
+++ b/platform/macos/app/TimeIntervalTransformer.m
@@ -0,0 +1,53 @@
+#import "TimeIntervalTransformer.h"
+
+@implementation TimeIntervalTransformer
+
++ (Class)transformedValueClass {
+ return [NSString class];
+}
+
++ (BOOL)allowsReverseTransformation {
+ return NO;
+}
+
+NSString *NumberAndUnitString(NSInteger quantity, NSString *singular, NSString *plural) {
+ return [NSString stringWithFormat:@"%ld %@", quantity, quantity == 1 ? singular : plural];
+}
+
+- (id)transformedValue:(id)value {
+ if (![value isKindOfClass:[NSValue class]]) {
+ return nil;
+ }
+
+ NSTimeInterval timeInterval = [value doubleValue];
+ NSInteger seconds = floor(timeInterval);
+ NSInteger minutes = floor(seconds / 60);
+ seconds -= minutes * 60;
+ NSInteger hours = floor(minutes / 60);
+ minutes -= hours * 60;
+ NSInteger days = floor(hours / 24);
+ hours -= days * 24;
+ NSInteger weeks = floor(days) / 7;
+ days -= weeks * 7;
+
+ NSMutableArray *components = [NSMutableArray array];
+ if (seconds || timeInterval < 60) {
+ [components addObject:NumberAndUnitString(seconds, @"second", @"seconds")];
+ }
+ if (minutes) {
+ [components insertObject:NumberAndUnitString(minutes, @"minute", @"minutes") atIndex:0];
+ }
+ if (hours) {
+ [components insertObject:NumberAndUnitString(hours, @"hour", @"hours") atIndex:0];
+ }
+ if (days) {
+ [components insertObject:NumberAndUnitString(days, @"day", @"days") atIndex:0];
+ }
+ if (weeks) {
+ [components insertObject:NumberAndUnitString(weeks, @"week", @"weeks") atIndex:0];
+ }
+
+ return [components componentsJoinedByString:@", "];
+}
+
+@end
diff --git a/platform/macos/app/main.m b/platform/macos/app/main.m
new file mode 100644
index 0000000000..8a6799b414
--- /dev/null
+++ b/platform/macos/app/main.m
@@ -0,0 +1,5 @@
+#import <Cocoa/Cocoa.h>
+
+int main(int argc, const char * argv[]) {
+ return NSApplicationMain(argc, argv);
+}