[ios] Partially offscreen annotations (without callouts) are no longer moved on-screen (#13727)

This commit is contained in:
Julian Rex
2019-01-22 17:45:06 -05:00
committed by GitHub
parent c7869e7eb4
commit b460efdf00
10 changed files with 563 additions and 62 deletions

View File

@@ -10,6 +10,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT
* Reinstates version 11 as the default Mapbox Streets style (as introduced in 4.7.0). ([#13690](https://github.com/mapbox/mapbox-gl-native/pull/13690))
* Added the `-[MGLShapeSource leavesOfCluster:offset:limit:]`, `-[MGLShapeSource childrenOfCluster:]`, `-[MGLShapeSource zoomLevelForExpandingCluster:]` methods for inspecting a cluster in an `MGLShapeSource`s created with the `MGLShapeSourceOptionClustered` option. Feature querying now returns clusters represented by `MGLPointFeatureCluster` objects (that conform to the `MGLCluster` protocol). ([#12952](https://github.com/mapbox/mapbox-gl-native/pull/12952)
* `MGLMapView` no longer freezes on external displays connected through AirPlay or CarPlay when the main device’s screen goes to sleep or the user manually locks the screen. ([#13701](https://github.com/mapbox/mapbox-gl-native/pull/13701))
* Fixed a bug where selecting partially on-screen annotations (without a callout) would move the map. ([#13727](https://github.com/mapbox/mapbox-gl-native/pull/13727))
## 4.7.1 - December 21, 2018

View File

@@ -2,9 +2,26 @@
#import "MGLTestUtility.h"
#import "MGLMapAccessibilityElement.h"
#import "MGLTestLocationManager.h"
#import "MGLCompactCalloutView.h"
@interface MGLTestCalloutView : MGLCompactCalloutView
@property (nonatomic) BOOL implementsMarginHints;
@end
@implementation MGLTestCalloutView
- (BOOL)respondsToSelector:(SEL)aSelector {
if (!self.implementsMarginHints &&
(aSelector == @selector(marginInsetsHintForPresentationFromRect:))) {
return NO;
}
return [super respondsToSelector:aSelector];
}
@end
@interface MGLMapView (Tests)
- (MGLAnnotationTag)annotationTagAtPoint:(CGPoint)point persistingResults:(BOOL)persist;
@property (nonatomic) UIView<MGLCalloutView> *calloutViewForSelectedAnnotation;
@end
@interface MGLAnnotationViewIntegrationTests : MGLMapViewIntegrationTest
@@ -12,6 +29,368 @@
@implementation MGLAnnotationViewIntegrationTests
#pragma mark - Offscreen/panning selection tests
typedef struct PanTestData {
CGPoint relativeCoord;
BOOL showsCallout;
BOOL implementsMargins;
BOOL moveIntoView;
BOOL expectMapToHavePanned;
BOOL calloutOnScreen;
} PanTestData;
#define PAN_TEST_TERMINATOR {{FLT_MAX, FLT_MAX}, NO, NO, NO, NO, NO}
static const CGFloat kAnnotationScale = 0.125f;
- (void)internalTestOffscreenSelectionTitle:(NSString*)title withTestData:(PanTestData)test animateSelection:(BOOL)animateSelection {
CGPoint relativeCoordinate = test.relativeCoord;
BOOL showsCallout = test.showsCallout;
BOOL calloutImplementsMarginHints = test.implementsMargins;
BOOL moveIntoView = test.moveIntoView;
BOOL expectMapToHavePanned = test.expectMapToHavePanned;
BOOL expectCalloutToBeFullyOnscreen = test.calloutOnScreen;
// Reset the map to a consistent state - want the map to be zoomed in, so that
// it's free to be panned without hitting boundaries.
[self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(0, 0) zoomLevel:14 animated:NO];
[self waitForMapViewToBeRenderedWithTimeout:1.0];
XCTAssert(self.mapView.annotations.count == 0);
NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReuseIdentifer";
CGSize size = self.mapView.bounds.size;
CGSize annotationSize = CGSizeMake(floor(size.width*kAnnotationScale), floor(size.height*kAnnotationScale));
self.viewForAnnotation = ^MGLAnnotationView*(MGLMapView *view, id<MGLAnnotation> annotation) {
if (![annotation isKindOfClass:[MGLPointAnnotation class]]) {
return nil;
}
// No dequeue
MGLAnnotationView *annotationView = [[MGLAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:MGLTestAnnotationReuseIdentifer];
annotationView.bounds = (CGRect){ .origin = CGPointZero, .size = annotationSize };
annotationView.backgroundColor = UIColor.redColor;
annotationView.enabled = YES;
return annotationView;
};
// Coordinate for annotation screen coordinate
CGPoint annotationPoint = CGPointMake(relativeCoordinate.x * size.width, relativeCoordinate.y * size.height);
CLLocationCoordinate2D coordinate = [self.mapView convertPoint:annotationPoint toCoordinateFromView:self.mapView];
MGLPointAnnotation *point = [[MGLPointAnnotation alloc] init];
point.title = title;
point.coordinate = coordinate;
self.mapViewAnnotationCanShowCalloutForAnnotation = ^BOOL(MGLMapView *mapView, id<MGLAnnotation> annotation) {
return showsCallout;
};
self.mapViewCalloutViewForAnnotation = ^id<MGLCalloutView>(MGLMapView *mapView, id<MGLAnnotation> annotation) {
if (!showsCallout)
return nil;
MGLTestCalloutView *calloutView = [[MGLTestCalloutView alloc] init];
calloutView.representedObject = annotation;
calloutView.implementsMarginHints = calloutImplementsMarginHints;
return calloutView;
};
[self.mapView addAnnotation:point];
// Check assumptions before selection
UIView *annotationViewBeforeSelection = [self.mapView viewForAnnotation:point];
XCTAssertNotNil(annotationViewBeforeSelection);
CLLocationCoordinate2D mapCenterBeforeSelection = self.mapView.centerCoordinate;
XCTAssert(CLLocationCoordinate2DIsValid(mapCenterBeforeSelection));
// Also note, that at this point, the internal mechanism that determines if
// an annotation view is offscreen and should be put back in the reuse queue
// will have run, and `viewForAnnotation` may return nil
[self.mapView selectAnnotation:point moveIntoView:moveIntoView animateSelection:animateSelection];
// Animated selection takes MGLAnimationDuration (0.3 seconds), so wait a little
// longer. We don't need to wait as long if we're not animated (but we do
// want the runloop to tick over)
[self waitFor:animateSelection ? 0.4: 0.05];
UIView *annotationViewAfterSelection = [self.mapView viewForAnnotation:point];
CLLocationCoordinate2D mapCenterAfterSelection = self.mapView.centerCoordinate;
XCTAssert(CLLocationCoordinate2DIsValid(mapCenterAfterSelection));
// If the annotation is still "offscreen" at this point, then the above annotation view
// may be nil, which is expected.
BOOL (^CGRectContainsRectWithAccuracy)(CGRect, CGRect, CGFloat) = ^(CGRect rect1, CGRect rect2, CGFloat accuracy) {
CGRect expandedRect1 = CGRectInset(rect1, -accuracy, -accuracy);
return CGRectContainsRect(expandedRect1, rect2);
};
CGFloat epsilon = 0.00001;
if (expectMapToHavePanned) {
CLLocationDegrees latitudeDelta = fabs(mapCenterAfterSelection.latitude - mapCenterBeforeSelection.latitude);
CLLocationDegrees longitudeDelta = fabs(mapCenterAfterSelection.longitude - mapCenterBeforeSelection.longitude);
XCTAssert( (latitudeDelta > epsilon) || (longitudeDelta > epsilon), @"Deltas: lat=%f, long=%f", latitudeDelta, longitudeDelta); // One of them should have moved
// If the map panned - the intention is that the annotation is on-screen,
// and so should have an annotation view and that it is fully on screen
CGRect annotationFrameAfterSelection = annotationViewAfterSelection.frame;
XCTAssertNotNil(annotationViewAfterSelection);
XCTAssert(CGRectContainsRectWithAccuracy(self.mapView.bounds, annotationFrameAfterSelection, 0.25), @"Mapview:%@ frame:%@", NSStringFromCGRect(self.mapView.bounds), NSStringFromCGRect(annotationFrameAfterSelection));
// Check the callout
if (showsCallout) {
UIView *calloutView = self.mapView.calloutViewForSelectedAnnotation;
XCTAssertNotNil(calloutView);
// If kAnnotationScale == 0.25, then the following assert can fail.
// This is really a warning (see https://github.com/mapbox/mapbox-gl-native/issues/13744 )
// If you need this NOT to fail the tests, consider replacing with MGLTestWarning
XCTAssert(expectCalloutToBeFullyOnscreen == CGRectContainsRectWithAccuracy(self.mapView.bounds, calloutView.frame, 0.25),
@"Expect contains:%d, Mapview:%@ annotation:%@ callout:%@",
expectCalloutToBeFullyOnscreen,
NSStringFromCGRect(self.mapView.bounds),
NSStringFromCGRect(annotationFrameAfterSelection),
NSStringFromCGRect(calloutView.frame));
}
}
else {
// The map shouldn't have moved, so use equality (rather than an error check)
XCTAssertEqual(mapCenterBeforeSelection.latitude, mapCenterAfterSelection.latitude);
XCTAssertEqual(mapCenterBeforeSelection.longitude, mapCenterAfterSelection.longitude);
// Annotation shouldn't have moved
CGPoint annotationPoint2 = [self.mapView convertCoordinate:point.coordinate toPointToView:self.mapView];
CGFloat xDelta = fabs(annotationPoint2.x - annotationPoint.x);
CGFloat yDelta = fabs(annotationPoint2.y - annotationPoint.y);
XCTAssert((xDelta < epsilon) && (yDelta < epsilon));
if (showsCallout) {
UIView *calloutView = self.mapView.calloutViewForSelectedAnnotation;
if (annotationViewAfterSelection) {
XCTAssertNotNil(calloutView);
// If kAnnotationScale == 0.25, then the following assert can fail.
// This is really a warning (see https://github.com/mapbox/mapbox-gl-native/issues/13744 )
// If you need this NOT to fail the tests, consider replacing with MGLTestWarning
XCTAssert((expectCalloutToBeFullyOnscreen == CGRectContainsRectWithAccuracy(self.mapView.bounds, calloutView.frame, 0.25)),
@"Mapview:%@ annotation:%@ callout:%@",
NSStringFromCGRect(self.mapView.bounds),
NSStringFromCGRect(annotationViewAfterSelection.frame),
NSStringFromCGRect(calloutView.frame));
}
else {
// If there's no annotation view, should we expect a callout?
XCTAssertNil(calloutView);
XCTAssertFalse(expectCalloutToBeFullyOnscreen);
}
}
}
// Remove the annotation
[self.mapView removeAnnotation:point];
XCTAssert(self.mapView.annotations.count == 0);
}
// See https://github.com/mapbox/mapbox-gl-native/pull/13727#issuecomment-454028698
// What follows are tests based on this table.
// This is not a full-set of possible combinations, just the most important/likely
// ones
- (void)internalRunTests:(PanTestData*)testData
{
// Test both animated and not-animated.
for (int i = 0; i<2; i++) {
int row = 0;
PanTestData *test = testData;
while (test->relativeCoord.x != FLT_MAX) {
NSString *activityTitle = [NSString stringWithFormat:@"Row %d/%d", row, i];
[XCTContext runActivityNamed:activityTitle
block:^(id<XCTActivity> _Nonnull activity) {
[self internalTestOffscreenSelectionTitle:activityTitle
withTestData:*test
animateSelection:!i];
}];
++test;
++row;
}
}
}
- (void)testBasicSelection {
// Tests moveIntoView:NO
// WITHOUT a callout
PanTestData tests[] = {
// Coord showsCallout impl margins? moveIntoView expectMapToPan calloutOnScreen
// Offscreen
{ {-1.0f, 0.5f}, NO, NO, NO, NO, NO },
{ { 2.0f, 0.5f}, NO, NO, NO, NO, NO },
{ { 0.5f,-1.0f}, NO, NO, NO, NO, NO },
{ { 0.5f, 2.0f}, NO, NO, NO, NO, NO },
// Partial
{ { 0.0f, 0.5f}, NO, NO, NO, NO, NO },
{ { 1.0f, 0.5f}, NO, NO, NO, NO, NO },
{ { 0.5f, 0.0f}, NO, NO, NO, NO, NO },
{ { 0.5f, 1.0f}, NO, NO, NO, NO, NO },
// Onscreen
{ { 0.5f, 0.5f}, NO, NO, NO, NO, NO },
PAN_TEST_TERMINATOR
};
[self internalRunTests:tests];
}
- (void)testBasicSelectionWithCallout {
// Tests moveIntoView:NO
// WITH the default callout (implements marginshint)
PanTestData tests[] = {
// Coord showsCallout impl margins? moveIntoView expectMapToPan calloutOnScreen
{ {-1.0f, 0.5f}, YES, YES, NO, NO, NO },
{ { 0.0f, 0.5f}, YES, YES, NO, NO, NO },
{ { 0.5f, 1.0f}, YES, YES, NO, NO, YES }, // Because annotation was off the bottom of screen, and callout is above annotation
{ { 0.5f, 0.5f}, YES, YES, NO, NO, YES },
PAN_TEST_TERMINATOR
};
[self internalRunTests:tests];
}
- (void)testSelectionMoveIntoView {
// Tests moveIntoView:YES
// without a callout
// From https://github.com/mapbox/mapbox-gl-native/pull/13727#issuecomment-454028698
//
// | Annotation position | Has callout? | Callout implements `marginInsets...`? | Map pans when selected with moveIntoView=YES? |
// |---------------------|--------------|---------------------------------------|-----------------------------------------------|
// | Offscreen | No | n/a | Yes (no margins) |
// | Partially | No | n/a | No |
// | Onscreen | No | n/a | No |
//
PanTestData tests[] = {
// Coord showsCallout impl margins? moveIntoView expectMapToPan calloutOnScreen
// Offscreen
{ {-1.0f, 0.5f}, NO, NO, YES, YES, NO },
{ { 2.0f, 0.5f}, NO, NO, YES, YES, NO },
{ { 0.5f,-1.0f}, NO, NO, YES, YES, NO },
{ { 0.5f, 2.0f}, NO, NO, YES, YES, NO },
// Partial
{ { 0.0f, 0.5f}, NO, NO, YES, NO, NO },
{ { 1.0f, 0.5f}, NO, NO, YES, NO, NO },
{ { 0.5f, 0.0f}, NO, NO, YES, NO, NO },
{ { 0.5f, 1.0f}, NO, NO, YES, NO, NO },
// Onscreen
{ { 0.5f, 0.5f}, NO, NO, YES, NO, NO },
PAN_TEST_TERMINATOR
};
[self internalRunTests:tests];
}
- (void)testSelectionMoveIntoViewWithCallout {
// Tests moveIntoView:YES
// WITH the default callout (implements marginshint)
// From https://github.com/mapbox/mapbox-gl-native/pull/13727#issuecomment-454028698
//
// | Annotation position | Has callout? | Callout implements `marginInsets...`? | Map pans when selected with moveIntoView=YES? |
// |---------------------|--------------|---------------------------------------|-----------------------------------------------|
// | Offscreen | Yes | Yes | Yes to ensure callout is fully visible |
// | Partially | Yes | Yes | Yes to ensure callout is fully visible |
// | Onscreen | Yes | Yes | Yes, but *only* to ensure callout is fully visible |
//
PanTestData tests[] = {
// Coord showsCallout impl margins? moveIntoView expectMapToPan calloutOnScreen
// Offscreen
{ {-1.0f, 0.5f}, YES, YES, YES, YES, YES },
{ { 2.0f, 0.5f}, YES, YES, YES, YES, YES },
{ { 0.5f,-1.0f}, YES, YES, YES, YES, YES },
{ { 0.5f, 2.0f}, YES, YES, YES, YES, YES },
// Partial
{ { 0.0f, 0.5f}, YES, YES, YES, YES, YES },
{ { 1.0f, 0.5f}, YES, YES, YES, YES, YES },
{ { 0.5f, 0.0f}, YES, YES, YES, YES, YES },
{ { 0.5f, 1.0f}, YES, YES, YES, YES, YES },
// Onscreen
{ { 0.5f, 0.5f}, YES, YES, YES, NO, YES },
// Just at the edge of the screen.
// Expects to move, because although onscreen, callout would not be.
// However, if the scale is 0.25, then expectToPan should be NO, because
// of the width of the annotation
//
// Coord showsCallout impl margins? moveIntoView expectMapToPan calloutOnScreen
{ {kAnnotationScale, 0.5f}, YES, YES, YES, (kAnnotationScale == 0.125f), YES },
PAN_TEST_TERMINATOR
};
[self internalRunTests:tests];
}
- (void)testSelectionMoveIntoViewWithBasicCallout {
// Tests moveIntoView:YES
// WITH a callout that DOES NOT implement marginshint
// From https://github.com/mapbox/mapbox-gl-native/pull/13727#issuecomment-454028698
//
// | Annotation position | Has callout? | Callout implements `marginInsets...`? | Map pans when selected with moveIntoView=YES? |
// |---------------------|--------------|---------------------------------------|-----------------------------------------------|
// | Offscreen | Yes | No | Yes, but only to show annotation (not callout) with no margins |
// | Partially | Yes | No | No |
// | Onscreen | Yes | No | No |
//
PanTestData tests[] = {
// Coord showsCallout impl margins? moveIntoView expectMapToPan calloutOnScreen
// Offscreen
{ {-1.0f, 0.5f}, YES, NO, YES, YES, NO },
{ { 2.0f, 0.5f}, YES, NO, YES, YES, NO },
{ { 0.5f,-1.0f}, YES, NO, YES, YES, NO },
{ { 0.5f, 2.0f}, YES, NO, YES, YES, YES }, // Because annotation was off the bottom of screen, and callout is above annotation
{ { 2.0f, 2.0f}, YES, NO, YES, YES, NO },
// Partial
{ { 0.0f, 0.5f}, YES, NO, YES, NO, NO },
{ { 1.0f, 0.5f}, YES, NO, YES, NO, NO },
{ { 0.5f, 0.0f}, YES, NO, YES, NO, NO },
{ { 0.5f, 1.0f}, YES, NO, YES, NO, YES }, // Because annotation was off the bottom of screen, and callout is above annotation
{ { 1.0f, 1.0f}, YES, NO, YES, NO, NO },
// Onscreen
{ { 0.5f, 0.5f}, YES, NO, YES, NO, YES },
PAN_TEST_TERMINATOR
};
[self internalRunTests:tests];
}
#pragma mark - Selection with an offset
- (void)testSelectingAnnotationWithCenterOffset {
for (CGFloat dx = -100.0; dx <= 100.0; dx += 100.0 ) {
@@ -66,8 +445,8 @@
// Check that the annotation is in the center of the view
CGPoint annotationPoint = [self.mapView convertCoordinate:point.coordinate toPointToView:self.mapView];
XCTAssertEqualWithAccuracy(annotationPoint.x, size.width/2.0, epsilon);
XCTAssertEqualWithAccuracy(annotationPoint.y, size.height/2.0, epsilon);
XCTAssertEqualWithAccuracy(annotationPoint.x, size.width/2.0, 0.25);
XCTAssertEqualWithAccuracy(annotationPoint.y, size.height/2.0, 0.25);
// Now test taps around the annotation
CGPoint tapPoint = CGPointMake(annotationPoint.x + offset.dx, annotationPoint.y + offset.dy);
@@ -117,6 +496,30 @@
XCTAssertEqual(originalFrame.origin.y + offset.y, offsetFrame.origin.y);
}
#pragma mark - Utilities
- (void)runRunLoop {
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
- (void)waitFor:(NSTimeInterval)seconds {
XCTestExpectation *timerExpired = [self expectationWithDescription:@"Timer expires"];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(runRunLoop)
userInfo:nil
repeats:YES];
double duration = seconds * (double)NSEC_PER_SEC;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)duration), dispatch_get_main_queue(), ^{
[timerExpired fulfill];
});
[self waitForExpectations:@[timerExpired] timeout:seconds + 1.0];
[timer invalidate];
}
- (void)waitForCollisionDetectionToRun {
XCTAssertNil(self.renderFinishedExpectation, @"Incorrect test setup");
@@ -129,6 +532,8 @@
});
[self waitForExpectations:@[timerExpired, self.renderFinishedExpectation] timeout:5];
self.renderFinishedExpectation = nil;
}
@end

View File

@@ -16,6 +16,13 @@
#define MGLTestAssertNotNil(myself, expression, ...) \
_XCTPrimitiveAssertNotNil(myself, expression, @#expression, __VA_ARGS__)
#define MGLTestWarning(expression, format, ...) \
({ \
if (!(expression)) { \
NSString *message = [NSString stringWithFormat:format, ##__VA_ARGS__]; \
printf("warning: Test Case '%s' at line %d: '%s' %s\n", __PRETTY_FUNCTION__, __LINE__, #expression, message.UTF8String); \
} \
})
@interface MGLMapViewIntegrationTest : XCTestCase <MGLMapViewDelegate>
@property (nonatomic) MGLMapView *mapView;
@@ -27,6 +34,8 @@
@property (nonatomic) void (^regionIsChanging)(MGLMapView *mapView);
@property (nonatomic) void (^regionDidChange)(MGLMapView *mapView, MGLCameraChangeReason reason, BOOL animated);
@property (nonatomic) CGPoint (^mapViewUserLocationAnchorPoint)(MGLMapView *mapView);
@property (nonatomic) BOOL (^mapViewAnnotationCanShowCalloutForAnnotation)(MGLMapView *mapView, id<MGLAnnotation> annotation);
@property (nonatomic) id<MGLCalloutView> (^mapViewCalloutViewForAnnotation)(MGLMapView *mapView, id<MGLAnnotation> annotation);
// Utility methods
- (NSString*)validAccessToken;

View File

@@ -108,6 +108,20 @@
return CGPointZero;
}
- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id<MGLAnnotation>)annotation {
if (self.mapViewAnnotationCanShowCalloutForAnnotation) {
return self.mapViewAnnotationCanShowCalloutForAnnotation(mapView, annotation);
}
return NO;
}
- (id<MGLCalloutView>)mapView:(MGLMapView *)mapView calloutViewForAnnotation:(id<MGLAnnotation>)annotation {
if (self.mapViewCalloutViewForAnnotation) {
return self.mapViewCalloutViewForAnnotation(mapView, annotation);
}
return nil;
}
#pragma mark - Utilities
- (void)waitForMapViewToFinishLoadingStyleWithTimeout:(NSTimeInterval)timeout {

View File

@@ -474,6 +474,8 @@
CA0C27942076CA19001CE5B7 /* MGLMapViewIntegrationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CA0C27932076CA19001CE5B7 /* MGLMapViewIntegrationTest.m */; };
CA1B4A512099FB2200EDD491 /* MGLMapSnapshotterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CA1B4A502099FB2200EDD491 /* MGLMapSnapshotterTest.m */; };
CA34C9C3207FD272005C1A06 /* MGLCameraTransitionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CA34C9C2207FD272005C1A06 /* MGLCameraTransitionTests.mm */; };
CA45105A21EE2989000C37D5 /* MGLCompactCalloutView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA8848451CBAFB9800AB86E3 /* MGLCompactCalloutView.m */; };
CA45105B21EE29B3000C37D5 /* SMCalloutView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA88488A1CBB037E00AB86E3 /* SMCalloutView.m */; };
CA4EB8C720863487006AB465 /* MGLStyleLayerIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CA4EB8C620863487006AB465 /* MGLStyleLayerIntegrationTests.m */; };
CA55CD41202C16AA00CE7095 /* MGLCameraChangeReason.h in Headers */ = {isa = PBXBuildFile; fileRef = CA55CD3E202C16AA00CE7095 /* MGLCameraChangeReason.h */; settings = {ATTRIBUTES = (Public, ); }; };
CA55CD42202C16AA00CE7095 /* MGLCameraChangeReason.h in Headers */ = {isa = PBXBuildFile; fileRef = CA55CD3E202C16AA00CE7095 /* MGLCameraChangeReason.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -3034,10 +3036,12 @@
buildActionMask = 2147483647;
files = (
CA4EB8C720863487006AB465 /* MGLStyleLayerIntegrationTests.m in Sources */,
CA45105B21EE29B3000C37D5 /* SMCalloutView.m in Sources */,
CA34C9C3207FD272005C1A06 /* MGLCameraTransitionTests.mm in Sources */,
16376B0A1FFD9DAF0000563E /* MBGLIntegrationTests.m in Sources */,
CA88DC3021C85D900059ED5A /* MGLStyleURLIntegrationTest.m in Sources */,
CA0C27942076CA19001CE5B7 /* MGLMapViewIntegrationTest.m in Sources */,
CA45105A21EE2989000C37D5 /* MGLCompactCalloutView.m in Sources */,
CAE7AD5520F46EF5003B6782 /* MGLMapSnapshotterSwiftTests.swift in Sources */,
CA0C27922076C804001CE5B7 /* MGLShapeSourceTests.m in Sources */,
077061DA215DA00E000FEF62 /* MGLTestLocationManager.m in Sources */,

View File

@@ -152,9 +152,6 @@ 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 };
@@ -229,6 +226,11 @@ public:
/// Currently shown popover representing the selected annotation.
@property (nonatomic) UIView<MGLCalloutView> *calloutViewForSelectedAnnotation;
/// Anchor coordinate from which to present callout views (for example, for shapes this
/// could be the touch point rather than its centroid)
@property (nonatomic) CLLocationCoordinate2D anchorCoordinateForSelectedAnnotation;
@property (nonatomic) MGLUserLocationAnnotationView *userLocationAnnotationView;
/// Indicates how thoroughly the map view is tracking the user location.
@@ -4537,7 +4539,7 @@ public:
- (BOOL)isMovingAnnotationIntoViewSupportedForAnnotation:(id<MGLAnnotation>)annotation animated:(BOOL)animated {
// Consider delegating
return animated && [annotation isKindOfClass:[MGLPointAnnotation class]];
return [annotation isKindOfClass:[MGLPointAnnotation class]];
}
- (id <MGLAnnotation>)selectedAnnotation
@@ -4644,12 +4646,15 @@ public:
CGPoint originPoint = [self convertCoordinate:origin toPointToView:self];
calloutPositioningRect = { .origin = originPoint, .size = CGSizeZero };
}
CGRect expandedPositioningRect = UIEdgeInsetsInsetRect(calloutPositioningRect, MGLMapViewOffscreenAnnotationPadding);
CGRect expandedPositioningRect = calloutPositioningRect;
// Used for callout positioning, and moving offscreen annotations onscreen.
CGRect constrainedRect = UIEdgeInsetsInsetRect(self.bounds, self.contentInset);
CGRect constrainedRect = self.contentFrame;
CGRect bounds = constrainedRect;
BOOL expandedPositioningRectToMoveCalloutIntoViewWithMargins = NO;
UIView <MGLCalloutView> *calloutView = nil;
if ([annotation respondsToSelector:@selector(title)] &&
@@ -4720,37 +4725,55 @@ public:
if (moveIntoView && [calloutView respondsToSelector:@selector(marginInsetsHintForPresentationFromRect:)]) {
UIEdgeInsets margins = [calloutView marginInsetsHintForPresentationFromRect:calloutPositioningRect];
expandedPositioningRect = UIEdgeInsetsInsetRect(expandedPositioningRect, margins);
expandedPositioningRectToMoveCalloutIntoViewWithMargins = YES;
}
}
if (!expandedPositioningRectToMoveCalloutIntoViewWithMargins)
{
// We don't have a callout (OR our callout didn't implement
// marginInsetsHintForPresentationFromRect: - in this case we need to
// ensure that partially off-screen annotations are NOT moved into view.
//
// We may want to create (and fallback to) an `MGLMapViewDelegate` version
// of the `-[MGLCalloutView marginInsetsHintForPresentationFromRect:]
// protocol method.
bounds = CGRectInset(bounds, -calloutPositioningRect.size.width, -calloutPositioningRect.size.height);
}
if (moveIntoView)
{
moveIntoView = 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;
CGFloat minX = CGRectGetMinX(expandedPositioningRect);
if (minX < CGRectGetMinX(bounds)) {
constrainedRect.origin.x = minX;
moveIntoView = YES;
}
else if (CGRectGetMaxX(calloutPositioningRect) > CGRectGetMaxX(bounds))
{
constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width;
moveIntoView = YES;
else {
CGFloat maxX = CGRectGetMaxX(expandedPositioningRect);
if (maxX > CGRectGetMaxX(bounds)) {
constrainedRect.origin.x = maxX - CGRectGetWidth(constrainedRect);
moveIntoView = YES;
}
}
if (CGRectGetMinY(calloutPositioningRect) < CGRectGetMinY(bounds))
{
constrainedRect.origin.y = expandedPositioningRect.origin.y;
CGFloat minY = CGRectGetMinY(expandedPositioningRect);
if (minY < CGRectGetMinY(bounds)) {
constrainedRect.origin.y = minY;
moveIntoView = YES;
}
else if (CGRectGetMaxY(calloutPositioningRect) > CGRectGetMaxY(bounds))
{
constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height;
moveIntoView = YES;
else {
CGFloat maxY = CGRectGetMaxY(expandedPositioningRect);
if (maxY > CGRectGetMaxY(bounds)) {
constrainedRect.origin.y = maxY - CGRectGetHeight(constrainedRect);
moveIntoView = YES;
}
}
}
@@ -4760,6 +4783,17 @@ public:
constrainedToRect:constrainedRect
animated:animateSelection];
// Save the anchor coordinate
if ([annotation isKindOfClass:[MGLPointAnnotation class]]) {
self.anchorCoordinateForSelectedAnnotation = annotation.coordinate;
}
else {
// This is used for features like polygons, so that if the map is dragged
// the callout doesn't ping to its coordinate.
CGPoint anchorPoint = CGPointMake(CGRectGetMidX(calloutPositioningRect), CGRectGetMidY(calloutPositioningRect));
self.anchorCoordinateForSelectedAnnotation = [self convertPoint:anchorPoint toCoordinateFromView:self];
}
// notify delegate
if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)])
{
@@ -4824,8 +4858,18 @@ public:
return CGRectNull;
}
CLLocationCoordinate2D coordinate;
if ((annotation == self.selectedAnnotation) &&
CLLocationCoordinate2DIsValid(self.anchorCoordinateForSelectedAnnotation)) {
coordinate = self.anchorCoordinateForSelectedAnnotation;
}
else {
coordinate = annotation.coordinate;
}
if ([annotation isKindOfClass:[MGLMultiPoint class]]) {
CLLocationCoordinate2D origin = annotation.coordinate;
CLLocationCoordinate2D origin = coordinate;
CGPoint originPoint = [self convertCoordinate:origin toPointToView:self];
return CGRectMake(originPoint.x, originPoint.y, MGLAnnotationImagePaddingForHitTest, MGLAnnotationImagePaddingForHitTest);
}
@@ -4840,7 +4884,7 @@ public:
return CGRectZero;
}
CGRect positioningRect = [self frameOfImage:image centeredAtCoordinate:annotation.coordinate];
CGRect positioningRect = [self frameOfImage:image centeredAtCoordinate:coordinate];
positioningRect.origin.x -= 0.5;
return CGRectInset(positioningRect, -MGLAnnotationImagePaddingForCallout,
@@ -4895,6 +4939,7 @@ public:
// clean up
self.calloutViewForSelectedAnnotation = nil;
self.selectedAnnotation = nil;
self.anchorCoordinateForSelectedAnnotation = kCLLocationCoordinate2DInvalid;
// notify delegate
if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)])
@@ -6158,7 +6203,9 @@ public:
annotationView = self.userLocationAnnotationView;
}
CGRect positioningRect = annotationView ? annotationView.frame : [self positioningRectForCalloutForAnnotationWithTag:tag];
CGRect positioningRect = annotationView ?
annotationView.frame :
[self positioningRectForCalloutForAnnotationWithTag:tag];
MGLAssert( ! CGRectIsNull(positioningRect), @"Positioning rect should not be CGRectNull by this point");

