This commit is contained in:
@@ -36,6 +36,8 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT
|
||||
* Fixed an issue preventing `MGLAnnotationImage.image` from being updated. ([#10372](https://github.com/mapbox/mapbox-gl-native/pull/10372))
|
||||
* 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. Setting the `MGLMapView.selectedAnnotations` property now animates. ([#3249](https://github.com/mapbox/mapbox-gl-native/issues/3249))
|
||||
|
||||
### Map snapshots
|
||||
|
||||
|
||||
@@ -37,10 +37,14 @@ 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:)])
|
||||
{
|
||||
@@ -108,5 +112,4 @@ static CGFloat const tipWidth = 10.0;
|
||||
CGPathRelease(tipPath);
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -54,6 +54,9 @@ typedef NS_ENUM(NSInteger, MBXSettingsAnnotationsRows) {
|
||||
MBXSettingsAnnotationsQueryAnnotations,
|
||||
MBXSettingsAnnotationsCustomUserDot,
|
||||
MBXSettingsAnnotationsRemoveAnnotations,
|
||||
MBXSettingsAnnotationSelectRandomOffscreenPointAnnotation,
|
||||
MBXSettingsAnnotationCenterSelectedAnnotation,
|
||||
MBXSettingsAnnotationAddVisibleAreaPolyline
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, MBXSettingsRuntimeStylingRows) {
|
||||
@@ -340,6 +343,9 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
|
||||
@"Query Annotations",
|
||||
[NSString stringWithFormat:@"%@ Custom User Dot", (_customUserLocationAnnnotationEnabled ? @"Disable" : @"Enable")],
|
||||
@"Remove Annotations",
|
||||
@"Select an offscreen point annotation",
|
||||
@"Center selected annotation",
|
||||
@"Add visible area polyline"
|
||||
]];
|
||||
break;
|
||||
case MBXSettingsRuntimeStyling:
|
||||
@@ -468,6 +474,18 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
|
||||
case MBXSettingsAnnotationsRemoveAnnotations:
|
||||
[self.mapView removeAnnotations:self.mapView.annotations];
|
||||
break;
|
||||
case MBXSettingsAnnotationSelectRandomOffscreenPointAnnotation:
|
||||
[self selectAnOffscreenPointAnnotation];
|
||||
break;
|
||||
|
||||
case MBXSettingsAnnotationCenterSelectedAnnotation:
|
||||
[self centerSelectedAnnotation];
|
||||
break;
|
||||
|
||||
case MBXSettingsAnnotationAddVisibleAreaPolyline:
|
||||
[self addVisibleAreaPolyline];
|
||||
break;
|
||||
|
||||
default:
|
||||
NSAssert(NO, @"All annotations setting rows should be implemented");
|
||||
break;
|
||||
@@ -1551,6 +1569,73 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (id<MGLAnnotation>)randomOffscreenPointAnnotation {
|
||||
|
||||
NSPredicate *pointAnnotationPredicate = [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
|
||||
return [evaluatedObject isKindOfClass:[MGLPointAnnotation class]];
|
||||
}];
|
||||
|
||||
NSArray *annotations = [self.mapView.annotations filteredArrayUsingPredicate:pointAnnotationPredicate];
|
||||
|
||||
if (annotations.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSArray *visibleAnnotations = [self.mapView.visibleAnnotations filteredArrayUsingPredicate:pointAnnotationPredicate];
|
||||
|
||||
if (visibleAnnotations.count == annotations.count) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *invisibleAnnotations = [annotations mutableCopy];
|
||||
|
||||
if (visibleAnnotations.count > 0) {
|
||||
[invisibleAnnotations removeObjectsInArray:visibleAnnotations];
|
||||
}
|
||||
|
||||
// Now pick a random offscreen annotation.
|
||||
uint32_t index = arc4random_uniform((uint32_t)invisibleAnnotations.count);
|
||||
return invisibleAnnotations[index];
|
||||
}
|
||||
|
||||
- (void)selectAnOffscreenPointAnnotation {
|
||||
id<MGLAnnotation> annotation = [self randomOffscreenPointAnnotation];
|
||||
if (annotation) {
|
||||
[self.mapView selectAnnotation:annotation animated:YES];
|
||||
|
||||
NSAssert(self.mapView.selectedAnnotations.firstObject, @"The annotation was not selected");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)centerSelectedAnnotation {
|
||||
id<MGLAnnotation> annotation = self.mapView.selectedAnnotations.firstObject;
|
||||
|
||||
if (!annotation)
|
||||
return;
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
- (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];
|
||||
@@ -1602,7 +1687,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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -48,6 +55,24 @@ 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 = -10.0, .bottom = 0.0, .right = -10.0 }` indicates
|
||||
a 50 point margin above the presentation origin rect (and 10 point margins to the left and the right)
|
||||
in which the callout is assumed to be displayed.
|
||||
|
||||
There are no assumed defaults for these margins, as they should be calculated from the callout that
|
||||
is to be presented. For example, `SMCalloutView` generates the top margin from the callout height, but
|
||||
the left and right margins from a minimum width that the callout should have.
|
||||
|
||||
@param rect Rect that the callout is presented from. This should be the same as the one passed in
|
||||
`-[MGLCalloutView presentCalloutFromRect:inView:constrainedToRect:animated:]`
|
||||
@return `UIEdgeInsets` representing the margins. Values should be negative.
|
||||
*/
|
||||
- (UIEdgeInsets)marginInsetsHintForPresentationFromRect:(CGRect)rect NS_SWIFT_NAME(marginInsetsHintForPresentation(from:));
|
||||
|
||||
/**
|
||||
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
|
||||
|
||||
@@ -1200,17 +1200,32 @@ MGL_EXPORT IB_DESIGNABLE
|
||||
|
||||
Assigning a new array to this property selects only the first annotation in
|
||||
the array.
|
||||
|
||||
If the annotation is of type `MGLPointAnnotation` and is offscreen, the camera
|
||||
will animate to bring the annotation and its callout just on screen. If you
|
||||
need finer control, consider using `-selectAnnotation:animated:`.
|
||||
|
||||
@note In versions prior to `4.0.0` if the annotation was offscreen it was not
|
||||
selected.
|
||||
*/
|
||||
@property (nonatomic, copy) NS_ARRAY_OF(id <MGLAnnotation>) *selectedAnnotations;
|
||||
|
||||
/**
|
||||
Selects an annotation and displays a callout view for it.
|
||||
Selects an annotation and displays its callout view.
|
||||
|
||||
If the given annotation is not visible within the current viewport, this
|
||||
method has no effect.
|
||||
The `animated` parameter determines whether the map is panned to bring the
|
||||
annotation on-screen, specifically:
|
||||
|
||||
| `animated` parameter | Effect |
|
||||
|------------------|--------|
|
||||
| `NO` | The annotation is selected, and the callout is presented. However the map is not panned to bring the annotation or callout onscreen. The presentation of the callout is animated. |
|
||||
| `YES` | The annotation is selected, and the callout is presented. If the annotation is offscreen *and* is of type `MGLPointAnnotation`, the map is panned so that the annotation and its callout are brought just onscreen. The annotation is *not* centered within the viewport. |
|
||||
|
||||
@param annotation The annotation object to select.
|
||||
@param animated If `YES`, the callout view is animated into position.
|
||||
@param animated If `YES`, the annotation and callout view are animated on-screen.
|
||||
|
||||
@note In versions prior to `4.0.0` selecting an offscreen annotation did not
|
||||
change the camera.
|
||||
*/
|
||||
- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated;
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -272,7 +275,7 @@ public:
|
||||
CADisplayLink *_displayLink;
|
||||
BOOL _needsDisplayRefresh;
|
||||
|
||||
NSUInteger _changeDelimiterSuppressionDepth;
|
||||
NSInteger _changeDelimiterSuppressionDepth;
|
||||
|
||||
/// Center coordinate of the pinch gesture on the previous iteration of the gesture.
|
||||
CLLocationCoordinate2D _previousPinchCenterCoordinate;
|
||||
@@ -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 moveOnscreen: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,20 +4283,16 @@ public:
|
||||
|
||||
if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) return;
|
||||
|
||||
// Select the annotation if it’s visible.
|
||||
if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds))
|
||||
{
|
||||
[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 moveOnscreen:animated animateSelection:YES calloutPositioningRect:positioningRect];
|
||||
}
|
||||
|
||||
- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect
|
||||
- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveOnscreen:(BOOL)moveOnscreen animateSelection:(BOOL)animateSelection calloutPositioningRect:(CGRect)calloutPositioningRect
|
||||
{
|
||||
if ( ! annotation) return;
|
||||
|
||||
@@ -4311,22 +4316,34 @@ 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're allowed to move this offscreen annotation on screen, even though we've asked it to
|
||||
if (moveOnscreen) {
|
||||
moveOnscreen = [self isBringingAnnotationOnscreenSupportedForAnnotation:annotation animated:animateSelection];
|
||||
}
|
||||
|
||||
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];
|
||||
@@ -4384,13 +4401,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 (moveOnscreen && [calloutView respondsToSelector:@selector(marginInsetsHintForPresentationFromRect:)]) {
|
||||
UIEdgeInsets margins = [calloutView marginInsetsHintForPresentationFromRect:calloutPositioningRect];
|
||||
expandedPositioningRect = UIEdgeInsetsInsetRect(expandedPositioningRect, margins);
|
||||
}
|
||||
}
|
||||
|
||||
if (moveOnscreen)
|
||||
{
|
||||
moveOnscreen = 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;
|
||||
moveOnscreen = YES;
|
||||
}
|
||||
else if (CGRectGetMaxX(calloutPositioningRect) > CGRectGetMaxX(bounds))
|
||||
{
|
||||
constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width;
|
||||
moveOnscreen = YES;
|
||||
}
|
||||
|
||||
if (CGRectGetMinY(calloutPositioningRect) < CGRectGetMinY(bounds))
|
||||
{
|
||||
constrainedRect.origin.y = expandedPositioningRect.origin.y;
|
||||
moveOnscreen = YES;
|
||||
}
|
||||
else if (CGRectGetMaxY(calloutPositioningRect) > CGRectGetMaxY(bounds))
|
||||
{
|
||||
constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height;
|
||||
moveOnscreen = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Remember, calloutView can be nil here.
|
||||
[calloutView presentCalloutFromRect:calloutPositioningRect
|
||||
inView:self.glView
|
||||
constrainedToRect:constrainedRect
|
||||
animated:animateSelection];
|
||||
|
||||
// notify delegate
|
||||
if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)])
|
||||
{
|
||||
@@ -4401,6 +4456,13 @@ public:
|
||||
{
|
||||
[self.delegate mapView:self didSelectAnnotationView:annotationView];
|
||||
}
|
||||
|
||||
if (moveOnscreen)
|
||||
{
|
||||
CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect));
|
||||
CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self];
|
||||
[self setCenterCoordinate:centerCoord animated:animateSelection];
|
||||
}
|
||||
}
|
||||
|
||||
- (MGLCompactCalloutView *)calloutViewForAnnotation:(id <MGLAnnotation>)annotation
|
||||
@@ -4591,6 +4653,7 @@ public:
|
||||
animated:animated];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Annotation Image Delegate
|
||||
|
||||
- (void)annotationImageNeedsRedisplay:(MGLAnnotationImage *)annotationImage
|
||||
|
||||
@@ -51,12 +51,15 @@ static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReu
|
||||
|
||||
- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated { }
|
||||
|
||||
- (void)presentCalloutFromRect:(CGRect)rect inView:(nonnull UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated {}
|
||||
|
||||
@end
|
||||
|
||||
@interface MGLAnnotationViewTests : XCTestCase <MGLMapViewDelegate>
|
||||
@property (nonatomic) XCTestExpectation *expectation;
|
||||
@property (nonatomic) MGLMapView *mapView;
|
||||
@property (nonatomic, weak) MGLAnnotationView *annotationView;
|
||||
@property (nonatomic) NSInteger annotationSelectedCount;
|
||||
@end
|
||||
|
||||
@implementation MGLAnnotationViewTests
|
||||
@@ -98,6 +101,61 @@ static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReu
|
||||
XCTAssertNotNil(customAnnotationView);
|
||||
}
|
||||
|
||||
- (void)testSelectingOffscreenAnnotation
|
||||
{
|
||||
// Partial test for https://github.com/mapbox/mapbox-gl-native/issues/9790
|
||||
|
||||
// This isn't quite the same as in updateAnnotationViews, but should be sufficient for this test.
|
||||
MGLCoordinateBounds coordinateBounds = [_mapView convertRect:_mapView.bounds toCoordinateBoundsFromView:_mapView];
|
||||
|
||||
// -90 latitude is invalid. TBD.
|
||||
BOOL anyOffscreen = NO;
|
||||
NSInteger selectionCount = 0;
|
||||
|
||||
for (NSInteger latitude = -89; latitude <= 90; latitude += 10)
|
||||
{
|
||||
for (NSInteger longitude = -180; longitude <= 180; longitude += 10)
|
||||
{
|
||||
MGLTestAnnotation *annotation = [[MGLTestAnnotation alloc] init];
|
||||
|
||||
annotation.coordinate = CLLocationCoordinate2DMake(latitude, longitude);
|
||||
[_mapView addAnnotation:annotation];
|
||||
|
||||
if (!(MGLCoordinateInCoordinateBounds(annotation.coordinate, coordinateBounds)))
|
||||
anyOffscreen = YES;
|
||||
|
||||
XCTAssertNil(_mapView.selectedAnnotations.firstObject, @"There should be no selected annotation");
|
||||
|
||||
// First selection
|
||||
[_mapView selectAnnotation:annotation animated:NO];
|
||||
selectionCount++;
|
||||
|
||||
XCTAssert(_mapView.selectedAnnotations.count == 1, @"There should only be 1 selected annotation");
|
||||
XCTAssertEqualObjects(_mapView.selectedAnnotations.firstObject, annotation, @"The annotation should be selected");
|
||||
|
||||
// Deselect
|
||||
[_mapView deselectAnnotation:annotation animated:NO];
|
||||
XCTAssert(_mapView.selectedAnnotations.count == 0, @"There should be no selected annotations");
|
||||
|
||||
// Second selection
|
||||
_mapView.selectedAnnotations = @[annotation];
|
||||
selectionCount++;
|
||||
|
||||
XCTAssert(_mapView.selectedAnnotations.count == 1, @"There should be 1 selected annotation");
|
||||
XCTAssertEqualObjects(_mapView.selectedAnnotations.firstObject, annotation, @"The annotation should be selected");
|
||||
|
||||
// Deselect
|
||||
[_mapView deselectAnnotation:annotation animated:NO];
|
||||
XCTAssert(_mapView.selectedAnnotations.count == 0, @"There should be no selected annotations");
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssert(anyOffscreen, @"At least one of these annotations should be offscreen");
|
||||
XCTAssertEqual(selectionCount, self.annotationSelectedCount, @"-mapView:didSelectAnnotation: should be called for each selection");
|
||||
}
|
||||
|
||||
#pragma mark - MGLMapViewDelegate -
|
||||
|
||||
- (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id<MGLAnnotation>)annotation
|
||||
{
|
||||
MGLAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:MGLTestAnnotationReuseIdentifer];
|
||||
@@ -117,4 +175,9 @@ static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReu
|
||||
[_expectation fulfill];
|
||||
}
|
||||
|
||||
- (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(id<MGLAnnotation>)annotation
|
||||
{
|
||||
self.annotationSelectedCount++;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -113,6 +113,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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
### Annotations
|
||||
* 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))
|
||||
|
||||
### Map snapshots
|
||||
|
||||
|
||||
@@ -545,7 +545,13 @@
|
||||
<action selector="drawAnimatedAnnotation:" target="-1" id="CYM-WB-s97"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All Annnotations" keyEquivalent="A" id="yMj-uM-8SN">
|
||||
<menuItem title="Select an Offscreen Point Annotation" id="Xy2-Cc-RUB">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="selectOffscreenPointAnnotation:" target="-1" id="Fhm-l3-G6h"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All Annotations" keyEquivalent="A" id="yMj-uM-8SN">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="showAllAnnotations:" target="-1" id="ahr-OR-Em2"/>
|
||||
@@ -664,7 +670,7 @@ CA
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="109" y="131" width="350" height="84"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
|
||||
<view key="contentView" id="eA4-n3-qPe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="350" height="84"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
@@ -740,7 +746,7 @@ CA
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" utility="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="830" y="430" width="400" height="300"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
|
||||
<view key="contentView" id="8ha-hw-zOD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="400" height="300"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
@@ -748,11 +754,11 @@ CA
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Q8b-0e-dLv">
|
||||
<rect key="frame" x="-1" y="20" width="402" height="281"/>
|
||||
<clipView key="contentView" id="J9U-Yx-o2S">
|
||||
<rect key="frame" x="1" y="0.0" width="400" height="280"/>
|
||||
<rect key="frame" x="1" y="0.0" width="400" height="265"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" autosaveColumns="NO" headerView="MAZ-Iq-hBi" id="Ato-Vu-HYT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="423" height="257"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="423" height="242"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -891,7 +897,7 @@ CA
|
||||
</subviews>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" verticalHuggingPriority="750" horizontal="YES" id="QLr-6P-Ogs">
|
||||
<rect key="frame" x="1" y="264" width="400" height="16"/>
|
||||
<rect key="frame" x="1" y="265" width="400" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="q0K-eE-mzL">
|
||||
|
||||
@@ -641,6 +641,51 @@ NS_ARRAY_OF(id <MGLAnnotation>) *MBXFlattenedShapes(NS_ARRAY_OF(id <MGLAnnotatio
|
||||
repeats:YES];
|
||||
}
|
||||
|
||||
|
||||
- (id<MGLAnnotation>)randomOffscreenPointAnnotation {
|
||||
|
||||
NSPredicate *pointAnnotationPredicate = [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
|
||||
return [evaluatedObject isKindOfClass:[MGLPointAnnotation class]];
|
||||
}];
|
||||
|
||||
NSArray *annotations = [self.mapView.annotations filteredArrayUsingPredicate:pointAnnotationPredicate];
|
||||
|
||||
if (annotations.count == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// NOTE: self.mapView.visibleAnnotations occasionally returns nil - see
|
||||
// https://github.com/mapbox/mapbox-gl-native/issues/11296
|
||||
NSArray *visibleAnnotations = [self.mapView.visibleAnnotations filteredArrayUsingPredicate:pointAnnotationPredicate];
|
||||
|
||||
NSLog(@"Number of visible point annotations = %ld", visibleAnnotations.count);
|
||||
|
||||
if (visibleAnnotations.count == annotations.count) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *invisibleAnnotations = [annotations mutableCopy];
|
||||
|
||||
if (visibleAnnotations.count > 0) {
|
||||
[invisibleAnnotations removeObjectsInArray:visibleAnnotations];
|
||||
}
|
||||
|
||||
// Now pick a random offscreen annotation.
|
||||
uint32_t index = arc4random_uniform((uint32_t)invisibleAnnotations.count);
|
||||
return invisibleAnnotations[index];
|
||||
}
|
||||
|
||||
- (IBAction)selectOffscreenPointAnnotation:(id)sender {
|
||||
id<MGLAnnotation> annotation = [self randomOffscreenPointAnnotation];
|
||||
if (annotation) {
|
||||
[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)updateAnimatedAnnotation:(NSTimer *)timer {
|
||||
DroppedPinAnnotation *annotation = timer.userInfo;
|
||||
double angle = timer.fireDate.timeIntervalSinceReferenceDate;
|
||||
@@ -1111,6 +1156,9 @@ 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(selectOffscreenPointAnnotation:)) {
|
||||
return YES;
|
||||
}
|
||||
if (menuItem.action == @selector(showAllAnnotations:) || menuItem.action == @selector(removeAllAnnotations:)) {
|
||||
return self.mapView.annotations.count > 0;
|
||||
}
|
||||
|
||||
@@ -721,16 +721,27 @@ MGL_EXPORT IB_DESIGNABLE
|
||||
|
||||
Assigning a new array to this property selects only the first annotation in the
|
||||
array.
|
||||
|
||||
If the annotation is of type `MGLPointAnnotation` and is offscreen, the map is
|
||||
panned so that the annotation and its callout are brought just onscreen. The
|
||||
annotation is *not* centered within the viewport.
|
||||
|
||||
@note In versions prior to `4.0.0` if the annotation was offscreen it was not
|
||||
selected.
|
||||
*/
|
||||
@property (nonatomic, copy) NS_ARRAY_OF(id <MGLAnnotation>) *selectedAnnotations;
|
||||
|
||||
/**
|
||||
Selects an annotation and displays a callout popover for it.
|
||||
|
||||
If the given annotation is not visible within the current viewport, this method
|
||||
has no effect.
|
||||
If the annotation is of type `MGLPointAnnotation` and is offscreen, the map is
|
||||
panned so that the annotation and its callout are brought just onscreen. The
|
||||
annotation is *not* centered within the viewport.
|
||||
|
||||
@param annotation The annotation object to select.
|
||||
|
||||
@note In versions prior to `4.0.0` selecting an offscreen annotation did not
|
||||
change the camera.
|
||||
*/
|
||||
- (void)selectAnnotation:(id <MGLAnnotation>)annotation;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2205,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
|
||||
@@ -2217,6 +2222,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) {
|
||||
@@ -2232,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);
|
||||
}
|
||||
|
||||
@@ -2254,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 {
|
||||
@@ -2393,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user