diff options
Diffstat (limited to 'platform/macos/src/MGLMapView.mm')
-rw-r--r-- | platform/macos/src/MGLMapView.mm | 190 |
1 files changed, 133 insertions, 57 deletions
diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm index 9ec9012198..9cab9a76da 100644 --- a/platform/macos/src/MGLMapView.mm +++ b/platform/macos/src/MGLMapView.mm @@ -97,6 +97,9 @@ const CGFloat MGLAnnotationImagePaddingForHitTest = 4; /// Distance from the callout’s anchor point to the annotation it points to. const CGFloat MGLAnnotationImagePaddingForCallout = 4; +/// Padding to edge of view that an offscreen annotation must have when being brought onscreen (by being selected) +const NSEdgeInsets MGLMapViewOffscreenAnnotationPadding = NSEdgeInsetsMake(-30.0f, -30.0f, -30.0f, -30.0f); + /// Unique identifier representing a single annotation in mbgl. typedef uint32_t MGLAnnotationTag; @@ -2099,44 +2102,41 @@ public: 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()); - + // The first selection in the cycle should be the one nearest to the + // tap. Also the annotation tags need to be stable in order to compare them with + // the remembered tags _annotationsNearbyLastClick. + CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self]; + std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, + coordinateA.longitude - currentCoordinate.longitude); + CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, + coordinateB.longitude - currentCoordinate.longitude); + return deltaA < deltaB; + }); + if (nearbyAnnotations == _annotationsNearbyLastClick) { - // The 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()) { + || _lastSelectedAnnotationTag == nearbyAnnotations.back()) { // Either no annotation is selected or the last annotation in // the set was selected. Wrap around to the first annotation in // the set. - hitAnnotationTag = _annotationsNearbyLastClick.front(); + hitAnnotationTag = nearbyAnnotations.front(); } else { - auto result = std::find(_annotationsNearbyLastClick.begin(), - _annotationsNearbyLastClick.end(), + auto result = std::find(nearbyAnnotations.begin(), + nearbyAnnotations.end(), _lastSelectedAnnotationTag); - if (result == _annotationsNearbyLastClick.end()) { + if (result == nearbyAnnotations.end()) { // An annotation from this set hasn’t been selected before. // Select the first (nearest) one. - hitAnnotationTag = _annotationsNearbyLastClick.front(); + hitAnnotationTag = nearbyAnnotations.front(); } else { // Step to the next annotation in the set. - auto distance = std::distance(_annotationsNearbyLastClick.begin(), result); - hitAnnotationTag = _annotationsNearbyLastClick[distance + 1]; + auto distance = std::distance(nearbyAnnotations.begin(), result); + hitAnnotationTag = nearbyAnnotations[distance + 1]; } } } else { @@ -2208,10 +2208,12 @@ public: return; } - // Select the annotation if it’s visible. - if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) { - [self selectAnnotation:firstAnnotation]; - } + [self selectAnnotation:firstAnnotation]; +} + +- (BOOL)isBringingAnnotationOnscreenSupportedForAnnotation:(id<MGLAnnotation>)annotation animated:(BOOL)animated { + // Consider delegating + return animated && [annotation isKindOfClass:[MGLPointAnnotation class]]; } - (void)selectAnnotation:(id <MGLAnnotation>)annotation @@ -2221,6 +2223,11 @@ public: - (void)selectAnnotation:(id <MGLAnnotation>)annotation atPoint:(NSPoint)gesturePoint { + [self selectAnnotation:annotation atPoint:gesturePoint moveOnscreen:YES animateSelection:YES]; +} + +- (void)selectAnnotation:(id <MGLAnnotation>)annotation atPoint:(NSPoint)gesturePoint moveOnscreen:(BOOL)moveOnscreen animateSelection:(BOOL)animateSelection +{ id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation; if (annotation == selectedAnnotation) { return; @@ -2235,9 +2242,14 @@ public: [self addAnnotation:annotation]; } + if (moveOnscreen) { + moveOnscreen = [self isBringingAnnotationOnscreenSupportedForAnnotation:annotation animated:animateSelection]; + } + // The annotation's anchor will bounce to the current click. NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; - if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) { + + if (!moveOnscreen && NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) { positioningRect = CGRectMake(gesturePoint.x, gesturePoint.y, positioningRect.size.width, positioningRect.size.height); } @@ -2257,11 +2269,65 @@ public: // alignment rect, or off the left edge in a right-to-left UI. callout.delegate = self; self.calloutForSelectedAnnotation = callout; + NSRectEdge edge = (self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft ? NSMinXEdge : NSMaxXEdge); + + // The following will do nothing if the positioning rect is not on-screen. See + // `-[MGLMapView updateAnnotationCallouts]` for presenting the callout when the selected + // annotation comes back on-screen. [callout showRelativeToRect:positioningRect ofView:self preferredEdge:edge]; } + + if (moveOnscreen) + { + moveOnscreen = NO; + + NSRect (^edgeInsetsInsetRect)(NSRect, NSEdgeInsets) = ^(NSRect rect, NSEdgeInsets insets) { + return NSMakeRect(rect.origin.x + insets.left, + rect.origin.y + insets.top, + rect.size.width - insets.left - insets.right, + rect.size.height - insets.top - insets.bottom); + }; + + // Add padding around the positioning rect (in essence an inset from the edge of the viewport + NSRect expandedPositioningRect = edgeInsetsInsetRect(positioningRect, MGLMapViewOffscreenAnnotationPadding); + + // Used for callout positioning, and moving offscreen annotations onscreen. + CGRect constrainedRect = edgeInsetsInsetRect(self.bounds, self.contentInsets); + CGRect bounds = constrainedRect; + + // Any one of these cases should trigger a move onscreen + if (CGRectGetMinX(positioningRect) < CGRectGetMinX(bounds)) + { + constrainedRect.origin.x = expandedPositioningRect.origin.x; + moveOnscreen = YES; + } + else if (CGRectGetMaxX(positioningRect) > CGRectGetMaxX(bounds)) + { + constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width; + moveOnscreen = YES; + } + + if (CGRectGetMinY(positioningRect) < CGRectGetMinY(bounds)) + { + constrainedRect.origin.y = expandedPositioningRect.origin.y; + moveOnscreen = YES; + } + else if (CGRectGetMaxY(positioningRect) > CGRectGetMaxY(bounds)) + { + constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height; + moveOnscreen = YES; + } + + if (moveOnscreen) + { + CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect)); + CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self]; + [self setCenterCoordinate:centerCoord animated:animateSelection]; + } + } } - (void)showAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations animated:(BOOL)animated { @@ -2396,7 +2462,25 @@ public: - (void)updateAnnotationCallouts { NSPopover *callout = self.calloutForSelectedAnnotation; if (callout) { - callout.positioningRect = [self positioningRectForCalloutForAnnotationWithTag:_selectedAnnotationTag]; + NSRect rect = [self positioningRectForCalloutForAnnotationWithTag:_selectedAnnotationTag]; + + if (!NSIsEmptyRect(NSIntersectionRect(rect, self.bounds))) { + + // It's possible that the current callout hasn't been presented (since the original + // positioningRect was offscreen). We can check that the callout has a valid window + // This results in the callout being presented just as the annotation comes on screen + // which matches MapKit, but (currently) not iOS. + if (!callout.contentViewController.view.window) { + NSRectEdge edge = (self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft + ? NSMinXEdge + : NSMaxXEdge); + // Re-present the callout + [callout showRelativeToRect:rect ofView:self preferredEdge:edge]; + } + else { + callout.positioningRect = rect; + } + } } } @@ -2717,32 +2801,24 @@ public: /// 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)); - } + auto bounds = mbgl::LatLngBounds::empty(); + auto bottomLeft = [self convertPoint:{ NSMinX(rect), NSMinY(rect) } toLatLngFromView:view]; + auto bottomRight = [self convertPoint:{ NSMaxX(rect), NSMinY(rect) } toLatLngFromView:view]; + auto topRight = [self convertPoint:{ NSMaxX(rect), NSMaxY(rect) } toLatLngFromView:view]; + auto topLeft = [self convertPoint:{ NSMinX(rect), NSMaxY(rect) } toLatLngFromView:view]; + + // If the bounds straddles the antimeridian, unwrap it so that one side + // extends beyond ±180° longitude. + auto center = [self convertPoint:{ NSMidX(rect), NSMidY(rect) } toLatLngFromView:view]; + bottomLeft.unwrapForShortestPath(center); + bottomRight.unwrapForShortestPath(center); + topRight.unwrapForShortestPath(center); + topLeft.unwrapForShortestPath(center); + + bounds.extend(bottomLeft); + bounds.extend(bottomRight); + bounds.extend(topRight); + bounds.extend(topLeft); return bounds; } |