View File

@@ -269,6 +269,8 @@ NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0;
- (UIEdgeInsets)marginInsetsHintForPresentationFromRect:(CGRect)rect {
const CGFloat defaultMargin = 20.0f;
// form our subviews based on our content set so far
[self rebuildSubviews];
@@ -281,16 +283,16 @@ NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0;
CGFloat horizontalMargin = fmaxf(0, ceilf((CALLOUT_MIN_WIDTH-rect.size.width)/2));
UIEdgeInsets insets = {
.top = 0.0f,
.right = -horizontalMargin,
.top = 0.0f,
.right = -defaultMargin - horizontalMargin,
.bottom = 0.0f,
.left = -horizontalMargin
.left = -defaultMargin - horizontalMargin
};
if (self.permittedArrowDirection == MGLSMCalloutArrowDirectionUp)
insets.bottom -= size.height;
insets.bottom -= (defaultMargin + size.height);
else
insets.top -= size.height;
insets.top -= (defaultMargin + size.height);
return insets;
}

View File

@@ -6,7 +6,7 @@
* Fixed a crash when casting large numbers in `NSExpression`. ([#13580](https://github.com/mapbox/mapbox-gl-native/pull/13580))
* Added the `-[MGLShapeSource leavesOfCluster:offset:limit:]`, `-[MGLShapeSource childrenOfCluster:]`, `-[MGLShapeSource zoomLevelForExpandingCluster:]` methods for inspecting a cluster in an `MGLShapeSource`s created with the `MGLShapeSourceOptionClustered` option. Feature querying now returns clusters represented by `MGLPointFeatureCluster` objects (that conform to the `MGLCluster` protocol). ([#12952](https://github.com/mapbox/mapbox-gl-native/pull/12952)
* Fixed a bug with `MGLMapView.visibleAnnotations` that resulted in incorrect results and performance degradation. ([#13745](https://github.com/mapbox/mapbox-gl-native/pull/13745))
* Fixed a bug where selecting partially on-screen annotations (without a callout) would move the map. ([#13727](https://github.com/mapbox/mapbox-gl-native/pull/13727))
## 0.13.0 - December 20, 2018

View File

@@ -2281,7 +2281,7 @@ public:
- (BOOL)isMovingAnnotationIntoViewSupportedForAnnotation:(id<MGLAnnotation>)annotation animated:(BOOL)animated {
// Consider delegating
return animated && [annotation isKindOfClass:[MGLPointAnnotation class]];
return [annotation isKindOfClass:[MGLPointAnnotation class]];
}
- (void)selectAnnotation:(id <MGLAnnotation>)annotation
@@ -2326,22 +2326,28 @@ public:
positioningRect.origin = [self convertCoordinate:origin toPointToView:self];
}
if (!moveIntoView && NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) {
if (!NSEqualPoints(gesturePoint, NSZeroPoint)) {
BOOL shouldShowCallout = ([annotation respondsToSelector:@selector(title)]
&& annotation.title
&& !self.calloutForSelectedAnnotation.shown
&& [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)]
&& [self.delegate mapView:self annotationCanShowCallout:annotation]);
if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) {
if (!moveIntoView && !NSEqualPoints(gesturePoint, NSZeroPoint)) {
positioningRect = CGRectMake(gesturePoint.x, gesturePoint.y, positioningRect.size.width, positioningRect.size.height);
}
}
// Onscreen or partially on-screen
else if (!shouldShowCallout) {
moveIntoView = NO;
}
self.selectedAnnotation = annotation;
// For the callout to be shown, the annotation must have a title, its
// callout must not already be shown, and the annotation must be able to
// show a callout according to the delegate.
if ([annotation respondsToSelector:@selector(title)]
&& annotation.title
&& !self.calloutForSelectedAnnotation.shown
&& [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)]
&& [self.delegate mapView:self annotationCanShowCallout:annotation]) {
if (shouldShowCallout) {
NSPopover *callout = [self calloutForAnnotation:annotation];
// Hang the callout off the right edge of the annotation image’s
@@ -2365,39 +2371,52 @@ public:
NSRect (^edgeInsetsInsetRect)(NSRect, NSEdgeInsets) = ^(NSRect rect, NSEdgeInsets insets) {
return NSMakeRect(rect.origin.x + insets.left,
rect.origin.y + insets.top,
rect.origin.y + insets.bottom,
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);
NSRect expandedPositioningRect = positioningRect;
if (shouldShowCallout) {
// If we have a callout, expand this rect to include a buffer
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;
CGFloat minX = CGRectGetMinX(expandedPositioningRect);
if (minX < CGRectGetMinX(bounds)) {
constrainedRect.origin.x = minX;
moveIntoView = YES;
}
else if (CGRectGetMaxX(positioningRect) > CGRectGetMaxX(bounds))
{
constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width;
else {
CGFloat maxX = CGRectGetMaxX(expandedPositioningRect);
if (maxX > CGRectGetMaxX(bounds)) {
constrainedRect.origin.x = maxX - CGRectGetWidth(constrainedRect);
moveIntoView = YES;
}
}
CGFloat minY = CGRectGetMinY(expandedPositioningRect);
if (minY < CGRectGetMinY(bounds)) {
constrainedRect.origin.y = minY;
moveIntoView = YES;
}
if (CGRectGetMinY(positioningRect) < CGRectGetMinY(bounds))
{
constrainedRect.origin.y = expandedPositioningRect.origin.y;
moveIntoView = YES;
}
else if (CGRectGetMaxY(positioningRect) > CGRectGetMaxY(bounds))
{
constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height;
moveIntoView = YES;
else {
CGFloat maxY = CGRectGetMaxY(expandedPositioningRect);
if (maxY > CGRectGetMaxY(bounds)) {
constrainedRect.origin.y = maxY - CGRectGetHeight(constrainedRect);
moveIntoView = YES;
}
}
if (moveIntoView)

View File

@@ -11,7 +11,7 @@
- (void)setUp
{
[super setUp];
_mapView = [[MGLMapView alloc] initWithFrame:CGRectMake(0, 0, 64, 64)];
_mapView = [[MGLMapView alloc] initWithFrame:CGRectMake(0, 0, 256, 256)];
_mapView.delegate = self;
}