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