summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Rex <julian.rex@mapbox.com>2018-03-13 00:23:13 -0400
committerJulian Rex <julian.rex@mapbox.com>2018-03-20 15:19:20 -0400
commit0a2dbc2e56ac4d33e15d1c23f194a98736243da6 (patch)
tree14b335082947f0a8a6ef902092d36f8b457668d6
parenta4e149431a218dc24b30541c940c6c1c0441a903 (diff)
downloadqtlocation-mapboxgl-0a2dbc2e56ac4d33e15d1c23f194a98736243da6.tar.gz
[ios,macos] Selecting an offscreen (point) annotation now moves that annotation just on screen.
-rw-r--r--platform/ios/CHANGELOG.md2
-rw-r--r--platform/ios/app/MBXCustomCalloutView.m7
-rw-r--r--platform/ios/app/MBXViewController.m43
-rw-r--r--platform/ios/src/MGLCalloutView.h20
-rw-r--r--platform/ios/src/MGLMapView.mm92
-rwxr-xr-xplatform/ios/vendor/SMCalloutView/SMCalloutView.h12
-rwxr-xr-xplatform/ios/vendor/SMCalloutView/SMCalloutView.m43
-rw-r--r--platform/macos/CHANGELOG.md2
-rw-r--r--platform/macos/app/Base.lproj/MainMenu.xib4
-rw-r--r--platform/macos/app/MapDocument.m37
-rw-r--r--platform/macos/src/MGLMapView.mm92
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;
+ }
+ }
}
}