summaryrefslogtreecommitdiff
path: root/platform/macos/src
diff options
context:
space:
mode:
Diffstat (limited to 'platform/macos/src')
-rw-r--r--platform/macos/src/MGLAnnotationImage.h64
-rw-r--r--platform/macos/src/MGLAnnotationImage.m26
-rw-r--r--platform/macos/src/MGLAnnotationImage_Private.h8
-rw-r--r--platform/macos/src/MGLAttributionButton.h15
-rw-r--r--platform/macos/src/MGLAttributionButton.m50
-rw-r--r--platform/macos/src/MGLCompassCell.h5
-rw-r--r--platform/macos/src/MGLCompassCell.m34
-rw-r--r--platform/macos/src/MGLMapView+IBAdditions.h68
-rw-r--r--platform/macos/src/MGLMapView+IBAdditions.m118
-rw-r--r--platform/macos/src/MGLMapView.h903
-rw-r--r--platform/macos/src/MGLMapView.mm2499
-rw-r--r--platform/macos/src/MGLMapViewDelegate.h220
-rw-r--r--platform/macos/src/MGLMapView_Private.h23
-rw-r--r--platform/macos/src/MGLOpenGLLayer.h12
-rw-r--r--platform/macos/src/MGLOpenGLLayer.mm49
-rw-r--r--platform/macos/src/Mapbox.h34
16 files changed, 4128 insertions, 0 deletions
diff --git a/platform/macos/src/MGLAnnotationImage.h b/platform/macos/src/MGLAnnotationImage.h
new file mode 100644
index 0000000000..ad44993ee1
--- /dev/null
+++ b/platform/macos/src/MGLAnnotationImage.h
@@ -0,0 +1,64 @@
+#import <AppKit/AppKit.h>
+
+#import "MGLTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ The `MGLAnnotationImage` class is responsible for presenting point-based
+ annotations visually on an `MGLMapView` instance. Annotation image objects pair
+ `NSImage` objects with annotation-related metadata. They may be recycled later
+ and put into a reuse queue that is maintained by the map view.
+ */
+@interface MGLAnnotationImage : NSObject
+
+#pragma mark Initializing and Preparing the Image Object
+
+/**
+ Initializes and returns a new annotation image object.
+
+ @param image The image to display for the annotation.
+ @param reuseIdentifier The string that identifies this annotation image in the
+ reuse queue.
+ @return The initialized annotation image object or `nil` if there was a problem
+ initializing the object.
+ */
++ (instancetype)annotationImageWithImage:(NSImage *)image reuseIdentifier:(NSString *)reuseIdentifier;
+
+#pragma mark Getting and Setting Attributes
+
+/** The image to display for the annotation. */
+@property (nonatomic, readonly) NSImage *image;
+
+/**
+ The string that identifies this annotation image in the reuse queue.
+ (read-only)
+
+ You specify the reuse identifier when you create the image object. You use this
+ type later to retrieve an annotation image object that was created previously
+ but which is currently unused because its annotation is not on-screen.
+
+ If you define distinctly different types of annotations (with distinctly
+ different annotation images to go with them), you can differentiate between the
+ annotation types by specifying different reuse identifiers for each one.
+ */
+@property (nonatomic, readonly) NSString *reuseIdentifier;
+
+/**
+ A Boolean value indicating whether the annotation is selectable.
+
+ The default value of this property is `YES`. If the value of this property is
+ `NO`, the annotation image ignores click events and cannot be selected.
+ */
+@property (nonatomic, getter=isSelectable) BOOL selectable;
+
+/**
+ The cursor that appears above any annotation using this annotation image.
+
+ By default, this property is set to `nil`, representing the current cursor.
+ */
+@property (nonatomic, nullable) NSCursor *cursor;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/macos/src/MGLAnnotationImage.m b/platform/macos/src/MGLAnnotationImage.m
new file mode 100644
index 0000000000..1b545651d2
--- /dev/null
+++ b/platform/macos/src/MGLAnnotationImage.m
@@ -0,0 +1,26 @@
+#import "MGLAnnotationImage_Private.h"
+
+@interface MGLAnnotationImage ()
+
+@property (nonatomic) NSImage *image;
+@property (nonatomic) NSString *reuseIdentifier;
+@property (nonatomic, strong, nullable) NSString *styleIconIdentifier;
+
+@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/macos/src/MGLAnnotationImage_Private.h b/platform/macos/src/MGLAnnotationImage_Private.h
new file mode 100644
index 0000000000..21963a86a0
--- /dev/null
+++ b/platform/macos/src/MGLAnnotationImage_Private.h
@@ -0,0 +1,8 @@
+#import <Mapbox/Mapbox.h>
+
+@interface MGLAnnotationImage (Private)
+
+/// Unique identifier of the sprite image used by the style to represent the receiver’s `image`.
+@property (nonatomic, strong, nullable) NSString *styleIconIdentifier;
+
+@end
diff --git a/platform/macos/src/MGLAttributionButton.h b/platform/macos/src/MGLAttributionButton.h
new file mode 100644
index 0000000000..9ff3137849
--- /dev/null
+++ b/platform/macos/src/MGLAttributionButton.h
@@ -0,0 +1,15 @@
+#import <Cocoa/Cocoa.h>
+
+/// Button that looks like a hyperlink and opens a URL.
+@interface MGLAttributionButton : NSButton
+
+/// Returns an `MGLAttributionButton` instance with the given title and URL.
+- (instancetype)initWithTitle:(NSString *)title URL:(NSURL *)url;
+
+/// The URL to open and display as a tooltip.
+@property (nonatomic) NSURL *URL;
+
+/// Opens the URL.
+- (IBAction)openURL:(id)sender;
+
+@end
diff --git a/platform/macos/src/MGLAttributionButton.m b/platform/macos/src/MGLAttributionButton.m
new file mode 100644
index 0000000000..e21b860794
--- /dev/null
+++ b/platform/macos/src/MGLAttributionButton.m
@@ -0,0 +1,50 @@
+#import "MGLAttributionButton.h"
+
+#import "NSBundle+MGLAdditions.h"
+
+@implementation MGLAttributionButton {
+ NSTrackingRectTag _trackingAreaTag;
+}
+
+- (instancetype)initWithTitle:(NSString *)title URL:(NSURL *)url {
+ if (self = [super initWithFrame:NSZeroRect]) {
+ self.bordered = NO;
+ self.bezelStyle = NSRegularSquareBezelStyle;
+
+ // Start with a copyright symbol. The whole string will be mini.
+ NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:NSLocalizedStringWithDefaultValue(@"COPYRIGHT_PREFIX", nil, nil, @"© ", @"Copyright notice prefix") attributes:@{
+ NSFontAttributeName: [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]],
+ }];
+ // Append the specified title, underlining it like a hyperlink.
+ [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 {
+ // The whole button gets a pointing hand cursor, just like a hyperlink.
+ [self addCursorRect:self.bounds cursor:[NSCursor pointingHandCursor]];
+}
+
+- (IBAction)openURL:(__unused id)sender {
+ [[NSWorkspace sharedWorkspace] openURL:self.URL];
+}
+
+@end
diff --git a/platform/macos/src/MGLCompassCell.h b/platform/macos/src/MGLCompassCell.h
new file mode 100644
index 0000000000..5ed70dcb06
--- /dev/null
+++ b/platform/macos/src/MGLCompassCell.h
@@ -0,0 +1,5 @@
+#import <Cocoa/Cocoa.h>
+
+/// Circular slider with an arrow pointing north.
+@interface MGLCompassCell : NSSliderCell
+@end
diff --git a/platform/macos/src/MGLCompassCell.m b/platform/macos/src/MGLCompassCell.m
new file mode 100644
index 0000000000..b3a4ad4544
--- /dev/null
+++ b/platform/macos/src/MGLCompassCell.m
@@ -0,0 +1,34 @@
+#import "MGLCompassCell.h"
+
+@implementation MGLCompassCell
+
+- (instancetype)init {
+ if (self = [super init]) {
+ self.sliderType = NSCircularSlider;
+ // A tick mark for each cardinal direction.
+ self.numberOfTickMarks = 4;
+ // This slider goes backwards!
+ self.minValue = -360;
+ self.maxValue = 0;
+ }
+ return self;
+}
+
+- (void)drawKnob:(NSRect)knobRect {
+ // Draw a red triangle pointing whichever way the slider is facing.
+ 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/macos/src/MGLMapView+IBAdditions.h b/platform/macos/src/MGLMapView+IBAdditions.h
new file mode 100644
index 0000000000..81f4506a57
--- /dev/null
+++ b/platform/macos/src/MGLMapView+IBAdditions.h
@@ -0,0 +1,68 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLMapView.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MGLMapView (IBAdditions)
+
+#if TARGET_INTERFACE_BUILDER
+
+// Core properties that can be manipulated in the Attributes inspector in
+// Interface Builder. These redeclarations merely add the IBInspectable keyword.
+// They appear here to ensure that they appear above the convenience properties;
+// inspectables declared in MGLMapView.h are always sorted before those in
+// MGLMapView+IBAdditions.h, due to ASCII sort order.
+
+// We want this property to look like a URL bar in the Attributes inspector, but
+// just calling it styleURL would violate Cocoa naming conventions and conflict
+// with the existing NSURL property. Fortunately, IB strips out the two
+// underscores for display.
+
+/** URL of the style currently displayed in the receiver.
+
+ The URL may be a full HTTP or HTTPS URL, a Mapbox URL indicating the style’s
+ map ID (`mapbox://styles/<user>/<style>`), or a path to a local file
+ relative to the application’s resource path. Leave this field blank for the
+ default style. */
+@property (nonatomic, nullable) IBInspectable NSString *styleURL__;
+
+// Convenience properties related to the initial viewport. These properties
+// are not meant to be used outside of Interface Builder. latitude and longitude
+// are backed by properties of type CLLocationDegrees, but these declarations
+// must use the type double because Interface Builder is unaware that
+// CLLocationDegrees is a typedef for double.
+
+/** The initial center latitude. */
+@property (nonatomic) IBInspectable double latitude;
+
+/** The initial center longitude. */
+@property (nonatomic) IBInspectable double longitude;
+
+@property (nonatomic) IBInspectable double zoomLevel;
+
+// Renamed properties. Interface Builder derives the display name of each
+// inspectable from the runtime name, but runtime names don’t always make sense
+// in UI.
+
+/** A Boolean value that determines whether the user may zoom the map, changing
+ its zoom level. */
+@property (nonatomic) IBInspectable BOOL allowsZooming;
+
+/** A Boolean value that determines whether the user may scroll around the map,
+ changing its center coordinate. */
+@property (nonatomic) IBInspectable BOOL allowsScrolling;
+
+/** A Boolean value that determines whether the user may rotate the map,
+ changing its direction. */
+@property (nonatomic) IBInspectable BOOL allowsRotating;
+
+/** A Boolean value that determines whether the user may tilt the map, changing
+ its pitch. */
+@property (nonatomic) IBInspectable BOOL allowsTilting;
+
+#endif
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/macos/src/MGLMapView+IBAdditions.m b/platform/macos/src/MGLMapView+IBAdditions.m
new file mode 100644
index 0000000000..eada47ef90
--- /dev/null
+++ b/platform/macos/src/MGLMapView+IBAdditions.m
@@ -0,0 +1,118 @@
+#import "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", @"camera", nil];
+}
+
+- (double)latitude {
+ return self.centerCoordinate.latitude;
+}
+
+- (void)setLatitude:(double)latitude {
+ if (!isnan(self.pendingLongitude)) {
+ // With both components present, set the real center coordinate and
+ // forget the pending parts.
+ 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", @"camera", nil];
+}
+
+- (double)longitude {
+ return self.centerCoordinate.longitude;
+}
+
+- (void)setLongitude:(double)longitude {
+ if (!isnan(self.pendingLatitude)) {
+ // With both components present, set the real center coordinate and
+ // forget the pending parts.
+ 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/macos/src/MGLMapView.h b/platform/macos/src/MGLMapView.h
new file mode 100644
index 0000000000..7b3efd293b
--- /dev/null
+++ b/platform/macos/src/MGLMapView.h
@@ -0,0 +1,903 @@
+#import <Cocoa/Cocoa.h>
+#import <CoreLocation/CoreLocation.h>
+
+#import "MGLGeometry.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Options for enabling debugging features in an MGLMapView instance. */
+typedef NS_OPTIONS(NSUInteger, MGLMapDebugMaskOptions) {
+ /** Edges of tile boundaries are shown as thick, red lines to help diagnose
+ tile clipping issues. */
+ MGLMapDebugTileBoundariesMask = 1 << 1,
+
+ /** Each tile shows its tile coordinate (x/y/z) in the upper-left corner. */
+ MGLMapDebugTileInfoMask = 1 << 2,
+
+ /** Each tile shows a timestamp indicating when it was loaded. */
+ MGLMapDebugTimestampsMask = 1 << 3,
+
+ /** Edges of glyphs and symbols are shown as faint, green lines to help
+ diagnose collision and label placement issues. */
+ MGLMapDebugCollisionBoxesMask = 1 << 4,
+
+ /** Line widths, backgrounds, and fill colors are ignored to create a
+ wireframe effect. */
+ MGLMapDebugWireframesMask = 1 << 5,
+
+ /** The stencil buffer is shown instead of the color buffer. */
+ MGLMapDebugStencilBufferMask = 1 << 6,
+};
+
+@class MGLAnnotationImage;
+@class MGLMapCamera;
+
+@protocol MGLAnnotation;
+@protocol MGLMapViewDelegate;
+@protocol MGLOverlay;
+@protocol MGLFeature;
+
+/**
+ An interactive, customizable map view with an interface similar to the one
+ provided by Apple’s MapKit.
+
+ Using `MGLMapView`, you can embed the map inside a view, allow users to
+ manipulate it with standard gestures, animate the map between different
+ viewpoints, and present information in the form of annotations and overlays.
+
+ The map view loads scalable vector tiles that conform to the
+ <a href="https://github.com/mapbox/vector-tile-spec">Mapbox Vector Tile Specification</a>.
+ It styles them with a style that conforms to the
+ <a href="https://www.mapbox.com/mapbox-gl-style-spec/">Mapbox GL style specification</a>.
+ Such styles can be designed in
+ <a href="https://www.mapbox.com/studio/">Mapbox Studio</a> and hosted on
+ mapbox.com.
+
+ A collection of Mapbox-hosted styles is available through the `MGLStyle` class.
+ These basic styles use
+ <a href="https://www.mapbox.com/developers/vector-tiles/mapbox-streets">Mapbox Streets</a>
+ or <a href="https://www.mapbox.com/satellite/">Mapbox Satellite</a> data
+ sources, but you can specify a custom style that makes use of your own data.
+
+ Mapbox-hosted vector tiles and styles require an API access token, which you
+ can obtain from the
+ <a href="https://www.mapbox.com/studio/account/tokens/">Mapbox account page</a>.
+ Access tokens associate requests to Mapbox’s vector tile and style APIs with
+ your Mapbox account. They also deter other developers from using your styles
+ without your permission.
+
+ @note You are responsible for getting permission to use the map data and for
+ ensuring that your use adheres to the relevant terms of use.
+ */
+IB_DESIGNABLE
+@interface MGLMapView : NSView
+
+#pragma mark Creating Instances
+
+/**
+ Initializes and returns a newly allocated map view with the specified frame and
+ the default style.
+
+ @param frame The frame for the view, measured in points.
+ @return An initialized map view.
+ */
+- (instancetype)initWithFrame:(NSRect)frame;
+
+/**
+ Initializes and returns a newly allocated map view with the specified frame and
+ style URL.
+
+ @param frame The frame for the view, measured in points.
+ @param styleURL URL of the map style to display. The URL may be a full HTTP or
+ HTTPS URL, a Mapbox URL indicating the style’s map ID
+ (`mapbox://styles/<user>/<style>`), or a path to a local file relative to
+ the application’s resource path. Specify `nil` for the default style.
+ @return An initialized map view.
+ */
+- (instancetype)initWithFrame:(NSRect)frame styleURL:(nullable NSURL *)styleURL;
+
+#pragma mark Accessing the Delegate
+
+/**
+ The receiver’s delegate.
+
+ A map view sends messages to its delegate to notify it of changes to its
+ contents or the viewpoint. The delegate also provides information about
+ annotations displayed on the map, such as the styles to apply to individual
+ annotations.
+ */
+@property (nonatomic, weak, nullable) IBOutlet id <MGLMapViewDelegate> delegate;
+
+#pragma mark Configuring the Map’s Appearance
+
+/**
+ URL of the style currently displayed in the receiver.
+
+ The URL may be a full HTTP or HTTPS URL, a Mapbox URL indicating the style’s
+ map ID (`mapbox://styles/<user>/<style>`), or a path to a local file relative
+ to the application’s resource path.
+
+ If you set this property to `nil`, the receiver will use the default style and
+ this property will automatically be set to that style’s URL.
+ */
+@property (nonatomic, null_resettable) NSURL *styleURL;
+
+/**
+ Reloads the style.
+
+ You do not normally need to call this method. The map view automatically
+ responds to changes in network connectivity by reloading the style. You may
+ need to call this method if you change the access token after a style has
+ loaded but before loading a style associated with a different Mapbox account.
+ */
+- (IBAction)reloadStyle:(id)sender;
+
+/**
+ A control for zooming in and out, positioned in the lower-right corner.
+ */
+@property (nonatomic, readonly) NSSegmentedControl *zoomControls;
+
+/**
+ A control indicating the map’s direction and allowing the user to manipulate
+ the direction, positioned above the zoom controls in the lower-right corner.
+ */
+@property (nonatomic, readonly) NSSlider *compass;
+
+/**
+ The Mapbox logo, positioned in the lower-left corner.
+
+ @note The Mapbox terms of service, which governs the use of Mapbox-hosted
+ vector tiles and styles,
+ <a href="https://www.mapbox.com/help/mapbox-logo/">requires</a> most Mapbox
+ customers to display the Mapbox logo. If this applies to you, do not hide
+ this view or change its contents.
+ */
+@property (nonatomic, readonly) NSImageView *logoView;
+
+/**
+ A view showing legally required copyright notices, positioned along the bottom
+ of the map view, to the left of the Mapbox logo.
+
+ @note The Mapbox terms of service, which governs the use of Mapbox-hosted
+ vector tiles and styles,
+ <a href="https://www.mapbox.com/help/attribution/">requires</a> these
+ copyright notices to accompany any map that features Mapbox-designed styles,
+ OpenStreetMap data, or other Mapbox data such as satellite or terrain data.
+ If that applies to this map view, do not hide this view or remove any
+ notices from it.
+ */
+@property (nonatomic, readonly) NSView *attributionView;
+
+#pragma mark Manipulating the Viewpoint
+
+/**
+ The geographic coordinate at the center of the map view.
+
+ Changing the value of this property centers the map on the new coordinate
+ without changing the current zoom level.
+
+ Changing the value of this property updates the map view immediately. If you
+ want to animate the change, use the `-setCenterCoordinate:animated:` method
+ instead.
+ */
+@property (nonatomic) CLLocationCoordinate2D centerCoordinate;
+
+/**
+ Changes the center coordinate of the map and optionally animates the change.
+
+ Changing the center coordinate centers the map on the new coordinate without
+ changing the current zoom level.
+
+ @param coordinate The new center coordinate for the map.
+ @param animated Specify `YES` if you want the map view to scroll to the new
+ location or `NO` if you want the map to display the new location
+ immediately.
+ */
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated;
+
+/**
+ The zoom level of the receiver.
+
+ In addition to affecting the visual size and detail of features on the map, the
+ zoom level affects the size of the vector tiles that are loaded. At zoom level
+ 0, each tile covers the entire world map; at zoom level 1, it covers ¼ of the
+ world; at zoom level 2, <sup>1</sup>⁄<sub>16</sub> of the world, and so on.
+
+ Changing the value of this property updates the map view immediately. If you
+ want to animate the change, use the `-setZoomLevel:animated:` method instead.
+ */
+@property (nonatomic) double zoomLevel;
+
+/**
+ The minimum zoom level at which the map can be shown.
+
+ Depending on the map view’s aspect ratio, the map view may be prevented from
+ reaching the minimum zoom level, in order to keep the map from repeating within
+ the current viewport.
+
+ If the value of this property is greater than that of the `maximumZoomLevel`
+ property, the behavior is undefined.
+
+ The default value of this property is 0.
+ */
+@property (nonatomic) double minimumZoomLevel;
+
+/**
+ The maximum zoom level the map can be shown at.
+
+ If the value of this property is smaller than that of the `minimumZoomLevel`
+ property, the behavior is undefined.
+
+ The default value of this property is 20.
+ */
+@property (nonatomic) double maximumZoomLevel;
+
+/**
+ Changes the zoom level of the map and optionally animates the change.
+
+ Changing the zoom level scales the map without changing the current center
+ coordinate.
+
+ @param zoomLevel The new zoom level for the map.
+ @param animated Specify `YES` if you want the map view to animate the change
+ to the new zoom level or `NO` if you want the map to display the new zoom
+ level immediately.
+ */
+- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated;
+
+/**
+ The heading of the map, measured in degrees clockwise from true north.
+
+ The value `0` means that the top edge of the map view corresponds to true
+ north. The value `90` means the top of the map is pointing due east. The value
+ `180` means the top of the map points due south, and so on.
+
+ Changing the value of this property updates the map view immediately. If you
+ want to animate the change, use the `-setDirection:animated:` method instead.
+ */
+@property (nonatomic) CLLocationDirection direction;
+
+/**
+ Changes the heading of the map and optionally animates the change.
+
+ Changing the heading rotates the map without changing the current center
+ coordinate or zoom level.
+
+ @param direction The heading of the map, measured in degrees clockwise from
+ true north.
+ @param animated Specify `YES` if you want the map view to animate the change
+ to the new heading or `NO` if you want the map to display the new heading
+ immediately.
+ */
+- (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated;
+
+/**
+ A camera representing the current viewpoint of the map.
+ */
+@property (nonatomic, copy) MGLMapCamera *camera;
+
+/**
+ Moves the viewpoint to a different location with respect to the map with an
+ optional transition animation.
+
+ @param camera The new viewpoint.
+ @param animated Specify `YES` if you want the map view to animate the change to
+ the new viewpoint or `NO` if you want the map to display the new viewpoint
+ immediately.
+ */
+- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated;
+
+/**
+ Moves the viewpoint to a different location with respect to the map with an
+ optional transition duration and timing function.
+
+ @param camera The new viewpoint.
+ @param duration The amount of time, measured in seconds, that the transition
+ animation should take. Specify `0` to jump to the new viewpoint
+ instantaneously.
+ @param function A timing function used for the animation. Set this parameter to
+ `nil` for a transition that matches most system animations. If the duration
+ is `0`, this parameter is ignored.
+ @param completion The block to execute after the animation finishes.
+ */
+- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion;
+
+/**
+ Moves the viewpoint to a different location using a transition animation that
+ evokes powered flight and a default duration based on the length of the flight
+ path.
+
+ The transition animation seamlessly incorporates zooming and panning to help
+ the user find his or her bearings even after traversing a great distance.
+
+ @param camera The new viewpoint.
+ @param completion The block to execute after the animation finishes.
+ */
+- (void)flyToCamera:(MGLMapCamera *)camera completionHandler:(nullable void (^)(void))completion;
+
+/**
+ Moves the viewpoint to a different location using a transition animation that
+ evokes powered flight and an optional transition duration.
+
+ The transition animation seamlessly incorporates zooming and panning to help
+ the user find his or her bearings even after traversing a great distance.
+
+ @param camera The new viewpoint.
+ @param duration The amount of time, measured in seconds, that the transition
+ animation should take. Specify `0` to jump to the new viewpoint
+ instantaneously. Specify a negative value to use the default duration, which
+ is based on the length of the flight path.
+ @param completion The block to execute after the animation finishes.
+ */
+- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion;
+
+/**
+ Moves the viewpoint to a different location using a transition animation that
+ evokes powered flight and an optional transition duration and peak altitude.
+
+ The transition animation seamlessly incorporates zooming and panning to help
+ the user find his or her bearings even after traversing a great distance.
+
+ @param camera The new viewpoint.
+ @param duration The amount of time, measured in seconds, that the transition
+ animation should take. Specify `0` to jump to the new viewpoint
+ instantaneously. Specify a negative value to use the default duration, which
+ is based on the length of the flight path.
+ @param peakAltitude The altitude, measured in meters, at the midpoint of the
+ animation. The value of this parameter is ignored if it is negative or if
+ the animation transition resulting from a similar call to
+ `-setCamera:animated:` would have a midpoint at a higher altitude.
+ @param completion The block to execute after the animation finishes.
+ */
+- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion;
+
+/**
+ The geographic coordinate bounds visible in the receiver’s viewport.
+
+ Changing the value of this property updates the receiver immediately. If you
+ want to animate the change, use the `-setVisibleCoordinateBounds:animated:`
+ method instead.
+ */
+@property (nonatomic) MGLCoordinateBounds visibleCoordinateBounds;
+
+/**
+ Changes the receiver’s viewport to fit the given coordinate bounds, optionally
+ animating the change.
+
+ @param bounds The bounds that the viewport will show in its entirety.
+ @param animated Specify `YES` to animate the change by smoothly scrolling and
+ zooming or `NO` to immediately display the given bounds.
+ */
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds animated:(BOOL)animated;
+
+/**
+ Changes the receiver’s viewport to fit the given coordinate bounds and
+ optionally some additional padding on each side.
+
+ @param bounds The bounds that the viewport will show in its entirety.
+ @param insets The minimum padding (in screen points) that will be visible
+ around the given coordinate bounds.
+ @param animated Specify `YES` to animate the change by smoothly scrolling and
+ zooming or `NO` to immediately display the given bounds.
+ */
+- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated;
+
+/**
+ Returns the camera that best fits the given coordinate bounds.
+
+ @param bounds The coordinate bounds to fit to the receiver’s viewport.
+ @return A camera object centered on the same location as the coordinate bounds
+ with zoom level as high (close to the ground) as possible while still
+ including the entire coordinate bounds. The camera object uses the current
+ direction and pitch.
+ */
+- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds;
+
+/**
+ Returns the camera that best fits the given coordinate bounds, optionally with
+ some additional padding on each side.
+
+ @param bounds The coordinate bounds to fit to the receiver’s viewport.
+ @param insets The minimum padding (in screen points) that would be visible
+ around the returned camera object if it were set as the receiver’s camera.
+ @return A camera object centered on the same location as the coordinate bounds
+ with zoom level as high (close to the ground) as possible while still
+ including the entire coordinate bounds. The camera object uses the current
+ direction and pitch.
+ */
+- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets;
+
+/**
+ A Boolean value indicating whether the receiver automatically adjusts its
+ content insets.
+
+ When the value of this property is `YES`, the map view automatically updates
+ its `contentInsets` property to account for any overlapping title bar or
+ toolbar. To overlap with the title bar or toolbar, the containing window’s
+ style mask must have `NSFullSizeContentViewWindowMask` set, and the title bar
+ must not be transparent.
+
+ The default value of this property is `YES`.
+ */
+@property (nonatomic, assign) BOOL automaticallyAdjustsContentInsets;
+
+/**
+ The distance from the edges of the map view’s frame to the edges of the map
+ view’s logical viewport.
+
+ When the value of this property is equal to `NSEdgeInsetsZero`, viewport
+ properties such as `centerCoordinate` assume a viewport that matches the map
+ view’s frame. Otherwise, those properties are inset, excluding part of the
+ frame from the viewport. For instance, if the only the top edge is inset, the
+ map center is effectively shifted downward.
+
+ When the value of the `automaticallyAdjustsContentInsets` property is `YES`,
+ the value of this property may be overridden at any time.
+
+ Changing the value of this property updates the map view immediately. If you
+ want to animate the change, use the `-setContentInsets:animated:` method
+ instead.
+ */
+@property (nonatomic, assign) NSEdgeInsets contentInsets;
+
+/**
+ Sets the distance from the edges of the map view’s frame to the edges of the
+ map view’s logical viewport, with an optional transition animation.
+
+ When the value of this property is equal to `NSEdgeInsetsZero`, viewport
+ properties such as `centerCoordinate` assume a viewport that matches the map
+ view’s frame. Otherwise, those properties are inset, excluding part of the
+ frame from the viewport. For instance, if the only the top edge is inset, the
+ map center is effectively shifted downward.
+
+ When the value of the `automaticallyAdjustsContentInsets` property is `YES`,
+ the value of this property may be overridden at any time.
+
+ @param contentInsets The new values to inset the content by.
+ @param animated Specify `YES` if you want the map view to animate the change to
+ the content insets or `NO` if you want the map to inset the content
+ immediately.
+ */
+- (void)setContentInsets:(NSEdgeInsets)contentInsets animated:(BOOL)animated;
+
+#pragma mark Configuring How the User Interacts with the Map
+
+/**
+ A Boolean value that determines whether the user may zoom the map in and out,
+ changing the zoom level.
+
+ When this property is set to `YES`, the default, the user may zoom the map in
+ and out by pinching two fingers, by using a scroll wheel on a traditional
+ mouse, or by dragging the mouse cursor up and down while holding down the Shift
+ key. When the receiver has focus, the user may also zoom by pressing the up and
+ down arrow keys while holding down the Option key.
+
+ This property controls only user interactions with the map. If you set the
+ value of this property to `NO`, you may still change the map zoom
+ programmatically.
+ */
+@property (nonatomic, getter=isZoomEnabled) BOOL zoomEnabled;
+
+/**
+ A Boolean value that determines whether the user may scroll around the map,
+ changing the center coordinate.
+
+ When this property is set to `YES`, the default, the user may scroll the map by
+ swiping with two fingers or dragging the mouse cursor. When the receiver has
+ focus, the user may also scroll around the map by pressing the arrow keys.
+
+ This property controls only user interactions with the map. If you set the
+ value of this property to `NO`, you may still change the map location
+ programmatically.
+ */
+@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
+
+/**
+ A Boolean value that determines whether the user may rotate the map, changing
+ the direction.
+
+ When this property is set to `YES`, the default, the user may rotate the map by
+ moving two fingers in a circular motion or by dragging the mouse cursor left
+ and right while holding down the Option key. When the receiver has focus, the
+ user may also zoom by pressing the left and right arrow keys while holding down
+ the Option key.
+
+ This property controls only user interactions with the map. If you set the
+ value of this property to `NO`, you may still rotate the map programmatically.
+ */
+@property (nonatomic, getter=isRotateEnabled) BOOL rotateEnabled;
+
+/**
+ A Boolean value that determines whether the user may tilt of the map, changing
+ the pitch.
+
+ When this property is set to `YES`, the default, the user may rotate the map by
+ dragging the mouse cursor up and down while holding down the Option key.
+
+ This property controls only user interactions with the map. If you set the
+ value of this property to `NO`, you may still change the pitch of the map
+ programmatically.
+ */
+@property (nonatomic, getter=isPitchEnabled) BOOL pitchEnabled;
+
+#pragma mark Annotating the Map
+
+/**
+ The complete list of annotations associated with the receiver. (read-only)
+
+ The objects in this array must adopt the `MGLAnnotation` protocol. If no
+ annotations are associated with the map view, the value of this property is
+ `nil`.
+ */
+@property (nonatomic, readonly, nullable) NS_ARRAY_OF(id <MGLAnnotation>) *annotations;
+
+/**
+ Adds an annotation to the map view.
+
+ @note `MGLMultiPolyline`, `MGLMultiPolygon`, and `MGLShapeCollection` objects
+ cannot be added to the map view at this time. Nor can `MGLMultiPoint`
+ objects that are not instances of `MGLPolyline` or `MGLPolygon`. Any
+ multipoint, multipolyline, multipolygon, or shape collection object that is
+ specified is silently ignored.
+
+ @param annotation The annotation object to add to the receiver. This object
+ must conform to the `MGLAnnotation` protocol. The map view retains the
+ annotation object.
+ */
+- (void)addAnnotation:(id <MGLAnnotation>)annotation;
+
+/**
+ Adds an array of annotations to the map view.
+
+ @note `MGLMultiPolyline`, `MGLMultiPolygon`, and `MGLShapeCollection` objects
+ cannot be added to the map view at this time. Nor can `MGLMultiPoint`
+ objects that are not instances of `MGLPolyline` or `MGLPolygon`. Any
+ multipoint, multipolyline, multipolygon, or shape collection objects that
+ are specified are silently ignored.
+
+ @param annotations An array of annotation objects. Each object in the array
+ must conform to the `MGLAnnotation` protocol. The map view retains each
+ individual annotation object.
+ */
+- (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations;
+
+/**
+ Removes an annotation from the map view, deselecting it if it is selected.
+
+ Removing an annotation object dissociates it from the map view entirely,
+ preventing it from being displayed on the map. Thus you would typically call
+ this method only when you want to hide or delete a given annotation.
+
+ @param annotation The annotation object to remove. This object must conform to
+ the `MGLAnnotation` protocol.
+ */
+- (void)removeAnnotation:(id <MGLAnnotation>)annotation;
+
+/**
+ Removes an array of annotations from the map view, deselecting any selected
+ annotations in the array.
+
+ Removing annotation objects dissociates them from the map view entirely,
+ preventing them from being displayed on the map. Thus you would typically call
+ this method only when you want to hide or delete the given annotations.
+
+ @param annotations The array of annotation objects to remove. Objects in the
+ array must conform to the `MGLAnnotation` protocol.
+ */
+- (void)removeAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations;
+
+/**
+ Returns a reusable annotation image object associated with its identifier.
+
+ For performance reasons, you should generally reuse `MGLAnnotationImage`
+ objects for identical-looking annotations in your map views. Dequeueing saves
+ time and memory during performance-critical operations such as scrolling.
+
+ @param identifier A string identifying the annotation image to be reused. This
+ string is the same one you specify when initially returning the annotation
+ image object using the `-mapView:imageForAnnotation:` method.
+ @return An annotation image object with the given identifier, or `nil` if no
+ such object exists in the reuse queue.
+ */
+- (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier;
+
+#pragma mark Managing Annotation Selections
+
+/**
+ The currently selected annotations.
+
+ Assigning a new array to this property selects only the first annotation in the
+ array.
+ */
+@property (nonatomic, copy) NS_ARRAY_OF(id <MGLAnnotation>) *selectedAnnotations;
+
+/**
+ Selects an annotation and displays a callout popover for it.
+
+ If the given annotation is not visible within the current viewport, this method
+ has no effect.
+
+ @param annotation The annotation object to select.
+ */
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation;
+
+/**
+ Deselects an annotation and hides its callout popover.
+
+ @param annotation The annotation object to deselect.
+ */
+- (void)deselectAnnotation:(nullable id <MGLAnnotation>)annotation;
+
+/**
+ A common view controller for managing a callout popover’s content view.
+
+ Like any instance of `NSPopover`, an annotation callout manages its contents
+ with a view controller. The annotation object is the view controller’s
+ represented object. This means that you can bind controls in the view
+ controller’s content view to KVO-compliant properties of the annotation object,
+ such as `title` and `subtitle`.
+
+ This property defines a common view controller that is used for every
+ annotation’s callout view. If you set this property to `nil`, a default view
+ controller will be used that manages a simple title label and subtitle label.
+ If you need distinct view controllers for different annotations, the map view’s
+ delegate should implement `-mapView:calloutViewControllerForAnnotation:`
+ instead.
+ */
+@property (nonatomic, strong, null_resettable) IBOutlet NSViewController *calloutViewController;
+
+#pragma mark Finding Annotations
+
+/**
+ Returns a point annotation located at the given point.
+
+ @param point A point in the view’s coordinate system.
+ @return A point annotation whose annotation image coincides with the point. If
+ multiple point annotations coincide with the point, the return value is the
+ annotation that would be selected if the user clicks at this point.
+ */
+- (id <MGLAnnotation>)annotationAtPoint:(NSPoint)point;
+
+#pragma mark Overlaying the Map
+
+/**
+ Adds a single overlay to the map.
+
+ To remove an overlay from a map, use the `-removeOverlay:` method.
+
+ @param overlay The overlay object to add. This object must conform to the
+ `MGLOverlay` protocol.
+ */
+- (void)addOverlay:(id <MGLOverlay>)overlay;
+
+/**
+ Adds an array of overlays to the map.
+
+ To remove multiple overlays from a map, use the `-removeOverlays:` method.
+
+ @param overlays An array of objects, each of which must conform to the
+ `MGLOverlay` protocol.
+ */
+- (void)addOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays;
+
+/**
+ Removes a single overlay from the map.
+
+ If the specified overlay is not currently associated with the map view, this
+ method does nothing.
+
+ @param overlay The overlay object to remove.
+ */
+- (void)removeOverlay:(id <MGLOverlay>)overlay;
+
+/**
+ Removes an array of overlays from the map.
+
+ If a given overlay object is not associated with the map view, it is ignored.
+
+ @param overlays An array of objects, each of which conforms to the `MGLOverlay`
+ protocol.
+ */
+- (void)removeOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays;
+
+#pragma mark Accessing the Underlying Map Data
+
+/**
+ Returns an array of rendered map features that intersect with a given point.
+
+ This method may return features from any of the map’s style layers. To restrict
+ the search to a particular layer or layers, use the
+ `-visibleFeaturesAtPoint:inStyleLayersWithIdentifiers:` method. For more
+ information about searching for map features, see that method’s documentation.
+
+ @param point A point expressed in the map view’s coordinate system.
+ @return An array of objects conforming to the `MGLFeature` protocol that
+ represent features in the sources used by the current style.
+ */
+- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesAtPoint:(NSPoint)point NS_SWIFT_NAME(visibleFeatures(_:));
+
+/**
+ Returns an array of rendered map features that intersect with a given point,
+ restricted to the given style layers.
+
+ Each object in the returned array represents a feature rendered by the
+ current style and provides access to attributes specified by the relevant
+ <a href="https://www.mapbox.com/mapbox-gl-style-spec/#sources">tile sources</a>.
+ The returned array includes features specified in vector and GeoJSON tile
+ sources but does not include anything from raster, image, or video sources.
+
+ Only visible features are returned. For example, suppose the current style uses
+ the
+ <a href="https://www.mapbox.com/vector-tiles/mapbox-streets/">Mapbox Streets source</a>,
+ but none of the specified style layers includes features that have the `maki`
+ property set to `bus`. If you pass a point corresponding to the location of a
+ bus stop into this method, the bus stop feature does not appear in the
+ resulting array. On the other hand, if the style does include bus stops, an
+ `MGLFeature` object representing that bus stop is returned and its
+ `attributes` dictionary has the `maki` key set to `bus` (along with other
+ attributes). The dictionary contains only the attributes provided by the
+ tile source; it does not include computed attribute values or rules about how
+ the feature is rendered by the current style.
+
+ The returned array is sorted by z-order, starting with the topmost rendered
+ feature and ending with the bottommost rendered feature. A feature that is
+ rendered multiple times due to wrapping across the antimeridian at low zoom
+ levels is included only once, subject to the caveat that follows.
+
+ Features come from tiled vector data or GeoJSON data that is converted to tiles
+ internally, so feature geometries are clipped at tile boundaries and features
+ may appear duplicated across tiles. For example, suppose the specified point
+ lies along a road that spans the screen. The resulting array includes those
+ parts of the road that lie within the map tile that contain the specified
+ point, even if the road extends into other tiles.
+
+ To find out the layer names in a particular style, view the style in
+ <a href="https://www.mapbox.com/studio/">Mapbox Studio</a>.
+
+ @param point A point expressed in the map view’s coordinate system.
+ @param styleLayerIdentifiers A set of strings that correspond to the names of
+ layers defined in the current style. Only the features contained in these
+ layers are included in the returned array.
+ @return An array of objects conforming to the `MGLFeature` protocol that
+ represent features in the sources used by the current style.
+ */
+- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesAtPoint:(NSPoint)point inStyleLayersWithIdentifiers:(nullable NS_SET_OF(NSString *) *)styleLayerIdentifiers NS_SWIFT_NAME(visibleFeatures(_:styleLayerIdentifiers:));
+
+/**
+ Returns an array of rendered map features that intersect with the given
+ rectangle.
+
+ This method may return features from any of the map’s style layers. To restrict
+ the search to a particular layer or layers, use the
+ `-visibleFeaturesAtPoint:inStyleLayersWithIdentifiers:` method. For more
+ information about searching for map features, see that method’s documentation.
+
+ @param rect A rectangle expressed in the map view’s coordinate system.
+ @return An array of objects conforming to the `MGLFeature` protocol that
+ represent features in the sources used by the current style.
+ */
+- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesInRect:(NSRect)rect NS_SWIFT_NAME(visibleFeatures(_:));
+
+/**
+ Returns an array of rendered map features that intersect with the given
+ rectangle, restricted to the given style layers.
+
+ Each object in the returned array represents a feature rendered by the
+ current style and provides access to attributes specified by the relevant
+ <a href="https://www.mapbox.com/mapbox-gl-style-spec/#sources">tile sources</a>.
+ The returned array includes features specified in vector and GeoJSON tile
+ sources but does not include anything from raster, image, or video sources.
+
+ Only visible features are returned. For example, suppose the current style uses
+ the
+ <a href="https://www.mapbox.com/vector-tiles/mapbox-streets/">Mapbox Streets source</a>,
+ but none of the specified style layers includes features that have the `maki`
+ property set to `bus`. If you pass a rectangle containing the location of a bus
+ stop into this method, the bus stop feature does not appear in the resulting
+ array. On the other hand, if the style does include bus stops, an `MGLFeature`
+ object representing that bus stop is returned and its `attributes` dictionary
+ has the `maki` key set to `bus` (along with other attributes). The dictionary
+ contains only the attributes provided by the tile source; it does not include
+ computed attribute values or rules about how the feature is rendered by the
+ current style.
+
+ The returned array is sorted by z-order, starting with the topmost rendered
+ feature and ending with the bottommost rendered feature. A feature that is
+ rendered multiple times due to wrapping across the antimeridian at low zoom
+ levels is included only once, subject to the caveat that follows.
+
+ Features come from tiled vector data or GeoJSON data that is converted to tiles
+ internally, so feature geometries are clipped at tile boundaries and features
+ may appear duplicated across tiles. For example, suppose the specified
+ rectangle intersects with a road that spans the screen. The resulting array
+ includes those parts of the road that lie within the map tiles covering the
+ specified rectangle, even if the road extends into other tiles. The portion of
+ the road within each map tile is included individually.
+
+ To find out the layer names in a particular style, view the style in
+ <a href="https://www.mapbox.com/studio/">Mapbox Studio</a>.
+
+ @param rect A rectangle expressed in the map view’s coordinate system.
+ @param styleLayerIdentifiers A set of strings that correspond to the names of
+ layers defined in the current style. Only the features contained in these
+ layers are included in the returned array.
+ @return An array of objects conforming to the `MGLFeature` protocol that
+ represent features in the sources used by the current style.
+ */
+- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesInRect:(NSRect)rect inStyleLayersWithIdentifiers:(nullable NS_SET_OF(NSString *) *)styleLayerIdentifiers NS_SWIFT_NAME(visibleFeatures(_:styleLayerIdentifiers:));
+
+#pragma mark Converting Geographic Coordinates
+
+/**
+ Converts a geographic coordinate to a point in the given view’s coordinate
+ system.
+
+ @param coordinate The geographic coordinate to convert.
+ @param view The view in whose coordinate system the returned point should be
+ expressed. If this parameter is `nil`, the returned point is expressed in
+ the window’s coordinate system. If `view` is not `nil`, it must belong to
+ the same window as the map view.
+ @return The point (in the appropriate view or window coordinate system)
+ corresponding to the given geographic coordinate.
+ */
+- (NSPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(nullable NSView *)view;
+
+/**
+ Converts a point in the given view’s coordinate system to a geographic
+ coordinate.
+
+ @param point The point to convert.
+ @param view The view in whose coordinate system the point is expressed.
+ @return The geographic coordinate at the given point.
+ */
+- (CLLocationCoordinate2D)convertPoint:(NSPoint)point toCoordinateFromView:(nullable NSView *)view;
+
+/**
+ Converts a geographic bounding box to a rectangle in the given view’s
+ coordinate system.
+
+ @param bounds The geographic bounding box to convert.
+ @param view The view in whose coordinate system the returned rectangle should
+ be expressed. If this parameter is `nil`, the returned rectangle is
+ expressed in the window’s coordinate system. If `view` is not `nil`, it must
+ belong to the same window as the map view.
+ */
+- (NSRect)convertCoordinateBounds:(MGLCoordinateBounds)bounds toRectToView:(nullable NSView *)view;
+
+/**
+ Converts a rectangle in the given view’s coordinate system to a geographic
+ bounding box.
+
+ @param rect The rectangle to convert.
+ @param view The view in whose coordinate system the rectangle is expressed.
+ @return The geographic bounding box coextensive with the given rectangle.
+ */
+- (MGLCoordinateBounds)convertRect:(NSRect)rect toCoordinateBoundsFromView:(nullable NSView *)view;
+
+/**
+ Returns the distance spanned by one point in the map view’s coordinate system
+ at the given latitude and current zoom level.
+
+ The distance between points decreases as the latitude approaches the poles.
+ This relationship parallels the relationship between longitudinal coordinates
+ at different latitudes.
+
+ @param latitude The latitude of the geographic coordinate represented by the
+ point.
+ @return The distance in meters spanned by a single point.
+ */
+- (CLLocationDistance)metersPerPointAtLatitude:(CLLocationDegrees)latitude;
+
+#pragma mark Debugging the Map
+
+/**
+ The options that determine which debugging aids are shown on the map.
+
+ These options are all disabled by default and should remain disabled in
+ released software for performance and aesthetic reasons.
+ */
+@property (nonatomic) MGLMapDebugMaskOptions debugMask;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm
new file mode 100644
index 0000000000..2f985b85d8
--- /dev/null
+++ b/platform/macos/src/MGLMapView.mm
@@ -0,0 +1,2499 @@
+#import "MGLMapView_Private.h"
+#import "MGLAnnotationImage_Private.h"
+#import "MGLAttributionButton.h"
+#import "MGLCompassCell.h"
+#import "MGLOpenGLLayer.h"
+#import "MGLStyle.h"
+
+#import "MGLFeature_Private.h"
+#import "MGLGeometry_Private.h"
+#import "MGLMultiPoint_Private.h"
+#import "MGLOfflineStorage_Private.h"
+
+#import "MGLAccountManager.h"
+#import "MGLMapCamera.h"
+#import "MGLPolygon.h"
+#import "MGLPolyline.h"
+#import "MGLAnnotationImage.h"
+#import "MGLMapViewDelegate.h"
+
+#import <mbgl/mbgl.hpp>
+#import <mbgl/annotation/annotation.hpp>
+#import <mbgl/map/camera.hpp>
+#import <mbgl/platform/darwin/reachability.h>
+#import <mbgl/gl/gl.hpp>
+#import <mbgl/sprite/sprite_image.hpp>
+#import <mbgl/storage/default_file_source.hpp>
+#import <mbgl/storage/network_status.hpp>
+#import <mbgl/math/wrap.hpp>
+#import <mbgl/util/constants.hpp>
+#import <mbgl/util/chrono.hpp>
+
+#import <map>
+#import <unordered_set>
+
+#import "NSBundle+MGLAdditions.h"
+#import "NSProcessInfo+MGLAdditions.h"
+#import "NSException+MGLAdditions.h"
+#import "NSString+MGLAdditions.h"
+
+#import <QuartzCore/QuartzCore.h>
+
+class MGLMapViewImpl;
+class MGLAnnotationContext;
+
+/// Distance from the edge of the view to ornament views (logo, attribution, etc.).
+const CGFloat MGLOrnamentPadding = 12;
+
+/// Alpha value of the ornament views (logo, attribution, etc.).
+const CGFloat MGLOrnamentOpacity = 0.9;
+
+/// Default duration for programmatic animations.
+const NSTimeInterval MGLAnimationDuration = 0.3;
+
+/// Distance in points that a single press of the panning keyboard shortcut pans the map by.
+const CGFloat MGLKeyPanningIncrement = 150;
+
+/// Degrees that a single press of the rotation keyboard shortcut rotates the map by.
+const CLLocationDegrees MGLKeyRotationIncrement = 25;
+
+/// Key for the user default that, when true, causes the map view to zoom in and out on scroll wheel events.
+NSString * const MGLScrollWheelZoomsMapViewDefaultKey = @"MGLScrollWheelZoomsMapView";
+
+/// Reuse identifier and file name of the default point annotation image.
+static NSString * const MGLDefaultStyleMarkerSymbolName = @"default_marker";
+
+/// Prefix that denotes a sprite installed by MGLMapView, to avoid collisions
+/// with style-defined sprites.
+static NSString * const MGLAnnotationSpritePrefix = @"com.mapbox.sprites.";
+
+/// Slop area around the hit testing point, allowing for imprecise annotation selection.
+const CGFloat MGLAnnotationImagePaddingForHitTest = 4;
+
+/// Distance from the callout’s anchor point to the annotation it points to.
+const CGFloat MGLAnnotationImagePaddingForCallout = 4;
+
+/// Copyright notices displayed in the attribution view.
+struct MGLAttribution {
+ /// Attribution button label text. A copyright symbol is prepended to this string.
+ NSString *title;
+ /// URL to open when the attribution button is clicked.
+ NSString *urlString;
+} MGLAttributions[] = {
+ {
+ .title = NSLocalizedStringWithDefaultValue(@"COPYRIGHT_MAPBOX", nil, nil, @"Mapbox", @"Linked part of copyright notice"),
+ .urlString = NSLocalizedStringWithDefaultValue(@"COPYRIGHT_MAPBOX_LINK", nil, nil, @"https://www.mapbox.com/about/maps/", @"Copyright notice link"),
+ },
+ {
+ .title = NSLocalizedStringWithDefaultValue(@"COPYRIGHT_OSM", nil, nil, @"OpenStreetMap", @"Linked part of copyright notice"),
+ .urlString = NSLocalizedStringWithDefaultValue(@"COPYRIGHT_OSM_LINK", nil, nil, @"http://www.openstreetmap.org/about/", @"Copyright notice link"),
+ },
+};
+
+/// Unique identifier representing a single annotation in mbgl.
+typedef uint32_t MGLAnnotationTag;
+
+/// An indication that the requested annotation was not found or is nonexistent.
+enum { MGLAnnotationTagNotFound = UINT32_MAX };
+
+/// Mapping from an annotation tag to metadata about that annotation, including
+/// the annotation itself.
+typedef std::map<MGLAnnotationTag, 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];
+}
+
+/// Converts from a duration in seconds to a duration object usable in mbgl.
+mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) {
+ return std::chrono::duration_cast<mbgl::Duration>(std::chrono::duration<NSTimeInterval>(duration));
+}
+
+/// Converts a media timing function into a unit bezier object usable in mbgl.
+mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction *function) {
+ if (!function) {
+ function = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
+ }
+ float p1[2], p2[2];
+ [function getControlPointAtIndex:0 values:p1];
+ [function getControlPointAtIndex:1 values:p2];
+ return { p1[0], p1[1], p2[0], p2[1] };
+}
+
+/// 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 }};
+}
+
+/// Lightweight container for metadata about an annotation, including the annotation itself.
+class MGLAnnotationContext {
+public:
+ id <MGLAnnotation> annotation;
+ /// The annotation’s image’s reuse identifier.
+ NSString *imageReuseIdentifier;
+};
+
+@interface MGLMapView () <NSPopoverDelegate, MGLMultiPointDelegate>
+
+@property (nonatomic, readwrite) NSSegmentedControl *zoomControls;
+@property (nonatomic, readwrite) NSSlider *compass;
+@property (nonatomic, readwrite) NSImageView *logoView;
+@property (nonatomic, readwrite) NSView *attributionView;
+
+/// Mapping from reusable identifiers to annotation images.
+@property (nonatomic) NS_MUTABLE_DICTIONARY_OF(NSString *, MGLAnnotationImage *) *annotationImagesByIdentifier;
+/// Currently shown popover representing the selected annotation.
+@property (nonatomic) NSPopover *calloutForSelectedAnnotation;
+
+@property (nonatomic, readwrite, getter=isDormant) BOOL dormant;
+
+@end
+
+@implementation MGLMapView {
+ /// Cross-platform map view controller.
+ mbgl::Map *_mbglMap;
+ MGLMapViewImpl *_mbglView;
+
+ NSPanGestureRecognizer *_panGestureRecognizer;
+ NSMagnificationGestureRecognizer *_magnificationGestureRecognizer;
+ NSRotationGestureRecognizer *_rotationGestureRecognizer;
+ double _scaleAtBeginningOfGesture;
+ CLLocationDirection _directionAtBeginningOfGesture;
+ CGFloat _pitchAtBeginningOfGesture;
+ BOOL _didHideCursorDuringGesture;
+
+ MGLAnnotationContextMap _annotationContextsByAnnotationTag;
+ MGLAnnotationTag _selectedAnnotationTag;
+ MGLAnnotationTag _lastSelectedAnnotationTag;
+ /// Size of the rectangle formed by unioning the maximum slop area around every annotation image.
+ NSSize _unionedAnnotationImageSize;
+ std::vector<MGLAnnotationTag> _annotationsNearbyLastClick;
+ /// True if any annotations that have tooltips have been installed.
+ BOOL _wantsToolTipRects;
+ /// True if any annotation images that have custom cursors have been installed.
+ BOOL _wantsCursorRects;
+
+ // Cached checks for delegate method implementations that may be called from
+ // MGLMultiPointDelegate methods.
+
+ BOOL _delegateHasAlphasForShapeAnnotations;
+ BOOL _delegateHasStrokeColorsForShapeAnnotations;
+ BOOL _delegateHasFillColorsForShapeAnnotations;
+ BOOL _delegateHasLineWidthsForShapeAnnotations;
+
+ /// True if the current process is the Interface Builder designable
+ /// renderer. When drawing the designable, the map is paused, so any call to
+ /// it may hang the process.
+ BOOL _isTargetingInterfaceBuilder;
+ CLLocationDegrees _pendingLatitude;
+ CLLocationDegrees _pendingLongitude;
+
+ /// True if the view is currently printing itself.
+ BOOL _isPrinting;
+}
+
+#pragma mark Lifecycle
+
++ (void)initialize {
+ if (self == [MGLMapView class]) {
+ [[NSUserDefaults standardUserDefaults] registerDefaults:@{
+ MGLScrollWheelZoomsMapViewDefaultKey: @NO,
+ }];
+ }
+}
+
+- (instancetype)initWithFrame:(NSRect)frameRect {
+ if (self = [super initWithFrame:frameRect]) {
+ [self commonInit];
+ self.styleURL = nil;
+ }
+ return self;
+}
+
+- (instancetype)initWithFrame:(NSRect)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 @[@"camera", @"debugMask"];
+}
+
+- (void)commonInit {
+ _isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent;
+
+ // Set up cross-platform controllers and resources.
+ _mbglView = new MGLMapViewImpl(self, [NSScreen mainScreen].backingScaleFactor);
+
+ // Delete the pre-offline ambient cache at
+ // ~/Library/Caches/com.mapbox.sdk.ios/cache.db.
+ NSURL *cachesDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory
+ inDomain:NSUserDomainMask
+ appropriateForURL:nil
+ create:NO
+ error:nil];
+ cachesDirectoryURL = [cachesDirectoryURL URLByAppendingPathComponent:
+ [NSBundle mgl_frameworkBundle].bundleIdentifier];
+ NSURL *legacyCacheURL = [cachesDirectoryURL URLByAppendingPathComponent:@"cache.db"];
+ [[NSFileManager defaultManager] removeItemAtURL:legacyCacheURL error:NULL];
+
+ mbgl::DefaultFileSource *mbglFileSource = [MGLOfflineStorage sharedOfflineStorage].mbglFileSource;
+ _mbglMap = new mbgl::Map(*_mbglView, *mbglFileSource, mbgl::MapMode::Continuous, mbgl::GLContextMode::Unique, mbgl::ConstrainMode::None, mbgl::ViewportMode::Default);
+ [self validateTileCacheSize];
+
+ // Install the OpenGL layer. Interface Builder’s synchronous drawing means
+ // we can’t display a map, so don’t even bother to have a map layer.
+ self.layer = _isTargetingInterfaceBuilder ? [CALayer layer] : [MGLOpenGLLayer layer];
+
+ // Notify map object when network reachability status changes.
+ MGLReachability *reachability = [MGLReachability reachabilityForInternetConnection];
+ reachability.reachableBlock = ^(MGLReachability *) {
+ mbgl::NetworkStatus::Reachable();
+ };
+ [reachability startNotifier];
+
+ // Install ornaments and gesture recognizers.
+ [self installZoomControls];
+ [self installCompass];
+ [self installLogoView];
+ [self installAttributionView];
+ [self installGestureRecognizers];
+
+ // Set up annotation management and selection state.
+ _annotationImagesByIdentifier = [NSMutableDictionary dictionary];
+ _annotationContextsByAnnotationTag = {};
+ _selectedAnnotationTag = MGLAnnotationTagNotFound;
+ _lastSelectedAnnotationTag = MGLAnnotationTagNotFound;
+ _annotationsNearbyLastClick = {};
+
+ // Jump to Null Island initially.
+ self.automaticallyAdjustsContentInsets = YES;
+ mbgl::CameraOptions options;
+ options.center = mbgl::LatLng(0, 0);
+ options.padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
+ options.zoom = _mbglMap->getMinZoom();
+ _mbglMap->jumpTo(options);
+ _pendingLatitude = NAN;
+ _pendingLongitude = NAN;
+}
+
+/// Adds zoom controls to the lower-right corner.
+- (void)installZoomControls {
+ _zoomControls = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect];
+ _zoomControls.wantsLayer = YES;
+ _zoomControls.layer.opacity = MGLOrnamentOpacity;
+ [(NSSegmentedCell *)_zoomControls.cell setTrackingMode:NSSegmentSwitchTrackingMomentary];
+ _zoomControls.continuous = YES;
+ _zoomControls.segmentCount = 2;
+ [_zoomControls setLabel:NSLocalizedStringWithDefaultValue(@"ZOOM_OUT_LABEL", nil, nil, @"−", @"Label of Zoom Out button; U+2212 MINUS SIGN") forSegment:0];
+ [(NSSegmentedCell *)_zoomControls.cell setTag:0 forSegment:0];
+ [(NSSegmentedCell *)_zoomControls.cell setToolTip:NSLocalizedStringWithDefaultValue(@"ZOOM_OUT_TOOLTIP", nil, nil, @"Zoom Out", @"Tooltip of Zoom Out button") forSegment:0];
+ [_zoomControls setLabel:NSLocalizedStringWithDefaultValue(@"ZOOM_IN_LABEL", nil, nil, @"+", @"Label of Zoom In button") forSegment:1];
+ [(NSSegmentedCell *)_zoomControls.cell setTag:1 forSegment:1];
+ [(NSSegmentedCell *)_zoomControls.cell setToolTip:NSLocalizedStringWithDefaultValue(@"ZOOM_IN_TOOLTIP", nil, nil, @"Zoom In", @"Tooltip of Zoom In button") forSegment:1];
+ _zoomControls.target = self;
+ _zoomControls.action = @selector(zoomInOrOut:);
+ _zoomControls.controlSize = NSRegularControlSize;
+ [_zoomControls sizeToFit];
+ _zoomControls.translatesAutoresizingMaskIntoConstraints = NO;
+ [self addSubview:_zoomControls];
+}
+
+/// Adds a rudimentary compass control to the lower-right corner.
+- (void)installCompass {
+ _compass = [[NSSlider alloc] initWithFrame:NSZeroRect];
+ _compass.wantsLayer = YES;
+ _compass.layer.opacity = MGLOrnamentOpacity;
+ _compass.cell = [[MGLCompassCell alloc] init];
+ _compass.continuous = YES;
+ _compass.target = self;
+ _compass.action = @selector(rotate:);
+ [_compass sizeToFit];
+ _compass.translatesAutoresizingMaskIntoConstraints = NO;
+ [self addSubview:_compass];
+}
+
+/// Adds a Mapbox logo to the lower-left corner.
+- (void)installLogoView {
+ _logoView = [[NSImageView alloc] initWithFrame:NSZeroRect];
+ _logoView.wantsLayer = YES;
+ NSImage *logoImage = [[NSImage alloc] initWithContentsOfFile:
+ [[NSBundle mgl_frameworkBundle] pathForResource:@"mapbox" ofType:@"pdf"]];
+ // Account for the image’s built-in padding when aligning other controls to the logo.
+ logoImage.alignmentRect = NSInsetRect(logoImage.alignmentRect, 3, 3);
+ _logoView.image = logoImage;
+ _logoView.translatesAutoresizingMaskIntoConstraints = NO;
+ _logoView.accessibilityTitle = NSLocalizedStringWithDefaultValue(@"MAP_A11Y_TITLE", nil, nil, @"Mapbox", @"Accessibility title");
+ [self addSubview:_logoView];
+}
+
+/// Adds legally required map attribution to the lower-left corner.
+- (void)installAttributionView {
+ _attributionView = [[NSView alloc] initWithFrame:NSZeroRect];
+ _attributionView.wantsLayer = YES;
+
+ // Make the background and foreground translucent to be unobtrusive.
+ _attributionView.layer.opacity = 0.6;
+
+ // Blur the background to prevent text underneath the view from running into
+ // the text in the view, rendering it illegible.
+ CIFilter *attributionBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
+ [attributionBlurFilter setDefaults];
+
+ // Brighten the background. This is similar to applying a translucent white
+ // background on the view, but the effect is a bit more subtle and works
+ // well with the blur above.
+ CIFilter *attributionColorFilter = [CIFilter filterWithName:@"CIColorControls"];
+ [attributionColorFilter setDefaults];
+ [attributionColorFilter setValue:@(0.1) forKey:kCIInputBrightnessKey];
+
+ // Apply the background effects and a standard button corner radius.
+ _attributionView.backgroundFilters = @[attributionColorFilter, attributionBlurFilter];
+ _attributionView.layer.cornerRadius = 4;
+
+ _attributionView.translatesAutoresizingMaskIntoConstraints = NO;
+ [self addSubview:_attributionView];
+ [self updateAttributionView];
+}
+
+/// Adds gesture recognizers for manipulating the viewport and selecting annotations.
+- (void)installGestureRecognizers {
+ _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 *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];
+}
+
+/// Updates the attribution view to reflect the sources used. For now, this is
+/// hard-coded to the standard Mapbox and OpenStreetMap attribution.
+- (void)updateAttributionView {
+ self.attributionView.subviews = @[];
+
+ for (NSUInteger i = 0; i < sizeof(MGLAttributions) / sizeof(MGLAttributions[0]); i++) {
+ // For each attribution, add a borderless button that responds to clicks
+ // and feels like a hyperlink.
+ NSURL *url = [NSURL URLWithString:MGLAttributions[i].urlString];
+ NSButton *button = [[MGLAttributionButton alloc] initWithTitle:MGLAttributions[i].title URL:url];
+ button.controlSize = NSMiniControlSize;
+ button.translatesAutoresizingMaskIntoConstraints = NO;
+
+ // Set the new button flush with the buttom of the container and to the
+ // right of the previous button, with standard spacing. If there is no
+ // previous button, align to the container instead.
+ 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 {
+ [self.window removeObserver:self forKeyPath:@"contentLayoutRect"];
+ [self.window removeObserver:self forKeyPath:@"titlebarAppearsTransparent"];
+
+ // Close any annotation callout immediately.
+ [self.calloutForSelectedAnnotation close];
+ self.calloutForSelectedAnnotation = nil;
+
+ // Removing the annotations unregisters any outstanding KVO observers.
+ [self removeAnnotations:self.annotations];
+
+ if (_mbglMap) {
+ delete _mbglMap;
+ _mbglMap = nullptr;
+ }
+ if (_mbglView) {
+ delete _mbglView;
+ _mbglView = nullptr;
+ }
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(__unused NSDictionary *)change context:(void *)context {
+ if ([keyPath isEqualToString:@"contentLayoutRect"] ||
+ [keyPath isEqualToString:@"titlebarAppearsTransparent"]) {
+ [self adjustContentInsets];
+ } else if ([keyPath isEqualToString:@"coordinate"] &&
+ [object conformsToProtocol:@protocol(MGLAnnotation)] &&
+ ![object isKindOfClass:[MGLMultiPoint class]]) {
+ id <MGLAnnotation> annotation = object;
+ MGLAnnotationTag annotationTag = (MGLAnnotationTag)(NSUInteger)context;
+ // We can get here because a subclass registered itself as an observer
+ // of the coordinate key path of a non-multipoint annotation but failed
+ // to handle the change. This check deters us from treating the
+ // subclass’s context as an annotation tag. If the context happens to
+ // match a valid annotation tag, the annotation will be unnecessarily
+ // but safely updated.
+ if (annotation == [self annotationWithTag:annotationTag]) {
+ const mbgl::Point<double> point = MGLPointFromLocationCoordinate2D(annotation.coordinate);
+ MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
+ _mbglMap->updateAnnotation(annotationTag, mbgl::SymbolAnnotation { point, annotationImage.styleIconIdentifier.UTF8String ?: "" });
+ if (annotationTag == _selectedAnnotationTag) {
+ [self deselectAnnotation:annotation];
+ }
+ }
+ }
+}
+
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+ return [key isEqualToString:@"annotations"] ? YES : [super automaticallyNotifiesObserversForKey:key];
+}
+
+- (void)setDelegate:(id<MGLMapViewDelegate>)delegate {
+ _delegate = delegate;
+
+ // Cache checks for delegate method implementations that may be called in a
+ // hot loop, namely the annotation style methods.
+ _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 clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+ if ([self.delegate respondsToSelector:@selector(mapView:regionWillChangeAnimated:)]) {
+ NSLog(@"-mapView:regionWillChangeAnimated: is not supported by the macOS SDK, but %@ implements it anyways. "
+ @"Please implement -[%@ mapView:cameraWillChangeAnimated:] instead.",
+ NSStringFromClass([delegate class]), NSStringFromClass([delegate class]));
+ }
+ if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)]) {
+ NSLog(@"-mapViewRegionIsChanging: is not supported by the macOS SDK, but %@ implements it anyways. "
+ @"Please implement -[%@ mapViewCameraIsChanging:] instead.",
+ NSStringFromClass([delegate class]), NSStringFromClass([delegate class]));
+ }
+ if ([self.delegate respondsToSelector:@selector(mapView:regionDidChangeAnimated:)]) {
+ NSLog(@"-mapView:regionDidChangeAnimated: is not supported by the macOS SDK, but %@ implements it anyways. "
+ @"Please implement -[%@ mapView:cameraDidChangeAnimated:] instead.",
+ NSStringFromClass([delegate class]), NSStringFromClass([delegate class]));
+ }
+#pragma clang diagnostic pop
+}
+
+#pragma mark Style
+
+- (nonnull NSURL *)styleURL {
+ NSString *styleURLString = @(_mbglMap->getStyleURL().c_str()).mgl_stringOrNilIfEmpty;
+ return styleURLString ? [NSURL URLWithString:styleURLString] : [MGLStyle streetsStyleURLWithVersion:MGLStyleDefaultVersion];
+}
+
+- (void)setStyleURL:(nullable NSURL *)styleURL {
+ if (_isTargetingInterfaceBuilder) {
+ return;
+ }
+
+ // Default to Streets.
+ if (!styleURL) {
+ // An access token is required to load any default style, including
+ // Streets.
+ if (![MGLAccountManager accessToken]) {
+ return;
+ }
+ styleURL = [MGLStyle streetsStyleURLWithVersion:MGLStyleDefaultVersion];
+ }
+
+ 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];
+ if (!self.dormant && !newWindow) {
+ self.dormant = YES;
+ }
+
+ [self.window removeObserver:self forKeyPath:@"contentLayoutRect"];
+ [self.window removeObserver:self forKeyPath:@"titlebarAppearsTransparent"];
+}
+
+- (void)viewDidMoveToWindow {
+ NSWindow *window = self.window;
+ if (self.dormant && window) {
+ self.dormant = NO;
+ }
+
+ if (window && _mbglMap->getConstrainMode() == mbgl::ConstrainMode::None) {
+ _mbglMap->setConstrainMode(mbgl::ConstrainMode::HeightOnly);
+ }
+
+ [window addObserver:self
+ forKeyPath:@"contentLayoutRect"
+ options:NSKeyValueObservingOptionInitial
+ context:NULL];
+ [window addObserver:self
+ forKeyPath:@"titlebarAppearsTransparent"
+ options:NSKeyValueObservingOptionInitial
+ context:NULL];
+}
+
+- (BOOL)wantsLayer {
+ return YES;
+}
+
+- (BOOL)wantsBestResolutionOpenGLSurface {
+ // Use an OpenGL layer, except when drawing the designable, which is just
+ // ordinary Cocoa.
+ return !_isTargetingInterfaceBuilder;
+}
+
+- (void)setFrame:(NSRect)frame {
+ super.frame = frame;
+ if (!NSEqualRects(frame, self.frame)) {
+ [self validateTileCacheSize];
+ }
+ if (!_isTargetingInterfaceBuilder) {
+ _mbglMap->update(mbgl::Update::Dimensions);
+ }
+}
+
+- (void)updateConstraints {
+ // Place the zoom controls at the lower-right corner of the view.
+ [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]];
+
+ // Center the compass above the zoom controls, assuming that the compass is
+ // narrower than the zoom controls.
+ [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]];
+
+ // Place the logo view in the lower-left corner of the view, accounting for
+ // the logo’s alignment rect.
+ [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]];
+
+ // Place the attribution view to the right of the logo view and size it to
+ // fit the buttons inside.
+ [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) {
+ // Enable vertex buffer objects.
+ 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);
+ });
+
+ _mbglMap->render();
+
+ if (_isPrinting) {
+ _isPrinting = NO;
+ std::string png = encodePNG(_mbglView->readStillImage());
+ NSData *data = [[NSData alloc] initWithBytes:png.data() length:png.size()];
+ NSImage *image = [[NSImage alloc] initWithData:data];
+ [self performSelector:@selector(printWithImage:) withObject:image afterDelay:0];
+ }
+
+// [self updateUserLocationAnnotationView];
+ }
+}
+
+- (void)validateTileCacheSize {
+ if (!_mbglMap) {
+ return;
+ }
+
+ CGFloat zoomFactor = self.maximumZoomLevel - self.minimumZoomLevel + 1;
+ CGFloat cpuFactor = [NSProcessInfo processInfo].processorCount;
+ CGFloat memoryFactor = (CGFloat)[NSProcessInfo processInfo].physicalMemory / 1000 / 1000 / 1000;
+ CGFloat sizeFactor = (NSWidth(self.bounds) / mbgl::util::tileSize) * (NSHeight(self.bounds) / mbgl::util::tileSize);
+
+ NSUInteger cacheSize = zoomFactor * cpuFactor * memoryFactor * sizeFactor * 0.5;
+
+ _mbglMap->setSourceTileCacheSize(cacheSize);
+}
+
+- (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:cameraWillChangeAnimated:)]) {
+ BOOL animated = change == mbgl::MapChangeRegionWillChangeAnimated;
+ [self.delegate mapView:self cameraWillChangeAnimated:animated];
+ }
+ break;
+ }
+ case mbgl::MapChangeRegionIsChanging:
+ {
+ // Update a minimum of UI that needs to stay attached to the map
+ // while animating.
+ [self updateCompass];
+ [self updateAnnotationCallouts];
+
+ if ([self.delegate respondsToSelector:@selector(mapViewCameraIsChanging:)]) {
+ [self.delegate mapViewCameraIsChanging:self];
+ }
+ break;
+ }
+ case mbgl::MapChangeRegionDidChange:
+ case mbgl::MapChangeRegionDidChangeAnimated:
+ {
+ // Update all UI at the end of an animation or atomic change to the
+ // viewport. More expensive updates can happen here, but care should
+ // still be taken to minimize the work done here because scroll
+ // gesture recognition and momentum scrolling is performed as a
+ // series of atomic changes, not an animation.
+ [self updateZoomControls];
+ [self updateCompass];
+ [self updateAnnotationCallouts];
+ [self updateAnnotationTrackingAreas];
+
+ if ([self.delegate respondsToSelector:@selector(mapView:cameraDidChangeAnimated:)]) {
+ BOOL animated = change == mbgl::MapChangeRegionDidChangeAnimated;
+ [self.delegate mapView:self cameraDidChangeAnimated: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 Printing
+
+- (void)print:(__unused id)sender {
+ _isPrinting = YES;
+ [self invalidate];
+}
+
+- (void)printWithImage:(NSImage *)image {
+ NSImageView *imageView = [[NSImageView alloc] initWithFrame:self.bounds];
+ imageView.image = image;
+
+ NSPrintOperation *op = [NSPrintOperation printOperationWithView:imageView];
+ [op runOperation];
+}
+
+#pragma mark Viewport
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate {
+ return [NSSet setWithObjects:@"latitude", @"longitude", @"camera", nil];
+}
+
+- (CLLocationCoordinate2D)centerCoordinate {
+ mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
+ return MGLLocationCoordinate2DFromLatLng(_mbglMap->getLatLng(padding));
+}
+
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate {
+ [self setCenterCoordinate:centerCoordinate animated:NO];
+}
+
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate animated:(BOOL)animated {
+ [self willChangeValueForKey:@"centerCoordinate"];
+ _mbglMap->setLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate),
+ MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets),
+ 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"];
+}
+
+- (CLLocationDegrees)pendingLatitude {
+ return _pendingLatitude;
+}
+
+- (void)setPendingLatitude:(CLLocationDegrees)pendingLatitude {
+ _pendingLatitude = pendingLatitude;
+}
+
+- (CLLocationDegrees)pendingLongitude {
+ return _pendingLongitude;
+}
+
+- (void)setPendingLongitude:(CLLocationDegrees)pendingLongitude {
+ _pendingLongitude = pendingLongitude;
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomLevel {
+ return [NSSet setWithObject:@"camera"];
+}
+
+- (double)zoomLevel {
+ return _mbglMap->getZoom();
+}
+
+- (void)setZoomLevel:(double)zoomLevel {
+ [self setZoomLevel:zoomLevel animated:NO];
+}
+
+- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated {
+ [self willChangeValueForKey:@"zoomLevel"];
+ _mbglMap->setZoom(zoomLevel,
+ MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets),
+ MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
+ [self didChangeValueForKey:@"zoomLevel"];
+}
+
+- (void)zoomBy:(double)zoomDelta animated:(BOOL)animated {
+ [self setZoomLevel:self.zoomLevel + zoomDelta animated:animated];
+}
+
+- (void)scaleBy:(double)scaleFactor atPoint:(NSPoint)point animated:(BOOL)animated {
+ [self willChangeValueForKey:@"centerCoordinate"];
+ [self willChangeValueForKey:@"zoomLevel"];
+ mbgl::ScreenCoordinate center(point.x, self.bounds.size.height - point.y);
+ _mbglMap->scaleBy(scaleFactor, center, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
+ [self didChangeValueForKey:@"zoomLevel"];
+ [self didChangeValueForKey:@"centerCoordinate"];
+}
+
+- (void)setMinimumZoomLevel:(double)minimumZoomLevel
+{
+ _mbglMap->setMinZoom(minimumZoomLevel);
+ [self validateTileCacheSize];
+}
+
+- (void)setMaximumZoomLevel:(double)maximumZoomLevel
+{
+ _mbglMap->setMaxZoom(maximumZoomLevel);
+ [self validateTileCacheSize];
+}
+
+- (double)maximumZoomLevel {
+ return _mbglMap->getMaxZoom();
+}
+
+- (double)minimumZoomLevel {
+ return _mbglMap->getMinZoom();
+}
+
+/// Respond to a click on the zoom control.
+- (IBAction)zoomInOrOut:(NSSegmentedControl *)sender {
+ switch (sender.selectedSegment) {
+ case 0:
+ // Zoom out.
+ [self moveToEndOfParagraph:sender];
+ break;
+ case 1:
+ // Zoom in.
+ [self moveToBeginningOfParagraph:sender];
+ break;
+ default:
+ break;
+ }
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingDirection {
+ return [NSSet setWithObject:@"camera"];
+}
+
+- (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,
+ MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets),
+ MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
+ [self didChangeValueForKey:@"direction"];
+}
+
+- (void)offsetDirectionBy:(CLLocationDegrees)delta animated:(BOOL)animated {
+ [self setDirection:_mbglMap->getBearing() + delta animated:animated];
+}
+
++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCamera {
+ return [NSSet setWithObjects:@"latitude", @"longitude", @"centerCoordinate", @"zoomLevel", @"direction", nil];
+}
+
+- (MGLMapCamera *)camera {
+ mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
+ return [self cameraForCameraOptions:_mbglMap->getCameraOptions(padding)];
+}
+
+- (void)setCamera:(MGLMapCamera *)camera {
+ [self setCamera:camera animated:NO];
+}
+
+- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated {
+ [self setCamera:camera withDuration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:NULL];
+}
+
+- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion {
+ _mbglMap->cancelTransitions();
+ if ([self.camera isEqual:camera]) {
+ return;
+ }
+
+ mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera];
+ mbgl::AnimationOptions animationOptions;
+ if (duration > 0) {
+ animationOptions.duration = MGLDurationInSeconds(duration);
+ animationOptions.easing = MGLUnitBezierForMediaTimingFunction(function);
+ }
+ if (completion) {
+ animationOptions.transitionFinishFn = [completion]() {
+ // Must run asynchronously after the transition is completely over.
+ // Otherwise, a call to -setCamera: within the completion handler
+ // would reenter the completion handler’s caller.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion();
+ });
+ };
+ }
+
+ [self willChangeValueForKey:@"camera"];
+ _mbglMap->easeTo(cameraOptions, animationOptions);
+ [self didChangeValueForKey:@"camera"];
+}
+
+- (void)flyToCamera:(MGLMapCamera *)camera completionHandler:(nullable void (^)(void))completion {
+ [self flyToCamera:camera withDuration:-1 completionHandler:completion];
+}
+
+- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion {
+ [self flyToCamera:camera withDuration:duration peakAltitude:-1 completionHandler:completion];
+}
+
+- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion {
+ _mbglMap->cancelTransitions();
+ if ([self.camera isEqual:camera]) {
+ return;
+ }
+
+ mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera];
+ mbgl::AnimationOptions animationOptions;
+ if (duration >= 0) {
+ animationOptions.duration = MGLDurationInSeconds(duration);
+ }
+ if (peakAltitude >= 0) {
+ CLLocationDegrees peakLatitude = (self.centerCoordinate.latitude + camera.centerCoordinate.latitude) / 2;
+ CLLocationDegrees peakPitch = (self.camera.pitch + camera.pitch) / 2;
+ animationOptions.minZoom = MGLZoomLevelForAltitude(peakAltitude, peakPitch,
+ peakLatitude, self.frame.size);
+ }
+ if (completion) {
+ animationOptions.transitionFinishFn = [completion]() {
+ // Must run asynchronously after the transition is completely over.
+ // Otherwise, a call to -setCamera: within the completion handler
+ // would reenter the completion handler’s caller.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion();
+ });
+ };
+ }
+
+ [self willChangeValueForKey:@"camera"];
+ _mbglMap->flyTo(cameraOptions, animationOptions);
+ [self didChangeValueForKey:@"camera"];
+}
+
+/// Returns a CameraOptions object that specifies parameters for animating to
+/// the given camera.
+- (mbgl::CameraOptions)cameraOptionsObjectForAnimatingToCamera:(MGLMapCamera *)camera {
+ mbgl::CameraOptions options;
+ options.center = MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate);
+ options.padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
+ options.zoom = MGLZoomLevelForAltitude(camera.altitude, camera.pitch,
+ camera.centerCoordinate.latitude,
+ self.frame.size);
+ if (camera.heading >= 0) {
+ options.angle = MGLRadiansFromDegrees(-camera.heading);
+ }
+ if (camera.pitch >= 0) {
+ options.pitch = MGLRadiansFromDegrees(camera.pitch);
+ }
+ return options;
+}
+
++ (NSSet *)keyPathsForValuesAffectingVisibleCoordinateBounds {
+ return [NSSet setWithObjects:@"centerCoordinate", @"zoomLevel", @"direction", @"bounds", nil];
+}
+
+- (MGLCoordinateBounds)visibleCoordinateBounds {
+ return [self convertRect:self.bounds toCoordinateBoundsFromView:self];
+}
+
+- (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 padding = MGLEdgeInsetsFromNSEdgeInsets(insets);
+ padding += MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
+ mbgl::CameraOptions cameraOptions = _mbglMap->cameraForLatLngBounds(MGLLatLngBoundsFromCoordinateBounds(bounds), padding);
+ mbgl::AnimationOptions animationOptions;
+ if (animated) {
+ animationOptions.duration = MGLDurationInSeconds(MGLAnimationDuration);
+ }
+
+ [self willChangeValueForKey:@"visibleCoordinateBounds"];
+ animationOptions.transitionFinishFn = ^() {
+ [self didChangeValueForKey:@"visibleCoordinateBounds"];
+ };
+ _mbglMap->easeTo(cameraOptions, animationOptions);
+}
+
+- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds {
+ return [self cameraThatFitsCoordinateBounds:bounds edgePadding:NSEdgeInsetsZero];
+}
+
+- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets {
+ mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(insets);
+ padding += MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
+ mbgl::CameraOptions cameraOptions = _mbglMap->cameraForLatLngBounds(MGLLatLngBoundsFromCoordinateBounds(bounds), padding);
+ return [self cameraForCameraOptions:cameraOptions];
+}
+
+- (MGLMapCamera *)cameraForCameraOptions:(const mbgl::CameraOptions &)cameraOptions {
+ CLLocationCoordinate2D centerCoordinate = MGLLocationCoordinate2DFromLatLng(cameraOptions.center ? *cameraOptions.center : _mbglMap->getLatLng());
+ double zoomLevel = cameraOptions.zoom ? *cameraOptions.zoom : self.zoomLevel;
+ CLLocationDirection direction = cameraOptions.angle ? -MGLDegreesFromRadians(*cameraOptions.angle) : self.direction;
+ CGFloat pitch = cameraOptions.pitch ? MGLDegreesFromRadians(*cameraOptions.pitch) : _mbglMap->getPitch();
+ CLLocationDistance altitude = MGLAltitudeForZoomLevel(zoomLevel, pitch,
+ centerCoordinate.latitude,
+ self.frame.size);
+ return [MGLMapCamera cameraLookingAtCenterCoordinate:centerCoordinate
+ fromDistance:altitude
+ pitch:pitch
+ heading:direction];
+}
+
+- (void)setAutomaticallyAdjustsContentInsets:(BOOL)automaticallyAdjustsContentInsets {
+ _automaticallyAdjustsContentInsets = automaticallyAdjustsContentInsets;
+ [self adjustContentInsets];
+}
+
+/// Updates `contentInsets` to reflect the current window geometry.
+- (void)adjustContentInsets {
+ if (!_automaticallyAdjustsContentInsets) {
+ return;
+ }
+
+ NSEdgeInsets contentInsets = self.contentInsets;
+ if ((self.window.styleMask & NSFullSizeContentViewWindowMask)
+ && !self.window.titlebarAppearsTransparent) {
+ NSRect contentLayoutRect = [self convertRect:self.window.contentLayoutRect fromView:nil];
+ if (NSMaxX(contentLayoutRect) > 0 && NSMaxY(contentLayoutRect) > 0) {
+ contentInsets = NSEdgeInsetsMake(NSHeight(self.bounds) - NSMaxY(contentLayoutRect),
+ NSMinX(contentLayoutRect),
+ NSMinY(contentLayoutRect),
+ NSWidth(self.bounds) - NSMaxX(contentLayoutRect));
+ }
+ } else {
+ contentInsets = NSEdgeInsetsZero;
+ }
+
+ self.contentInsets = contentInsets;
+}
+
+- (void)setContentInsets:(NSEdgeInsets)contentInsets {
+ [self setContentInsets:contentInsets animated:NO];
+}
+
+- (void)setContentInsets:(NSEdgeInsets)contentInsets animated:(BOOL)animated {
+ if (NSEdgeInsetsEqual(contentInsets, self.contentInsets)) {
+ return;
+ }
+
+ // After adjusting the content insets, move the center coordinate from the
+ // old frame of reference to the new one represented by the newly set
+ // content insets.
+ CLLocationCoordinate2D oldCenter = self.centerCoordinate;
+ _contentInsets = contentInsets;
+ [self setCenterCoordinate:oldCenter animated:animated];
+}
+
+#pragma mark Mouse events and gestures
+
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+/// Drag to pan, plus drag to zoom, rotate, and tilt when a modifier key is held
+/// down.
+- (void)handlePanGesture:(NSPanGestureRecognizer *)gestureRecognizer {
+ NSPoint delta = [gestureRecognizer translationInView:self];
+ NSPoint endPoint = [gestureRecognizer locationInView:self];
+ NSPoint startPoint = NSMakePoint(endPoint.x - delta.x, endPoint.y - delta.y);
+
+ NSEventModifierFlags flags = [NSApp currentEvent].modifierFlags;
+ if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
+ [self.window invalidateCursorRectsForView:self];
+ _mbglMap->setGestureInProgress(true);
+
+ if (![self isPanningWithGesture]) {
+ // Hide the cursor except when panning.
+ CGDisplayHideCursor(kCGDirectMainDisplay);
+ _didHideCursorDuringGesture = YES;
+ }
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
+ || gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
+ _mbglMap->setGestureInProgress(false);
+ [self.window invalidateCursorRectsForView:self];
+
+ if (_didHideCursorDuringGesture) {
+ _didHideCursorDuringGesture = NO;
+ // Move the cursor back to the start point and show it again, creating
+ // the illusion that it has stayed in place during the entire gesture.
+ CGPoint cursorPoint = [self convertPoint:startPoint toView:nil];
+ cursorPoint = [self.window convertRectToScreen:{ startPoint, NSZeroSize }].origin;
+ cursorPoint.y = [NSScreen mainScreen].frame.size.height - cursorPoint.y;
+ CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cursorPoint);
+ CGDisplayShowCursor(kCGDirectMainDisplay);
+ }
+ }
+
+ if (flags & NSShiftKeyMask) {
+ // Shift-drag to zoom.
+ if (!self.zoomEnabled) {
+ return;
+ }
+
+ _mbglMap->cancelTransitions();
+
+ if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
+ _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 (flags & NSAlternateKeyMask) {
+ // Option-drag to rotate and/or tilt.
+ _mbglMap->cancelTransitions();
+
+ if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
+ _directionAtBeginningOfGesture = self.direction;
+ _pitchAtBeginningOfGesture = _mbglMap->getPitch();
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
+ mbgl::ScreenCoordinate center(startPoint.x, self.bounds.size.height - 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, center);
+ }
+ }
+ } else if (self.scrollEnabled) {
+ // Otherwise, drag to pan.
+ _mbglMap->cancelTransitions();
+
+ if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
+ delta.y *= -1;
+ [self offsetCenterCoordinateBy:delta animated:NO];
+ [gestureRecognizer setTranslation:NSZeroPoint inView:nil];
+ }
+ }
+}
+
+/// Returns whether the user is panning using a gesture.
+- (BOOL)isPanningWithGesture {
+ NSGestureRecognizerState state = _panGestureRecognizer.state;
+ NSEventModifierFlags flags = [NSApp currentEvent].modifierFlags;
+ return ((state == NSGestureRecognizerStateBegan || state == NSGestureRecognizerStateChanged)
+ && !(flags & NSShiftKeyMask || flags & NSAlternateKeyMask));
+}
+
+/// Pinch to zoom.
+- (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::ScreenCoordinate 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);
+ }
+}
+
+/// Click or tap to select an annotation.
+- (void)handleClickGesture:(NSClickGestureRecognizer *)gestureRecognizer {
+ if (gestureRecognizer.state != NSGestureRecognizerStateEnded
+ || [self subviewContainingGesture:gestureRecognizer]) {
+ return;
+ }
+
+ NSPoint gesturePoint = [gestureRecognizer locationInView:self];
+ MGLAnnotationTag hitAnnotationTag = [self annotationTagAtPoint:gesturePoint persistingResults:YES];
+ if (hitAnnotationTag != MGLAnnotationTagNotFound) {
+ if (hitAnnotationTag != _selectedAnnotationTag) {
+ id <MGLAnnotation> annotation = [self annotationWithTag:hitAnnotationTag];
+ NSAssert(annotation, @"Cannot select nonexistent annotation with tag %u", hitAnnotationTag);
+ [self selectAnnotation:annotation];
+ }
+ } else {
+ [self deselectAnnotation:self.selectedAnnotation];
+ }
+}
+
+/// Double-click or double-tap to zoom in.
+- (void)handleDoubleClickGesture:(NSClickGestureRecognizer *)gestureRecognizer {
+ if (!self.zoomEnabled || gestureRecognizer.state != NSGestureRecognizerStateEnded
+ || [self subviewContainingGesture:gestureRecognizer]) {
+ return;
+ }
+
+ _mbglMap->cancelTransitions();
+
+ NSPoint gesturePoint = [gestureRecognizer locationInView:self];
+ [self scaleBy:2 atPoint:gesturePoint animated:YES];
+}
+
+- (void)smartMagnifyWithEvent:(NSEvent *)event {
+ if (!self.zoomEnabled) {
+ return;
+ }
+
+ _mbglMap->cancelTransitions();
+
+ // Tap with two fingers (“right-click”) to zoom out on mice but not trackpads.
+ NSPoint gesturePoint = [self convertPoint:event.locationInWindow fromView:nil];
+ [self scaleBy:0.5 atPoint:gesturePoint animated:YES];
+}
+
+/// Rotate fingers to rotate.
+- (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::ScreenCoordinate center(rotationPoint.x, self.bounds.size.height - rotationPoint.y);
+ _mbglMap->setBearing(_directionAtBeginningOfGesture + gestureRecognizer.rotationInDegrees, center);
+ } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
+ || gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
+ _mbglMap->setGestureInProgress(false);
+ }
+}
+
+- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(__unused NSEventGestureAxis)axis {
+ // Track both horizontal and vertical swipes in -scrollWheel:.
+ return YES;
+}
+
+- (void)scrollWheel:(NSEvent *)event {
+ // https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKitOlderNotes/#10_7Dragging
+ BOOL isScrollWheel = event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone && !event.hasPreciseScrollingDeltas;
+ if (isScrollWheel || [[NSUserDefaults standardUserDefaults] boolForKey:MGLScrollWheelZoomsMapViewDefaultKey]) {
+ // A traditional, vertical scroll wheel zooms instead of panning.
+ if (self.zoomEnabled) {
+ const double delta =
+ event.scrollingDeltaY / ([event hasPreciseScrollingDeltas] ? 100 : 10);
+ if (delta != 0) {
+ double scale = 2.0 / (1.0 + std::exp(-std::abs(delta)));
+
+ // Zooming out.
+ if (delta < 0) {
+ scale = 1.0 / scale;
+ }
+
+ NSPoint gesturePoint = [self convertPoint:event.locationInWindow fromView:nil];
+ [self scaleBy:scale atPoint:gesturePoint animated:NO];
+ }
+ }
+ } else if (self.scrollEnabled
+ && _magnificationGestureRecognizer.state == NSGestureRecognizerStatePossible
+ && _rotationGestureRecognizer.state == NSGestureRecognizerStatePossible) {
+ // Scroll to pan.
+ _mbglMap->cancelTransitions();
+
+ CGFloat x = event.scrollingDeltaX;
+ CGFloat y = event.scrollingDeltaY;
+ if (x || y) {
+ [self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO];
+ }
+
+ // Drift pan.
+ if (event.momentumPhase != NSEventPhaseNone) {
+ [self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO];
+ }
+ }
+}
+
+/// Returns the subview that the gesture is located in.
+- (NSView *)subviewContainingGesture:(NSGestureRecognizer *)gestureRecognizer {
+ if (NSPointInRect([gestureRecognizer locationInView:self.compass], self.compass.bounds)) {
+ return self.compass;
+ }
+ if (NSPointInRect([gestureRecognizer locationInView:self.zoomControls], self.zoomControls.bounds)) {
+ return self.zoomControls;
+ }
+ if (NSPointInRect([gestureRecognizer locationInView:self.attributionView], self.attributionView.bounds)) {
+ return self.attributionView;
+ }
+ return nil;
+}
+
+#pragma mark Keyboard events
+
+- (void)keyDown:(NSEvent *)event {
+ if (event.modifierFlags & NSNumericPadKeyMask) {
+ // This is the recommended way to handle arrow key presses, causing
+ // methods like -moveUp: and -moveToBeginningOfParagraph: to be called
+ // for various standard keybindings.
+ [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 zoomBy:1 animated:YES];
+ }
+}
+
+- (IBAction)moveToEndOfParagraph:(__unused id)sender {
+ if (self.zoomEnabled) {
+ [self zoomBy:-1 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
+
+/// Updates the zoom controls’ enabled state based on the current zoom level.
+- (void)updateZoomControls {
+ [_zoomControls setEnabled:self.zoomLevel > self.minimumZoomLevel forSegment:0];
+ [_zoomControls setEnabled:self.zoomLevel < self.maximumZoomLevel forSegment:1];
+}
+
+/// Updates the compass to point in the same direction as the map.
+- (void)updateCompass {
+ // The circular slider control goes counterclockwise, whereas our map
+ // measures its direction clockwise.
+ _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 (_annotationContextsByAnnotationTag.empty()) {
+ return nil;
+ }
+
+ // Map all the annotation tags to the annotations themselves.
+ std::vector<id <MGLAnnotation>> annotations;
+ std::transform(_annotationContextsByAnnotationTag.begin(),
+ _annotationContextsByAnnotationTag.end(),
+ std::back_inserter(annotations),
+ ^ id <MGLAnnotation> (const std::pair<MGLAnnotationTag, MGLAnnotationContext> &pair) {
+ return pair.second.annotation;
+ });
+ return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()];
+}
+
+/// Returns the annotation assigned the given tag. Cheap.
+- (id <MGLAnnotation>)annotationWithTag:(MGLAnnotationTag)tag {
+ if (!_annotationContextsByAnnotationTag.count(tag)) {
+ return nil;
+ }
+
+ MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag[tag];
+ return annotationContext.annotation;
+}
+
+/// Returns the annotation tag assigned to the given annotation. Relatively expensive.
+- (MGLAnnotationTag)annotationTagForAnnotation:(id <MGLAnnotation>)annotation {
+ if (!annotation) {
+ return MGLAnnotationTagNotFound;
+ }
+
+ for (auto &pair : _annotationContextsByAnnotationTag) {
+ if (pair.second.annotation == annotation) {
+ return pair.first;
+ }
+ }
+ return MGLAnnotationTagNotFound;
+}
+
+- (void)addAnnotation:(id <MGLAnnotation>)annotation {
+ if (annotation) {
+ [self addAnnotations:@[annotation]];
+ }
+}
+
+- (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations {
+ if (!annotations) {
+ return;
+ }
+
+ [self willChangeValueForKey:@"annotations"];
+
+ BOOL delegateHasImagesForAnnotations = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)];
+
+ for (id <MGLAnnotation> annotation in annotations) {
+ NSAssert([annotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation");
+
+ if ([annotation isKindOfClass:[MGLMultiPoint class]]) {
+ // Actual multipoints aren’t supported as annotations.
+ if ([annotation isMemberOfClass:[MGLMultiPoint class]]
+ || [annotation isMemberOfClass:[MGLMultiPointFeature class]]) {
+ continue;
+ }
+
+ // The multipoint knows how to style itself (with the map view’s help).
+ MGLMultiPoint *multiPoint = (MGLMultiPoint *)annotation;
+ if (!multiPoint.pointCount) {
+ continue;
+ }
+
+ MGLAnnotationTag annotationTag = _mbglMap->addAnnotation([multiPoint annotationObjectWithDelegate:self]);
+ MGLAnnotationContext context;
+ context.annotation = annotation;
+ _annotationContextsByAnnotationTag[annotationTag] = context;
+ } else if (![annotation isKindOfClass:[MGLMultiPolyline class]]
+ || ![annotation isKindOfClass:[MGLMultiPolygon class]]
+ || ![annotation isKindOfClass:[MGLShapeCollection class]]) {
+ MGLAnnotationImage *annotationImage = nil;
+ if (delegateHasImagesForAnnotations) {
+ annotationImage = [self.delegate mapView:self imageForAnnotation:annotation];
+ }
+ if (!annotationImage) {
+ annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName];
+ }
+ if (!annotationImage) {
+ annotationImage = self.defaultAnnotationImage;
+ }
+
+ NSString *symbolName = annotationImage.styleIconIdentifier;
+ if (!symbolName) {
+ symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
+ annotationImage.styleIconIdentifier = symbolName;
+ }
+
+ if (!self.annotationImagesByIdentifier[annotationImage.reuseIdentifier]) {
+ self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage;
+ [self installAnnotationImage:annotationImage];
+ }
+
+ MGLAnnotationTag annotationTag = _mbglMap->addAnnotation(mbgl::SymbolAnnotation {
+ MGLPointFromLocationCoordinate2D(annotation.coordinate),
+ symbolName.UTF8String ?: ""
+ });
+
+ MGLAnnotationContext context;
+ context.annotation = annotation;
+ context.imageReuseIdentifier = annotationImage.reuseIdentifier;
+ _annotationContextsByAnnotationTag[annotationTag] = context;
+
+ if ([annotation isKindOfClass:[NSObject class]]) {
+ NSAssert(![annotation isKindOfClass:[MGLMultiPoint class]], @"Point annotation should not be MGLMultiPoint.");
+ [(NSObject *)annotation addObserver:self forKeyPath:@"coordinate" options:0 context:(void *)(NSUInteger)annotationTag];
+ }
+
+ // Opt into potentially expensive tooltip tracking areas.
+ if (annotation.toolTip.length) {
+ _wantsToolTipRects = YES;
+ }
+ }
+ }
+
+ [self didChangeValueForKey:@"annotations"];
+
+ [self updateAnnotationTrackingAreas];
+}
+
+/// Initializes and returns a default annotation image that depicts a round pin
+/// rising from the center, with a shadow slightly below center. The alignment
+/// rect therefore excludes the bottom half.
+- (MGLAnnotationImage *)defaultAnnotationImage {
+ NSImage *image = MGLDefaultMarkerImage();
+ NSRect alignmentRect = image.alignmentRect;
+ alignmentRect.origin.y = NSMidY(alignmentRect);
+ alignmentRect.size.height /= 2;
+ image.alignmentRect = alignmentRect;
+ return [MGLAnnotationImage annotationImageWithImage:image
+ reuseIdentifier:MGLDefaultStyleMarkerSymbolName];
+}
+
+/// Sends the raw pixel data of the annotation image’s image to mbgl and
+/// calculates state needed for hit testing later.
+- (void)installAnnotationImage:(MGLAnnotationImage *)annotationImage {
+ NSString *iconIdentifier = annotationImage.styleIconIdentifier;
+ self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage;
+
+ NSImage *image = annotationImage.image;
+ NSSize size = image.size;
+ if (size.width == 0 || size.height == 0 || !image.valid) {
+ // Can’t create an empty sprite. An image that hasn’t loaded is also useless.
+ return;
+ }
+
+ // Create a bitmap image representation from the image, respecting backing
+ // scale factor and any resizing done on the image at runtime.
+ // 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];
+
+ // Get the image’s raw pixel data as an RGBA buffer.
+ std::string pixelString((const char *)rep.bitmapData, rep.pixelsWide * rep.pixelsHigh * 4 /* RGBA */);
+
+ mbgl::PremultipliedImage cPremultipliedImage(rep.pixelsWide, rep.pixelsHigh);
+ std::copy(rep.bitmapData, rep.bitmapData + cPremultipliedImage.size(), cPremultipliedImage.data.get());
+ auto cSpriteImage = std::make_shared<mbgl::SpriteImage>(std::move(cPremultipliedImage),
+ (float)(rep.pixelsWide / size.width));
+ _mbglMap->addAnnotationIcon(iconIdentifier.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));
+
+ // Opt into potentially expensive cursor tracking areas.
+ 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;
+ }
+
+ [self willChangeValueForKey:@"annotations"];
+
+ for (id <MGLAnnotation> annotation in annotations) {
+ NSAssert([annotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation");
+
+ MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
+ NSAssert(annotationTag != MGLAnnotationTagNotFound, @"No ID for annotation %@", annotation);
+
+ if (annotationTag == _selectedAnnotationTag) {
+ [self deselectAnnotation:annotation];
+ }
+ if (annotationTag == _lastSelectedAnnotationTag) {
+ _lastSelectedAnnotationTag = MGLAnnotationTagNotFound;
+ }
+
+ _annotationContextsByAnnotationTag.erase(annotationTag);
+
+ if ([annotation isKindOfClass:[NSObject class]] &&
+ ![annotation isKindOfClass:[MGLMultiPoint class]]) {
+ [(NSObject *)annotation removeObserver:self forKeyPath:@"coordinate" context:(void *)(NSUInteger)annotationTag];
+ }
+
+ _mbglMap->removeAnnotation(annotationTag);
+ }
+
+ [self didChangeValueForKey:@"annotations"];
+
+ [self updateAnnotationTrackingAreas];
+}
+
+- (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier {
+ return self.annotationImagesByIdentifier[identifier];
+}
+
+- (id <MGLAnnotation>)annotationAtPoint:(NSPoint)point {
+ return [self annotationWithTag:[self annotationTagAtPoint:point persistingResults:NO]];
+}
+
+/**
+ Returns the tag of the annotation at the given point in the view.
+
+ This is more involved than it sounds: if multiple point annotations overlap
+ near the point, this method cycles through them so that each of them is
+ accessible to the user at some point.
+
+ @param persist True to remember the cycleable set of annotations, so that a
+ different annotation is returned the next time this method is called
+ with the same point. Setting this parameter to false is useful for
+ asking “what if?”
+ */
+- (MGLAnnotationTag)annotationTagAtPoint:(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<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect];
+
+ if (nearbyAnnotations.size()) {
+ // Assume that the user is fat-fingering an annotation.
+ NSRect hitRect = NSInsetRect({ point, NSZeroSize },
+ -MGLAnnotationImagePaddingForHitTest,
+ -MGLAnnotationImagePaddingForHitTest);
+
+ // Filter out any annotation whose image is unselectable or for which
+ // hit testing fails.
+ auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag annotationTag) {
+ NSAssert(_annotationContextsByAnnotationTag.count(annotationTag) != 0, @"Unknown annotation found nearby click");
+ id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
+ if (!annotation) {
+ return true;
+ }
+
+ MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
+ if (!annotationImage.selectable) {
+ return true;
+ }
+
+ // Filter out the annotation if the fattened finger didn’t land on a
+ // translucent or opaque pixel in the image.
+ NSRect annotationRect = [self frameOfImage:annotationImage.image
+ centeredAtCoordinate:annotation.coordinate];
+ return !!![annotationImage.image hitTestRect:hitRect withImageDestinationRect:annotationRect
+ context:nil hints:nil flipped:NO];
+ });
+ nearbyAnnotations.resize(std::distance(nearbyAnnotations.begin(), end));
+ }
+
+ MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound;
+ if (nearbyAnnotations.size()) {
+ // The annotation tags need to be stable in order to compare them with
+ // the remembered tags.
+ std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end());
+
+ if (nearbyAnnotations == _annotationsNearbyLastClick) {
+ // The first selection in the cycle should be the one nearest to the
+ // click.
+ CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self];
+ std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) {
+ CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate];
+ CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate];
+ CLLocationDegrees distanceA = hypot(coordinateA.latitude - currentCoordinate.latitude,
+ coordinateA.longitude - currentCoordinate.longitude);
+ CLLocationDegrees distanceB = hypot(coordinateB.latitude - currentCoordinate.latitude,
+ coordinateB.longitude - currentCoordinate.longitude);
+ return distanceA < distanceB;
+ });
+
+ // The last time we persisted a set of annotations, we had the same
+ // set of annotations as we do now. Cycle through them.
+ if (_lastSelectedAnnotationTag == MGLAnnotationTagNotFound
+ || _lastSelectedAnnotationTag == _annotationsNearbyLastClick.back()) {
+ // Either no annotation is selected or the last annotation in
+ // the set was selected. Wrap around to the first annotation in
+ // the set.
+ hitAnnotationTag = _annotationsNearbyLastClick.front();
+ } else {
+ auto result = std::find(_annotationsNearbyLastClick.begin(),
+ _annotationsNearbyLastClick.end(),
+ _lastSelectedAnnotationTag);
+ if (result == _annotationsNearbyLastClick.end()) {
+ // An annotation from this set hasn’t been selected before.
+ // Select the first (nearest) one.
+ hitAnnotationTag = _annotationsNearbyLastClick.front();
+ } else {
+ // Step to the next annotation in the set.
+ auto distance = std::distance(_annotationsNearbyLastClick.begin(), result);
+ hitAnnotationTag = _annotationsNearbyLastClick[distance + 1];
+ }
+ }
+ } else {
+ // Remember the nearby annotations for the next time this method is
+ // called.
+ if (persist) {
+ _annotationsNearbyLastClick = nearbyAnnotations;
+ }
+
+ // Choose the first nearby annotation.
+ if (nearbyAnnotations.size()) {
+ hitAnnotationTag = nearbyAnnotations.front();
+ }
+ }
+ }
+
+ return hitAnnotationTag;
+}
+
+/// Returns the tags of the annotations coincident with the given rectangle.
+- (std::vector<MGLAnnotationTag>)annotationTagsInRect:(NSRect)rect {
+ mbgl::LatLngBounds queryBounds = [self convertRect:rect toLatLngBoundsFromView:self];
+ return _mbglMap->getPointAnnotationsInBounds(queryBounds);
+}
+
+- (id <MGLAnnotation>)selectedAnnotation {
+ if (!_annotationContextsByAnnotationTag.count(_selectedAnnotationTag)) {
+ return nil;
+ }
+ MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(_selectedAnnotationTag);
+ return annotationContext.annotation;
+}
+
+- (void)setSelectedAnnotation:(id <MGLAnnotation>)annotation {
+ [self willChangeValueForKey:@"selectedAnnotations"];
+ _selectedAnnotationTag = [self annotationTagForAnnotation:annotation];
+ if (_selectedAnnotationTag != MGLAnnotationTagNotFound) {
+ _lastSelectedAnnotationTag = _selectedAnnotationTag;
+ }
+ [self didChangeValueForKey:@"selectedAnnotations"];
+}
+
+- (NS_ARRAY_OF(id <MGLAnnotation>) *)selectedAnnotations {
+ id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation;
+ return selectedAnnotation ? @[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;
+ }
+
+ // Select the annotation if it’s visible.
+ if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) {
+ [self selectAnnotation:firstAnnotation];
+ }
+}
+
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation
+{
+ // Only point annotations can be selected.
+ if (!annotation || [annotation isKindOfClass:[MGLMultiPoint class]]) {
+ return;
+ }
+
+ id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation;
+ if (annotation == selectedAnnotation) {
+ return;
+ }
+
+ // Deselect the annotation before reselecting it.
+ [self deselectAnnotation:selectedAnnotation];
+
+ // Add the annotation to the map if it hasn’t been added yet.
+ MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
+ if (annotationTag == MGLAnnotationTagNotFound) {
+ [self addAnnotation:annotation];
+ }
+
+ // The annotation can’t be selected if no part of it is hittable.
+ NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag];
+ if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) {
+ return;
+ }
+
+ self.selectedAnnotation = annotation;
+
+ // For the callout to be shown, the annotation must have a title, its
+ // callout must not already be shown, and the annotation must be able to
+ // show a callout according to the delegate.
+ 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];
+
+ // Hang the callout off the right edge of the annotation image’s
+ // alignment rect, or off the left edge in a right-to-left UI.
+ callout.delegate = self;
+ self.calloutForSelectedAnnotation = callout;
+ NSRectEdge edge = (self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft
+ ? NSMinXEdge
+ : NSMaxXEdge);
+ [callout showRelativeToRect:positioningRect ofView:self preferredEdge:edge];
+ }
+}
+
+/// Returns a popover detailing the annotation.
+- (NSPopover *)calloutForAnnotation:(id <MGLAnnotation>)annotation {
+ NSPopover *callout = [[NSPopover alloc] init];
+ callout.behavior = NSPopoverBehaviorTransient;
+
+ NSViewController *viewController;
+ if ([self.delegate respondsToSelector:@selector(mapView:calloutViewControllerForAnnotation:)]) {
+ NSViewController *viewControllerFromDelegate = [self.delegate mapView:self
+ calloutViewControllerForAnnotation:annotation];
+ if (viewControllerFromDelegate) {
+ viewController = viewControllerFromDelegate;
+ }
+ }
+ if (!viewController) {
+ viewController = self.calloutViewController;
+ }
+ NSAssert(viewController, @"Unable to load MGLAnnotationCallout view controller");
+ // The popover’s view controller can bind to KVO-compliant key paths of the
+ // annotation.
+ viewController.representedObject = annotation;
+ callout.contentViewController = viewController;
+
+ return callout;
+}
+
+- (NSViewController *)calloutViewController {
+ // Lazily load a default view controller.
+ if (!_calloutViewController) {
+ _calloutViewController = [[NSViewController alloc] initWithNibName:@"MGLAnnotationCallout"
+ bundle:[NSBundle mgl_frameworkBundle]];
+ }
+ return _calloutViewController;
+}
+
+/// Returns the rectangle that represents the annotation image of the annotation
+/// with the given tag. This rectangle is fitted to the image’s alignment rect
+/// and is appropriate for positioning a popover.
+- (NSRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annotationTag {
+ id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
+ if (!annotation) {
+ return NSZeroRect;
+ }
+ NSImage *image = [self imageOfAnnotationWithTag:annotationTag].image;
+ if (!image) {
+ image = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName].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);
+}
+
+/// Returns the rectangle relative to the viewport that represents the given
+/// image centered at the given coordinate.
+- (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);
+}
+
+/// Returns the annotation image assigned to the annotation with the given tag.
+- (MGLAnnotationImage *)imageOfAnnotationWithTag:(MGLAnnotationTag)annotationTag {
+ if (annotationTag == MGLAnnotationTagNotFound
+ || _annotationContextsByAnnotationTag.count(annotationTag) == 0) {
+ return nil;
+ }
+
+ NSString *customSymbol = _annotationContextsByAnnotationTag.at(annotationTag).imageReuseIdentifier;
+ NSString *symbolName = customSymbol.length ? customSymbol : MGLDefaultStyleMarkerSymbolName;
+
+ return [self dequeueReusableAnnotationImageWithIdentifier:symbolName];
+}
+
+- (void)deselectAnnotation:(id <MGLAnnotation>)annotation {
+ if (!annotation || self.selectedAnnotation != annotation) {
+ return;
+ }
+
+ // Close the callout popover gracefully.
+ NSPopover *callout = self.calloutForSelectedAnnotation;
+ [callout performClose:self];
+
+ self.selectedAnnotation = nil;
+}
+
+/// Move the annotation callout to point to the selected annotation at its
+/// current position.
+- (void)updateAnnotationCallouts {
+ NSPopover *callout = self.calloutForSelectedAnnotation;
+ if (callout) {
+ callout.positioningRect = [self positioningRectForCalloutForAnnotationWithTag:_selectedAnnotationTag];
+ }
+}
+
+#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 selectedMenuItemColor]);
+ return MGLColorObjectFromNSColor(color);
+}
+
+- (mbgl::Color)fillColorForPolygonAnnotation:(MGLPolygon *)annotation {
+ NSColor *color = (_delegateHasFillColorsForShapeAnnotations
+ ? [self.delegate mapView:self fillColorForPolygonAnnotation:annotation]
+ : [NSColor selectedMenuItemColor]);
+ 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 (annotation && [self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) {
+ [self.delegate mapView:self didSelectAnnotation:annotation];
+ }
+}
+
+- (void)popoverDidClose:(__unused NSNotification *)notification {
+ // Deselect the closed popover, in case the popover was closed due to user
+ // action.
+ 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<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:self.bounds];
+ for (MGLAnnotationTag annotationTag : annotationTags) {
+ MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
+ id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
+ if (annotation.toolTip.length) {
+ // Add a tooltip tracking area over the annotation image’s
+ // frame, accounting for the image’s alignment rect.
+ 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)annotationTag];
+ }
+ }
+ // Opt into potentially expensive cursor tracking areas.
+ if (annotationImage.cursor) {
+ _wantsCursorRects = YES;
+ }
+ }
+ }
+
+ // Blow away any cursor tracking areas and rebuild them. That’s the
+ // potentially expensive part.
+ if (_wantsCursorRects) {
+ [self.window invalidateCursorRectsForView:self];
+ }
+}
+
+- (NSString *)view:(__unused NSView *)view stringForToolTip:(__unused NSToolTipTag)tag point:(__unused NSPoint)point userData:(void *)data {
+ NSAssert((NSUInteger)data < MGLAnnotationTagNotFound, @"Invalid annotation tag in tooltip rect user data.");
+ MGLAnnotationTag annotationTag = (MGLAnnotationTag)MIN((NSUInteger)data, MGLAnnotationTagNotFound);
+ id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
+ return annotation.toolTip;
+}
+
+- (void)resetCursorRects {
+ // Drag to pan has a grabbing hand cursor.
+ if ([self isPanningWithGesture]) {
+ [self addCursorRect:self.bounds cursor:[NSCursor closedHandCursor]];
+ return;
+ }
+
+ // The rest of this method can be expensive, so bail if no annotations have
+ // ever had custom cursors.
+ if (!_wantsCursorRects) {
+ return;
+ }
+
+ std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:self.bounds];
+ for (MGLAnnotationTag annotationTag : annotationTags) {
+ id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
+ MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
+ if (annotationImage.cursor) {
+ // Add a cursor tracking area over the annotation image, respecting
+ // the image’s alignment rect.
+ 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 Data
+
+- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesAtPoint:(NSPoint)point {
+ return [self visibleFeaturesAtPoint:point inStyleLayersWithIdentifiers:nil];
+}
+
+- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesAtPoint:(NSPoint)point inStyleLayersWithIdentifiers:(NS_SET_OF(NSString *) *)styleLayerIdentifiers {
+ // Cocoa origin is at the lower-left corner.
+ mbgl::ScreenCoordinate screenCoordinate = { point.x, NSHeight(self.bounds) - point.y };
+
+ mbgl::optional<std::vector<std::string>> optionalLayerIDs;
+ if (styleLayerIdentifiers) {
+ __block std::vector<std::string> layerIDs;
+ layerIDs.reserve(styleLayerIdentifiers.count);
+ [styleLayerIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull identifier, BOOL * _Nonnull stop) {
+ layerIDs.push_back(identifier.UTF8String);
+ }];
+ optionalLayerIDs = layerIDs;
+ }
+
+ std::vector<mbgl::Feature> features = _mbglMap->queryRenderedFeatures(screenCoordinate, optionalLayerIDs);
+ return MGLFeaturesFromMBGLFeatures(features);
+}
+
+- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesInRect:(NSRect)rect {
+ return [self visibleFeaturesInRect:rect inStyleLayersWithIdentifiers:nil];
+}
+
+- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesInRect:(NSRect)rect inStyleLayersWithIdentifiers:(NS_SET_OF(NSString *) *)styleLayerIdentifiers {
+ // Cocoa origin is at the lower-left corner.
+ mbgl::ScreenBox screenBox = {
+ { NSMinX(rect), NSHeight(self.bounds) - NSMaxY(rect) },
+ { NSMaxX(rect), NSHeight(self.bounds) - NSMinY(rect) },
+ };
+
+ mbgl::optional<std::vector<std::string>> optionalLayerIDs;
+ if (styleLayerIdentifiers) {
+ __block std::vector<std::string> layerIDs;
+ layerIDs.reserve(styleLayerIdentifiers.count);
+ [styleLayerIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull identifier, BOOL * _Nonnull stop) {
+ layerIDs.push_back(identifier.UTF8String);
+ }];
+ optionalLayerIDs = layerIDs;
+ }
+
+ std::vector<mbgl::Feature> features = _mbglMap->queryRenderedFeatures(screenBox, optionalLayerIDs);
+ return MGLFeaturesFromMBGLFeatures(features);
+}
+
+#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
+
+- (NSPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(nullable NSView *)view {
+ return [self convertLatLng:MGLLatLngFromLocationCoordinate2D(coordinate) toPointToView:view];
+}
+
+/// Converts a geographic coordinate to a point in the view’s coordinate system.
+- (NSPoint)convertLatLng:(mbgl::LatLng)latLng toPointToView:(nullable NSView *)view {
+ mbgl::ScreenCoordinate pixel = _mbglMap->pixelForLatLng(latLng);
+ // Cocoa origin is at the lower-left corner.
+ pixel.y = NSHeight(self.bounds) - pixel.y;
+ return [self convertPoint:NSMakePoint(pixel.x, pixel.y) toView:view];
+}
+
+- (CLLocationCoordinate2D)convertPoint:(NSPoint)point toCoordinateFromView:(nullable NSView *)view {
+ return MGLLocationCoordinate2DFromLatLng([self convertPoint:point toLatLngFromView:view]);
+}
+
+/// Converts a point in the view’s coordinate system to a geographic coordinate.
+- (mbgl::LatLng)convertPoint:(NSPoint)point toLatLngFromView:(nullable NSView *)view {
+ NSPoint convertedPoint = [self convertPoint:point fromView:view];
+ return _mbglMap->latLngForPixel({
+ convertedPoint.x,
+ // mbgl origin is at the top-left corner.
+ NSHeight(self.bounds) - convertedPoint.y,
+ }).wrapped();
+}
+
+- (NSRect)convertCoordinateBounds:(MGLCoordinateBounds)bounds toRectToView:(nullable NSView *)view {
+ return [self convertLatLngBounds:MGLLatLngBoundsFromCoordinateBounds(bounds) toRectToView:view];
+}
+
+/// Converts a geographic bounding box to a rectangle in the view’s coordinate
+/// system.
+- (NSRect)convertLatLngBounds:(mbgl::LatLngBounds)bounds toRectToView:(nullable NSView *)view {
+ NSRect rect = { [self convertLatLng:bounds.southwest() toPointToView:view], NSZeroSize };
+ rect = MGLExtendRect(rect, [self convertLatLng:bounds.northeast() toPointToView:view]);
+ return rect;
+}
+
+- (MGLCoordinateBounds)convertRect:(NSRect)rect toCoordinateBoundsFromView:(nullable NSView *)view {
+ return MGLCoordinateBoundsFromLatLngBounds([self convertRect:rect toLatLngBoundsFromView:view]);
+}
+
+/// Converts a rectangle in the given view’s coordinate system to a geographic
+/// bounding box.
+- (mbgl::LatLngBounds)convertRect:(NSRect)rect toLatLngBoundsFromView:(nullable NSView *)view {
+ mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty();
+ bounds.extend([self convertPoint:rect.origin toLatLngFromView:view]);
+ bounds.extend([self convertPoint:{ NSMaxX(rect), NSMinY(rect) } toLatLngFromView:view]);
+ bounds.extend([self convertPoint:{ NSMaxX(rect), NSMaxY(rect) } toLatLngFromView:view]);
+ bounds.extend([self convertPoint:{ NSMinX(rect), NSMaxY(rect) } toLatLngFromView:view]);
+
+ // The world is wrapping if a point just outside the bounds is also within
+ // the rect.
+ mbgl::LatLng outsideLatLng;
+ if (bounds.west() > -180) {
+ outsideLatLng = {
+ (bounds.south() + bounds.north()) / 2,
+ bounds.west() - 1,
+ };
+ } else if (bounds.northeast().longitude < 180) {
+ outsideLatLng = {
+ (bounds.south() + bounds.north()) / 2,
+ bounds.east() + 1,
+ };
+ }
+
+ // If the world is wrapping, extend the bounds to cover all longitudes.
+ if (NSPointInRect([self convertLatLng:outsideLatLng toPointToView:view], rect)) {
+ bounds.extend(mbgl::LatLng(bounds.south(), -180));
+ bounds.extend(mbgl::LatLng(bounds.south(), 180));
+ }
+
+ return bounds;
+}
+
+- (CLLocationDistance)metersPerPointAtLatitude:(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;
+ }
+ if (options & mbgl::MapDebugOptions::Wireframe) {
+ mask |= MGLMapDebugWireframesMask;
+ }
+ if (options & mbgl::MapDebugOptions::StencilClip) {
+ mask |= MGLMapDebugStencilBufferMask;
+ }
+ 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;
+ }
+ if (debugMask & MGLMapDebugWireframesMask) {
+ options |= mbgl::MapDebugOptions::Wireframe;
+ }
+ if (debugMask & MGLMapDebugStencilBufferMask) {
+ options |= mbgl::MapDebugOptions::StencilClip;
+ }
+ _mbglMap->setDebug(options);
+}
+
+/// Adapter responsible for bridging calls from mbgl to MGLMapView and Cocoa.
+class MGLMapViewImpl : public mbgl::View {
+public:
+ MGLMapViewImpl(MGLMapView *nativeView_, const float scaleFactor_)
+ : nativeView(nativeView_), scaleFactor(scaleFactor_) {}
+
+ 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 notifyMapChange(mbgl::MapChange change) override {
+ [nativeView notifyMapChange:change];
+ }
+
+ void invalidate() override {
+ [nativeView invalidate];
+ }
+
+ void activate() override {
+ MGLOpenGLLayer *layer = (MGLOpenGLLayer *)nativeView.layer;
+ [layer.openGLContext makeCurrentContext];
+ }
+
+ void deactivate() override {
+ [NSOpenGLContext clearCurrentContext];
+ }
+
+ mbgl::PremultipliedImage readStillImage() override {
+ auto size = getFramebufferSize();
+ const unsigned int w = size[0];
+ const unsigned int h = size[1];
+
+ mbgl::PremultipliedImage image { w, h };
+ MBGL_CHECK_ERROR(glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, image.data.get()));
+
+ const size_t stride = image.stride();
+ auto tmp = std::make_unique<uint8_t[]>(stride);
+ uint8_t *rgba = image.data.get();
+ for (int i = 0, j = h - 1; i < j; i++, j--) {
+ std::memcpy(tmp.get(), rgba + i * stride, stride);
+ std::memcpy(rgba + i * stride, rgba + j * stride, stride);
+ std::memcpy(rgba + j * stride, tmp.get(), stride);
+ }
+
+ return image;
+ }
+
+private:
+ /// Cocoa map view that this adapter bridges to.
+ __weak MGLMapView *nativeView = nullptr;
+
+ /// Backing scale factor of the view.
+ const float scaleFactor;
+};
+
+@end
diff --git a/platform/macos/src/MGLMapViewDelegate.h b/platform/macos/src/MGLMapViewDelegate.h
new file mode 100644
index 0000000000..0b7eec84ac
--- /dev/null
+++ b/platform/macos/src/MGLMapViewDelegate.h
@@ -0,0 +1,220 @@
+#import <Foundation/Foundation.h>
+
+#import "MGLTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class MGLMapView;
+@class MGLAnnotationImage;
+@class MGLPolygon;
+@class MGLPolyline;
+@class MGLShape;
+
+/**
+ The `MGLMapViewDelegate` protocol defines a set of optional methods that you
+ can use to receive messages from an `MGLMapView` instance. Because many map
+ operations require the `MGLMapView` class to load data asynchronously, the map
+ view calls these methods to notify your application when specific operations
+ complete. The map view also uses these methods to request information about
+ annotations displayed on the map, such as the styles and interaction modes to
+ apply to individual annotations.
+ */
+@protocol MGLMapViewDelegate <NSObject>
+
+@optional
+
+#pragma mark Responding to Map Viewpoint Changes
+
+/**
+ Tells the delegate that the viewpoint depicted by the map view is about to
+ change.
+
+ This method is called whenever the currently displayed map camera will start
+ changing for any reason.
+
+ @param mapView The map view whose viewpoint will change.
+ @param animated Whether the change will cause an animated effect on the map.
+ */
+- (void)mapView:(MGLMapView *)mapView cameraWillChangeAnimated:(BOOL)animated;
+
+/**
+ Tells the delegate that the viewpoint depicted by the map view is changing.
+
+ This method is called as the currently displayed map camera changes due to
+ animation. During movement, this method may be called many times to report
+ updates to the viewpoint. Therefore, your implementation of this method should
+ be as lightweight as possible to avoid affecting performance.
+
+ @param mapView The map view whose viewpoint is changing.
+ */
+- (void)mapViewCameraIsChanging:(MGLMapView *)mapView;
+
+/**
+ Tells the delegate that the viewpoint depicted by the map view has finished
+ changing.
+
+ This method is called whenever the currently displayed map camera has finished
+ changing.
+
+ @param mapView The map view whose viewpoint has changed.
+ @param animated Whether the change caused an animated effect on the map.
+ */
+- (void)mapView:(MGLMapView *)mapView cameraDidChangeAnimated:(BOOL)animated;
+
+#pragma mark Loading the Map
+
+/**
+ Tells the delegate that the map view will begin to load.
+
+ This method is called whenever the map view starts loading, including when a
+ new style has been set and the map must reload.
+
+ @param mapView The map view that is starting to load.
+ */
+- (void)mapViewWillStartLoadingMap:(MGLMapView *)mapView;
+
+/**
+ Tells the delegate that the map view has finished loading.
+
+ This method is called whenever the map view finishes loading, either after the
+ initial load or after a style change has forced a reload.
+
+ @param mapView The map view that has finished loading.
+ */
+- (void)mapViewDidFinishLoadingMap:(MGLMapView *)mapView;
+
+- (void)mapViewWillStartRenderingMap:(MGLMapView *)mapView;
+- (void)mapViewDidFinishRenderingMap:(MGLMapView *)mapView fullyRendered:(BOOL)fullyRendered;
+- (void)mapViewWillStartRenderingFrame:(MGLMapView *)mapView;
+- (void)mapViewDidFinishRenderingFrame:(MGLMapView *)mapView fullyRendered:(BOOL)fullyRendered;
+
+#pragma mark Managing the Display of Annotations
+
+/**
+ Returns an annotation image object to mark the given point annotation object on
+ the map.
+
+ @param mapView The map view that requested the annotation image.
+ @param annotation The object representing the annotation that is about to be
+ displayed.
+ @return The image object to display for the given annotation or `nil` if you
+ want to display the default marker image.
+ */
+- (nullable MGLAnnotationImage *)mapView:(MGLMapView *)mapView imageForAnnotation:(id <MGLAnnotation>)annotation;
+
+/**
+ Returns the alpha value to use when rendering a shape annotation.
+
+ A value of 0.0 results in a completely transparent shape. A value of 1.0, the
+ default, results in a completely opaque shape.
+
+ @param mapView The map view rendering the shape annotation.
+ @param annotation The annotation being rendered.
+ @return An alpha value between 0 and 1.0.
+ */
+- (CGFloat)mapView:(MGLMapView *)mapView alphaForShapeAnnotation:(MGLShape *)annotation;
+
+/**
+ Returns the color to use when rendering the outline of a shape annotation.
+
+ The default stroke color is the selected menu item color. If a pattern color is
+ specified, the result is undefined.
+
+ @param mapView The map view rendering the shape annotation.
+ @param annotation The annotation being rendered.
+ @return A color to use for the shape outline.
+ */
+- (NSColor *)mapView:(MGLMapView *)mapView strokeColorForShapeAnnotation:(MGLShape *)annotation;
+
+/**
+ Returns the color to use when rendering the fill of a polygon annotation.
+
+ The default fill color is selected menu item color. If a pattern color is
+ specified, the result is undefined.
+
+ @param mapView The map view rendering the polygon annotation.
+ @param annotation The annotation being rendered.
+ @return The polygon’s interior fill color.
+ */
+- (NSColor *)mapView:(MGLMapView *)mapView fillColorForPolygonAnnotation:(MGLPolygon *)annotation;
+
+/**
+ Returns the line width in points to use when rendering the outline of a
+ polyline annotation.
+
+ By default, the polyline is outlined with a line 3.0 points wide.
+
+ @param mapView The map view rendering the polygon annotation.
+ @param annotation The annotation being rendered.
+ @return A line width for the polyline, measured in points.
+ */
+- (CGFloat)mapView:(MGLMapView *)mapView lineWidthForPolylineAnnotation:(MGLPolyline *)annotation;
+
+#pragma mark Selecting Annotations
+
+/**
+ Tells the delegate that one of its annotations has been selected.
+
+ You can use this method to track changes to the selection state of annotations.
+
+ @param mapView The map view containing the annotation.
+ @param annotation The annotation that was selected.
+ */
+- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id <MGLAnnotation>)annotation;
+
+/**
+ Tells the delegate that one of its annotations has been deselected.
+
+ You can use this method to track changes in the selection state of annotations.
+
+ @param mapView The map view containing the annotation.
+ @param annotation The annotation that was deselected.
+ */
+- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id <MGLAnnotation>)annotation;
+
+#pragma mark Displaying Information About Annotations
+
+/**
+ Returns a Boolean value indicating whether the annotation is able to display
+ extra information in a callout popover.
+
+ This method is called after an annotation is selected, before any callout is
+ displayed for the annotation.
+
+ If the return value is `YES`, a callout popover is shown when the user clicks
+ on a selected annotation. The default callout displays the annotation’s title
+ and subtitle. You can customize the popover’s contents by implementing the
+ `-mapView:calloutViewControllerForAnnotation:` method.
+
+ If the return value is `NO`, or if this method is unimplemented, or if the
+ annotation lacks a title, the annotation will not show a callout even when
+ selected.
+
+ @param mapView The map view that has selected the annotation.
+ @param annotation The object representing the annotation.
+ @return A Boolean value indicating whether the annotation should show a
+ callout.
+ */
+- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation;
+
+/**
+ Returns a view controller to manage the callout popover’s content view.
+
+ Like any instance of `NSPopover`, an annotation callout manages its contents
+ with a view controller. The annotation object is the view controller’s
+ represented object. This means that you can bind controls in the view
+ controller’s content view to KVO-compliant properties of the annotation object,
+ such as `title` and `subtitle`.
+
+ If each annotation should have an identical callout, you can set the
+ `MGLMapView` instance’s `-setCalloutViewController:` method instead.
+
+ @param mapView The map view that is requesting a callout view controller.
+ @param annotation The object representing the annotation.
+ @return A view controller for the given annotation.
+ */
+- (nullable NSViewController *)mapView:(MGLMapView *)mapView calloutViewControllerForAnnotation:(id <MGLAnnotation>)annotation;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/macos/src/MGLMapView_Private.h b/platform/macos/src/MGLMapView_Private.h
new file mode 100644
index 0000000000..76b1727925
--- /dev/null
+++ b/platform/macos/src/MGLMapView_Private.h
@@ -0,0 +1,23 @@
+#import "MGLMapView.h"
+
+@interface MGLMapView (Private)
+
+/// True if the view or application is in a state where it is not expected to be
+/// actively drawing.
+@property (nonatomic, readonly, getter=isDormant) BOOL dormant;
+
+// These properties exist because initially, both the latitude and longitude are
+// NaN. You have to set both the latitude and longitude simultaneously. If you
+// set the latitude but reuse the current longitude, and the current longitude
+// happens to be NaN, there will be no change because the resulting coordinate
+// pair is invalid.
+
+/// Center latitude set independently of the center longitude in an inspectable.
+@property (nonatomic) CLLocationDegrees pendingLatitude;
+/// Center longitude set independently of the center latitude in an inspectable.
+@property (nonatomic) CLLocationDegrees pendingLongitude;
+
+/// Synchronously render a frame of the map.
+- (void)renderSync;
+
+@end
diff --git a/platform/macos/src/MGLOpenGLLayer.h b/platform/macos/src/MGLOpenGLLayer.h
new file mode 100644
index 0000000000..5c8ac43e9e
--- /dev/null
+++ b/platform/macos/src/MGLOpenGLLayer.h
@@ -0,0 +1,12 @@
+#import <Cocoa/Cocoa.h>
+
+#import "MGLTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// A subclass of NSOpenGLLayer that creates the environment mbgl needs to
+/// render good-looking maps.
+@interface MGLOpenGLLayer : NSOpenGLLayer
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/macos/src/MGLOpenGLLayer.mm b/platform/macos/src/MGLOpenGLLayer.mm
new file mode 100644
index 0000000000..e8fa521351
--- /dev/null
+++ b/platform/macos/src/MGLOpenGLLayer.mm
@@ -0,0 +1,49 @@
+#import "MGLOpenGLLayer.h"
+
+#import "MGLMapView_Private.h"
+
+#import <mbgl/gl/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/macos/src/Mapbox.h b/platform/macos/src/Mapbox.h
new file mode 100644
index 0000000000..e4545e04bc
--- /dev/null
+++ b/platform/macos/src/Mapbox.h
@@ -0,0 +1,34 @@
+#import <Cocoa/Cocoa.h>
+
+/// Project version number for Mapbox.
+FOUNDATION_EXPORT double MapboxVersionNumber;
+
+/// Project version string for Mapbox.
+FOUNDATION_EXPORT const unsigned char MapboxVersionString[];
+
+#import "MGLAccountManager.h"
+#import "MGLAnnotation.h"
+#import "MGLAnnotationImage.h"
+#import "MGLClockDirectionFormatter.h"
+#import "MGLCompassDirectionFormatter.h"
+#import "MGLCoordinateFormatter.h"
+#import "MGLFeature.h"
+#import "MGLGeometry.h"
+#import "MGLMapCamera.h"
+#import "MGLMapView.h"
+#import "MGLMapView+IBAdditions.h"
+#import "MGLMapViewDelegate.h"
+#import "MGLMultiPoint.h"
+#import "MGLOfflinePack.h"
+#import "MGLOfflineRegion.h"
+#import "MGLOfflineStorage.h"
+#import "MGLOverlay.h"
+#import "MGLPointAnnotation.h"
+#import "MGLPolygon.h"
+#import "MGLPolyline.h"
+#import "MGLShape.h"
+#import "MGLShapeCollection.h"
+#import "MGLStyle.h"
+#import "MGLTilePyramidOfflineRegion.h"
+#import "MGLTypes.h"
+#import "NSValue+MGLAdditions.h"