Files
MapLibreParticleLab/platform/macos/src/MLNMapView.mm
Bart Louwers 550f64be22 core: rename mbgl namespace to mln (#4487)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-14 23:25:20 +02:00

3494 lines
133 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import "MLNMapView_Private.h"
#import "MLNAttributionButton.h"
#import "MLNCompassCell.h"
#import "MLNRendererConfiguration.h"
#import "MLNRendererFrontend.h"
#import "MLNStyle.h"
#import "MLNAnnotationImage_Private.h"
#import "MLNAttributionInfo_Private.h"
#import "MLNFeature_Private.h"
#import "MLNFoundation_Private.h"
#import "MLNGeometry_Private.h"
#import "MLNMultiPoint_Private.h"
#import "MLNOfflineStorage_Private.h"
#import "MLNShape_Private.h"
#import "MLNStyle_Private.h"
#import "MLNAnnotationImage.h"
#import "MLNImageSource.h"
#import "MLNMapCamera.h"
#import "MLNMapViewDelegate.h"
#import "MLNPolygon.h"
#import "MLNPolyline.h"
#import "MLNSettings.h"
#include <optional>
#import <mbgl/annotation/annotation.hpp>
#import <mbgl/map/camera.hpp>
#import <mbgl/map/map.hpp>
#import <mbgl/map/map_options.hpp>
#import <mbgl/math/wrap.hpp>
#import <mbgl/renderer/renderer.hpp>
#import <mbgl/storage/network_status.hpp>
#import <mbgl/storage/resource_options.hpp>
#import <mbgl/style/image.hpp>
#import <mbgl/style/style.hpp>
#import <mbgl/util/action_journal.hpp>
#import <mbgl/util/chrono.hpp>
#import <mbgl/util/client_options.hpp>
#import <mbgl/util/constants.hpp>
#import <mbgl/util/exception.hpp>
#import <mbgl/util/projection.hpp>
#import <mbgl/util/run_loop.hpp>
#import <mbgl/util/string.hpp>
#import <map>
#import <unordered_map>
#import <unordered_set>
#import "MLNActionJournalOptions_Private.h"
#import "MLNBackendResource.h"
#import "MLNLoggingConfiguration_Private.h"
#import "MLNMapView+Impl.h"
#import "MLNNetworkConfiguration_Private.h"
#import "MLNReachability.h"
#import "MLNRenderingStats_Private.h"
#import "MLNSettings_Private.h"
#import "NSBundle+MLNAdditions.h"
#import "NSColor+MLNAdditions.h"
#import "NSDate+MLNAdditions.h"
#import "NSException+MLNAdditions.h"
#import "NSImage+MLNAdditions.h"
#import "NSPredicate+MLNPrivateAdditions.h"
#import "NSProcessInfo+MLNAdditions.h"
#import "NSString+MLNAdditions.h"
#import "NSURL+MLNAdditions.h"
#import <CoreImage/CIFilter.h>
class MLNAnnotationContext;
/// Distance from the edge of the view to ornament views (logo, attribution, etc.).
const CGFloat MLNOrnamentPadding = 12;
/// Alpha value of the ornament views (logo, attribution, etc.).
const CGFloat MLNOrnamentOpacity = 0.9;
/// Default duration for programmatic animations.
const NSTimeInterval MLNAnimationDuration = 0.3;
/// Distance in points that a single press of the panning keyboard shortcut pans the map by.
const CGFloat MLNKeyPanningIncrement = 150;
/// Degrees that a single press of the rotation keyboard shortcut rotates the map by.
const CLLocationDegrees MLNKeyRotationIncrement = 25;
/// Key for the user default that, when true, causes the map view to zoom in and out on scroll wheel
/// events.
NSString *const MLNScrollWheelZoomsMapViewDefaultKey = @"MLNScrollWheelZoomsMapView";
/// Reuse identifier and file name of the default point annotation image.
static NSString *const MLNDefaultStyleMarkerSymbolName = @"default_marker";
/// Prefix that denotes a sprite installed by MLNMapView, to avoid collisions
/// with style-defined sprites.
static NSString *const MLNAnnotationSpritePrefix = @"org.maplibre.sprites.";
/// Slop area around the hit testing point, allowing for imprecise annotation selection.
const CGFloat MLNAnnotationImagePaddingForHitTest = 4;
/// Distance from the callout’s anchor point to the annotation it points to.
const CGFloat MLNAnnotationImagePaddingForCallout = 4;
/// Padding to edge of view that an offscreen annotation must have when being brought onscreen (by
/// being selected)
const NSEdgeInsets MLNMapViewOffscreenAnnotationPadding =
NSEdgeInsetsMake(-30.0f, -30.0f, -30.0f, -30.0f);
/// Unique identifier representing a single annotation in mbgl.
typedef uint64_t MLNAnnotationTag;
/// An indication that the requested annotation was not found or is nonexistent.
enum { MLNAnnotationTagNotFound = UINT64_MAX };
/// Mapping from an annotation tag to metadata about that annotation, including
/// the annotation itself.
typedef std::unordered_map<MLNAnnotationTag, MLNAnnotationContext> MLNAnnotationTagContextMap;
/// Mapping from an annotation object to an annotation tag.
typedef std::map<id<MLNAnnotation>, MLNAnnotationTag> MLNAnnotationObjectTagMap;
/// Returns an NSImage for the default marker image.
NSImage *MLNDefaultMarkerImage() {
NSString *path = [[NSBundle mgl_frameworkBundle] pathForResource:MLNDefaultStyleMarkerSymbolName
ofType:@"pdf"];
return [[NSImage alloc] initWithContentsOfFile:path];
}
/// Converts a media timing function into a unit bezier object usable in mbgl.
mln::util::UnitBezier MLNUnitBezierForMediaTimingFunction(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]};
}
/// Lightweight container for metadata about an annotation, including the annotation itself.
class MLNAnnotationContext {
public:
id<MLNAnnotation> annotation;
/// The annotation’s image’s reuse identifier.
NSString *imageReuseIdentifier;
};
@interface MLNMapView () <NSPopoverDelegate, MLNMultiPointDelegate, NSGestureRecognizerDelegate>
@property (nonatomic, readwrite) NSSegmentedControl *zoomControls;
@property (nonatomic, readwrite) NSSlider *compass;
@property (nonatomic, readwrite) NSImageView *logoView;
@property (nonatomic, readwrite) NSView *attributionView;
@property (nonatomic, readwrite) MLNStyle *style;
/// Mapping from reusable identifiers to annotation images.
@property (nonatomic)
NSMutableDictionary<NSString *, MLNAnnotationImage *> *annotationImagesByIdentifier;
/// Currently shown popover representing the selected annotation.
@property (nonatomic) NSPopover *calloutForSelectedAnnotation;
@property (nonatomic, readwrite, getter=isDormant) BOOL dormant;
@end
@implementation MLNMapView {
/// Cross-platform map view controller.
std::unique_ptr<mln::Map> _mbglMap;
std::unique_ptr<MLNMapViewImpl> _mbglView;
std::unique_ptr<MLNRenderFrontend> _rendererFrontend;
NSPanGestureRecognizer *_panGestureRecognizer;
NSMagnificationGestureRecognizer *_magnificationGestureRecognizer;
NSRotationGestureRecognizer *_rotationGestureRecognizer;
NSClickGestureRecognizer *_singleClickRecognizer;
double _zoomAtBeginningOfGesture;
CLLocationDirection _directionAtBeginningOfGesture;
CGFloat _pitchAtBeginningOfGesture;
BOOL _didHideCursorDuringGesture;
MLNAnnotationTagContextMap _annotationContextsByAnnotationTag;
MLNAnnotationObjectTagMap _annotationTagsByAnnotation;
MLNAnnotationTag _selectedAnnotationTag;
MLNAnnotationTag _lastSelectedAnnotationTag;
/// Size of the rectangle formed by unioning the maximum slop area around every annotation image.
NSSize _unionedAnnotationImageSize;
std::vector<MLNAnnotationTag> _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;
/// True if a willChange notification has been issued for shape annotation layers and a didChange
/// notification is pending.
BOOL _isChangingAnnotationLayers;
// Cached checks for delegate method implementations that may be called from
// MLNMultiPointDelegate 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;
/// reachability instance
MLNReachability *_reachability;
MLNRenderingStats *_renderingStats;
}
// MARK: Lifecycle
+ (void)initialize {
if (self == [MLNMapView class]) {
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
MLNScrollWheelZoomsMapViewDefaultKey : @NO,
}];
}
}
- (instancetype)initWithFrame:(NSRect)frameRect {
if (self = [super initWithFrame:frameRect]) {
MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class]));
MLNLogDebug(@"Initializing frame: %@", NSStringFromRect(frameRect));
[self commonInitWithOptions:nil];
self.styleURL = nil;
MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class]));
}
return self;
}
- (instancetype)initWithFrame:(NSRect)frame styleURL:(nullable NSURL *)styleURL {
if (self = [super initWithFrame:frame]) {
MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class]));
MLNLogDebug(@"Initializing frame: %@ styleURL: %@", NSStringFromRect(frame), styleURL);
[self commonInitWithOptions:nil];
self.styleURL = styleURL;
MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class]));
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame options:(MLNMapOptions *)options {
if (self = [super initWithFrame:frame]) {
MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class]));
MLNLogDebug(@"Initializing frame: %@ with options", NSStringFromRect(frame));
[self commonInitWithOptions:options];
if (options) {
if (options.styleURL) {
self.styleURL = options.styleURL;
}
} else {
self.styleURL = nil;
}
MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class]));
}
return self;
}
- (instancetype)initWithCoder:(nonnull NSCoder *)decoder {
if (self = [super initWithCoder:decoder]) {
MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class]));
[self commonInitWithOptions:nil];
MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class]));
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
// If the Style URL inspectable was not set, make sure to go through
// -setStyleURL: to load the default style.
if (_mbglMap->getStyle().getURL().empty()) {
self.styleURL = nil;
}
}
+ (NSArray *)restorableStateKeyPaths {
return @[ @"camera", @"debugMask" ];
}
- (void)commonInitWithOptions:(MLNMapOptions *)mlnMapoptions {
if (mlnMapoptions == nil) {
mlnMapoptions = [[MLNMapOptions alloc] init];
}
[MLNNetworkConfiguration sharedManager];
_isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent;
// Set up cross-platform controllers and resources.
_mbglView = MLNMapViewImpl::Create(self);
// Delete the pre-offline ambient cache at
// ~/Library/Caches/com.mapbox.MapboxGL/cache.db.
NSURL *cachesDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:NO
error:nil];
cachesDirectoryURL = [cachesDirectoryURL URLByAppendingPathComponent:@"com.mapbox.MapboxGL"];
NSURL *legacyCacheURL = [cachesDirectoryURL URLByAppendingPathComponent:@"cache.db"];
[[NSFileManager defaultManager] removeItemAtURL:legacyCacheURL error:NULL];
MLNRendererConfiguration *config = [MLNRendererConfiguration currentConfiguration];
std::optional<std::string> localFontFamilyName =
config.localFontFamilyName ? std::optional(std::string(config.localFontFamilyName.UTF8String))
: std::nullopt;
auto renderer = std::make_unique<mln::Renderer>(_mbglView->getRendererBackend(),
config.scaleFactor, localFontFamilyName);
BOOL enableCrossSourceCollisions = !config.perSourceCollisions;
_rendererFrontend = std::make_unique<MLNRenderFrontend>(std::move(renderer), self,
_mbglView->getRendererBackend(), true);
mln::MapOptions mapOptions;
mapOptions.withMapMode(mln::MapMode::Continuous)
.withSize(self.size)
.withPixelRatio(config.scaleFactor)
.withConstrainMode(mln::ConstrainMode::None)
.withViewportMode(mln::ViewportMode::Default)
.withCrossSourceCollisions(enableCrossSourceCollisions);
auto tileServerOptions = [[MLNSettings sharedSettings] tileServerOptionsInternal];
mln::ResourceOptions resourceOptions;
resourceOptions.withTileServerOptions(*tileServerOptions)
.withCachePath(MLNOfflineStorage.sharedOfflineStorage.databasePath.UTF8String)
.withAssetPath([NSBundle mainBundle].resourceURL.path.UTF8String);
mln::ClientOptions clientOptions;
auto apiKey = [[MLNSettings sharedSettings] apiKey];
if (apiKey) {
resourceOptions.withApiKey([apiKey UTF8String]);
}
const mln::util::ActionJournalOptions &actionJournalOptions =
[mlnMapoptions.actionJournalOptions getCoreOptions];
_mbglMap = std::make_unique<mln::Map>(*_rendererFrontend, *_mbglView, mapOptions, resourceOptions,
clientOptions, actionJournalOptions);
// Notify map object when network reachability status changes.
_reachability = [MLNReachability reachabilityForInternetConnection];
_reachability.reachableBlock = ^(MLNReachability *) {
mln::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 = {};
_annotationTagsByAnnotation = {};
_selectedAnnotationTag = MLNAnnotationTagNotFound;
_lastSelectedAnnotationTag = MLNAnnotationTagNotFound;
_annotationsNearbyLastClick = {};
// Jump to Null Island initially.
self.automaticallyAdjustsContentInsets = YES;
mln::CameraOptions options;
options.center = mln::LatLng(0, 0);
options.padding = MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets);
options.zoom = *_mbglMap->getBounds().minZoom;
_mbglMap->jumpTo(options);
_pendingLatitude = NAN;
_pendingLongitude = NAN;
}
- (mln::Size)size {
// check for minimum texture size supported by OpenGL ES 2.0
//
CGSize size = CGSizeMake(MAX(self.bounds.size.width, 64), MAX(self.bounds.size.height, 64));
return {static_cast<uint32_t>(size.width), static_cast<uint32_t>(size.height)};
}
- (mln::Size)framebufferSize {
NSRect bounds = [self convertRectToBacking:self.bounds];
return {static_cast<uint32_t>(bounds.size.width), static_cast<uint32_t>(bounds.size.height)};
}
/// Adds zoom controls to the lower-right corner.
- (void)installZoomControls {
_zoomControls = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect];
_zoomControls.wantsLayer = YES;
_zoomControls.layer.opacity = MLNOrnamentOpacity;
[(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 = NSControlSizeRegular;
[_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 = MLNOrnamentOpacity;
_compass.cell = [[MLNCompassCell 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 = NSOffsetRect(logoImage.alignmentRect, 0, 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 removeFromSuperview];
_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];
_singleClickRecognizer =
[[NSClickGestureRecognizer alloc] initWithTarget:self action:@selector(handleClickGesture:)];
_singleClickRecognizer.delaysPrimaryMouseButtonEvents = NO;
_singleClickRecognizer.delegate = self;
[self addGestureRecognizer:_singleClickRecognizer];
NSClickGestureRecognizer *rightClickGestureRecognizer =
[[NSClickGestureRecognizer alloc] initWithTarget:self
action:@selector(handleRightClickGesture:)];
rightClickGestureRecognizer.buttonMask = 0x2;
[self addGestureRecognizer:rightClickGestureRecognizer];
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 {
NSView *attributionView = self.attributionView;
for (NSView *button in attributionView.subviews) {
[button removeConstraints:button.constraints];
}
attributionView.subviews = @[];
[attributionView removeConstraints:attributionView.constraints];
// Make the whole string mini by default.
// Force links to be black, because the default blue is distracting.
CGFloat miniSize = [NSFont systemFontSizeForControlSize:NSControlSizeMini];
NSArray *attributionInfos = [self.style attributionInfosWithFontSize:miniSize
linkColor:[NSColor blackColor]];
for (MLNAttributionInfo *info in attributionInfos) {
// Feedback links are added to the Help menu.
if (info.feedbackLink) {
continue;
}
// For each attribution, add a borderless button that responds to clicks
// and feels like a hyperlink.
NSButton *button = [[MLNAttributionButton alloc] initWithAttributionInfo:info];
button.controlSize = NSControlSizeMini;
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 = attributionView.subviews.lastObject;
[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]];
[attributionView addConstraint:[NSLayoutConstraint constraintWithItem:button
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:attributionView
attribute:NSLayoutAttributeTop
multiplier:1
constant:0]];
}
if (attributionInfos.count) {
[attributionView
addConstraint:[NSLayoutConstraint constraintWithItem:attributionView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:attributionView.subviews.lastObject
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:8]];
}
}
- (void)dealloc {
[_reachability stopNotifier];
[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];
_mbglMap.reset();
_mbglView.reset();
}
- (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(MLNAnnotation)] &&
![object isKindOfClass:[MLNMultiPoint class]]) {
id<MLNAnnotation> annotation = object;
MLNAnnotationTag annotationTag = (MLNAnnotationTag)(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 mln::Point<double> point = MLNPointFromLocationCoordinate2D(annotation.coordinate);
MLNAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
_mbglMap->updateAnnotation(
annotationTag,
mln::SymbolAnnotation{point, annotationImage.styleIconIdentifier.UTF8String ?: ""});
[self updateAnnotationCallouts];
}
} else if ([keyPath isEqualToString:@"coordinates"] &&
[object isKindOfClass:[MLNMultiPoint class]]) {
MLNMultiPoint *annotation = object;
MLNAnnotationTag annotationTag = (MLNAnnotationTag)(NSUInteger)context;
// We can get here because a subclass registered itself as an observer
// of the coordinates key path of a 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]) {
_mbglMap->updateAnnotation(annotationTag, [annotation annotationObjectWithDelegate:self]);
[self updateAnnotationCallouts];
}
}
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return [key isEqualToString:@"annotations"] ? YES
: [super automaticallyNotifiesObserversForKey:key];
}
- (void)setDelegate:(id<MLNMapViewDelegate>)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
}
// MARK: Style
+ (NSSet<NSString *> *)keyPathsForValuesAffectingStyle {
return [NSSet setWithObject:@"styleURL"];
}
- (nonnull NSURL *)styleURL {
NSString *styleURLString = @(_mbglMap->getStyle().getURL().c_str()).mgl_stringOrNilIfEmpty;
return styleURLString ? [NSURL URLWithString:styleURLString] : [MLNStyle defaultStyleURL];
}
- (void)setStyleURL:(nullable NSURL *)styleURL {
if (_isTargetingInterfaceBuilder) {
return;
}
if (!styleURL) {
styleURL = [MLNStyle defaultStyleURL];
}
MLNLogDebug(@"Setting styleURL: %@", styleURL);
// An access token is required to load any default style, including Streets.
if ([[MLNSettings sharedSettings] tileServerOptionsInternal]->requiresApiKey() &&
![MLNSettings apiKey]) {
NSLog(@"Cannot set the style URL to %@ because no API key has been specified.", styleURL);
return;
}
styleURL = styleURL.mgl_URLByStandardizingScheme;
self.style = nil;
_mbglMap->getStyle().loadURL(styleURL.absoluteString.UTF8String);
}
- (IBAction)reloadStyle:(__unused id)sender {
MLNLogInfo(@"Reloading style.");
NSURL *styleURL = self.styleURL;
_mbglMap->getStyle().loadURL("");
self.styleURL = styleURL;
}
- (void)setPrefetchesTiles:(BOOL)prefetchesTiles {
_mbglMap->setPrefetchZoomDelta(prefetchesTiles ? mln::util::DEFAULT_PREFETCH_ZOOM_DELTA : 0);
}
- (BOOL)prefetchesTiles {
return _mbglMap->getPrefetchZoomDelta() > 0 ? YES : NO;
}
- (void)setTileLodMinRadius:(double)tileLodMinRadius {
_mbglMap->setTileLodMinRadius(tileLodMinRadius);
}
- (double)tileLodMinRadius {
return _mbglMap->getTileLodMinRadius();
}
- (void)setTileLodScale:(double)tileLodScale {
_mbglMap->setTileLodScale(tileLodScale);
}
- (double)tileLodScale {
return _mbglMap->getTileLodScale();
}
- (void)setTileLodPitchThreshold:(double)tileLodPitchThreshold {
_mbglMap->setTileLodPitchThreshold(tileLodPitchThreshold);
}
- (double)tileLodPitchThreshold {
return _mbglMap->getTileLodPitchThreshold();
}
- (void)setTileLodZoomShift:(double)tileLodZoomShift {
_mbglMap->setTileLodZoomShift(tileLodZoomShift);
}
- (double)tileLodZoomShift {
return _mbglMap->getTileLodZoomShift();
}
- (mln::Renderer *)renderer {
return _rendererFrontend->getRenderer();
}
// 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->getMapOptions().constrainMode() == mln::ConstrainMode::None) {
_mbglMap->setConstrainMode(mln::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;
}
- (CGLContextObj)context {
return _mbglView->getCGLContextObj();
}
- (void)setFrame:(NSRect)frame {
super.frame = frame;
if (!_isTargetingInterfaceBuilder) {
_mbglMap->setSize(self.size);
}
}
- (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:MLNOrnamentPadding]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:_zoomControls
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:MLNOrnamentPadding]];
// 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:MLNOrnamentPadding -
_logoView.image.alignmentRect.origin.y]];
[self
addConstraint:[NSLayoutConstraint constraintWithItem:_logoView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeLeading
multiplier:1
constant:MLNOrnamentPadding -
_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]];
[super updateConstraints];
}
- (void)renderSync {
if (!self.dormant && _rendererFrontend) {
_rendererFrontend->render();
if (_isPrinting) {
_isPrinting = NO;
NSImage *image = [[NSImage alloc] initWithMLNPremultipliedImage:_mbglView->readStillImage()];
[self performSelector:@selector(printWithImage:) withObject:image afterDelay:0];
}
// [self updateUserLocationAnnotationView];
}
}
- (BOOL)isTargetingInterfaceBuilder {
return _isTargetingInterfaceBuilder;
}
- (void)setNeedsRerender {
MLNAssertIsMainThread();
if (!_mbglView) {
return;
}
_mbglView->display();
}
- (void)cameraWillChangeAnimated:(BOOL)animated {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:cameraWillChangeAnimated:)]) {
[self.delegate mapView:self cameraWillChangeAnimated:animated];
}
}
- (void)cameraIsChanging {
if (!_mbglMap) {
return;
}
// 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];
}
}
- (void)cameraDidChangeAnimated:(BOOL)animated {
if (!_mbglMap) {
return;
}
// 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:)]) {
[self.delegate mapView:self cameraDidChangeAnimated:animated];
}
}
- (void)mapViewWillStartLoadingMap {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapViewWillStartLoadingMap:)]) {
[self.delegate mapViewWillStartLoadingMap:self];
}
}
- (void)mapViewDidFinishLoadingMap {
if (!_mbglMap) {
return;
}
[self.style willChangeValueForKey:@"sources"];
[self.style didChangeValueForKey:@"sources"];
[self.style willChangeValueForKey:@"layers"];
[self.style didChangeValueForKey:@"layers"];
if ([self.delegate respondsToSelector:@selector(mapViewDidFinishLoadingMap:)]) {
[self.delegate mapViewDidFinishLoadingMap:self];
}
}
- (void)mapViewDidFailLoadingMapWithError:(NSError *)error {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapViewDidFailLoadingMap:withError:)]) {
[self.delegate mapViewDidFailLoadingMap:self withError:error];
}
}
- (void)mapViewWillStartRenderingFrame {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingFrame:)]) {
[self.delegate mapViewWillStartRenderingFrame:self];
}
}
- (void)mapViewDidFinishRenderingFrameFullyRendered:(BOOL)fullyRendered
renderingStats:(const mln::gfx::RenderingStats &)stats {
if (!_mbglMap) {
return;
}
if (_isChangingAnnotationLayers) {
_isChangingAnnotationLayers = NO;
[self.style didChangeValueForKey:@"layers"];
}
if ([self.delegate respondsToSelector:@selector
(mapViewDidFinishRenderingFrame:fullyRendered:renderingStats:)]) {
if (!_renderingStats) {
_renderingStats = [[MLNRenderingStats alloc] init];
}
[_renderingStats setCoreData:stats];
[self.delegate mapViewDidFinishRenderingFrame:self
fullyRendered:fullyRendered
renderingStats:_renderingStats];
} else if ([self.delegate
respondsToSelector:@selector
(mapViewDidFinishRenderingFrame:
fullyRendered:frameEncodingTime:frameRenderingTime:)]) {
[self.delegate mapViewDidFinishRenderingFrame:self
fullyRendered:fullyRendered
frameEncodingTime:stats.encodingTime
frameRenderingTime:stats.renderingTime];
} else if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingFrame:
fullyRendered:)]) {
[self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:fullyRendered];
}
}
- (void)mapViewWillStartRenderingMap {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingMap:)]) {
[self.delegate mapViewWillStartRenderingMap:self];
}
}
- (void)mapViewDidFinishRenderingMapFullyRendered:(BOOL)fullyRendered {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)]) {
[self.delegate mapViewDidFinishRenderingMap:self fullyRendered:fullyRendered];
}
}
- (void)mapViewDidBecomeIdle {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapViewDidBecomeIdle:)]) {
[self.delegate mapViewDidBecomeIdle:self];
}
}
- (void)mapViewDidFinishLoadingStyle {
if (!_mbglMap) {
return;
}
self.style = [[MLNStyle alloc] initWithRawStyle:&_mbglMap->getStyle() stylable:self];
if ([self.delegate respondsToSelector:@selector(mapView:didFinishLoadingStyle:)]) {
[self.delegate mapView:self didFinishLoadingStyle:self.style];
}
}
- (void)sourceDidChange:(MLNSource *)source {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:sourceDidChange:)]) {
[self.delegate mapView:self sourceDidChange:source];
}
// Attribution only applies to tiled sources
if ([source isKindOfClass:[MLNTileSource class]]) {
[self installAttributionView];
}
self.needsUpdateConstraints = YES;
self.needsDisplay = YES;
}
- (BOOL)shouldRemoveStyleImage:(NSString *)imageName {
if ([self.delegate respondsToSelector:@selector(mapView:shouldRemoveStyleImage:)]) {
return [self.delegate mapView:self shouldRemoveStyleImage:imageName];
}
return YES;
}
- (void)shaderWillCompile:(NSInteger)id
backend:(NSInteger)backend
defines:(nonnull NSString *)defines {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:shaderWillCompile:backend:defines:)]) {
[self.delegate mapView:self shaderWillCompile:id backend:backend defines:defines];
}
}
- (void)shaderDidCompile:(NSInteger)id
backend:(NSInteger)backend
defines:(nonnull NSString *)defines {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:shaderDidCompile:backend:defines:)]) {
[self.delegate mapView:self shaderDidCompile:id backend:backend defines:defines];
}
}
- (void)shaderDidFailCompile:(NSInteger)id
backend:(NSInteger)backend
defines:(nonnull NSString *)defines {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:shaderDidFailCompile:backend:defines:)]) {
[self.delegate mapView:self shaderDidFailCompile:id backend:backend defines:defines];
}
}
- (void)glyphsWillLoad:(nonnull NSArray<NSString *> *)fontStack range:(NSRange)range {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:glyphsWillLoad:range:)]) {
[self.delegate mapView:self glyphsWillLoad:fontStack range:range];
}
}
- (void)glyphsDidLoad:(nonnull NSArray<NSString *> *)fontStack range:(NSRange)range {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:glyphsDidLoad:range:)]) {
[self.delegate mapView:self glyphsDidLoad:fontStack range:range];
}
}
- (void)glyphsDidError:(nonnull NSArray<NSString *> *)fontStack range:(NSRange)range {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:glyphsDidError:range:)]) {
[self.delegate mapView:self glyphsDidError:fontStack range:range];
}
}
- (void)tileDidTriggerAction:(MLNTileOperation)operation
x:(NSInteger)x
y:(NSInteger)y
z:(NSInteger)z
wrap:(NSInteger)wrap
overscaledZ:(NSInteger)overscaledZ
sourceID:(nonnull NSString *)sourceID {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector
(mapView:tileDidTriggerAction:x:y:z:wrap:overscaledZ:sourceID:)]) {
[self.delegate mapView:self
tileDidTriggerAction:operation
x:x
y:y
z:z
wrap:wrap
overscaledZ:overscaledZ
sourceID:sourceID];
}
}
- (void)spriteWillLoad:(nullable NSString *)id url:(nullable NSString *)url {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:spriteWillLoad:url:)]) {
[self.delegate mapView:self spriteWillLoad:id url:url];
}
}
- (void)spriteDidLoad:(nullable NSString *)id url:(nullable NSString *)url {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:spriteDidLoad:url:)]) {
[self.delegate mapView:self spriteDidLoad:id url:url];
}
}
- (void)spriteDidError:(nullable NSString *)id url:(nullable NSString *)url {
if (!_mbglMap) {
return;
}
if ([self.delegate respondsToSelector:@selector(mapView:spriteDidError:url:)]) {
[self.delegate mapView:self spriteDidError:id url:url];
}
}
// MARK: Printing
- (void)print:(__unused id)sender {
_isPrinting = YES;
[self setNeedsRerender];
}
- (void)printWithImage:(NSImage *)image {
NSImageView *imageView = [[NSImageView alloc] initWithFrame:self.bounds];
imageView.image = image;
NSPrintOperation *op = [NSPrintOperation printOperationWithView:imageView];
[op runOperation];
}
// MARK: Viewport
+ (NSSet<NSString *> *)keyPathsForValuesAffectingCenterCoordinate {
return [NSSet setWithObjects:@"latitude", @"longitude", @"camera", nil];
}
- (CLLocationCoordinate2D)centerCoordinate {
mln::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets);
return MLNLocationCoordinate2DFromLatLng(*_mbglMap->getCameraOptions(padding).center);
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate {
MLNLogDebug(@"Setting centerCoordinate: %@",
MLNStringFromCLLocationCoordinate2D(centerCoordinate));
[self setCenterCoordinate:centerCoordinate animated:NO];
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate animated:(BOOL)animated {
[self setCenterCoordinate:centerCoordinate animated:animated completionHandler:nil];
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
animated:(BOOL)animated
completionHandler:(nullable void (^)(void))completion {
MLNLogDebug(@"Setting centerCoordinate: %@ animated: %@",
MLNStringFromCLLocationCoordinate2D(centerCoordinate), MLNStringFromBOOL(animated));
mln::AnimationOptions animationOptions =
MLNDurationFromTimeInterval(animated ? MLNAnimationDuration : 0);
animationOptions.transitionFinishFn = ^() {
[self didChangeValueForKey:@"centerCoordinate"];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
};
[self willChangeValueForKey:@"centerCoordinate"];
_mbglMap->easeTo(mln::CameraOptions()
.withCenter(MLNLatLngFromLocationCoordinate2D(centerCoordinate))
.withPadding(MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets)),
animationOptions);
}
- (void)offsetCenterCoordinateBy:(NSPoint)delta animated:(BOOL)animated {
[self willChangeValueForKey:@"centerCoordinate"];
_mbglMap->cancelTransitions();
MLNMapCamera *oldCamera = self.camera;
_mbglMap->moveBy({delta.x, delta.y},
MLNDurationFromTimeInterval(animated ? MLNAnimationDuration : 0));
if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] &&
![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) {
self.camera = oldCamera;
}
[self didChangeValueForKey:@"centerCoordinate"];
}
- (CLLocationDegrees)pendingLatitude {
return _pendingLatitude;
}
- (void)setPendingLatitude:(CLLocationDegrees)pendingLatitude {
_pendingLatitude = pendingLatitude;
}
- (CLLocationDegrees)pendingLongitude {
return _pendingLongitude;
}
- (void)setPendingLongitude:(CLLocationDegrees)pendingLongitude {
_pendingLongitude = pendingLongitude;
}
+ (NSSet<NSString *> *)keyPathsForValuesAffectingZoomLevel {
return [NSSet setWithObject:@"camera"];
}
- (double)zoomLevel {
mln::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets);
return *_mbglMap->getCameraOptions(padding).zoom;
}
- (void)setZoomLevel:(double)zoomLevel {
MLNLogDebug(@"Setting zoomLevel: %f", zoomLevel);
[self setZoomLevel:zoomLevel animated:NO];
}
- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated {
MLNLogDebug(@"Setting zoomLevel: %f animated: %@", zoomLevel, MLNStringFromBOOL(animated));
[self willChangeValueForKey:@"zoomLevel"];
_mbglMap->easeTo(mln::CameraOptions().withZoom(zoomLevel).withPadding(
MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets)),
MLNDurationFromTimeInterval(animated ? MLNAnimationDuration : 0));
[self didChangeValueForKey:@"zoomLevel"];
}
- (void)setZoomLevel:(double)zoomLevel atPoint:(NSPoint)point animated:(BOOL)animated {
[self willChangeValueForKey:@"centerCoordinate"];
[self willChangeValueForKey:@"zoomLevel"];
MLNMapCamera *oldCamera = self.camera;
mln::ScreenCoordinate center(point.x, self.bounds.size.height - point.y);
_mbglMap->easeTo(mln::CameraOptions().withZoom(zoomLevel).withAnchor(center),
MLNDurationFromTimeInterval(animated ? MLNAnimationDuration : 0));
if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] &&
![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) {
self.camera = oldCamera;
}
[self didChangeValueForKey:@"zoomLevel"];
[self didChangeValueForKey:@"centerCoordinate"];
}
- (void)setMinimumZoomLevel:(double)minimumZoomLevel {
MLNLogDebug(@"Setting minimumZoomLevel: %f", minimumZoomLevel);
_mbglMap->setBounds(mln::BoundOptions().withMinZoom(minimumZoomLevel));
}
- (void)setMaximumZoomLevel:(double)maximumZoomLevel {
MLNLogDebug(@"Setting maximumZoomLevel: %f", maximumZoomLevel);
_mbglMap->setBounds(mln::BoundOptions().withMaxZoom(maximumZoomLevel));
}
- (double)maximumZoomLevel {
return *_mbglMap->getBounds().maxZoom;
}
- (double)minimumZoomLevel {
return *_mbglMap->getBounds().minZoom;
}
/// 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;
}
}
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDirection {
return [NSSet setWithObject:@"camera"];
}
- (CLLocationDirection)direction {
return mln::util::wrap(*_mbglMap->getCameraOptions().bearing, 0., 360.);
}
- (void)setDirection:(CLLocationDirection)direction {
MLNLogDebug(@"Setting direction: %f", direction);
[self setDirection:direction animated:NO];
}
- (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated {
MLNLogDebug(@"Setting direction: %f animated: %@", direction, MLNStringFromBOOL(animated));
[self willChangeValueForKey:@"direction"];
_mbglMap->easeTo(mln::CameraOptions().withBearing(direction).withPadding(
MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets)),
MLNDurationFromTimeInterval(animated ? MLNAnimationDuration : 0));
[self didChangeValueForKey:@"direction"];
}
- (void)offsetDirectionBy:(CLLocationDegrees)delta animated:(BOOL)animated {
[self setDirection:*_mbglMap->getCameraOptions().bearing + delta animated:animated];
}
- (CGFloat)minimumPitch {
return *_mbglMap->getBounds().minPitch;
}
- (void)setMinimumPitch:(CGFloat)minimumPitch {
MLNLogDebug(@"Setting minimumPitch: %f", minimumPitch);
_mbglMap->setBounds(mln::BoundOptions().withMinPitch(minimumPitch));
}
- (CGFloat)maximumPitch {
return *_mbglMap->getBounds().maxPitch;
}
- (void)setMaximumPitch:(CGFloat)maximumPitch {
MLNLogDebug(@"Setting maximumPitch: %f", maximumPitch);
_mbglMap->setBounds(mln::BoundOptions().withMaxPitch(maximumPitch));
}
+ (NSSet<NSString *> *)keyPathsForValuesAffectingCamera {
return [NSSet setWithObjects:@"latitude", @"longitude", @"centerCoordinate", @"zoomLevel",
@"direction", nil];
}
- (MLNMapCamera *)camera {
mln::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets);
return [self cameraForCameraOptions:_mbglMap->getCameraOptions(padding)];
}
- (void)setCamera:(MLNMapCamera *)camera {
MLNLogDebug(@"Setting camera: %@", camera);
[self setCamera:camera animated:NO];
}
- (void)setCamera:(MLNMapCamera *)camera animated:(BOOL)animated {
MLNLogDebug(@"Setting camera: %@ animated: %@", camera, MLNStringFromBOOL(animated));
[self setCamera:camera
withDuration:animated ? MLNAnimationDuration : 0
animationTimingFunction:nil
completionHandler:NULL];
}
- (void)setCamera:(MLNMapCamera *)camera
withDuration:(NSTimeInterval)duration
animationTimingFunction:(nullable CAMediaTimingFunction *)function
completionHandler:(nullable void (^)(void))completion {
MLNLogDebug(@"Setting camera: %@ duration: %f animationTimingFunction: %@", camera, duration,
function);
[self setCamera:camera
withDuration:duration
animationTimingFunction:function
edgePadding:NSEdgeInsetsZero
completionHandler:completion];
}
- (void)setCamera:(MLNMapCamera *)camera
withDuration:(NSTimeInterval)duration
animationTimingFunction:(nullable CAMediaTimingFunction *)function
edgePadding:(NSEdgeInsets)edgePadding
completionHandler:(nullable void (^)(void))completion {
edgePadding = MLNEdgeInsetsInsetEdgeInset(edgePadding, self.contentInsets);
mln::AnimationOptions animationOptions;
if (duration > 0) {
animationOptions.duration.emplace(MLNDurationFromTimeInterval(duration));
animationOptions.easing.emplace(MLNUnitBezierForMediaTimingFunction(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();
});
};
}
if ([self.camera isEqualToMapCamera:camera]) {
if (completion) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
completion();
});
}
return;
}
[self willChangeValueForKey:@"camera"];
_mbglMap->cancelTransitions();
mln::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera
edgePadding:edgePadding];
_mbglMap->easeTo(cameraOptions, animationOptions);
[self didChangeValueForKey:@"camera"];
}
- (void)flyToCamera:(MLNMapCamera *)camera completionHandler:(nullable void (^)(void))completion {
MLNLogDebug(@"Setting flyToCamera: %@ completionHandler: %@", camera, completion);
[self flyToCamera:camera withDuration:-1 completionHandler:completion];
}
- (void)flyToCamera:(MLNMapCamera *)camera
withDuration:(NSTimeInterval)duration
completionHandler:(nullable void (^)(void))completion {
MLNLogDebug(@"Setting flyToCamera: %@ withDuration: %f completionHandler: %@", camera, duration,
completion);
[self flyToCamera:camera withDuration:duration peakAltitude:-1 completionHandler:completion];
}
- (void)flyToCamera:(MLNMapCamera *)camera
withDuration:(NSTimeInterval)duration
peakAltitude:(CLLocationDistance)peakAltitude
completionHandler:(nullable void (^)(void))completion {
MLNLogDebug(@"Setting flyToCamera: %@ withDuration: %f peakAltitude: %f completionHandler: %@",
camera, duration, peakAltitude, completion);
mln::AnimationOptions animationOptions;
if (duration >= 0) {
animationOptions.duration = MLNDurationFromTimeInterval(duration);
}
if (peakAltitude >= 0) {
CLLocationDegrees peakLatitude =
(self.centerCoordinate.latitude + camera.centerCoordinate.latitude) / 2;
CLLocationDegrees peakPitch = (self.camera.pitch + camera.pitch) / 2;
animationOptions.minZoom =
MLNZoomLevelForAltitude(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();
});
};
}
if ([self.camera isEqualToMapCamera:camera]) {
if (completion) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
completion();
});
}
return;
}
[self willChangeValueForKey:@"camera"];
_mbglMap->cancelTransitions();
mln::CameraOptions cameraOptions =
[self cameraOptionsObjectForAnimatingToCamera:camera edgePadding:self.contentInsets];
_mbglMap->flyTo(cameraOptions, animationOptions);
[self didChangeValueForKey:@"camera"];
}
/// Returns a CameraOptions object that specifies parameters for animating to
/// the given camera.
- (mln::CameraOptions)cameraOptionsObjectForAnimatingToCamera:(MLNMapCamera *)camera
edgePadding:(NSEdgeInsets)edgePadding {
mln::CameraOptions options;
options.center = MLNLatLngFromLocationCoordinate2D(camera.centerCoordinate);
options.padding = MLNEdgeInsetsFromNSEdgeInsets(edgePadding);
options.zoom = MLNZoomLevelForAltitude(camera.altitude, camera.pitch,
camera.centerCoordinate.latitude, self.frame.size);
if (camera.heading >= 0) {
options.bearing = camera.heading;
}
if (camera.pitch >= 0) {
options.pitch = camera.pitch;
}
return options;
}
+ (NSSet *)keyPathsForValuesAffectingVisibleCoordinateBounds {
return [NSSet setWithObjects:@"centerCoordinate", @"zoomLevel", @"direction", @"bounds", nil];
}
- (MLNCoordinateBounds)visibleCoordinateBounds {
return [self convertRect:self.bounds toCoordinateBoundsFromView:self];
}
- (void)setVisibleCoordinateBounds:(MLNCoordinateBounds)bounds {
MLNLogDebug(@"Setting visibleCoordinateBounds: %@", MLNStringFromCoordinateBounds(bounds));
[self setVisibleCoordinateBounds:bounds animated:NO];
}
- (void)setVisibleCoordinateBounds:(MLNCoordinateBounds)bounds animated:(BOOL)animated {
MLNLogDebug(@"Setting visibleCoordinateBounds: %@ animated: %@",
MLNStringFromCoordinateBounds(bounds), MLNStringFromBOOL(animated));
[self setVisibleCoordinateBounds:bounds edgePadding:NSEdgeInsetsZero animated:animated];
}
- (void)setVisibleCoordinateBounds:(MLNCoordinateBounds)bounds
edgePadding:(NSEdgeInsets)insets
animated:(BOOL)animated {
[self setVisibleCoordinateBounds:bounds
edgePadding:insets
animated:animated
completionHandler:nil];
}
- (void)setVisibleCoordinateBounds:(MLNCoordinateBounds)bounds
edgePadding:(NSEdgeInsets)insets
animated:(BOOL)animated
completionHandler:(nullable void (^)(void))completion {
_mbglMap->cancelTransitions();
mln::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets);
mln::CameraOptions cameraOptions =
_mbglMap->cameraForLatLngBounds(MLNLatLngBoundsFromCoordinateBounds(bounds), padding);
mln::AnimationOptions animationOptions;
if (animated) {
animationOptions.duration = MLNDurationFromTimeInterval(MLNAnimationDuration);
}
MLNMapCamera *camera = [self cameraForCameraOptions:cameraOptions];
if ([self.camera isEqualToMapCamera:camera]) {
completion();
return;
}
[self willChangeValueForKey:@"visibleCoordinateBounds"];
animationOptions.transitionFinishFn = ^() {
[self didChangeValueForKey:@"visibleCoordinateBounds"];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
};
_mbglMap->easeTo(cameraOptions, animationOptions);
}
- (MLNMapCamera *)cameraThatFitsCoordinateBounds:(MLNCoordinateBounds)bounds {
return [self cameraThatFitsCoordinateBounds:bounds edgePadding:NSEdgeInsetsZero];
}
- (MLNMapCamera *)cameraThatFitsCoordinateBounds:(MLNCoordinateBounds)bounds
edgePadding:(NSEdgeInsets)insets {
mln::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets);
mln::CameraOptions cameraOptions =
_mbglMap->cameraForLatLngBounds(MLNLatLngBoundsFromCoordinateBounds(bounds), padding);
return [self cameraForCameraOptions:cameraOptions];
}
- (MLNMapCamera *)camera:(MLNMapCamera *)camera
fittingCoordinateBounds:(MLNCoordinateBounds)bounds
edgePadding:(NSEdgeInsets)insets {
mln::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets);
MLNMapCamera *currentCamera = self.camera;
CGFloat pitch = camera.pitch < 0 ? currentCamera.pitch : camera.pitch;
CLLocationDirection direction = camera.heading < 0 ? currentCamera.heading : camera.heading;
mln::CameraOptions cameraOptions = _mbglMap->cameraForLatLngBounds(
MLNLatLngBoundsFromCoordinateBounds(bounds), padding, direction, pitch);
return [self cameraForCameraOptions:cameraOptions];
}
- (MLNMapCamera *)camera:(MLNMapCamera *)camera
fittingShape:(MLNShape *)shape
edgePadding:(NSEdgeInsets)insets {
mln::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets);
MLNMapCamera *currentCamera = self.camera;
CGFloat pitch = camera.pitch < 0 ? currentCamera.pitch : camera.pitch;
CLLocationDirection direction = camera.heading < 0 ? currentCamera.heading : camera.heading;
mln::CameraOptions cameraOptions =
_mbglMap->cameraForGeometry([shape geometryObject], padding, direction, pitch);
return [self cameraForCameraOptions:cameraOptions];
}
- (MLNMapCamera *)cameraThatFitsShape:(MLNShape *)shape
direction:(CLLocationDirection)direction
edgePadding:(NSEdgeInsets)insets {
mln::EdgeInsets padding = MLNEdgeInsetsFromNSEdgeInsets(insets);
padding += MLNEdgeInsetsFromNSEdgeInsets(self.contentInsets);
mln::CameraOptions cameraOptions =
_mbglMap->cameraForGeometry([shape geometryObject], padding, direction);
return [self cameraForCameraOptions:cameraOptions];
}
- (MLNMapCamera *)cameraForCameraOptions:(const mln::CameraOptions &)cameraOptions {
mln::CameraOptions mapCamera = _mbglMap->getCameraOptions();
CLLocationCoordinate2D centerCoordinate = MLNLocationCoordinate2DFromLatLng(
cameraOptions.center ? *cameraOptions.center : *mapCamera.center);
double zoomLevel = cameraOptions.zoom ? *cameraOptions.zoom : self.zoomLevel;
CLLocationDirection direction =
cameraOptions.bearing ? mln::util::wrap(*cameraOptions.bearing, 0., 360.) : self.direction;
CGFloat pitch = cameraOptions.pitch ? *cameraOptions.pitch : *mapCamera.pitch;
CLLocationDistance altitude =
MLNAltitudeForZoomLevel(zoomLevel, pitch, centerCoordinate.latitude, self.frame.size);
return [MLNMapCamera cameraLookingAtCenterCoordinate:centerCoordinate
altitude: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 & NSWindowStyleMaskFullSizeContentView) &&
!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), MAX(NSMinX(contentLayoutRect), 0),
MAX(NSMinY(contentLayoutRect), 0), NSWidth(self.bounds) - NSMaxX(contentLayoutRect));
}
} else {
contentInsets = NSEdgeInsetsZero;
}
self.contentInsets = contentInsets;
}
- (void)setContentInsets:(NSEdgeInsets)contentInsets {
[self setContentInsets:contentInsets animated:NO completionHandler:nil];
}
- (void)setContentInsets:(NSEdgeInsets)contentInsets animated:(BOOL)animated {
[self setContentInsets:contentInsets animated:animated completionHandler:nil];
}
- (void)setContentInsets:(NSEdgeInsets)contentInsets
animated:(BOOL)animated
completionHandler:(nullable void (^)(void))completion {
if (NSEdgeInsetsEqual(contentInsets, self.contentInsets)) {
if (completion) {
completion();
}
return;
}
MLNLogDebug(@"Setting contentInset: %@ animated:", MLNStringFromNSEdgeInsets(contentInsets),
MLNStringFromBOOL(animated));
// 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 completionHandler:completion];
}
// 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:{cursorPoint, NSZeroSize}].origin;
cursorPoint.y = self.window.screen.frame.size.height - cursorPoint.y;
CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cursorPoint);
CGDisplayShowCursor(kCGDirectMainDisplay);
}
}
if (flags & NSEventModifierFlagShift) {
// Shift-drag to zoom.
if (!self.zoomEnabled) {
return;
}
_mbglMap->cancelTransitions();
if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
_zoomAtBeginningOfGesture = [self zoomLevel];
} else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
CGFloat newZoomLevel = _zoomAtBeginningOfGesture - delta.y / 75;
[self setZoomLevel:newZoomLevel atPoint:startPoint animated:NO];
}
} else if (flags & NSEventModifierFlagOption) {
// Option-drag to rotate and/or tilt.
_mbglMap->cancelTransitions();
if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
_directionAtBeginningOfGesture = self.direction;
_pitchAtBeginningOfGesture = *_mbglMap->getCameraOptions().pitch;
} else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
MLNMapCamera *oldCamera = self.camera;
BOOL didChangeCamera = NO;
mln::ScreenCoordinate center(startPoint.x, self.bounds.size.height - startPoint.y);
if (self.rotateEnabled) {
CLLocationDirection newDirection = _directionAtBeginningOfGesture - delta.x / 10;
[self willChangeValueForKey:@"direction"];
_mbglMap->jumpTo(mln::CameraOptions().withBearing(newDirection).withAnchor(center));
didChangeCamera = YES;
[self didChangeValueForKey:@"direction"];
}
if (self.pitchEnabled) {
_mbglMap->jumpTo(mln::CameraOptions()
.withPitch(_pitchAtBeginningOfGesture + delta.y / 5)
.withAnchor(center));
didChangeCamera = YES;
}
if (didChangeCamera &&
[self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] &&
![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) {
self.camera = oldCamera;
}
}
} 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 & NSEventModifierFlagShift || flags & NSEventModifierFlagOption));
}
/// Pinch to zoom.
- (void)handleMagnificationGesture:(NSMagnificationGestureRecognizer *)gestureRecognizer {
if (!self.zoomEnabled) {
return;
}
_mbglMap->cancelTransitions();
if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
_mbglMap->setGestureInProgress(true);
_zoomAtBeginningOfGesture = [self zoomLevel];
} else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
NSPoint zoomInPoint = [gestureRecognizer locationInView:self];
mln::ScreenCoordinate center(zoomInPoint.x, self.bounds.size.height - zoomInPoint.y);
if (gestureRecognizer.magnification > -1) {
[self willChangeValueForKey:@"zoomLevel"];
[self willChangeValueForKey:@"centerCoordinate"];
MLNMapCamera *oldCamera = self.camera;
_mbglMap->jumpTo(
mln::CameraOptions()
.withZoom(_zoomAtBeginningOfGesture + log2(1 + gestureRecognizer.magnification))
.withAnchor(center));
if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] &&
![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) {
self.camera = oldCamera;
}
[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];
MLNAnnotationTag hitAnnotationTag = [self annotationTagAtPoint:gesturePoint
persistingResults:YES];
if (hitAnnotationTag != MLNAnnotationTagNotFound) {
if (hitAnnotationTag != _selectedAnnotationTag) {
id<MLNAnnotation> annotation = [self annotationWithTag:hitAnnotationTag];
NSAssert(annotation, @"Cannot select nonexistent annotation with tag %llu", hitAnnotationTag);
[self selectAnnotation:annotation atPoint:gesturePoint];
}
} else {
[self deselectAnnotation:self.selectedAnnotation];
}
}
/// Right-click to show the context menu.
- (void)handleRightClickGesture:(NSClickGestureRecognizer *)gestureRecognizer {
NSMenu *menu = [self menuForEvent:[NSApp currentEvent]];
if (menu) {
[NSMenu popUpContextMenu:menu withEvent:[NSApp currentEvent] forView:self];
}
}
/// 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 setZoomLevel:round(self.zoomLevel) + 1 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 setZoomLevel:round(self.zoomLevel) - 1 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) {
MLNMapCamera *oldCamera = self.camera;
NSPoint rotationPoint = [gestureRecognizer locationInView:self];
mln::ScreenCoordinate anchor(rotationPoint.x, self.bounds.size.height - rotationPoint.y);
_mbglMap->jumpTo(
mln::CameraOptions()
.withBearing(_directionAtBeginningOfGesture + gestureRecognizer.rotationInDegrees)
.withAnchor(anchor));
if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] &&
![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) {
self.camera = oldCamera;
}
} 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:MLNScrollWheelZoomsMapViewDefaultKey]) {
// 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 setZoomLevel:self.zoomLevel + log2(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;
}
// MARK: NSGestureRecognizerDelegate methods
- (BOOL)gestureRecognizer:(NSGestureRecognizer *)gestureRecognizer
shouldAttemptToRecognizeWithEvent:(NSEvent *)event {
if (gestureRecognizer == _singleClickRecognizer) {
if (!self.selectedAnnotation) {
NSPoint gesturePoint = [self convertPoint:[event locationInWindow] fromView:nil];
MLNAnnotationTag hitAnnotationTag = [self annotationTagAtPoint:gesturePoint
persistingResults:NO];
if (hitAnnotationTag == MLNAnnotationTagNotFound) {
return NO;
}
}
}
return YES;
}
// MARK: Keyboard events
- (void)keyDown:(NSEvent *)event {
// 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 ]];
}
// The following action methods are declared in NSResponder.h.
- (void)insertTab:(id)sender {
if (self.window.firstResponder == self) {
[self.window selectNextKeyView:self];
}
}
- (void)insertBacktab:(id)sender {
if (self.window.firstResponder == self) {
[self.window selectPreviousKeyView:self];
}
}
- (void)insertText:(NSString *)insertString {
switch (insertString.length == 1 ? [insertString characterAtIndex:0] : 0) {
case '-':
[self moveToEndOfParagraph:nil];
break;
case '+':
case '=':
[self moveToBeginningOfParagraph:nil];
break;
default:
[super insertText:insertString];
break;
}
}
- (IBAction)moveUp:(__unused id)sender {
[self offsetCenterCoordinateBy:NSMakePoint(0, MLNKeyPanningIncrement) animated:YES];
}
- (IBAction)moveDown:(__unused id)sender {
[self offsetCenterCoordinateBy:NSMakePoint(0, -MLNKeyPanningIncrement) animated:YES];
}
- (IBAction)moveLeft:(__unused id)sender {
[self offsetCenterCoordinateBy:NSMakePoint(MLNKeyPanningIncrement, 0) animated:YES];
}
- (IBAction)moveRight:(__unused id)sender {
[self offsetCenterCoordinateBy:NSMakePoint(-MLNKeyPanningIncrement, 0) animated:YES];
}
- (IBAction)moveToBeginningOfParagraph:(__unused id)sender {
if (self.zoomEnabled) {
[self setZoomLevel:round(self.zoomLevel) + 1 animated:YES];
}
}
- (IBAction)moveToEndOfParagraph:(__unused id)sender {
if (self.zoomEnabled) {
[self setZoomLevel:round(self.zoomLevel) - 1 animated:YES];
}
}
- (IBAction)moveWordLeft:(__unused id)sender {
if (self.rotateEnabled) {
[self offsetDirectionBy:MLNKeyRotationIncrement animated:YES];
}
}
- (IBAction)moveWordRight:(__unused id)sender {
if (self.rotateEnabled) {
[self offsetDirectionBy:-MLNKeyRotationIncrement 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;
}
// 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];
}
// MARK: Annotations
- (nullable NSArray<id<MLNAnnotation>> *)annotations {
if (_annotationContextsByAnnotationTag.empty()) {
return nil;
}
// Map all the annotation tags to the annotations themselves.
std::vector<id<MLNAnnotation>> annotations;
std::transform(_annotationContextsByAnnotationTag.begin(),
_annotationContextsByAnnotationTag.end(), std::back_inserter(annotations),
^id<MLNAnnotation>(const std::pair<MLNAnnotationTag, MLNAnnotationContext> &pair) {
return pair.second.annotation;
});
return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()];
}
- (nullable NSArray<id<MLNAnnotation>> *)visibleAnnotations {
return [self visibleAnnotationsInRect:self.bounds];
}
- (nullable NSArray<id<MLNAnnotation>> *)visibleAnnotationsInRect:(CGRect)rect {
if (_annotationContextsByAnnotationTag.empty()) {
return nil;
}
std::vector<MLNAnnotationTag> annotationTags = [self annotationTagsInRect:rect];
std::vector<MLNAnnotationTag> shapeAnnotationTags = [self shapeAnnotationTagsInRect:rect];
if (shapeAnnotationTags.size()) {
annotationTags.insert(annotationTags.end(), shapeAnnotationTags.begin(),
shapeAnnotationTags.end());
}
if (annotationTags.size()) {
NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:annotationTags.size()];
for (auto const &annotationTag : annotationTags) {
if (!_annotationContextsByAnnotationTag.count(annotationTag) ||
annotationTag == MLNAnnotationTagNotFound) {
continue;
}
MLNAnnotationContext annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
NSAssert(annotationContext.annotation, @"Missing annotation for tag %llu.", annotationTag);
if (annotationContext.annotation) {
[annotations addObject:annotationContext.annotation];
}
}
return [annotations copy];
}
return nil;
}
/// Returns the annotation assigned the given tag. Cheap.
- (id<MLNAnnotation>)annotationWithTag:(MLNAnnotationTag)tag {
if (!_annotationContextsByAnnotationTag.count(tag) || tag == MLNAnnotationTagNotFound) {
return nil;
}
MLNAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(tag);
return annotationContext.annotation;
}
/// Returns the annotation tag assigned to the given annotation.
- (MLNAnnotationTag)annotationTagForAnnotation:(id<MLNAnnotation>)annotation {
if (!annotation || _annotationTagsByAnnotation.count(annotation) == 0) {
return MLNAnnotationTagNotFound;
}
return _annotationTagsByAnnotation.at(annotation);
}
- (void)addAnnotation:(id<MLNAnnotation>)annotation {
if (annotation) {
[self addAnnotations:@[ annotation ]];
}
}
- (void)addAnnotations:(NSArray<id<MLNAnnotation>> *)annotations {
if (!annotations) {
return;
}
[self willChangeValueForKey:@"annotations"];
BOOL delegateHasImagesForAnnotations =
[self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)];
for (id<MLNAnnotation> annotation in annotations) {
NSAssert([annotation conformsToProtocol:@protocol(MLNAnnotation)],
@"Annotation does not conform to MLNAnnotation");
// adding the same annotation object twice is a no-op
if (_annotationTagsByAnnotation.count(annotation) != 0) {
continue;
}
if ([annotation isKindOfClass:[MLNMultiPoint class]]) {
// The multipoint knows how to style itself (with the map view’s help).
MLNMultiPoint *multiPoint = (MLNMultiPoint *)annotation;
if (!multiPoint.pointCount) {
continue;
}
_isChangingAnnotationLayers = YES;
MLNAnnotationTag annotationTag =
_mbglMap->addAnnotation([multiPoint annotationObjectWithDelegate:self]);
MLNAnnotationContext context;
context.annotation = annotation;
_annotationContextsByAnnotationTag[annotationTag] = context;
_annotationTagsByAnnotation[annotation] = annotationTag;
[(NSObject *)annotation addObserver:self
forKeyPath:@"coordinates"
options:0
context:(void *)(NSUInteger)annotationTag];
} else if (![annotation isKindOfClass:[MLNMultiPolyline class]] &&
![annotation isKindOfClass:[MLNMultiPolygon class]] &&
![annotation isKindOfClass:[MLNShapeCollection class]] &&
![annotation isKindOfClass:[MLNPointCollection class]]) {
MLNAnnotationImage *annotationImage = nil;
if (delegateHasImagesForAnnotations) {
annotationImage = [self.delegate mapView:self imageForAnnotation:annotation];
}
if (!annotationImage) {
annotationImage =
[self dequeueReusableAnnotationImageWithIdentifier:MLNDefaultStyleMarkerSymbolName];
}
if (!annotationImage) {
annotationImage = self.defaultAnnotationImage;
}
NSString *symbolName = annotationImage.styleIconIdentifier;
if (!symbolName) {
symbolName =
[MLNAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
annotationImage.styleIconIdentifier = symbolName;
}
if (!self.annotationImagesByIdentifier[annotationImage.reuseIdentifier]) {
self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage;
[self installAnnotationImage:annotationImage];
}
MLNAnnotationTag annotationTag = _mbglMap->addAnnotation(mln::SymbolAnnotation{
MLNPointFromLocationCoordinate2D(annotation.coordinate), symbolName.UTF8String ?: ""});
MLNAnnotationContext context;
context.annotation = annotation;
context.imageReuseIdentifier = annotationImage.reuseIdentifier;
_annotationContextsByAnnotationTag[annotationTag] = context;
_annotationTagsByAnnotation[annotation] = annotationTag;
if ([annotation isKindOfClass:[NSObject class]]) {
NSAssert(![annotation isKindOfClass:[MLNMultiPoint class]],
@"Point annotation should not be MLNMultiPoint.");
[(NSObject *)annotation addObserver:self
forKeyPath:@"coordinate"
options:0
context:(void *)(NSUInteger)annotationTag];
}
// Opt into potentially expensive tooltip tracking areas.
if ([annotation respondsToSelector:@selector(toolTip)] && annotation.toolTip.length) {
_wantsToolTipRects = YES;
}
}
}
[self didChangeValueForKey:@"annotations"];
if (_isChangingAnnotationLayers) {
[self.style willChangeValueForKey:@"layers"];
}
[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.
- (MLNAnnotationImage *)defaultAnnotationImage {
NSImage *image = MLNDefaultMarkerImage();
NSRect alignmentRect = image.alignmentRect;
alignmentRect.origin.y = NSMidY(alignmentRect);
alignmentRect.size.height /= 2;
image.alignmentRect = alignmentRect;
return [MLNAnnotationImage annotationImageWithImage:image
reuseIdentifier:MLNDefaultStyleMarkerSymbolName];
}
/// Sends the raw pixel data of the annotation image’s image to mbgl and
/// calculates state needed for hit testing later.
- (void)installAnnotationImage:(MLNAnnotationImage *)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;
}
_mbglMap->addAnnotationImage([annotationImage.image mgl_styleImageWithIdentifier:iconIdentifier]);
// 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<MLNAnnotation>)annotation {
if (annotation) {
[self removeAnnotations:@[ annotation ]];
}
}
- (void)removeAnnotations:(NSArray<id<MLNAnnotation>> *)annotations {
if (!annotations) {
return;
}
[self willChangeValueForKey:@"annotations"];
for (id<MLNAnnotation> annotation in annotations) {
NSAssert([annotation conformsToProtocol:@protocol(MLNAnnotation)],
@"Annotation does not conform to MLNAnnotation");
MLNAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
NSAssert(annotationTag != MLNAnnotationTagNotFound, @"No ID for annotation %@", annotation);
if (annotationTag == _selectedAnnotationTag) {
[self deselectAnnotation:annotation];
}
if (annotationTag == _lastSelectedAnnotationTag) {
_lastSelectedAnnotationTag = MLNAnnotationTagNotFound;
}
_annotationContextsByAnnotationTag.erase(annotationTag);
_annotationTagsByAnnotation.erase(annotation);
if ([annotation isKindOfClass:[NSObject class]] &&
![annotation isKindOfClass:[MLNMultiPoint class]]) {
[(NSObject *)annotation removeObserver:self
forKeyPath:@"coordinate"
context:(void *)(NSUInteger)annotationTag];
} else if ([annotation isKindOfClass:[MLNMultiPoint class]]) {
[(NSObject *)annotation removeObserver:self
forKeyPath:@"coordinates"
context:(void *)(NSUInteger)annotationTag];
}
_isChangingAnnotationLayers = YES;
_mbglMap->removeAnnotation(annotationTag);
}
[self didChangeValueForKey:@"annotations"];
if (_isChangingAnnotationLayers) {
[self.style willChangeValueForKey:@"layers"];
}
[self updateAnnotationTrackingAreas];
}
- (nullable MLNAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:
(NSString *)identifier {
return self.annotationImagesByIdentifier[identifier];
}
- (id<MLNAnnotation>)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?”
*/
- (MLNAnnotationTag)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, -MLNAnnotationImagePaddingForHitTest,
-MLNAnnotationImagePaddingForHitTest);
std::vector<MLNAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect];
std::vector<MLNAnnotationTag> nearbyShapeAnnotations = [self shapeAnnotationTagsInRect:queryRect];
if (nearbyShapeAnnotations.size()) {
nearbyAnnotations.insert(nearbyAnnotations.end(), nearbyShapeAnnotations.begin(),
nearbyShapeAnnotations.end());
}
if (nearbyAnnotations.size()) {
// Assume that the user is fat-fingering an annotation.
NSRect hitRect = NSInsetRect({point, NSZeroSize}, -MLNAnnotationImagePaddingForHitTest,
-MLNAnnotationImagePaddingForHitTest);
// Filter out any annotation whose image is unselectable or for which
// hit testing fails.
auto end = std::remove_if(
nearbyAnnotations.begin(), nearbyAnnotations.end(),
[&](const MLNAnnotationTag annotationTag) {
id<MLNAnnotation> annotation = [self annotationWithTag:annotationTag];
NSAssert(annotation, @"Unknown annotation found nearby click");
if (!annotation) {
return true;
}
if ([annotation isKindOfClass:[MLNMultiPoint class]]) {
if ([self.delegate respondsToSelector:@selector(mapView:shapeAnnotationIsEnabled:)]) {
return !!(![self.delegate mapView:self
shapeAnnotationIsEnabled:(MLNMultiPoint *)annotation]);
} else {
return false;
}
}
MLNAnnotationImage *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));
}
MLNAnnotationTag hitAnnotationTag = MLNAnnotationTagNotFound;
if (nearbyAnnotations.size()) {
// The first selection in the cycle should be the one nearest to the
// tap. Also the annotation tags need to be stable in order to compare them with
// the remembered tags _annotationsNearbyLastClick.
CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self];
std::sort(
nearbyAnnotations.begin(), nearbyAnnotations.end(),
[&](const MLNAnnotationTag tagA, const MLNAnnotationTag tagB) {
CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate];
CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate];
CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude,
coordinateA.longitude - currentCoordinate.longitude);
CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude,
coordinateB.longitude - currentCoordinate.longitude);
return deltaA < deltaB;
});
if (nearbyAnnotations == _annotationsNearbyLastClick) {
// 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 == MLNAnnotationTagNotFound ||
_lastSelectedAnnotationTag == nearbyAnnotations.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 = nearbyAnnotations.front();
} else {
auto result = std::find(nearbyAnnotations.begin(), nearbyAnnotations.end(),
_lastSelectedAnnotationTag);
if (result == nearbyAnnotations.end()) {
// An annotation from this set hasn’t been selected before.
// Select the first (nearest) one.
hitAnnotationTag = nearbyAnnotations.front();
} else {
// Step to the next annotation in the set.
auto distance = std::distance(nearbyAnnotations.begin(), result);
hitAnnotationTag = nearbyAnnotations[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<MLNAnnotationTag>)annotationTagsInRect:(NSRect)rect {
// Cocoa origin is at the lower-left corner.
return self.renderer->queryPointAnnotations({
{NSMinX(rect), NSHeight(self.bounds) - NSMaxY(rect)},
{NSMaxX(rect), NSHeight(self.bounds) - NSMinY(rect)},
});
}
- (std::vector<MLNAnnotationTag>)shapeAnnotationTagsInRect:(NSRect)rect {
// Cocoa origin is at the lower-left corner.
return _rendererFrontend->getRenderer()->queryShapeAnnotations({
{NSMinX(rect), NSHeight(self.bounds) - NSMaxY(rect)},
{NSMaxX(rect), NSHeight(self.bounds) - NSMinY(rect)},
});
}
- (id<MLNAnnotation>)selectedAnnotation {
if (!_annotationContextsByAnnotationTag.count(_selectedAnnotationTag) ||
_selectedAnnotationTag == MLNAnnotationTagNotFound) {
return nil;
}
MLNAnnotationContext &annotationContext =
_annotationContextsByAnnotationTag.at(_selectedAnnotationTag);
return annotationContext.annotation;
}
- (void)setSelectedAnnotation:(id<MLNAnnotation>)annotation {
MLNLogDebug(@"Selecting annotation: %@", annotation);
[self willChangeValueForKey:@"selectedAnnotations"];
_selectedAnnotationTag = [self annotationTagForAnnotation:annotation];
if (_selectedAnnotationTag != MLNAnnotationTagNotFound) {
_lastSelectedAnnotationTag = _selectedAnnotationTag;
}
[self didChangeValueForKey:@"selectedAnnotations"];
}
- (NSArray<id<MLNAnnotation>> *)selectedAnnotations {
id<MLNAnnotation> selectedAnnotation = self.selectedAnnotation;
return selectedAnnotation ? @[ selectedAnnotation ] : @[];
}
- (void)setSelectedAnnotations:(NSArray<id<MLNAnnotation>> *)selectedAnnotations {
MLNLogDebug(@"Selecting: %lu annotations", selectedAnnotations.count);
if (!selectedAnnotations.count) {
return;
}
id<MLNAnnotation> firstAnnotation = selectedAnnotations[0];
NSAssert([firstAnnotation conformsToProtocol:@protocol(MLNAnnotation)],
@"Annotation does not conform to MLNAnnotation");
if ([firstAnnotation isKindOfClass:[MLNMultiPoint class]]) {
return;
}
[self selectAnnotation:firstAnnotation];
}
- (BOOL)isMovingAnnotationIntoViewSupportedForAnnotation:(id<MLNAnnotation>)annotation
animated:(BOOL)animated {
// Consider delegating
return [annotation isKindOfClass:[MLNPointAnnotation class]];
}
- (void)selectAnnotation:(id<MLNAnnotation>)annotation {
MLNLogDebug(@"Selecting annotation: %@", annotation);
[self selectAnnotation:annotation atPoint:NSZeroPoint];
}
- (void)selectAnnotation:(id<MLNAnnotation>)annotation atPoint:(NSPoint)gesturePoint {
MLNLogDebug(@"Selecting annotation: %@ atPoint: %@", annotation, NSStringFromPoint(gesturePoint));
[self selectAnnotation:annotation atPoint:gesturePoint moveIntoView:YES animateSelection:YES];
}
- (void)selectAnnotation:(id<MLNAnnotation>)annotation
atPoint:(NSPoint)gesturePoint
moveIntoView:(BOOL)moveIntoView
animateSelection:(BOOL)animateSelection {
MLNLogDebug(@"Selecting annotation: %@ atPoint: %@ moveIntoView: %@ animateSelection: %@",
annotation, NSStringFromPoint(gesturePoint), MLNStringFromBOOL(moveIntoView),
MLNStringFromBOOL(animateSelection));
id<MLNAnnotation> 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.
MLNAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
if (annotationTag == MLNAnnotationTagNotFound) {
[self addAnnotation:annotation];
}
if (moveIntoView) {
moveIntoView = [self isMovingAnnotationIntoViewSupportedForAnnotation:annotation
animated:animateSelection];
}
// The annotation's anchor will bounce to the current click.
NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag];
// Check for invalid (zero) positioning rect
if (NSEqualRects(positioningRect, NSZeroRect)) {
CLLocationCoordinate2D origin = annotation.coordinate;
positioningRect.origin = [self convertCoordinate:origin toPointToView:self];
}
BOOL shouldShowCallout = ([annotation respondsToSelector:@selector(title)] && annotation.title &&
!self.calloutForSelectedAnnotation.shown &&
[self.delegate respondsToSelector:@selector(mapView:
annotationCanShowCallout:)] &&
[self.delegate mapView:self annotationCanShowCallout:annotation]);
if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) {
if (!moveIntoView && !NSEqualPoints(gesturePoint, NSZeroPoint)) {
positioningRect = CGRectMake(gesturePoint.x, gesturePoint.y, positioningRect.size.width,
positioningRect.size.height);
}
}
// Onscreen or partially on-screen
else if (!shouldShowCallout) {
moveIntoView = NO;
}
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 (shouldShowCallout) {
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);
// The following will do nothing if the positioning rect is not on-screen. See
// ``MLNMapView/updateAnnotationCallouts`` for presenting the callout when the selected
// annotation comes back on-screen.
[callout showRelativeToRect:positioningRect ofView:self preferredEdge:edge];
}
if (moveIntoView) {
moveIntoView = NO;
NSRect (^edgeInsetsInsetRect)(NSRect, NSEdgeInsets) = ^(NSRect rect, NSEdgeInsets insets) {
return NSMakeRect(rect.origin.x + insets.left, rect.origin.y + insets.bottom,
rect.size.width - insets.left - insets.right,
rect.size.height - insets.top - insets.bottom);
};
// Add padding around the positioning rect (in essence an inset from the edge of the viewport
NSRect expandedPositioningRect = positioningRect;
if (shouldShowCallout) {
// If we have a callout, expand this rect to include a buffer
expandedPositioningRect =
edgeInsetsInsetRect(positioningRect, MLNMapViewOffscreenAnnotationPadding);
}
// Used for callout positioning, and moving offscreen annotations onscreen.
CGRect constrainedRect = edgeInsetsInsetRect(self.bounds, self.contentInsets);
CGRect bounds = constrainedRect;
// Any one of these cases should trigger a move onscreen
CGFloat minX = CGRectGetMinX(expandedPositioningRect);
if (minX < CGRectGetMinX(bounds)) {
constrainedRect.origin.x = minX;
moveIntoView = YES;
} else {
CGFloat maxX = CGRectGetMaxX(expandedPositioningRect);
if (maxX > CGRectGetMaxX(bounds)) {
constrainedRect.origin.x = maxX - CGRectGetWidth(constrainedRect);
moveIntoView = YES;
}
}
CGFloat minY = CGRectGetMinY(expandedPositioningRect);
if (minY < CGRectGetMinY(bounds)) {
constrainedRect.origin.y = minY;
moveIntoView = YES;
} else {
CGFloat maxY = CGRectGetMaxY(expandedPositioningRect);
if (maxY > CGRectGetMaxY(bounds)) {
constrainedRect.origin.y = maxY - CGRectGetHeight(constrainedRect);
moveIntoView = YES;
}
}
if (moveIntoView) {
CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect));
CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self];
[self setCenterCoordinate:centerCoord animated:animateSelection];
}
}
}
- (void)showAnnotations:(NSArray<id<MLNAnnotation>> *)annotations animated:(BOOL)animated {
MLNLogDebug(@"Showing: %lu annotations animated: %@", annotations.count,
MLNStringFromBOOL(animated));
CGFloat maximumPadding = 100;
CGFloat yPadding =
(NSHeight(self.bounds) / 5 <= maximumPadding) ? (NSHeight(self.bounds) / 5) : maximumPadding;
CGFloat xPadding =
(NSWidth(self.bounds) / 5 <= maximumPadding) ? (NSWidth(self.bounds) / 5) : maximumPadding;
NSEdgeInsets edgeInsets = NSEdgeInsetsMake(yPadding, xPadding, yPadding, xPadding);
[self showAnnotations:annotations edgePadding:edgeInsets animated:animated];
}
- (void)showAnnotations:(NSArray<id<MLNAnnotation>> *)annotations
edgePadding:(NSEdgeInsets)insets
animated:(BOOL)animated {
[self showAnnotations:annotations edgePadding:insets animated:animated completionHandler:nil];
}
- (void)showAnnotations:(NSArray<id<MLNAnnotation>> *)annotations
edgePadding:(NSEdgeInsets)insets
animated:(BOOL)animated
completionHandler:(nullable void (^)(void))completion {
if (!annotations.count) {
if (completion) {
completion();
}
return;
}
mln::LatLngBounds bounds = mln::LatLngBounds::empty();
for (id<MLNAnnotation> annotation in annotations) {
if ([annotation conformsToProtocol:@protocol(MLNOverlay)]) {
bounds.extend(
MLNLatLngBoundsFromCoordinateBounds(((id<MLNOverlay>)annotation).overlayBounds));
} else {
bounds.extend(MLNLatLngFromLocationCoordinate2D(annotation.coordinate));
}
}
[self setVisibleCoordinateBounds:MLNCoordinateBoundsFromLatLngBounds(bounds)
edgePadding:insets
animated:animated
completionHandler:completion];
}
/// Returns a popover detailing the annotation.
- (NSPopover *)calloutForAnnotation:(id<MLNAnnotation>)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 MLNAnnotationCallout 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:@"MLNAnnotationCallout"
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:(MLNAnnotationTag)annotationTag {
id<MLNAnnotation> annotation = [self annotationWithTag:annotationTag];
if (!annotation) {
return NSZeroRect;
}
if ([annotation isKindOfClass:[MLNMultiPoint class]]) {
CLLocationCoordinate2D origin = annotation.coordinate;
CGPoint originPoint = [self convertCoordinate:origin toPointToView:self];
return CGRectMake(originPoint.x, originPoint.y, MLNAnnotationImagePaddingForHitTest,
MLNAnnotationImagePaddingForHitTest);
}
NSImage *image = [self imageOfAnnotationWithTag:annotationTag].image;
if (!image) {
image =
[self dequeueReusableAnnotationImageWithIdentifier:MLNDefaultStyleMarkerSymbolName].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, -MLNAnnotationImagePaddingForCallout,
-MLNAnnotationImagePaddingForCallout);
}
/// 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.
- (MLNAnnotationImage *)imageOfAnnotationWithTag:(MLNAnnotationTag)annotationTag {
if (annotationTag == MLNAnnotationTagNotFound ||
_annotationContextsByAnnotationTag.count(annotationTag) == 0) {
return nil;
}
NSString *customSymbol =
_annotationContextsByAnnotationTag.at(annotationTag).imageReuseIdentifier;
NSString *symbolName = customSymbol.length ? customSymbol : MLNDefaultStyleMarkerSymbolName;
return [self dequeueReusableAnnotationImageWithIdentifier:symbolName];
}
- (void)deselectAnnotation:(id<MLNAnnotation>)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) {
NSRect rect = [self positioningRectForCalloutForAnnotationWithTag:_selectedAnnotationTag];
NSAssert(!NSEqualRects(rect, NSZeroRect), @"Positioning rect should be non-zero");
if (!NSIsEmptyRect(NSIntersectionRect(rect, self.bounds))) {
// It's possible that the current callout hasn't been presented (since the original
// positioningRect was offscreen). We can check that the callout has a valid window
// This results in the callout being presented just as the annotation comes on screen
// which matches MapKit, but (currently) not iOS.
if (!callout.contentViewController.view.window) {
NSRectEdge edge =
(self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft
? NSMinXEdge
: NSMaxXEdge);
// Re-present the callout
[callout showRelativeToRect:rect ofView:self preferredEdge:edge];
} else {
callout.positioningRect = rect;
}
}
}
}
// MARK: MLNMultiPointDelegate methods
- (double)alphaForShapeAnnotation:(MLNShape *)annotation {
if (_delegateHasAlphasForShapeAnnotations) {
return [self.delegate mapView:self alphaForShapeAnnotation:annotation];
}
return 1.0;
}
- (mln::Color)strokeColorForShapeAnnotation:(MLNShape *)annotation {
NSColor *color = (_delegateHasStrokeColorsForShapeAnnotations
? [self.delegate mapView:self strokeColorForShapeAnnotation:annotation]
: [NSColor selectedMenuItemColor]);
return color.mgl_color;
}
- (mln::Color)fillColorForPolygonAnnotation:(MLNPolygon *)annotation {
NSColor *color = (_delegateHasFillColorsForShapeAnnotations
? [self.delegate mapView:self fillColorForPolygonAnnotation:annotation]
: [NSColor selectedMenuItemColor]);
return color.mgl_color;
}
- (CGFloat)lineWidthForPolylineAnnotation:(MLNPolyline *)annotation {
if (_delegateHasLineWidthsForShapeAnnotations) {
return [self.delegate mapView:self lineWidthForPolylineAnnotation:(MLNPolyline *)annotation];
}
return 3.0;
}
// MARK: MLNPopoverDelegate methods
- (void)popoverDidShow:(__unused NSNotification *)notification {
id<MLNAnnotation> 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<MLNAnnotation> annotation =
self.calloutForSelectedAnnotation.contentViewController.representedObject;
self.calloutForSelectedAnnotation = nil;
self.selectedAnnotation = nil;
if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)]) {
[self.delegate mapView:self didDeselectAnnotation:annotation];
}
}
// MARK: Overlays
- (nonnull NSArray<id<MLNOverlay>> *)overlays {
if (self.annotations == nil) {
return @[];
}
NSMutableArray<id<MLNOverlay>> *mutableOverlays = [NSMutableArray array];
[self.annotations enumerateObjectsUsingBlock:^(id<MLNAnnotation> _Nonnull annotation,
NSUInteger idx, BOOL *_Nonnull stop) {
if ([annotation conformsToProtocol:@protocol(MLNOverlay)]) {
[mutableOverlays addObject:(id<MLNOverlay>)annotation];
}
}];
return [NSArray arrayWithArray:mutableOverlays];
}
- (void)addOverlay:(id<MLNOverlay>)overlay {
MLNLogDebug(@"Adding overlay: %@", overlay);
[self addOverlays:@[ overlay ]];
}
- (void)addOverlays:(NSArray<id<MLNOverlay>> *)overlays {
MLNLogDebug(@"Adding: %lu overlays", overlays.count);
#if DEBUG
for (id<MLNOverlay> overlay in overlays) {
NSAssert([overlay conformsToProtocol:@protocol(MLNOverlay)],
@"Overlay does not conform to MLNOverlay");
}
#endif
[self addAnnotations:overlays];
}
- (void)removeOverlay:(id<MLNOverlay>)overlay {
MLNLogDebug(@"Removing overlay: %@", overlay);
[self removeOverlays:@[ overlay ]];
}
- (void)removeOverlays:(NSArray<id<MLNOverlay>> *)overlays {
MLNLogDebug(@"Removing: %lu overlays", overlays.count);
#if DEBUG
for (id<MLNOverlay> overlay in overlays) {
NSAssert([overlay conformsToProtocol:@protocol(MLNOverlay)],
@"Overlay does not conform to MLNOverlay");
}
#endif
[self removeAnnotations:overlays];
}
// MARK: Tooltips and cursors
- (void)updateAnnotationTrackingAreas {
if (_wantsToolTipRects) {
[self removeAllToolTips];
std::vector<MLNAnnotationTag> annotationTags = [self annotationTagsInRect:self.bounds];
for (MLNAnnotationTag annotationTag : annotationTags) {
MLNAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
id<MLNAnnotation> annotation = [self annotationWithTag:annotationTag];
if ([annotation respondsToSelector:@selector(toolTip)] && 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 < MLNAnnotationTagNotFound,
@"Invalid annotation tag in tooltip rect user data.");
MLNAnnotationTag annotationTag =
(MLNAnnotationTag)MIN((NSUInteger)data, MLNAnnotationTagNotFound);
id<MLNAnnotation> 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<MLNAnnotationTag> annotationTags = [self annotationTagsInRect:self.bounds];
for (MLNAnnotationTag annotationTag : annotationTags) {
id<MLNAnnotation> annotation = [self annotationWithTag:annotationTag];
MLNAnnotationImage *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];
}
}
}
// MARK: Data
- (NSArray<id<MLNFeature>> *)visibleFeaturesAtPoint:(NSPoint)point {
MLNLogDebug(@"Querying visibleFeaturesAtPoint: %@", NSStringFromPoint(point));
return [self visibleFeaturesAtPoint:point inStyleLayersWithIdentifiers:nil];
}
- (NSArray<id<MLNFeature>> *)visibleFeaturesAtPoint:(CGPoint)point
inStyleLayersWithIdentifiers:(NSSet<NSString *> *)styleLayerIdentifiers {
MLNLogDebug(@"Querying visibleFeaturesAtPoint: %@ inStyleLayersWithIdentifiers: %@",
NSStringFromPoint(point), styleLayerIdentifiers);
return [self visibleFeaturesAtPoint:point
inStyleLayersWithIdentifiers:styleLayerIdentifiers
predicate:nil];
}
- (NSArray<id<MLNFeature>> *)visibleFeaturesAtPoint:(NSPoint)point
inStyleLayersWithIdentifiers:(NSSet<NSString *> *)styleLayerIdentifiers
predicate:(NSPredicate *)predicate {
MLNLogDebug(@"Querying visibleFeaturesAtPoint: %@ inStyleLayersWithIdentifiers: %@ predicate: %@",
NSStringFromPoint(point), styleLayerIdentifiers, predicate);
// Cocoa origin is at the lower-left corner.
mln::ScreenCoordinate screenCoordinate = {point.x, NSHeight(self.bounds) - point.y};
std::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::optional<mln::style::Filter> optionalFilter;
if (predicate) {
optionalFilter = predicate.mgl_filter;
}
std::vector<mln::Feature> features = _rendererFrontend->getRenderer()->queryRenderedFeatures(
screenCoordinate, {optionalLayerIDs, optionalFilter});
return MLNFeaturesFromMBGLFeatures(features);
}
- (NSArray<id<MLNFeature>> *)visibleFeaturesInRect:(NSRect)rect {
MLNLogDebug(@"Querying visibleFeaturesInRect: %@", NSStringFromRect(rect));
return [self visibleFeaturesInRect:rect inStyleLayersWithIdentifiers:nil];
}
- (NSArray<id<MLNFeature>> *)visibleFeaturesInRect:(CGRect)rect
inStyleLayersWithIdentifiers:(NSSet<NSString *> *)styleLayerIdentifiers {
MLNLogDebug(@"Querying visibleFeaturesInRect: %@ inStyleLayersWithIdentifiers: %@",
NSStringFromRect(rect), styleLayerIdentifiers);
return [self visibleFeaturesInRect:rect
inStyleLayersWithIdentifiers:styleLayerIdentifiers
predicate:nil];
}
- (NSArray<id<MLNFeature>> *)visibleFeaturesInRect:(NSRect)rect
inStyleLayersWithIdentifiers:(NSSet<NSString *> *)styleLayerIdentifiers
predicate:(NSPredicate *)predicate {
MLNLogDebug(@"Querying visibleFeaturesInRect: %@ inStyleLayersWithIdentifiers: %@ predicate: %@",
NSStringFromRect(rect), styleLayerIdentifiers, predicate);
// Cocoa origin is at the lower-left corner.
mln::ScreenBox screenBox = {
{NSMinX(rect), NSHeight(self.bounds) - NSMaxY(rect)},
{NSMaxX(rect), NSHeight(self.bounds) - NSMinY(rect)},
};
std::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::optional<mln::style::Filter> optionalFilter;
if (predicate) {
optionalFilter = predicate.mgl_filter;
}
std::vector<mln::Feature> features = _rendererFrontend->getRenderer()->queryRenderedFeatures(
screenBox, {optionalLayerIDs, optionalFilter});
return MLNFeaturesFromMBGLFeatures(features);
}
// MARK: User interface validation
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
return NO;
}
// 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 = MLNDefaultMarkerImage();
self.layer.contentsGravity = kCAGravityCenter;
self.layer.contentsScale = [NSScreen mainScreen].backingScaleFactor;
}
// MARK: Geometric methods
- (NSPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate
toPointToView:(nullable NSView *)view {
if (!CLLocationCoordinate2DIsValid(coordinate)) {
return NSMakePoint(NAN, NAN);
}
return [self convertLatLng:MLNLatLngFromLocationCoordinate2D(coordinate) toPointToView:view];
}
/// Converts a geographic coordinate to a point in the view’s coordinate system.
- (NSPoint)convertLatLng:(mln::LatLng)latLng toPointToView:(nullable NSView *)view {
mln::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 MLNLocationCoordinate2DFromLatLng([self convertPoint:point toLatLngFromView:view]);
}
/// Converts a point in the view’s coordinate system to a geographic coordinate.
- (mln::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:(MLNCoordinateBounds)bounds toRectToView:(nullable NSView *)view {
return [self convertLatLngBounds:MLNLatLngBoundsFromCoordinateBounds(bounds) toRectToView:view];
}
/// Converts a geographic bounding box to a rectangle in the view’s coordinate
/// system.
- (NSRect)convertLatLngBounds:(mln::LatLngBounds)bounds toRectToView:(nullable NSView *)view {
auto northwest = bounds.northwest();
auto northeast = bounds.northeast();
auto southwest = bounds.southwest();
auto southeast = bounds.southeast();
auto center = [self convertPoint:{NSMidX(view.bounds), NSMidY(view.bounds)}
toLatLngFromView:view];
// Extend bounds to account for the antimeridian
northwest.unwrapForShortestPath(center);
northeast.unwrapForShortestPath(center);
southwest.unwrapForShortestPath(center);
southeast.unwrapForShortestPath(center);
auto correctedLatLngBounds = mln::LatLngBounds::empty();
correctedLatLngBounds.extend(northwest);
correctedLatLngBounds.extend(northeast);
correctedLatLngBounds.extend(southwest);
correctedLatLngBounds.extend(southeast);
NSRect rect = {[self convertLatLng:correctedLatLngBounds.southwest() toPointToView:view],
CGSizeZero};
rect = MLNExtendRect(rect, [self convertLatLng:correctedLatLngBounds.northeast()
toPointToView:view]);
return rect;
}
- (MLNCoordinateBounds)convertRect:(NSRect)rect toCoordinateBoundsFromView:(nullable NSView *)view {
return MLNCoordinateBoundsFromLatLngBounds([self convertRect:rect toLatLngBoundsFromView:view]);
}
/// Converts a rectangle in the given view’s coordinate system to a geographic
/// bounding box.
- (mln::LatLngBounds)convertRect:(NSRect)rect toLatLngBoundsFromView:(nullable NSView *)view {
auto bounds = mln::LatLngBounds::empty();
auto bottomLeft = [self convertPoint:{NSMinX(rect), NSMinY(rect)} toLatLngFromView:view];
auto bottomRight = [self convertPoint:{NSMaxX(rect), NSMinY(rect)} toLatLngFromView:view];
auto topRight = [self convertPoint:{NSMaxX(rect), NSMaxY(rect)} toLatLngFromView:view];
auto topLeft = [self convertPoint:{NSMinX(rect), NSMaxY(rect)} toLatLngFromView:view];
// If the bounds straddles the antimeridian, unwrap it so that one side
// extends beyond ±180° longitude.
auto center = [self convertPoint:{NSMidX(rect), NSMidY(rect)} toLatLngFromView:view];
bottomLeft.unwrapForShortestPath(center);
bottomRight.unwrapForShortestPath(center);
topRight.unwrapForShortestPath(center);
topLeft.unwrapForShortestPath(center);
bounds.extend(bottomLeft);
bounds.extend(bottomRight);
bounds.extend(topRight);
bounds.extend(topLeft);
return bounds;
}
- (CLLocationDistance)metersPerPointAtLatitude:(CLLocationDegrees)latitude {
return mln::Projection::getMetersPerPixelAtLatitude(latitude, self.zoomLevel);
}
// MARK: Debugging
- (MLNMapDebugMaskOptions)debugMask {
mln::MapDebugOptions options = _mbglMap->getDebug();
MLNMapDebugMaskOptions mask = 0;
if (options & mln::MapDebugOptions::TileBorders) {
mask |= MLNMapDebugTileBoundariesMask;
}
if (options & mln::MapDebugOptions::ParseStatus) {
mask |= MLNMapDebugTileInfoMask;
}
if (options & mln::MapDebugOptions::Timestamps) {
mask |= MLNMapDebugTimestampsMask;
}
if (options & mln::MapDebugOptions::Collision) {
mask |= MLNMapDebugCollisionBoxesMask;
}
if (options & mln::MapDebugOptions::Overdraw) {
mask |= MLNMapDebugOverdrawVisualizationMask;
}
if (options & mln::MapDebugOptions::StencilClip) {
mask |= MLNMapDebugStencilBufferMask;
}
if (options & mln::MapDebugOptions::DepthBuffer) {
mask |= MLNMapDebugDepthBufferMask;
}
return mask;
}
- (void)setDebugMask:(MLNMapDebugMaskOptions)debugMask {
mln::MapDebugOptions options = mln::MapDebugOptions::NoDebug;
if (debugMask & MLNMapDebugTileBoundariesMask) {
options |= mln::MapDebugOptions::TileBorders;
}
if (debugMask & MLNMapDebugTileInfoMask) {
options |= mln::MapDebugOptions::ParseStatus;
}
if (debugMask & MLNMapDebugTimestampsMask) {
options |= mln::MapDebugOptions::Timestamps;
}
if (debugMask & MLNMapDebugCollisionBoxesMask) {
options |= mln::MapDebugOptions::Collision;
}
if (debugMask & MLNMapDebugOverdrawVisualizationMask) {
options |= mln::MapDebugOptions::Overdraw;
}
if (debugMask & MLNMapDebugStencilBufferMask) {
options |= mln::MapDebugOptions::StencilClip;
}
if (debugMask & MLNMapDebugDepthBufferMask) {
options |= mln::MapDebugOptions::DepthBuffer;
}
_mbglMap->setDebug(options);
}
- (NSArray<NSString *> *)getActionJournalLog {
const auto &actionJournal = _mbglMap->getActionJournal();
if (!actionJournal) {
return nil;
}
const auto &log = actionJournal->getLog();
NSMutableArray<NSString *> *objcLog = [NSMutableArray new];
for (const auto &event : log) {
[objcLog addObject:[NSString stringWithUTF8String:event.c_str()]];
}
return objcLog;
}
- (void)clearActionJournalLog {
const auto &actionJournal = _mbglMap->getActionJournal();
if (!actionJournal) {
return;
}
actionJournal->clearLog();
}
#if MLN_RENDER_BACKEND_METAL
- (MLNBackendResource *)backendResource {
return _mbglView->getObject();
}
#endif
@end