summaryrefslogtreecommitdiff
path: root/platform/osx/src
diff options
context:
space:
mode:
Diffstat (limited to 'platform/osx/src')
-rw-r--r--platform/osx/src/MGLAccountManager.m56
-rw-r--r--platform/osx/src/MGLAccountManager_Private.h10
-rw-r--r--platform/osx/src/MGLAnnotationImage.m25
-rw-r--r--platform/osx/src/MGLAttributionButton.h11
-rw-r--r--platform/osx/src/MGLAttributionButton.m46
-rw-r--r--platform/osx/src/MGLCompassCell.h5
-rw-r--r--platform/osx/src/MGLCompassCell.m31
-rw-r--r--platform/osx/src/MGLMapView+IBAdditions.m114
-rw-r--r--platform/osx/src/MGLMapView.mm1784
-rw-r--r--platform/osx/src/MGLMapView_Private.h12
-rw-r--r--platform/osx/src/MGLOpenGLLayer.h10
-rw-r--r--platform/osx/src/MGLOpenGLLayer.mm49
-rw-r--r--platform/osx/src/NSBundle+MGLAdditions.h15
-rw-r--r--platform/osx/src/NSBundle+MGLAdditions.m13
-rw-r--r--platform/osx/src/NSProcessInfo+MGLAdditions.h9
-rw-r--r--platform/osx/src/NSProcessInfo+MGLAdditions.m11
-rw-r--r--platform/osx/src/resources/MGLAnnotationCallout.xib53
-rw-r--r--platform/osx/src/resources/default_marker.pdfbin0 -> 2601 bytes
-rw-r--r--platform/osx/src/resources/mapbox.pdfbin0 -> 3762 bytes
19 files changed, 2254 insertions, 0 deletions
diff --git a/platform/osx/src/MGLAccountManager.m b/platform/osx/src/MGLAccountManager.m
new file mode 100644
index 0000000000..14969c101b
--- /dev/null
+++ b/platform/osx/src/MGLAccountManager.m
@@ -0,0 +1,56 @@
+#import "MGLAccountManager_Private.h"
+
+#import <mbgl/osx/MGLMapView.h>
+
+@interface MGLAccountManager ()
+
+@property (atomic) NSString *accessToken;
+
+@end
+
+@implementation MGLAccountManager
+
+#pragma mark - Internal
+
++ (void)load {
+ // Read the initial configuration from Info.plist.
+ NSString *accessToken = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMapboxAccessToken"];
+ if (accessToken.length) {
+ self.accessToken = accessToken;
+ }
+}
+
+// Can be called from any thread.
+//
++ (instancetype)sharedManager {
+ static dispatch_once_t onceToken;
+ static MGLAccountManager *_sharedManager;
+ void (^setupBlock)() = ^{
+ dispatch_once(&onceToken, ^{
+ _sharedManager = [[self alloc] init];
+ });
+ };
+ if (![[NSThread currentThread] isMainThread]) {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ setupBlock();
+ });
+ }
+ else {
+ setupBlock();
+ }
+ return _sharedManager;
+}
+
++ (void)setAccessToken:(NSString *)accessToken {
+ accessToken = [accessToken stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ if (![accessToken length]) return;
+
+ [MGLAccountManager sharedManager].accessToken = accessToken;
+}
+
++ (NSString *)accessToken {
+ return [MGLAccountManager sharedManager].accessToken;
+}
+
+@end
diff --git a/platform/osx/src/MGLAccountManager_Private.h b/platform/osx/src/MGLAccountManager_Private.h
new file mode 100644
index 0000000000..c0b8d2666a
--- /dev/null
+++ b/platform/osx/src/MGLAccountManager_Private.h
@@ -0,0 +1,10 @@
+#import <mbgl/osx/MGLAccountManager.h>
+
+@interface MGLAccountManager (Private)
+
+/** Returns the shared instance of the `MGLAccountManager` class. */
++ (instancetype)sharedManager;
+
+@property (atomic) NSString *accessToken;
+
+@end
diff --git a/platform/osx/src/MGLAnnotationImage.m b/platform/osx/src/MGLAnnotationImage.m
new file mode 100644
index 0000000000..855105fded
--- /dev/null
+++ b/platform/osx/src/MGLAnnotationImage.m
@@ -0,0 +1,25 @@
+#import <mbgl/osx/MGLAnnotationImage.h>
+
+@interface MGLAnnotationImage ()
+
+@property (nonatomic) NSImage *image;
+@property (nonatomic) NSString *reuseIdentifier;
+
+@end
+
+@implementation MGLAnnotationImage
+
++ (instancetype)annotationImageWithImage:(NSImage *)image reuseIdentifier:(NSString *)reuseIdentifier {
+ return [[self alloc] initWithImage:image reuseIdentifier:reuseIdentifier];
+}
+
+- (instancetype)initWithImage:(NSImage *)image reuseIdentifier:(NSString *)reuseIdentifier {
+ if (self = [super init]) {
+ _image = image;
+ _reuseIdentifier = [reuseIdentifier copy];
+ _selectable = YES;
+ }
+ return self;
+}
+
+@end
diff --git a/platform/osx/src/MGLAttributionButton.h b/platform/osx/src/MGLAttributionButton.h
new file mode 100644
index 0000000000..7bc0fa5357
--- /dev/null
+++ b/platform/osx/src/MGLAttributionButton.h
@@ -0,0 +1,11 @@
+#import <Cocoa/Cocoa.h>
+
+@interface MGLAttributionButton : NSButton
+
+- (instancetype)initWithTitle:(NSString *)title URL:(NSURL *)url;
+
+@property (nonatomic) NSURL *URL;
+
+- (IBAction)openURL:(id)sender;
+
+@end
diff --git a/platform/osx/src/MGLAttributionButton.m b/platform/osx/src/MGLAttributionButton.m
new file mode 100644
index 0000000000..642f71f608
--- /dev/null
+++ b/platform/osx/src/MGLAttributionButton.m
@@ -0,0 +1,46 @@
+#import "MGLAttributionButton.h"
+
+@implementation MGLAttributionButton {
+ NSTrackingRectTag _trackingAreaTag;
+}
+
+- (instancetype)initWithTitle:(NSString *)title URL:(NSURL *)url {
+ if (self = [super initWithFrame:NSZeroRect]) {
+ self.bordered = NO;
+ self.bezelStyle = NSRegularSquareBezelStyle;
+
+ NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:@"© "
+ attributes:@{
+ NSFontAttributeName: [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]],
+ }];
+ [attributedTitle appendAttributedString:
+ [[NSAttributedString alloc] initWithString:title
+ attributes:@{
+ NSFontAttributeName: [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]],
+ NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle),
+ }]];
+ self.attributedTitle = attributedTitle;
+ [self sizeToFit];
+
+ _URL = url;
+ self.toolTip = _URL.absoluteString;
+
+ self.target = self;
+ self.action = @selector(openURL:);
+ }
+ return self;
+}
+
+- (BOOL)wantsLayer {
+ return YES;
+}
+
+- (void)resetCursorRects {
+ [self addCursorRect:self.bounds cursor:[NSCursor pointingHandCursor]];
+}
+
+- (IBAction)openURL:(__unused id)sender {
+ [[NSWorkspace sharedWorkspace] openURL:self.URL];
+}
+
+@end
diff --git a/platform/osx/src/MGLCompassCell.h b/platform/osx/src/MGLCompassCell.h
new file mode 100644
index 0000000000..8c89b43e18
--- /dev/null
+++ b/platform/osx/src/MGLCompassCell.h
@@ -0,0 +1,5 @@
+#import <Cocoa/Cocoa.h>
+
+@interface MGLCompassCell : NSSliderCell
+
+@end
diff --git a/platform/osx/src/MGLCompassCell.m b/platform/osx/src/MGLCompassCell.m
new file mode 100644
index 0000000000..88911de2ff
--- /dev/null
+++ b/platform/osx/src/MGLCompassCell.m
@@ -0,0 +1,31 @@
+#import "MGLCompassCell.h"
+
+@implementation MGLCompassCell
+
+- (instancetype)init {
+ if (self = [super init]) {
+ self.sliderType = NSCircularSlider;
+ self.numberOfTickMarks = 4;
+ self.minValue = -360;
+ self.maxValue = 0;
+ }
+ return self;
+}
+
+- (void)drawKnob:(NSRect)knobRect {
+ NSBezierPath *trianglePath = [NSBezierPath bezierPath];
+ [trianglePath moveToPoint:NSMakePoint(NSMinX(knobRect), NSMaxY(knobRect))];
+ [trianglePath lineToPoint:NSMakePoint(NSMaxX(knobRect), NSMaxY(knobRect))];
+ [trianglePath lineToPoint:NSMakePoint(NSMidX(knobRect), NSMinY(knobRect))];
+ [trianglePath closePath];
+ NSAffineTransform *transform = [NSAffineTransform transform];
+ [transform translateXBy:NSMidX(knobRect) yBy:NSMidY(knobRect)];
+ [transform scaleBy:0.8];
+ [transform rotateByDegrees:self.doubleValue];
+ [transform translateXBy:-NSMidX(knobRect) yBy:-NSMidY(knobRect)];
+ [trianglePath transformUsingAffineTransform:transform];
+ [[NSColor redColor] setFill];
+ [trianglePath fill];
+}
+
+@end
diff --git a/platform/osx/src/MGLMapView+IBAdditions.m b/platform/osx/src/MGLMapView+IBAdditions.m
new file mode 100644
index 0000000000..504bc789de
--- /dev/null
+++ b/platform/osx/src/MGLMapView+IBAdditions.m
@@ -0,0 +1,114 @@
+#import <mbgl/osx/MGLMapView+IBAdditions.h>
+
+#import "MGLMapView_Private.h"
+
+@implementation MGLMapView (IBAdditions)
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingStyleURL__ {
+ return [NSSet setWithObject:@"styleURL"];
+}
+
+- (nullable NSString *)styleURL__ {
+ return self.styleURL.absoluteString;
+}
+
+- (void)setStyleURL__:(nullable NSString *)URLString {
+ URLString = [URLString stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ NSURL *url = URLString.length ? [NSURL URLWithString:URLString] : nil;
+ if (URLString.length && !url) {
+ [NSException raise:@"Invalid style URL"
+ format:@"“%@” is not a valid style URL.", URLString];
+ }
+ self.styleURL = url;
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLatitude {
+ return [NSSet setWithObjects:@"centerCoordinate", nil];
+}
+
+- (double)latitude {
+ return self.centerCoordinate.latitude;
+}
+
+- (void)setLatitude:(double)latitude {
+ if (!isnan(self.pendingLongitude)) {
+ self.centerCoordinate = CLLocationCoordinate2DMake(latitude, self.pendingLongitude);
+ self.pendingLatitude = NAN;
+ self.pendingLongitude = NAN;
+ } else {
+ // Not enough info to make a valid center coordinate yet. Stash this
+ // latitude away until the longitude is set too.
+ self.pendingLatitude = latitude;
+ }
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLongitude {
+ return [NSSet setWithObjects:@"centerCoordinate", nil];
+}
+
+- (double)longitude {
+ return self.centerCoordinate.longitude;
+}
+
+- (void)setLongitude:(double)longitude {
+ if (!isnan(self.pendingLatitude)) {
+ self.centerCoordinate = CLLocationCoordinate2DMake(self.pendingLatitude, longitude);
+ self.pendingLatitude = NAN;
+ self.pendingLongitude = NAN;
+ } else {
+ // Not enough info to make a valid center coordinate yet. Stash this
+ // longitude away until the latitude is set too.
+ self.pendingLongitude = longitude;
+ }
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingAllowsZooming {
+ return [NSSet setWithObject:@"zoomEnabled"];
+}
+
+- (BOOL)allowsZooming {
+ return self.zoomEnabled;
+}
+
+- (void)setAllowsZooming:(BOOL)allowsZooming {
+ self.zoomEnabled = allowsZooming;
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingAllowsScrolling {
+ return [NSSet setWithObject:@"scrollEnabled"];
+}
+
+- (BOOL)allowsScrolling {
+ return self.scrollEnabled;
+}
+
+- (void)setAllowsScrolling:(BOOL)allowsScrolling {
+ self.scrollEnabled = allowsScrolling;
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingAllowsRotating {
+ return [NSSet setWithObject:@"rotateEnabled"];
+}
+
+- (BOOL)allowsRotating {
+ return self.rotateEnabled;
+}
+
+- (void)setAllowsRotating:(BOOL)allowsRotating {
+ self.rotateEnabled = allowsRotating;
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingAllowsTilting {
+ return [NSSet setWithObject:@"pitchEnabled"];
+}
+
+- (BOOL)allowsTilting {
+ return self.pitchEnabled;
+}
+
+- (void)setAllowsTilting:(BOOL)allowsTilting {
+ self.pitchEnabled = allowsTilting;
+}
+
+@end
diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm
new file mode 100644
index 0000000000..a1cfe3c33d
--- /dev/null
+++ b/platform/osx/src/MGLMapView.mm
@@ -0,0 +1,1784 @@
+#import "MGLMapView_Private.h"
+#import "MGLAccountManager_Private.h"
+#import "MGLAttributionButton.h"
+#import "MGLCompassCell.h"
+#import "MGLOpenGLLayer.h"
+#import "MGLStyle.h"
+
+#import "../../darwin/MGLGeometry_Private.h"
+#import "../../darwin/MGLMultiPoint_Private.h"
+
+#import <mbgl/darwin/MGLPolygon.h>
+#import <mbgl/darwin/MGLPolyline.h>
+#import <mbgl/osx/MGLAnnotationImage.h>
+#import <mbgl/osx/MGLMapViewDelegate.h>
+
+#import <mbgl/mbgl.hpp>
+#import <mbgl/annotation/point_annotation.hpp>
+#import <mbgl/map/camera.hpp>
+#import <mbgl/platform/darwin/reachability.h>
+#import <mbgl/platform/gl.hpp>
+#import <mbgl/sprite/sprite_image.hpp>
+#import <mbgl/storage/default_file_source.hpp>
+#import <mbgl/storage/network_status.hpp>
+#import <mbgl/storage/sqlite_cache.hpp>
+#import <mbgl/util/constants.hpp>
+#import <mbgl/util/math.hpp>
+#import <mbgl/util/std.hpp>
+
+#import <map>
+#import <unordered_set>
+
+#import "NSBundle+MGLAdditions.h"
+#import "NSProcessInfo+MGLAdditions.h"
+#import "../../darwin/NSException+MGLAdditions.h"
+#import "../../darwin/NSString+MGLAdditions.h"
+
+#import <QuartzCore/QuartzCore.h>
+
+class MGLMapViewImpl;
+class MGLAnnotationContext;
+
+const CGFloat MGLOrnamentPadding = 12;
+
+const NSTimeInterval MGLAnimationDuration = 0.3;
+const CGFloat MGLKeyPanningIncrement = 150;
+const CLLocationDegrees MGLKeyRotationIncrement = 25;
+
+static NSString * const MGLVendorDirectoryName = @"com.mapbox.MapboxGL";
+
+static NSString * const MGLDefaultStyleMarkerSymbolName = @"default_marker";
+static NSString * const MGLAnnotationSpritePrefix = @"com.mapbox.sprites.";
+const CGFloat MGLAnnotationImagePaddingForHitTest = 4;
+const CGFloat MGLAnnotationImagePaddingForCallout = 4;
+
+struct MGLAttribution {
+ NSString *title;
+ NSString *urlString;
+} MGLAttributions[] = {
+ { @"Mapbox", @"https://www.mapbox.com/about/maps/" },
+ { @"OpenStreetMap", @"http://www.openstreetmap.org/about/" },
+};
+
+typedef uint32_t MGLAnnotationID;
+enum { MGLAnnotationNotFound = UINT32_MAX };
+typedef std::map<MGLAnnotationID, MGLAnnotationContext> MGLAnnotationContextMap;
+
+/// Returns an NSImage for the default marker image.
+NSImage *MGLDefaultMarkerImage() {
+ NSString *path = [[NSBundle mgl_frameworkBundle] pathForResource:MGLDefaultStyleMarkerSymbolName
+ ofType:@"pdf"];
+ return [[NSImage alloc] initWithContentsOfFile:path];
+}
+
+std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) {
+ return std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<float, std::chrono::seconds::period>(duration));
+}
+
+/// Converts the given color into an mbgl::Color in calibrated RGB space.
+mbgl::Color MGLColorObjectFromNSColor(NSColor *color) {
+ if (!color) {
+ return {{ 0, 0, 0, 0 }};
+ }
+ CGFloat r, g, b, a;
+ [[color colorUsingColorSpaceName:NSCalibratedRGBColorSpace] getRed:&r green:&g blue:&b alpha:&a];
+ return {{ (float)r, (float)g, (float)b, (float)a }};
+}
+
+class MGLAnnotationContext {
+public:
+ id <MGLAnnotation> annotation;
+ NSString *symbolIdentifier;
+};
+
+@interface MGLMapView () <NSPopoverDelegate, MGLMultiPointDelegate>
+
+@property (nonatomic, readwrite) NSSegmentedControl *zoomControls;
+@property (nonatomic, readwrite) NSSlider *compass;
+@property (nonatomic, readwrite) NSImageView *logoView;
+@property (nonatomic, readwrite) NSView *attributionView;
+
+@property (nonatomic) NS_MUTABLE_DICTIONARY_OF(NSString *, MGLAnnotationImage *) *annotationImagesByIdentifier;
+@property (nonatomic) NSPopover *calloutForSelectedAnnotation;
+
+@property (nonatomic, readwrite, getter=isDormant) BOOL dormant;
+
+@end
+
+@implementation MGLMapView {
+ mbgl::Map *_mbglMap;
+ MGLMapViewImpl *_mbglView;
+ std::shared_ptr<mbgl::SQLiteCache> _mbglFileCache;
+ mbgl::DefaultFileSource *_mbglFileSource;
+
+ NSPanGestureRecognizer *_panGestureRecognizer;
+ NSMagnificationGestureRecognizer *_magnificationGestureRecognizer;
+ NSRotationGestureRecognizer *_rotationGestureRecognizer;
+ double _scaleAtBeginningOfGesture;
+ CLLocationDirection _directionAtBeginningOfGesture;
+ CGFloat _pitchAtBeginningOfGesture;
+
+ MGLAnnotationContextMap _annotationContextsByAnnotationID;
+ MGLAnnotationID _selectedAnnotationID;
+ MGLAnnotationID _lastSelectedAnnotationID;
+ NSSize _unionedAnnotationImageSize;
+ std::vector<MGLAnnotationID> _annotationsNearbyLastClick;
+ BOOL _wantsToolTipRects;
+ BOOL _wantsCursorRects;
+ BOOL _delegateHasAlphasForShapeAnnotations;
+ BOOL _delegateHasStrokeColorsForShapeAnnotations;
+ BOOL _delegateHasFillColorsForShapeAnnotations;
+ BOOL _delegateHasLineWidthsForShapeAnnotations;
+
+ BOOL _isTargetingInterfaceBuilder;
+ CLLocationDegrees _pendingLatitude;
+ CLLocationDegrees _pendingLongitude;
+}
+
+#pragma mark Lifecycle
+
+- (instancetype)initWithFrame:(NSRect)frameRect {
+ if (self = [super initWithFrame:frameRect]) {
+ [self commonInit];
+ self.styleURL = nil;
+ }
+ return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame styleURL:(nullable NSURL *)styleURL {
+ if (self = [super initWithFrame:frame]) {
+ [self commonInit];
+ self.styleURL = styleURL;
+ }
+ return self;
+}
+
+- (instancetype)initWithCoder:(nonnull NSCoder *)decoder {
+ if (self = [super initWithCoder:decoder]) {
+ [self commonInit];
+ }
+ return self;
+}
+
+- (void)awakeFromNib {
+ [super awakeFromNib];
+
+ self.styleURL = nil;
+}
+
++ (NSArray *)restorableStateKeyPaths {
+ return @[@"zoomLevel", @"direction"];
+}
+
+- (void)commonInit {
+ _isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent;
+
+ _mbglView = new MGLMapViewImpl(self, [NSScreen mainScreen].backingScaleFactor);
+
+ // Place the cache in a location that can be shared among all the
+ // applications that embed the Mapbox OS X SDK.
+ NSURL *cacheDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory
+ inDomain:NSUserDomainMask
+ appropriateForURL:nil
+ create:YES
+ error:nil];
+ cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:MGLVendorDirectoryName];
+ [[NSFileManager defaultManager] createDirectoryAtURL:cacheDirectoryURL
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:nil];
+ NSURL *cacheURL = [cacheDirectoryURL URLByAppendingPathComponent:@"cache.db"];
+ NSString *cachePath = cacheURL ? cacheURL.path : @"";
+ _mbglFileCache = mbgl::SharedSQLiteCache::get(cachePath.UTF8String);
+ _mbglFileSource = new mbgl::DefaultFileSource(_mbglFileCache.get());
+
+ _mbglMap = new mbgl::Map(*_mbglView, *_mbglFileSource, mbgl::MapMode::Continuous);
+
+ self.layer = _isTargetingInterfaceBuilder ? [CALayer layer] : [MGLOpenGLLayer layer];
+
+ // Observe for changes to the global access token (and find out the current one).
+ [[MGLAccountManager sharedManager] addObserver:self
+ forKeyPath:@"accessToken"
+ options:(NSKeyValueObservingOptionInitial |
+ NSKeyValueObservingOptionNew)
+ context:NULL];
+
+ // Notify map object when network reachability status changes.
+ MGLReachability *reachability = [MGLReachability reachabilityForInternetConnection];
+ reachability.reachableBlock = ^(MGLReachability *) {
+ mbgl::NetworkStatus::Reachable();
+ };
+ [reachability startNotifier];
+
+ [self installZoomControls];
+ [self installCompass];
+ [self installLogoView];
+ [self installAttributionView];
+ [self installGestureRecognizers];
+
+ _annotationImagesByIdentifier = [NSMutableDictionary dictionary];
+ _annotationContextsByAnnotationID = {};
+ _selectedAnnotationID = MGLAnnotationNotFound;
+ _lastSelectedAnnotationID = MGLAnnotationNotFound;
+ _annotationsNearbyLastClick = {};
+
+ mbgl::CameraOptions options;
+ options.center = mbgl::LatLng(0, 0);
+ options.zoom = _mbglMap->getMinZoom();
+ _mbglMap->jumpTo(options);
+}
+
+- (void)installZoomControls {
+ _zoomControls = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect];
+ _zoomControls.wantsLayer = YES;
+ _zoomControls.layer.opacity = 0.9;
+ [(NSSegmentedCell *)_zoomControls.cell setTrackingMode:NSSegmentSwitchTrackingMomentary];
+ _zoomControls.continuous = YES;
+ _zoomControls.segmentCount = 2;
+ [_zoomControls setLabel:@"−" forSegment:0];
+ [(NSSegmentedCell *)_zoomControls.cell setTag:0 forSegment:0];
+ [(NSSegmentedCell *)_zoomControls.cell setToolTip:@"Zoom Out" forSegment:0];
+ [_zoomControls setLabel:@"+" forSegment:1];
+ [(NSSegmentedCell *)_zoomControls.cell setTag:1 forSegment:1];
+ [(NSSegmentedCell *)_zoomControls.cell setToolTip:@"Zoom In" forSegment:1];
+ _zoomControls.target = self;
+ _zoomControls.action = @selector(zoomInOrOut:);
+ _zoomControls.controlSize = NSRegularControlSize;
+ [_zoomControls sizeToFit];
+ _zoomControls.translatesAutoresizingMaskIntoConstraints = NO;
+ [self addSubview:_zoomControls];
+}
+
+- (void)installCompass {
+ _compass = [[NSSlider alloc] initWithFrame:NSZeroRect];
+ _compass.wantsLayer = YES;
+ _compass.layer.opacity = 0.9;
+ _compass.cell = [[MGLCompassCell alloc] init];
+ _compass.continuous = YES;
+ _compass.target = self;
+ _compass.action = @selector(rotate:);
+ [_compass sizeToFit];
+ _compass.translatesAutoresizingMaskIntoConstraints = NO;
+ [self addSubview:_compass];
+}
+
+- (void)installLogoView {
+ _logoView = [[NSImageView alloc] initWithFrame:NSZeroRect];
+ _logoView.wantsLayer = YES;
+ NSImage *logoImage = [[NSImage alloc] initWithContentsOfFile:
+ [[NSBundle mgl_frameworkBundle] pathForResource:@"mapbox" ofType:@"pdf"]];
+ logoImage.alignmentRect = NSInsetRect(logoImage.alignmentRect, 3, 3);
+ _logoView.image = logoImage;
+ _logoView.translatesAutoresizingMaskIntoConstraints = NO;
+ _logoView.accessibilityTitle = @"Mapbox";
+ [self addSubview:_logoView];
+}
+
+- (void)installAttributionView {
+ _attributionView = [[NSView alloc] initWithFrame:NSZeroRect];
+ _attributionView.wantsLayer = YES;
+ _attributionView.layer.opacity = 0.6;
+ CIFilter *attributionBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
+ [attributionBlurFilter setDefaults];
+ CIFilter *attributionColorFilter = [CIFilter filterWithName:@"CIColorControls"];
+ [attributionColorFilter setDefaults];
+ [attributionColorFilter setValue:@(0.1) forKey:kCIInputBrightnessKey];
+ _attributionView.backgroundFilters = @[attributionColorFilter, attributionBlurFilter];
+ _attributionView.layer.cornerRadius = 4;
+ _attributionView.translatesAutoresizingMaskIntoConstraints = NO;
+ [self addSubview:_attributionView];
+ [self updateAttributionView];
+}
+
+- (void)installGestureRecognizers {
+ self.acceptsTouchEvents = YES;
+ _scrollEnabled = YES;
+ _zoomEnabled = YES;
+ _rotateEnabled = YES;
+ _pitchEnabled = YES;
+
+ _panGestureRecognizer = [[NSPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
+ _panGestureRecognizer.delaysKeyEvents = YES;
+ [self addGestureRecognizer:_panGestureRecognizer];
+
+ NSClickGestureRecognizer *clickGestureRecognizer = [[NSClickGestureRecognizer alloc] initWithTarget:self action:@selector(handleClickGesture:)];
+ clickGestureRecognizer.delaysPrimaryMouseButtonEvents = NO;
+ [self addGestureRecognizer:clickGestureRecognizer];
+
+ NSClickGestureRecognizer *secondaryClickGestureRecognizer = [[NSClickGestureRecognizer alloc] initWithTarget:self action:@selector(handleSecondaryClickGesture:)];
+ secondaryClickGestureRecognizer.buttonMask = 0x2;
+ [self addGestureRecognizer:secondaryClickGestureRecognizer];
+
+ NSClickGestureRecognizer *doubleClickGestureRecognizer = [[NSClickGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleClickGesture:)];
+ doubleClickGestureRecognizer.numberOfClicksRequired = 2;
+ doubleClickGestureRecognizer.delaysPrimaryMouseButtonEvents = NO;
+ [self addGestureRecognizer:doubleClickGestureRecognizer];
+
+ _magnificationGestureRecognizer = [[NSMagnificationGestureRecognizer alloc] initWithTarget:self action:@selector(handleMagnificationGesture:)];
+ [self addGestureRecognizer:_magnificationGestureRecognizer];
+
+ _rotationGestureRecognizer = [[NSRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotationGesture:)];
+ [self addGestureRecognizer:_rotationGestureRecognizer];
+}
+
+- (void)updateAttributionView {
+ self.attributionView.subviews = @[];
+
+ for (NSUInteger i = 0; i < sizeof(MGLAttributions) / sizeof(MGLAttributions[0]); i++) {
+ NSURL *url = [NSURL URLWithString:MGLAttributions[i].urlString];
+ NSButton *button = [[MGLAttributionButton alloc] initWithTitle:MGLAttributions[i].title URL:url];
+ button.controlSize = NSMiniControlSize;
+ button.translatesAutoresizingMaskIntoConstraints = NO;
+
+ NSView *previousView = self.attributionView.subviews.lastObject;
+ [self.attributionView addSubview:button];
+
+ [_attributionView addConstraint:
+ [NSLayoutConstraint constraintWithItem:button
+ attribute:NSLayoutAttributeBottom
+ relatedBy:NSLayoutRelationEqual
+ toItem:_attributionView
+ attribute:NSLayoutAttributeBottom
+ multiplier:1
+ constant:0]];
+ [_attributionView addConstraint:
+ [NSLayoutConstraint constraintWithItem:button
+ attribute:NSLayoutAttributeLeading
+ relatedBy:NSLayoutRelationEqual
+ toItem:previousView ? previousView : _attributionView
+ attribute:previousView ? NSLayoutAttributeTrailing : NSLayoutAttributeLeading
+ multiplier:1
+ constant:8]];
+ }
+}
+
+- (void)dealloc {
+ [[MGLAccountManager sharedManager] removeObserver:self forKeyPath:@"accessToken"];
+
+ [self.calloutForSelectedAnnotation close];
+ self.calloutForSelectedAnnotation = nil;
+
+ if (_mbglMap) {
+ delete _mbglMap;
+ _mbglMap = nullptr;
+ }
+ if (_mbglFileSource) {
+ delete _mbglFileSource;
+ _mbglFileSource = nullptr;
+ }
+ if (_mbglView) {
+ delete _mbglView;
+ _mbglView = nullptr;
+ }
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(__unused void *)context {
+ // Synchronize mbgl::Map’s access token with the global one in MGLAccountManager.
+ if ([keyPath isEqualToString:@"accessToken"] && object == [MGLAccountManager sharedManager]) {
+ NSString *accessToken = change[NSKeyValueChangeNewKey];
+ if (![accessToken isKindOfClass:[NSNull class]]) {
+ _mbglFileSource->setAccessToken((std::string)[accessToken UTF8String]);
+ }
+ }
+}
+
+- (void)setDelegate:(id<MGLMapViewDelegate>)delegate {
+ _delegate = delegate;
+
+ _delegateHasAlphasForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:alphaForShapeAnnotation:)];
+ _delegateHasStrokeColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:strokeColorForShapeAnnotation:)];
+ _delegateHasFillColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:fillColorForPolygonAnnotation:)];
+ _delegateHasLineWidthsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:lineWidthForPolylineAnnotation:)];
+}
+
+#pragma mark Style
+
+- (nonnull NSURL *)styleURL {
+ NSString *styleURLString = @(_mbglMap->getStyleURL().c_str()).mgl_stringOrNilIfEmpty;
+ return styleURLString ? [NSURL URLWithString:styleURLString] : [MGLStyle streetsStyleURL];
+}
+
+- (void)setStyleURL:(nullable NSURL *)styleURL {
+ if (_isTargetingInterfaceBuilder) {
+ return;
+ }
+
+ if (!styleURL) {
+ if (![MGLAccountManager accessToken]) {
+ return;
+ }
+ styleURL = [MGLStyle streetsStyleURL];
+ }
+
+ if (![styleURL scheme]) {
+ // Assume a relative path into the application’s resource folder.
+ styleURL = [NSURL URLWithString:[@"asset://" stringByAppendingString:[styleURL absoluteString]]];
+ }
+
+ _mbglMap->setStyleURL([[styleURL absoluteString] UTF8String]);
+}
+
+- (IBAction)reloadStyle:(__unused id)sender {
+ NSURL *styleURL = self.styleURL;
+ _mbglMap->setStyleURL("");
+ self.styleURL = styleURL;
+}
+
+#pragma mark View hierarchy and drawing
+
+- (void)viewWillMoveToWindow:(NSWindow *)newWindow {
+ [self deselectAnnotation:self.selectedAnnotation animated:NO];
+ if (!self.dormant && !newWindow) {
+ self.dormant = YES;
+ _mbglMap->pause();
+ }
+}
+
+- (void)viewDidMoveToWindow {
+ if (self.dormant && self.window) {
+ _mbglMap->resume();
+ self.dormant = NO;
+ }
+}
+
+- (BOOL)wantsLayer {
+ return YES;
+}
+
+- (BOOL)wantsBestResolutionOpenGLSurface {
+ return !_isTargetingInterfaceBuilder;
+}
+
+- (void)setFrame:(NSRect)frame {
+ super.frame = frame;
+ if (!_isTargetingInterfaceBuilder) {
+ _mbglMap->update(mbgl::Update::Dimensions);
+ }
+}
+
+- (void)updateConstraints {
+ [self addConstraint:
+ [NSLayoutConstraint constraintWithItem:self
+ attribute:NSLayoutAttributeBottom
+ relatedBy:NSLayoutRelationEqual
+ toItem:_zoomControls
+ attribute:NSLayoutAttributeBottom
+ multiplier:1
+ constant:MGLOrnamentPadding]];
+ [self addConstraint:
+ [NSLayoutConstraint constraintWithItem:self
+ attribute:NSLayoutAttributeTrailing
+ relatedBy:NSLayoutRelationEqual
+ toItem:_zoomControls
+ attribute:NSLayoutAttributeTrailing
+ multiplier:1
+ constant:MGLOrnamentPadding]];
+
+ [self addConstraint:
+ [NSLayoutConstraint constraintWithItem:_compass
+ attribute:NSLayoutAttributeCenterX
+ relatedBy:NSLayoutRelationEqual
+ toItem:_zoomControls
+ attribute:NSLayoutAttributeCenterX
+ multiplier:1
+ constant:0]];
+ [self addConstraint:
+ [NSLayoutConstraint constraintWithItem:_zoomControls
+ attribute:NSLayoutAttributeTop
+ relatedBy:NSLayoutRelationEqual
+ toItem:_compass
+ attribute:NSLayoutAttributeBottom
+ multiplier:1
+ constant:8]];
+
+ [self addConstraint:
+ [NSLayoutConstraint constraintWithItem:self
+ attribute:NSLayoutAttributeBottom
+ relatedBy:NSLayoutRelationEqual
+ toItem:_logoView
+ attribute:NSLayoutAttributeBottom
+ multiplier:1
+ constant:MGLOrnamentPadding - _logoView.image.alignmentRect.origin.y]];
+ [self addConstraint:
+ [NSLayoutConstraint constraintWithItem:_logoView
+ attribute:NSLayoutAttributeLeading
+ relatedBy:NSLayoutRelationEqual
+ toItem:self
+ attribute:NSLayoutAttributeLeading
+ multiplier:1
+ constant:MGLOrnamentPadding - _logoView.image.alignmentRect.origin.x]];
+
+ [self addConstraint:[NSLayoutConstraint constraintWithItem:_logoView
+ attribute:NSLayoutAttributeBaseline
+ relatedBy:NSLayoutRelationEqual
+ toItem:_attributionView
+ attribute:NSLayoutAttributeBaseline
+ multiplier:1
+ constant:_logoView.image.alignmentRect.origin.y]];
+ [self addConstraint:[NSLayoutConstraint constraintWithItem:_attributionView
+ attribute:NSLayoutAttributeLeading
+ relatedBy:NSLayoutRelationEqual
+ toItem:_logoView
+ attribute:NSLayoutAttributeTrailing
+ multiplier:1
+ constant:8]];
+ [self addConstraint:[NSLayoutConstraint constraintWithItem:_attributionView.subviews.firstObject
+ attribute:NSLayoutAttributeTop
+ relatedBy:NSLayoutRelationEqual
+ toItem:_attributionView
+ attribute:NSLayoutAttributeTop
+ multiplier:1
+ constant:0]];
+ [self addConstraint:[NSLayoutConstraint constraintWithItem:_attributionView
+ attribute:NSLayoutAttributeTrailing
+ relatedBy:NSLayoutRelationEqual
+ toItem:_attributionView.subviews.lastObject
+ attribute:NSLayoutAttributeTrailing
+ multiplier:1
+ constant:8]];
+
+ [super updateConstraints];
+}
+
+- (void)renderSync {
+ if (!self.dormant) {
+ CGFloat zoomFactor = _mbglMap->getMaxZoom() - _mbglMap->getMinZoom() + 1;
+ CGFloat cpuFactor = (CGFloat)[NSProcessInfo processInfo].processorCount;
+ CGFloat memoryFactor = (CGFloat)[NSProcessInfo processInfo].physicalMemory / 1000 / 1000 / 1000;
+ CGFloat sizeFactor = ((CGFloat)_mbglMap->getWidth() / mbgl::util::tileSize) * ((CGFloat)_mbglMap->getHeight() / mbgl::util::tileSize);
+
+ NSUInteger cacheSize = zoomFactor * cpuFactor * memoryFactor * sizeFactor * 0.5;
+
+ _mbglMap->setSourceTileCacheSize(cacheSize);
+ _mbglMap->renderSync();
+
+// [self updateUserLocationAnnotationView];
+ }
+}
+
+- (void)invalidate {
+ MGLAssertIsMainThread();
+
+ [self.layer setNeedsDisplay];
+}
+
+- (void)notifyMapChange:(mbgl::MapChange)change {
+ // Ignore map updates when the Map object isn't set.
+ if (!_mbglMap) {
+ return;
+ }
+
+ switch (change) {
+ case mbgl::MapChangeRegionWillChange:
+ case mbgl::MapChangeRegionWillChangeAnimated:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapView:regionWillChangeAnimated:)]) {
+ BOOL animated = change == mbgl::MapChangeRegionWillChangeAnimated;
+ [self.delegate mapView:self regionWillChangeAnimated:animated];
+ }
+ break;
+ }
+ case mbgl::MapChangeRegionIsChanging:
+ {
+ [self updateCompass];
+ [self updateAnnotationCallouts];
+
+ if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)]) {
+ [self.delegate mapViewRegionIsChanging:self];
+ }
+ break;
+ }
+ case mbgl::MapChangeRegionDidChange:
+ case mbgl::MapChangeRegionDidChangeAnimated:
+ {
+ [self updateZoomControls];
+ [self updateCompass];
+ [self updateAnnotationCallouts];
+ [self updateAnnotationTrackingAreas];
+
+ if ([self.delegate respondsToSelector:@selector(mapView:regionDidChangeAnimated:)]) {
+ BOOL animated = change == mbgl::MapChangeRegionDidChangeAnimated;
+ [self.delegate mapView:self regionDidChangeAnimated:animated];
+ }
+ break;
+ }
+ case mbgl::MapChangeWillStartLoadingMap:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewWillStartLoadingMap:)]) {
+ [self.delegate mapViewWillStartLoadingMap:self];
+ }
+ break;
+ }
+ case mbgl::MapChangeDidFinishLoadingMap:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewDidFinishLoadingMap:)]) {
+ [self.delegate mapViewDidFinishLoadingMap:self];
+ }
+ break;
+ }
+ case mbgl::MapChangeDidFailLoadingMap:
+ {
+ // Not yet implemented.
+ break;
+ }
+ case mbgl::MapChangeWillStartRenderingMap:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingMap:)]) {
+ [self.delegate mapViewWillStartRenderingMap:self];
+ }
+ break;
+ }
+ case mbgl::MapChangeDidFinishRenderingMap:
+ case mbgl::MapChangeDidFinishRenderingMapFullyRendered:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)]) {
+ BOOL fullyRendered = change == mbgl::MapChangeDidFinishRenderingMapFullyRendered;
+ [self.delegate mapViewDidFinishRenderingMap:self fullyRendered:fullyRendered];
+ }
+ break;
+ }
+ case mbgl::MapChangeWillStartRenderingFrame:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingFrame:)]) {
+ [self.delegate mapViewWillStartRenderingFrame:self];
+ }
+ break;
+ }
+ case mbgl::MapChangeDidFinishRenderingFrame:
+ case mbgl::MapChangeDidFinishRenderingFrameFullyRendered:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingFrame:fullyRendered:)]) {
+ BOOL fullyRendered = change == mbgl::MapChangeDidFinishRenderingFrameFullyRendered;
+ [self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:fullyRendered];
+ }
+ break;
+ }
+ }
+}
+
+#pragma mark Viewport
+
+- (CLLocationCoordinate2D)centerCoordinate {
+ return MGLLocationCoordinate2DFromLatLng(_mbglMap->getLatLng());
+}
+
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate {
+ [self setCenterCoordinate:centerCoordinate animated:NO];
+}
+
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate animated:(BOOL)animated {
+ [self willChangeValueForKey:@"centerCoordinate"];
+ _mbglMap->setLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate),
+ MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
+ [self didChangeValueForKey:@"centerCoordinate"];
+}
+
+- (void)offsetCenterCoordinateBy:(NSPoint)delta animated:(BOOL)animated {
+ [self willChangeValueForKey:@"centerCoordinate"];
+ _mbglMap->cancelTransitions();
+ _mbglMap->moveBy({ delta.x, delta.y },
+ MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
+ [self didChangeValueForKey:@"centerCoordinate"];
+}
+
+- (double)zoomLevel {
+ return _mbglMap->getZoom();
+}
+
+- (void)setZoomLevel:(double)zoomLevel {
+ [self setZoomLevel:zoomLevel animated:NO];
+}
+
+- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated {
+ _mbglMap->setZoom(zoomLevel, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
+}
+
+- (void)scaleBy:(double)scaleFactor atPoint:(NSPoint)point animated:(BOOL)animated {
+ [self willChangeValueForKey:@"zoomLevel"];
+ mbgl::PrecisionPoint center(point.x, point.y);
+ _mbglMap->scaleBy(scaleFactor, center, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
+ [self didChangeValueForKey:@"zoomLevel"];
+}
+
+- (double)maximumZoomLevel {
+ return _mbglMap->getMaxZoom();
+}
+
+- (double)minimumZoomLevel {
+ return _mbglMap->getMinZoom();
+}
+
+- (IBAction)zoomInOrOut:(NSSegmentedControl *)sender {
+ switch (sender.selectedSegment) {
+ case 0:
+ [self moveToEndOfParagraph:sender];
+ break;
+ case 1:
+ [self moveToBeginningOfParagraph:sender];
+ break;
+ default:
+ break;
+ }
+}
+
+- (CLLocationDirection)direction {
+ return mbgl::util::wrap(_mbglMap->getBearing(), 0., 360.);
+}
+
+- (void)setDirection:(CLLocationDirection)direction {
+ [self setDirection:direction animated:NO];
+}
+
+- (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated {
+ [self willChangeValueForKey:@"direction"];
+ _mbglMap->setBearing(direction, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
+ [self didChangeValueForKey:@"direction"];
+}
+
+- (void)offsetDirectionBy:(CLLocationDegrees)delta animated:(BOOL)animated {
+ [self willChangeValueForKey:@"direction"];
+ _mbglMap->cancelTransitions();
+ _mbglMap->setBearing(_mbglMap->getBearing() + delta, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
+ [self didChangeValueForKey:@"direction"];
+}
+
++ (NSSet *)keyPathsForValuesAffectingVisibleCoordinateBounds {
+ return [NSSet setWithObjects:@"centerCoordinate", @"zoomLevel", @"direction", @"bounds", nil];
+}
+
+- (MGLCoordinateBounds)visibleCoordinateBounds {
+ return [self convertRectToCoordinateBounds:self.bounds];
+}
+
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds {
+ [self setVisibleCoordinateBounds:bounds animated:NO];
+}
+
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds animated:(BOOL)animated {
+ [self setVisibleCoordinateBounds:bounds edgePadding:NSEdgeInsetsZero animated:animated];
+}
+
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated {
+ _mbglMap->cancelTransitions();
+
+ mbgl::EdgeInsets mbglInsets = {insets.top, insets.left, insets.bottom, insets.right};
+ mbgl::CameraOptions options = _mbglMap->cameraForLatLngBounds(MGLLatLngBoundsFromCoordinateBounds(bounds), mbglInsets);
+ if (animated) {
+ options.duration = MGLDurationInSeconds(MGLAnimationDuration);
+ }
+
+ [self willChangeValueForKey:@"visibleCoordinateBounds"];
+ options.transitionFinishFn = ^() {
+ [self didChangeValueForKey:@"visibleCoordinateBounds"];
+ };
+ _mbglMap->easeTo(options);
+}
+
+#pragma mark Mouse events and gestures
+
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+- (void)handlePanGesture:(NSPanGestureRecognizer *)gestureRecognizer {
+ NSPoint delta = [gestureRecognizer translationInView:self];
+ NSPoint endPoint = [gestureRecognizer locationInView:self];
+ NSPoint startPoint = NSMakePoint(endPoint.x - delta.x, self.bounds.size.height - (endPoint.y - delta.y));
+
+ NSEventModifierFlags flags = [NSApp currentEvent].modifierFlags;
+ if (flags & NSShiftKeyMask) {
+ if (!self.zoomEnabled) {
+ return;
+ }
+
+ _mbglMap->cancelTransitions();
+
+ if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
+ _mbglMap->setGestureInProgress(true);
+ _scaleAtBeginningOfGesture = _mbglMap->getScale();
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
+ CGFloat newZoomLevel = log2f(_scaleAtBeginningOfGesture) - delta.y / 75;
+ [self scaleBy:powf(2, newZoomLevel) / _mbglMap->getScale() atPoint:startPoint animated:NO];
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
+ || gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
+ _mbglMap->setGestureInProgress(false);
+ // Maps.app locks the cursor to the start point, but that would
+ // interfere with the pan gesture recognizer. Just move the cursor
+ // back at the end of the gesture.
+ CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, startPoint);
+ }
+ } else if (flags & NSAlternateKeyMask) {
+ _mbglMap->cancelTransitions();
+
+ if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
+ _mbglMap->setGestureInProgress(true);
+ _directionAtBeginningOfGesture = self.direction;
+ _pitchAtBeginningOfGesture = _mbglMap->getPitch();
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
+ mbgl::PrecisionPoint center(startPoint.x, startPoint.y);
+ if (self.rotateEnabled) {
+ CLLocationDirection newDirection = _directionAtBeginningOfGesture - delta.x / 10;
+ [self willChangeValueForKey:@"direction"];
+ _mbglMap->setBearing(newDirection, center);
+ [self didChangeValueForKey:@"direction"];
+ }
+ if (self.pitchEnabled) {
+ _mbglMap->setPitch(_pitchAtBeginningOfGesture + delta.y / 5);
+ }
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
+ || gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
+ _mbglMap->setGestureInProgress(false);
+ }
+ } else if (self.scrollEnabled) {
+ _mbglMap->cancelTransitions();
+
+ if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
+ [self.window invalidateCursorRectsForView:self];
+ _mbglMap->setGestureInProgress(true);
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
+ delta.y *= -1;
+ [self offsetCenterCoordinateBy:delta animated:NO];
+ [gestureRecognizer setTranslation:NSZeroPoint inView:self];
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
+ || gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
+ _mbglMap->setGestureInProgress(false);
+ [self.window invalidateCursorRectsForView:self];
+ }
+ }
+}
+
+- (void)handleMagnificationGesture:(NSMagnificationGestureRecognizer *)gestureRecognizer {
+ if (!self.zoomEnabled) {
+ return;
+ }
+
+ _mbglMap->cancelTransitions();
+
+ if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
+ _mbglMap->setGestureInProgress(true);
+ _scaleAtBeginningOfGesture = _mbglMap->getScale();
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
+ NSPoint zoomInPoint = [gestureRecognizer locationInView:self];
+ mbgl::PrecisionPoint center(zoomInPoint.x, self.bounds.size.height - zoomInPoint.y);
+ if (gestureRecognizer.magnification > -1) {
+ [self willChangeValueForKey:@"zoomLevel"];
+ [self willChangeValueForKey:@"centerCoordinate"];
+ _mbglMap->setScale(_scaleAtBeginningOfGesture * (1 + gestureRecognizer.magnification), center);
+ [self didChangeValueForKey:@"centerCoordinate"];
+ [self didChangeValueForKey:@"zoomLevel"];
+ }
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
+ || gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
+ _mbglMap->setGestureInProgress(false);
+ }
+}
+
+- (void)handleClickGesture:(NSClickGestureRecognizer *)gestureRecognizer {
+ if (gestureRecognizer.state != NSGestureRecognizerStateEnded) {
+ return;
+ }
+
+ NSPoint gesturePoint = [gestureRecognizer locationInView:self];
+ MGLAnnotationID hitAnnotationID = [self annotationIDAtPoint:gesturePoint persistingResults:YES];
+ if (hitAnnotationID != MGLAnnotationNotFound) {
+ if (hitAnnotationID != _selectedAnnotationID) {
+ id <MGLAnnotation> annotation = [self annotationWithID:hitAnnotationID];
+ NSAssert(annotation, @"Cannot select nonexistent annotation with ID %i", hitAnnotationID);
+ [self selectAnnotation:annotation animated:YES];
+ }
+ } else {
+ [self deselectAnnotation:self.selectedAnnotation animated:YES];
+ }
+}
+
+- (void)handleSecondaryClickGesture:(NSClickGestureRecognizer *)gestureRecognizer {
+ if (!self.zoomEnabled) {
+ return;
+ }
+
+ _mbglMap->cancelTransitions();
+
+ NSPoint gesturePoint = [gestureRecognizer locationInView:self];
+ [self scaleBy:0.5 atPoint:NSMakePoint(gesturePoint.x, self.bounds.size.height - gesturePoint.y) animated:YES];
+}
+
+- (void)handleDoubleClickGesture:(NSClickGestureRecognizer *)gestureRecognizer {
+ if (!self.zoomEnabled) {
+ return;
+ }
+
+ _mbglMap->cancelTransitions();
+
+ NSPoint gesturePoint = [gestureRecognizer locationInView:self];
+ [self scaleBy:2 atPoint:NSMakePoint(gesturePoint.x, self.bounds.size.height - gesturePoint.y) animated:YES];
+}
+
+- (void)handleRotationGesture:(NSRotationGestureRecognizer *)gestureRecognizer {
+ if (!self.rotateEnabled) {
+ return;
+ }
+
+ _mbglMap->cancelTransitions();
+
+ if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
+ _mbglMap->setGestureInProgress(true);
+ _directionAtBeginningOfGesture = self.direction;
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
+ NSPoint rotationPoint = [gestureRecognizer locationInView:self];
+ mbgl::PrecisionPoint center(rotationPoint.x, rotationPoint.y);
+ _mbglMap->setBearing(_directionAtBeginningOfGesture + gestureRecognizer.rotationInDegrees, center);
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
+ || gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
+ _mbglMap->setGestureInProgress(false);
+ }
+}
+
+- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(__unused NSEventGestureAxis)axis {
+ return YES;
+}
+
+- (void)scrollWheel:(NSEvent *)event {
+ // https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKitOlderNotes/#10_7Dragging
+ if (event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone) {
+ // A traditional, vertical scroll wheel zooms instead of panning.
+ if (self.zoomEnabled && std::abs(event.scrollingDeltaX) < std::abs(event.scrollingDeltaY)) {
+ _mbglMap->cancelTransitions();
+
+ [self willChangeValueForKey:@"zoomLevel"];
+ [self willChangeValueForKey:@"centerCoordinate"];
+ NSPoint gesturePoint = [self convertPoint:event.locationInWindow fromView:nil];
+ mbgl::PrecisionPoint center(gesturePoint.x, self.bounds.size.height - gesturePoint.y);
+ _mbglMap->scaleBy(exp2(event.scrollingDeltaY / 20), center);
+ [self didChangeValueForKey:@"centerCoordinate"];
+ [self didChangeValueForKey:@"zoomLevel"];
+ }
+ } else if (self.scrollEnabled
+ && _magnificationGestureRecognizer.state == NSGestureRecognizerStatePossible
+ && _rotationGestureRecognizer.state == NSGestureRecognizerStatePossible) {
+ _mbglMap->cancelTransitions();
+
+ CGFloat x = event.scrollingDeltaX;
+ CGFloat y = event.scrollingDeltaY;
+ if (x || y) {
+ [self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO];
+ }
+
+ if (event.momentumPhase != NSEventPhaseNone) {
+ [self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO];
+ }
+ }
+}
+
+#pragma mark Keyboard events
+
+- (void)keyDown:(NSEvent *)event {
+ if (event.modifierFlags & NSNumericPadKeyMask) {
+ [self interpretKeyEvents:@[event]];
+ } else {
+ [super keyDown:event];
+ }
+}
+
+- (IBAction)moveUp:(__unused id)sender {
+ [self offsetCenterCoordinateBy:NSMakePoint(0, MGLKeyPanningIncrement) animated:YES];
+}
+
+- (IBAction)moveDown:(__unused id)sender {
+ [self offsetCenterCoordinateBy:NSMakePoint(0, -MGLKeyPanningIncrement) animated:YES];
+}
+
+- (IBAction)moveLeft:(__unused id)sender {
+ [self offsetCenterCoordinateBy:NSMakePoint(MGLKeyPanningIncrement, 0) animated:YES];
+}
+
+- (IBAction)moveRight:(__unused id)sender {
+ [self offsetCenterCoordinateBy:NSMakePoint(-MGLKeyPanningIncrement, 0) animated:YES];
+}
+
+- (IBAction)moveToBeginningOfParagraph:(__unused id)sender {
+ if (self.zoomEnabled) {
+ [self scaleBy:2 atPoint:NSZeroPoint animated:YES];
+ }
+}
+
+- (IBAction)moveToEndOfParagraph:(__unused id)sender {
+ if (self.zoomEnabled) {
+ [self scaleBy:0.5 atPoint:NSZeroPoint animated:YES];
+ }
+}
+
+- (IBAction)moveWordLeft:(__unused id)sender {
+ if (self.rotateEnabled) {
+ [self offsetDirectionBy:MGLKeyRotationIncrement animated:YES];
+ }
+}
+
+- (IBAction)moveWordRight:(__unused id)sender {
+ if (self.rotateEnabled) {
+ [self offsetDirectionBy:-MGLKeyRotationIncrement animated:YES];
+ }
+}
+
+- (void)setZoomEnabled:(BOOL)zoomEnabled {
+ _zoomEnabled = zoomEnabled;
+ _zoomControls.enabled = zoomEnabled;
+ _zoomControls.hidden = !zoomEnabled;
+}
+
+- (void)setRotateEnabled:(BOOL)rotateEnabled {
+ _rotateEnabled = rotateEnabled;
+ _compass.enabled = rotateEnabled;
+ _compass.hidden = !rotateEnabled;
+}
+
+#pragma mark Ornaments
+
+- (void)updateZoomControls {
+ [_zoomControls setEnabled:self.zoomLevel > self.minimumZoomLevel forSegment:0];
+ [_zoomControls setEnabled:self.zoomLevel < self.maximumZoomLevel forSegment:1];
+}
+
+- (void)updateCompass {
+ _compass.doubleValue = -self.direction;
+}
+
+- (IBAction)rotate:(NSSlider *)sender {
+ [self setDirection:-sender.doubleValue animated:YES];
+}
+
+#pragma mark Annotations
+
+- (nullable NS_ARRAY_OF(id <MGLAnnotation>) *)annotations {
+ if (_annotationContextsByAnnotationID.empty()) {
+ return nil;
+ }
+
+ std::vector<id <MGLAnnotation> > annotations;
+ std::transform(_annotationContextsByAnnotationID.begin(),
+ _annotationContextsByAnnotationID.end(),
+ std::back_inserter(annotations),
+ ^ id <MGLAnnotation> (const std::pair<MGLAnnotationID, MGLAnnotationContext> &pair) {
+ return pair.second.annotation;
+ });
+ return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()];
+}
+
+- (id <MGLAnnotation>)annotationWithID:(MGLAnnotationID)annotationID {
+ if (!_annotationContextsByAnnotationID.count(annotationID)) {
+ return nil;
+ }
+
+ MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationID[annotationID];
+ return annotationContext.annotation;
+}
+
+- (MGLAnnotationID)annotationIDForAnnotation:(id <MGLAnnotation>)annotation {
+ if (!annotation) {
+ return MGLAnnotationNotFound;
+ }
+
+ for (auto &pair : _annotationContextsByAnnotationID) {
+ if (pair.second.annotation == annotation) {
+ return pair.first;
+ }
+ }
+ return MGLAnnotationNotFound;
+}
+
+- (void)addAnnotation:(id <MGLAnnotation>)annotation {
+ if (annotation) {
+ [self addAnnotations:@[annotation]];
+ }
+}
+
+- (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations {
+ if (!annotations) {
+ return;
+ }
+
+ BOOL delegateHasImagesForAnnotations = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)];
+
+ std::vector<mbgl::PointAnnotation> points;
+ std::vector<mbgl::ShapeAnnotation> shapes;
+
+ for (id <MGLAnnotation> annotation in annotations) {
+ NSAssert([annotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation");
+
+ if ([annotation isKindOfClass:[MGLMultiPoint class]]) {
+ [(MGLMultiPoint *)annotation addShapeAnnotationObjectToCollection:shapes withDelegate:self];
+ } else {
+ MGLAnnotationImage *annotationImage = nil;
+ if (delegateHasImagesForAnnotations) {
+ annotationImage = [self.delegate mapView:self imageForAnnotation:annotation];
+ }
+ if (!annotationImage) {
+ annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName];
+ }
+ if (!annotationImage) {
+ NSImage *image = MGLDefaultMarkerImage();
+ NSRect alignmentRect = image.alignmentRect;
+ alignmentRect.origin.y = NSMidY(alignmentRect);
+ alignmentRect.size.height /= 2;
+ image.alignmentRect = alignmentRect;
+ annotationImage = [MGLAnnotationImage annotationImageWithImage:image
+ reuseIdentifier:MGLDefaultStyleMarkerSymbolName];
+ }
+
+ if (!self.annotationImagesByIdentifier[annotationImage.reuseIdentifier]) {
+ self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage;
+ [self installAnnotationImage:annotationImage];
+ }
+
+ NSString *symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
+ points.emplace_back(MGLLatLngFromLocationCoordinate2D(annotation.coordinate), symbolName ? [symbolName UTF8String] : "");
+
+ if (annotation.toolTip.length) {
+ _wantsToolTipRects = YES;
+ }
+ }
+ }
+
+ if (points.size()) {
+ std::vector<MGLAnnotationID> pointAnnotationIDs = _mbglMap->addPointAnnotations(points);
+
+ for (size_t i = 0; i < pointAnnotationIDs.size(); ++i) {
+ MGLAnnotationContext context;
+ context.annotation = annotations[i];
+ context.symbolIdentifier = @(points[i].icon.c_str());
+ _annotationContextsByAnnotationID[pointAnnotationIDs[i]] = context;
+ }
+ }
+
+ if (shapes.size()) {
+ std::vector<MGLAnnotationID> shapeAnnotationIDs = _mbglMap->addShapeAnnotations(shapes);
+
+ for (size_t i = 0; i < shapeAnnotationIDs.size(); ++i) {
+ MGLAnnotationContext context;
+ context.annotation = annotations[i];
+ _annotationContextsByAnnotationID[shapeAnnotationIDs[i]] = context;
+ }
+ }
+
+ [self updateAnnotationTrackingAreas];
+}
+
+- (void)installAnnotationImage:(MGLAnnotationImage *)annotationImage {
+ NSImage *image = annotationImage.image;
+ NSSize size = image.size;
+ if (size.width < 1 || size.height < 1 || !image.valid) {
+ return;
+ }
+
+ // http://www.cocoabuilder.com/archive/cocoa/82430-nsimage-getting-raw-bitmap-data.html#82431
+ [image lockFocus];
+ NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:{ NSZeroPoint, size }];
+ [image unlockFocus];
+
+ std::string pixelString((const char *)rep.bitmapData, rep.pixelsWide * rep.pixelsHigh * 4 /* RGBA */);
+ auto cSpriteImage = std::make_shared<mbgl::SpriteImage>((uint16_t)rep.size.width,
+ (uint16_t)rep.size.height,
+ (float)(rep.pixelsWide / size.width),
+ std::move(pixelString));
+ NSString *symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
+ _mbglMap->addAnnotationIcon(symbolName.UTF8String, cSpriteImage);
+
+ // Create a slop area with a “radius” equal to the annotation image’s entire
+ // size, allowing the eventual click to be on any point within this image.
+ // Union this slop area with any existing slop areas.
+ _unionedAnnotationImageSize = NSMakeSize(MAX(_unionedAnnotationImageSize.width, size.width),
+ MAX(_unionedAnnotationImageSize.height, size.height));
+
+ if (annotationImage.cursor) {
+ _wantsCursorRects = YES;
+ }
+}
+
+- (void)removeAnnotation:(id <MGLAnnotation>)annotation {
+ if (annotation) {
+ [self removeAnnotations:@[annotation]];
+ }
+}
+
+- (void)removeAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations {
+ if (!annotations) {
+ return;
+ }
+
+ std::vector<MGLAnnotationID> annotationIDsToRemove;
+ annotationIDsToRemove.reserve(annotations.count);
+
+ for (id <MGLAnnotation> annotation in annotations) {
+ NSAssert([annotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation");
+
+ MGLAnnotationID annotationID = [self annotationIDForAnnotation:annotation];
+ NSAssert(annotationID != MGLAnnotationNotFound, @"No ID for annotation %@", annotation);
+ annotationIDsToRemove.push_back(annotationID);
+
+ _annotationContextsByAnnotationID.erase(annotationID);
+
+ if (annotationID == _selectedAnnotationID) {
+ [self deselectAnnotation:annotation animated:NO];
+ }
+
+ if (annotationID == _lastSelectedAnnotationID) {
+ _lastSelectedAnnotationID = MGLAnnotationNotFound;
+ }
+ }
+
+ _mbglMap->removeAnnotations(annotationIDsToRemove);
+
+ [self updateAnnotationTrackingAreas];
+}
+
+- (id <MGLAnnotation>)selectedAnnotation {
+ if (!_annotationContextsByAnnotationID.count(_selectedAnnotationID)) {
+ return nil;
+ }
+ MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationID.at(_selectedAnnotationID);
+ return annotationContext.annotation;
+}
+
+- (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier {
+ if ([identifier hasPrefix:MGLAnnotationSpritePrefix]) {
+ identifier = [identifier substringFromIndex:MGLAnnotationSpritePrefix.length];
+ }
+ return self.annotationImagesByIdentifier[identifier];
+}
+
+- (id <MGLAnnotation>)annotationAtPoint:(NSPoint)point {
+ return [self annotationWithID:[self annotationIDAtPoint:point persistingResults:NO]];
+}
+
+- (MGLAnnotationID)annotationIDAtPoint:(NSPoint)point persistingResults:(BOOL)persist {
+ // Look for any annotation near the click. An annotation is “near” if the
+ // distance between its center and the click is less than the maximum height
+ // or width of an installed annotation image.
+ NSRect queryRect = NSInsetRect({ point, NSZeroSize },
+ -_unionedAnnotationImageSize.width / 2,
+ -_unionedAnnotationImageSize.height / 2);
+ queryRect = NSInsetRect(queryRect, -MGLAnnotationImagePaddingForHitTest,
+ -MGLAnnotationImagePaddingForHitTest);
+ std::vector<MGLAnnotationID> nearbyAnnotations = [self annotationIDsInRect:queryRect];
+
+ if (nearbyAnnotations.size()) {
+ NSRect hitRect = NSInsetRect({ point, NSZeroSize },
+ -MGLAnnotationImagePaddingForHitTest,
+ -MGLAnnotationImagePaddingForHitTest);
+ mbgl::util::erase_if(nearbyAnnotations, [&](const MGLAnnotationID annotationID) {
+ NSAssert(_annotationContextsByAnnotationID.count(annotationID) != 0, @"Unknown annotation found nearby click");
+ id <MGLAnnotation> annotation = [self annotationWithID:annotationID];
+ if (!annotation) {
+ return true;
+ }
+
+ MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithID:annotationID];
+ if (!annotationImage.selectable) {
+ return true;
+ }
+
+ NSRect annotationRect = [self frameOfImage:annotationImage.image
+ centeredAtCoordinate:annotation.coordinate];
+ return !!![annotationImage.image hitTestRect:hitRect withImageDestinationRect:annotationRect
+ context:nil hints:nil flipped:NO];
+ });
+ }
+
+ MGLAnnotationID hitAnnotationID = MGLAnnotationNotFound;
+ if (nearbyAnnotations.size()) {
+ std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end());
+
+ if (nearbyAnnotations == _annotationsNearbyLastClick) {
+ if (_lastSelectedAnnotationID == _annotationsNearbyLastClick.back()
+ || _lastSelectedAnnotationID == MGLAnnotationNotFound) {
+ hitAnnotationID = _annotationsNearbyLastClick.front();
+ } else {
+ auto result = std::find(_annotationsNearbyLastClick.begin(),
+ _annotationsNearbyLastClick.end(),
+ _lastSelectedAnnotationID);
+ auto distance = std::distance(_annotationsNearbyLastClick.begin(), result);
+ hitAnnotationID = _annotationsNearbyLastClick[distance + 1];
+ }
+ } else {
+ if (persist) {
+ _annotationsNearbyLastClick = nearbyAnnotations;
+ }
+ if (_annotationsNearbyLastClick.size()) {
+ hitAnnotationID = _annotationsNearbyLastClick.front();
+ }
+ }
+ }
+
+ return hitAnnotationID;
+}
+
+- (std::vector<MGLAnnotationID>)annotationIDsInRect:(NSRect)rect {
+ mbgl::LatLngBounds queryBounds = [self convertRectToLatLngBounds:rect];
+ return _mbglMap->getPointAnnotationsInBounds(queryBounds);
+}
+
+- (NS_ARRAY_OF(id <MGLAnnotation>) *)selectedAnnotations {
+ id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation;
+ return selectedAnnotation ? @[selectedAnnotation] : @[];
+}
+
+- (void)setSelectedAnnotation:(id <MGLAnnotation>)selectedAnnotation {
+ _selectedAnnotationID = [self annotationIDForAnnotation:selectedAnnotation];
+}
+
+- (void)setSelectedAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)selectedAnnotations {
+ if (!selectedAnnotations.count) {
+ return;
+ }
+
+ id <MGLAnnotation> firstAnnotation = selectedAnnotations[0];
+ NSAssert([firstAnnotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation");
+ if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) {
+ return;
+ }
+
+ if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) {
+ [self selectAnnotation:firstAnnotation animated:NO];
+ }
+}
+
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated
+{
+ if (!annotation || [annotation isKindOfClass:[MGLMultiPoint class]]) {
+ return;
+ }
+
+ id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation;
+ if (annotation == selectedAnnotation) {
+ return;
+ }
+
+ [self deselectAnnotation:selectedAnnotation animated:NO];
+
+ MGLAnnotationID annotationID = [self annotationIDForAnnotation:annotation];
+ if (annotationID == MGLAnnotationNotFound) {
+ [self addAnnotation:annotation];
+ }
+
+ NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithID:annotationID];
+ if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) {
+ return;
+ }
+
+ [self willChangeValueForKey:@"selectedAnnotation"];
+ _selectedAnnotationID = annotationID;
+ _lastSelectedAnnotationID = _selectedAnnotationID;
+ [self didChangeValueForKey:@"selectedAnnotation"];
+
+ if ([annotation respondsToSelector:@selector(title)]
+ && annotation.title
+ && !self.calloutForSelectedAnnotation.shown
+ && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)]
+ && [self.delegate mapView:self annotationCanShowCallout:annotation]) {
+ NSPopover *callout = [self calloutForAnnotation:annotation];
+ callout.animates = animated;
+
+ callout.delegate = self;
+ self.calloutForSelectedAnnotation = callout;
+ NSRectEdge edge = (self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft
+ ? NSMinXEdge
+ : NSMaxXEdge);
+ [callout showRelativeToRect:positioningRect ofView:self preferredEdge:edge];
+ }
+}
+
+- (NSPopover *)calloutForAnnotation:(id <MGLAnnotation>)annotation {
+ NSPopover *callout = [[NSPopover alloc] init];
+ callout.behavior = NSPopoverBehaviorTransient;
+
+ NSViewController *viewController;
+ if ([self.delegate respondsToSelector:@selector(mapView:calloutViewControllerForAnnotation:)]) {
+ viewController = [self.delegate mapView:self calloutViewControllerForAnnotation:annotation];
+ }
+ if (!viewController) {
+ viewController = [[NSViewController alloc] initWithNibName:@"MGLAnnotationCallout"
+ bundle:[NSBundle mgl_frameworkBundle]];
+ }
+ NSAssert(viewController, @"Unable to load MGLAnnotationCallout view controller");
+ viewController.representedObject = annotation;
+ callout.contentViewController = viewController;
+
+ return callout;
+}
+
+- (NSRect)positioningRectForCalloutForAnnotationWithID:(MGLAnnotationID)annotationID {
+ id <MGLAnnotation> annotation = [self annotationWithID:annotationID];
+ if (!annotation) {
+ return NSZeroRect;
+ }
+ NSImage *image = [self imageOfAnnotationWithID:annotationID].image;
+ if (!image) {
+ return NSZeroRect;
+ }
+
+ NSRect positioningRect = [self frameOfImage:image centeredAtCoordinate:annotation.coordinate];
+ positioningRect = NSOffsetRect(image.alignmentRect, positioningRect.origin.x, positioningRect.origin.y);
+ return NSInsetRect(positioningRect, -MGLAnnotationImagePaddingForCallout,
+ -MGLAnnotationImagePaddingForCallout);
+}
+
+- (NSRect)frameOfImage:(NSImage *)image centeredAtCoordinate:(CLLocationCoordinate2D)coordinate {
+ NSPoint calloutAnchorPoint = [self convertCoordinate:coordinate toPointToView:self];
+ return NSInsetRect({ calloutAnchorPoint, NSZeroSize }, -image.size.width / 2, -image.size.height / 2);
+}
+
+- (MGLAnnotationImage *)imageOfAnnotationWithID:(MGLAnnotationID)annotationID {
+ if (annotationID == MGLAnnotationNotFound
+ || _annotationContextsByAnnotationID.count(annotationID) == 0) {
+ return nil;
+ }
+
+ NSString *customSymbol = _annotationContextsByAnnotationID.at(annotationID).symbolIdentifier;
+ NSString *symbolName = customSymbol.length ? customSymbol : MGLDefaultStyleMarkerSymbolName;
+
+ return [self dequeueReusableAnnotationImageWithIdentifier:symbolName];
+}
+
+- (void)deselectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated {
+ if (!annotation || self.selectedAnnotation != annotation) {
+ return;
+ }
+
+ NSPopover *callout = self.calloutForSelectedAnnotation;
+ callout.animates = animated;
+ [callout performClose:self];
+
+ self.selectedAnnotation = nil;
+}
+
+- (void)updateAnnotationCallouts {
+ NSPopover *callout = self.calloutForSelectedAnnotation;
+ if (callout) {
+ callout.positioningRect = [self positioningRectForCalloutForAnnotationWithID:_selectedAnnotationID];
+ }
+}
+
+#pragma mark MGLMultiPointDelegate methods
+
+- (double)alphaForShapeAnnotation:(MGLShape *)annotation {
+ if (_delegateHasAlphasForShapeAnnotations) {
+ return [self.delegate mapView:self alphaForShapeAnnotation:annotation];
+ }
+ return 1.0;
+}
+
+- (mbgl::Color)strokeColorForShapeAnnotation:(MGLShape *)annotation {
+ NSColor *color = (_delegateHasStrokeColorsForShapeAnnotations
+ ? [self.delegate mapView:self strokeColorForShapeAnnotation:annotation]
+ : [NSColor blackColor]);
+ return MGLColorObjectFromNSColor(color);
+}
+
+- (mbgl::Color)fillColorForPolygonAnnotation:(MGLPolygon *)annotation {
+ NSColor *color = (_delegateHasFillColorsForShapeAnnotations
+ ? [self.delegate mapView:self fillColorForPolygonAnnotation:annotation]
+ : [NSColor blueColor]);
+ return MGLColorObjectFromNSColor(color);
+}
+
+- (CGFloat)lineWidthForPolylineAnnotation:(MGLPolyline *)annotation {
+ if (_delegateHasLineWidthsForShapeAnnotations) {
+ return [self.delegate mapView:self lineWidthForPolylineAnnotation:(MGLPolyline *)annotation];
+ }
+ return 3.0;
+}
+
+#pragma mark MGLPopoverDelegate methods
+
+- (void)popoverDidShow:(__unused NSNotification *)notification {
+ id <MGLAnnotation> annotation = self.selectedAnnotation;
+ if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) {
+ [self.delegate mapView:self didSelectAnnotation:annotation];
+ }
+}
+
+- (void)popoverDidClose:(__unused NSNotification *)notification {
+ id <MGLAnnotation> annotation = self.calloutForSelectedAnnotation.contentViewController.representedObject;
+ self.calloutForSelectedAnnotation = nil;
+ self.selectedAnnotation = nil;
+
+ if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)]) {
+ [self.delegate mapView:self didDeselectAnnotation:annotation];
+ }
+}
+
+#pragma mark Overlays
+
+- (void)addOverlay:(id <MGLOverlay>)overlay {
+ [self addOverlays:@[overlay]];
+}
+
+- (void)addOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays
+{
+ for (id <MGLOverlay> overlay in overlays) {
+ NSAssert([overlay conformsToProtocol:@protocol(MGLOverlay)], @"Overlay does not conform to MGLOverlay");
+ }
+ [self addAnnotations:overlays];
+}
+
+- (void)removeOverlay:(id <MGLOverlay>)overlay {
+ [self removeOverlays:@[overlay]];
+}
+
+- (void)removeOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays {
+ for (id <MGLOverlay> overlay in overlays) {
+ NSAssert([overlay conformsToProtocol:@protocol(MGLOverlay)], @"Overlay does not conform to MGLOverlay");
+ }
+ [self removeAnnotations:overlays];
+}
+
+#pragma mark Tooltips and cursors
+
+- (void)updateAnnotationTrackingAreas {
+ if (_wantsToolTipRects) {
+ [self removeAllToolTips];
+ std::vector<MGLAnnotationID> annotationIDs = [self annotationIDsInRect:self.bounds];
+ for (MGLAnnotationID annotationID : annotationIDs) {
+ MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithID:annotationID];
+ id <MGLAnnotation> annotation = [self annotationWithID:annotationID];
+ if (annotation.toolTip.length) {
+ NSImage *image = annotationImage.image;
+ NSRect annotationRect = [self frameOfImage:image
+ centeredAtCoordinate:annotation.coordinate];
+ annotationRect = NSOffsetRect(image.alignmentRect, annotationRect.origin.x, annotationRect.origin.y);
+ if (!NSIsEmptyRect(annotationRect)) {
+ [self addToolTipRect:annotationRect owner:self userData:(void *)(NSUInteger)annotationID];
+ }
+ }
+ if (annotationImage.cursor) {
+ _wantsCursorRects = YES;
+ }
+ }
+ }
+
+ if (_wantsCursorRects) {
+ [self.window invalidateCursorRectsForView:self];
+ }
+}
+
+- (NSString *)view:(__unused NSView *)view stringForToolTip:(__unused NSToolTipTag)tag point:(__unused NSPoint)point userData:(void *)data {
+ if ((NSUInteger)data >= MGLAnnotationNotFound) {
+ return nil;
+ }
+ MGLAnnotationID annotationID = (NSUInteger)data;
+ id <MGLAnnotation> annotation = [self annotationWithID:annotationID];
+ return annotation.toolTip;
+}
+
+- (void)resetCursorRects {
+ if (_panGestureRecognizer.state == NSGestureRecognizerStateBegan
+ || _panGestureRecognizer.state == NSGestureRecognizerStateChanged) {
+ [self addCursorRect:self.bounds cursor:[NSCursor closedHandCursor]];
+ return;
+ }
+ if (!_wantsCursorRects) {
+ return;
+ }
+
+ std::vector<MGLAnnotationID> annotationIDs = [self annotationIDsInRect:self.bounds];
+ for (MGLAnnotationID annotationID : annotationIDs) {
+ id <MGLAnnotation> annotation = [self annotationWithID:annotationID];
+ MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithID:annotationID];
+ if (annotationImage.cursor) {
+ NSImage *image = annotationImage.image;
+ NSRect annotationRect = [self frameOfImage:image
+ centeredAtCoordinate:annotation.coordinate];
+ annotationRect = NSOffsetRect(image.alignmentRect, annotationRect.origin.x, annotationRect.origin.y);
+ [self addCursorRect:annotationRect cursor:annotationImage.cursor];
+ }
+ }
+}
+
+#pragma mark Interface Builder methods
+
+- (void)prepareForInterfaceBuilder {
+ [super prepareForInterfaceBuilder];
+
+ // Color the background a glorious Mapbox teal.
+ self.layer.borderColor = [NSColor colorWithRed:59/255.
+ green:178/255.
+ blue:208/255.
+ alpha:0.8].CGColor;
+ self.layer.borderWidth = 2;
+ self.layer.backgroundColor = [NSColor colorWithRed:59/255.
+ green:178/255.
+ blue:208/255.
+ alpha:0.6].CGColor;
+
+ // Place a playful marker right smack dab in the middle.
+ self.layer.contents = MGLDefaultMarkerImage();
+ self.layer.contentsGravity = kCAGravityCenter;
+ self.layer.contentsScale = [NSScreen mainScreen].backingScaleFactor;
+}
+
+#pragma mark Geometric methods
+
+- (CLLocationCoordinate2D)convertPoint:(NSPoint)point toCoordinateFromView:(nullable NSView *)view {
+ return MGLLocationCoordinate2DFromLatLng([self convertPoint:point toLatLngFromView:view]);
+}
+
+- (mbgl::LatLng)convertPoint:(NSPoint)point toLatLngFromView:(nullable NSView *)view {
+ NSPoint convertedPoint = [self convertPoint:point fromView:view];
+ return _mbglMap->latLngForPixel(mbgl::PrecisionPoint(convertedPoint.x, convertedPoint.y));
+}
+
+- (NSPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(nullable NSView *)view {
+ return [self convertLatLng:MGLLatLngFromLocationCoordinate2D(coordinate) toPointToView:view];
+}
+
+- (NSPoint)convertLatLng:(mbgl::LatLng)latLng toPointToView:(nullable NSView *)view {
+ mbgl::vec2<double> pixel = _mbglMap->pixelForLatLng(latLng);
+ return [self convertPoint:NSMakePoint(pixel.x, pixel.y) toView:view];
+}
+
+- (MGLCoordinateBounds)convertRectToCoordinateBounds:(NSRect)rect {
+ return MGLCoordinateBoundsFromLatLngBounds([self convertRectToLatLngBounds:rect]);
+}
+
+- (mbgl::LatLngBounds)convertRectToLatLngBounds:(NSRect)rect {
+ mbgl::LatLngBounds bounds = mbgl::LatLngBounds::getExtendable();
+ bounds.extend([self convertPoint:rect.origin toLatLngFromView:self]);
+ bounds.extend([self convertPoint:{ NSMaxX(rect), NSMinY(rect) } toLatLngFromView:self]);
+ bounds.extend([self convertPoint:{ NSMaxX(rect), NSMaxY(rect) } toLatLngFromView:self]);
+ bounds.extend([self convertPoint:{ NSMinX(rect), NSMaxY(rect) } toLatLngFromView:self]);
+
+ // The world is wrapping if a point just outside the bounds is also within
+ // the rect.
+ mbgl::LatLng outsideLatLng;
+ if (bounds.sw.longitude > -180) {
+ outsideLatLng = {
+ (bounds.sw.latitude + bounds.ne.latitude) / 2,
+ bounds.sw.longitude - 1,
+ };
+ } else if (bounds.ne.longitude < 180) {
+ outsideLatLng = {
+ (bounds.sw.latitude + bounds.ne.latitude) / 2,
+ bounds.ne.longitude + 1,
+ };
+ }
+
+ // If the world is wrapping, extend the bounds to cover all longitudes.
+ if (NSPointInRect([self convertLatLng:outsideLatLng toPointToView:self], rect)) {
+ bounds.sw.longitude = -180;
+ bounds.ne.longitude = 180;
+ }
+
+ return bounds;
+}
+
+- (CLLocationDistance)metersPerPixelAtLatitude:(CLLocationDegrees)latitude {
+ return _mbglMap->getMetersPerPixelAtLatitude(latitude, self.zoomLevel);
+}
+
+#pragma mark Debugging
+
+- (MGLMapDebugMaskOptions)debugMask {
+ mbgl::MapDebugOptions options = _mbglMap->getDebug();
+ MGLMapDebugMaskOptions mask = 0;
+ if (options & mbgl::MapDebugOptions::TileBorders) {
+ mask |= MGLMapDebugTileBoundariesMask;
+ }
+ if (options & mbgl::MapDebugOptions::ParseStatus) {
+ mask |= MGLMapDebugTileInfoMask;
+ }
+ if (options & mbgl::MapDebugOptions::Timestamps) {
+ mask |= MGLMapDebugTimestampsMask;
+ }
+ if (options & mbgl::MapDebugOptions::Collision) {
+ mask |= MGLMapDebugCollisionBoxesMask;
+ }
+ return mask;
+}
+
+- (void)setDebugMask:(MGLMapDebugMaskOptions)debugMask {
+ mbgl::MapDebugOptions options = mbgl::MapDebugOptions::NoDebug;
+ if (debugMask & MGLMapDebugTileBoundariesMask) {
+ options |= mbgl::MapDebugOptions::TileBorders;
+ }
+ if (debugMask & MGLMapDebugTileInfoMask) {
+ options |= mbgl::MapDebugOptions::ParseStatus;
+ }
+ if (debugMask & MGLMapDebugTimestampsMask) {
+ options |= mbgl::MapDebugOptions::Timestamps;
+ }
+ if (debugMask & MGLMapDebugCollisionBoxesMask) {
+ options |= mbgl::MapDebugOptions::Collision;
+ }
+ _mbglMap->setDebug(options);
+}
+
+class MGLMapViewImpl : public mbgl::View {
+public:
+ MGLMapViewImpl(MGLMapView *nativeView_, const float scaleFactor_)
+ : nativeView(nativeView_), scaleFactor(scaleFactor_) {}
+ virtual ~MGLMapViewImpl() {}
+
+
+ float getPixelRatio() const override {
+ return scaleFactor;
+ }
+
+ std::array<uint16_t, 2> getSize() const override {
+ return {{ static_cast<uint16_t>(nativeView.bounds.size.width),
+ static_cast<uint16_t>(nativeView.bounds.size.height) }};
+ }
+
+ std::array<uint16_t, 2> getFramebufferSize() const override {
+ NSRect bounds = [nativeView convertRectToBacking:nativeView.bounds];
+ return {{ static_cast<uint16_t>(bounds.size.width),
+ static_cast<uint16_t>(bounds.size.height) }};
+ }
+
+ void notify() override {}
+
+ void notifyMapChange(mbgl::MapChange change) override {
+ assert([[NSThread currentThread] isMainThread]);
+ [nativeView notifyMapChange:change];
+ }
+
+ void activate() override {
+ MGLOpenGLLayer *layer = (MGLOpenGLLayer *)nativeView.layer;
+ if ([NSOpenGLContext currentContext] != layer.openGLContext) {
+ [layer.openGLContext makeCurrentContext];
+
+ mbgl::gl::InitializeExtensions([](const char *name) {
+ static CFBundleRef framework = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.opengl"));
+ if (!framework) {
+ throw std::runtime_error("Failed to load OpenGL framework.");
+ }
+
+ CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, name, kCFStringEncodingASCII);
+ void *symbol = CFBundleGetFunctionPointerForName(framework, str);
+ CFRelease(str);
+
+ return reinterpret_cast<mbgl::gl::glProc>(symbol);
+ });
+ }
+ }
+
+ void deactivate() override {
+ [NSOpenGLContext clearCurrentContext];
+ }
+
+ void invalidate() override {
+ [nativeView performSelectorOnMainThread:@selector(invalidate)
+ withObject:nil
+ waitUntilDone:NO];
+ }
+
+ void beforeRender() override {
+ activate();
+ }
+
+ void afterRender() override {}
+
+private:
+ __weak MGLMapView *nativeView = nullptr;
+ const float scaleFactor;
+};
+
+@end
diff --git a/platform/osx/src/MGLMapView_Private.h b/platform/osx/src/MGLMapView_Private.h
new file mode 100644
index 0000000000..3b37ce24b4
--- /dev/null
+++ b/platform/osx/src/MGLMapView_Private.h
@@ -0,0 +1,12 @@
+#import <mbgl/osx/MGLMapView.h>
+
+@interface MGLMapView (Private)
+
+@property (nonatomic, readonly, getter=isDormant) BOOL dormant;
+
+@property (nonatomic) CLLocationDegrees pendingLatitude;
+@property (nonatomic) CLLocationDegrees pendingLongitude;
+
+- (void)renderSync;
+
+@end
diff --git a/platform/osx/src/MGLOpenGLLayer.h b/platform/osx/src/MGLOpenGLLayer.h
new file mode 100644
index 0000000000..9429b01bb5
--- /dev/null
+++ b/platform/osx/src/MGLOpenGLLayer.h
@@ -0,0 +1,10 @@
+#import <Cocoa/Cocoa.h>
+
+#import "MGLTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MGLOpenGLLayer : NSOpenGLLayer
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/osx/src/MGLOpenGLLayer.mm b/platform/osx/src/MGLOpenGLLayer.mm
new file mode 100644
index 0000000000..06a7be961c
--- /dev/null
+++ b/platform/osx/src/MGLOpenGLLayer.mm
@@ -0,0 +1,49 @@
+#import "MGLOpenGLLayer.h"
+
+#import "MGLMapView_Private.h"
+
+#import <mbgl/platform/gl.hpp>
+
+@implementation MGLOpenGLLayer
+
+- (MGLMapView *)mapView {
+ return (MGLMapView *)super.view;
+}
+
+//- (BOOL)isAsynchronous {
+// return YES;
+//}
+
+- (BOOL)needsDisplayOnBoundsChange {
+ return YES;
+}
+
+- (CGRect)frame {
+ return self.view.bounds;
+}
+
+- (NSOpenGLPixelFormat *)openGLPixelFormatForDisplayMask:(uint32_t)mask {
+ NSOpenGLPixelFormatAttribute pfas[] = {
+ NSOpenGLPFAAccelerated,
+ NSOpenGLPFAClosestPolicy,
+ NSOpenGLPFAAccumSize, 32,
+ NSOpenGLPFAColorSize, 24,
+ NSOpenGLPFAAlphaSize, 8,
+ NSOpenGLPFADepthSize, 16,
+ NSOpenGLPFAStencilSize, 8,
+ NSOpenGLPFAScreenMask, mask,
+ 0
+ };
+ return [[NSOpenGLPixelFormat alloc] initWithAttributes:pfas];
+}
+
+- (BOOL)canDrawInOpenGLContext:(__unused NSOpenGLContext *)context pixelFormat:(__unused NSOpenGLPixelFormat *)pixelFormat forLayerTime:(__unused CFTimeInterval)t displayTime:(__unused const CVTimeStamp *)ts {
+ return !self.mapView.dormant;
+}
+
+- (void)drawInOpenGLContext:(NSOpenGLContext *)context pixelFormat:(NSOpenGLPixelFormat *)pixelFormat forLayerTime:(CFTimeInterval)t displayTime:(const CVTimeStamp *)ts {
+ [self.mapView renderSync];
+ [super drawInOpenGLContext:context pixelFormat:pixelFormat forLayerTime:t displayTime:ts];
+}
+
+@end
diff --git a/platform/osx/src/NSBundle+MGLAdditions.h b/platform/osx/src/NSBundle+MGLAdditions.h
new file mode 100644
index 0000000000..6d6ebc35ad
--- /dev/null
+++ b/platform/osx/src/NSBundle+MGLAdditions.h
@@ -0,0 +1,15 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+void mgl_linkBundleCategory();
+
+@interface NSBundle (MGLAdditions)
+
++ (instancetype)mgl_frameworkBundle;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/osx/src/NSBundle+MGLAdditions.m b/platform/osx/src/NSBundle+MGLAdditions.m
new file mode 100644
index 0000000000..658d515ebf
--- /dev/null
+++ b/platform/osx/src/NSBundle+MGLAdditions.m
@@ -0,0 +1,13 @@
+#import "NSBundle+MGLAdditions.h"
+
+#import "MGLAccountManager.h"
+
+void mgl_linkBundleCategory() {}
+
+@implementation NSBundle (MGLAdditions)
+
++ (instancetype)mgl_frameworkBundle {
+ return [self bundleForClass:[MGLAccountManager class]];
+}
+
+@end
diff --git a/platform/osx/src/NSProcessInfo+MGLAdditions.h b/platform/osx/src/NSProcessInfo+MGLAdditions.h
new file mode 100644
index 0000000000..6b34f54756
--- /dev/null
+++ b/platform/osx/src/NSProcessInfo+MGLAdditions.h
@@ -0,0 +1,9 @@
+#import <Foundation/Foundation.h>
+
+void mgl_linkProcessInfoCategory();
+
+@interface NSProcessInfo (MGLAdditions)
+
+- (BOOL)mgl_isInterfaceBuilderDesignablesAgent;
+
+@end
diff --git a/platform/osx/src/NSProcessInfo+MGLAdditions.m b/platform/osx/src/NSProcessInfo+MGLAdditions.m
new file mode 100644
index 0000000000..16f869703c
--- /dev/null
+++ b/platform/osx/src/NSProcessInfo+MGLAdditions.m
@@ -0,0 +1,11 @@
+#import "NSProcessInfo+MGLAdditions.h"
+
+void mgl_linkProcessInfoCategory() {}
+
+@implementation NSProcessInfo (MGLAdditions)
+
+- (BOOL)mgl_isInterfaceBuilderDesignablesAgent {
+ return [self.processName isEqualToString:@"IBDesignablesAgent"];
+}
+
+@end
diff --git a/platform/osx/src/resources/MGLAnnotationCallout.xib b/platform/osx/src/resources/MGLAnnotationCallout.xib
new file mode 100644
index 0000000000..edf84a26a7
--- /dev/null
+++ b/platform/osx/src/resources/MGLAnnotationCallout.xib
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="9531" systemVersion="15B42" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="9531"/>
+ </dependencies>
+ <objects>
+ <customObject id="-2" userLabel="File's Owner" customClass="NSViewController">
+ <connections>
+ <outlet property="view" destination="c22-O7-iKe" id="QAM-0O-WIj"/>
+ </connections>
+ </customObject>
+ <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+ <customObject id="-3" userLabel="Application" customClass="NSObject"/>
+ <customView id="c22-O7-iKe">
+ <rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+ <subviews>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="k5x-ao-Pz3">
+ <rect key="frame" x="18" y="25" width="234" height="17"/>
+ <textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" title="Title" id="nVE-Zi-KcG">
+ <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>
+ <binding destination="-2" name="value" keyPath="representedObject.title" id="3nD-YS-gzq"/>
+ </connections>
+ </textField>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e9C-Ve-ccw">
+ <rect key="frame" x="18" y="8" width="234" height="14"/>
+ <textFieldCell key="cell" controlSize="small" selectable="YES" sendsActionOnEndEditing="YES" title="Subtitle" id="eKw-tQ-dw8">
+ <font key="font" metaFont="smallSystem"/>
+ <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ <connections>
+ <binding destination="-2" name="value" keyPath="representedObject.subtitle" id="RQf-48-DyH"/>
+ </connections>
+ </textField>
+ </subviews>
+ <constraints>
+ <constraint firstItem="e9C-Ve-ccw" firstAttribute="leading" secondItem="k5x-ao-Pz3" secondAttribute="leading" id="ApT-ew-CYb"/>
+ <constraint firstAttribute="bottom" secondItem="e9C-Ve-ccw" secondAttribute="bottom" constant="8" id="CWV-Dd-8oi"/>
+ <constraint firstItem="k5x-ao-Pz3" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="20" id="UUL-GB-Jtv"/>
+ <constraint firstItem="e9C-Ve-ccw" firstAttribute="top" secondItem="k5x-ao-Pz3" secondAttribute="bottom" constant="3" id="Urc-wn-m8X"/>
+ <constraint firstItem="e9C-Ve-ccw" firstAttribute="trailing" secondItem="k5x-ao-Pz3" secondAttribute="trailing" id="gss-6G-9GF"/>
+ <constraint firstAttribute="trailing" secondItem="k5x-ao-Pz3" secondAttribute="trailing" constant="20" id="xCZ-s9-HaP"/>
+ <constraint firstItem="k5x-ao-Pz3" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="8" id="xcm-oY-jjy"/>
+ </constraints>
+ <point key="canvasLocation" x="257" y="355"/>
+ </customView>
+ </objects>
+</document>
diff --git a/platform/osx/src/resources/default_marker.pdf b/platform/osx/src/resources/default_marker.pdf
new file mode 100644
index 0000000000..4e2e332301
--- /dev/null
+++ b/platform/osx/src/resources/default_marker.pdf
Binary files differ
diff --git a/platform/osx/src/resources/mapbox.pdf b/platform/osx/src/resources/mapbox.pdf
new file mode 100644
index 0000000000..c08a0e3135
--- /dev/null
+++ b/platform/osx/src/resources/mapbox.pdf
Binary files differ