diff options
author | Julian Rex <julian.rex@mapbox.com> | 2018-03-13 00:23:13 -0400 |
---|---|---|
committer | Julian Rex <julian.rex@mapbox.com> | 2018-03-20 15:19:20 -0400 |
commit | 0a2dbc2e56ac4d33e15d1c23f194a98736243da6 (patch) | |
tree | 14b335082947f0a8a6ef902092d36f8b457668d6 | |
parent | a4e149431a218dc24b30541c940c6c1c0441a903 (diff) | |
download | qtlocation-mapboxgl-0a2dbc2e56ac4d33e15d1c23f194a98736243da6.tar.gz |
[ios,macos] Selecting an offscreen (point) annotation now moves that annotation just on screen.
-rw-r--r-- | platform/ios/CHANGELOG.md | 2 | ||||
-rw-r--r-- | platform/ios/app/MBXCustomCalloutView.m | 7 | ||||
-rw-r--r-- | platform/ios/app/MBXViewController.m | 43 | ||||
-rw-r--r-- | platform/ios/src/MGLCalloutView.h | 20 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 92 | ||||
-rwxr-xr-x | platform/ios/vendor/SMCalloutView/SMCalloutView.h | 12 | ||||
-rwxr-xr-x | platform/ios/vendor/SMCalloutView/SMCalloutView.m | 43 | ||||
-rw-r--r-- | platform/macos/CHANGELOG.md | 2 | ||||
-rw-r--r-- | platform/macos/app/Base.lproj/MainMenu.xib | 4 | ||||
-rw-r--r-- | platform/macos/app/MapDocument.m | 37 | ||||
-rw-r--r-- | platform/macos/src/MGLMapView.mm | 92 |
11 files changed, 295 insertions, 59 deletions
diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index fa5f1a7fab..bd09d0bf53 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -37,6 +37,8 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * Improved performance of `MGLAnnotationView`-backed annotations that have `scalesWithViewingDistance` enabled. ([#10951](https://github.com/mapbox/mapbox-gl-native/pull/10951)) * Fix an issue where a wrong annotation may selected if annotations were set close together. ([#11438](https://github.com/mapbox/mapbox-gl-native/pull/11438)) * The `MGLMapView.selectedAnnotations` property (backed by `-[MGLMapView setSelectedAnnotations:]`) now selects annotations that are off-screen. ([#9790](https://github.com/mapbox/mapbox-gl-native/issues/9790)) +* The `animated` parameter to `-[MGLMapView selectAnnotation:animated:]` now controls whether the annotation and its callout are brought on-screen. If `animated` is `NO` then the annotation is selected if offscreen, but the map is not panned. Currently only point annotations are supported.([#3249](https://github.com/mapbox/mapbox-gl-native/issues/3249)) +* Setting the `MGLMapView.selectedAnnotations` property, now animates to match macOS. ### Map snapshots diff --git a/platform/ios/app/MBXCustomCalloutView.m b/platform/ios/app/MBXCustomCalloutView.m index 13564c5cbf..0626b0997a 100644 --- a/platform/ios/app/MBXCustomCalloutView.m +++ b/platform/ios/app/MBXCustomCalloutView.m @@ -37,11 +37,15 @@ static CGFloat const tipWidth = 10.0; return self; } - #pragma mark - API - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated { + [self presentCalloutFromRect:rect inView:view constrainedToRect:CGRectNull animated:animated]; +} + +- (void)presentCalloutFromRect:(CGRect)rect inView:(nonnull UIView *)view constrainedToRect:(__unused CGRect)constrainedRect animated:(BOOL)animated +{ if ([self.delegate respondsToSelector:@selector(calloutViewWillAppear:)]) { [self.delegate performSelector:@selector(calloutViewWillAppear:) withObject:self]; @@ -108,5 +112,4 @@ static CGFloat const tipWidth = 10.0; CGPathRelease(tipPath); } - @end diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index fcdc9af2b3..020cebddf0 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -55,7 +55,8 @@ typedef NS_ENUM(NSInteger, MBXSettingsAnnotationsRows) { MBXSettingsAnnotationsCustomUserDot, MBXSettingsAnnotationsRemoveAnnotations, MBXSettingsAnnotationSelectRandomOffscreenAnnotation, - MBXSettingsAnnotationCenterSelectedAnnotation + MBXSettingsAnnotationCenterSelectedAnnotation, + MBXSettingsAnnotationAddVisibleAreaPolyline }; typedef NS_ENUM(NSInteger, MBXSettingsRuntimeStylingRows) { @@ -343,7 +344,8 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { [NSString stringWithFormat:@"%@ Custom User Dot", (_customUserLocationAnnnotationEnabled ? @"Disable" : @"Enable")], @"Remove Annotations", @"Select an offscreen annotation", - @"Center selected annotation" + @"Center selected annotation", + @"Add visible area polyline" ]]; break; case MBXSettingsRuntimeStyling: @@ -480,6 +482,10 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { [self centerSelectedAnnotation]; break; + case MBXSettingsAnnotationAddVisibleAreaPolyline: + [self addVisibleAreaPolyline]; + break; + default: NSAssert(NO, @"All annotations setting rows should be implemented"); break; @@ -1587,12 +1593,11 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { - (void)selectAnOffscreenAnnotation { id<MGLAnnotation> annotation = [self randomOffscreenAnnotation]; - [self.mapView selectAnnotation:annotation animated:NO]; + if (annotation) { + [self.mapView selectAnnotation:annotation animated:YES]; - // Alternative method to select the annotation (NOT ANIMATED). These two should do the same thing. - // self.mapView.selectedAnnotations = @[annotation]; - - NSAssert(self.mapView.selectedAnnotations.firstObject, @"The annotation was not selected"); + NSAssert(self.mapView.selectedAnnotations.firstObject, @"The annotation was not selected"); + } } - (void)centerSelectedAnnotation { @@ -1608,6 +1613,22 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { [self.mapView setCenterCoordinate:center animated:YES]; } +- (void)addVisibleAreaPolyline { + CGRect constrainedRect = UIEdgeInsetsInsetRect(self.mapView.bounds, self.mapView.contentInset); + + CLLocationCoordinate2D lineCoords[5]; + + lineCoords[0] = [self.mapView convertPoint: CGPointMake(CGRectGetMinX(constrainedRect), CGRectGetMinY(constrainedRect)) toCoordinateFromView:self.mapView]; + lineCoords[1] = [self.mapView convertPoint: CGPointMake(CGRectGetMaxX(constrainedRect), CGRectGetMinY(constrainedRect)) toCoordinateFromView:self.mapView]; + lineCoords[2] = [self.mapView convertPoint: CGPointMake(CGRectGetMaxX(constrainedRect), CGRectGetMaxY(constrainedRect)) toCoordinateFromView:self.mapView]; + lineCoords[3] = [self.mapView convertPoint: CGPointMake(CGRectGetMinX(constrainedRect), CGRectGetMaxY(constrainedRect)) toCoordinateFromView:self.mapView]; + lineCoords[4] = lineCoords[0]; + + MGLPolyline *line = [MGLPolyline polylineWithCoordinates:lineCoords + count:sizeof(lineCoords)/sizeof(lineCoords[0])]; + [self.mapView addAnnotation:line]; +} + - (void)printTelemetryLogFile { NSString *fileContents = [NSString stringWithContentsOfFile:[self telemetryDebugLogFilePath] encoding:NSUTF8StringEncoding error:nil]; @@ -1659,7 +1680,13 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { toCoordinateFromView:self.mapView]; pin.title = title ?: @"Dropped Pin"; pin.subtitle = [[[MGLCoordinateFormatter alloc] init] stringFromCoordinate:pin.coordinate]; - // Calling `addAnnotation:` on mapView is not required since `selectAnnotation:animated` has the side effect of adding the annotation if required + + + // Calling `addAnnotation:` on mapView is required here (since `selectAnnotation:animated` has + // the side effect of adding the annotation if required, but returning an incorrect callout + // positioning rect) + + [self.mapView addAnnotation:pin]; [self.mapView selectAnnotation:pin animated:YES]; } } diff --git a/platform/ios/src/MGLCalloutView.h b/platform/ios/src/MGLCalloutView.h index 0481a39680..057d5f2bcc 100644 --- a/platform/ios/src/MGLCalloutView.h +++ b/platform/ios/src/MGLCalloutView.h @@ -41,6 +41,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated; + +/** + Presents a callout view by adding it to `view` and pointing at the given rect + of `view`’s bounds. Constrains the callout to the rect in the space of `view`. + */ +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated; + /** Dismisses the callout view. */ @@ -49,6 +56,19 @@ NS_ASSUME_NONNULL_BEGIN @optional /** + If implemented, should provide margins to expand the rect the callout is presented from. + + These are used to determine positioning. Currently only the top and bottom properties of the return + value are used. For example, `{ .top = -50.0, .left = 0.0, .bottom = 0.0, .right = 0.0 }` indicates + a 50 point margin above the presentation origin rect, in which the callout is assumed to be displayed. + + @param rect rect that the callout is presented from. This should be the same as the one passed in + `-presentCalloutFromRect:inView:constrainedToRect:animated:` + @return margins. Since this is a UIEdgeInsets, values should be negative. + */ +- (UIEdgeInsets)marginInsetsHintForPresentationFromRect:(CGRect)rect; + +/** A Boolean value indicating whether the callout view should be anchored to the corresponding annotation. You can adjust the callout view’s precise location by overriding -[UIView setCenter:]. The callout view will not be anchored to the diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index a639588caa..82f78ba169 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -144,6 +144,9 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1; const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10); +/// Padding to edge of view that an offscreen annotation must have when being brought onscreen (by being selected) +const UIEdgeInsets MGLMapViewOffscreenAnnotationPadding = UIEdgeInsetsMake(-20.0f, -20.0f, -20.0f, -20.0f); + /// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationTagNotFound = UINT32_MAX }; @@ -1625,7 +1628,7 @@ public: { CGPoint calloutPoint = [singleTap locationInView:self]; CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint]; - [self selectAnnotation:annotation animated:YES calloutPositioningRect:positionRect]; + [self selectAnnotation:annotation animated:YES animateSelection:YES calloutPositioningRect:positionRect]; } else if (self.selectedAnnotation) { @@ -4234,6 +4237,12 @@ public: }); } + +- (BOOL)isBringingAnnotationOnscreenSupportedForAnnotation:(id<MGLAnnotation>)annotation animated:(BOOL)animated { + // Consider delegating + return animated && [annotation isKindOfClass:[MGLPointAnnotation class]]; +} + - (id <MGLAnnotation>)selectedAnnotation { if (_userLocationAnnotationIsSelected) @@ -4274,16 +4283,16 @@ public: if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) return; - [self selectAnnotation:firstAnnotation animated:NO]; + [self selectAnnotation:firstAnnotation animated:YES]; } - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero]; - [self selectAnnotation:annotation animated:animated calloutPositioningRect:positioningRect]; + [self selectAnnotation:annotation animated:animated animateSelection:YES calloutPositioningRect:positioningRect]; } -- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect +- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated animateSelection:(BOOL)animateSelection calloutPositioningRect:(CGRect)calloutPositioningRect { if ( ! annotation) return; @@ -4307,22 +4316,31 @@ public: MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); annotationView = annotationContext.annotationView; if (annotationView && annotationView.enabled) { - // Annotations represented by views use the view frame as the positioning rect. - calloutPositioningRect = annotationView.frame; - [annotationView.superview bringSubviewToFront:annotationView]; - [annotationView setSelected:YES animated:animated]; + // Annotations represented by views use the view frame as the positioning rect. + calloutPositioningRect = annotationView.frame; + [annotationView.superview bringSubviewToFront:annotationView]; + + [annotationView setSelected:YES animated:animateSelection]; } } self.selectedAnnotation = annotation; + // Determine if we need to move offscreen annotations on screen + BOOL moveOffscreenAnnotation = [self isBringingAnnotationOnscreenSupportedForAnnotation:annotation animated:animated]; + CGRect expandedPositioningRect = UIEdgeInsetsInsetRect(calloutPositioningRect, MGLMapViewOffscreenAnnotationPadding); + + // Used for callout positioning, and moving offscreen annotations onscreen. + CGRect constrainedRect = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + UIView <MGLCalloutView> *calloutView = nil; + if ([annotation respondsToSelector:@selector(title)] && annotation.title && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] && [self.delegate mapView:self annotationCanShowCallout:annotation]) { // build the callout - UIView <MGLCalloutView> *calloutView; if ([self.delegate respondsToSelector:@selector(mapView:calloutViewForAnnotation:)]) { id providedCalloutView = [self.delegate mapView:self calloutViewForAnnotation:annotation]; @@ -4380,13 +4398,51 @@ public: // set annotation delegate to handle taps on the callout view calloutView.delegate = self; - // present popup - [calloutView presentCalloutFromRect:calloutPositioningRect - inView:self.glView - constrainedToView:self.glView - animated:animated]; + // If the callout view provides inset (outset) information, we can use it to expand our positioning + // rect, which we then use to help move the annotation on-screen if want need to. + if (moveOffscreenAnnotation && [calloutView respondsToSelector:@selector(marginInsetsHintForPresentationFromRect:)]) { + UIEdgeInsets margins = [calloutView marginInsetsHintForPresentationFromRect:calloutPositioningRect]; + expandedPositioningRect = UIEdgeInsetsInsetRect(expandedPositioningRect, margins); + } + } + + if (moveOffscreenAnnotation) + { + moveOffscreenAnnotation = NO; + + // Need to consider the content insets. + CGRect bounds = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + // Any one of these cases should trigger a move onscreen + if (CGRectGetMinX(calloutPositioningRect) < CGRectGetMinX(bounds)) + { + constrainedRect.origin.x = expandedPositioningRect.origin.x; + moveOffscreenAnnotation = YES; + } + else if (CGRectGetMaxX(calloutPositioningRect) > CGRectGetMaxX(bounds)) + { + constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width; + moveOffscreenAnnotation = YES; + } + + if (CGRectGetMinY(calloutPositioningRect) < CGRectGetMinY(bounds)) + { + constrainedRect.origin.y = expandedPositioningRect.origin.y; + moveOffscreenAnnotation = YES; + } + else if (CGRectGetMaxY(calloutPositioningRect) > CGRectGetMaxY(bounds)) + { + constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height; + moveOffscreenAnnotation = YES; + } } + // Remember, calloutView can be nil here. + [calloutView presentCalloutFromRect:calloutPositioningRect + inView:self.glView + constrainedToRect:constrainedRect + animated:animated]; + // notify delegate if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) { @@ -4397,6 +4453,13 @@ public: { [self.delegate mapView:self didSelectAnnotationView:annotationView]; } + + if (moveOffscreenAnnotation) + { + CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect)); + CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self]; + [self setCenterCoordinate:centerCoord animated:animated]; + } } - (MGLCompactCalloutView *)calloutViewForAnnotation:(id <MGLAnnotation>)annotation @@ -4587,6 +4650,7 @@ public: animated:animated]; } + #pragma mark Annotation Image Delegate - (void)annotationImageNeedsRedisplay:(MGLAnnotationImage *)annotationImage diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.h b/platform/ios/vendor/SMCalloutView/SMCalloutView.h index 0b14913626..5bb73d4c84 100755 --- a/platform/ios/vendor/SMCalloutView/SMCalloutView.h +++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.h @@ -114,6 +114,18 @@ extern NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView; - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated; /** + @brief Presents a callout view by adding it to "inView" and pointing at the given rect of inView's bounds. + + @discussion Constrains the callout to the rect (in the space of the given view). + + @param rect @c CGRect to present the view from + @param view view to 'constrain' the @c constrainedView to + @param constrainedRect Rect to constrain the callout to + @param animated @c BOOL if presentation should be animated + */ +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated; + +/** @brief Present a callout layer in the `layer` and pointing at the given rect of the `layer` bounds @discussion Same as the view-based presentation, but inserts the callout into a CALayer hierarchy instead. diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.m b/platform/ios/vendor/SMCalloutView/SMCalloutView.m index 9631ca0367..a0049a3e2d 100755 --- a/platform/ios/vendor/SMCalloutView/SMCalloutView.m +++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.m @@ -247,6 +247,35 @@ NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0; return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom); } +- (UIEdgeInsets)marginInsetsHintForPresentationFromRect:(CGRect)rect { + + // form our subviews based on our content set so far + [self rebuildSubviews]; + + // size the callout to fit the width constraint as best as possible + CGFloat height = self.calloutHeight; + CGSize size = [self sizeThatFits:CGSizeMake(0.0f, height)]; + + // Without re-jigging presentCalloutFromRect, let's just make a best-guess with what we have + // right now. + CGFloat horizontalMargin = fmaxf(0, ceilf((CALLOUT_MIN_WIDTH-rect.size.width)/2)); + + UIEdgeInsets insets = { + .top = 0.0f, + .right = -horizontalMargin, + .bottom = 0.0f, + .left = -horizontalMargin + }; + + if (self.permittedArrowDirection == MGLSMCalloutArrowDirectionUp) + insets.bottom -= size.height; + else + insets.top -= size.height; + + return insets; +} + + - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated { [self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated]; } @@ -255,8 +284,18 @@ NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0; [self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated]; } -// this private method handles both CALayer and UIView parents depending on what's passed. - (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated { + // figure out the constrained view's rect in our popup view's coordinate system + CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer]; + [self presentCalloutFromRect:rect inLayer:layer ofView:view constrainedToRect:constrainedRect animated:animated]; +} + +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated { + [self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToRect:constrainedRect animated:animated]; +} + +// this private method handles both CALayer and UIView parents depending on what's passed. +- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated { // Sanity check: dismiss this callout immediately if it's displayed somewhere if (self.layer.superlayer) [self dismissCalloutAnimated:NO]; @@ -265,8 +304,6 @@ NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0; [self.layer removeAnimationForKey:@"present"]; [self.layer removeAnimationForKey:@"dismiss"]; - // figure out the constrained view's rect in our popup view's coordinate system - CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer]; // apply our edge constraints constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets); diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md index 4bbb2dfb63..ea5b7d9578 100644 --- a/platform/macos/CHANGELOG.md +++ b/platform/macos/CHANGELOG.md @@ -38,6 +38,8 @@ * Feature querying results now account for the `MGLSymbolStyleLayer.circleStrokeWidth` property. ([#10897](https://github.com/mapbox/mapbox-gl-native/pull/10897)) * Removed methods, properties, and constants that had been deprecated as of v0.6.1. ([#11205](https://github.com/mapbox/mapbox-gl-native/pull/11205)) * The `MGLMapView.selectedAnnotations` property (backed by `-[MGLMapView setSelectedAnnotations:]`) now selects annotations that are off-screen. ([#9790](https://github.com/mapbox/mapbox-gl-native/issues/9790)) +* The `animated` parameter to `-[MGLMapView selectAnnotation:animated:]` now controls whether the annotation and its callout are brought on-screen. If `animated` is `NO` then the annotation is selected if offscreen, but the map is not panned. Currently only point annotations are supported.([#3249](https://github.com/mapbox/mapbox-gl-native/issues/3249)) + ## v0.6.1 - January 16, 2018 diff --git a/platform/macos/app/Base.lproj/MainMenu.xib b/platform/macos/app/Base.lproj/MainMenu.xib index 64d776832f..c90a4a6016 100644 --- a/platform/macos/app/Base.lproj/MainMenu.xib +++ b/platform/macos/app/Base.lproj/MainMenu.xib @@ -545,10 +545,10 @@ <action selector="drawAnimatedAnnotation:" target="-1" id="CYM-WB-s97"/> </connections> </menuItem> - <menuItem title="Select and Center Offscreen Annotation" id="Xy2-Cc-RUB"> + <menuItem title="Select an Offscreen Annotation" id="Xy2-Cc-RUB"> <modifierMask key="keyEquivalentModifierMask"/> <connections> - <action selector="selectAndCenterOffscreenAnnotation:" target="-1" id="W0A-AH-NqK"/> + <action selector="selectOffscreenAnnotation:" target="-1" id="AHm-rf-mG4"/> </connections> </menuItem> <menuItem title="Show All Annotations" keyEquivalent="A" id="yMj-uM-8SN"> diff --git a/platform/macos/app/MapDocument.m b/platform/macos/app/MapDocument.m index 9127df9346..14d765d5c9 100644 --- a/platform/macos/app/MapDocument.m +++ b/platform/macos/app/MapDocument.m @@ -652,7 +652,7 @@ NS_ARRAY_OF(id <MGLAnnotation>) *MBXFlattenedShapes(NS_ARRAY_OF(id <MGLAnnotatio // https://github.com/mapbox/mapbox-gl-native/issues/11296 NSArray *visibleAnnotations = self.mapView.visibleAnnotations; - NSLog(@"Number of visible annotations = %d", visibleAnnotations.count); + NSLog(@"Number of visible annotations = %ld", visibleAnnotations.count); if (visibleAnnotations.count == annotations.count) return nil; @@ -668,36 +668,17 @@ NS_ARRAY_OF(id <MGLAnnotation>) *MBXFlattenedShapes(NS_ARRAY_OF(id <MGLAnnotatio return invisibleAnnotations[index]; } -- (void)selectAnOffscreenAnnotation { +- (IBAction)selectOffscreenAnnotation:(id)sender { id<MGLAnnotation> annotation = [self randomOffscreenAnnotation]; - [self.mapView selectAnnotation:annotation]; - - // Alternative method to select the annotation. These two should do the same thing. -// self.mapView.selectedAnnotations = @[annotation]; - - NSAssert(self.mapView.selectedAnnotations.firstObject, @"The annotation was not selected"); -} - -- (void)centerSelectedAnnotation { - id<MGLAnnotation> annotation = self.mapView.selectedAnnotations.firstObject; - - if (!annotation) - return; + if (annotation) { + [self.mapView selectAnnotation:annotation]; - CGPoint point = [self.mapView convertCoordinate:annotation.coordinate toPointToView:self.mapView]; - - // Animate, so that point becomes the the center - CLLocationCoordinate2D center = [self.mapView convertPoint:point toCoordinateFromView:self.mapView]; - [self.mapView setCenterCoordinate:center animated:YES]; -} - -- (IBAction)selectAndCenterOffscreenAnnotation:(id)sender { - [self selectAnOffscreenAnnotation]; - [self centerSelectedAnnotation]; + // Alternative method to select the annotation. These two should do the same thing. + // self.mapView.selectedAnnotations = @[annotation]; + NSAssert(self.mapView.selectedAnnotations.firstObject, @"The annotation was not selected"); + } } - - - (void)updateAnimatedAnnotation:(NSTimer *)timer { DroppedPinAnnotation *annotation = timer.userInfo; double angle = timer.fireDate.timeIntervalSinceReferenceDate; @@ -1168,7 +1149,7 @@ NS_ARRAY_OF(id <MGLAnnotation>) *MBXFlattenedShapes(NS_ARRAY_OF(id <MGLAnnotatio if (menuItem.action == @selector(insertGraticuleLayer:)) { return ![self.mapView.style sourceWithIdentifier:@"graticule"]; } - if (menuItem.action == @selector(selectAndCenterOffscreenAnnotation:)) { + if (menuItem.action == @selector(selectOffscreenAnnotation:)) { return YES; } if (menuItem.action == @selector(showAllAnnotations:) || menuItem.action == @selector(removeAllAnnotations:)) { diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm index ffca1d30ad..d4ddf69a5e 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; @@ -2208,6 +2211,11 @@ public: [self selectAnnotation:firstAnnotation]; } +- (BOOL)isBringingAnnotationOnscreenSupportedForAnnotation:(id<MGLAnnotation>)annotation animated:(BOOL)animated { + // Consider delegating + return animated && [annotation isKindOfClass:[MGLPointAnnotation class]]; +} + - (void)selectAnnotation:(id <MGLAnnotation>)annotation { [self selectAnnotation:annotation atPoint:NSZeroPoint]; @@ -2215,6 +2223,11 @@ public: - (void)selectAnnotation:(id <MGLAnnotation>)annotation atPoint:(NSPoint)gesturePoint { + [self selectAnnotation:annotation atPoint:gesturePoint animated:YES]; +} + +- (void)selectAnnotation:(id <MGLAnnotation>)annotation atPoint:(NSPoint)gesturePoint animated:(BOOL)animated +{ id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation; if (annotation == selectedAnnotation) { return; @@ -2229,9 +2242,12 @@ public: [self addAnnotation:annotation]; } + BOOL checkOffscreenAnnotation = [self isBringingAnnotationOnscreenSupportedForAnnotation:annotation animated:animated]; + // The annotation's anchor will bounce to the current click. NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; - if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) { + + if (!checkOffscreenAnnotation && NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) { positioningRect = CGRectMake(gesturePoint.x, gesturePoint.y, positioningRect.size.width, positioningRect.size.height); } @@ -2251,11 +2267,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 (checkOffscreenAnnotation) + { + 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; + + BOOL moveOffscreenAnnotation = NO; + + // Any one of these cases should trigger a move onscreen + if (CGRectGetMinX(positioningRect) < CGRectGetMinX(bounds)) + { + constrainedRect.origin.x = expandedPositioningRect.origin.x; + moveOffscreenAnnotation = YES; + } + else if (CGRectGetMaxX(positioningRect) > CGRectGetMaxX(bounds)) + { + constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width; + moveOffscreenAnnotation = YES; + } + + if (CGRectGetMinY(positioningRect) < CGRectGetMinY(bounds)) + { + constrainedRect.origin.y = expandedPositioningRect.origin.y; + moveOffscreenAnnotation = YES; + } + else if (CGRectGetMaxY(positioningRect) > CGRectGetMaxY(bounds)) + { + constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height; + moveOffscreenAnnotation = YES; + } + + if (moveOffscreenAnnotation) + { + CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect)); + CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self]; + [self setCenterCoordinate:centerCoord animated:animated]; + } + } } - (void)showAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations animated:(BOOL)animated { @@ -2390,7 +2460,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; + } + } } } |