From 1598330a9384de7c89c8e3cac55a8333e6b97c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Wed, 2 Dec 2015 09:56:23 -0800 Subject: [osx] Added inline comments to MGLMapView --- platform/osx/src/MGLMapView.mm | 257 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 238 insertions(+), 19 deletions(-) (limited to 'platform/osx/src/MGLMapView.mm') diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index a1cfe3c33d..ede38dd150 100644 --- a/platform/osx/src/MGLMapView.mm +++ b/platform/osx/src/MGLMapView.mm @@ -39,29 +39,53 @@ 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; -const CLLocationDegrees MGLKeyRotationIncrement = 25; -static NSString * const MGLVendorDirectoryName = @"com.mapbox.MapboxGL"; +/// Degrees that a single press of the rotation keyboard shortcut rotates the map by. +const CLLocationDegrees MGLKeyRotationIncrement = 25; +/// 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[] = { { @"Mapbox", @"https://www.mapbox.com/about/maps/" }, { @"OpenStreetMap", @"http://www.openstreetmap.org/about/" }, }; +/// Unique identifier representing a single annotation in mbgl. typedef uint32_t MGLAnnotationID; + +/// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationNotFound = UINT32_MAX }; + +/// Mapping from an annotation’s unique identifier to metadata about that +/// annotation, including the annotation itself. typedef std::map MGLAnnotationContextMap; /// Returns an NSImage for the default marker image. @@ -71,8 +95,10 @@ NSImage *MGLDefaultMarkerImage() { return [[NSImage alloc] initWithContentsOfFile:path]; } -std::chrono::steady_clock::duration MGLDurationInSeconds(float duration) { - return std::chrono::duration_cast(std::chrono::duration(duration)); +/// Converts from a duration in seconds to a duration object usable in mbgl. +std::chrono::steady_clock::duration MGLDurationInSeconds(NSTimeInterval duration) { + return std::chrono::duration_cast( + std::chrono::duration(duration)); } /// Converts the given color into an mbgl::Color in calibrated RGB space. @@ -85,9 +111,12 @@ mbgl::Color MGLColorObjectFromNSColor(NSColor *color) { return {{ (float)r, (float)g, (float)b, (float)a }}; } +/// Lightweight container for metadata about an annotation, including the annotation itself. class MGLAnnotationContext { public: id annotation; + /// mbgl-given identifier for the annotation image used by this annotation. + /// Based on the annotation image’s reusable identifier. NSString *symbolIdentifier; }; @@ -98,7 +127,9 @@ public: @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; @@ -106,6 +137,7 @@ public: @end @implementation MGLMapView { + /// Cross-platform map view controller. mbgl::Map *_mbglMap; MGLMapViewImpl *_mbglView; std::shared_ptr _mbglFileCache; @@ -121,15 +153,25 @@ public: MGLAnnotationContextMap _annotationContextsByAnnotationID; MGLAnnotationID _selectedAnnotationID; MGLAnnotationID _lastSelectedAnnotationID; + /// Size of the rectangle formed by unioning the maximum slop area around every annotation image. NSSize _unionedAnnotationImageSize; std::vector _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; @@ -173,6 +215,7 @@ public: - (void)commonInit { _isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent; + // Set up cross-platform controllers and resources. _mbglView = new MGLMapViewImpl(self, [NSScreen mainScreen].backingScaleFactor); // Place the cache in a location that can be shared among all the @@ -182,7 +225,8 @@ public: appropriateForURL:nil create:YES error:nil]; - cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:MGLVendorDirectoryName]; + cacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent: + [[NSBundle mgl_frameworkBundle] bundleIdentifier]]; [[NSFileManager defaultManager] createDirectoryAtURL:cacheDirectoryURL withIntermediateDirectories:YES attributes:nil @@ -194,6 +238,8 @@ public: _mbglMap = new mbgl::Map(*_mbglView, *_mbglFileSource, mbgl::MapMode::Continuous); + // 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]; // Observe for changes to the global access token (and find out the current one). @@ -210,32 +256,36 @@ public: }; [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]; _annotationContextsByAnnotationID = {}; _selectedAnnotationID = MGLAnnotationNotFound; _lastSelectedAnnotationID = MGLAnnotationNotFound; _annotationsNearbyLastClick = {}; + // Jump to Null Island initially. mbgl::CameraOptions options; options.center = mbgl::LatLng(0, 0); options.zoom = _mbglMap->getMinZoom(); _mbglMap->jumpTo(options); } +/// Adds zoom controls to the lower-right corner. - (void)installZoomControls { _zoomControls = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect]; _zoomControls.wantsLayer = YES; - _zoomControls.layer.opacity = 0.9; + _zoomControls.layer.opacity = MGLOrnamentOpacity; [(NSSegmentedCell *)_zoomControls.cell setTrackingMode:NSSegmentSwitchTrackingMomentary]; _zoomControls.continuous = YES; _zoomControls.segmentCount = 2; - [_zoomControls setLabel:@"−" forSegment:0]; + [_zoomControls setLabel:@"−" forSegment:0]; // U+2212 MINUS SIGN [(NSSegmentedCell *)_zoomControls.cell setTag:0 forSegment:0]; [(NSSegmentedCell *)_zoomControls.cell setToolTip:@"Zoom Out" forSegment:0]; [_zoomControls setLabel:@"+" forSegment:1]; @@ -249,10 +299,11 @@ public: [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 = 0.9; + _compass.layer.opacity = MGLOrnamentOpacity; _compass.cell = [[MGLCompassCell alloc] init]; _compass.continuous = YES; _compass.target = self; @@ -262,11 +313,13 @@ public: [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; @@ -274,22 +327,36 @@ public: [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 { self.acceptsTouchEvents = YES; _scrollEnabled = YES; @@ -321,18 +388,24 @@ public: [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 @@ -355,6 +428,7 @@ public: - (void)dealloc { [[MGLAccountManager sharedManager] removeObserver:self forKeyPath:@"accessToken"]; + // Close any annotation callout immediately. [self.calloutForSelectedAnnotation close]; self.calloutForSelectedAnnotation = nil; @@ -377,7 +451,7 @@ public: if ([keyPath isEqualToString:@"accessToken"] && object == [MGLAccountManager sharedManager]) { NSString *accessToken = change[NSKeyValueChangeNewKey]; if (![accessToken isKindOfClass:[NSNull class]]) { - _mbglFileSource->setAccessToken((std::string)[accessToken UTF8String]); + _mbglFileSource->setAccessToken((std::string)accessToken.UTF8String); } } } @@ -385,6 +459,8 @@ public: - (void)setDelegate:(id)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:)]; @@ -403,7 +479,10 @@ public: return; } + // Default to Streets. if (!styleURL) { + // An access token is required to load any default style, including + // Streets. if (![MGLAccountManager accessToken]) { return; } @@ -411,11 +490,12 @@ public: } if (![styleURL scheme]) { - // Assume a relative path into the application’s resource folder. - styleURL = [NSURL URLWithString:[@"asset://" stringByAppendingString:[styleURL absoluteString]]]; + // Assume a relative path into the application’s resource folder, + // defined in mbgl::platform::assetRoot(). + styleURL = [NSURL URLWithString:[@"asset://" stringByAppendingString:styleURL.absoluteString]]; } - _mbglMap->setStyleURL([[styleURL absoluteString] UTF8String]); + _mbglMap->setStyleURL(styleURL.absoluteString.UTF8String); } - (IBAction)reloadStyle:(__unused id)sender { @@ -446,6 +526,8 @@ public: } - (BOOL)wantsBestResolutionOpenGLSurface { + // Use an OpenGL layer, except when drawing the designable, which is just + // ordinary Cocoa. return !_isTargetingInterfaceBuilder; } @@ -457,6 +539,7 @@ public: } - (void)updateConstraints { + // Place the zoom controls at the lower-right corner of the view. [self addConstraint: [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom @@ -474,6 +557,8 @@ public: 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 @@ -491,6 +576,8 @@ public: 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 @@ -508,6 +595,8 @@ public: 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 @@ -580,6 +669,8 @@ public: } case mbgl::MapChangeRegionIsChanging: { + // Update a minimum of UI that needs to stay attached to the map + // while animating. [self updateCompass]; [self updateAnnotationCallouts]; @@ -591,6 +682,11 @@ public: 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]; @@ -708,12 +804,15 @@ public: 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: @@ -761,7 +860,7 @@ public: - (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(NSEdgeInsets)insets animated:(BOOL)animated { _mbglMap->cancelTransitions(); - mbgl::EdgeInsets mbglInsets = {insets.top, insets.left, insets.bottom, insets.right}; + mbgl::EdgeInsets mbglInsets = MGLEdgeInsetsFromNSEdgeInsets(insets); mbgl::CameraOptions options = _mbglMap->cameraForLatLngBounds(MGLLatLngBoundsFromCoordinateBounds(bounds), mbglInsets); if (animated) { options.duration = MGLDurationInSeconds(MGLAnimationDuration); @@ -780,6 +879,8 @@ public: 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]; @@ -787,6 +888,7 @@ public: NSEventModifierFlags flags = [NSApp currentEvent].modifierFlags; if (flags & NSShiftKeyMask) { + // Shift-drag to zoom. if (!self.zoomEnabled) { return; } @@ -808,6 +910,7 @@ public: CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, startPoint); } } else if (flags & NSAlternateKeyMask) { + // Option-drag to rotate and/or tilt. _mbglMap->cancelTransitions(); if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { @@ -830,6 +933,7 @@ public: _mbglMap->setGestureInProgress(false); } } else if (self.scrollEnabled) { + // Otherwise, drag to pan. _mbglMap->cancelTransitions(); if (gestureRecognizer.state == NSGestureRecognizerStateBegan) { @@ -847,6 +951,7 @@ public: } } +/// Pinch to zoom. - (void)handleMagnificationGesture:(NSMagnificationGestureRecognizer *)gestureRecognizer { if (!self.zoomEnabled) { return; @@ -873,6 +978,7 @@ public: } } +/// Click or tap to select an annotation. - (void)handleClickGesture:(NSClickGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.state != NSGestureRecognizerStateEnded) { return; @@ -891,6 +997,7 @@ public: } } +/// Tap with two fingers (“right-click”) to zoom out. - (void)handleSecondaryClickGesture:(NSClickGestureRecognizer *)gestureRecognizer { if (!self.zoomEnabled) { return; @@ -902,6 +1009,7 @@ public: [self scaleBy:0.5 atPoint:NSMakePoint(gesturePoint.x, self.bounds.size.height - gesturePoint.y) animated:YES]; } +/// Double-click or double-tap to zoom in. - (void)handleDoubleClickGesture:(NSClickGestureRecognizer *)gestureRecognizer { if (!self.zoomEnabled) { return; @@ -913,6 +1021,7 @@ public: [self scaleBy:2 atPoint:NSMakePoint(gesturePoint.x, self.bounds.size.height - gesturePoint.y) animated:YES]; } +/// Rotate fingers to rotate. - (void)handleRotationGesture:(NSRotationGestureRecognizer *)gestureRecognizer { if (!self.rotateEnabled) { return; @@ -934,6 +1043,7 @@ public: } - (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(__unused NSEventGestureAxis)axis { + // Track both horizontal and vertical swipes in -scrollWheel:. return YES; } @@ -955,6 +1065,7 @@ public: } else if (self.scrollEnabled && _magnificationGestureRecognizer.state == NSGestureRecognizerStatePossible && _rotationGestureRecognizer.state == NSGestureRecognizerStatePossible) { + // Scroll to pan. _mbglMap->cancelTransitions(); CGFloat x = event.scrollingDeltaX; @@ -963,6 +1074,7 @@ public: [self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO]; } + // Drift pan. if (event.momentumPhase != NSEventPhaseNone) { [self offsetCenterCoordinateBy:NSMakePoint(x, y) animated:NO]; } @@ -973,6 +1085,9 @@ public: - (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]; @@ -1033,12 +1148,16 @@ public: #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; } @@ -1053,7 +1172,8 @@ public: return nil; } - std::vector > annotations; + // Map all the annotation IDs to the annotations themselves. + std::vector> annotations; std::transform(_annotationContextsByAnnotationID.begin(), _annotationContextsByAnnotationID.end(), std::back_inserter(annotations), @@ -1063,6 +1183,7 @@ public: return [NSArray arrayWithObjects:&annotations[0] count:annotations.size()]; } +/// Returns the annotation assigned the given ID. Cheap. - (id )annotationWithID:(MGLAnnotationID)annotationID { if (!_annotationContextsByAnnotationID.count(annotationID)) { return nil; @@ -1072,6 +1193,7 @@ public: return annotationContext.annotation; } +/// Returns the annotation ID assigned to the given annotation. Relatively expensive. - (MGLAnnotationID)annotationIDForAnnotation:(id )annotation { if (!annotation) { return MGLAnnotationNotFound; @@ -1105,6 +1227,7 @@ public: NSAssert([annotation conformsToProtocol:@protocol(MGLAnnotation)], @"Annotation does not conform to MGLAnnotation"); if ([annotation isKindOfClass:[MGLMultiPoint class]]) { + // The multipoint knows how to style itself (with the map view’s help). [(MGLMultiPoint *)annotation addShapeAnnotationObjectToCollection:shapes withDelegate:self]; } else { MGLAnnotationImage *annotationImage = nil; @@ -1115,6 +1238,9 @@ public: annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]; } if (!annotationImage) { + // Create 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. NSImage *image = MGLDefaultMarkerImage(); NSRect alignmentRect = image.alignmentRect; alignmentRect.origin.y = NSMidY(alignmentRect); @@ -1132,12 +1258,14 @@ public: NSString *symbolName = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier]; points.emplace_back(MGLLatLngFromLocationCoordinate2D(annotation.coordinate), symbolName ? [symbolName UTF8String] : ""); + // Opt into potentially expensive tooltip tracking areas. if (annotation.toolTip.length) { _wantsToolTipRects = YES; } } } + // Add any point annotations to mbgl and our own index. if (points.size()) { std::vector pointAnnotationIDs = _mbglMap->addPointAnnotations(points); @@ -1149,6 +1277,7 @@ public: } } + // Add any shape annotations to mbgl and our own index. if (shapes.size()) { std::vector shapeAnnotationIDs = _mbglMap->addShapeAnnotations(shapes); @@ -1162,18 +1291,24 @@ public: [self updateAnnotationTrackingAreas]; } +/// 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 { NSImage *image = annotationImage.image; NSSize size = image.size; - if (size.width < 1 || size.height < 1 || !image.valid) { + 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 */); auto cSpriteImage = std::make_shared((uint16_t)rep.size.width, (uint16_t)rep.size.height, @@ -1188,6 +1323,7 @@ public: _unionedAnnotationImageSize = NSMakeSize(MAX(_unionedAnnotationImageSize.width, size.width), MAX(_unionedAnnotationImageSize.height, size.height)); + // Opt into potentially expensive cursor tracking areas. if (annotationImage.cursor) { _wantsCursorRects = YES; } @@ -1239,6 +1375,8 @@ public: } - (nullable MGLAnnotationImage *)dequeueReusableAnnotationImageWithIdentifier:(NSString *)identifier { + // This prefix is used to avoid collisions with style-defined sprites in + // mbgl, but reusable identifiers are never prefixed. if ([identifier hasPrefix:MGLAnnotationSpritePrefix]) { identifier = [identifier substringFromIndex:MGLAnnotationSpritePrefix.length]; } @@ -1249,6 +1387,18 @@ public: return [self annotationWithID:[self annotationIDAtPoint:point persistingResults:NO]]; } +/** + Returns the ID 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?” + */ - (MGLAnnotationID)annotationIDAtPoint:(NSPoint)point persistingResults:(BOOL)persist { // Look for any annotation near the click. An annotation is “near” if the // distance between its center and the click is less than the maximum height @@ -1261,9 +1411,13 @@ public: std::vector nearbyAnnotations = [self annotationIDsInRect: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. mbgl::util::erase_if(nearbyAnnotations, [&](const MGLAnnotationID annotationID) { NSAssert(_annotationContextsByAnnotationID.count(annotationID) != 0, @"Unknown annotation found nearby click"); id annotation = [self annotationWithID:annotationID]; @@ -1276,6 +1430,8 @@ public: 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 @@ -1285,13 +1441,21 @@ public: MGLAnnotationID hitAnnotationID = MGLAnnotationNotFound; if (nearbyAnnotations.size()) { + // The annotation IDs need to be stable in order to compare them with + // the remembered IDs. std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end()); if (nearbyAnnotations == _annotationsNearbyLastClick) { - if (_lastSelectedAnnotationID == _annotationsNearbyLastClick.back() - || _lastSelectedAnnotationID == MGLAnnotationNotFound) { + // The last time we persisted a set of annotations, we had the same + // set of annotations as we do now. Cycle through them. + if (_lastSelectedAnnotationID == MGLAnnotationNotFound + || _lastSelectedAnnotationID == _annotationsNearbyLastClick.back()) { + // Either an annotation from this set hasn’t been selected + // before or the last annotation in the set was selected. Wrap + // around to the first annotation in the set. hitAnnotationID = _annotationsNearbyLastClick.front(); } else { + // Step to the next annotation in the set. auto result = std::find(_annotationsNearbyLastClick.begin(), _annotationsNearbyLastClick.end(), _lastSelectedAnnotationID); @@ -1299,9 +1463,13 @@ public: hitAnnotationID = _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 (_annotationsNearbyLastClick.size()) { hitAnnotationID = _annotationsNearbyLastClick.front(); } @@ -1311,6 +1479,7 @@ public: return hitAnnotationID; } +/// Returns the IDs of the annotations coincident with the given rectangle. - (std::vector)annotationIDsInRect:(NSRect)rect { mbgl::LatLngBounds queryBounds = [self convertRectToLatLngBounds:rect]; return _mbglMap->getPointAnnotationsInBounds(queryBounds); @@ -1336,6 +1505,7 @@ public: return; } + // Select the annotation if it’s visible. if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) { [self selectAnnotation:firstAnnotation animated:NO]; } @@ -1343,6 +1513,7 @@ public: - (void)selectAnnotation:(id )annotation animated:(BOOL)animated { + // Only point annotations can be selected. if (!annotation || [annotation isKindOfClass:[MGLMultiPoint class]]) { return; } @@ -1352,13 +1523,16 @@ public: return; } + // Deselect the annotation before reselecting it. [self deselectAnnotation:selectedAnnotation animated:NO]; + // Add the annotation to the map if it hasn’t been added yet. MGLAnnotationID annotationID = [self annotationIDForAnnotation:annotation]; if (annotationID == MGLAnnotationNotFound) { [self addAnnotation:annotation]; } + // The annotation can’t be selected if no part of it is hittable. NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithID:annotationID]; if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) { return; @@ -1369,6 +1543,9 @@ public: _lastSelectedAnnotationID = _selectedAnnotationID; [self didChangeValueForKey:@"selectedAnnotation"]; + // 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 @@ -1377,6 +1554,8 @@ public: NSPopover *callout = [self calloutForAnnotation:annotation]; callout.animates = animated; + // 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 @@ -1386,6 +1565,7 @@ public: } } +/// Returns a popover detailing the annotation. - (NSPopover *)calloutForAnnotation:(id )annotation { NSPopover *callout = [[NSPopover alloc] init]; callout.behavior = NSPopoverBehaviorTransient; @@ -1399,12 +1579,17 @@ public: bundle:[NSBundle mgl_frameworkBundle]]; } 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; } +/// Returns the rectangle that represents the annotation image of the annotation +/// with the given ID. This rectangle is fitted to the image’s alignment rect +/// and is appropriate for positioning a popover. - (NSRect)positioningRectForCalloutForAnnotationWithID:(MGLAnnotationID)annotationID { id annotation = [self annotationWithID:annotationID]; if (!annotation) { @@ -1421,11 +1606,14 @@ public: -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 ID. - (MGLAnnotationImage *)imageOfAnnotationWithID:(MGLAnnotationID)annotationID { if (annotationID == MGLAnnotationNotFound || _annotationContextsByAnnotationID.count(annotationID) == 0) { @@ -1443,6 +1631,7 @@ public: return; } + // Close the callout popover gracefully. NSPopover *callout = self.calloutForSelectedAnnotation; callout.animates = animated; [callout performClose:self]; @@ -1450,6 +1639,8 @@ public: 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) { @@ -1497,6 +1688,8 @@ public: } - (void)popoverDidClose:(__unused NSNotification *)notification { + // Deselect the closed popover, in case the popover was closed due to user + // action. id annotation = self.calloutForSelectedAnnotation.contentViewController.representedObject; self.calloutForSelectedAnnotation = nil; self.selectedAnnotation = nil; @@ -1541,6 +1734,8 @@ public: MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithID:annotationID]; id annotation = [self annotationWithID:annotationID]; 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]; @@ -1549,12 +1744,15 @@ public: [self addToolTipRect:annotationRect owner:self userData:(void *)(NSUInteger)annotationID]; } } + // 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]; } @@ -1570,11 +1768,15 @@ public: } - (void)resetCursorRects { + // Drag to pan has a grabbing hand cursor. if (_panGestureRecognizer.state == NSGestureRecognizerStateBegan || _panGestureRecognizer.state == NSGestureRecognizerStateChanged) { [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; } @@ -1584,6 +1786,8 @@ public: id annotation = [self annotationWithID:annotationID]; MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithID:annotationID]; 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]; @@ -1621,6 +1825,7 @@ public: return MGLLocationCoordinate2DFromLatLng([self convertPoint:point toLatLngFromView:view]); } +/// Converts a point in the view’s coordinate system to a coordinate pair. - (mbgl::LatLng)convertPoint:(NSPoint)point toLatLngFromView:(nullable NSView *)view { NSPoint convertedPoint = [self convertPoint:point fromView:view]; return _mbglMap->latLngForPixel(mbgl::PrecisionPoint(convertedPoint.x, convertedPoint.y)); @@ -1630,15 +1835,20 @@ public: return [self convertLatLng:MGLLatLngFromLocationCoordinate2D(coordinate) toPointToView:view]; } +/// Converts a coordinate pair to a point in the view’s coordinate system. - (NSPoint)convertLatLng:(mbgl::LatLng)latLng toPointToView:(nullable NSView *)view { mbgl::vec2 pixel = _mbglMap->pixelForLatLng(latLng); return [self convertPoint:NSMakePoint(pixel.x, pixel.y) toView:view]; } +/// Converts a rectangle in the view’s coordinate system to a coordinate +/// bounding box. - (MGLCoordinateBounds)convertRectToCoordinateBounds:(NSRect)rect { return MGLCoordinateBoundsFromLatLngBounds([self convertRectToLatLngBounds:rect]); } +/// Converts a rectangle in the view’s coordinate system to a coordinate +/// bounding box. - (mbgl::LatLngBounds)convertRectToLatLngBounds:(NSRect)rect { mbgl::LatLngBounds bounds = mbgl::LatLngBounds::getExtendable(); bounds.extend([self convertPoint:rect.origin toLatLngFromView:self]); @@ -1670,7 +1880,7 @@ public: return bounds; } -- (CLLocationDistance)metersPerPixelAtLatitude:(CLLocationDegrees)latitude { +- (CLLocationDistance)metersPerPointAtLatitude:(CLLocationDegrees)latitude { return _mbglMap->getMetersPerPixelAtLatitude(latitude, self.zoomLevel); } @@ -1711,6 +1921,7 @@ public: _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_) @@ -1743,8 +1954,10 @@ public: void activate() override { MGLOpenGLLayer *layer = (MGLOpenGLLayer *)nativeView.layer; if ([NSOpenGLContext currentContext] != layer.openGLContext) { + // Enable our OpenGL context on the Map thread. [layer.openGLContext makeCurrentContext]; + // Enable vertex buffer objects. mbgl::gl::InitializeExtensions([](const char *name) { static CFBundleRef framework = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.opengl")); if (!framework) { @@ -1771,13 +1984,19 @@ public: } void beforeRender() override { + // This normally gets called right away by mbgl::Map, but only on the + // main thread. OpenGL contexts and extensions are thread-local, so this + // has to happen on the Map thread too. activate(); } void afterRender() override {} private: + /// Cocoa map view that this adapter bridges to. __weak MGLMapView *nativeView = nullptr; + + /// Backing scale factor of the view. const float scaleFactor; }; -- cgit v1.2.1