summaryrefslogtreecommitdiff
path: root/platform/ios/src
diff options
context:
space:
mode:
Diffstat (limited to 'platform/ios/src')
-rw-r--r--platform/ios/src/MGLFaux3DUserLocationAnnotationView.h10
-rw-r--r--platform/ios/src/MGLFaux3DUserLocationAnnotationView.m180
-rw-r--r--platform/ios/src/MGLMapView+IBAdditions.h1
-rw-r--r--platform/ios/src/MGLMapView.h46
-rw-r--r--platform/ios/src/MGLMapView.mm471
-rw-r--r--platform/ios/src/MGLUserLocation.h3
-rw-r--r--platform/ios/src/MGLUserLocation.m3
-rw-r--r--platform/ios/src/MGLUserLocationHeadingArrowLayer.h11
-rw-r--r--platform/ios/src/MGLUserLocationHeadingArrowLayer.m59
-rw-r--r--platform/ios/src/MGLUserLocationHeadingBeamLayer.h11
-rw-r--r--platform/ios/src/MGLUserLocationHeadingBeamLayer.m104
-rw-r--r--platform/ios/src/MGLUserLocationHeadingIndicator.h10
12 files changed, 632 insertions, 277 deletions
diff --git a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.h b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.h
index c48dd6b27b..da5c7155a5 100644
--- a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.h
+++ b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.h
@@ -1,7 +1,15 @@
#import <UIKit/UIKit.h>
#import "MGLUserLocationAnnotationView.h"
+const CGFloat MGLUserLocationAnnotationDotSize = 22.0;
+const CGFloat MGLUserLocationAnnotationHaloSize = 115.0;
+
+const CGFloat MGLUserLocationAnnotationPuckSize = 45.0;
+const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.6;
+
+// Threshold in radians between heading indicator rotation updates.
+const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01;
+
@interface MGLFaux3DUserLocationAnnotationView : MGLUserLocationAnnotationView
@end
-
diff --git a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m
index 5f67f24f4e..7001569b30 100644
--- a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m
+++ b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m
@@ -2,14 +2,9 @@
#import "MGLMapView.h"
#import "MGLUserLocation.h"
-
-const CGFloat MGLUserLocationAnnotationDotSize = 22.0;
-const CGFloat MGLUserLocationAnnotationHaloSize = 115.0;
-
-const CGFloat MGLUserLocationAnnotationPuckSize = 45.0;
-const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.6;
-
-#pragma mark -
+#import "MGLUserLocationHeadingIndicator.h"
+#import "MGLUserLocationHeadingArrowLayer.h"
+#import "MGLUserLocationHeadingBeamLayer.h"
@implementation MGLFaux3DUserLocationAnnotationView
{
@@ -18,14 +13,13 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
CALayer *_puckDot;
CAShapeLayer *_puckArrow;
- CALayer *_headingIndicatorLayer;
- CAShapeLayer *_headingIndicatorMaskLayer;
+ CALayer<MGLUserLocationHeadingIndicator> *_headingIndicatorLayer;
CALayer *_accuracyRingLayer;
CALayer *_dotBorderLayer;
CALayer *_dotLayer;
CALayer *_haloLayer;
- double _oldHeadingAccuracy;
+ CLLocationDirection _oldHeadingAccuracy;
CLLocationAccuracy _oldHorizontalAccuracy;
double _oldZoom;
double _oldPitch;
@@ -56,21 +50,18 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
- (void)setTintColor:(UIColor *)tintColor
{
+ CGColorRef newTintColor = [tintColor CGColor];
+
if (_puckModeActivated)
{
- _puckArrow.fillColor = [tintColor CGColor];
+ _puckArrow.fillColor = newTintColor;
}
else
{
- if (_accuracyRingLayer)
- {
- _accuracyRingLayer.backgroundColor = [tintColor CGColor];
- }
-
- _haloLayer.backgroundColor = [tintColor CGColor];
- _dotLayer.backgroundColor = [tintColor CGColor];
-
- _headingIndicatorLayer.contents = (__bridge id)[[self headingIndicatorTintedGradientImage] CGImage];
+ _accuracyRingLayer.backgroundColor = newTintColor;
+ _haloLayer.backgroundColor = newTintColor;
+ _dotLayer.backgroundColor = newTintColor;
+ [_headingIndicatorLayer updateTintColor:newTintColor];
}
}
@@ -80,7 +71,7 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
{
// disable implicit animation
[CATransaction begin];
- [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
+ [CATransaction setDisableActions:YES];
CATransform3D t = CATransform3DRotate(CATransform3DIdentity, MGLRadiansFromDegrees(self.mapView.camera.pitch), 1.0, 0, 0);
self.layer.sublayerTransform = t;
@@ -138,7 +129,6 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
self.layer.sublayers = nil;
_headingIndicatorLayer = nil;
- _headingIndicatorMaskLayer = nil;
_accuracyRingLayer = nil;
_haloLayer = nil;
_dotBorderLayer = nil;
@@ -225,70 +215,67 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
[self updateFrameWithSize:MGLUserLocationAnnotationDotSize];
}
- BOOL showHeadingIndicator = self.mapView.userTrackingMode == MGLUserTrackingModeFollowWithHeading;
-
- // update heading indicator
+ // heading indicator (tinted, beam or arrow)
//
+ BOOL headingTrackingModeEnabled = self.mapView.userTrackingMode == MGLUserTrackingModeFollowWithHeading;
+ BOOL showHeadingIndicator = self.mapView.showsUserHeadingIndicator || headingTrackingModeEnabled;
+
if (showHeadingIndicator)
{
_headingIndicatorLayer.hidden = NO;
+ CLLocationDirection headingAccuracy = self.userLocation.heading.headingAccuracy;
- // heading indicator (tinted, semi-circle)
- //
- if ( ! _headingIndicatorLayer && self.userLocation.heading.headingAccuracy)
+ if (([_headingIndicatorLayer isMemberOfClass:[MGLUserLocationHeadingBeamLayer class]] && ! headingTrackingModeEnabled) ||
+ ([_headingIndicatorLayer isMemberOfClass:[MGLUserLocationHeadingArrowLayer class]] && headingTrackingModeEnabled))
{
- CGFloat headingIndicatorSize = MGLUserLocationAnnotationHaloSize;
-
- _headingIndicatorLayer = [CALayer layer];
- _headingIndicatorLayer.bounds = CGRectMake(0, 0, headingIndicatorSize, headingIndicatorSize);
- _headingIndicatorLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0);
- _headingIndicatorLayer.contents = (__bridge id)[[self headingIndicatorTintedGradientImage] CGImage];
- _headingIndicatorLayer.contentsGravity = kCAGravityBottom;
- _headingIndicatorLayer.contentsScale = [UIScreen mainScreen].scale;
- _headingIndicatorLayer.opacity = 0.4;
- _headingIndicatorLayer.shouldRasterize = YES;
- _headingIndicatorLayer.rasterizationScale = [UIScreen mainScreen].scale;
- _headingIndicatorLayer.drawsAsynchronously = YES;
-
- [self.layer insertSublayer:_headingIndicatorLayer below:_dotBorderLayer];
+ [_headingIndicatorLayer removeFromSuperlayer];
+ _headingIndicatorLayer = nil;
+ _oldHeadingAccuracy = -1;
}
- // heading indicator accuracy mask (fan-shaped)
- //
- if ( ! _headingIndicatorMaskLayer && self.userLocation.heading.headingAccuracy)
+ if ( ! _headingIndicatorLayer && headingAccuracy)
{
- _headingIndicatorMaskLayer = [CAShapeLayer layer];
- _headingIndicatorMaskLayer.frame = _headingIndicatorLayer.bounds;
- _headingIndicatorMaskLayer.path = [[self headingIndicatorClippingMask] CGPath];
-
- // apply the mask to the halo-radius-sized gradient layer
- _headingIndicatorLayer.mask = _headingIndicatorMaskLayer;
-
- _oldHeadingAccuracy = self.userLocation.heading.headingAccuracy;
-
+ if (headingTrackingModeEnabled)
+ {
+ _headingIndicatorLayer = [[MGLUserLocationHeadingBeamLayer alloc] initWithUserLocationAnnotationView:self];
+ [self.layer insertSublayer:_headingIndicatorLayer below:_dotBorderLayer];
+ }
+ else
+ {
+ _headingIndicatorLayer = [[MGLUserLocationHeadingArrowLayer alloc] initWithUserLocationAnnotationView:self];
+ [self.layer addSublayer:_headingIndicatorLayer];
+ _headingIndicatorLayer.zPosition = 1;
+ }
}
- else if (_oldHeadingAccuracy != self.userLocation.heading.headingAccuracy)
- {
- // recalculate the clipping mask based on updated accuracy
- _headingIndicatorMaskLayer.path = [[self headingIndicatorClippingMask] CGPath];
- _oldHeadingAccuracy = self.userLocation.heading.headingAccuracy;
+ if (_oldHeadingAccuracy != headingAccuracy)
+ {
+ [_headingIndicatorLayer updateHeadingAccuracy:headingAccuracy];
+ _oldHeadingAccuracy = headingAccuracy;
}
if (self.userLocation.heading.trueHeading >= 0)
{
- _headingIndicatorLayer.affineTransform = CGAffineTransformRotate(CGAffineTransformIdentity, -MGLRadiansFromDegrees(self.mapView.direction - self.userLocation.heading.trueHeading));
+ CGFloat rotation = -MGLRadiansFromDegrees(self.mapView.direction - self.userLocation.heading.trueHeading);
+
+ // Don't rotate if the change is imperceptible.
+ if (fabs(rotation) > MGLUserLocationHeadingUpdateThreshold)
+ {
+ [CATransaction begin];
+ [CATransaction setDisableActions:YES];
+
+ _headingIndicatorLayer.affineTransform = CGAffineTransformRotate(CGAffineTransformIdentity, rotation);
+
+ [CATransaction commit];
+ }
}
}
else
{
[_headingIndicatorLayer removeFromSuperlayer];
- [_headingIndicatorMaskLayer removeFromSuperlayer];
_headingIndicatorLayer = nil;
- _headingIndicatorMaskLayer = nil;
}
-
// update accuracy ring (if zoom or horizontal accuracy have changed)
//
if (_accuracyRingLayer && (_oldZoom != self.mapView.zoomLevel || _oldHorizontalAccuracy != self.userLocation.location.horizontalAccuracy))
@@ -301,10 +288,10 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
_accuracyRingLayer.hidden = NO;
// disable implicit animation of the accuracy ring, unless triggered by a change in accuracy
- id shouldDisableActions = (_oldHorizontalAccuracy == self.userLocation.location.horizontalAccuracy) ? (id)kCFBooleanTrue : (id)kCFBooleanFalse;
+ BOOL shouldDisableActions = _oldHorizontalAccuracy == self.userLocation.location.horizontalAccuracy;
[CATransaction begin];
- [CATransaction setValue:shouldDisableActions forKey:kCATransactionDisableActions];
+ [CATransaction setDisableActions:shouldDisableActions];
_accuracyRingLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize);
_accuracyRingLayer.cornerRadius = accuracyRingSize / 2;
@@ -464,65 +451,4 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
return self.userLocation.location.horizontalAccuracy / [self.mapView metersPerPointAtLatitude:self.userLocation.coordinate.latitude] * 2.0;
}
-- (UIImage *)headingIndicatorTintedGradientImage
-{
- UIImage *image;
-
- CGFloat haloRadius = MGLUserLocationAnnotationHaloSize / 2.0;
-
- UIGraphicsBeginImageContextWithOptions(CGSizeMake(MGLUserLocationAnnotationHaloSize, haloRadius), NO, 0);
-
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
- CGContextRef context = UIGraphicsGetCurrentContext();
-
- // gradient from the tint color to no-alpha tint color
- CGFloat gradientLocations[] = {0.0, 1.0};
- CGGradientRef gradient = CGGradientCreateWithColors(
- colorSpace,
- (__bridge CFArrayRef)@[
- (id)[self.mapView.tintColor CGColor],
- (id)[[self.mapView.tintColor colorWithAlphaComponent:0] CGColor]],
- gradientLocations);
-
- // draw the gradient from the center point to the edge (full halo radius)
- CGPoint centerPoint = CGPointMake(haloRadius, haloRadius);
- CGContextDrawRadialGradient(context, gradient,
- centerPoint, 0.0,
- centerPoint, haloRadius,
- kNilOptions);
-
- image = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
-
- CGGradientRelease(gradient);
- CGColorSpaceRelease(colorSpace);
-
- return image;
-}
-
-- (UIBezierPath *)headingIndicatorClippingMask
-{
- CGFloat accuracy = self.userLocation.heading.headingAccuracy;
-
- // size the mask using accuracy, but keep within a good display range
- CGFloat clippingDegrees = 90 - accuracy;
- clippingDegrees = fmin(clippingDegrees, 70); // most accurate
- clippingDegrees = fmax(clippingDegrees, 10); // least accurate
-
- CGRect ovalRect = CGRectMake(0, 0, MGLUserLocationAnnotationHaloSize, MGLUserLocationAnnotationHaloSize);
- UIBezierPath *ovalPath = UIBezierPath.bezierPath;
-
- // clip the oval to ± incoming accuracy degrees (converted to radians), from the top
- [ovalPath addArcWithCenter:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))
- radius:CGRectGetWidth(ovalRect) / 2.0
- startAngle:MGLRadiansFromDegrees(-180 + clippingDegrees)
- endAngle:MGLRadiansFromDegrees(-clippingDegrees)
- clockwise:YES];
-
- [ovalPath addLineToPoint:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))];
- [ovalPath closePath];
-
- return ovalPath;
-}
-
@end
diff --git a/platform/ios/src/MGLMapView+IBAdditions.h b/platform/ios/src/MGLMapView+IBAdditions.h
index 3766d044d8..d02c938c57 100644
--- a/platform/ios/src/MGLMapView+IBAdditions.h
+++ b/platform/ios/src/MGLMapView+IBAdditions.h
@@ -41,6 +41,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) IBInspectable BOOL allowsRotating;
@property (nonatomic) IBInspectable BOOL allowsTilting;
@property (nonatomic) IBInspectable BOOL showsUserLocation;
+@property (nonatomic) IBInspectable BOOL showsHeading;
@end
diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h
index c3ffe1983e..528606fd4e 100644
--- a/platform/ios/src/MGLMapView.h
+++ b/platform/ios/src/MGLMapView.h
@@ -370,6 +370,23 @@ MGL_EXPORT IB_DESIGNABLE
- (void)setUserLocationVerticalAlignment:(MGLAnnotationVerticalAlignment)alignment animated:(BOOL)animated;
/**
+ A Boolean value indicating whether the user location annotation may display a
+ permanent heading indicator.
+
+ Setting this property to `YES` causes the default user location annotation to
+ appear and always show an arrow-shaped heading indicator, if heading is
+ available. This property does not rotate the map; for that, see
+ `MGLUserTrackingModeFollowWithHeading`.
+
+ This property has no effect when `userTrackingMode` is
+ `MGLUserTrackingModeFollowWithHeading` or
+ `MGLUserTrackingModeFollowWithCourse`.
+
+ The default value of this property is `NO`.
+ */
+@property (nonatomic, assign) BOOL showsUserHeadingIndicator;
+
+/**
Whether the map view should display a heading calibration alert when necessary.
The default value is `YES`.
*/
@@ -589,7 +606,7 @@ MGL_EXPORT IB_DESIGNABLE
*
* The default minimumZoomLevel is 0.
*/
-@property (nonatomic) double minimumZoomLevel;
+@property (nonatomic) IBInspectable double minimumZoomLevel;
/**
* The maximum zoom level the map can be shown at.
@@ -600,7 +617,7 @@ MGL_EXPORT IB_DESIGNABLE
* The default maximumZoomLevel is 22. The upper bound for this property
* is 25.5.
*/
-@property (nonatomic) double maximumZoomLevel;
+@property (nonatomic) IBInspectable double maximumZoomLevel;
/**
The heading of the map, measured in degrees clockwise from true north.
@@ -1004,16 +1021,6 @@ MGL_EXPORT IB_DESIGNABLE
@property (nonatomic, readonly, nullable) NS_ARRAY_OF(id <MGLAnnotation>) *annotations;
/**
- The complete list of annotations associated with the receiver that are
- currently visible.
-
- The objects in this array must adopt the `MGLAnnotation` protocol. If no
- annotations are associated with the map view or if no annotations associated
- with the map view are currently visible, the value of this property is `nil`.
- */
-@property (nonatomic, readonly, nullable) NS_ARRAY_OF(id <MGLAnnotation>) *visibleAnnotations;
-
-/**
Adds an annotation to the map view.
@note `MGLMultiPolyline`, `MGLMultiPolygon`, `MGLShapeCollection`, and
@@ -1108,6 +1115,16 @@ MGL_EXPORT IB_DESIGNABLE
- (nullable __kindof MGLAnnotationView *)dequeueReusableAnnotationViewWithIdentifier:(NSString *)identifier;
/**
+ The complete list of annotations associated with the receiver that are
+ currently visible.
+
+ The objects in this array must adopt the `MGLAnnotation` protocol. If no
+ annotations are associated with the map view or if no annotations associated
+ with the map view are currently visible, the value of this property is `nil`.
+ */
+@property (nonatomic, readonly, nullable) NS_ARRAY_OF(id <MGLAnnotation>) *visibleAnnotations;
+
+/**
Returns the list of annotations associated with the receiver that intersect with
the given rectangle.
@@ -1276,6 +1293,11 @@ MGL_EXPORT IB_DESIGNABLE
`-[MGLVectorSource featuresInSourceLayersWithIdentifiers:predicate:]` and
`-[MGLShapeSource featuresMatchingPredicate:]` methods on the relevant sources.
+ The returned features may also include features corresponding to annotations.
+ These features are not object-equal to the `MGLAnnotation` objects that were
+ originally added to the map. To query the map for annotations, use
+ `visibleAnnotations` or `-[MGLMapView visibleAnnotationsInRect:]`.
+
@note Layer identifiers are not guaranteed to exist across styles or different
versions of the same style. Applications that use this API must first set
the style URL to an explicitly versioned style using a convenience method
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index 49ef2bfb81..a03a5ad357 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -138,9 +138,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;
@@ -242,10 +239,14 @@ 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;
@@ -302,8 +303,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;
@@ -483,21 +482,31 @@ 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;
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
+ if ([_logoView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _logoView.accessibilityIgnoresInvertColors = YES; }
+#endif
[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");
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
+ if ([_attributionButton respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _attributionButton.accessibilityIgnoresInvertColors = YES; }
+#endif
[_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];
// setup compass
@@ -509,12 +518,22 @@ 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;
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
+ if ([_compassView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _compassView.accessibilityIgnoresInvertColors = YES; }
+#endif
[self addSubview:_compassView];
+ _compassViewConstraints = [NSMutableArray array];
// setup scale control
//
_scaleBar = [[MGLScaleBar alloc] init];
+ _scaleBar.translatesAutoresizingMaskIntoConstraints = NO;
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
+ if ([_scaleBar respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _scaleBar.accessibilityIgnoresInvertColors = YES; }
+#endif
[self addSubview:_scaleBar];
+ _scaleBarConstraints = [NSMutableArray array];
// setup interaction
//
@@ -627,6 +646,9 @@ public:
_glView.contentScaleFactor = [UIScreen instancesRespondToSelector:@selector(nativeScale)] ? [[UIScreen mainScreen] nativeScale] : [[UIScreen mainScreen] scale];
_glView.layer.opaque = _opaque;
_glView.delegate = self;
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
+ if ([_glView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _glView.accessibilityIgnoresInvertColors = YES; }
+#endif
[_glView bindDrawable];
[self insertSubview:_glView atIndex:0];
_glView.contentMode = UIViewContentModeCenter;
@@ -674,48 +696,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];
@@ -724,8 +704,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)
@@ -751,6 +729,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
@@ -797,15 +787,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
@@ -835,8 +1017,6 @@ public:
[super layoutSubviews];
[self adjustContentInset];
-
- [self layoutOrnaments];
if (!_isTargetingInterfaceBuilder) {
_mbglMap->setSize([self size]);
@@ -844,44 +1024,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.
@@ -2043,10 +2194,6 @@ public:
[self updateCalloutView];
}
}
- else if (context == MGLLayoutGuidesUpdatedContext && [keyPath isEqualToString:@"bounds"])
- {
- [self setNeedsLayout];
- }
}
+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomEnabled
@@ -4146,33 +4293,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)
+ {
+ hasAlwaysUsageDescription = !![[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysAndWhenInUseUsageDescription"] && hasWhenInUseUsageDescription;
+ }
+ else
{
- [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:@"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 +4451,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 +4463,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 +4479,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 +4532,43 @@ 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];
@@ -4640,6 +4816,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];
@@ -4676,30 +4857,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;
+ }
}
}
@@ -5719,4 +5909,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
diff --git a/platform/ios/src/MGLUserLocation.h b/platform/ios/src/MGLUserLocation.h
index 8c6fe46136..91abadbcb7 100644
--- a/platform/ios/src/MGLUserLocation.h
+++ b/platform/ios/src/MGLUserLocation.h
@@ -20,8 +20,7 @@ MGL_EXPORT
/**
The current location of the device. (read-only)
- This property contains `nil` if the map view is not currently showing the user
- location or if the user’s location has not yet been determined.
+ This property returns `nil` if the user’s location has not yet been determined.
*/
@property (nonatomic, readonly, nullable) CLLocation *location;
diff --git a/platform/ios/src/MGLUserLocation.m b/platform/ios/src/MGLUserLocation.m
index 1c9649c09e..074d138a72 100644
--- a/platform/ios/src/MGLUserLocation.m
+++ b/platform/ios/src/MGLUserLocation.m
@@ -19,7 +19,6 @@ NS_ASSUME_NONNULL_END
{
if (self = [super init])
{
- _location = [[CLLocation alloc] initWithLatitude:MAXFLOAT longitude:MAXFLOAT];
_mapView = mapView;
}
@@ -102,7 +101,7 @@ NS_ASSUME_NONNULL_END
- (CLLocationCoordinate2D)coordinate
{
- return self.location.coordinate;
+ return _location ? _location.coordinate : kCLLocationCoordinate2DInvalid;
}
- (NSString *)title
diff --git a/platform/ios/src/MGLUserLocationHeadingArrowLayer.h b/platform/ios/src/MGLUserLocationHeadingArrowLayer.h
new file mode 100644
index 0000000000..6c01356944
--- /dev/null
+++ b/platform/ios/src/MGLUserLocationHeadingArrowLayer.h
@@ -0,0 +1,11 @@
+#import <QuartzCore/QuartzCore.h>
+#import "MGLUserLocationAnnotationView.h"
+#import "MGLUserLocationHeadingIndicator.h"
+
+@interface MGLUserLocationHeadingArrowLayer : CAShapeLayer <MGLUserLocationHeadingIndicator>
+
+- (instancetype)initWithUserLocationAnnotationView:(MGLUserLocationAnnotationView *)userLocationView;
+- (void)updateHeadingAccuracy:(CLLocationDirection)accuracy;
+- (void)updateTintColor:(CGColorRef)color;
+
+@end
diff --git a/platform/ios/src/MGLUserLocationHeadingArrowLayer.m b/platform/ios/src/MGLUserLocationHeadingArrowLayer.m
new file mode 100644
index 0000000000..912ce30c35
--- /dev/null
+++ b/platform/ios/src/MGLUserLocationHeadingArrowLayer.m
@@ -0,0 +1,59 @@
+#import "MGLUserLocationHeadingArrowLayer.h"
+
+#import "MGLFaux3DUserLocationAnnotationView.h"
+#import "MGLGeometry.h"
+
+const CGFloat MGLUserLocationHeadingArrowSize = 6;
+
+@implementation MGLUserLocationHeadingArrowLayer
+
+- (instancetype)initWithUserLocationAnnotationView:(MGLUserLocationAnnotationView *)userLocationView
+{
+ CGFloat size = userLocationView.bounds.size.width + MGLUserLocationHeadingArrowSize;
+
+ self = [super init];
+ self.bounds = CGRectMake(0, 0, size, size);
+ self.position = CGPointMake(CGRectGetMidX(userLocationView.bounds), CGRectGetMidY(userLocationView.bounds));
+ self.path = [self arrowPath];
+ self.fillColor = userLocationView.tintColor.CGColor;
+ self.shouldRasterize = YES;
+ self.rasterizationScale = UIScreen.mainScreen.scale;
+ self.drawsAsynchronously = YES;
+
+ self.strokeColor = UIColor.whiteColor.CGColor;
+ self.lineWidth = 1.0;
+ self.lineJoin = kCALineJoinRound;
+
+ return self;
+}
+
+- (void)updateHeadingAccuracy:(CLLocationDirection)accuracy
+{
+ // unimplemented
+}
+
+- (void)updateTintColor:(CGColorRef)color
+{
+ self.fillColor = color;
+}
+
+- (CGPathRef)arrowPath {
+ CGFloat center = roundf(CGRectGetMidX(self.bounds));
+ CGFloat size = MGLUserLocationHeadingArrowSize;
+
+ CGPoint top = CGPointMake(center, 0);
+ CGPoint left = CGPointMake(center - size, size);
+ CGPoint right = CGPointMake(center + size, size);
+ CGPoint middle = CGPointMake(center, size / M_PI);
+
+ UIBezierPath *bezierPath = [UIBezierPath bezierPath];
+ [bezierPath moveToPoint:top];
+ [bezierPath addLineToPoint:left];
+ [bezierPath addQuadCurveToPoint:right controlPoint:middle];
+ [bezierPath addLineToPoint:top];
+ [bezierPath closePath];
+
+ return bezierPath.CGPath;
+}
+
+@end
diff --git a/platform/ios/src/MGLUserLocationHeadingBeamLayer.h b/platform/ios/src/MGLUserLocationHeadingBeamLayer.h
new file mode 100644
index 0000000000..93f8ea17ab
--- /dev/null
+++ b/platform/ios/src/MGLUserLocationHeadingBeamLayer.h
@@ -0,0 +1,11 @@
+#import <QuartzCore/QuartzCore.h>
+#import "MGLUserLocationAnnotationView.h"
+#import "MGLUserLocationHeadingIndicator.h"
+
+@interface MGLUserLocationHeadingBeamLayer : CALayer <MGLUserLocationHeadingIndicator>
+
+- (MGLUserLocationHeadingBeamLayer *)initWithUserLocationAnnotationView:(MGLUserLocationAnnotationView *)userLocationView;
+- (void)updateHeadingAccuracy:(CLLocationDirection)accuracy;
+- (void)updateTintColor:(CGColorRef)color;
+
+@end
diff --git a/platform/ios/src/MGLUserLocationHeadingBeamLayer.m b/platform/ios/src/MGLUserLocationHeadingBeamLayer.m
new file mode 100644
index 0000000000..efe7e4db93
--- /dev/null
+++ b/platform/ios/src/MGLUserLocationHeadingBeamLayer.m
@@ -0,0 +1,104 @@
+#import "MGLUserLocationHeadingBeamLayer.h"
+
+#import "MGLFaux3DUserLocationAnnotationView.h"
+#import "MGLGeometry.h"
+
+@implementation MGLUserLocationHeadingBeamLayer
+{
+ CAShapeLayer *_maskLayer;
+}
+
+- (instancetype)initWithUserLocationAnnotationView:(MGLUserLocationAnnotationView *)userLocationView
+{
+ CGFloat size = MGLUserLocationAnnotationHaloSize;
+
+ self = [super init];
+ self.bounds = CGRectMake(0, 0, size, size);
+ self.position = CGPointMake(CGRectGetMidX(userLocationView.bounds), CGRectGetMidY(userLocationView.bounds));
+ self.contents = (__bridge id)[self gradientImageWithTintColor:userLocationView.tintColor.CGColor];
+ self.contentsGravity = kCAGravityBottom;
+ self.contentsScale = UIScreen.mainScreen.scale;
+ self.opacity = 0.4;
+ self.shouldRasterize = YES;
+ self.rasterizationScale = UIScreen.mainScreen.scale;
+ self.drawsAsynchronously = YES;
+
+ _maskLayer = [CAShapeLayer layer];
+ _maskLayer.frame = self.bounds;
+ _maskLayer.path = [self clippingMaskForAccuracy:0];
+ self.mask = _maskLayer;
+
+ return self;
+}
+
+- (void)updateHeadingAccuracy:(CLLocationDirection)accuracy
+{
+ // recalculate the clipping mask based on updated accuracy
+ _maskLayer.path = [self clippingMaskForAccuracy:accuracy];
+}
+
+- (void)updateTintColor:(CGColorRef)color
+{
+ // redraw the raw tinted gradient
+ self.contents = (__bridge id)[self gradientImageWithTintColor:color];
+}
+
+- (CGImageRef)gradientImageWithTintColor:(CGColorRef)tintColor
+{
+ UIImage *image;
+
+ CGFloat haloRadius = MGLUserLocationAnnotationHaloSize / 2.0;
+
+ UIGraphicsBeginImageContextWithOptions(CGSizeMake(MGLUserLocationAnnotationHaloSize, haloRadius), NO, 0);
+
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
+ CGContextRef context = UIGraphicsGetCurrentContext();
+
+ // gradient from the tint color to no-alpha tint color
+ CGFloat gradientLocations[] = {0.0, 1.0};
+ CGGradientRef gradient = CGGradientCreateWithColors(
+ colorSpace,
+ (__bridge CFArrayRef)@[(__bridge id)tintColor,
+ (id)CFBridgingRelease(CGColorCreateCopyWithAlpha(tintColor, 0))],
+ gradientLocations);
+
+ // draw the gradient from the center point to the edge (full halo radius)
+ CGPoint centerPoint = CGPointMake(haloRadius, haloRadius);
+ CGContextDrawRadialGradient(context, gradient,
+ centerPoint, 0.0,
+ centerPoint, haloRadius,
+ kNilOptions);
+
+ image = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ CGGradientRelease(gradient);
+ CGColorSpaceRelease(colorSpace);
+
+ return image.CGImage;
+}
+
+- (CGPathRef)clippingMaskForAccuracy:(CGFloat)accuracy
+{
+ // size the mask using accuracy, but keep within a good display range
+ CGFloat clippingDegrees = 90 - accuracy;
+ clippingDegrees = fmin(clippingDegrees, 70); // most accurate
+ clippingDegrees = fmax(clippingDegrees, 10); // least accurate
+
+ CGRect ovalRect = CGRectMake(0, 0, MGLUserLocationAnnotationHaloSize, MGLUserLocationAnnotationHaloSize);
+ UIBezierPath *ovalPath = UIBezierPath.bezierPath;
+
+ // clip the oval to ± incoming accuracy degrees (converted to radians), from the top
+ [ovalPath addArcWithCenter:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))
+ radius:CGRectGetWidth(ovalRect) / 2.0
+ startAngle:MGLRadiansFromDegrees(-180 + clippingDegrees)
+ endAngle:MGLRadiansFromDegrees(-clippingDegrees)
+ clockwise:YES];
+
+ [ovalPath addLineToPoint:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))];
+ [ovalPath closePath];
+
+ return ovalPath.CGPath;
+}
+
+@end
diff --git a/platform/ios/src/MGLUserLocationHeadingIndicator.h b/platform/ios/src/MGLUserLocationHeadingIndicator.h
new file mode 100644
index 0000000000..61476b96a2
--- /dev/null
+++ b/platform/ios/src/MGLUserLocationHeadingIndicator.h
@@ -0,0 +1,10 @@
+#import <QuartzCore/QuartzCore.h>
+#import "MGLUserLocationAnnotationView.h"
+
+@protocol MGLUserLocationHeadingIndicator <NSObject>
+
+- (instancetype)initWithUserLocationAnnotationView:(MGLUserLocationAnnotationView *)userLocationView;
+- (void)updateHeadingAccuracy:(CLLocationDirection)accuracy;
+- (void)updateTintColor:(CGColorRef)color;
+
+@end