diff options
Diffstat (limited to 'platform/ios/src/MGLMapView.mm')
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 1416 |
1 files changed, 977 insertions, 439 deletions
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 6c6d8d2980..8dec9e520d 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -6,7 +6,6 @@ #import <OpenGLES/EAGL.h> #include <mbgl/map/map.hpp> -#include <mbgl/map/view.hpp> #include <mbgl/annotation/annotation.hpp> #include <mbgl/map/camera.hpp> #include <mbgl/map/mode.hpp> @@ -19,8 +18,10 @@ #include <mbgl/style/image.hpp> #include <mbgl/style/transition_options.hpp> #include <mbgl/style/layers/custom_layer.hpp> -#include <mbgl/map/backend.hpp> -#include <mbgl/map/backend_scope.hpp> +#include <mbgl/renderer/mode.hpp> +#include <mbgl/renderer/renderer.hpp> +#include <mbgl/renderer/renderer_backend.hpp> +#include <mbgl/renderer/backend_scope.hpp> #include <mbgl/math/wrap.hpp> #include <mbgl/util/exception.hpp> #include <mbgl/util/geo.hpp> @@ -35,11 +36,15 @@ #include <mbgl/util/projection.hpp> #import "Mapbox.h" +#import "MGLShape_Private.h" #import "MGLFeature_Private.h" #import "MGLGeometry_Private.h" #import "MGLMultiPoint_Private.h" #import "MGLOfflineStorage_Private.h" +#import "MGLVectorSource_Private.h" #import "MGLFoundation_Private.h" +#import "MGLRendererFrontend.h" +#import "MGLRendererConfiguration.h" #import "NSBundle+MGLAdditions.h" #import "NSDate+MGLAdditions.h" @@ -66,6 +71,7 @@ #import "MGLAnnotationContainerView.h" #import "MGLAnnotationContainerView_Private.h" #import "MGLAttributionInfo_Private.h" +#import "MGLMapAccessibilityElement.h" #include <algorithm> #include <cstdlib> @@ -85,6 +91,8 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { MGLUserTrackingStatePossible = 0, /// The map view has begun to move to the first reported user location. MGLUserTrackingStateBegan, + /// The map view begins a significant transition. + MGLUserTrackingStateBeginSignificantTransition, /// The map view has finished moving to the first reported user location. MGLUserTrackingStateChanged, }; @@ -113,6 +121,9 @@ const NSUInteger MGLTargetFrameInterval = 1; // Target FPS will be 60 divided b /// Tolerance for snapping to true north, measured in degrees in either direction. const CLLocationDirection MGLToleranceForSnappingToNorth = 7; +/// Distance threshold to stop the camera while animating. +const CLLocationDistance MGLDistanceThresholdForCameraPause = 500; + /// Reuse identifier and file name of the default point annotation image. static NSString * const MGLDefaultStyleMarkerSymbolName = @"default_marker"; @@ -132,12 +143,6 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1; const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10); -// Context for KVO observing UILayoutGuides. -static void * MGLLayoutGuidesUpdatedContext = &MGLLayoutGuidesUpdatedContext; - -/// Unique identifier representing a single annotation in mbgl. -typedef uint32_t MGLAnnotationTag; - /// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationTagNotFound = UINT32_MAX }; @@ -160,38 +165,6 @@ mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction return { p1[0], p1[1], p2[0], p2[1] }; } -@interface MGLAnnotationAccessibilityElement : UIAccessibilityElement - -@property (nonatomic) MGLAnnotationTag tag; - -- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)identifier NS_DESIGNATED_INITIALIZER; - -@end - -@implementation MGLAnnotationAccessibilityElement - -- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)tag -{ - if (self = [super initWithAccessibilityContainer:container]) - { - _tag = tag; - self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable; - } - return self; -} - -- (void)accessibilityIncrement -{ - [self.accessibilityContainer accessibilityIncrement]; -} - -- (void)accessibilityDecrement -{ - [self.accessibilityContainer accessibilityDecrement]; -} - -@end - /// Lightweight container for metadata about an annotation, including the annotation itself. class MGLAnnotationContext { public: @@ -203,32 +176,12 @@ public: NSString *viewReuseIdentifier; }; -/** An accessibility element representing the MGLMapView at large. */ -@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement - -@end - -@implementation MGLMapViewProxyAccessibilityElement - -- (instancetype)initWithAccessibilityContainer:(id)container -{ - if (self = [super initWithAccessibilityContainer:container]) - { - self.accessibilityTraits = UIAccessibilityTraitButton; - self.accessibilityLabel = [self.accessibilityContainer accessibilityLabel]; - self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"CLOSE_CALLOUT_A11Y_HINT", nil, nil, @"Returns to the map", @"Accessibility hint for closing the selected annotation’s callout view and returning to the map"); - } - return self; -} - -@end - #pragma mark - Private - @interface MGLMapView () <UIGestureRecognizerDelegate, GLKViewDelegate, CLLocationManagerDelegate, - SMCalloutViewDelegate, + MGLSMCalloutViewDelegate, MGLCalloutViewDelegate, MGLMultiPointDelegate, MGLAnnotationImageDelegate> @@ -236,11 +189,18 @@ public: @property (nonatomic) EAGLContext *context; @property (nonatomic) GLKView *glView; @property (nonatomic) UIImageView *glSnapshotView; + +@property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *scaleBarConstraints; @property (nonatomic, readwrite) MGLScaleBar *scaleBar; @property (nonatomic, readwrite) UIImageView *compassView; +@property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *compassViewConstraints; @property (nonatomic, readwrite) UIImageView *logoView; +@property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *logoViewConstraints; @property (nonatomic, readwrite) UIButton *attributionButton; +@property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *attributionButtonConstraints; + @property (nonatomic, readwrite) MGLStyle *style; + @property (nonatomic) UITapGestureRecognizer *singleTapGestureRecognizer; @property (nonatomic) UITapGestureRecognizer *doubleTap; @property (nonatomic) UITapGestureRecognizer *twoFingerTap; @@ -249,11 +209,14 @@ public: @property (nonatomic) UIRotationGestureRecognizer *rotate; @property (nonatomic) UILongPressGestureRecognizer *quickZoom; @property (nonatomic) UIPanGestureRecognizer *twoFingerDrag; + /// Mapping from reusable identifiers to annotation images. @property (nonatomic) NS_MUTABLE_DICTIONARY_OF(NSString *, MGLAnnotationImage *) *annotationImagesByIdentifier; + /// Currently shown popover representing the selected annotation. @property (nonatomic) UIView<MGLCalloutView> *calloutViewForSelectedAnnotation; @property (nonatomic) MGLUserLocationAnnotationView *userLocationAnnotationView; + /// Indicates how thoroughly the map view is tracking the user location. @property (nonatomic) MGLUserTrackingState userTrackingState; @property (nonatomic) CLLocationManager *locationManager; @@ -273,6 +236,8 @@ public: { mbgl::Map *_mbglMap; MBGLView *_mbglView; + std::unique_ptr<MGLRenderFrontend> _rendererFrontend; + std::shared_ptr<mbgl::ThreadPool> _mbglThreadPool; BOOL _opaque; @@ -294,8 +259,6 @@ public: NSDate *_userLocationAnimationCompletionDate; /// True if a willChange notification has been issued for shape annotation layers and a didChange notification is pending. BOOL _isChangingAnnotationLayers; - BOOL _isObservingTopLayoutGuide; - BOOL _isObservingBottomLayoutGuide; BOOL _isWaitingForRedundantReachableNotification; BOOL _isTargetingInterfaceBuilder; @@ -310,6 +273,8 @@ public: /// Center coordinate of the pinch gesture on the previous iteration of the gesture. CLLocationCoordinate2D _previousPinchCenterCoordinate; NSUInteger _previousPinchNumberOfTouches; + + CLLocationDistance _distanceFromOldUserLocation; BOOL _delegateHasAlphasForShapeAnnotations; BOOL _delegateHasStrokeColorsForShapeAnnotations; @@ -317,6 +282,10 @@ public: BOOL _delegateHasLineWidthsForShapeAnnotations; MGLCompassDirectionFormatter *_accessibilityCompassFormatter; + NS_ARRAY_OF(id <MGLFeature>) *_visiblePlaceFeatures; + NS_ARRAY_OF(id <MGLFeature>) *_visibleRoadFeatures; + NS_MUTABLE_SET_OF(MGLFeatureAccessibilityElement *) *_featureAccessibilityElements; + BOOL _accessibilityValueAnnouncementIsPending; MGLReachability *_reachability; } @@ -355,7 +324,7 @@ public: + (void)initialize { - if (self == [MGLMapView self]) + if (self == [MGLMapView class]) { [MGLSDKUpdateChecker checkForUpdates]; } @@ -403,6 +372,11 @@ public: return _mbglMap; } +- (mbgl::Renderer *)renderer +{ + return _rendererFrontend->getRenderer(); +} + - (void)commonInit { _isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent; @@ -421,10 +395,9 @@ public: self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction | UIAccessibilityTraitAdjustable; _accessibilityCompassFormatter = [[MGLCompassDirectionFormatter alloc] init]; _accessibilityCompassFormatter.unitStyle = NSFormattingUnitStyleLong; - self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = YES; - + if (@available(iOS 11.0, *)) { self.accessibilityIgnoresInvertColors = YES; } // setup mbgl view _mbglView = new MBGLView(self); @@ -434,11 +407,12 @@ public: [[NSFileManager defaultManager] removeItemAtPath:fileCachePath error:NULL]; // setup mbgl map - mbgl::DefaultFileSource *mbglFileSource = [MGLOfflineStorage sharedOfflineStorage].mbglFileSource; - const float scaleFactor = [UIScreen instancesRespondToSelector:@selector(nativeScale)] ? [[UIScreen mainScreen] nativeScale] : [[UIScreen mainScreen] scale]; + MGLRendererConfiguration *config = [MGLRendererConfiguration currentConfiguration]; _mbglThreadPool = mbgl::sharedThreadPool(); - _mbglMap = new mbgl::Map(*_mbglView, self.size, scaleFactor, *mbglFileSource, *_mbglThreadPool, mbgl::MapMode::Continuous, mbgl::GLContextMode::Unique, mbgl::ConstrainMode::None, mbgl::ViewportMode::Default); - [self validateTileCacheSize]; + + auto renderer = std::make_unique<mbgl::Renderer>(*_mbglView, config.scaleFactor, *config.fileSource, *_mbglThreadPool, config.contextMode, config.cacheDir, config.localFontFamilyName); + _rendererFrontend = std::make_unique<MGLRenderFrontend>(std::move(renderer), self, *_mbglView); + _mbglMap = new mbgl::Map(*_rendererFrontend, *_mbglView, self.size, config.scaleFactor, *[config fileSource], *_mbglThreadPool, mbgl::MapMode::Continuous, mbgl::ConstrainMode::None, mbgl::ViewportMode::Default); // start paused if in IB if (_isTargetingInterfaceBuilder || background) { @@ -466,23 +440,30 @@ public: _selectedAnnotationTag = MGLAnnotationTagNotFound; _annotationsNearbyLastTap = {}; - // setup logo bug + // setup logo // UIImage *logo = [MGLMapView resourceImageNamed:@"mapbox"]; _logoView = [[UIImageView alloc] initWithImage:logo]; _logoView.accessibilityTraits = UIAccessibilityTraitStaticText; _logoView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"LOGO_A11Y_LABEL", nil, nil, @"Mapbox", @"Accessibility label"); + _logoView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_logoView]; + _logoViewConstraints = [NSMutableArray array]; // setup attribution // _attributionButton = [UIButton buttonWithType:UIButtonTypeInfoLight]; _attributionButton.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_LABEL", nil, nil, @"About this map", @"Accessibility label"); _attributionButton.accessibilityHint = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_HINT", nil, nil, @"Shows credits, a feedback form, and more", @"Accessibility hint"); - [_attributionButton addTarget:self action:@selector(showAttribution) forControlEvents:UIControlEventTouchUpInside]; + [_attributionButton addTarget:self action:@selector(showAttribution:) forControlEvents:UIControlEventTouchUpInside]; + _attributionButton.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_attributionButton]; + _attributionButtonConstraints = [NSMutableArray array]; [_attributionButton addObserver:self forKeyPath:@"hidden" options:NSKeyValueObservingOptionNew context:NULL]; + UILongPressGestureRecognizer *attributionLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(showAttribution:)]; + [_attributionButton addGestureRecognizer:attributionLongPress]; + // setup compass // _compassView = [[UIImageView alloc] initWithImage:self.compassImage]; @@ -492,12 +473,16 @@ public: _compassView.accessibilityTraits = UIAccessibilityTraitButton; _compassView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_LABEL", nil, nil, @"Compass", @"Accessibility label"); _compassView.accessibilityHint = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_HINT", nil, nil, @"Rotates the map to face due north", @"Accessibility hint"); + _compassView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_compassView]; + _compassViewConstraints = [NSMutableArray array]; // setup scale control // _scaleBar = [[MGLScaleBar alloc] init]; + _scaleBar.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_scaleBar]; + _scaleBarConstraints = [NSMutableArray array]; // setup interaction // @@ -526,21 +511,21 @@ public: _singleTapGestureRecognizer.delegate = self; [self addGestureRecognizer:_singleTapGestureRecognizer]; - _twoFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerTapGesture:)]; - _twoFingerTap.numberOfTouchesRequired = 2; - [_twoFingerTap requireGestureRecognizerToFail:_pinch]; - [_twoFingerTap requireGestureRecognizerToFail:_rotate]; - [self addGestureRecognizer:_twoFingerTap]; - _twoFingerDrag = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerDragGesture:)]; _twoFingerDrag.minimumNumberOfTouches = 2; _twoFingerDrag.maximumNumberOfTouches = 2; _twoFingerDrag.delegate = self; - [_twoFingerDrag requireGestureRecognizerToFail:_twoFingerTap]; [_twoFingerDrag requireGestureRecognizerToFail:_pan]; [self addGestureRecognizer:_twoFingerDrag]; _pitchEnabled = YES; + _twoFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerTapGesture:)]; + _twoFingerTap.numberOfTouchesRequired = 2; + [_twoFingerTap requireGestureRecognizerToFail:_pinch]; + [_twoFingerTap requireGestureRecognizerToFail:_rotate]; + [_twoFingerTap requireGestureRecognizerToFail:_twoFingerDrag]; + [self addGestureRecognizer:_twoFingerTap]; + _decelerationRate = MGLMapViewDecelerationRateNormal; _quickZoom = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleQuickZoomGesture:)]; @@ -610,6 +595,7 @@ public: _glView.contentScaleFactor = [UIScreen instancesRespondToSelector:@selector(nativeScale)] ? [[UIScreen mainScreen] nativeScale] : [[UIScreen mainScreen] scale]; _glView.layer.opaque = _opaque; _glView.delegate = self; + [_glView bindDrawable]; [self insertSubview:_glView atIndex:0]; _glView.contentMode = UIViewContentModeCenter; @@ -657,48 +643,6 @@ public: _isWaitingForRedundantReachableNotification = NO; } -- (void)willMoveToWindow:(UIWindow *)newWindow -{ - [super willMoveToWindow:newWindow]; - - if (newWindow) { - [self addLayoutGuideObserversIfNeeded]; - } else { - [self removeLayoutGuideObserversIfNeeded]; - } -} - -- (void)addLayoutGuideObserversIfNeeded -{ - UIViewController *viewController = self.viewControllerForLayoutGuides; - BOOL useLayoutGuides = viewController.view && viewController.automaticallyAdjustsScrollViewInsets; - - if (useLayoutGuides && viewController.topLayoutGuide && !_isObservingTopLayoutGuide) { - [(NSObject *)viewController.topLayoutGuide addObserver:self forKeyPath:@"bounds" options:0 context:(void *)&MGLLayoutGuidesUpdatedContext]; - _isObservingTopLayoutGuide = YES; - } - - if (useLayoutGuides && viewController.bottomLayoutGuide && !_isObservingBottomLayoutGuide) { - [(NSObject *)viewController.bottomLayoutGuide addObserver:self forKeyPath:@"bounds" options:0 context:(void *)&MGLLayoutGuidesUpdatedContext]; - _isObservingBottomLayoutGuide = YES; - } -} - -- (void)removeLayoutGuideObserversIfNeeded -{ - UIViewController *viewController = self.viewControllerForLayoutGuides; - - if (_isObservingTopLayoutGuide) { - [(NSObject *)viewController.topLayoutGuide removeObserver:self forKeyPath:@"bounds" context:(void *)&MGLLayoutGuidesUpdatedContext]; - _isObservingTopLayoutGuide = NO; - } - - if (_isObservingBottomLayoutGuide) { - [(NSObject *)viewController.bottomLayoutGuide removeObserver:self forKeyPath:@"bounds" context:(void *)&MGLLayoutGuidesUpdatedContext]; - _isObservingBottomLayoutGuide = NO; - } -} - - (void)dealloc { [_reachability stopNotifier]; @@ -707,8 +651,6 @@ public: [[NSNotificationCenter defaultCenter] removeObserver:self]; [_attributionButton removeObserver:self forKeyPath:@"hidden"]; - [self removeLayoutGuideObserversIfNeeded]; - // Removing the annotations unregisters any outstanding KVO observers. NSArray *annotations = self.annotations; if (annotations) @@ -734,6 +676,18 @@ public: { [EAGLContext setCurrentContext:nil]; } + + [self.compassViewConstraints removeAllObjects]; + self.compassViewConstraints = nil; + + [self.scaleBarConstraints removeAllObjects]; + self.scaleBarConstraints = nil; + + [self.logoViewConstraints removeAllObjects]; + self.logoViewConstraints = nil; + + [self.attributionButtonConstraints removeAllObjects]; + self.attributionButtonConstraints = nil; } - (void)setDelegate:(nullable id<MGLMapViewDelegate>)delegate @@ -752,47 +706,11 @@ public: { MGLAssertIsMainThread(); - _mbglMap->onLowMemory(); + _rendererFrontend->onLowMemory(); } #pragma mark - Layout - -- (void)setFrame:(CGRect)frame -{ - [super setFrame:frame]; - if ( ! CGRectEqualToRect(frame, self.frame)) - { - [self validateTileCacheSize]; - } -} - -- (void)setBounds:(CGRect)bounds -{ - [super setBounds:bounds]; - if ( ! CGRectEqualToRect(bounds, self.bounds)) - { - [self validateTileCacheSize]; - } -} - -- (void)validateTileCacheSize -{ - if ( ! _mbglMap) - { - return; - } - - CGFloat zoomFactor = self.maximumZoomLevel - self.minimumZoomLevel + 1; - CGFloat cpuFactor = [NSProcessInfo processInfo].processorCount; - CGFloat memoryFactor = (CGFloat)[NSProcessInfo processInfo].physicalMemory / 1000 / 1000 / 1000; - CGFloat sizeFactor = (CGRectGetWidth(self.bounds) / mbgl::util::tileSize) * - (CGRectGetHeight(self.bounds) / mbgl::util::tileSize); - - NSUInteger cacheSize = zoomFactor * cpuFactor * memoryFactor * sizeFactor * 0.5; - - _mbglMap->setSourceTileCacheSize(cacheSize); -} - + (BOOL)requiresConstraintBasedLayout { return YES; @@ -816,15 +734,207 @@ public: return nil; } +- (void)updateConstraintsPreiOS11 { + // If we have a view controller reference and its automaticallyAdjustsScrollViewInsets + // is set to YES, use its view as the parent for constraints. -[MGLMapView adjustContentInset] + // already take top and bottom layout guides into account. If we don't have a reference, apply + // constraints against ourself to maintain placement of the subviews. + // + UIViewController *viewController = self.viewControllerForLayoutGuides; + BOOL useLayoutGuides = viewController.view && viewController.automaticallyAdjustsScrollViewInsets; + UIView *containerView = useLayoutGuides ? viewController.view : self; + + // compass view + // + [containerView removeConstraints:self.compassViewConstraints]; + [self.compassViewConstraints removeAllObjects]; + + if (useLayoutGuides) { + [self.compassViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.compassView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:viewController.topLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:5.0 + self.contentInset.top]]; + } + [self.compassViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.compassView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:5.0 + self.contentInset.top]]; + [self.compassViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.compassView + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:5.0 + self.contentInset.right]]; + + [containerView addConstraints:self.compassViewConstraints]; + + // scale bar view + // + [containerView removeConstraints:self.scaleBarConstraints]; + [self.scaleBarConstraints removeAllObjects]; + + if (useLayoutGuides) { + [self.scaleBarConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.scaleBar + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:viewController.topLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:5.0 + self.contentInset.top]]; + } + [self.scaleBarConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.scaleBar + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:5.0 + self.contentInset.top]]; + [self.scaleBarConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.scaleBar + attribute:NSLayoutAttributeLeft + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeLeft + multiplier:1.0 + constant:8.0 + self.contentInset.left]]; + + [containerView addConstraints:self.scaleBarConstraints]; + + // logo view + // + [containerView removeConstraints:self.logoViewConstraints]; + [self.logoViewConstraints removeAllObjects]; + + if (useLayoutGuides) { + [self.logoViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:viewController.bottomLayoutGuide + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self.logoView + attribute:NSLayoutAttributeBaseline + multiplier:1.0 + constant:8.0 + self.contentInset.bottom]]; + } + [self.logoViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self.logoView + attribute:NSLayoutAttributeBaseline + multiplier:1 + constant:8 + self.contentInset.bottom]]; + [self.logoViewConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.logoView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:8.0 + self.contentInset.left]]; + [containerView addConstraints:self.logoViewConstraints]; + + // attribution button + // + [containerView removeConstraints:self.attributionButtonConstraints]; + [self.attributionButtonConstraints removeAllObjects]; + + if (useLayoutGuides) { + [self.attributionButtonConstraints addObject: + [NSLayoutConstraint constraintWithItem:viewController.bottomLayoutGuide + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self.attributionButton + attribute:NSLayoutAttributeBaseline + multiplier:1 + constant:8 + self.contentInset.bottom]]; + } + [self.attributionButtonConstraints addObject: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self.attributionButton + attribute:NSLayoutAttributeBaseline + multiplier:1 + constant:8 + self.contentInset.bottom]]; + + [self.attributionButtonConstraints addObject: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.attributionButton + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:8 + self.contentInset.right]]; + [containerView addConstraints:self.attributionButtonConstraints]; +} + - (void)updateConstraints { - [super updateConstraints]; - // If we have a view controller reference and its automaticallyAdjustsScrollViewInsets - // is set to YES, -[MGLMapView adjustContentInset] takes top and bottom layout - // guides into account. To get notified about changes to the layout guides, - // we need to observe their bounds and re-layout accordingly. - [self addLayoutGuideObserversIfNeeded]; +// If compiling with the iOS 11+ SDK +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + // If safeAreaLayoutGuide API exists + if ( [self respondsToSelector:@selector(safeAreaLayoutGuide)] ) { + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + UILayoutGuide *safeAreaLayoutGuide = self.safeAreaLayoutGuide; +#pragma clang diagnostic pop + // compass view + [self removeConstraints:self.compassViewConstraints]; + [self.compassViewConstraints removeAllObjects]; + [self.compassViewConstraints addObject:[self.compassView.topAnchor constraintEqualToAnchor:safeAreaLayoutGuide.topAnchor + constant:5.0 + self.contentInset.top]]; + [self.compassViewConstraints addObject:[safeAreaLayoutGuide.rightAnchor constraintEqualToAnchor:self.compassView.rightAnchor + constant:8.0 + self.contentInset.right]]; + [self addConstraints:self.compassViewConstraints]; + + // scale bar view + [self removeConstraints:self.scaleBarConstraints]; + [self.scaleBarConstraints removeAllObjects]; + [self.scaleBarConstraints addObject:[self.scaleBar.topAnchor constraintEqualToAnchor:safeAreaLayoutGuide.topAnchor + constant:5.0 + self.contentInset.top]]; + [self.scaleBarConstraints addObject:[self.scaleBar.leftAnchor constraintEqualToAnchor:safeAreaLayoutGuide.leftAnchor + constant:8.0 + self.contentInset.left]]; + [self addConstraints:self.scaleBarConstraints]; + + // logo view + [self removeConstraints:self.logoViewConstraints]; + [self.logoViewConstraints removeAllObjects]; + [self.logoViewConstraints addObject:[safeAreaLayoutGuide.bottomAnchor constraintEqualToAnchor:self.logoView.bottomAnchor + constant:8.0 + self.contentInset.bottom]]; + [self.logoViewConstraints addObject:[self.logoView.leftAnchor constraintEqualToAnchor:safeAreaLayoutGuide.leftAnchor + constant:8.0 + self.contentInset.left]]; + [self addConstraints:self.logoViewConstraints]; + + // attribution button + [self removeConstraints:self.attributionButtonConstraints]; + [self.attributionButtonConstraints removeAllObjects]; + [self.attributionButtonConstraints addObject:[safeAreaLayoutGuide.bottomAnchor constraintEqualToAnchor:self.attributionButton.bottomAnchor + constant:8.0 + self.contentInset.bottom]]; + [self.attributionButtonConstraints addObject:[safeAreaLayoutGuide.rightAnchor constraintEqualToAnchor:self.attributionButton.rightAnchor + constant:8.0 + self.contentInset.right]]; + [self addConstraints:self.attributionButtonConstraints]; + } else { + [self updateConstraintsPreiOS11]; + } +#else + [self updateConstraintsPreiOS11]; +#endif + + [super updateConstraints]; } - (BOOL)isOpaque @@ -840,12 +950,9 @@ public: // This is the delegate of the GLKView object's display call. - (void)glkView:(__unused GLKView *)view drawInRect:(__unused CGRect)rect { - if ( ! self.dormant) + if ( ! self.dormant || ! _rendererFrontend) { - // The OpenGL implementation automatically enables the OpenGL context for us. - mbgl::BackendScope scope { *_mbglView, mbgl::BackendScope::ScopeType::Implicit }; - - _mbglMap->render(*_mbglView); + _rendererFrontend->render(); [self updateUserLocationAnnotationView]; } @@ -854,11 +961,17 @@ public: // This gets called when the view dimension changes, e.g. because the device is being rotated. - (void)layoutSubviews { + // Calling this here instead of in the scale bar itself because if this is done in the + // scale bar instance, it triggers a call to this this `layoutSubviews` method that + // calls `_mbglMap->setSize()` just below that triggers rendering update which triggers + // another scale bar update which causes a rendering update loop and a major performace + // degradation. The only time the scale bar's intrinsic content size _must_ invalidated + // is here as a reaction to this object's view dimension changes. + [self.scaleBar invalidateIntrinsicContentSize]; + [super layoutSubviews]; [self adjustContentInset]; - - [self layoutOrnaments]; if (!_isTargetingInterfaceBuilder) { _mbglMap->setSize([self size]); @@ -866,44 +979,15 @@ public: if (self.compassView.alpha) { - [self updateHeadingForDeviceOrientation]; [self updateCompass]; } - [self updateUserLocationAnnotationView]; -} + if (self.compassView.alpha || self.showsUserHeadingIndicator) + { + [self updateHeadingForDeviceOrientation]; + } -- (void)layoutOrnaments -{ - // scale bar - self.scaleBar.frame = { - self.contentInset.left+8, - self.contentInset.top+5, - CGRectGetWidth(self.scaleBar.frame), - CGRectGetHeight(self.scaleBar.frame) - }; - - // compass - self.compassView.center = { - .x = CGRectGetWidth(self.bounds)-CGRectGetMidX(self.compassView.bounds)-self.contentInset.right-5, - .y = CGRectGetMidY(self.compassView.bounds)+self.contentInset.top+5 - }; - - // logo bug - self.logoView.frame = { - self.contentInset.left+8, - CGRectGetHeight(self.bounds)-8-self.contentInset.bottom-CGRectGetHeight(self.logoView.bounds), - CGRectGetWidth(self.logoView.bounds), - CGRectGetHeight(self.logoView.bounds) - }; - - // attribution - self.attributionButton.frame = { - CGRectGetWidth(self.bounds)-CGRectGetWidth(self.attributionButton.bounds)-self.contentInset.right-8, - CGRectGetHeight(self.bounds)-CGRectGetHeight(self.attributionButton.bounds)-self.contentInset.bottom-8, - CGRectGetWidth(self.attributionButton.bounds), - CGRectGetHeight(self.attributionButton.bounds) - }; + [self updateUserLocationAnnotationView]; } /// Updates `contentInset` to reflect the current window geometry. @@ -975,7 +1059,7 @@ public: } // Compass, logo and attribution button constraints needs to be updated. - [self setNeedsLayout]; + [self setNeedsUpdateConstraints]; } /// Returns the frame of inset content within the map view. @@ -1138,8 +1222,10 @@ public: - (void)updateTintColorForView:(UIView *)view { - // stop at recursing container & annotation views (#8522) - if ([view isEqual:self.annotationContainerView]) return; + // Don't update: + // - annotation views + // - attribution button (handled automatically) + if ([view isEqual:self.annotationContainerView] || [view isEqual:self.attributionButton]) return; if ([view respondsToSelector:@selector(setTintColor:)]) view.tintColor = self.tintColor; @@ -1167,6 +1253,10 @@ public: { _changeDelimiterSuppressionDepth = 0; _mbglMap->setGestureInProgress(false); + if (self.userTrackingState == MGLUserTrackingStateBegan) + { + [self setUserTrackingMode:MGLUserTrackingModeNone animated:NO]; + } _mbglMap->cancelTransitions(); } @@ -1268,8 +1358,6 @@ public: { if ( ! self.isZoomEnabled) return; - if (_mbglMap->getZoom() <= _mbglMap->getMinZoom() && pinch.scale < 1) return; - _mbglMap->cancelTransitions(); CGPoint centerPoint = [self anchorPointForGesture:pinch]; @@ -1285,27 +1373,27 @@ public: } else if (pinch.state == UIGestureRecognizerStateChanged) { + // Zoom limiting happens at the core level. CGFloat newScale = self.scale * pinch.scale; - double zoom = log2(newScale); - if (zoom < _mbglMap->getMinZoom()) return; - + double newZoom = log2(newScale); + // Calculates the final camera zoom, has no effect within current map camera. - MGLMapCamera *toCamera = [self cameraByZoomingToZoomLevel:zoom aroundAnchorPoint:centerPoint]; + MGLMapCamera *toCamera = [self cameraByZoomingToZoomLevel:newZoom aroundAnchorPoint:centerPoint]; if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) { - _mbglMap->setZoom(zoom, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); - } - // The gesture recognizer only reports the gesture’s current center - // point, so use the previous center point to anchor the transition. - // If the number of touches has changed, the remembered center point is - // meaningless. - if (self.userTrackingMode == MGLUserTrackingModeNone && pinch.numberOfTouches == _previousPinchNumberOfTouches) - { - CLLocationCoordinate2D centerCoordinate = _previousPinchCenterCoordinate; - _mbglMap->setLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate), - mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + _mbglMap->setZoom(newZoom, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + // The gesture recognizer only reports the gesture’s current center + // point, so use the previous center point to anchor the transition. + // If the number of touches has changed, the remembered center point is + // meaningless. + if (self.userTrackingMode == MGLUserTrackingModeNone && pinch.numberOfTouches == _previousPinchNumberOfTouches) + { + CLLocationCoordinate2D centerCoordinate = _previousPinchCenterCoordinate; + _mbglMap->setLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate), + mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + } } [self cameraIsChanging]; } @@ -1473,7 +1561,9 @@ public: id<MGLAnnotation>annotation = [self annotationForGestureRecognizer:singleTap persistingResults:YES]; if(annotation) { - [self selectAnnotation:annotation animated:YES]; + CGPoint calloutPoint = [singleTap locationInView:self]; + CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint]; + [self selectAnnotation:annotation animated:YES calloutPositioningRect:positionRect]; } else { @@ -1643,9 +1733,9 @@ public: { CGFloat distance = [quickZoom locationInView:quickZoom.view].y - self.quickZoomStart; - CGFloat newZoom = log2f(self.scale) + (distance / 75); + CGFloat newZoom = MAX(log2f(self.scale) + (distance / 75), _mbglMap->getMinZoom()); - if (newZoom < _mbglMap->getMinZoom()) return; + if (_mbglMap->getZoom() == newZoom) return; CGPoint centerPoint = [self anchorPointForGesture:quickZoom]; @@ -1672,14 +1762,14 @@ public: if ( ! self.isPitchEnabled) return; _mbglMap->cancelTransitions(); - MGLMapCamera *oldCamera = self.camera; if (twoFingerDrag.state == UIGestureRecognizerStateBegan) { [self trackGestureEvent:MGLEventGesturePitchStart forRecognizer:twoFingerDrag]; [self notifyGestureDidBegin]; } - else if (twoFingerDrag.state == UIGestureRecognizerStateBegan || twoFingerDrag.state == UIGestureRecognizerStateChanged) + + if (twoFingerDrag.state == UIGestureRecognizerStateBegan || twoFingerDrag.state == UIGestureRecognizerStateChanged) { CGFloat gestureDistance = CGPoint([twoFingerDrag translationInView:twoFingerDrag.view]).y; CGFloat currentPitch = _mbglMap->getPitch(); @@ -1689,6 +1779,7 @@ public: CGPoint centerPoint = [self anchorPointForGesture:twoFingerDrag]; + MGLMapCamera *oldCamera = self.camera; MGLMapCamera *toCamera = [self cameraByTiltingToPitch:pitchNew]; if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || @@ -1777,39 +1868,6 @@ public: return [gesture locationInView:gesture.view]; } -- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer -{ - if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) - { - UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer; - - if (panGesture.minimumNumberOfTouches == 2) - { - CGPoint velocity = [panGesture velocityInView:panGesture.view]; - double gestureAngle = MGLDegreesFromRadians(atan(velocity.y / velocity.x)); - double horizontalToleranceDegrees = 20.0; - - // cancel if gesture angle is not 90º±20º (more or less vertical) - if ( ! (fabs((fabs(gestureAngle) - 90.0)) < horizontalToleranceDegrees)) - { - return NO; - } - } - } - else if (gestureRecognizer == _singleTapGestureRecognizer) - { - // Gesture will be recognized if it could deselect an annotation - if(!self.selectedAnnotation) - { - id<MGLAnnotation>annotation = [self annotationForGestureRecognizer:(UITapGestureRecognizer*)gestureRecognizer persistingResults:NO]; - if(!annotation) { - return NO; - } - } - } - return YES; -} - - (void)handleCalloutAccessoryTapGesture:(UITapGestureRecognizer *)tap { if ([self.delegate respondsToSelector:@selector(mapView:annotation:calloutAccessoryControlTapped:)]) @@ -1827,7 +1885,7 @@ public: return [self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)]; } -- (void)calloutViewClicked:(__unused SMCalloutView *)calloutView +- (void)calloutViewClicked:(__unused MGLSMCalloutView *)calloutView { if ([self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)]) { @@ -1853,6 +1911,44 @@ public: UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, calloutView); } +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) + { + UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer; + + if (panGesture.minimumNumberOfTouches == 2) + { + CGPoint west = [panGesture locationOfTouch:0 inView:panGesture.view]; + CGPoint east = [panGesture locationOfTouch:1 inView:panGesture.view]; + + if (west.x > east.x) { + CGPoint swap = west; + west = east; + east = swap; + } + + CLLocationDegrees horizontalToleranceDegrees = 60.0; + if ([self angleBetweenPoints:west east:east] > horizontalToleranceDegrees) { + return NO; + } + + } + } + else if (gestureRecognizer == _singleTapGestureRecognizer) + { + // Gesture will be recognized if it could deselect an annotation + if(!self.selectedAnnotation) + { + id<MGLAnnotation>annotation = [self annotationForGestureRecognizer:(UITapGestureRecognizer*)gestureRecognizer persistingResults:NO]; + if(!annotation) { + return NO; + } + } + } + return YES; +} + - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { NSArray *validSimultaneousGestures = @[ self.pan, self.pinch, self.rotate ]; @@ -1860,6 +1956,16 @@ public: return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]); } +- (CLLocationDegrees)angleBetweenPoints:(CGPoint)west east:(CGPoint)east +{ + CGFloat slope = (west.y - east.y) / (west.x - east.x); + + CGFloat angle = atan(fabs(slope)); + CLLocationDegrees degrees = MGLDegreesFromRadians(angle); + + return degrees; +} + - (void)trackGestureEvent:(NSString *)gestureID forRecognizer:(UIGestureRecognizer *)recognizer { CGPoint pointInView = CGPointMake([recognizer locationInView:recognizer.view].x, [recognizer locationInView:recognizer.view].y); @@ -1876,13 +1982,28 @@ public: #pragma mark - Attribution - -- (void)showAttribution +- (void)showAttribution:(id)sender { - NSString *title = NSLocalizedStringWithDefaultValue(@"SDK_NAME", nil, nil, @"Mapbox iOS SDK", @"Action sheet title"); + BOOL shouldShowVersion = [sender isKindOfClass:[UILongPressGestureRecognizer class]]; + if (shouldShowVersion) + { + UILongPressGestureRecognizer *longPress = (UILongPressGestureRecognizer *)sender; + if (longPress.state != UIGestureRecognizerStateBegan) + { + return; + } + } + + NSString *title = NSLocalizedStringWithDefaultValue(@"SDK_NAME", nil, nil, @"Mapbox Maps SDK for iOS", @"Action sheet title"); UIAlertController *attributionController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleActionSheet]; - + + if (shouldShowVersion) + { + attributionController.title = [title stringByAppendingFormat:@" %@", [NSBundle mgl_frameworkInfoDictionary][@"MGLSemanticVersionString"]]; + } + NSArray *attributionInfos = [self.style attributionInfosWithFontSize:[UIFont buttonFontSize] linkColor:nil]; for (MGLAttributionInfo *info in attributionInfos) @@ -2045,10 +2166,6 @@ public: [self updateCalloutView]; } } - else if (context == MGLLayoutGuidesUpdatedContext && [keyPath isEqualToString:@"bounds"]) - { - [self setNeedsLayout]; - } } + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomEnabled @@ -2153,10 +2270,11 @@ public: - (void)resetPosition { - CGFloat pitch = _mbglMap->getStyle().getDefaultPitch(); - CLLocationDirection heading = mbgl::util::wrap(_mbglMap->getStyle().getDefaultBearing(), 0., 360.); - CLLocationDistance distance = MGLAltitudeForZoomLevel(_mbglMap->getStyle().getDefaultZoom(), pitch, 0, self.frame.size); - self.camera = [MGLMapCamera cameraLookingAtCenterCoordinate:MGLLocationCoordinate2DFromLatLng(_mbglMap->getStyle().getDefaultLatLng()) + auto camera = _mbglMap->getStyle().getDefaultCamera(); + CGFloat pitch = *camera.pitch; + CLLocationDirection heading = mbgl::util::wrap(*camera.angle, 0., 360.); + CLLocationDistance distance = MGLAltitudeForZoomLevel(*camera.zoom, pitch, 0, self.frame.size); + self.camera = [MGLMapCamera cameraLookingAtCenterCoordinate:MGLLocationCoordinate2DFromLatLng(*camera.center) fromDistance:distance pitch:pitch heading:heading]; @@ -2164,7 +2282,7 @@ public: - (void)emptyMemoryCache { - _mbglMap->onLowMemory(); + _rendererFrontend->onLowMemory(); } - (void)setZoomEnabled:(BOOL)zoomEnabled @@ -2198,8 +2316,61 @@ public: - (NSString *)accessibilityValue { + NSMutableArray *facts = [NSMutableArray array]; + double zoomLevel = round(self.zoomLevel + 1); - return [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE", nil, nil, @"Zoom %dx\n%ld annotation(s) visible", @"Map accessibility value"), (int)zoomLevel, (long)self.accessibilityAnnotationCount]; + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ZOOM", nil, nil, @"Zoom %dx.", @"Map accessibility value; {zoom level}"), (int)zoomLevel]]; + + NSInteger annotationCount = self.accessibilityAnnotationCount; + if (annotationCount) { + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ANNOTATIONS", nil, nil, @"%ld annotation(s) visible.", @"Map accessibility value; {number of visible annotations}"), (long)self.accessibilityAnnotationCount]]; + } + + NSArray *placeFeatures = self.visiblePlaceFeatures; + if (placeFeatures.count) { + NSMutableArray *placesArray = [NSMutableArray arrayWithCapacity:placeFeatures.count]; + NSMutableSet *placesSet = [NSMutableSet setWithCapacity:placeFeatures.count]; + for (id <MGLFeature> placeFeature in placeFeatures.reverseObjectEnumerator) { + NSString *name = [placeFeature attributeForKey:@"name"]; + if (![placesSet containsObject:name]) { + [placesArray addObject:name]; + [placesSet addObject:name]; + } + if (placesArray.count >= 3) { + break; + } + } + NSString *placesString = [placesArray componentsJoinedByString:NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator")]; + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_PLACES", nil, nil, @"Places visible: %@.", @"Map accessibility value; {list of visible places}"), placesString]]; + } + + NSArray *roadFeatures = self.visibleRoadFeatures; + if (roadFeatures.count) { + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ROADS", nil, nil, @"%ld road(s) visible.", @"Map accessibility value; {number of visible roads}"), roadFeatures.count]]; + } + + NSString *value = [facts componentsJoinedByString:@" "]; + return value; +} + +- (NS_ARRAY_OF(id <MGLFeature>) *)visiblePlaceFeatures +{ + if (!_visiblePlaceFeatures) + { + NSArray *placeStyleLayerIdentifiers = [self.style.placeStyleLayers valueForKey:@"identifier"]; + _visiblePlaceFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:placeStyleLayerIdentifiers]]; + } + return _visiblePlaceFeatures; +} + +- (NS_ARRAY_OF(id <MGLFeature>) *)visibleRoadFeatures +{ + if (!_visibleRoadFeatures) + { + NSArray *roadStyleLayerIdentifiers = [self.style.roadStyleLayers valueForKey:@"identifier"]; + _visibleRoadFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:roadStyleLayerIdentifiers]]; + } + return _visibleRoadFeatures; } - (CGRect)accessibilityFrame @@ -2233,14 +2404,9 @@ public: { if (self.calloutViewForSelectedAnnotation) { - return 2 /* selectedAnnotationCalloutView, mapViewProxyAccessibilityElement */; + return 2 /* calloutViewForSelectedAnnotation, mapViewProxyAccessibilityElement */; } - NSInteger count = self.accessibilityAnnotationCount + 2 /* compass, attributionButton */; - if (self.userLocationAnnotationView) - { - count++; - } - return count; + return !!self.userLocationAnnotationView + self.accessibilityAnnotationCount + self.visiblePlaceFeatures.count + self.visibleRoadFeatures.count + 2 /* compass, attributionButton */; } - (NSInteger)accessibilityAnnotationCount @@ -2265,67 +2431,123 @@ public: } return nil; } - std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds]; - - // Ornaments - if (index == 0) + + // Compass + NSUInteger compassIndex = 0; + if (index == compassIndex) { return self.compassView; } - if ( ! self.userLocationAnnotationView) - { - index++; - } - else if (index == 1) + + // User location annotation + NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView); + if (NSLocationInRange(index, userLocationAnnotationRange)) { return self.userLocationAnnotationView; } - if (index > 0 && (NSUInteger)index == visibleAnnotations.size() + 2 /* compass, userLocationAnnotationView */) - { - return self.attributionButton; - } - - std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + CGPoint centerPoint = self.contentCenter; if (self.userTrackingMode != MGLUserTrackingModeNone) { centerPoint = self.userLocationAnnotationViewCenter; } - CLLocationCoordinate2D currentCoordinate = [self convertPoint:centerPoint toCoordinateFromView:self]; - std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { - CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; - CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; - CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, - coordinateA.longitude - currentCoordinate.longitude); - CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, - coordinateB.longitude - currentCoordinate.longitude); - return deltaA < deltaB; - }); - - NSUInteger annotationIndex = MGLAnnotationTagNotFound; - if (index >= 0 && (NSUInteger)(index - 2) < visibleAnnotations.size()) + + // Visible annotations + std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds]; + NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); + if (NSLocationInRange(index, visibleAnnotationRange)) + { + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CGPoint pointA = [self convertCoordinate:coordinateA toPointToView:self]; + CGPoint pointB = [self convertCoordinate:coordinateB toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return deltaA < deltaB; + }); + + NSUInteger annotationIndex = index - visibleAnnotationRange.location; + MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex]; + NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", (long)index); + return [self accessibilityElementForAnnotationWithTag:annotationTag]; + } + + // Visible place features + NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures; + NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count); + if (NSLocationInRange(index, visiblePlaceFeatureRange)) + { + visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = visiblePlaceFeatures[index - visiblePlaceFeatureRange.location]; + return [self accessibilityElementForPlaceFeature:feature]; + } + + // Visible road features + NSArray *visibleRoadFeatures = self.visibleRoadFeatures; + NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count); + if (NSLocationInRange(index, visibleRoadFeatureRange)) + { + visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = visibleRoadFeatures[index - visibleRoadFeatureRange.location]; + return [self accessibilityElementForRoadFeature:feature]; + } + + // Attribution button + NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange); + if (index == attributionButtonIndex) { - annotationIndex = index - 2 /* compass, userLocationAnnotationView */; + return self.attributionButton; } - MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex]; - NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", (long)index); + + NSAssert(NO, @"Index %ld not in recognized accessibility element ranges. " + @"User location annotation range: %@; visible annotation range: %@; " + @"visible place feature range: %@; visible road feature range: %@.", + (long)index, NSStringFromRange(userLocationAnnotationRange), + NSStringFromRange(visibleAnnotationRange), NSStringFromRange(visiblePlaceFeatureRange), + NSStringFromRange(visibleRoadFeatureRange)); + return nil; +} + +/** + Returns an accessibility element corresponding to a visible annotation with the given tag. + + @param annotationTag Tag of the annotation represented by the accessibility element to return. + */ +- (id)accessibilityElementForAnnotationWithTag:(MGLAnnotationTag)annotationTag +{ NSAssert(_annotationContextsByAnnotationTag.count(annotationTag), @"Missing annotation for tag %u.", annotationTag); MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); id <MGLAnnotation> annotation = annotationContext.annotation; - + // Let the annotation view serve as its own accessibility element. MGLAnnotationView *annotationView = annotationContext.annotationView; if (annotationView && annotationView.superview) { return annotationView; } - + // Lazily create an accessibility element for the found annotation. if ( ! annotationContext.accessibilityElement) { annotationContext.accessibilityElement = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self tag:annotationTag]; } - + // Update the accessibility element. MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; CGRect annotationFrame = [self frameOfImage:annotationImage.image centeredAtCoordinate:annotation.coordinate]; @@ -2336,8 +2558,7 @@ public: annotationFrame = CGRectUnion(annotationFrame, minimumFrame); CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); annotationContext.accessibilityElement.accessibilityFrame = screenRect; - annotationContext.accessibilityElement.accessibilityHint = NSLocalizedStringWithDefaultValue(@"ANNOTATION_A11Y_HINT", nil, nil, @"Shows more info", @"Accessibility hint"); - + if ([annotation respondsToSelector:@selector(title)]) { annotationContext.accessibilityElement.accessibilityLabel = annotation.title; @@ -2346,10 +2567,114 @@ public: { annotationContext.accessibilityElement.accessibilityValue = annotation.subtitle; } - + return annotationContext.accessibilityElement; } +/** + Returns an accessibility element corresponding to the given place feature. + + @param feature The place feature represented by the accessibility element. + */ +- (id)accessibilityElementForPlaceFeature:(id <MGLFeature>)feature +{ + if (!_featureAccessibilityElements) + { + _featureAccessibilityElements = [NSMutableSet set]; + } + + MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) { + return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier]; + }].anyObject; + if (!element) + { + element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + } + CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self]; + CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MGLAnnotationAccessibilityElementMinimumSize.width / 2, -MGLAnnotationAccessibilityElementMinimumSize.width / 2); + CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); + element.accessibilityFrame = screenRect; + + [_featureAccessibilityElements addObject:element]; + + return element; +} + +/** + Returns an accessibility element corresponding to the given road feature. + + @param feature The road feature represented by the accessibility element. + */ +- (id)accessibilityElementForRoadFeature:(id <MGLFeature>)feature +{ + if (!_featureAccessibilityElements) + { + _featureAccessibilityElements = [NSMutableSet set]; + } + + MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) { + return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier]; + }].anyObject; + if (!element) + { + element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + } + + UIBezierPath *path; + if ([feature isKindOfClass:[MGLPointFeature class]]) + { + CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self]; + CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MGLAnnotationAccessibilityElementMinimumSize.width / 2, -MGLAnnotationAccessibilityElementMinimumSize.width / 2); + CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); + element.accessibilityFrame = screenRect; + } + else if ([feature isKindOfClass:[MGLPolylineFeature class]]) + { + path = [self pathOfPolyline:(MGLPolyline *)feature]; + } + else if ([feature isKindOfClass:[MGLMultiPolylineFeature class]]) + { + path = [UIBezierPath bezierPath]; + for (MGLPolyline *polyline in [(MGLMultiPolylineFeature *)feature polylines]) + { + [path appendPath:[self pathOfPolyline:polyline]]; + } + } + + if (path) + { + CGPathRef strokedCGPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, MGLAnnotationAccessibilityElementMinimumSize.width, kCGLineCapButt, kCGLineJoinMiter, 0); + UIBezierPath *strokedPath = [UIBezierPath bezierPathWithCGPath:strokedCGPath]; + CGPathRelease(strokedCGPath); + UIBezierPath *screenPath = UIAccessibilityConvertPathToScreenCoordinates(strokedPath, self); + element.accessibilityPath = screenPath; + } + + [_featureAccessibilityElements addObject:element]; + + return element; +} + +- (UIBezierPath *)pathOfPolyline:(MGLPolyline *)polyline +{ + CLLocationCoordinate2D *coordinates = polyline.coordinates; + NSUInteger pointCount = polyline.pointCount; + UIBezierPath *path = [UIBezierPath bezierPath]; + for (NSUInteger i = 0; i < pointCount; i++) + { + CGPoint point = [self convertCoordinate:coordinates[i] toPointToView:self]; + if (i) + { + [path addLineToPoint:point]; + } + else + { + [path moveToPoint:point]; + } + } + return path; +} + - (NSInteger)indexOfAccessibilityElement:(id)element { if (self.calloutViewForSelectedAnnotation) @@ -2357,17 +2682,30 @@ public: return [@[self.calloutViewForSelectedAnnotation, self.mapViewProxyAccessibilityElement] indexOfObject:element]; } + + // Compass + NSUInteger compassIndex = 0; if (element == self.compassView) { - return 0; + return compassIndex; } + + // User location annotation + NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView); if (element == self.userLocationAnnotationView) { - return 1; + return userLocationAnnotationRange.location; } - + + CGPoint centerPoint = self.contentCenter; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + centerPoint = self.userLocationAnnotationViewCenter; + } + + // Visible annotations std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds]; - + NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); MGLAnnotationTag tag = MGLAnnotationTagNotFound; if ([element isKindOfClass:[MGLAnnotationView class]]) { @@ -2378,22 +2716,92 @@ public: { tag = [(MGLAnnotationAccessibilityElement *)element tag]; } - else if (element == self.attributionButton) - { - return !!self.userLocationAnnotationView + visibleAnnotations.size(); + + if (tag != MGLAnnotationTagNotFound) + { + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CGPoint pointA = [self convertCoordinate:coordinateA toPointToView:self]; + CGPoint pointB = [self convertCoordinate:coordinateB toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return deltaA < deltaB; + }); + + auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag); + if (foundElement == visibleAnnotations.end()) + { + return NSNotFound; + } + return visibleAnnotationRange.location + std::distance(visibleAnnotations.begin(), foundElement); } - else - { - return NSNotFound; + + // Visible place features + NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures; + NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count); + if ([element isKindOfClass:[MGLPlaceFeatureAccessibilityElement class]]) + { + visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = [(MGLPlaceFeatureAccessibilityElement *)element feature]; + NSUInteger featureIndex = [visiblePlaceFeatures indexOfObject:feature]; + if (featureIndex == NSNotFound) + { + featureIndex = [visiblePlaceFeatures indexOfObjectPassingTest:^BOOL (id <MGLFeature> _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) { + return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier]; + }]; + } + if (featureIndex == NSNotFound) + { + return NSNotFound; + } + return visiblePlaceFeatureRange.location + featureIndex; } - - std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); - auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag); - if (foundElement == visibleAnnotations.end()) + + // Visible road features + NSArray *visibleRoadFeatures = self.visibleRoadFeatures; + NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count); + if ([element isKindOfClass:[MGLRoadFeatureAccessibilityElement class]]) + { + visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = [(MGLRoadFeatureAccessibilityElement *)element feature]; + NSUInteger featureIndex = [visibleRoadFeatures indexOfObject:feature]; + if (featureIndex == NSNotFound) + { + featureIndex = [visibleRoadFeatures indexOfObjectPassingTest:^BOOL (id <MGLFeature> _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) { + return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier]; + }]; + } + if (featureIndex == NSNotFound) + { + return NSNotFound; + } + return visibleRoadFeatureRange.location + featureIndex; + } + + // Attribution button + NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange); + if (element == self.attributionButton) { - return NSNotFound; + return attributionButtonIndex; } - return !!self.userLocationAnnotationView + std::distance(visibleAnnotations.begin(), foundElement) + 1 /* compass */; + + return NSNotFound; } - (MGLMapViewProxyAccessibilityElement *)mapViewProxyAccessibilityElement @@ -2424,10 +2832,11 @@ public: { centerPoint = self.userLocationAnnotationViewCenter; } - _mbglMap->setZoom(_mbglMap->getZoom() + log2(scaleFactor), mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + double newZoom = round(self.zoomLevel) + log2(scaleFactor); + _mbglMap->setZoom(newZoom, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); [self unrotateIfNeededForGesture]; - UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.accessibilityValue); + _accessibilityValueAnnouncementIsPending = YES; } #pragma mark - Geography - @@ -2547,8 +2956,6 @@ public: - (void)setMinimumZoomLevel:(double)minimumZoomLevel { - _mbglMap->setMinZoom(minimumZoomLevel); - [self validateTileCacheSize]; } - (double)minimumZoomLevel @@ -2559,7 +2966,6 @@ public: - (void)setMaximumZoomLevel:(double)maximumZoomLevel { _mbglMap->setMaxZoom(maximumZoomLevel); - [self validateTileCacheSize]; } - (double)maximumZoomLevel @@ -2762,6 +3168,10 @@ public: - (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion { + [self setCamera:camera withDuration:duration animationTimingFunction:function edgePadding:self.contentInset completionHandler:completion]; +} + +- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function edgePadding:(UIEdgeInsets)edgePadding completionHandler:(nullable void (^)(void))completion { mbgl::AnimationOptions animationOptions; if (duration > 0) { @@ -2790,7 +3200,7 @@ public: [self willChangeValueForKey:@"camera"]; _mbglMap->cancelTransitions(); - mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera edgePadding:self.contentInset]; + mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera edgePadding:edgePadding]; _mbglMap->easeTo(cameraOptions, animationOptions); [self didChangeValueForKey:@"camera"]; } @@ -2864,6 +3274,16 @@ public: return [self cameraForCameraOptions:cameraOptions]; } +- (MGLMapCamera *)cameraThatFitsShape:(MGLShape *)shape direction:(CLLocationDirection)direction edgePadding:(UIEdgeInsets)insets { + mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(insets); + padding += MGLEdgeInsetsFromNSEdgeInsets(self.contentInset); + + mbgl::CameraOptions cameraOptions = _mbglMap->cameraForGeometry([shape geometryObject], padding, direction); + + return [self cameraForCameraOptions:cameraOptions]; + +} + - (MGLMapCamera *)cameraForCameraOptions:(const mbgl::CameraOptions &)cameraOptions { CLLocationCoordinate2D centerCoordinate = MGLLocationCoordinate2DFromLatLng(cameraOptions.center ? *cameraOptions.center : _mbglMap->getLatLng()); @@ -3099,6 +3519,12 @@ public: } std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:rect]; + std::vector<MGLAnnotationTag> shapeAnnotationTags = [self shapeAnnotationTagsInRect:rect]; + + if (shapeAnnotationTags.size()) { + annotationTags.insert(annotationTags.end(), shapeAnnotationTags.begin(), shapeAnnotationTags.end()); + } + if (annotationTags.size()) { NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:annotationTags.size()]; @@ -3215,7 +3641,7 @@ public: { annotationViewsForAnnotation[annotationValue] = annotationView; annotationView.annotation = annotation; - annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self]; + annotationView.center = MGLPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]); [newAnnotationViews addObject:annotationView]; MGLAnnotationImage *annotationImage = self.invisibleAnnotationImage; @@ -3604,6 +4030,11 @@ public: queryRect = CGRectInset(queryRect, -MGLAnnotationImagePaddingForHitTest, -MGLAnnotationImagePaddingForHitTest); std::vector<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect]; + std::vector<MGLAnnotationTag> nearbyShapeAnnotations = [self shapeAnnotationTagsInRect:queryRect]; + + if (nearbyShapeAnnotations.size()) { + nearbyAnnotations.insert(nearbyAnnotations.end(), nearbyShapeAnnotations.begin(), nearbyShapeAnnotations.end()); + } if (nearbyAnnotations.size()) { @@ -3611,54 +4042,59 @@ public: CGRect hitRect = CGRectInset({ point, CGSizeZero }, -MGLAnnotationImagePaddingForHitTest, -MGLAnnotationImagePaddingForHitTest); - + // Filter out any annotation whose image or view is unselectable or for which // hit testing fails. - auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), - [&](const MGLAnnotationTag annotationTag) - { + auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag annotationTag) { id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag]; NSAssert(annotation, @"Unknown annotation found nearby tap"); if ( ! annotation) { return true; } - + MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); CGRect annotationRect; - + MGLAnnotationView *annotationView = annotationContext.annotationView; + if (annotationView) { if ( ! annotationView.enabled) { return true; } - - CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self]; + + CGPoint calloutAnchorPoint = MGLPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]); CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -CGRectGetWidth(annotationView.frame) / 2, -CGRectGetHeight(annotationView.frame) / 2); annotationRect = UIEdgeInsetsInsetRect(frame, annotationView.alignmentRectInsets); } else { + if ([annotation isKindOfClass:[MGLShape class]]) + { + return false; + } + MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; if ( ! annotationImage.enabled) { return true; } - + MGLAnnotationImage *fallbackAnnotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]; UIImage *fallbackImage = fallbackAnnotationImage.image; - + annotationRect = [self frameOfImage:annotationImage.image ?: fallbackImage centeredAtCoordinate:annotation.coordinate]; } - + // Filter out the annotation if the fattened finger didn’t land // within the image’s alignment rect. return !!!CGRectIntersectsRect(annotationRect, hitRect); }); - + nearbyAnnotations.resize(std::distance(nearbyAnnotations.begin(), end)); + } MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound; @@ -3735,7 +4171,15 @@ public: /// Returns the tags of the annotations coincident with the given rectangle. - (std::vector<MGLAnnotationTag>)annotationTagsInRect:(CGRect)rect { - return _mbglMap->queryPointAnnotations({ + return _rendererFrontend->getRenderer()->queryPointAnnotations({ + { CGRectGetMinX(rect), CGRectGetMinY(rect) }, + { CGRectGetMaxX(rect), CGRectGetMaxY(rect) }, + }); +} + +- (std::vector<MGLAnnotationTag>)shapeAnnotationTagsInRect:(CGRect)rect +{ + return _rendererFrontend->getRenderer()->queryShapeAnnotations({ { CGRectGetMinX(rect), CGRectGetMinY(rect) }, { CGRectGetMaxX(rect), CGRectGetMaxY(rect) }, }); @@ -3790,17 +4234,16 @@ public: - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { - if ( ! annotation) return; + CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero]; + [self selectAnnotation:annotation animated:animated calloutPositioningRect:positioningRect]; +} - if ([annotation isKindOfClass:[MGLMultiPoint class]]) return; +- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect +{ + if ( ! annotation) return; if (annotation == self.selectedAnnotation) return; - if (annotation != self.userLocation) - { - self.userTrackingMode = MGLUserTrackingModeNone; - } - [self deselectAnnotation:self.selectedAnnotation animated:NO]; // Add the annotation to the map if it hasn’t been added yet. @@ -3812,9 +4255,6 @@ public: if (annotationTag == MGLAnnotationTagNotFound) return; } - // By default attempt to use the GL annotation image frame as the positioning rect. - CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; - MGLAnnotationView *annotationView = nil; if (annotation != self.userLocation) @@ -3822,21 +4262,12 @@ 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. - positioningRect = 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:animated]; } } - } - - // The client can request that any annotation be selected (even ones that are offscreen). - // The annotation can’t be selected if no part of it is hittable. - if ( ! CGRectIntersectsRect(positioningRect, self.bounds) && annotation != self.userLocation) - { - return; - } self.selectedAnnotation = annotation; @@ -3866,7 +4297,7 @@ public: if (_userLocationAnnotationIsSelected) { - positioningRect = [self.userLocationAnnotationView.layer.presentationLayer frame]; + calloutPositioningRect = [self.userLocationAnnotationView.layer.presentationLayer frame]; CGRect implicitAnnotationFrame = [self.userLocationAnnotationView.layer.presentationLayer frame]; CGRect explicitAnnotationFrame = self.userLocationAnnotationView.frame; @@ -3882,7 +4313,7 @@ public: if ([calloutView.leftAccessoryView isKindOfClass:[UIControl class]]) { UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleCalloutAccessoryTapGesture:)]; + action:@selector(handleCalloutAccessoryTapGesture:)]; [calloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap]; } @@ -3895,7 +4326,7 @@ public: if ([calloutView.rightAccessoryView isKindOfClass:[UIControl class]]) { UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleCalloutAccessoryTapGesture:)]; + action:@selector(handleCalloutAccessoryTapGesture:)]; [calloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap]; } @@ -3905,7 +4336,7 @@ public: calloutView.delegate = self; // present popup - [calloutView presentCalloutFromRect:positioningRect + [calloutView presentCalloutFromRect:calloutPositioningRect inView:self.glView constrainedToView:self.glView animated:animated]; @@ -3935,6 +4366,27 @@ public: /// Returns the rectangle that represents the annotation image of the annotation /// with the given tag. This rectangle is fitted to the image’s alignment rect /// and is appropriate for positioning a popover. +/// If a shape annotation is visible but its centroid is not, and a default point is specified, +/// the callout view is anchored to the default callout point. +- (CGRect)positioningRectForAnnotation:(id <MGLAnnotation>)annotation defaultCalloutPoint:(CGPoint)calloutPoint +{ + MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation]; + CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; + + // For annotations which `coordinate` falls offscreen it will use the current tap point as anchor instead. + if ( ! CGRectIntersectsRect(positioningRect, self.bounds) && annotation != self.userLocation) + { + if (!CGPointEqualToPoint(calloutPoint, CGPointZero)) { + positioningRect = CGRectMake(calloutPoint.x, calloutPoint.y, positioningRect.size.width, positioningRect.size.height); + } + } + + return positioningRect; +} + +/// Returns the rectangle that represents the annotation image of the annotation +/// with the given tag. This rectangle is fitted to the image’s alignment rect +/// and is appropriate for positioning a popover. - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annotationTag { id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag]; @@ -3942,6 +4394,13 @@ public: { return CGRectZero; } + + if ([annotation isKindOfClass:[MGLMultiPoint class]]) { + CLLocationCoordinate2D origin = annotation.coordinate; + CGPoint originPoint = [self convertCoordinate:origin toPointToView:self]; + return CGRectMake(originPoint.x, originPoint.y, MGLAnnotationImagePaddingForHitTest, MGLAnnotationImagePaddingForHitTest); + + } UIImage *image = [self imageOfAnnotationWithTag:annotationTag].image; if ( ! image) { @@ -3963,7 +4422,7 @@ public: /// image centered at the given coordinate. - (CGRect)frameOfImage:(UIImage *)image centeredAtCoordinate:(CLLocationCoordinate2D)coordinate { - CGPoint calloutAnchorPoint = [self convertCoordinate:coordinate toPointToView:self]; + CGPoint calloutAnchorPoint = MGLPointRounded([self convertCoordinate:coordinate toPointToView:self]); CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -image.size.width / 2, -image.size.height / 2); return UIEdgeInsetsInsetRect(frame, image.alignmentRectInsets); } @@ -4146,33 +4605,41 @@ public: { self.locationManager = [[CLLocationManager alloc] init]; - if ([CLLocationManager instancesRespondToSelector:@selector(requestWhenInUseAuthorization)] && [CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) + if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) { - BOOL hasLocationDescription = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"] || [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"]; - if (!hasLocationDescription) + BOOL requiresWhenInUseUsageDescription = [NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){11,0,0}]; + BOOL hasWhenInUseUsageDescription = !![[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"]; + BOOL hasAlwaysUsageDescription; + if (requiresWhenInUseUsageDescription) { - [NSException raise:@"Missing Location Services usage description" format: - @"This app must have a value for NSLocationAlwaysUsageDescription or NSLocationWhenInUseUsageDescription in its Info.plist."]; + hasAlwaysUsageDescription = !![[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysAndWhenInUseUsageDescription"] && hasWhenInUseUsageDescription; + } + else + { + hasAlwaysUsageDescription = !![[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"]; } - if ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"]) + if (hasAlwaysUsageDescription) { [self.locationManager requestAlwaysAuthorization]; } - else if ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"]) + else if (hasWhenInUseUsageDescription) { [self.locationManager requestWhenInUseAuthorization]; } + else + { + NSString *suggestedUsageKeys = requiresWhenInUseUsageDescription ? + @"NSLocationWhenInUseUsageDescription and (optionally) NSLocationAlwaysAndWhenInUseUsageDescription" : + @"NSLocationWhenInUseUsageDescription and/or NSLocationAlwaysUsageDescription"; + [NSException raise:@"Missing Location Services usage description" format:@"This app must have a value for %@ in its Info.plist.", suggestedUsageKeys]; + } } - self.locationManager.headingFilter = 5.0; self.locationManager.delegate = self; [self.locationManager startUpdatingLocation]; - if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) - { - [self.locationManager startUpdatingHeading]; - } + [self validateUserHeadingUpdating]; } else if ( ! shouldEnableLocationServices && self.locationManager) { @@ -4296,8 +4763,6 @@ public: { self.userTrackingState = MGLUserTrackingStatePossible; - [self.locationManager stopUpdatingHeading]; - // Immediately update the annotation view; other cases update inside // the locationManager:didUpdateLocations: method. [self updateUserLocationAnnotationView]; @@ -4310,14 +4775,6 @@ public: self.userTrackingState = animated ? MGLUserTrackingStatePossible : MGLUserTrackingStateChanged; self.showsUserLocation = YES; - [self.locationManager stopUpdatingHeading]; - - CLLocation *location = self.userLocation.location; - if (location && self.userLocationAnnotationView) - { - [self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated]; - } - break; } case MGLUserTrackingModeFollowWithHeading: @@ -4334,19 +4791,21 @@ public: [self setZoomLevel:self.currentMinimumZoom animated:YES]; } - if (self.userLocationAnnotationView) - { - [self locationManager:self.locationManager didUpdateLocations:@[self.userLocation.location] animated:animated]; - } - - [self updateHeadingForDeviceOrientation]; - - [self.locationManager startUpdatingHeading]; - break; } } + if (_userTrackingMode != MGLUserTrackingModeNone) + { + CLLocation *location = self.userLocation.location; + if (location && self.userLocationAnnotationView) + { + [self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated]; + } + } + + [self validateUserHeadingUpdating]; + if ([self.delegate respondsToSelector:@selector(mapView:didChangeUserTrackingMode:animated:)]) { [self.delegate mapView:self didChangeUserTrackingMode:_userTrackingMode animated:animated]; @@ -4385,14 +4844,42 @@ public: if (self.userTrackingMode == MGLUserTrackingModeFollowWithCourse) { self.userTrackingState = MGLUserTrackingStatePossible; - if (self.userLocation.location) + + CLLocation *location = self.userLocation.location; + if (location) { - [self locationManager:self.locationManager didUpdateLocations:@[self.userLocation.location] animated:animated]; + [self locationManager:self.locationManager didUpdateLocations:@[location] animated:animated]; } } } } +- (void)setShowsUserHeadingIndicator:(BOOL)showsUserHeadingIndicator +{ + _showsUserHeadingIndicator = showsUserHeadingIndicator; + + if (_showsUserHeadingIndicator) + { + self.showsUserLocation = YES; + } + [self validateUserHeadingUpdating]; +} + +- (void)validateUserHeadingUpdating +{ + BOOL canShowPermanentHeadingIndicator = self.showsUserHeadingIndicator && self.userTrackingMode != MGLUserTrackingModeFollowWithCourse; + + if (canShowPermanentHeadingIndicator || self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) + { + [self updateHeadingForDeviceOrientation]; + [self.locationManager startUpdatingHeading]; + } + else + { + [self.locationManager stopUpdatingHeading]; + } +} + - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { [self locationManager:manager didUpdateLocations:locations animated:YES]; @@ -4402,6 +4889,7 @@ public: { CLLocation *oldLocation = self.userLocation.location; CLLocation *newLocation = locations.lastObject; + _distanceFromOldUserLocation = [newLocation distanceFromLocation:oldLocation]; if ( ! _showsUserLocation || ! newLocation || ! CLLocationCoordinate2DIsValid(newLocation.coordinate)) return; @@ -4495,7 +4983,12 @@ public: /// first location update. - (void)didUpdateLocationSignificantlyAnimated:(BOOL)animated { - self.userTrackingState = MGLUserTrackingStateBegan; + + if (_distanceFromOldUserLocation >= MGLDistanceThresholdForCameraPause) { + self.userTrackingState = MGLUserTrackingStateBeginSignificantTransition; + } else { + self.userTrackingState = MGLUserTrackingStateBegan; + } MGLMapCamera *camera = self.camera; camera.centerCoordinate = self.userLocation.location.coordinate; @@ -4515,7 +5008,8 @@ public: peakAltitude:-1 completionHandler:^{ MGLMapView *strongSelf = weakSelf; - if (strongSelf.userTrackingState == MGLUserTrackingStateBegan) + if (strongSelf.userTrackingState == MGLUserTrackingStateBegan || + strongSelf.userTrackingState == MGLDistanceThresholdForCameraPause) { strongSelf.userTrackingState = MGLUserTrackingStateChanged; } @@ -4633,6 +5127,11 @@ public: self.userLocation.heading = newHeading; + if (self.showsUserHeadingIndicator || self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) + { + [self updateUserLocationAnnotationView]; + } + if ([self.delegate respondsToSelector:@selector(mapView:didUpdateUserLocation:)]) { [self.delegate mapView:self didUpdateUserLocation:self.userLocation]; @@ -4669,30 +5168,39 @@ public: { // note that right/left device and interface orientations are opposites (see UIApplication.h) // + CLDeviceOrientation orientation; switch ([[UIApplication sharedApplication] statusBarOrientation]) { case (UIInterfaceOrientationLandscapeLeft): { - self.locationManager.headingOrientation = CLDeviceOrientationLandscapeRight; + orientation = CLDeviceOrientationLandscapeRight; break; } case (UIInterfaceOrientationLandscapeRight): { - self.locationManager.headingOrientation = CLDeviceOrientationLandscapeLeft; + orientation = CLDeviceOrientationLandscapeLeft; break; } case (UIInterfaceOrientationPortraitUpsideDown): { - self.locationManager.headingOrientation = CLDeviceOrientationPortraitUpsideDown; + orientation = CLDeviceOrientationPortraitUpsideDown; break; } case (UIInterfaceOrientationPortrait): default: { - self.locationManager.headingOrientation = CLDeviceOrientationPortrait; + orientation = CLDeviceOrientationPortrait; break; } } + + // Setting the location manager's heading orientation causes it to send + // a heading event, which in turn makes us redraw, which kicks off a + // loop... so don't do that. rdar://34059173 + if (self.locationManager.headingOrientation != orientation) + { + self.locationManager.headingOrientation = orientation; + } } } @@ -4728,7 +5236,7 @@ public: optionalFilter = predicate.mgl_filter; } - std::vector<mbgl::Feature> features = _mbglMap->queryRenderedFeatures(screenCoordinate, { optionalLayerIDs, optionalFilter }); + std::vector<mbgl::Feature> features = _rendererFrontend->getRenderer()->queryRenderedFeatures(screenCoordinate, { optionalLayerIDs, optionalFilter }); return MGLFeaturesFromMBGLFeatures(features); } @@ -4761,7 +5269,7 @@ public: optionalFilter = predicate.mgl_filter; } - std::vector<mbgl::Feature> features = _mbglMap->queryRenderedFeatures(screenBox, { optionalLayerIDs, optionalFilter }); + std::vector<mbgl::Feature> features = _rendererFrontend->getRenderer()->queryRenderedFeatures(screenBox, { optionalLayerIDs, optionalFilter }); return MGLFeaturesFromMBGLFeatures(features); } @@ -4888,12 +5396,26 @@ public: { if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + _featureAccessibilityElements = nil; + _visiblePlaceFeatures = nil; + _visibleRoadFeatures = nil; + if (_accessibilityValueAnnouncementIsPending) { + _accessibilityValueAnnouncementIsPending = NO; + [self performSelector:@selector(announceAccessibilityValue) withObject:nil afterDelay:0.1]; + } else { + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + } } [self.delegate mapView:self regionDidChangeAnimated:animated]; } } +- (void)announceAccessibilityValue +{ + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.accessibilityValue); + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); +} + - (void)mapViewWillStartLoadingMap { if (!_mbglMap) { return; @@ -4975,6 +5497,8 @@ public: if (!_mbglMap) { return; } + + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)]) { @@ -5058,7 +5582,7 @@ public: if (annotationView) { - annotationView.center = [self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self]; + annotationView.center = MGLPointRounded([self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self]); } } @@ -5176,7 +5700,7 @@ public: } else { - userPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self]; + userPoint = MGLPointRounded([self convertCoordinate:self.userLocation.coordinate toPointToView:self]); } if ( ! annotationView.superview) @@ -5429,7 +5953,7 @@ public: return _annotationViewReuseQueueByIdentifier[identifier]; } -class MBGLView : public mbgl::View, public mbgl::Backend +class MBGLView : public mbgl::RendererBackend, public mbgl::MapObserver { public: MBGLView(MGLMapView* nativeView_) : nativeView(nativeView_) { @@ -5457,6 +5981,10 @@ public: } } + mbgl::Size getFramebufferSize() const override { + return nativeView.framebufferSize; + } + void onCameraWillChange(mbgl::MapObserver::CameraChangeMode mode) override { bool animated = mode == mbgl::MapObserver::CameraChangeMode::Animated; [nativeView cameraWillChangeAnimated:animated]; @@ -5527,7 +6055,7 @@ public: [nativeView didFinishLoadingStyle]; } - mbgl::gl::ProcAddress initializeExtension(const char* name) override { + mbgl::gl::ProcAddress getExtensionFunctionPointer(const char* name) override { static CFBundleRef framework = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.opengles")); if (!framework) { throw std::runtime_error("Failed to load OpenGL framework."); @@ -5541,11 +6069,6 @@ public: return reinterpret_cast<mbgl::gl::ProcAddress>(symbol); } - void invalidate() override - { - [nativeView setNeedsGLDisplay]; - } - void activate() override { if (activationCount++) @@ -5713,4 +6236,19 @@ private: self.pitchEnabled = allowsTilting; } ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingShowsHeading +{ + return [NSSet setWithObject:@"showsUserHeadingIndicator"]; +} + +- (BOOL)showsHeading +{ + return self.showsUserHeadingIndicator; +} + +- (void)setShowsHeading:(BOOL)showsHeading +{ + self.showsUserHeadingIndicator = showsHeading; +} + @end |