2714 lines
111 KiB
Plaintext
2714 lines
111 KiB
Plaintext
#import "MGLMapView_Private.h"
|
||
#import "MGLAnnotationImage_Private.h"
|
||
#import "MGLAttributionButton.h"
|
||
#import "MGLAttributionInfo.h"
|
||
#import "MGLCompassCell.h"
|
||
#import "MGLOpenGLLayer.h"
|
||
#import "MGLStyle.h"
|
||
|
||
#import "MGLFeature_Private.h"
|
||
#import "MGLGeometry_Private.h"
|
||
#import "MGLMultiPoint_Private.h"
|
||
#import "MGLOfflineStorage_Private.h"
|
||
#import "MGLStyle_Private.h"
|
||
|
||
#import "MGLAccountManager.h"
|
||
#import "MGLMapCamera.h"
|
||
#import "MGLPolygon.h"
|
||
#import "MGLPolyline.h"
|
||
#import "MGLAnnotationImage.h"
|
||
#import "MGLMapViewDelegate.h"
|
||
|
||
#import <mbgl/mbgl.hpp>
|
||
#import <mbgl/annotation/annotation.hpp>
|
||
#import <mbgl/map/camera.hpp>
|
||
#import <mbgl/platform/darwin/reachability.h>
|
||
#import <mbgl/platform/default/thread_pool.hpp>
|
||
#import <mbgl/gl/extension.hpp>
|
||
#import <mbgl/gl/gl.hpp>
|
||
#import <mbgl/gl/context.hpp>
|
||
#import <mbgl/map/backend.hpp>
|
||
#import <mbgl/sprite/sprite_image.hpp>
|
||
#import <mbgl/storage/default_file_source.hpp>
|
||
#import <mbgl/storage/network_status.hpp>
|
||
#import <mbgl/math/wrap.hpp>
|
||
#import <mbgl/util/constants.hpp>
|
||
#import <mbgl/util/chrono.hpp>
|
||
#import <mbgl/util/run_loop.hpp>
|
||
|
||
#import <map>
|
||
#import <unordered_map>
|
||
#import <unordered_set>
|
||
|
||
#import "NSBundle+MGLAdditions.h"
|
||
#import "NSDate+MGLAdditions.h"
|
||
#import "NSProcessInfo+MGLAdditions.h"
|
||
#import "NSException+MGLAdditions.h"
|
||
#import "NSString+MGLAdditions.h"
|
||
#import "NSURL+MGLAdditions.h"
|
||
#import "NSColor+MGLAdditions.h"
|
||
#import "NSImage+MGLAdditions.h"
|
||
|
||
#import <QuartzCore/QuartzCore.h>
|
||
|
||
class MGLMapViewImpl;
|
||
class MGLAnnotationContext;
|
||
|
||
/// Distance from the edge of the view to ornament views (logo, attribution, etc.).
|
||
const CGFloat MGLOrnamentPadding = 12;
|
||
|
||
/// Alpha value of the ornament views (logo, attribution, etc.).
|
||
const CGFloat MGLOrnamentOpacity = 0.9;
|
||
|
||
/// Default duration for programmatic animations.
|
||
const NSTimeInterval MGLAnimationDuration = 0.3;
|
||
|
||
/// Distance in points that a single press of the panning keyboard shortcut pans the map by.
|
||
const CGFloat MGLKeyPanningIncrement = 150;
|
||
|
||
/// Degrees that a single press of the rotation keyboard shortcut rotates the map by.
|
||
const CLLocationDegrees MGLKeyRotationIncrement = 25;
|
||
|
||
/// Key for the user default that, when true, causes the map view to zoom in and out on scroll wheel events.
|
||
NSString * const MGLScrollWheelZoomsMapViewDefaultKey = @"MGLScrollWheelZoomsMapView";
|
||
|
||
/// Reuse identifier and file name of the default point annotation image.
|
||
static NSString * const MGLDefaultStyleMarkerSymbolName = @"default_marker";
|
||
|
||
/// Prefix that denotes a sprite installed by MGLMapView, to avoid collisions
|
||
/// with style-defined sprites.
|
||
static NSString * const MGLAnnotationSpritePrefix = @"com.mapbox.sprites.";
|
||
|
||
/// Slop area around the hit testing point, allowing for imprecise annotation selection.
|
||
const CGFloat MGLAnnotationImagePaddingForHitTest = 4;
|
||
|
||
/// Distance from the callout’s anchor point to the annotation it points to.
|
||
const CGFloat MGLAnnotationImagePaddingForCallout = 4;
|
||
|
||
/// Unique identifier representing a single annotation in mbgl.
|
||
typedef uint32_t MGLAnnotationTag;
|
||
|
||
/// An indication that the requested annotation was not found or is nonexistent.
|
||
enum { MGLAnnotationTagNotFound = UINT32_MAX };
|
||
|
||
/// Mapping from an annotation tag to metadata about that annotation, including
|
||
/// the annotation itself.
|
||
typedef std::unordered_map<MGLAnnotationTag, MGLAnnotationContext> MGLAnnotationTagContextMap;
|
||
|
||
/// Mapping from an annotation object to an annotation tag.
|
||
typedef std::map<id<MGLAnnotation>, MGLAnnotationTag> MGLAnnotationObjectTagMap;
|
||
|
||
/// Returns an NSImage for the default marker image.
|
||
NSImage *MGLDefaultMarkerImage() {
|
||
NSString *path = [[NSBundle mgl_frameworkBundle] pathForResource:MGLDefaultStyleMarkerSymbolName
|
||
ofType:@"pdf"];
|
||
return [[NSImage alloc] initWithContentsOfFile:path];
|
||
}
|
||
|
||
/// Initializes the run loop shim that lives on the main thread.
|
||
void MGLinitializeRunLoop() {
|
||
static mbgl::util::RunLoop mainRunLoop;
|
||
}
|
||
|
||
/// Converts a media timing function into a unit bezier object usable in mbgl.
|
||
mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction *function) {
|
||
if (!function) {
|
||
function = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
|
||
}
|
||
float p1[2], p2[2];
|
||
[function getControlPointAtIndex:0 values:p1];
|
||
[function getControlPointAtIndex:1 values:p2];
|
||
return { p1[0], p1[1], p2[0], p2[1] };
|
||
}
|
||
|
||
/// Lightweight container for metadata about an annotation, including the annotation itself.
|
||
class MGLAnnotationContext {
|
||
public:
|
||
id <MGLAnnotation> annotation;
|
||
/// The annotation’s image’s reuse identifier.
|
||
NSString *imageReuseIdentifier;
|
||
};
|
||
|
||
@interface MGLMapView () <NSPopoverDelegate, MGLMultiPointDelegate, NSGestureRecognizerDelegate>
|
||
|
||
@property (nonatomic, readwrite) NSSegmentedControl *zoomControls;
|
||
@property (nonatomic, readwrite) NSSlider *compass;
|
||
@property (nonatomic, readwrite) NSImageView *logoView;
|
||
@property (nonatomic, readwrite) NSView *attributionView;
|
||
|
||
@property (nonatomic, readwrite) MGLStyle *style;
|
||
|
||
/// Mapping from reusable identifiers to annotation images.
|
||
@property (nonatomic) NS_MUTABLE_DICTIONARY_OF(NSString *, MGLAnnotationImage *) *annotationImagesByIdentifier;
|
||
/// Currently shown popover representing the selected annotation.
|
||
@property (nonatomic) NSPopover *calloutForSelectedAnnotation;
|
||
|
||
@property (nonatomic, readwrite, getter=isDormant) BOOL dormant;
|
||
|
||
@end
|
||
|
||
@implementation MGLMapView {
|
||
/// Cross-platform map view controller.
|
||
mbgl::Map *_mbglMap;
|
||
MGLMapViewImpl *_mbglView;
|
||
mbgl::ThreadPool *_mbglThreadPool;
|
||
|
||
NSPanGestureRecognizer *_panGestureRecognizer;
|
||
NSMagnificationGestureRecognizer *_magnificationGestureRecognizer;
|
||
NSRotationGestureRecognizer *_rotationGestureRecognizer;
|
||
NSClickGestureRecognizer *_singleClickRecognizer;
|
||
double _scaleAtBeginningOfGesture;
|
||
CLLocationDirection _directionAtBeginningOfGesture;
|
||
CGFloat _pitchAtBeginningOfGesture;
|
||
BOOL _didHideCursorDuringGesture;
|
||
|
||
MGLAnnotationTagContextMap _annotationContextsByAnnotationTag;
|
||
MGLAnnotationObjectTagMap _annotationTagsByAnnotation;
|
||
MGLAnnotationTag _selectedAnnotationTag;
|
||
MGLAnnotationTag _lastSelectedAnnotationTag;
|
||
/// Size of the rectangle formed by unioning the maximum slop area around every annotation image.
|
||
NSSize _unionedAnnotationImageSize;
|
||
std::vector<MGLAnnotationTag> _annotationsNearbyLastClick;
|
||
/// True if any annotations that have tooltips have been installed.
|
||
BOOL _wantsToolTipRects;
|
||
/// True if any annotation images that have custom cursors have been installed.
|
||
BOOL _wantsCursorRects;
|
||
/// 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
|
||
// MGLMultiPointDelegate methods.
|
||
|
||
BOOL _delegateHasAlphasForShapeAnnotations;
|
||
BOOL _delegateHasStrokeColorsForShapeAnnotations;
|
||
BOOL _delegateHasFillColorsForShapeAnnotations;
|
||
BOOL _delegateHasLineWidthsForShapeAnnotations;
|
||
|
||
/// True if the current process is the Interface Builder designable
|
||
/// renderer. When drawing the designable, the map is paused, so any call to
|
||
/// it may hang the process.
|
||
BOOL _isTargetingInterfaceBuilder;
|
||
CLLocationDegrees _pendingLatitude;
|
||
CLLocationDegrees _pendingLongitude;
|
||
|
||
/// True if the view is currently printing itself.
|
||
BOOL _isPrinting;
|
||
}
|
||
|
||
#pragma mark Lifecycle
|
||
|
||
+ (void)initialize {
|
||
if (self == [MGLMapView class]) {
|
||
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
|
||
MGLScrollWheelZoomsMapViewDefaultKey: @NO,
|
||
}];
|
||
}
|
||
}
|
||
|
||
- (instancetype)initWithFrame:(NSRect)frameRect {
|
||
if (self = [super initWithFrame:frameRect]) {
|
||
[self commonInit];
|
||
self.styleURL = nil;
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (instancetype)initWithFrame:(NSRect)frame styleURL:(nullable NSURL *)styleURL {
|
||
if (self = [super initWithFrame:frame]) {
|
||
[self commonInit];
|
||
self.styleURL = styleURL;
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (instancetype)initWithCoder:(nonnull NSCoder *)decoder {
|
||
if (self = [super initWithCoder:decoder]) {
|
||
[self commonInit];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)awakeFromNib {
|
||
[super awakeFromNib];
|
||
|
||
self.styleURL = nil;
|
||
}
|
||
|
||
+ (NSArray *)restorableStateKeyPaths {
|
||
return @[@"camera", @"debugMask"];
|
||
}
|
||
|
||
- (void)commonInit {
|
||
MGLinitializeRunLoop();
|
||
|
||
_isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent;
|
||
|
||
// Set up cross-platform controllers and resources.
|
||
_mbglView = new MGLMapViewImpl(self);
|
||
|
||
// Delete the pre-offline ambient cache at
|
||
// ~/Library/Caches/com.mapbox.sdk.ios/cache.db.
|
||
NSURL *cachesDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory
|
||
inDomain:NSUserDomainMask
|
||
appropriateForURL:nil
|
||
create:NO
|
||
error:nil];
|
||
cachesDirectoryURL = [cachesDirectoryURL URLByAppendingPathComponent:
|
||
[NSBundle mgl_frameworkBundle].bundleIdentifier];
|
||
NSURL *legacyCacheURL = [cachesDirectoryURL URLByAppendingPathComponent:@"cache.db"];
|
||
[[NSFileManager defaultManager] removeItemAtURL:legacyCacheURL error:NULL];
|
||
|
||
mbgl::DefaultFileSource* mbglFileSource = [MGLOfflineStorage sharedOfflineStorage].mbglFileSource;
|
||
|
||
const std::array<uint16_t, 2> size = {{ static_cast<uint16_t>(self.bounds.size.width),
|
||
static_cast<uint16_t>(self.bounds.size.height) }};
|
||
_mbglThreadPool = new mbgl::ThreadPool(4);
|
||
_mbglMap = new mbgl::Map(*_mbglView, size, [NSScreen mainScreen].backingScaleFactor, *mbglFileSource, *_mbglThreadPool, mbgl::MapMode::Continuous, mbgl::GLContextMode::Unique, mbgl::ConstrainMode::None, mbgl::ViewportMode::Default);
|
||
[self validateTileCacheSize];
|
||
|
||
// Install the OpenGL layer. Interface Builder’s synchronous drawing means
|
||
// we can’t display a map, so don’t even bother to have a map layer.
|
||
self.layer = _isTargetingInterfaceBuilder ? [CALayer layer] : [MGLOpenGLLayer layer];
|
||
|
||
// Notify map object when network reachability status changes.
|
||
MGLReachability *reachability = [MGLReachability reachabilityForInternetConnection];
|
||
reachability.reachableBlock = ^(MGLReachability *) {
|
||
mbgl::NetworkStatus::Reachable();
|
||
};
|
||
[reachability startNotifier];
|
||
|
||
// Install ornaments and gesture recognizers.
|
||
[self installZoomControls];
|
||
[self installCompass];
|
||
[self installLogoView];
|
||
[self installAttributionView];
|
||
[self installGestureRecognizers];
|
||
|
||
// Set up annotation management and selection state.
|
||
_annotationImagesByIdentifier = [NSMutableDictionary dictionary];
|
||
_annotationContextsByAnnotationTag = {};
|
||
_annotationTagsByAnnotation = {};
|
||
_selectedAnnotationTag = MGLAnnotationTagNotFound;
|
||
_lastSelectedAnnotationTag = MGLAnnotationTagNotFound;
|
||
_annotationsNearbyLastClick = {};
|
||
|
||
// Jump to Null Island initially.
|
||
self.automaticallyAdjustsContentInsets = YES;
|
||
mbgl::CameraOptions options;
|
||
options.center = mbgl::LatLng(0, 0);
|
||
options.padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
|
||
options.zoom = _mbglMap->getMinZoom();
|
||
_mbglMap->jumpTo(options);
|
||
_pendingLatitude = NAN;
|
||
_pendingLongitude = NAN;
|
||
}
|
||
|
||
/// Adds zoom controls to the lower-right corner.
|
||
- (void)installZoomControls {
|
||
_zoomControls = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect];
|
||
_zoomControls.wantsLayer = YES;
|
||
_zoomControls.layer.opacity = MGLOrnamentOpacity;
|
||
[(NSSegmentedCell *)_zoomControls.cell setTrackingMode:NSSegmentSwitchTrackingMomentary];
|
||
_zoomControls.continuous = YES;
|
||
_zoomControls.segmentCount = 2;
|
||
[_zoomControls setLabel:NSLocalizedStringWithDefaultValue(@"ZOOM_OUT_LABEL", nil, nil, @"−", @"Label of Zoom Out button; U+2212 MINUS SIGN") forSegment:0];
|
||
[(NSSegmentedCell *)_zoomControls.cell setTag:0 forSegment:0];
|
||
[(NSSegmentedCell *)_zoomControls.cell setToolTip:NSLocalizedStringWithDefaultValue(@"ZOOM_OUT_TOOLTIP", nil, nil, @"Zoom Out", @"Tooltip of Zoom Out button") forSegment:0];
|
||
[_zoomControls setLabel:NSLocalizedStringWithDefaultValue(@"ZOOM_IN_LABEL", nil, nil, @"+", @"Label of Zoom In button") forSegment:1];
|
||
[(NSSegmentedCell *)_zoomControls.cell setTag:1 forSegment:1];
|
||
[(NSSegmentedCell *)_zoomControls.cell setToolTip:NSLocalizedStringWithDefaultValue(@"ZOOM_IN_TOOLTIP", nil, nil, @"Zoom In", @"Tooltip of Zoom In button") forSegment:1];
|
||
_zoomControls.target = self;
|
||
_zoomControls.action = @selector(zoomInOrOut:);
|
||
_zoomControls.controlSize = NSRegularControlSize;
|
||
[_zoomControls sizeToFit];
|
||
_zoomControls.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[self addSubview:_zoomControls];
|
||
}
|
||
|
||
/// Adds a rudimentary compass control to the lower-right corner.
|
||
- (void)installCompass {
|
||
_compass = [[NSSlider alloc] initWithFrame:NSZeroRect];
|
||
_compass.wantsLayer = YES;
|
||
_compass.layer.opacity = MGLOrnamentOpacity;
|
||
_compass.cell = [[MGLCompassCell alloc] init];
|
||
_compass.continuous = YES;
|
||
_compass.target = self;
|
||
_compass.action = @selector(rotate:);
|
||
[_compass sizeToFit];
|
||
_compass.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[self addSubview:_compass];
|
||
}
|
||
|
||
/// Adds a Mapbox logo to the lower-left corner.
|
||
- (void)installLogoView {
|
||
_logoView = [[NSImageView alloc] initWithFrame:NSZeroRect];
|
||
_logoView.wantsLayer = YES;
|
||
NSImage *logoImage = [[NSImage alloc] initWithContentsOfFile:
|
||
[[NSBundle mgl_frameworkBundle] pathForResource:@"mapbox" ofType:@"pdf"]];
|
||
// Account for the image’s built-in padding when aligning other controls to the logo.
|
||
logoImage.alignmentRect = NSInsetRect(logoImage.alignmentRect, 3, 3);
|
||
_logoView.image = logoImage;
|
||
_logoView.translatesAutoresizingMaskIntoConstraints = NO;
|
||
_logoView.accessibilityTitle = NSLocalizedStringWithDefaultValue(@"MAP_A11Y_TITLE", nil, nil, @"Mapbox", @"Accessibility title");
|
||
[self addSubview:_logoView];
|
||
}
|
||
|
||
/// Adds legally required map attribution to the lower-left corner.
|
||
- (void)installAttributionView {
|
||
[_attributionView 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:NSMiniControlSize];
|
||
NSArray *attributionInfos = [self.style attributionInfosWithFontSize:miniSize linkColor:[NSColor blackColor]];
|
||
for (MGLAttributionInfo *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 = [[MGLAttributionButton alloc] initWithAttributionInfo:info];
|
||
button.controlSize = NSMiniControlSize;
|
||
button.translatesAutoresizingMaskIntoConstraints = NO;
|
||
|
||
// Set the new button flush with the buttom of the container and to the
|
||
// right of the previous button, with standard spacing. If there is no
|
||
// previous button, align to the container instead.
|
||
NSView *previousView = 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 {
|
||
[self.window removeObserver:self forKeyPath:@"contentLayoutRect"];
|
||
[self.window removeObserver:self forKeyPath:@"titlebarAppearsTransparent"];
|
||
|
||
// Close any annotation callout immediately.
|
||
[self.calloutForSelectedAnnotation close];
|
||
self.calloutForSelectedAnnotation = nil;
|
||
|
||
// Removing the annotations unregisters any outstanding KVO observers.
|
||
[self removeAnnotations:self.annotations];
|
||
|
||
if (_mbglMap) {
|
||
delete _mbglMap;
|
||
_mbglMap = nullptr;
|
||
}
|
||
if (_mbglView) {
|
||
delete _mbglView;
|
||
_mbglView = nullptr;
|
||
}
|
||
if (_mbglThreadPool) {
|
||
delete _mbglThreadPool;
|
||
_mbglThreadPool = nullptr;
|
||
}
|
||
}
|
||
|
||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(__unused NSDictionary *)change context:(void *)context {
|
||
if ([keyPath isEqualToString:@"contentLayoutRect"] ||
|
||
[keyPath isEqualToString:@"titlebarAppearsTransparent"]) {
|
||
[self adjustContentInsets];
|
||
} else if ([keyPath isEqualToString:@"coordinate"] &&
|
||
[object conformsToProtocol:@protocol(MGLAnnotation)] &&
|
||
![object isKindOfClass:[MGLMultiPoint class]]) {
|
||
id <MGLAnnotation> annotation = object;
|
||
MGLAnnotationTag annotationTag = (MGLAnnotationTag)(NSUInteger)context;
|
||
// We can get here because a subclass registered itself as an observer
|
||
// of the coordinate key path of a non-multipoint annotation but failed
|
||
// to handle the change. This check deters us from treating the
|
||
// subclass’s context as an annotation tag. If the context happens to
|
||
// match a valid annotation tag, the annotation will be unnecessarily
|
||
// but safely updated.
|
||
if (annotation == [self annotationWithTag:annotationTag]) {
|
||
const mbgl::Point<double> point = MGLPointFromLocationCoordinate2D(annotation.coordinate);
|
||
MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
|
||
_mbglMap->updateAnnotation(annotationTag, mbgl::SymbolAnnotation { point, annotationImage.styleIconIdentifier.UTF8String ?: "" });
|
||
if (annotationTag == _selectedAnnotationTag) {
|
||
[self deselectAnnotation:annotation];
|
||
}
|
||
}
|
||
} else if ([keyPath isEqualToString:@"coordinates"] &&
|
||
[object isKindOfClass:[MGLMultiPoint class]]) {
|
||
MGLMultiPoint *annotation = object;
|
||
MGLAnnotationTag annotationTag = (MGLAnnotationTag)(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]);
|
||
// We don't current support shape multipoint annotation selection, but let's make sure
|
||
// deselection is handled just to avoid problems in the future.
|
||
if (annotationTag == _selectedAnnotationTag)
|
||
{
|
||
[self deselectAnnotation:annotation];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
|
||
return [key isEqualToString:@"annotations"] ? YES : [super automaticallyNotifiesObserversForKey:key];
|
||
}
|
||
|
||
- (void)setDelegate:(id<MGLMapViewDelegate>)delegate {
|
||
_delegate = delegate;
|
||
|
||
// Cache checks for delegate method implementations that may be called in a
|
||
// hot loop, namely the annotation style methods.
|
||
_delegateHasAlphasForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:alphaForShapeAnnotation:)];
|
||
_delegateHasStrokeColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:strokeColorForShapeAnnotation:)];
|
||
_delegateHasFillColorsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:fillColorForPolygonAnnotation:)];
|
||
_delegateHasLineWidthsForShapeAnnotations = [_delegate respondsToSelector:@selector(mapView:lineWidthForPolylineAnnotation:)];
|
||
|
||
#pragma clang diagnostic push
|
||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||
if ([self.delegate respondsToSelector:@selector(mapView:regionWillChangeAnimated:)]) {
|
||
NSLog(@"-mapView:regionWillChangeAnimated: is not supported by the macOS SDK, but %@ implements it anyways. "
|
||
@"Please implement -[%@ mapView:cameraWillChangeAnimated:] instead.",
|
||
NSStringFromClass([delegate class]), NSStringFromClass([delegate class]));
|
||
}
|
||
if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)]) {
|
||
NSLog(@"-mapViewRegionIsChanging: is not supported by the macOS SDK, but %@ implements it anyways. "
|
||
@"Please implement -[%@ mapViewCameraIsChanging:] instead.",
|
||
NSStringFromClass([delegate class]), NSStringFromClass([delegate class]));
|
||
}
|
||
if ([self.delegate respondsToSelector:@selector(mapView:regionDidChangeAnimated:)]) {
|
||
NSLog(@"-mapView:regionDidChangeAnimated: is not supported by the macOS SDK, but %@ implements it anyways. "
|
||
@"Please implement -[%@ mapView:cameraDidChangeAnimated:] instead.",
|
||
NSStringFromClass([delegate class]), NSStringFromClass([delegate class]));
|
||
}
|
||
#pragma clang diagnostic pop
|
||
}
|
||
|
||
#pragma mark Style
|
||
|
||
+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingStyle {
|
||
return [NSSet setWithObject:@"styleURL"];
|
||
}
|
||
|
||
- (nonnull NSURL *)styleURL {
|
||
NSString *styleURLString = @(_mbglMap->getStyleURL().c_str()).mgl_stringOrNilIfEmpty;
|
||
return styleURLString ? [NSURL URLWithString:styleURLString] : [MGLStyle streetsStyleURLWithVersion:MGLStyleDefaultVersion];
|
||
}
|
||
|
||
- (void)setStyleURL:(nullable NSURL *)styleURL {
|
||
if (_isTargetingInterfaceBuilder) {
|
||
return;
|
||
}
|
||
|
||
// Default to Streets.
|
||
if (!styleURL) {
|
||
// An access token is required to load any default style, including
|
||
// Streets.
|
||
if (![MGLAccountManager accessToken]) {
|
||
return;
|
||
}
|
||
styleURL = [MGLStyle streetsStyleURLWithVersion:MGLStyleDefaultVersion];
|
||
}
|
||
|
||
styleURL = styleURL.mgl_URLByStandardizingScheme;
|
||
self.style = nil;
|
||
_mbglMap->setStyleURL(styleURL.absoluteString.UTF8String);
|
||
}
|
||
|
||
- (IBAction)reloadStyle:(__unused id)sender {
|
||
NSURL *styleURL = self.styleURL;
|
||
_mbglMap->setStyleURL("");
|
||
self.styleURL = styleURL;
|
||
}
|
||
|
||
- (mbgl::Map *)mbglMap {
|
||
return _mbglMap;
|
||
}
|
||
|
||
#pragma mark View hierarchy and drawing
|
||
|
||
- (void)viewWillMoveToWindow:(NSWindow *)newWindow {
|
||
[self deselectAnnotation:self.selectedAnnotation];
|
||
if (!self.dormant && !newWindow) {
|
||
self.dormant = YES;
|
||
}
|
||
|
||
[self.window removeObserver:self forKeyPath:@"contentLayoutRect"];
|
||
[self.window removeObserver:self forKeyPath:@"titlebarAppearsTransparent"];
|
||
}
|
||
|
||
- (void)viewDidMoveToWindow {
|
||
NSWindow *window = self.window;
|
||
if (self.dormant && window) {
|
||
self.dormant = NO;
|
||
}
|
||
|
||
if (window && _mbglMap->getConstrainMode() == mbgl::ConstrainMode::None) {
|
||
_mbglMap->setConstrainMode(mbgl::ConstrainMode::HeightOnly);
|
||
}
|
||
|
||
[window addObserver:self
|
||
forKeyPath:@"contentLayoutRect"
|
||
options:NSKeyValueObservingOptionInitial
|
||
context:NULL];
|
||
[window addObserver:self
|
||
forKeyPath:@"titlebarAppearsTransparent"
|
||
options:NSKeyValueObservingOptionInitial
|
||
context:NULL];
|
||
}
|
||
|
||
- (BOOL)wantsLayer {
|
||
return YES;
|
||
}
|
||
|
||
- (BOOL)wantsBestResolutionOpenGLSurface {
|
||
// Use an OpenGL layer, except when drawing the designable, which is just
|
||
// ordinary Cocoa.
|
||
return !_isTargetingInterfaceBuilder;
|
||
}
|
||
|
||
- (void)setFrame:(NSRect)frame {
|
||
super.frame = frame;
|
||
if (!NSEqualRects(frame, self.frame)) {
|
||
[self validateTileCacheSize];
|
||
}
|
||
if (!_isTargetingInterfaceBuilder) {
|
||
_mbglMap->setSize({{ static_cast<uint16_t>(self.bounds.size.width),
|
||
static_cast<uint16_t>(self.bounds.size.height) }});
|
||
}
|
||
}
|
||
|
||
- (void)updateConstraints {
|
||
// Place the zoom controls at the lower-right corner of the view.
|
||
[self addConstraint:
|
||
[NSLayoutConstraint constraintWithItem:self
|
||
attribute:NSLayoutAttributeBottom
|
||
relatedBy:NSLayoutRelationEqual
|
||
toItem:_zoomControls
|
||
attribute:NSLayoutAttributeBottom
|
||
multiplier:1
|
||
constant:MGLOrnamentPadding]];
|
||
[self addConstraint:
|
||
[NSLayoutConstraint constraintWithItem:self
|
||
attribute:NSLayoutAttributeTrailing
|
||
relatedBy:NSLayoutRelationEqual
|
||
toItem:_zoomControls
|
||
attribute:NSLayoutAttributeTrailing
|
||
multiplier:1
|
||
constant:MGLOrnamentPadding]];
|
||
|
||
// Center the compass above the zoom controls, assuming that the compass is
|
||
// narrower than the zoom controls.
|
||
[self addConstraint:
|
||
[NSLayoutConstraint constraintWithItem:_compass
|
||
attribute:NSLayoutAttributeCenterX
|
||
relatedBy:NSLayoutRelationEqual
|
||
toItem:_zoomControls
|
||
attribute:NSLayoutAttributeCenterX
|
||
multiplier:1
|
||
constant:0]];
|
||
[self addConstraint:
|
||
[NSLayoutConstraint constraintWithItem:_zoomControls
|
||
attribute:NSLayoutAttributeTop
|
||
relatedBy:NSLayoutRelationEqual
|
||
toItem:_compass
|
||
attribute:NSLayoutAttributeBottom
|
||
multiplier:1
|
||
constant:8]];
|
||
|
||
// Place the logo view in the lower-left corner of the view, accounting for
|
||
// the logo’s alignment rect.
|
||
[self addConstraint:
|
||
[NSLayoutConstraint constraintWithItem:self
|
||
attribute:NSLayoutAttributeBottom
|
||
relatedBy:NSLayoutRelationEqual
|
||
toItem:_logoView
|
||
attribute:NSLayoutAttributeBottom
|
||
multiplier:1
|
||
constant:MGLOrnamentPadding - _logoView.image.alignmentRect.origin.y]];
|
||
[self addConstraint:
|
||
[NSLayoutConstraint constraintWithItem:_logoView
|
||
attribute:NSLayoutAttributeLeading
|
||
relatedBy:NSLayoutRelationEqual
|
||
toItem:self
|
||
attribute:NSLayoutAttributeLeading
|
||
multiplier:1
|
||
constant:MGLOrnamentPadding - _logoView.image.alignmentRect.origin.x]];
|
||
|
||
// Place the attribution view to the right of the logo view and size it to
|
||
// fit the buttons inside.
|
||
[self addConstraint:[NSLayoutConstraint constraintWithItem:_logoView
|
||
attribute:NSLayoutAttributeBaseline
|
||
relatedBy:NSLayoutRelationEqual
|
||
toItem:_attributionView
|
||
attribute:NSLayoutAttributeBaseline
|
||
multiplier:1
|
||
constant:_logoView.image.alignmentRect.origin.y]];
|
||
[self addConstraint:[NSLayoutConstraint constraintWithItem:_attributionView
|
||
attribute:NSLayoutAttributeLeading
|
||
relatedBy:NSLayoutRelationEqual
|
||
toItem:_logoView
|
||
attribute:NSLayoutAttributeTrailing
|
||
multiplier:1
|
||
constant:8]];
|
||
|
||
[super updateConstraints];
|
||
}
|
||
|
||
- (void)renderSync {
|
||
if (!self.dormant) {
|
||
// Enable vertex buffer objects.
|
||
mbgl::gl::InitializeExtensions([](const char *name) {
|
||
static CFBundleRef framework = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.opengl"));
|
||
if (!framework) {
|
||
throw std::runtime_error("Failed to load OpenGL framework.");
|
||
}
|
||
|
||
CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, name, kCFStringEncodingASCII);
|
||
void *symbol = CFBundleGetFunctionPointerForName(framework, str);
|
||
CFRelease(str);
|
||
|
||
return reinterpret_cast<mbgl::gl::glProc>(symbol);
|
||
});
|
||
|
||
_mbglView->updateViewBinding();
|
||
_mbglMap->render(*_mbglView);
|
||
|
||
if (_isPrinting) {
|
||
_isPrinting = NO;
|
||
std::string png = encodePNG(_mbglView->readStillImage());
|
||
NSData *data = [[NSData alloc] initWithBytes:png.data() length:png.size()];
|
||
NSImage *image = [[NSImage alloc] initWithData:data];
|
||
[self performSelector:@selector(printWithImage:) withObject:image afterDelay:0];
|
||
}
|
||
|
||
// [self updateUserLocationAnnotationView];
|
||
}
|
||
}
|
||
|
||
- (void)validateTileCacheSize {
|
||
if (!_mbglMap) {
|
||
return;
|
||
}
|
||
|
||
CGFloat zoomFactor = self.maximumZoomLevel - self.minimumZoomLevel + 1;
|
||
CGFloat cpuFactor = [NSProcessInfo processInfo].processorCount;
|
||
CGFloat memoryFactor = (CGFloat)[NSProcessInfo processInfo].physicalMemory / 1000 / 1000 / 1000;
|
||
CGFloat sizeFactor = (NSWidth(self.bounds) / mbgl::util::tileSize) * (NSHeight(self.bounds) / mbgl::util::tileSize);
|
||
|
||
NSUInteger cacheSize = zoomFactor * cpuFactor * memoryFactor * sizeFactor * 0.5;
|
||
|
||
_mbglMap->setSourceTileCacheSize(cacheSize);
|
||
}
|
||
|
||
- (void)setNeedsGLDisplay {
|
||
MGLAssertIsMainThread();
|
||
|
||
[self.layer setNeedsDisplay];
|
||
}
|
||
|
||
- (void)notifyMapChange:(mbgl::MapChange)change {
|
||
// Ignore map updates when the Map object isn't set.
|
||
if (!_mbglMap) {
|
||
return;
|
||
}
|
||
|
||
switch (change) {
|
||
case mbgl::MapChangeRegionWillChange:
|
||
case mbgl::MapChangeRegionWillChangeAnimated:
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapView:cameraWillChangeAnimated:)]) {
|
||
BOOL animated = change == mbgl::MapChangeRegionWillChangeAnimated;
|
||
[self.delegate mapView:self cameraWillChangeAnimated:animated];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeRegionIsChanging:
|
||
{
|
||
// Update a minimum of UI that needs to stay attached to the map
|
||
// while animating.
|
||
[self updateCompass];
|
||
[self updateAnnotationCallouts];
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapViewCameraIsChanging:)]) {
|
||
[self.delegate mapViewCameraIsChanging:self];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeRegionDidChange:
|
||
case mbgl::MapChangeRegionDidChangeAnimated:
|
||
{
|
||
// Update all UI at the end of an animation or atomic change to the
|
||
// viewport. More expensive updates can happen here, but care should
|
||
// still be taken to minimize the work done here because scroll
|
||
// gesture recognition and momentum scrolling is performed as a
|
||
// series of atomic changes, not an animation.
|
||
[self updateZoomControls];
|
||
[self updateCompass];
|
||
[self updateAnnotationCallouts];
|
||
[self updateAnnotationTrackingAreas];
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:cameraDidChangeAnimated:)]) {
|
||
BOOL animated = change == mbgl::MapChangeRegionDidChangeAnimated;
|
||
[self.delegate mapView:self cameraDidChangeAnimated:animated];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeWillStartLoadingMap:
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapViewWillStartLoadingMap:)]) {
|
||
[self.delegate mapViewWillStartLoadingMap:self];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeDidFinishLoadingMap:
|
||
{
|
||
[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];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeDidFailLoadingMap:
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapViewDidFailLoadingMap:withError:)]) {
|
||
NSError *error = [NSError errorWithDomain:MGLErrorDomain code:0 userInfo:nil];
|
||
[self.delegate mapViewDidFailLoadingMap:self withError:error];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeWillStartRenderingMap:
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingMap:)]) {
|
||
[self.delegate mapViewWillStartRenderingMap:self];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeDidFinishRenderingMap:
|
||
case mbgl::MapChangeDidFinishRenderingMapFullyRendered:
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)]) {
|
||
BOOL fullyRendered = change == mbgl::MapChangeDidFinishRenderingMapFullyRendered;
|
||
[self.delegate mapViewDidFinishRenderingMap:self fullyRendered:fullyRendered];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeWillStartRenderingFrame:
|
||
{
|
||
if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingFrame:)]) {
|
||
[self.delegate mapViewWillStartRenderingFrame:self];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeDidFinishRenderingFrame:
|
||
case mbgl::MapChangeDidFinishRenderingFrameFullyRendered:
|
||
{
|
||
if (_isChangingAnnotationLayers) {
|
||
_isChangingAnnotationLayers = NO;
|
||
[self.style didChangeValueForKey:@"layers"];
|
||
}
|
||
if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingFrame:fullyRendered:)]) {
|
||
BOOL fullyRendered = change == mbgl::MapChangeDidFinishRenderingFrameFullyRendered;
|
||
[self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:fullyRendered];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeDidFinishLoadingStyle:
|
||
{
|
||
self.style = [[MGLStyle alloc] initWithMapView:self];
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didFinishLoadingStyle:)])
|
||
{
|
||
[self.delegate mapView:self didFinishLoadingStyle:self.style];
|
||
}
|
||
break;
|
||
}
|
||
case mbgl::MapChangeSourceDidChange:
|
||
{
|
||
[self installAttributionView];
|
||
self.needsUpdateConstraints = YES;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
#pragma mark Printing
|
||
|
||
- (void)print:(__unused id)sender {
|
||
_isPrinting = YES;
|
||
[self setNeedsGLDisplay];
|
||
}
|
||
|
||
- (void)printWithImage:(NSImage *)image {
|
||
NSImageView *imageView = [[NSImageView alloc] initWithFrame:self.bounds];
|
||
imageView.image = image;
|
||
|
||
NSPrintOperation *op = [NSPrintOperation printOperationWithView:imageView];
|
||
[op runOperation];
|
||
}
|
||
|
||
#pragma mark Viewport
|
||
|
||
+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate {
|
||
return [NSSet setWithObjects:@"latitude", @"longitude", @"camera", nil];
|
||
}
|
||
|
||
- (CLLocationCoordinate2D)centerCoordinate {
|
||
mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
|
||
return MGLLocationCoordinate2DFromLatLng(_mbglMap->getLatLng(padding));
|
||
}
|
||
|
||
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate {
|
||
[self setCenterCoordinate:centerCoordinate animated:NO];
|
||
}
|
||
|
||
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate animated:(BOOL)animated {
|
||
[self willChangeValueForKey:@"centerCoordinate"];
|
||
_mbglMap->setLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate),
|
||
MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets),
|
||
MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
|
||
[self didChangeValueForKey:@"centerCoordinate"];
|
||
}
|
||
|
||
- (void)offsetCenterCoordinateBy:(NSPoint)delta animated:(BOOL)animated {
|
||
[self willChangeValueForKey:@"centerCoordinate"];
|
||
_mbglMap->cancelTransitions();
|
||
_mbglMap->moveBy({ delta.x, delta.y },
|
||
MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
|
||
[self didChangeValueForKey:@"centerCoordinate"];
|
||
}
|
||
|
||
- (CLLocationDegrees)pendingLatitude {
|
||
return _pendingLatitude;
|
||
}
|
||
|
||
- (void)setPendingLatitude:(CLLocationDegrees)pendingLatitude {
|
||
_pendingLatitude = pendingLatitude;
|
||
}
|
||
|
||
- (CLLocationDegrees)pendingLongitude {
|
||
return _pendingLongitude;
|
||
}
|
||
|
||
- (void)setPendingLongitude:(CLLocationDegrees)pendingLongitude {
|
||
_pendingLongitude = pendingLongitude;
|
||
}
|
||
|
||
+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomLevel {
|
||
return [NSSet setWithObject:@"camera"];
|
||
}
|
||
|
||
- (double)zoomLevel {
|
||
return _mbglMap->getZoom();
|
||
}
|
||
|
||
- (void)setZoomLevel:(double)zoomLevel {
|
||
[self setZoomLevel:zoomLevel animated:NO];
|
||
}
|
||
|
||
- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated {
|
||
[self willChangeValueForKey:@"zoomLevel"];
|
||
_mbglMap->setZoom(zoomLevel,
|
||
MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets),
|
||
MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
|
||
[self didChangeValueForKey:@"zoomLevel"];
|
||
}
|
||
|
||
- (void)zoomBy:(double)zoomDelta animated:(BOOL)animated {
|
||
[self setZoomLevel:self.zoomLevel + zoomDelta animated:animated];
|
||
}
|
||
|
||
- (void)scaleBy:(double)scaleFactor atPoint:(NSPoint)point animated:(BOOL)animated {
|
||
[self willChangeValueForKey:@"centerCoordinate"];
|
||
[self willChangeValueForKey:@"zoomLevel"];
|
||
mbgl::ScreenCoordinate center(point.x, self.bounds.size.height - point.y);
|
||
_mbglMap->scaleBy(scaleFactor, center, MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
|
||
[self didChangeValueForKey:@"zoomLevel"];
|
||
[self didChangeValueForKey:@"centerCoordinate"];
|
||
}
|
||
|
||
- (void)setMinimumZoomLevel:(double)minimumZoomLevel
|
||
{
|
||
_mbglMap->setMinZoom(minimumZoomLevel);
|
||
[self validateTileCacheSize];
|
||
}
|
||
|
||
- (void)setMaximumZoomLevel:(double)maximumZoomLevel
|
||
{
|
||
_mbglMap->setMaxZoom(maximumZoomLevel);
|
||
[self validateTileCacheSize];
|
||
}
|
||
|
||
- (double)maximumZoomLevel {
|
||
return _mbglMap->getMaxZoom();
|
||
}
|
||
|
||
- (double)minimumZoomLevel {
|
||
return _mbglMap->getMinZoom();
|
||
}
|
||
|
||
/// Respond to a click on the zoom control.
|
||
- (IBAction)zoomInOrOut:(NSSegmentedControl *)sender {
|
||
switch (sender.selectedSegment) {
|
||
case 0:
|
||
// Zoom out.
|
||
[self moveToEndOfParagraph:sender];
|
||
break;
|
||
case 1:
|
||
// Zoom in.
|
||
[self moveToBeginningOfParagraph:sender];
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingDirection {
|
||
return [NSSet setWithObject:@"camera"];
|
||
}
|
||
|
||
- (CLLocationDirection)direction {
|
||
return mbgl::util::wrap(_mbglMap->getBearing(), 0., 360.);
|
||
}
|
||
|
||
- (void)setDirection:(CLLocationDirection)direction {
|
||
[self setDirection:direction animated:NO];
|
||
}
|
||
|
||
- (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated {
|
||
[self willChangeValueForKey:@"direction"];
|
||
_mbglMap->setBearing(direction,
|
||
MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets),
|
||
MGLDurationInSeconds(animated ? MGLAnimationDuration : 0));
|
||
[self didChangeValueForKey:@"direction"];
|
||
}
|
||
|
||
- (void)offsetDirectionBy:(CLLocationDegrees)delta animated:(BOOL)animated {
|
||
[self setDirection:_mbglMap->getBearing() + delta animated:animated];
|
||
}
|
||
|
||
+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCamera {
|
||
return [NSSet setWithObjects:@"latitude", @"longitude", @"centerCoordinate", @"zoomLevel", @"direction", nil];
|
||
}
|
||
|
||
- (MGLMapCamera *)camera {
|
||
mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
|
||
return [self cameraForCameraOptions:_mbglMap->getCameraOptions(padding)];
|
||
}
|
||
|
||
- (void)setCamera:(MGLMapCamera *)camera {
|
||
[self setCamera:camera animated:NO];
|
||
}
|
||
|
||
- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated {
|
||
[self setCamera:camera withDuration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:NULL];
|
||
}
|
||
|
||
- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion {
|
||
_mbglMap->cancelTransitions();
|
||
if ([self.camera isEqual:camera]) {
|
||
return;
|
||
}
|
||
|
||
mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera];
|
||
mbgl::AnimationOptions animationOptions;
|
||
if (duration > 0) {
|
||
animationOptions.duration.emplace(MGLDurationInSeconds(duration));
|
||
animationOptions.easing.emplace(MGLUnitBezierForMediaTimingFunction(function));
|
||
}
|
||
if (completion) {
|
||
animationOptions.transitionFinishFn = [completion]() {
|
||
// Must run asynchronously after the transition is completely over.
|
||
// Otherwise, a call to -setCamera: within the completion handler
|
||
// would reenter the completion handler’s caller.
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
completion();
|
||
});
|
||
};
|
||
}
|
||
|
||
[self willChangeValueForKey:@"camera"];
|
||
_mbglMap->easeTo(cameraOptions, animationOptions);
|
||
[self didChangeValueForKey:@"camera"];
|
||
}
|
||
|
||
- (void)flyToCamera:(MGLMapCamera *)camera completionHandler:(nullable void (^)(void))completion {
|
||
[self flyToCamera:camera withDuration:-1 completionHandler:completion];
|
||
}
|
||
|
||
- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion {
|
||
[self flyToCamera:camera withDuration:duration peakAltitude:-1 completionHandler:completion];
|
||
}
|
||
|
||
- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion {
|
||
_mbglMap->cancelTransitions();
|
||
if ([self.camera isEqual:camera]) {
|
||
return;
|
||
}
|
||
|
||
mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera];
|
||
mbgl::AnimationOptions animationOptions;
|
||
if (duration >= 0) {
|
||
animationOptions.duration = MGLDurationInSeconds(duration);
|
||
}
|
||
if (peakAltitude >= 0) {
|
||
CLLocationDegrees peakLatitude = (self.centerCoordinate.latitude + camera.centerCoordinate.latitude) / 2;
|
||
CLLocationDegrees peakPitch = (self.camera.pitch + camera.pitch) / 2;
|
||
animationOptions.minZoom = MGLZoomLevelForAltitude(peakAltitude, peakPitch,
|
||
peakLatitude, self.frame.size);
|
||
}
|
||
if (completion) {
|
||
animationOptions.transitionFinishFn = [completion]() {
|
||
// Must run asynchronously after the transition is completely over.
|
||
// Otherwise, a call to -setCamera: within the completion handler
|
||
// would reenter the completion handler’s caller.
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
completion();
|
||
});
|
||
};
|
||
}
|
||
|
||
[self willChangeValueForKey:@"camera"];
|
||
_mbglMap->flyTo(cameraOptions, animationOptions);
|
||
[self didChangeValueForKey:@"camera"];
|
||
}
|
||
|
||
/// Returns a CameraOptions object that specifies parameters for animating to
|
||
/// the given camera.
|
||
- (mbgl::CameraOptions)cameraOptionsObjectForAnimatingToCamera:(MGLMapCamera *)camera {
|
||
mbgl::CameraOptions options;
|
||
options.center = MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate);
|
||
options.padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
|
||
options.zoom = MGLZoomLevelForAltitude(camera.altitude, camera.pitch,
|
||
camera.centerCoordinate.latitude,
|
||
self.frame.size);
|
||
if (camera.heading >= 0) {
|
||
options.angle = MGLRadiansFromDegrees(-camera.heading);
|
||
}
|
||
if (camera.pitch >= 0) {
|
||
options.pitch = MGLRadiansFromDegrees(camera.pitch);
|
||
}
|
||
return options;
|
||
}
|
||
|
||
+ (NSSet *)keyPathsForValuesAffectingVisibleCoordinateBounds {
|
||
return [NSSet setWithObjects:@"centerCoordinate", @"zoomLevel", @"direction", @"bounds", nil];
|
||
}
|
||
|
||
- (MGLCoordinateBounds)visibleCoordinateBounds {
|
||
return [self convertRect:self.bounds toCoordinateBoundsFromView:self];
|
||
}
|
||
|
||
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds {
|
||
[self setVisibleCoordinateBounds:bounds animated:NO];
|
||
}
|
||
|
||
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds animated:(BOOL)animated {
|
||
[self setVisibleCoordinateBounds:bounds edgePadding:NSEdgeInsetsZero animated:animated];
|
||
}
|
||
|
||
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated {
|
||
_mbglMap->cancelTransitions();
|
||
|
||
mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(insets);
|
||
padding += MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
|
||
mbgl::CameraOptions cameraOptions = _mbglMap->cameraForLatLngBounds(MGLLatLngBoundsFromCoordinateBounds(bounds), padding);
|
||
mbgl::AnimationOptions animationOptions;
|
||
if (animated) {
|
||
animationOptions.duration = MGLDurationInSeconds(MGLAnimationDuration);
|
||
}
|
||
|
||
[self willChangeValueForKey:@"visibleCoordinateBounds"];
|
||
animationOptions.transitionFinishFn = ^() {
|
||
[self didChangeValueForKey:@"visibleCoordinateBounds"];
|
||
};
|
||
_mbglMap->easeTo(cameraOptions, animationOptions);
|
||
}
|
||
|
||
- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds {
|
||
return [self cameraThatFitsCoordinateBounds:bounds edgePadding:NSEdgeInsetsZero];
|
||
}
|
||
|
||
- (MGLMapCamera *)cameraThatFitsCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets {
|
||
mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(insets);
|
||
padding += MGLEdgeInsetsFromNSEdgeInsets(self.contentInsets);
|
||
mbgl::CameraOptions cameraOptions = _mbglMap->cameraForLatLngBounds(MGLLatLngBoundsFromCoordinateBounds(bounds), padding);
|
||
return [self cameraForCameraOptions:cameraOptions];
|
||
}
|
||
|
||
- (MGLMapCamera *)cameraForCameraOptions:(const mbgl::CameraOptions &)cameraOptions {
|
||
CLLocationCoordinate2D centerCoordinate = MGLLocationCoordinate2DFromLatLng(cameraOptions.center ? *cameraOptions.center : _mbglMap->getLatLng());
|
||
double zoomLevel = cameraOptions.zoom ? *cameraOptions.zoom : self.zoomLevel;
|
||
CLLocationDirection direction = cameraOptions.angle ? -MGLDegreesFromRadians(*cameraOptions.angle) : self.direction;
|
||
CGFloat pitch = cameraOptions.pitch ? MGLDegreesFromRadians(*cameraOptions.pitch) : _mbglMap->getPitch();
|
||
CLLocationDistance altitude = MGLAltitudeForZoomLevel(zoomLevel, pitch,
|
||
centerCoordinate.latitude,
|
||
self.frame.size);
|
||
return [MGLMapCamera cameraLookingAtCenterCoordinate:centerCoordinate
|
||
fromDistance:altitude
|
||
pitch:pitch
|
||
heading:direction];
|
||
}
|
||
|
||
- (void)setAutomaticallyAdjustsContentInsets:(BOOL)automaticallyAdjustsContentInsets {
|
||
_automaticallyAdjustsContentInsets = automaticallyAdjustsContentInsets;
|
||
[self adjustContentInsets];
|
||
}
|
||
|
||
/// Updates `contentInsets` to reflect the current window geometry.
|
||
- (void)adjustContentInsets {
|
||
if (!_automaticallyAdjustsContentInsets) {
|
||
return;
|
||
}
|
||
|
||
NSEdgeInsets contentInsets = self.contentInsets;
|
||
if ((self.window.styleMask & NSFullSizeContentViewWindowMask)
|
||
&& !self.window.titlebarAppearsTransparent) {
|
||
NSRect contentLayoutRect = [self convertRect:self.window.contentLayoutRect fromView:nil];
|
||
if (NSMaxX(contentLayoutRect) > 0 && NSMaxY(contentLayoutRect) > 0) {
|
||
contentInsets = NSEdgeInsetsMake(NSHeight(self.bounds) - NSMaxY(contentLayoutRect),
|
||
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];
|
||
}
|
||
|
||
- (void)setContentInsets:(NSEdgeInsets)contentInsets animated:(BOOL)animated {
|
||
if (NSEdgeInsetsEqual(contentInsets, self.contentInsets)) {
|
||
return;
|
||
}
|
||
|
||
// After adjusting the content insets, move the center coordinate from the
|
||
// old frame of reference to the new one represented by the newly set
|
||
// content insets.
|
||
CLLocationCoordinate2D oldCenter = self.centerCoordinate;
|
||
_contentInsets = contentInsets;
|
||
[self setCenterCoordinate:oldCenter animated:animated];
|
||
}
|
||
|
||
#pragma mark Mouse events and gestures
|
||
|
||
- (BOOL)acceptsFirstResponder {
|
||
return YES;
|
||
}
|
||
|
||
/// Drag to pan, plus drag to zoom, rotate, and tilt when a modifier key is held
|
||
/// down.
|
||
- (void)handlePanGesture:(NSPanGestureRecognizer *)gestureRecognizer {
|
||
NSPoint delta = [gestureRecognizer translationInView:self];
|
||
NSPoint endPoint = [gestureRecognizer locationInView:self];
|
||
NSPoint startPoint = NSMakePoint(endPoint.x - delta.x, endPoint.y - delta.y);
|
||
|
||
NSEventModifierFlags flags = [NSApp currentEvent].modifierFlags;
|
||
if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
|
||
[self.window invalidateCursorRectsForView:self];
|
||
_mbglMap->setGestureInProgress(true);
|
||
|
||
if (![self isPanningWithGesture]) {
|
||
// Hide the cursor except when panning.
|
||
CGDisplayHideCursor(kCGDirectMainDisplay);
|
||
_didHideCursorDuringGesture = YES;
|
||
}
|
||
} else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
|
||
|| gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
|
||
_mbglMap->setGestureInProgress(false);
|
||
[self.window invalidateCursorRectsForView:self];
|
||
|
||
if (_didHideCursorDuringGesture) {
|
||
_didHideCursorDuringGesture = NO;
|
||
// Move the cursor back to the start point and show it again, creating
|
||
// the illusion that it has stayed in place during the entire gesture.
|
||
CGPoint cursorPoint = [self convertPoint:startPoint toView:nil];
|
||
cursorPoint = [self.window convertRectToScreen:{ startPoint, NSZeroSize }].origin;
|
||
cursorPoint.y = [NSScreen mainScreen].frame.size.height - cursorPoint.y;
|
||
CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cursorPoint);
|
||
CGDisplayShowCursor(kCGDirectMainDisplay);
|
||
}
|
||
}
|
||
|
||
if (flags & NSShiftKeyMask) {
|
||
// Shift-drag to zoom.
|
||
if (!self.zoomEnabled) {
|
||
return;
|
||
}
|
||
|
||
_mbglMap->cancelTransitions();
|
||
|
||
if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
|
||
_scaleAtBeginningOfGesture = _mbglMap->getScale();
|
||
} else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
|
||
CGFloat newZoomLevel = log2f(_scaleAtBeginningOfGesture) - delta.y / 75;
|
||
[self scaleBy:powf(2, newZoomLevel) / _mbglMap->getScale() atPoint:startPoint animated:NO];
|
||
}
|
||
} else if (flags & NSAlternateKeyMask) {
|
||
// Option-drag to rotate and/or tilt.
|
||
_mbglMap->cancelTransitions();
|
||
|
||
if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
|
||
_directionAtBeginningOfGesture = self.direction;
|
||
_pitchAtBeginningOfGesture = _mbglMap->getPitch();
|
||
} else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
|
||
mbgl::ScreenCoordinate center(startPoint.x, self.bounds.size.height - startPoint.y);
|
||
if (self.rotateEnabled) {
|
||
CLLocationDirection newDirection = _directionAtBeginningOfGesture - delta.x / 10;
|
||
[self willChangeValueForKey:@"direction"];
|
||
_mbglMap->setBearing(newDirection, center);
|
||
[self didChangeValueForKey:@"direction"];
|
||
}
|
||
if (self.pitchEnabled) {
|
||
_mbglMap->setPitch(_pitchAtBeginningOfGesture + delta.y / 5, center);
|
||
}
|
||
}
|
||
} else if (self.scrollEnabled) {
|
||
// Otherwise, drag to pan.
|
||
_mbglMap->cancelTransitions();
|
||
|
||
if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
|
||
delta.y *= -1;
|
||
[self offsetCenterCoordinateBy:delta animated:NO];
|
||
[gestureRecognizer setTranslation:NSZeroPoint inView:nil];
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Returns whether the user is panning using a gesture.
|
||
- (BOOL)isPanningWithGesture {
|
||
NSGestureRecognizerState state = _panGestureRecognizer.state;
|
||
NSEventModifierFlags flags = [NSApp currentEvent].modifierFlags;
|
||
return ((state == NSGestureRecognizerStateBegan || state == NSGestureRecognizerStateChanged)
|
||
&& !(flags & NSShiftKeyMask || flags & NSAlternateKeyMask));
|
||
}
|
||
|
||
/// Pinch to zoom.
|
||
- (void)handleMagnificationGesture:(NSMagnificationGestureRecognizer *)gestureRecognizer {
|
||
if (!self.zoomEnabled) {
|
||
return;
|
||
}
|
||
|
||
_mbglMap->cancelTransitions();
|
||
|
||
if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
|
||
_mbglMap->setGestureInProgress(true);
|
||
_scaleAtBeginningOfGesture = _mbglMap->getScale();
|
||
} else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
|
||
NSPoint zoomInPoint = [gestureRecognizer locationInView:self];
|
||
mbgl::ScreenCoordinate center(zoomInPoint.x, self.bounds.size.height - zoomInPoint.y);
|
||
if (gestureRecognizer.magnification > -1) {
|
||
[self willChangeValueForKey:@"zoomLevel"];
|
||
[self willChangeValueForKey:@"centerCoordinate"];
|
||
_mbglMap->setScale(_scaleAtBeginningOfGesture * (1 + gestureRecognizer.magnification), center);
|
||
[self didChangeValueForKey:@"centerCoordinate"];
|
||
[self didChangeValueForKey:@"zoomLevel"];
|
||
}
|
||
} else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
|
||
|| gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
|
||
_mbglMap->setGestureInProgress(false);
|
||
}
|
||
}
|
||
|
||
/// Click or tap to select an annotation.
|
||
- (void)handleClickGesture:(NSClickGestureRecognizer *)gestureRecognizer {
|
||
if (gestureRecognizer.state != NSGestureRecognizerStateEnded
|
||
|| [self subviewContainingGesture:gestureRecognizer]) {
|
||
return;
|
||
}
|
||
|
||
NSPoint gesturePoint = [gestureRecognizer locationInView:self];
|
||
MGLAnnotationTag hitAnnotationTag = [self annotationTagAtPoint:gesturePoint persistingResults:YES];
|
||
if (hitAnnotationTag != MGLAnnotationTagNotFound) {
|
||
if (hitAnnotationTag != _selectedAnnotationTag) {
|
||
id <MGLAnnotation> annotation = [self annotationWithTag:hitAnnotationTag];
|
||
NSAssert(annotation, @"Cannot select nonexistent annotation with tag %u", hitAnnotationTag);
|
||
[self selectAnnotation:annotation];
|
||
}
|
||
} else {
|
||
[self deselectAnnotation:self.selectedAnnotation];
|
||
}
|
||
}
|
||
|
||
/// 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 scaleBy:2 atPoint:gesturePoint animated:YES];
|
||
}
|
||
|
||
- (void)smartMagnifyWithEvent:(NSEvent *)event {
|
||
if (!self.zoomEnabled) {
|
||
return;
|
||
}
|
||
|
||
_mbglMap->cancelTransitions();
|
||
|
||
// Tap with two fingers (“right-click”) to zoom out on mice but not trackpads.
|
||
NSPoint gesturePoint = [self convertPoint:event.locationInWindow fromView:nil];
|
||
[self scaleBy:0.5 atPoint:gesturePoint animated:YES];
|
||
}
|
||
|
||
/// Rotate fingers to rotate.
|
||
- (void)handleRotationGesture:(NSRotationGestureRecognizer *)gestureRecognizer {
|
||
if (!self.rotateEnabled) {
|
||
return;
|
||
}
|
||
|
||
_mbglMap->cancelTransitions();
|
||
|
||
if (gestureRecognizer.state == NSGestureRecognizerStateBegan) {
|
||
_mbglMap->setGestureInProgress(true);
|
||
_directionAtBeginningOfGesture = self.direction;
|
||
} else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) {
|
||
NSPoint rotationPoint = [gestureRecognizer locationInView:self];
|
||
mbgl::ScreenCoordinate center(rotationPoint.x, self.bounds.size.height - rotationPoint.y);
|
||
_mbglMap->setBearing(_directionAtBeginningOfGesture + gestureRecognizer.rotationInDegrees, center);
|
||
} else if (gestureRecognizer.state == NSGestureRecognizerStateEnded
|
||
|| gestureRecognizer.state == NSGestureRecognizerStateCancelled) {
|
||
_mbglMap->setGestureInProgress(false);
|
||
}
|
||
}
|
||
|
||
- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(__unused NSEventGestureAxis)axis {
|
||
// Track both horizontal and vertical swipes in -scrollWheel:.
|
||
return YES;
|
||
}
|
||
|
||
- (void)scrollWheel:(NSEvent *)event {
|
||
// https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKitOlderNotes/#10_7Dragging
|
||
BOOL isScrollWheel = event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone && !event.hasPreciseScrollingDeltas;
|
||
if (isScrollWheel || [[NSUserDefaults standardUserDefaults] boolForKey:MGLScrollWheelZoomsMapViewDefaultKey]) {
|
||
// A traditional, vertical scroll wheel zooms instead of panning.
|
||
if (self.zoomEnabled) {
|
||
const double delta =
|
||
event.scrollingDeltaY / ([event hasPreciseScrollingDeltas] ? 100 : 10);
|
||
if (delta != 0) {
|
||
double scale = 2.0 / (1.0 + std::exp(-std::abs(delta)));
|
||
|
||
// Zooming out.
|
||
if (delta < 0) {
|
||
scale = 1.0 / scale;
|
||
}
|
||
|
||
NSPoint gesturePoint = [self convertPoint:event.locationInWindow fromView:nil];
|
||
[self scaleBy:scale atPoint:gesturePoint animated:NO];
|
||
}
|
||
}
|
||
} else if (self.scrollEnabled
|
||
&& _magnificationGestureRecognizer.state == NSGestureRecognizerStatePossible
|
||
&& _rotationGestureRecognizer.state == NSGestureRecognizerStatePossible) {
|
||
// Scroll to pan.
|
||
_mbglMap->cancelTransitions();
|
||
|
||
CGFloat x = event.scrollingDeltaX;
|
||
CGFloat y = event.scrollingDeltaY;
|
||
if (x || y) {
|
||
[self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO];
|
||
}
|
||
|
||
// Drift pan.
|
||
if (event.momentumPhase != NSEventPhaseNone) {
|
||
[self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO];
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Returns the subview that the gesture is located in.
|
||
- (NSView *)subviewContainingGesture:(NSGestureRecognizer *)gestureRecognizer {
|
||
if (NSPointInRect([gestureRecognizer locationInView:self.compass], self.compass.bounds)) {
|
||
return self.compass;
|
||
}
|
||
if (NSPointInRect([gestureRecognizer locationInView:self.zoomControls], self.zoomControls.bounds)) {
|
||
return self.zoomControls;
|
||
}
|
||
if (NSPointInRect([gestureRecognizer locationInView:self.attributionView], self.attributionView.bounds)) {
|
||
return self.attributionView;
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
#pragma mark NSGestureRecognizerDelegate methods
|
||
- (BOOL)gestureRecognizer:(NSGestureRecognizer *)gestureRecognizer shouldAttemptToRecognizeWithEvent:(NSEvent *)event {
|
||
if (gestureRecognizer == _singleClickRecognizer) {
|
||
if (!self.selectedAnnotation) {
|
||
NSPoint gesturePoint = [self convertPoint:[event locationInWindow] fromView:nil];
|
||
MGLAnnotationTag hitAnnotationTag = [self annotationTagAtPoint:gesturePoint persistingResults:NO];
|
||
if (hitAnnotationTag == MGLAnnotationTagNotFound) {
|
||
return NO;
|
||
}
|
||
}
|
||
}
|
||
return YES;
|
||
}
|
||
|
||
#pragma mark Keyboard events
|
||
|
||
- (void)keyDown:(NSEvent *)event {
|
||
if (event.modifierFlags & NSNumericPadKeyMask) {
|
||
// This is the recommended way to handle arrow key presses, causing
|
||
// methods like -moveUp: and -moveToBeginningOfParagraph: to be called
|
||
// for various standard keybindings.
|
||
[self interpretKeyEvents:@[event]];
|
||
} else {
|
||
[super keyDown:event];
|
||
}
|
||
}
|
||
|
||
- (IBAction)moveUp:(__unused id)sender {
|
||
[self offsetCenterCoordinateBy:NSMakePoint(0, MGLKeyPanningIncrement) animated:YES];
|
||
}
|
||
|
||
- (IBAction)moveDown:(__unused id)sender {
|
||
[self offsetCenterCoordinateBy:NSMakePoint(0, -MGLKeyPanningIncrement) animated:YES];
|
||
}
|
||
|
||
- (IBAction)moveLeft:(__unused id)sender {
|
||
[self offsetCenterCoordinateBy:NSMakePoint(MGLKeyPanningIncrement, 0) animated:YES];
|
||
}
|
||
|
||
- (IBAction)moveRight:(__unused id)sender {
|
||
[self offsetCenterCoordinateBy:NSMakePoint(-MGLKeyPanningIncrement, 0) animated:YES];
|
||
}
|
||
|
||
- (IBAction)moveToBeginningOfParagraph:(__unused id)sender {
|
||
if (self.zoomEnabled) {
|
||
[self zoomBy:1 animated:YES];
|
||
}
|
||
}
|
||
|
||
- (IBAction)moveToEndOfParagraph:(__unused id)sender {
|
||
if (self.zoomEnabled) {
|
||
[self zoomBy:-1 animated:YES];
|
||
}
|
||
}
|
||
|
||
- (IBAction)moveWordLeft:(__unused id)sender {
|
||
if (self.rotateEnabled) {
|
||
[self offsetDirectionBy:MGLKeyRotationIncrement animated:YES];
|
||
}
|
||
}
|
||
|
||
- (IBAction)moveWordRight:(__unused id)sender {
|
||
if (self.rotateEnabled) {
|
||
[self offsetDirectionBy:-MGLKeyRotationIncrement animated:YES];
|
||
}
|
||
}
|
||
|
||
- (void)setZoomEnabled:(BOOL)zoomEnabled {
|
||
_zoomEnabled = zoomEnabled;
|
||
_zoomControls.enabled = zoomEnabled;
|
||
_zoomControls.hidden = !zoomEnabled;
|
||
}
|
||
|
||
- (void)setRotateEnabled:(BOOL)rotateEnabled {
|
||
_rotateEnabled = rotateEnabled;
|
||
_compass.enabled = rotateEnabled;
|
||
_compass.hidden = !rotateEnabled;
|
||
}
|
||
|
||
#pragma mark Ornaments
|
||
|
||
/// Updates the zoom controls’ enabled state based on the current zoom level.
|
||
- (void)updateZoomControls {
|
||
[_zoomControls setEnabled:self.zoomLevel > self.minimumZoomLevel forSegment:0];
|
||
[_zoomControls setEnabled:self.zoomLevel < self.maximumZoomLevel forSegment:1];
|
||
}
|
||
|
||
/// Updates the compass to point in the same direction as the map.
|
||
- (void)updateCompass {
|
||
// The circular slider control goes counterclockwise, whereas our map
|
||
// measures its direction clockwise.
|
||
_compass.doubleValue = -self.direction;
|
||
}
|
||
|
||
- (IBAction)rotate:(NSSlider *)sender {
|
||
[self setDirection:-sender.doubleValue animated:YES];
|
||
}
|
||
|
||
- (IBAction)giveFeedback:(id)sender {
|
||
CLLocationCoordinate2D centerCoordinate = self.centerCoordinate;
|
||
double zoomLevel = self.zoomLevel;
|
||
NSMutableArray *urls = [NSMutableArray array];
|
||
for (MGLAttributionInfo *info in [self.style attributionInfosWithFontSize:0 linkColor:nil]) {
|
||
NSURL *url = [info feedbackURLAtCenterCoordinate:centerCoordinate zoomLevel:zoomLevel];
|
||
if (url) {
|
||
[urls addObject:url];
|
||
}
|
||
}
|
||
[[NSWorkspace sharedWorkspace] openURLs:urls
|
||
withAppBundleIdentifier:nil
|
||
options:0
|
||
additionalEventParamDescriptor:nil
|
||
launchIdentifiers:nil];
|
||
}
|
||
|
||
#pragma mark Annotations
|
||
|
||
- (nullable NS_ARRAY_OF(id <MGLAnnotation>) *)annotations {
|
||
if (_annotationContextsByAnnotationTag.empty()) {
|
||
return nil;
|
||
}
|
||
|
||
// Map all the annotation tags to the annotations themselves.
|
||
std::vector<id <MGLAnnotation>> annotations;
|
||
std::transform(_annotationContextsByAnnotationTag.begin(),
|
||
_annotationContextsByAnnotationTag.end(),
|
||
std::back_inserter(annotations),
|
||
^ id <MGLAnnotation> (const std::pair<MGLAnnotationTag, MGLAnnotationContext> &pair) {
|
||
return pair.second.annotation;
|
||
});
|
||
return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()];
|
||
}
|
||
|
||
- (nullable NS_ARRAY_OF(id <MGLAnnotation>) *)visibleAnnotations
|
||
{
|
||
return [self visibleFeaturesInRect:self.bounds];
|
||
}
|
||
|
||
- (nullable NS_ARRAY_OF(id <MGLAnnotation>) *)visibleAnnotationsInRect:(CGRect)rect
|
||
{
|
||
if (_annotationContextsByAnnotationTag.empty())
|
||
{
|
||
return nil;
|
||
}
|
||
|
||
std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:rect];
|
||
if (annotationTags.size())
|
||
{
|
||
NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:annotationTags.size()];
|
||
|
||
for (auto const& annotationTag: annotationTags)
|
||
{
|
||
if (!_annotationContextsByAnnotationTag.count(annotationTag))
|
||
{
|
||
continue;
|
||
}
|
||
MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
|
||
[annotations addObject:annotationContext.annotation];
|
||
}
|
||
|
||
return [annotations copy];
|
||
}
|
||
|
||
return nil;
|
||
}
|
||
|
||
/// Returns the annotation assigned the given tag. Cheap.
|
||
- (id <MGLAnnotation>)annotationWithTag:(MGLAnnotationTag)tag {
|
||
if (!_annotationContextsByAnnotationTag.count(tag)) {
|
||
return nil;
|
||
}
|
||
|
||
MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag[tag];
|
||
return annotationContext.annotation;
|
||
}
|
||
|
||
/// Returns the annotation tag assigned to the given annotation. Relatively expensive.
|
||
- (MGLAnnotationTag)annotationTagForAnnotation:(id <MGLAnnotation>)annotation {
|
||
if (!annotation || _annotationTagsByAnnotation.count(annotation) == 0) {
|
||
return MGLAnnotationTagNotFound;
|
||
}
|
||
|
||
return _annotationTagsByAnnotation.at(annotation);
|
||
}
|
||
|
||
- (void)addAnnotation:(id <MGLAnnotation>)annotation {
|
||
if (annotation) {
|
||
[self addAnnotations:@[annotation]];
|
||
}
|
||
}
|
||
|
||
- (void)addAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations {
|
||
if (!annotations) {
|
||
return;
|
||
}
|
||
|
||
[self willChangeValueForKey:@"annotations"];
|
||
|
||
BOOL delegateHasImagesForAnnotations = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)];
|
||
|
||
for (id <MGLAnnotation> annotation in annotations) {
|
||
NSAssert([annotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation");
|
||
|
||
// adding the same annotation object twice is a no-op
|
||
if ([self.annotations containsObject:annotation])
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if ([annotation isKindOfClass:[MGLMultiPoint class]]) {
|
||
// The multipoint knows how to style itself (with the map view’s help).
|
||
MGLMultiPoint *multiPoint = (MGLMultiPoint *)annotation;
|
||
if (!multiPoint.pointCount) {
|
||
continue;
|
||
}
|
||
|
||
_isChangingAnnotationLayers = YES;
|
||
MGLAnnotationTag annotationTag = _mbglMap->addAnnotation([multiPoint annotationObjectWithDelegate:self]);
|
||
MGLAnnotationContext 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:[MGLMultiPolyline class]]
|
||
&& ![annotation isKindOfClass:[MGLMultiPolygon class]]
|
||
&& ![annotation isKindOfClass:[MGLShapeCollection class]]
|
||
&& ![annotation isKindOfClass:[MGLPointCollection class]]) {
|
||
MGLAnnotationImage *annotationImage = nil;
|
||
if (delegateHasImagesForAnnotations) {
|
||
annotationImage = [self.delegate mapView:self imageForAnnotation:annotation];
|
||
}
|
||
if (!annotationImage) {
|
||
annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName];
|
||
}
|
||
if (!annotationImage) {
|
||
annotationImage = self.defaultAnnotationImage;
|
||
}
|
||
|
||
NSString *symbolName = annotationImage.styleIconIdentifier;
|
||
if (!symbolName) {
|
||
symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
|
||
annotationImage.styleIconIdentifier = symbolName;
|
||
}
|
||
|
||
if (!self.annotationImagesByIdentifier[annotationImage.reuseIdentifier]) {
|
||
self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage;
|
||
[self installAnnotationImage:annotationImage];
|
||
}
|
||
|
||
MGLAnnotationTag annotationTag = _mbglMap->addAnnotation(mbgl::SymbolAnnotation {
|
||
MGLPointFromLocationCoordinate2D(annotation.coordinate),
|
||
symbolName.UTF8String ?: ""
|
||
});
|
||
|
||
MGLAnnotationContext context;
|
||
context.annotation = annotation;
|
||
context.imageReuseIdentifier = annotationImage.reuseIdentifier;
|
||
_annotationContextsByAnnotationTag[annotationTag] = context;
|
||
_annotationTagsByAnnotation[annotation] = annotationTag;
|
||
|
||
if ([annotation isKindOfClass:[NSObject class]]) {
|
||
NSAssert(![annotation isKindOfClass:[MGLMultiPoint class]], @"Point annotation should not be MGLMultiPoint.");
|
||
[(NSObject *)annotation addObserver:self forKeyPath:@"coordinate" options:0 context:(void *)(NSUInteger)annotationTag];
|
||
}
|
||
|
||
// Opt into potentially expensive tooltip tracking areas.
|
||
if ([annotation 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.
|
||
- (MGLAnnotationImage *)defaultAnnotationImage {
|
||
NSImage *image = MGLDefaultMarkerImage();
|
||
NSRect alignmentRect = image.alignmentRect;
|
||
alignmentRect.origin.y = NSMidY(alignmentRect);
|
||
alignmentRect.size.height /= 2;
|
||
image.alignmentRect = alignmentRect;
|
||
return [MGLAnnotationImage annotationImageWithImage:image
|
||
reuseIdentifier:MGLDefaultStyleMarkerSymbolName];
|
||
}
|
||
|
||
/// Sends the raw pixel data of the annotation image’s image to mbgl and
|
||
/// calculates state needed for hit testing later.
|
||
- (void)installAnnotationImage:(MGLAnnotationImage *)annotationImage {
|
||
NSString *iconIdentifier = annotationImage.styleIconIdentifier;
|
||
self.annotationImagesByIdentifier[annotationImage.reuseIdentifier] = annotationImage;
|
||
|
||
NSImage *image = annotationImage.image;
|
||
NSSize size = image.size;
|
||
if (size.width == 0 || size.height == 0 || !image.valid) {
|
||
// Can’t create an empty sprite. An image that hasn’t loaded is also useless.
|
||
return;
|
||
}
|
||
|
||
std::shared_ptr<mbgl::SpriteImage> sprite(annotationImage.image.mgl_spriteImage);
|
||
_mbglMap->addAnnotationIcon(iconIdentifier.UTF8String, sprite);
|
||
|
||
// Create a slop area with a “radius” equal to the annotation image’s entire
|
||
// size, allowing the eventual click to be on any point within this image.
|
||
// Union this slop area with any existing slop areas.
|
||
_unionedAnnotationImageSize = NSMakeSize(MAX(_unionedAnnotationImageSize.width, size.width),
|
||
MAX(_unionedAnnotationImageSize.height, size.height));
|
||
|
||
// Opt into potentially expensive cursor tracking areas.
|
||
if (annotationImage.cursor) {
|
||
_wantsCursorRects = YES;
|
||
}
|
||
}
|
||
|
||
- (void)removeAnnotation:(id <MGLAnnotation>)annotation {
|
||
if (annotation) {
|
||
[self removeAnnotations:@[annotation]];
|
||
}
|
||
}
|
||
|
||
- (void)removeAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations {
|
||
if (!annotations) {
|
||
return;
|
||
}
|
||
|
||
[self willChangeValueForKey:@"annotations"];
|
||
|
||
for (id <MGLAnnotation> annotation in annotations) {
|
||
NSAssert([annotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation");
|
||
|
||
MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
|
||
NSAssert(annotationTag != MGLAnnotationTagNotFound, @"No ID for annotation %@", annotation);
|
||
|
||
if (annotationTag == _selectedAnnotationTag) {
|
||
[self deselectAnnotation:annotation];
|
||
}
|
||
if (annotationTag == _lastSelectedAnnotationTag) {
|
||
_lastSelectedAnnotationTag = MGLAnnotationTagNotFound;
|
||
}
|
||
|
||
_annotationContextsByAnnotationTag.erase(annotationTag);
|
||
_annotationTagsByAnnotation.erase(annotation);
|
||
|
||
if ([annotation isKindOfClass:[NSObject class]] &&
|
||
![annotation isKindOfClass:[MGLMultiPoint class]]) {
|
||
[(NSObject *)annotation removeObserver:self forKeyPath:@"coordinate" context:(void *)(NSUInteger)annotationTag];
|
||
} else if ([annotation isKindOfClass:[MGLMultiPoint 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 MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier {
|
||
return self.annotationImagesByIdentifier[identifier];
|
||
}
|
||
|
||
- (id <MGLAnnotation>)annotationAtPoint:(NSPoint)point {
|
||
return [self annotationWithTag:[self annotationTagAtPoint:point persistingResults:NO]];
|
||
}
|
||
|
||
/**
|
||
Returns the tag of the annotation at the given point in the view.
|
||
|
||
This is more involved than it sounds: if multiple point annotations overlap
|
||
near the point, this method cycles through them so that each of them is
|
||
accessible to the user at some point.
|
||
|
||
@param persist True to remember the cycleable set of annotations, so that a
|
||
different annotation is returned the next time this method is called
|
||
with the same point. Setting this parameter to false is useful for
|
||
asking “what if?”
|
||
*/
|
||
- (MGLAnnotationTag)annotationTagAtPoint:(NSPoint)point persistingResults:(BOOL)persist {
|
||
// Look for any annotation near the click. An annotation is “near” if the
|
||
// distance between its center and the click is less than the maximum height
|
||
// or width of an installed annotation image.
|
||
NSRect queryRect = NSInsetRect({ point, NSZeroSize },
|
||
-_unionedAnnotationImageSize.width / 2,
|
||
-_unionedAnnotationImageSize.height / 2);
|
||
queryRect = NSInsetRect(queryRect, -MGLAnnotationImagePaddingForHitTest,
|
||
-MGLAnnotationImagePaddingForHitTest);
|
||
std::vector<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect];
|
||
|
||
if (nearbyAnnotations.size()) {
|
||
// Assume that the user is fat-fingering an annotation.
|
||
NSRect hitRect = NSInsetRect({ point, NSZeroSize },
|
||
-MGLAnnotationImagePaddingForHitTest,
|
||
-MGLAnnotationImagePaddingForHitTest);
|
||
|
||
// Filter out any annotation whose image is unselectable or for which
|
||
// hit testing fails.
|
||
auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag annotationTag) {
|
||
NSAssert(_annotationContextsByAnnotationTag.count(annotationTag) != 0, @"Unknown annotation found nearby click");
|
||
id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
|
||
if (!annotation) {
|
||
return true;
|
||
}
|
||
|
||
MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
|
||
if (!annotationImage.selectable) {
|
||
return true;
|
||
}
|
||
|
||
// Filter out the annotation if the fattened finger didn’t land on a
|
||
// translucent or opaque pixel in the image.
|
||
NSRect annotationRect = [self frameOfImage:annotationImage.image
|
||
centeredAtCoordinate:annotation.coordinate];
|
||
return !!![annotationImage.image hitTestRect:hitRect withImageDestinationRect:annotationRect
|
||
context:nil hints:nil flipped:NO];
|
||
});
|
||
nearbyAnnotations.resize(std::distance(nearbyAnnotations.begin(), end));
|
||
}
|
||
|
||
MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound;
|
||
if (nearbyAnnotations.size()) {
|
||
// The annotation tags need to be stable in order to compare them with
|
||
// the remembered tags.
|
||
std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end());
|
||
|
||
if (nearbyAnnotations == _annotationsNearbyLastClick) {
|
||
// The first selection in the cycle should be the one nearest to the
|
||
// click.
|
||
CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self];
|
||
std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) {
|
||
CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate];
|
||
CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate];
|
||
CLLocationDegrees distanceA = hypot(coordinateA.latitude - currentCoordinate.latitude,
|
||
coordinateA.longitude - currentCoordinate.longitude);
|
||
CLLocationDegrees distanceB = hypot(coordinateB.latitude - currentCoordinate.latitude,
|
||
coordinateB.longitude - currentCoordinate.longitude);
|
||
return distanceA < distanceB;
|
||
});
|
||
|
||
// The last time we persisted a set of annotations, we had the same
|
||
// set of annotations as we do now. Cycle through them.
|
||
if (_lastSelectedAnnotationTag == MGLAnnotationTagNotFound
|
||
|| _lastSelectedAnnotationTag == _annotationsNearbyLastClick.back()) {
|
||
// Either no annotation is selected or the last annotation in
|
||
// the set was selected. Wrap around to the first annotation in
|
||
// the set.
|
||
hitAnnotationTag = _annotationsNearbyLastClick.front();
|
||
} else {
|
||
auto result = std::find(_annotationsNearbyLastClick.begin(),
|
||
_annotationsNearbyLastClick.end(),
|
||
_lastSelectedAnnotationTag);
|
||
if (result == _annotationsNearbyLastClick.end()) {
|
||
// An annotation from this set hasn’t been selected before.
|
||
// Select the first (nearest) one.
|
||
hitAnnotationTag = _annotationsNearbyLastClick.front();
|
||
} else {
|
||
// Step to the next annotation in the set.
|
||
auto distance = std::distance(_annotationsNearbyLastClick.begin(), result);
|
||
hitAnnotationTag = _annotationsNearbyLastClick[distance + 1];
|
||
}
|
||
}
|
||
} else {
|
||
// Remember the nearby annotations for the next time this method is
|
||
// called.
|
||
if (persist) {
|
||
_annotationsNearbyLastClick = nearbyAnnotations;
|
||
}
|
||
|
||
// Choose the first nearby annotation.
|
||
if (nearbyAnnotations.size()) {
|
||
hitAnnotationTag = nearbyAnnotations.front();
|
||
}
|
||
}
|
||
}
|
||
|
||
return hitAnnotationTag;
|
||
}
|
||
|
||
/// Returns the tags of the annotations coincident with the given rectangle.
|
||
- (std::vector<MGLAnnotationTag>)annotationTagsInRect:(NSRect)rect {
|
||
// Cocoa origin is at the lower-left corner.
|
||
return _mbglMap->queryPointAnnotations({
|
||
{ NSMinX(rect), NSHeight(self.bounds) - NSMaxY(rect) },
|
||
{ NSMaxX(rect), NSHeight(self.bounds) - NSMinY(rect) },
|
||
});
|
||
}
|
||
|
||
- (id <MGLAnnotation>)selectedAnnotation {
|
||
if (!_annotationContextsByAnnotationTag.count(_selectedAnnotationTag)) {
|
||
return nil;
|
||
}
|
||
MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(_selectedAnnotationTag);
|
||
return annotationContext.annotation;
|
||
}
|
||
|
||
- (void)setSelectedAnnotation:(id <MGLAnnotation>)annotation {
|
||
[self willChangeValueForKey:@"selectedAnnotations"];
|
||
_selectedAnnotationTag = [self annotationTagForAnnotation:annotation];
|
||
if (_selectedAnnotationTag != MGLAnnotationTagNotFound) {
|
||
_lastSelectedAnnotationTag = _selectedAnnotationTag;
|
||
}
|
||
[self didChangeValueForKey:@"selectedAnnotations"];
|
||
}
|
||
|
||
- (NS_ARRAY_OF(id <MGLAnnotation>) *)selectedAnnotations {
|
||
id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation;
|
||
return selectedAnnotation ? @[selectedAnnotation] : @[];
|
||
}
|
||
|
||
- (void)setSelectedAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)selectedAnnotations {
|
||
if (!selectedAnnotations.count) {
|
||
return;
|
||
}
|
||
|
||
id <MGLAnnotation> firstAnnotation = selectedAnnotations[0];
|
||
NSAssert([firstAnnotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation");
|
||
if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) {
|
||
return;
|
||
}
|
||
|
||
// Select the annotation if it’s visible.
|
||
if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) {
|
||
[self selectAnnotation:firstAnnotation];
|
||
}
|
||
}
|
||
|
||
- (void)selectAnnotation:(id <MGLAnnotation>)annotation
|
||
{
|
||
// Only point annotations can be selected.
|
||
if (!annotation || [annotation isKindOfClass:[MGLMultiPoint class]]) {
|
||
return;
|
||
}
|
||
|
||
id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation;
|
||
if (annotation == selectedAnnotation) {
|
||
return;
|
||
}
|
||
|
||
// Deselect the annotation before reselecting it.
|
||
[self deselectAnnotation:selectedAnnotation];
|
||
|
||
// Add the annotation to the map if it hasn’t been added yet.
|
||
MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
|
||
if (annotationTag == MGLAnnotationTagNotFound) {
|
||
[self addAnnotation:annotation];
|
||
}
|
||
|
||
// The annotation can’t be selected if no part of it is hittable.
|
||
NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag];
|
||
if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) {
|
||
return;
|
||
}
|
||
|
||
self.selectedAnnotation = annotation;
|
||
|
||
// For the callout to be shown, the annotation must have a title, its
|
||
// callout must not already be shown, and the annotation must be able to
|
||
// show a callout according to the delegate.
|
||
if ([annotation respondsToSelector:@selector(title)]
|
||
&& annotation.title
|
||
&& !self.calloutForSelectedAnnotation.shown
|
||
&& [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)]
|
||
&& [self.delegate mapView:self annotationCanShowCallout:annotation]) {
|
||
NSPopover *callout = [self calloutForAnnotation:annotation];
|
||
|
||
// Hang the callout off the right edge of the annotation image’s
|
||
// alignment rect, or off the left edge in a right-to-left UI.
|
||
callout.delegate = self;
|
||
self.calloutForSelectedAnnotation = callout;
|
||
NSRectEdge edge = (self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft
|
||
? NSMinXEdge
|
||
: NSMaxXEdge);
|
||
[callout showRelativeToRect:positioningRect ofView:self preferredEdge:edge];
|
||
}
|
||
}
|
||
|
||
- (void)showAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations animated:(BOOL)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:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated {
|
||
if ( ! annotations || ! annotations.count) return;
|
||
|
||
mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty();
|
||
|
||
for (id <MGLAnnotation> annotation in annotations)
|
||
{
|
||
if ([annotation conformsToProtocol:@protocol(MGLOverlay)])
|
||
{
|
||
bounds.extend(MGLLatLngBoundsFromCoordinateBounds(((id <MGLOverlay>)annotation).overlayBounds));
|
||
}
|
||
else
|
||
{
|
||
bounds.extend(MGLLatLngFromLocationCoordinate2D(annotation.coordinate));
|
||
}
|
||
}
|
||
|
||
[self setVisibleCoordinateBounds:MGLCoordinateBoundsFromLatLngBounds(bounds)
|
||
edgePadding:insets
|
||
animated:animated];
|
||
}
|
||
|
||
/// Returns a popover detailing the annotation.
|
||
- (NSPopover *)calloutForAnnotation:(id <MGLAnnotation>)annotation {
|
||
NSPopover *callout = [[NSPopover alloc] init];
|
||
callout.behavior = NSPopoverBehaviorTransient;
|
||
|
||
NSViewController *viewController;
|
||
if ([self.delegate respondsToSelector:@selector(mapView:calloutViewControllerForAnnotation:)]) {
|
||
NSViewController *viewControllerFromDelegate = [self.delegate mapView:self
|
||
calloutViewControllerForAnnotation:annotation];
|
||
if (viewControllerFromDelegate) {
|
||
viewController = viewControllerFromDelegate;
|
||
}
|
||
}
|
||
if (!viewController) {
|
||
viewController = self.calloutViewController;
|
||
}
|
||
NSAssert(viewController, @"Unable to load MGLAnnotationCallout view controller");
|
||
// The popover’s view controller can bind to KVO-compliant key paths of the
|
||
// annotation.
|
||
viewController.representedObject = annotation;
|
||
callout.contentViewController = viewController;
|
||
|
||
return callout;
|
||
}
|
||
|
||
- (NSViewController *)calloutViewController {
|
||
// Lazily load a default view controller.
|
||
if (!_calloutViewController) {
|
||
_calloutViewController = [[NSViewController alloc] initWithNibName:@"MGLAnnotationCallout"
|
||
bundle:[NSBundle mgl_frameworkBundle]];
|
||
}
|
||
return _calloutViewController;
|
||
}
|
||
|
||
/// Returns the rectangle that represents the annotation image of the annotation
|
||
/// with the given tag. This rectangle is fitted to the image’s alignment rect
|
||
/// and is appropriate for positioning a popover.
|
||
- (NSRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annotationTag {
|
||
id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
|
||
if (!annotation) {
|
||
return NSZeroRect;
|
||
}
|
||
NSImage *image = [self imageOfAnnotationWithTag:annotationTag].image;
|
||
if (!image) {
|
||
image = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName].image;
|
||
}
|
||
if (!image) {
|
||
return NSZeroRect;
|
||
}
|
||
|
||
NSRect positioningRect = [self frameOfImage:image centeredAtCoordinate:annotation.coordinate];
|
||
positioningRect = NSOffsetRect(image.alignmentRect, positioningRect.origin.x, positioningRect.origin.y);
|
||
return NSInsetRect(positioningRect, -MGLAnnotationImagePaddingForCallout,
|
||
-MGLAnnotationImagePaddingForCallout);
|
||
}
|
||
|
||
/// Returns the rectangle relative to the viewport that represents the given
|
||
/// image centered at the given coordinate.
|
||
- (NSRect)frameOfImage:(NSImage *)image centeredAtCoordinate:(CLLocationCoordinate2D)coordinate {
|
||
NSPoint calloutAnchorPoint = [self convertCoordinate:coordinate toPointToView:self];
|
||
return NSInsetRect({ calloutAnchorPoint, NSZeroSize }, -image.size.width / 2, -image.size.height / 2);
|
||
}
|
||
|
||
/// Returns the annotation image assigned to the annotation with the given tag.
|
||
- (MGLAnnotationImage *)imageOfAnnotationWithTag:(MGLAnnotationTag)annotationTag {
|
||
if (annotationTag == MGLAnnotationTagNotFound
|
||
|| _annotationContextsByAnnotationTag.count(annotationTag) == 0) {
|
||
return nil;
|
||
}
|
||
|
||
NSString *customSymbol = _annotationContextsByAnnotationTag.at(annotationTag).imageReuseIdentifier;
|
||
NSString *symbolName = customSymbol.length ? customSymbol : MGLDefaultStyleMarkerSymbolName;
|
||
|
||
return [self dequeueReusableAnnotationImageWithIdentifier:symbolName];
|
||
}
|
||
|
||
- (void)deselectAnnotation:(id <MGLAnnotation>)annotation {
|
||
if (!annotation || self.selectedAnnotation != annotation) {
|
||
return;
|
||
}
|
||
|
||
// Close the callout popover gracefully.
|
||
NSPopover *callout = self.calloutForSelectedAnnotation;
|
||
[callout performClose:self];
|
||
|
||
self.selectedAnnotation = nil;
|
||
}
|
||
|
||
/// Move the annotation callout to point to the selected annotation at its
|
||
/// current position.
|
||
- (void)updateAnnotationCallouts {
|
||
NSPopover *callout = self.calloutForSelectedAnnotation;
|
||
if (callout) {
|
||
callout.positioningRect = [self positioningRectForCalloutForAnnotationWithTag:_selectedAnnotationTag];
|
||
}
|
||
}
|
||
|
||
#pragma mark MGLMultiPointDelegate methods
|
||
|
||
- (double)alphaForShapeAnnotation:(MGLShape *)annotation {
|
||
if (_delegateHasAlphasForShapeAnnotations) {
|
||
return [self.delegate mapView:self alphaForShapeAnnotation:annotation];
|
||
}
|
||
return 1.0;
|
||
}
|
||
|
||
- (mbgl::Color)strokeColorForShapeAnnotation:(MGLShape *)annotation {
|
||
NSColor *color = (_delegateHasStrokeColorsForShapeAnnotations
|
||
? [self.delegate mapView:self strokeColorForShapeAnnotation:annotation]
|
||
: [NSColor selectedMenuItemColor]);
|
||
return color.mgl_color;
|
||
}
|
||
|
||
- (mbgl::Color)fillColorForPolygonAnnotation:(MGLPolygon *)annotation {
|
||
NSColor *color = (_delegateHasFillColorsForShapeAnnotations
|
||
? [self.delegate mapView:self fillColorForPolygonAnnotation:annotation]
|
||
: [NSColor selectedMenuItemColor]);
|
||
return color.mgl_color;
|
||
}
|
||
|
||
- (CGFloat)lineWidthForPolylineAnnotation:(MGLPolyline *)annotation {
|
||
if (_delegateHasLineWidthsForShapeAnnotations) {
|
||
return [self.delegate mapView:self lineWidthForPolylineAnnotation:(MGLPolyline *)annotation];
|
||
}
|
||
return 3.0;
|
||
}
|
||
|
||
#pragma mark MGLPopoverDelegate methods
|
||
|
||
- (void)popoverDidShow:(__unused NSNotification *)notification {
|
||
id <MGLAnnotation> annotation = self.selectedAnnotation;
|
||
if (annotation && [self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) {
|
||
[self.delegate mapView:self didSelectAnnotation:annotation];
|
||
}
|
||
}
|
||
|
||
- (void)popoverDidClose:(__unused NSNotification *)notification {
|
||
// Deselect the closed popover, in case the popover was closed due to user
|
||
// action.
|
||
id <MGLAnnotation> annotation = self.calloutForSelectedAnnotation.contentViewController.representedObject;
|
||
self.calloutForSelectedAnnotation = nil;
|
||
self.selectedAnnotation = nil;
|
||
|
||
if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)]) {
|
||
[self.delegate mapView:self didDeselectAnnotation:annotation];
|
||
}
|
||
}
|
||
|
||
#pragma mark Overlays
|
||
|
||
- (void)addOverlay:(id <MGLOverlay>)overlay {
|
||
[self addOverlays:@[overlay]];
|
||
}
|
||
|
||
- (void)addOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays
|
||
{
|
||
for (id <MGLOverlay> overlay in overlays) {
|
||
NSAssert([overlay conformsToProtocol:@protocol(MGLOverlay)], @"Overlay does not conform to MGLOverlay");
|
||
}
|
||
[self addAnnotations:overlays];
|
||
}
|
||
|
||
- (void)removeOverlay:(id <MGLOverlay>)overlay {
|
||
[self removeOverlays:@[overlay]];
|
||
}
|
||
|
||
- (void)removeOverlays:(NS_ARRAY_OF(id <MGLOverlay>) *)overlays {
|
||
for (id <MGLOverlay> overlay in overlays) {
|
||
NSAssert([overlay conformsToProtocol:@protocol(MGLOverlay)], @"Overlay does not conform to MGLOverlay");
|
||
}
|
||
[self removeAnnotations:overlays];
|
||
}
|
||
|
||
#pragma mark Tooltips and cursors
|
||
|
||
- (void)updateAnnotationTrackingAreas {
|
||
if (_wantsToolTipRects) {
|
||
[self removeAllToolTips];
|
||
std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:self.bounds];
|
||
for (MGLAnnotationTag annotationTag : annotationTags) {
|
||
MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
|
||
id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
|
||
if ([annotation 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 < MGLAnnotationTagNotFound, @"Invalid annotation tag in tooltip rect user data.");
|
||
MGLAnnotationTag annotationTag = (MGLAnnotationTag)MIN((NSUInteger)data, MGLAnnotationTagNotFound);
|
||
id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
|
||
return annotation.toolTip;
|
||
}
|
||
|
||
- (void)resetCursorRects {
|
||
// Drag to pan has a grabbing hand cursor.
|
||
if ([self isPanningWithGesture]) {
|
||
[self addCursorRect:self.bounds cursor:[NSCursor closedHandCursor]];
|
||
return;
|
||
}
|
||
|
||
// The rest of this method can be expensive, so bail if no annotations have
|
||
// ever had custom cursors.
|
||
if (!_wantsCursorRects) {
|
||
return;
|
||
}
|
||
|
||
std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:self.bounds];
|
||
for (MGLAnnotationTag annotationTag : annotationTags) {
|
||
id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag];
|
||
MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
|
||
if (annotationImage.cursor) {
|
||
// Add a cursor tracking area over the annotation image, respecting
|
||
// the image’s alignment rect.
|
||
NSImage *image = annotationImage.image;
|
||
NSRect annotationRect = [self frameOfImage:image
|
||
centeredAtCoordinate:annotation.coordinate];
|
||
annotationRect = NSOffsetRect(image.alignmentRect, annotationRect.origin.x, annotationRect.origin.y);
|
||
[self addCursorRect:annotationRect cursor:annotationImage.cursor];
|
||
}
|
||
}
|
||
}
|
||
|
||
#pragma mark Data
|
||
|
||
- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesAtPoint:(NSPoint)point {
|
||
return [self visibleFeaturesAtPoint:point inStyleLayersWithIdentifiers:nil];
|
||
}
|
||
|
||
- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesAtPoint:(NSPoint)point inStyleLayersWithIdentifiers:(NS_SET_OF(NSString *) *)styleLayerIdentifiers {
|
||
// Cocoa origin is at the lower-left corner.
|
||
mbgl::ScreenCoordinate screenCoordinate = { point.x, NSHeight(self.bounds) - point.y };
|
||
|
||
mbgl::optional<std::vector<std::string>> optionalLayerIDs;
|
||
if (styleLayerIdentifiers) {
|
||
__block std::vector<std::string> layerIDs;
|
||
layerIDs.reserve(styleLayerIdentifiers.count);
|
||
[styleLayerIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull identifier, BOOL * _Nonnull stop) {
|
||
layerIDs.push_back(identifier.UTF8String);
|
||
}];
|
||
optionalLayerIDs = layerIDs;
|
||
}
|
||
|
||
std::vector<mbgl::Feature> features = _mbglMap->queryRenderedFeatures(screenCoordinate, optionalLayerIDs);
|
||
return MGLFeaturesFromMBGLFeatures(features);
|
||
}
|
||
|
||
- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesInRect:(NSRect)rect {
|
||
return [self visibleFeaturesInRect:rect inStyleLayersWithIdentifiers:nil];
|
||
}
|
||
|
||
- (NS_ARRAY_OF(id <MGLFeature>) *)visibleFeaturesInRect:(NSRect)rect inStyleLayersWithIdentifiers:(NS_SET_OF(NSString *) *)styleLayerIdentifiers {
|
||
// Cocoa origin is at the lower-left corner.
|
||
mbgl::ScreenBox screenBox = {
|
||
{ NSMinX(rect), NSHeight(self.bounds) - NSMaxY(rect) },
|
||
{ NSMaxX(rect), NSHeight(self.bounds) - NSMinY(rect) },
|
||
};
|
||
|
||
mbgl::optional<std::vector<std::string>> optionalLayerIDs;
|
||
if (styleLayerIdentifiers) {
|
||
__block std::vector<std::string> layerIDs;
|
||
layerIDs.reserve(styleLayerIdentifiers.count);
|
||
[styleLayerIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull identifier, BOOL * _Nonnull stop) {
|
||
layerIDs.push_back(identifier.UTF8String);
|
||
}];
|
||
optionalLayerIDs = layerIDs;
|
||
}
|
||
|
||
std::vector<mbgl::Feature> features = _mbglMap->queryRenderedFeatures(screenBox, optionalLayerIDs);
|
||
return MGLFeaturesFromMBGLFeatures(features);
|
||
}
|
||
|
||
#pragma mark User interface validation
|
||
|
||
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
|
||
if (menuItem.action == @selector(giveFeedback:)) {
|
||
return YES;
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
#pragma mark Interface Builder methods
|
||
|
||
- (void)prepareForInterfaceBuilder {
|
||
[super prepareForInterfaceBuilder];
|
||
|
||
// Color the background a glorious Mapbox teal.
|
||
self.layer.borderColor = [NSColor colorWithRed:59/255.
|
||
green:178/255.
|
||
blue:208/255.
|
||
alpha:0.8].CGColor;
|
||
self.layer.borderWidth = 2;
|
||
self.layer.backgroundColor = [NSColor colorWithRed:59/255.
|
||
green:178/255.
|
||
blue:208/255.
|
||
alpha:0.6].CGColor;
|
||
|
||
// Place a playful marker right smack dab in the middle.
|
||
self.layer.contents = MGLDefaultMarkerImage();
|
||
self.layer.contentsGravity = kCAGravityCenter;
|
||
self.layer.contentsScale = [NSScreen mainScreen].backingScaleFactor;
|
||
}
|
||
|
||
#pragma mark Geometric methods
|
||
|
||
- (NSPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(nullable NSView *)view {
|
||
return [self convertLatLng:MGLLatLngFromLocationCoordinate2D(coordinate) toPointToView:view];
|
||
}
|
||
|
||
/// Converts a geographic coordinate to a point in the view’s coordinate system.
|
||
- (NSPoint)convertLatLng:(mbgl::LatLng)latLng toPointToView:(nullable NSView *)view {
|
||
mbgl::ScreenCoordinate pixel = _mbglMap->pixelForLatLng(latLng);
|
||
// Cocoa origin is at the lower-left corner.
|
||
pixel.y = NSHeight(self.bounds) - pixel.y;
|
||
return [self convertPoint:NSMakePoint(pixel.x, pixel.y) toView:view];
|
||
}
|
||
|
||
- (CLLocationCoordinate2D)convertPoint:(NSPoint)point toCoordinateFromView:(nullable NSView *)view {
|
||
return MGLLocationCoordinate2DFromLatLng([self convertPoint:point toLatLngFromView:view]);
|
||
}
|
||
|
||
/// Converts a point in the view’s coordinate system to a geographic coordinate.
|
||
- (mbgl::LatLng)convertPoint:(NSPoint)point toLatLngFromView:(nullable NSView *)view {
|
||
NSPoint convertedPoint = [self convertPoint:point fromView:view];
|
||
return _mbglMap->latLngForPixel({
|
||
convertedPoint.x,
|
||
// mbgl origin is at the top-left corner.
|
||
NSHeight(self.bounds) - convertedPoint.y,
|
||
}).wrapped();
|
||
}
|
||
|
||
- (NSRect)convertCoordinateBounds:(MGLCoordinateBounds)bounds toRectToView:(nullable NSView *)view {
|
||
return [self convertLatLngBounds:MGLLatLngBoundsFromCoordinateBounds(bounds) toRectToView:view];
|
||
}
|
||
|
||
/// Converts a geographic bounding box to a rectangle in the view’s coordinate
|
||
/// system.
|
||
- (NSRect)convertLatLngBounds:(mbgl::LatLngBounds)bounds toRectToView:(nullable NSView *)view {
|
||
NSRect rect = { [self convertLatLng:bounds.southwest() toPointToView:view], NSZeroSize };
|
||
rect = MGLExtendRect(rect, [self convertLatLng:bounds.northeast() toPointToView:view]);
|
||
return rect;
|
||
}
|
||
|
||
- (MGLCoordinateBounds)convertRect:(NSRect)rect toCoordinateBoundsFromView:(nullable NSView *)view {
|
||
return MGLCoordinateBoundsFromLatLngBounds([self convertRect:rect toLatLngBoundsFromView:view]);
|
||
}
|
||
|
||
/// Converts a rectangle in the given view’s coordinate system to a geographic
|
||
/// bounding box.
|
||
- (mbgl::LatLngBounds)convertRect:(NSRect)rect toLatLngBoundsFromView:(nullable NSView *)view {
|
||
mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty();
|
||
bounds.extend([self convertPoint:rect.origin toLatLngFromView:view]);
|
||
bounds.extend([self convertPoint:{ NSMaxX(rect), NSMinY(rect) } toLatLngFromView:view]);
|
||
bounds.extend([self convertPoint:{ NSMaxX(rect), NSMaxY(rect) } toLatLngFromView:view]);
|
||
bounds.extend([self convertPoint:{ NSMinX(rect), NSMaxY(rect) } toLatLngFromView:view]);
|
||
|
||
// The world is wrapping if a point just outside the bounds is also within
|
||
// the rect.
|
||
mbgl::LatLng outsideLatLng;
|
||
if (bounds.west() > -180) {
|
||
outsideLatLng = {
|
||
(bounds.south() + bounds.north()) / 2,
|
||
bounds.west() - 1,
|
||
};
|
||
} else if (bounds.northeast().longitude < 180) {
|
||
outsideLatLng = {
|
||
(bounds.south() + bounds.north()) / 2,
|
||
bounds.east() + 1,
|
||
};
|
||
}
|
||
|
||
// If the world is wrapping, extend the bounds to cover all longitudes.
|
||
if (NSPointInRect([self convertLatLng:outsideLatLng toPointToView:view], rect)) {
|
||
bounds.extend(mbgl::LatLng(bounds.south(), -180));
|
||
bounds.extend(mbgl::LatLng(bounds.south(), 180));
|
||
}
|
||
|
||
return bounds;
|
||
}
|
||
|
||
- (CLLocationDistance)metersPerPointAtLatitude:(CLLocationDegrees)latitude {
|
||
return _mbglMap->getMetersPerPixelAtLatitude(latitude, self.zoomLevel);
|
||
}
|
||
|
||
#pragma mark Debugging
|
||
|
||
- (MGLMapDebugMaskOptions)debugMask {
|
||
mbgl::MapDebugOptions options = _mbglMap->getDebug();
|
||
MGLMapDebugMaskOptions mask = 0;
|
||
if (options & mbgl::MapDebugOptions::TileBorders) {
|
||
mask |= MGLMapDebugTileBoundariesMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::ParseStatus) {
|
||
mask |= MGLMapDebugTileInfoMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::Timestamps) {
|
||
mask |= MGLMapDebugTimestampsMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::Collision) {
|
||
mask |= MGLMapDebugCollisionBoxesMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::Overdraw) {
|
||
mask |= MGLMapDebugOverdrawVisualizationMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::StencilClip) {
|
||
mask |= MGLMapDebugStencilBufferMask;
|
||
}
|
||
if (options & mbgl::MapDebugOptions::DepthBuffer) {
|
||
mask |= MGLMapDebugDepthBufferMask;
|
||
}
|
||
return mask;
|
||
}
|
||
|
||
- (void)setDebugMask:(MGLMapDebugMaskOptions)debugMask {
|
||
mbgl::MapDebugOptions options = mbgl::MapDebugOptions::NoDebug;
|
||
if (debugMask & MGLMapDebugTileBoundariesMask) {
|
||
options |= mbgl::MapDebugOptions::TileBorders;
|
||
}
|
||
if (debugMask & MGLMapDebugTileInfoMask) {
|
||
options |= mbgl::MapDebugOptions::ParseStatus;
|
||
}
|
||
if (debugMask & MGLMapDebugTimestampsMask) {
|
||
options |= mbgl::MapDebugOptions::Timestamps;
|
||
}
|
||
if (debugMask & MGLMapDebugCollisionBoxesMask) {
|
||
options |= mbgl::MapDebugOptions::Collision;
|
||
}
|
||
if (debugMask & MGLMapDebugOverdrawVisualizationMask) {
|
||
options |= mbgl::MapDebugOptions::Overdraw;
|
||
}
|
||
if (debugMask & MGLMapDebugStencilBufferMask) {
|
||
options |= mbgl::MapDebugOptions::StencilClip;
|
||
}
|
||
if (debugMask & MGLMapDebugDepthBufferMask) {
|
||
options |= mbgl::MapDebugOptions::DepthBuffer;
|
||
}
|
||
_mbglMap->setDebug(options);
|
||
}
|
||
|
||
/// Adapter responsible for bridging calls from mbgl to MGLMapView and Cocoa.
|
||
class MGLMapViewImpl : public mbgl::View, public mbgl::Backend {
|
||
public:
|
||
MGLMapViewImpl(MGLMapView *nativeView_)
|
||
: nativeView(nativeView_) {}
|
||
|
||
void notifyMapChange(mbgl::MapChange change) override {
|
||
[nativeView notifyMapChange:change];
|
||
}
|
||
|
||
void invalidate() override {
|
||
[nativeView setNeedsGLDisplay];
|
||
}
|
||
|
||
void activate() override {
|
||
if (activationCount++) {
|
||
return;
|
||
}
|
||
|
||
MGLOpenGLLayer *layer = (MGLOpenGLLayer *)nativeView.layer;
|
||
[layer.openGLContext makeCurrentContext];
|
||
}
|
||
|
||
void deactivate() override {
|
||
if (--activationCount) {
|
||
return;
|
||
}
|
||
|
||
[NSOpenGLContext clearCurrentContext];
|
||
}
|
||
|
||
mbgl::gl::value::Viewport::Type getViewport() const {
|
||
return { 0, 0, static_cast<uint16_t>(nativeView.bounds.size.width),
|
||
static_cast<uint16_t>(nativeView.bounds.size.height) };
|
||
}
|
||
|
||
void updateViewBinding() {
|
||
fbo = mbgl::gl::value::BindFramebuffer::Get();
|
||
getContext().bindFramebuffer.setCurrentValue(fbo);
|
||
getContext().viewport.setCurrentValue(getViewport());
|
||
}
|
||
|
||
void bind() override {
|
||
getContext().bindFramebuffer = fbo;
|
||
getContext().viewport = getViewport();
|
||
}
|
||
|
||
mbgl::PremultipliedImage readStillImage() {
|
||
NSRect bounds = [nativeView convertRectToBacking:nativeView.bounds];
|
||
const uint16_t width = bounds.size.width;
|
||
const uint16_t height = bounds.size.height;
|
||
mbgl::PremultipliedImage image{ width, height };
|
||
MBGL_CHECK_ERROR(
|
||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.data.get()));
|
||
|
||
const size_t stride = image.stride();
|
||
auto tmp = std::make_unique<uint8_t[]>(stride);
|
||
uint8_t *rgba = image.data.get();
|
||
for (int i = 0, j = height - 1; i < j; i++, j--) {
|
||
std::memcpy(tmp.get(), rgba + i * stride, stride);
|
||
std::memcpy(rgba + i * stride, rgba + j * stride, stride);
|
||
std::memcpy(rgba + j * stride, tmp.get(), stride);
|
||
}
|
||
|
||
return image;
|
||
}
|
||
|
||
private:
|
||
/// Cocoa map view that this adapter bridges to.
|
||
__weak MGLMapView *nativeView = nullptr;
|
||
|
||
/// The current framebuffer of the NSOpenGLLayer we are painting to.
|
||
GLint fbo = 0;
|
||
|
||
/// The reference counted count of activation calls
|
||
NSUInteger activationCount = 0;
|
||
};
|
||
|
||
@end
|