855 lines
35 KiB
Plaintext
855 lines
35 KiB
Plaintext
#import "MLNCompactCalloutView.h"
|
|
#import "MLNMapAccessibilityElement.h"
|
|
#import "MLNMapViewIntegrationTest.h"
|
|
#import "MLNTestLocationManager.h"
|
|
#import "MLNTestUtility.h"
|
|
|
|
#import "MLNGeometry_Private.h"
|
|
#import "MLNMapView_Private.h"
|
|
|
|
#include <mbgl/map/camera.hpp>
|
|
#include <mbgl/map/map.hpp>
|
|
#include <mbgl/util/geo.hpp>
|
|
|
|
@interface MLNTestCalloutView : MLNCompactCalloutView
|
|
@property (nonatomic) BOOL implementsMarginHints;
|
|
@end
|
|
|
|
@implementation MLNTestCalloutView
|
|
- (BOOL)respondsToSelector:(SEL)aSelector {
|
|
if (!self.implementsMarginHints &&
|
|
(aSelector == @selector(marginInsetsHintForPresentationFromRect:))) {
|
|
return NO;
|
|
}
|
|
return [super respondsToSelector:aSelector];
|
|
}
|
|
@end
|
|
|
|
@interface MLNMapView (Tests)
|
|
- (MLNAnnotationTag)annotationTagAtPoint:(CGPoint)point persistingResults:(BOOL)persist;
|
|
- (id<MLNAnnotation>)annotationWithTag:(MLNAnnotationTag)tag;
|
|
- (MLNMapCamera *)cameraByRotatingToDirection:(CLLocationDirection)degrees
|
|
aroundAnchorPoint:(CGPoint)anchorPoint;
|
|
- (MLNMapCamera *)cameraByZoomingToZoomLevel:(double)zoom aroundAnchorPoint:(CGPoint)anchorPoint;
|
|
- (MLNMapCamera *)cameraForCameraOptions:(const mbgl::CameraOptions &)cameraOptions;
|
|
@property (nonatomic) UIView<MLNCalloutView> *calloutViewForSelectedAnnotation;
|
|
@end
|
|
|
|
@interface MLNAnnotationViewIntegrationTests : MLNMapViewIntegrationTest
|
|
@end
|
|
|
|
@implementation MLNAnnotationViewIntegrationTests
|
|
|
|
// 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 CGPoint kAnnotationRelativeScale = {0.05f, 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 MLNTestAnnotationReuseIdentifer = @"MLNTestAnnotationReuseIdentifer";
|
|
CGSize size = self.mapView.bounds.size;
|
|
CGSize annotationSize = CGSizeMake(floor(size.width * kAnnotationRelativeScale.x),
|
|
floor(size.height * kAnnotationRelativeScale.y));
|
|
|
|
self.viewForAnnotation = ^MLNAnnotationView *(MLNMapView *view, id<MLNAnnotation> annotation) {
|
|
if (![annotation isKindOfClass:[MLNPointAnnotation class]]) {
|
|
return nil;
|
|
}
|
|
|
|
// No dequeue
|
|
MLNAnnotationView *annotationView =
|
|
[[MLNAnnotationView alloc] initWithAnnotation:annotation
|
|
reuseIdentifier:MLNTestAnnotationReuseIdentifer];
|
|
annotationView.bounds = (CGRect){.origin = CGPointZero, .size = annotationSize};
|
|
annotationView.backgroundColor = UIColor.redColor;
|
|
annotationView.enabled = YES;
|
|
|
|
return annotationView;
|
|
};
|
|
|
|
// Coordinate for annotation screen coordinate
|
|
CGPoint annotationPoint = CGPointMake(floor(relativeCoordinate.x * size.width),
|
|
floor(relativeCoordinate.y * size.height));
|
|
CLLocationCoordinate2D coordinate = [self.mapView convertPoint:annotationPoint
|
|
toCoordinateFromView:self.mapView];
|
|
|
|
MLNPointAnnotation *point = [[MLNPointAnnotation alloc] init];
|
|
point.title = title;
|
|
point.coordinate = coordinate;
|
|
|
|
self.mapViewAnnotationCanShowCalloutForAnnotation =
|
|
^BOOL(MLNMapView *mapView, id<MLNAnnotation> annotation) {
|
|
return showsCallout;
|
|
};
|
|
|
|
self.mapViewCalloutViewForAnnotation =
|
|
^id<MLNCalloutView>(MLNMapView *mapView, id<MLNAnnotation> annotation) {
|
|
if (!showsCallout) return nil;
|
|
|
|
MLNTestCalloutView *calloutView = [[MLNTestCalloutView 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
|
|
|
|
XCTestExpectation *selectionCompleted = [self expectationWithDescription:@"Selection completed"];
|
|
[self.mapView selectAnnotation:point
|
|
moveIntoView:moveIntoView
|
|
animateSelection:animateSelection
|
|
completionHandler:^{
|
|
[selectionCompleted fulfill];
|
|
}];
|
|
|
|
// Animated selection takes MLNAnimationDuration (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)
|
|
|
|
#if TARGET_OS_SIMULATOR
|
|
// Something is amiss with dispatch_async & XCTest on simulators in a recent
|
|
// iOS SDK release.
|
|
// TODO: Add issue
|
|
[self waitForExpectations:@[ selectionCompleted ] timeout:animateSelection ? 0.4 : 0.1];
|
|
#else
|
|
[self waitForExpectations:@[ selectionCompleted ] timeout:animateSelection ? 0.4 : 0.05];
|
|
#endif
|
|
|
|
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 (BOOL)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);
|
|
|
|
// This can fail if the callout view's width is < the annotations. This is really a warning,
|
|
// so if you need this NOT to fail the tests, consider replacing with MLNTestWarning
|
|
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 MLNTestWarning
|
|
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 |
|
|
//
|
|
|
|
CGFloat offset = kAnnotationRelativeScale.x * 0.5f;
|
|
|
|
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
|
|
{{offset, 0.5f}, YES, YES, YES, YES, YES},
|
|
{{(CGFloat)(1.0 - offset), 0.5f}, YES, YES, YES, YES, 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];
|
|
}
|
|
|
|
// MARK: - Selection with an offset
|
|
|
|
- (void)testSelectingAnnotationWithCenterOffset {
|
|
for (CGFloat dx = -100.0; dx <= 100.0; dx += 100.0) {
|
|
for (CGFloat dy = -100.0; dy <= 100.0; dy += 100.0) {
|
|
CGVector offset = CGVectorMake(dx, dy);
|
|
UIEdgeInsets edgeInsets =
|
|
UIEdgeInsetsMake(fmax(-dy, 0.0), fmax(-dy, 0.0), fmax(dy, 0.0), fmax(dx, 0.0));
|
|
[self internalTestSelectingAnnotationWithCenterOffsetWithOffset:offset edgeInsets:edgeInsets];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)internalTestSelectingAnnotationWithCenterOffsetWithOffset:(CGVector)offset
|
|
edgeInsets:(UIEdgeInsets)edgeInsets {
|
|
NSString *const MLNTestAnnotationReuseIdentifer = @"MLNTestAnnotationReuseIdentifer";
|
|
|
|
self.mapView.contentInset = edgeInsets;
|
|
CGSize size = self.mapView.bounds.size;
|
|
|
|
CGSize annotationSize = CGSizeMake(40.0, 40.0);
|
|
|
|
self.viewForAnnotation = ^MLNAnnotationView *(MLNMapView *view, id<MLNAnnotation> annotation) {
|
|
if (![annotation isKindOfClass:[MLNPointAnnotation class]]) {
|
|
return nil;
|
|
}
|
|
|
|
// No dequeue
|
|
MLNAnnotationView *annotationView =
|
|
[[MLNAnnotationView alloc] initWithAnnotation:annotation
|
|
reuseIdentifier:MLNTestAnnotationReuseIdentifer];
|
|
annotationView.bounds = (CGRect){.origin = CGPointZero, .size = annotationSize};
|
|
annotationView.backgroundColor = UIColor.redColor;
|
|
annotationView.enabled = YES;
|
|
annotationView.centerOffset = offset;
|
|
|
|
return annotationView;
|
|
};
|
|
|
|
MLNPointAnnotation *point = [[MLNPointAnnotation alloc] init];
|
|
point.title = NSStringFromSelector(_cmd);
|
|
point.coordinate = CLLocationCoordinate2DMake(0.0, 0.0);
|
|
[self.mapView addAnnotation:point];
|
|
|
|
// From https://github.com/mapbox/mapbox-gl-native/issues/12259#issuecomment-401414168
|
|
//
|
|
// queryRenderedFeatures depends on collision detection having been run
|
|
// before it shows results [...]. Collision detection runs asynchronously
|
|
// (at least every 300ms, sometimes more often), and therefore the results
|
|
// of queryRenderedFeatures are similarly asynchronous.
|
|
//
|
|
// So, we need to wait before `annotationTagAtPoint:persistingResults:` will
|
|
// return out newly added annotation
|
|
|
|
[self waitForCollisionDetectionToRun];
|
|
|
|
// 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 - edgeInsets.right + edgeInsets.left) / 2.0, 0.25);
|
|
XCTAssertEqualWithAccuracy(annotationPoint.y,
|
|
(size.height - edgeInsets.bottom + edgeInsets.top) / 2.0, 0.25);
|
|
|
|
// Now test taps around the annotation
|
|
CGPoint tapPoint = CGPointMake(annotationPoint.x + offset.dx, annotationPoint.y + offset.dy);
|
|
|
|
MLNAnnotationTag tagAtPoint = [self.mapView annotationTagAtPoint:tapPoint persistingResults:YES];
|
|
XCTAssert(tagAtPoint != UINT32_MAX, @"Should have tapped on annotation");
|
|
|
|
CGPoint testPoints[] = {{tapPoint.x - annotationSize.width, tapPoint.y},
|
|
{tapPoint.x + annotationSize.width, tapPoint.y},
|
|
{tapPoint.x, tapPoint.y - annotationSize.height},
|
|
{tapPoint.x, tapPoint.y + annotationSize.height},
|
|
CGPointZero};
|
|
|
|
CGPoint *testPoint = testPoints;
|
|
|
|
while (!CGPointEqualToPoint(*testPoint, CGPointZero)) {
|
|
tagAtPoint = [self.mapView annotationTagAtPoint:*testPoints persistingResults:YES];
|
|
XCTAssert(tagAtPoint == UINT32_MAX, @"Tap should to the side of the annotation");
|
|
testPoint++;
|
|
}
|
|
}
|
|
|
|
- (void)testUserLocationWithOffsetAnchorPoint {
|
|
[self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(37.787357, -122.39899)];
|
|
MLNTestLocationManager *locationManager = [[MLNTestLocationManager alloc] init];
|
|
self.mapView.locationManager = locationManager;
|
|
|
|
[self.mapView setUserTrackingMode:MLNUserTrackingModeFollow animated:NO completionHandler:nil];
|
|
CGRect originalFrame = [self.mapView viewForAnnotation:self.mapView.userLocation].frame;
|
|
|
|
// Temporarily disable location tracking so we can save the value of
|
|
// the originalFrame in memory
|
|
[self.mapView setUserTrackingMode:MLNUserTrackingModeNone animated:NO completionHandler:nil];
|
|
|
|
CGPoint offset = CGPointMake(20, 20);
|
|
|
|
self.mapViewUserLocationAnchorPoint = ^CGPoint(MLNMapView *mapView) {
|
|
return offset;
|
|
;
|
|
};
|
|
|
|
[self.mapView setUserTrackingMode:MLNUserTrackingModeFollow animated:NO completionHandler:nil];
|
|
CGRect offsetFrame = [self.mapView viewForAnnotation:self.mapView.userLocation].frame;
|
|
|
|
XCTAssertEqual(originalFrame.origin.x + offset.x, offsetFrame.origin.x);
|
|
XCTAssertEqual(originalFrame.origin.y + offset.y, offsetFrame.origin.y);
|
|
}
|
|
|
|
// MARK: - Rotating/zooming
|
|
|
|
- (void)testSelectingAnnotationWhenMapIsRotated {
|
|
CLLocationCoordinate2D coordinates[] = {{40.0, 40.0}, {NAN, NAN}};
|
|
|
|
NSArray *annotations = [self internalAddAnnotationsAtCoordinates:coordinates];
|
|
MLNPointAnnotation *annotation = annotations.firstObject;
|
|
|
|
// Rotate
|
|
CLLocationDirection lastAngle = 0.0;
|
|
|
|
srand48(0);
|
|
for (NSInteger iter = 0; iter < 10; iter++) {
|
|
CLLocationDirection angle = (CLLocationDirection)((drand48() * 1080.0) - 540.0);
|
|
|
|
CGPoint anchor = CGPointMake(drand48() * CGRectGetWidth(self.mapView.bounds),
|
|
drand48() * CGRectGetHeight(self.mapView.bounds));
|
|
|
|
NSString *activityTitle =
|
|
[NSString stringWithFormat:@"Rotate to: %0.1f from: %0.1f", angle, lastAngle];
|
|
[XCTContext runActivityNamed:activityTitle
|
|
block:^(id<XCTActivity> _Nonnull activity) {
|
|
MLNMapCamera *toCamera =
|
|
[self.mapView cameraByRotatingToDirection:angle
|
|
aroundAnchorPoint:anchor];
|
|
[self internalTestSelecting:annotation withCamera:toCamera];
|
|
}];
|
|
|
|
lastAngle = angle;
|
|
}
|
|
}
|
|
|
|
- (void)testSelectingAnnotationWhenMapIsScaled {
|
|
CLLocationCoordinate2D coordinates[] = {{0.005, 0.005}, {NAN, NAN}};
|
|
|
|
NSArray *annotations = [self internalAddAnnotationsAtCoordinates:coordinates];
|
|
MLNPointAnnotation *annotation = annotations.firstObject;
|
|
|
|
CGPoint anchor =
|
|
CGPointMake(CGRectGetMidX(self.mapView.bounds), CGRectGetMidY(self.mapView.bounds));
|
|
|
|
srand48(0);
|
|
for (NSInteger iter = 0; iter < 10; iter++) {
|
|
double zoom = (double)(drand48() * 14.0);
|
|
|
|
NSString *activityTitle = [NSString stringWithFormat:@"Zoom to %0.1f", zoom];
|
|
[XCTContext runActivityNamed:activityTitle
|
|
block:^(id<XCTActivity> _Nonnull activity) {
|
|
MLNMapCamera *toCamera =
|
|
[self.mapView cameraByZoomingToZoomLevel:zoom
|
|
aroundAnchorPoint:anchor];
|
|
[self internalTestSelecting:annotation withCamera:toCamera];
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)testSelectingAnnotationWhenMapIsScaledAndRotated {
|
|
CLLocationCoordinate2D coordinates[] = {{0.005, 0.005}, {NAN, NAN}};
|
|
|
|
NSArray *annotations = [self internalAddAnnotationsAtCoordinates:coordinates];
|
|
MLNPointAnnotation *annotation = annotations.firstObject;
|
|
|
|
srand48(0);
|
|
for (NSInteger iter = 0; iter < 10; iter++) {
|
|
double zoom = (double)(7.0 + drand48() * 7.0);
|
|
CLLocationDirection angle = (CLLocationDirection)((drand48() * 1080.0) - 540.0);
|
|
|
|
CGPoint anchor = CGPointMake(drand48() * CGRectGetWidth(self.mapView.bounds),
|
|
drand48() * CGRectGetHeight(self.mapView.bounds));
|
|
|
|
NSString *activityTitle = [NSString stringWithFormat:@"Zoom to %0.1f", zoom];
|
|
[XCTContext runActivityNamed:activityTitle
|
|
block:^(id<XCTActivity> _Nonnull activity) {
|
|
mbgl::CameraOptions currentCameraOptions;
|
|
|
|
currentCameraOptions.bearing = angle;
|
|
currentCameraOptions.anchor =
|
|
mbgl::ScreenCoordinate{anchor.x, anchor.y};
|
|
currentCameraOptions.zoom = zoom;
|
|
MLNMapCamera *toCamera =
|
|
[self.mapView cameraForCameraOptions:currentCameraOptions];
|
|
|
|
[self internalTestSelecting:annotation withCamera:toCamera];
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)testShowingAnnotationsThenSelectingAnimated {
|
|
[self internalTestShowingAnnotationsThenSelectingAnimated:YES];
|
|
}
|
|
|
|
- (void)testShowingAnnotationsThenSelecting {
|
|
[self internalTestShowingAnnotationsThenSelectingAnimated:NO];
|
|
}
|
|
|
|
- (void)internalTestShowingAnnotationsThenSelectingAnimated:(BOOL)animated {
|
|
srand48(0);
|
|
|
|
CGFloat maxXPadding = std::max(CGRectGetWidth(self.mapView.bounds) / 5.0, 100.0);
|
|
CGFloat maxYPadding = std::max(CGRectGetHeight(self.mapView.bounds) / 5.0, 100.0);
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
UIEdgeInsets edgePadding;
|
|
edgePadding.top = floor(drand48() * maxYPadding);
|
|
edgePadding.bottom = floor(drand48() * maxYPadding);
|
|
edgePadding.left = floor(drand48() * maxXPadding);
|
|
edgePadding.right = floor(drand48() * maxXPadding);
|
|
|
|
UIEdgeInsets contentInsets;
|
|
contentInsets.top = floor(drand48() * maxYPadding);
|
|
contentInsets.bottom = floor(drand48() * maxYPadding);
|
|
contentInsets.left = floor(drand48() * maxXPadding);
|
|
contentInsets.right = floor(drand48() * maxXPadding);
|
|
|
|
[self internalTestShowingAnnotationsThenSelectingAnimated:animated
|
|
edgePadding:edgePadding
|
|
contentInsets:contentInsets];
|
|
}
|
|
}
|
|
|
|
- (void)internalTestShowingAnnotationsThenSelectingAnimated:(BOOL)animated
|
|
edgePadding:(UIEdgeInsets)edgeInsets
|
|
contentInsets:(UIEdgeInsets)contentInsets {
|
|
CLLocationCoordinate2D coordinates[21];
|
|
|
|
for (int i = 0; i < (int)(sizeof(coordinates) / sizeof(coordinates[0])); i++) {
|
|
coordinates[i].latitude = drand48();
|
|
coordinates[i].longitude = drand48();
|
|
}
|
|
coordinates[20] = CLLocationCoordinate2DMake(NAN, NAN);
|
|
|
|
NSArray *annotations = [self internalAddAnnotationsAtCoordinates:coordinates];
|
|
|
|
XCTestExpectation *showCompleted = [self expectationWithDescription:@"showCompleted"];
|
|
|
|
self.mapView.contentInset = contentInsets;
|
|
[self.mapView showAnnotations:annotations
|
|
edgePadding:edgeInsets
|
|
animated:animated
|
|
completionHandler:^{
|
|
[showCompleted fulfill];
|
|
}];
|
|
|
|
[self waitForExpectations:@[ showCompleted ] timeout:3.5];
|
|
|
|
// These tests will fail if this isn't here. But this isn't quite what we're
|
|
// seeing in https://github.com/mapbox/mapbox-gl-native/issues/15106
|
|
[self waitForCollisionDetectionToRun];
|
|
|
|
for (MLNPointAnnotation *point in annotations) {
|
|
[self internalSelectDeselectAnnotation:point];
|
|
}
|
|
|
|
[self.mapView removeAnnotations:annotations];
|
|
self.mapView.contentInset = UIEdgeInsetsZero;
|
|
[self waitForCollisionDetectionToRun];
|
|
}
|
|
|
|
- (NSArray *)internalAddAnnotationsAtCoordinates:(CLLocationCoordinate2D *)coordinates {
|
|
__block NSMutableArray *annotations = [NSMutableArray array];
|
|
|
|
[XCTContext runActivityNamed:@"Map setup"
|
|
block:^(id<XCTActivity> _Nonnull activity) {
|
|
NSString *const MLNTestAnnotationReuseIdentifer =
|
|
@"MLNTestAnnotationReuseIdentifer";
|
|
|
|
CGSize annotationSize = CGSizeMake(40.0, 40.0);
|
|
|
|
self.viewForAnnotation = ^MLNAnnotationView *(
|
|
MLNMapView *view, id<MLNAnnotation> annotation2) {
|
|
if (![annotation2 isKindOfClass:[MLNPointAnnotation class]]) {
|
|
return nil;
|
|
}
|
|
|
|
// No dequeue
|
|
MLNAnnotationView *annotationView = [[MLNAnnotationView alloc]
|
|
initWithAnnotation:annotation2
|
|
reuseIdentifier:MLNTestAnnotationReuseIdentifer];
|
|
annotationView.bounds =
|
|
(CGRect){.origin = CGPointZero, .size = annotationSize};
|
|
annotationView.backgroundColor = UIColor.redColor;
|
|
annotationView.enabled = YES;
|
|
|
|
return annotationView;
|
|
};
|
|
|
|
CLLocationCoordinate2D *coordinatePtr = coordinates;
|
|
while (!isnan(coordinatePtr->latitude)) {
|
|
CLLocationCoordinate2D coordinate = *coordinatePtr++;
|
|
|
|
MLNPointAnnotation *annotation = [[MLNPointAnnotation alloc] init];
|
|
annotation.title = NSStringFromSelector(_cmd);
|
|
annotation.coordinate = coordinate;
|
|
[annotations addObject:annotation];
|
|
}
|
|
|
|
[self.mapView addAnnotations:annotations];
|
|
}];
|
|
|
|
NSArray *copiedAnnotations = [annotations copy];
|
|
annotations = nil;
|
|
|
|
return copiedAnnotations;
|
|
}
|
|
|
|
- (void)internalTestSelecting:(MLNPointAnnotation *)point withCamera:(MLNMapCamera *)camera {
|
|
// Rotate
|
|
XCTestExpectation *rotationCompleted = [self expectationWithDescription:@"rotationCompleted"];
|
|
[self.mapView setCamera:camera
|
|
withDuration:0.1
|
|
animationTimingFunction:nil
|
|
completionHandler:^{
|
|
[rotationCompleted fulfill];
|
|
}];
|
|
|
|
[self waitForExpectations:@[ rotationCompleted ] timeout:1.5];
|
|
|
|
// Collision detection may not have completed, if not we may not get our annotation.
|
|
[self waitForCollisionDetectionToRun];
|
|
|
|
// Look up annotation at point
|
|
[self internalSelectDeselectAnnotation:point];
|
|
}
|
|
|
|
- (void)internalSelectDeselectAnnotation:(MLNPointAnnotation *)point {
|
|
[XCTContext
|
|
runActivityNamed:[NSString stringWithFormat:@"Select annotation: %@", point]
|
|
block:^(id<XCTActivity> _Nonnull activity) {
|
|
CGPoint annotationPoint = [self.mapView convertCoordinate:point.coordinate
|
|
toPointToView:self.mapView];
|
|
|
|
MLNAnnotationTag tagAtPoint = [self.mapView annotationTagAtPoint:annotationPoint
|
|
persistingResults:YES];
|
|
if (tagAtPoint != UINT32_MAX) {
|
|
id<MLNAnnotation> annotation = [self.mapView annotationWithTag:tagAtPoint];
|
|
XCTAssertNotNil(annotation);
|
|
|
|
// Select
|
|
XCTestExpectation *selectionCompleted =
|
|
[self expectationWithDescription:@"Selection completed"];
|
|
[self.mapView selectAnnotation:annotation
|
|
moveIntoView:NO
|
|
animateSelection:NO
|
|
completionHandler:^{
|
|
[selectionCompleted fulfill];
|
|
}];
|
|
|
|
[self waitForExpectations:@[ selectionCompleted ] timeout:0.05];
|
|
|
|
XCTAssert(self.mapView.selectedAnnotations.count == 1,
|
|
@"There should only be 1 selected annotation");
|
|
XCTAssertEqualObjects(self.mapView.selectedAnnotations.firstObject, annotation,
|
|
@"The annotation should be selected");
|
|
|
|
// Deselect
|
|
[self.mapView deselectAnnotation:annotation animated:NO];
|
|
} else {
|
|
XCTFail(@"Should be an annotation at this point: %@",
|
|
NSStringFromCGPoint(annotationPoint));
|
|
}
|
|
}];
|
|
}
|
|
|
|
// 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");
|
|
|
|
[self.mapView setNeedsRerender];
|
|
self.renderFinishedExpectation = [self expectationWithDescription:@"Map view should be rendered"];
|
|
XCTestExpectation *timerExpired = [self expectationWithDescription:@"Timer expires"];
|
|
|
|
// Wait 1/2 second
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC >> 1)),
|
|
dispatch_get_main_queue(), ^{
|
|
[timerExpired fulfill];
|
|
});
|
|
|
|
[self waitForExpectations:@[ timerExpired, self.renderFinishedExpectation ] timeout:5];
|
|
|
|
self.renderFinishedExpectation = nil;
|
|
}
|
|
|
|
@end
|