summaryrefslogtreecommitdiff
path: root/platform/osx/src/MGLMapView.mm
diff options
context:
space:
mode:
authorMinh Nguyễn <mxn@1ec5.org>2015-12-02 09:56:23 -0800
committerMinh Nguyễn <mxn@1ec5.org>2015-12-13 17:26:53 -0800
commit1598330a9384de7c89c8e3cac55a8333e6b97c75 (patch)
treecf88d1eff2b5b050cb0d9008df2a4294df48f749 /platform/osx/src/MGLMapView.mm
parentdf0511af185c334c7480f06bbc21b974a2a320fb (diff)
downloadqtlocation-mapboxgl-1598330a9384de7c89c8e3cac55a8333e6b97c75.tar.gz
[osx] Added inline comments to MGLMapView
Diffstat (limited to 'platform/osx/src/MGLMapView.mm')
-rw-r--r--platform/osx/src/MGLMapView.mm257
1 files changed, 238 insertions, 19 deletions
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<MGLAnnotationID, MGLAnnotationContext> 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::steady_clock::duration>(std::chrono::duration<float, std::chrono::seconds::period>(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::steady_clock::duration>(
+ std::chrono::duration<NSTimeInterval, std::chrono::seconds::period>(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 <MGLAnnotation> 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<mbgl::SQLiteCache> _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<MGLAnnotationID> _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<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:)];
@@ -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<id <MGLAnnotation> > annotations;
+ // Map all the annotation IDs to the annotations themselves.
+ std::vector<id <MGLAnnotation>> 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 <MGLAnnotation>)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 <MGLAnnotation>)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<MGLAnnotationID> pointAnnotationIDs = _mbglMap->addPointAnnotations(points);
@@ -1149,6 +1277,7 @@ public:
}
}
+ // Add any shape annotations to mbgl and our own index.
if (shapes.size()) {
std::vector<MGLAnnotationID> 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<mbgl::SpriteImage>((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<MGLAnnotationID> 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 <MGLAnnotation> 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<MGLAnnotationID>)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 <MGLAnnotation>)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 <MGLAnnotation>)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 <MGLAnnotation> 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 <MGLAnnotation> annotation = self.calloutForSelectedAnnotation.contentViewController.representedObject;
self.calloutForSelectedAnnotation = nil;
self.selectedAnnotation = nil;
@@ -1541,6 +1734,8 @@ public:
MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithID:annotationID];
id <MGLAnnotation> 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 <MGLAnnotation> 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<double> 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;
};