summaryrefslogtreecommitdiff
path: root/platform/ios/src
diff options
context:
space:
mode:
Diffstat (limited to 'platform/ios/src')
-rw-r--r--platform/ios/src/MGLFaux3DUserLocationAnnotationView.h7
-rw-r--r--platform/ios/src/MGLFaux3DUserLocationAnnotationView.m510
-rw-r--r--platform/ios/src/MGLMapView.mm52
-rw-r--r--platform/ios/src/MGLUserLocationAnnotationView.h52
-rw-r--r--platform/ios/src/MGLUserLocationAnnotationView.m543
-rw-r--r--platform/ios/src/MGLUserLocationAnnotationView_Private.h15
-rw-r--r--platform/ios/src/Mapbox.h1
7 files changed, 636 insertions, 544 deletions
diff --git a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.h b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.h
new file mode 100644
index 0000000000..c48dd6b27b
--- /dev/null
+++ b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.h
@@ -0,0 +1,7 @@
+#import <UIKit/UIKit.h>
+#import "MGLUserLocationAnnotationView.h"
+
+@interface MGLFaux3DUserLocationAnnotationView : MGLUserLocationAnnotationView
+
+@end
+
diff --git a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m
new file mode 100644
index 0000000000..ac0551430b
--- /dev/null
+++ b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m
@@ -0,0 +1,510 @@
+#import "MGLFaux3DUserLocationAnnotationView.h"
+
+#import "MGLMapView.h"
+
+const CGFloat MGLUserLocationAnnotationDotSize = 22.0;
+const CGFloat MGLUserLocationAnnotationHaloSize = 115.0;
+
+const CGFloat MGLUserLocationAnnotationPuckSize = 45.0;
+const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.6;
+
+#pragma mark -
+
+@implementation MGLFaux3DUserLocationAnnotationView
+{
+ BOOL _puckModeActivated;
+
+ CALayer *_puckDot;
+ CAShapeLayer *_puckArrow;
+
+ CALayer *_headingIndicatorLayer;
+ CAShapeLayer *_headingIndicatorMaskLayer;
+ CALayer *_accuracyRingLayer;
+ CALayer *_dotBorderLayer;
+ CALayer *_dotLayer;
+ CALayer *_haloLayer;
+
+ double _oldHeadingAccuracy;
+ CLLocationAccuracy _oldHorizontalAccuracy;
+ double _oldZoom;
+ double _oldPitch;
+}
+
+- (CALayer *)hitTestLayer
+{
+ // Only the main dot should be interactive (i.e., exclude the accuracy ring and halo).
+ return _dotBorderLayer ?: _puckDot;
+}
+
+- (void)update
+{
+ if (CGSizeEqualToSize(self.frame.size, CGSizeZero))
+ {
+ CGFloat frameSize = (self.mapView.userTrackingMode == MGLUserTrackingModeFollowWithCourse) ? MGLUserLocationAnnotationPuckSize : MGLUserLocationAnnotationDotSize;
+ [self updateFrameWithSize:frameSize];
+ }
+
+ if (CLLocationCoordinate2DIsValid(self.userLocation.coordinate))
+ {
+ (self.mapView.userTrackingMode == MGLUserTrackingModeFollowWithCourse) ? [self drawPuck] : [self drawDot];
+ [self updatePitch];
+ }
+
+ _haloLayer.hidden = ! CLLocationCoordinate2DIsValid(self.mapView.userLocation.coordinate) || self.mapView.userLocation.location.horizontalAccuracy > 10;
+}
+
+- (void)setTintColor:(UIColor *)tintColor
+{
+ if (_puckModeActivated)
+ {
+ _puckArrow.fillColor = [tintColor CGColor];
+ }
+ else
+ {
+ if (_accuracyRingLayer)
+ {
+ _accuracyRingLayer.backgroundColor = [tintColor CGColor];
+ }
+
+ _haloLayer.backgroundColor = [tintColor CGColor];
+ _dotLayer.backgroundColor = [tintColor CGColor];
+
+ _headingIndicatorLayer.contents = (__bridge id)[[self headingIndicatorTintedGradientImage] CGImage];
+ }
+}
+
+- (void)updatePitch
+{
+ if (self.mapView.camera.pitch != _oldPitch)
+ {
+ CATransform3D t = CATransform3DRotate(CATransform3DIdentity, MGLRadiansFromDegrees(self.mapView.camera.pitch), 1.0, 0, 0);
+ self.layer.sublayerTransform = t;
+
+ [self updateFaux3DEffect];
+
+ _oldPitch = self.mapView.camera.pitch;
+ }
+}
+
+- (void)updateFaux3DEffect
+{
+ CGFloat pitch = MGLRadiansFromDegrees(self.mapView.camera.pitch);
+
+ if (_puckDot)
+ {
+ _puckDot.shadowOffset = CGSizeMake(0, fmaxf(pitch * 10.f, 1.f));
+ _puckDot.shadowRadius = fmaxf(pitch * 5.f, 0.75f);
+ }
+
+ if (_dotBorderLayer)
+ {
+ _dotBorderLayer.shadowOffset = CGSizeMake(0.f, pitch * 10.f);
+ _dotBorderLayer.shadowRadius = fmaxf(pitch * 5.f, 3.f);
+ }
+
+ if (_dotLayer)
+ {
+ _dotLayer.zPosition = pitch * 2.f;
+ }
+}
+
+- (void)updateFrameWithSize:(CGFloat)size
+{
+ CGSize newSize = CGSizeMake(size, size);
+ if (CGSizeEqualToSize(self.frame.size, newSize))
+ {
+ return;
+ }
+
+ // Update frame size, keeping the existing center point.
+ CGPoint oldCenter = self.center;
+ CGRect newFrame = self.frame;
+ newFrame.size = newSize;
+ [self setFrame:newFrame];
+ [self setCenter:oldCenter];
+}
+
+- (void)drawPuck
+{
+ if ( ! _puckModeActivated)
+ {
+ self.layer.sublayers = nil;
+
+ _headingIndicatorLayer = nil;
+ _headingIndicatorMaskLayer = nil;
+ _accuracyRingLayer = nil;
+ _haloLayer = nil;
+ _dotBorderLayer = nil;
+ _dotLayer = nil;
+
+ [self updateFrameWithSize:MGLUserLocationAnnotationPuckSize];
+ }
+
+ // background dot (white with black shadow)
+ //
+ if ( ! _puckDot)
+ {
+ _puckDot = [self circleLayerWithSize:MGLUserLocationAnnotationPuckSize];
+ _puckDot.backgroundColor = [[UIColor whiteColor] CGColor];
+ _puckDot.shadowColor = [[UIColor blackColor] CGColor];
+ _puckDot.shadowOpacity = 0.25;
+
+ if (self.mapView.camera.pitch)
+ {
+ [self updateFaux3DEffect];
+ }
+ else
+ {
+ _puckDot.shadowOffset = CGSizeMake(0, 1);
+ _puckDot.shadowRadius = 0.75;
+ }
+
+ [self.layer addSublayer:_puckDot];
+ }
+
+ // arrow
+ //
+ if ( ! _puckArrow)
+ {
+ _puckArrow = [CAShapeLayer layer];
+ _puckArrow.path = [[self puckArrow] CGPath];
+ _puckArrow.fillColor = [self.mapView.tintColor CGColor];
+ _puckArrow.bounds = CGRectMake(0, 0, MGLUserLocationAnnotationArrowSize, MGLUserLocationAnnotationArrowSize);
+ _puckArrow.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0);
+ _puckArrow.shouldRasterize = YES;
+ _puckArrow.rasterizationScale = [UIScreen mainScreen].scale;
+ _puckArrow.drawsAsynchronously = YES;
+
+ [self.layer addSublayer:_puckArrow];
+ }
+ if (self.userLocation.location.course >= 0)
+ {
+ _puckArrow.affineTransform = CGAffineTransformRotate(CGAffineTransformIdentity, -MGLRadiansFromDegrees(self.mapView.direction - self.userLocation.location.course));
+ }
+
+ if ( ! _puckModeActivated)
+ {
+ _puckModeActivated = YES;
+
+ [self updateFaux3DEffect];
+ }
+}
+
+- (UIBezierPath *)puckArrow
+{
+ CGFloat max = MGLUserLocationAnnotationArrowSize;
+
+ UIBezierPath *bezierPath = UIBezierPath.bezierPath;
+ [bezierPath moveToPoint: CGPointMake(max * 0.5, 0)];
+ [bezierPath addLineToPoint: CGPointMake(max * 0.1, max)];
+ [bezierPath addLineToPoint: CGPointMake(max * 0.5, max * 0.65)];
+ [bezierPath addLineToPoint: CGPointMake(max * 0.9, max)];
+ [bezierPath addLineToPoint: CGPointMake(max * 0.5, 0)];
+ [bezierPath closePath];
+
+ return bezierPath;
+}
+
+- (void)drawDot
+{
+ if (_puckModeActivated)
+ {
+ self.layer.sublayers = nil;
+
+ _puckDot = nil;
+ _puckArrow = nil;
+
+ [self updateFrameWithSize:MGLUserLocationAnnotationDotSize];
+ }
+
+ BOOL showHeadingIndicator = self.mapView.userTrackingMode == MGLUserTrackingModeFollowWithHeading;
+
+ // update heading indicator
+ //
+ if (showHeadingIndicator)
+ {
+ _headingIndicatorLayer.hidden = NO;
+
+ // heading indicator (tinted, semi-circle)
+ //
+ if ( ! _headingIndicatorLayer && self.userLocation.heading.headingAccuracy)
+ {
+ 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];
+ }
+
+ // heading indicator accuracy mask (fan-shaped)
+ //
+ if ( ! _headingIndicatorMaskLayer && self.userLocation.heading.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;
+
+ }
+ 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 (self.userLocation.heading.trueHeading >= 0)
+ {
+ _headingIndicatorLayer.affineTransform = CGAffineTransformRotate(CGAffineTransformIdentity, -MGLRadiansFromDegrees(self.mapView.direction - self.userLocation.heading.trueHeading));
+ }
+ }
+ 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))
+ {
+ CGFloat accuracyRingSize = [self calculateAccuracyRingSize];
+
+ // only show the accuracy ring if it won't be obscured by the location dot
+ if (accuracyRingSize > MGLUserLocationAnnotationDotSize + 15)
+ {
+ _accuracyRingLayer.hidden = NO;
+ _accuracyRingLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize);
+ _accuracyRingLayer.cornerRadius = accuracyRingSize / 2;
+
+ // match the halo to the accuracy ring
+ _haloLayer.bounds = _accuracyRingLayer.bounds;
+ _haloLayer.cornerRadius = _accuracyRingLayer.cornerRadius;
+ _haloLayer.shouldRasterize = NO;
+ }
+ else
+ {
+ _accuracyRingLayer.hidden = YES;
+
+ _haloLayer.bounds = CGRectMake(0, 0, MGLUserLocationAnnotationHaloSize, MGLUserLocationAnnotationHaloSize);
+ _haloLayer.cornerRadius = MGLUserLocationAnnotationHaloSize / 2.0;
+ _haloLayer.shouldRasterize = YES;
+ _haloLayer.rasterizationScale = [UIScreen mainScreen].scale;
+ }
+
+ // store accuracy and zoom so we're not redrawing unchanged location updates
+ _oldHorizontalAccuracy = self.userLocation.location.horizontalAccuracy;
+ _oldZoom = self.mapView.zoomLevel;
+ }
+
+ // accuracy ring (circular, tinted, mostly-transparent)
+ //
+ if ( ! _accuracyRingLayer && self.userLocation.location.horizontalAccuracy)
+ {
+ CGFloat accuracyRingSize = [self calculateAccuracyRingSize];
+ _accuracyRingLayer = [self circleLayerWithSize:accuracyRingSize];
+ _accuracyRingLayer.backgroundColor = [self.mapView.tintColor CGColor];
+ _accuracyRingLayer.opacity = 0.1;
+ _accuracyRingLayer.shouldRasterize = NO;
+ _accuracyRingLayer.allowsGroupOpacity = NO;
+
+ [self.layer addSublayer:_accuracyRingLayer];
+ }
+
+ // expanding sonar-like pulse (circular, tinted, fades out)
+ //
+ if ( ! _haloLayer)
+ {
+ _haloLayer = [self circleLayerWithSize:MGLUserLocationAnnotationHaloSize];
+ _haloLayer.backgroundColor = [self.mapView.tintColor CGColor];
+ _haloLayer.allowsGroupOpacity = NO;
+ _haloLayer.zPosition = -0.1f;
+
+ // set defaults for the animations
+ CAAnimationGroup *animationGroup = [self loopingAnimationGroupWithDuration:3.0];
+
+ // scale out radially with initial acceleration
+ CAKeyframeAnimation *boundsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.xy"];
+ boundsAnimation.values = @[@0, @0.35, @1];
+ boundsAnimation.keyTimes = @[@0, @0.2, @1];
+
+ // go transparent as scaled out, start semi-opaque
+ CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
+ opacityAnimation.values = @[@0.4, @0.4, @0];
+ opacityAnimation.keyTimes = @[@0, @0.2, @1];
+
+ animationGroup.animations = @[boundsAnimation, opacityAnimation];
+
+ [_haloLayer addAnimation:animationGroup forKey:@"animateTransformAndOpacity"];
+
+ [self.layer addSublayer:_haloLayer];
+ }
+
+ // background dot (white with black shadow)
+ //
+ if ( ! _dotBorderLayer)
+ {
+ _dotBorderLayer = [self circleLayerWithSize:MGLUserLocationAnnotationDotSize];
+ _dotBorderLayer.backgroundColor = [[UIColor whiteColor] CGColor];
+ _dotBorderLayer.shadowColor = [[UIColor blackColor] CGColor];
+ _dotBorderLayer.shadowOpacity = 0.25;
+
+ if (self.mapView.camera.pitch)
+ {
+ [self updateFaux3DEffect];
+ }
+ else
+ {
+ _dotBorderLayer.shadowOffset = CGSizeMake(0, 0);
+ _dotBorderLayer.shadowRadius = 3;
+ }
+
+ [self.layer addSublayer:_dotBorderLayer];
+ }
+
+ // inner dot (pulsing, tinted)
+ //
+ if ( ! _dotLayer)
+ {
+ _dotLayer = [self circleLayerWithSize:MGLUserLocationAnnotationDotSize * 0.75];
+ _dotLayer.backgroundColor = [self.mapView.tintColor CGColor];
+ _dotLayer.shouldRasterize = NO;
+
+ // set defaults for the animations
+ CAAnimationGroup *animationGroup = [self loopingAnimationGroupWithDuration:1.5];
+ animationGroup.autoreverses = YES;
+ animationGroup.fillMode = kCAFillModeBoth;
+
+ // scale the dot up and down
+ CABasicAnimation *pulseAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale.xy"];
+ pulseAnimation.fromValue = @0.8;
+ pulseAnimation.toValue = @1;
+
+ // fade opacity in and out, subtly
+ CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ opacityAnimation.fromValue = @0.8;
+ opacityAnimation.toValue = @1;
+
+ animationGroup.animations = @[pulseAnimation, opacityAnimation];
+
+ [_dotLayer addAnimation:animationGroup forKey:@"animateTransformAndOpacity"];
+
+ [self.layer addSublayer:_dotLayer];
+ }
+
+ if (_puckModeActivated)
+ {
+ _puckModeActivated = NO;
+
+ [self updateFaux3DEffect];
+ }
+}
+
+- (CALayer *)circleLayerWithSize:(CGFloat)layerSize
+{
+ CALayer *circleLayer = [CALayer layer];
+ circleLayer.bounds = CGRectMake(0, 0, layerSize, layerSize);
+ circleLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0);
+ circleLayer.cornerRadius = layerSize / 2.0;
+ circleLayer.shouldRasterize = YES;
+ circleLayer.rasterizationScale = [UIScreen mainScreen].scale;
+ circleLayer.drawsAsynchronously = YES;
+
+ return circleLayer;
+}
+
+- (CAAnimationGroup *)loopingAnimationGroupWithDuration:(CGFloat)animationDuration
+{
+ CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
+ animationGroup.duration = animationDuration;
+ animationGroup.repeatCount = INFINITY;
+ animationGroup.removedOnCompletion = NO;
+ animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
+
+ return animationGroup;
+}
+
+- (CGFloat)calculateAccuracyRingSize
+{
+ CGFloat latRadians = self.userLocation.coordinate.latitude * M_PI / 180.0f;
+ CGFloat pixelRadius = self.userLocation.location.horizontalAccuracy / cos(latRadians) / [self.mapView metersPerPointAtLatitude:self.userLocation.coordinate.latitude];
+
+ return pixelRadius * 2;
+}
+
+- (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 exagerated accuracy, but keep within a good display range
+ CGFloat clippingDegrees = 90 - (accuracy * 1.5);
+ clippingDegrees = fmin(clippingDegrees, 55);
+ clippingDegrees = fmax(clippingDegrees, 10);
+
+ 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:(-180 + clippingDegrees) * M_PI / 180
+ endAngle:-clippingDegrees * M_PI / 180
+ clockwise:YES];
+
+ [ovalPath addLineToPoint:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))];
+ [ovalPath closePath];
+
+ return ovalPath;
+}
+
+@end
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index 766e7e64f0..0eadba973c 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -37,7 +37,10 @@
#import "NSException+MGLAdditions.h"
#import "UIColor+MGLAdditions.hpp"
#import "NSURL+MGLAdditions.h"
+
+#import "MGLFaux3DUserLocationAnnotationView.h"
#import "MGLUserLocationAnnotationView.h"
+#import "MGLUserLocationAnnotationView_Private.h"
#import "MGLUserLocation_Private.h"
#import "MGLAnnotationImage_Private.h"
#import "MGLAnnotationView_Private.h"
@@ -240,6 +243,7 @@ public:
@property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed;
@property (nonatomic) MGLMapViewProxyAccessibilityElement *mapViewProxyAccessibilityElement;
@property (nonatomic) MGLAnnotationContainerView *annotationContainerView;
+@property (nonatomic) MGLUserLocation *userLocation;
@end
@@ -1388,10 +1392,23 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
return;
}
+ CGPoint tapPoint = [singleTap locationInView:self];
+
if (self.userLocationVisible)
{
- CGPoint tapPointForUserLocation = [singleTap locationInView:self.userLocationAnnotationView];
+ CGPoint tapPointForUserLocation;
+ if (self.userLocationAnnotationView.hitTestLayer == self.userLocationAnnotationView.layer.presentationLayer)
+ {
+ tapPointForUserLocation = tapPoint;
+ }
+ else
+ {
+ // Get the tap point within the custom hit test layer.
+ tapPointForUserLocation = [singleTap locationInView:self.userLocationAnnotationView];
+ }
+
CALayer *hitLayer = [self.userLocationAnnotationView.hitTestLayer hitTest:tapPointForUserLocation];
+
if (hitLayer)
{
if ( ! _userLocationAnnotationIsSelected)
@@ -1402,8 +1419,6 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
}
}
- CGPoint tapPoint = [singleTap locationInView:self];
-
// Handle the case of an offset annotation view by converting the tap point to be the geo location
// of the annotation itself that the view represents
for (MGLAnnotationView *view in self.annotationContainerView.annotationViews)
@@ -3825,8 +3840,25 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
{
[self.delegate mapViewWillStartLocatingUser:self];
}
-
- self.userLocationAnnotationView = [[MGLUserLocationAnnotationView alloc] initInMapView:self];
+
+ self.userLocation = [[MGLUserLocation alloc] initWithMapView:self];
+
+ MGLUserLocationAnnotationView *userLocationAnnotationView;
+
+ if ([self.delegate respondsToSelector:@selector(mapView:viewForAnnotation:)])
+ {
+ userLocationAnnotationView = (MGLUserLocationAnnotationView *)[self.delegate mapView:self viewForAnnotation:self.userLocation];
+ if (userLocationAnnotationView)
+ {
+ NSAssert([userLocationAnnotationView.class isSubclassOfClass:MGLUserLocationAnnotationView.class],
+ @"User location annotation view must be a subclass of MGLUserLocationAnnotationView");
+ }
+ }
+
+ self.userLocationAnnotationView = userLocationAnnotationView ?: [[MGLFaux3DUserLocationAnnotationView alloc] init];
+ self.userLocationAnnotationView.mapView = self;
+ self.userLocationAnnotationView.userLocation = self.userLocation;
+
self.userLocationAnnotationView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
@@ -3862,11 +3894,6 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
return [NSSet setWithObject:@"userLocationAnnotationView"];
}
-- (nullable MGLUserLocation *)userLocation
-{
- return self.userLocationAnnotationView.annotation;
-}
-
- (BOOL)isUserLocationVisible
{
if (self.userLocationAnnotationView)
@@ -4035,9 +4062,6 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
[self didUpdateLocationWithUserTrackingAnimated:animated];
- self.userLocationAnnotationView.haloLayer.hidden = ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate) ||
- newLocation.horizontalAccuracy > 10;
-
NSTimeInterval duration = MGLAnimationDuration;
if (oldLocation && ! CGPointEqualToPoint(self.userLocationAnnotationView.center, CGPointZero))
{
@@ -4676,7 +4700,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
_userLocationAnimationCompletionDate = [NSDate dateWithTimeIntervalSinceNow:duration];
annotationView.hidden = NO;
- [annotationView setupLayers];
+ [annotationView update];
if (_userLocationAnnotationIsSelected)
{
diff --git a/platform/ios/src/MGLUserLocationAnnotationView.h b/platform/ios/src/MGLUserLocationAnnotationView.h
index f6b62f2b23..5e0a805f3a 100644
--- a/platform/ios/src/MGLUserLocationAnnotationView.h
+++ b/platform/ios/src/MGLUserLocationAnnotationView.h
@@ -1,5 +1,6 @@
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
+#import <Mapbox/Mapbox.h>
#import "MGLTypes.h"
@@ -9,15 +10,48 @@ NS_ASSUME_NONNULL_BEGIN
@class MGLUserLocation;
/** View representing an `MGLUserLocation` on screen. */
-@interface MGLUserLocationAnnotationView : UIView
-
-@property (nonatomic, weak) MGLMapView *mapView;
-@property (nonatomic) MGLUserLocation *annotation;
-@property (nonatomic, readonly, nullable) CALayer *haloLayer;
-@property (nonatomic, readonly) CALayer *hitTestLayer;
-
-- (instancetype)initInMapView:(MGLMapView *)mapView NS_DESIGNATED_INITIALIZER;
-- (void)setupLayers;
+@interface MGLUserLocationAnnotationView : MGLAnnotationView
+
+/**
+ Returns the associated map view.
+
+ The value of this property is nil during initialization.
+ */
+@property (nonatomic, readonly, weak, nullable) MGLMapView *mapView;
+
+/**
+ Returns the annotation object indicating the user’s current location.
+
+ The value of this property is nil during initialization and while user tracking
+ is inactive.
+ */
+@property (nonatomic, readonly, weak, nullable) MGLUserLocation *userLocation;
+
+/**
+ Returns the layer that should be used for annotation selection hit testing.
+
+ The default value of this property is the presentation layer of the view’s Core
+ Animation layer. When subclassing, you may override this property to specify a
+ different layer to be used for hit testing. This can be useful when you wish to
+ limit the interactive area of the annotation to a specific sublayer.
+ */
+@property (nonatomic, readonly, weak) CALayer *hitTestLayer;
+
+/**
+ Updates the user location annotation.
+
+ Use this method to update the appearance of the user location annotation. This
+ method is called by the associated map view when it has determined that the
+ user location annotation needs to be updated. This can happen in response to
+ user interaction, a change in the user’s location, when the user tracking mode
+ changes, or when the viewport changes.
+
+ @note During user interaction with the map, this method may be called many
+ times to update the user location annotation. Therefore, your implementation of
+ this method should be as lightweight as possible to avoid negatively affecting
+ performance.
+ */
+- (void)update;
@end
diff --git a/platform/ios/src/MGLUserLocationAnnotationView.m b/platform/ios/src/MGLUserLocationAnnotationView.m
index 3ab2c5a796..cda2695315 100644
--- a/platform/ios/src/MGLUserLocationAnnotationView.m
+++ b/platform/ios/src/MGLUserLocationAnnotationView.m
@@ -2,73 +2,43 @@
#import "MGLUserLocation.h"
#import "MGLUserLocation_Private.h"
+#import "MGLAnnotationView_Private.h"
#import "MGLAnnotation.h"
#import "MGLMapView.h"
#import "MGLCoordinateFormatter.h"
#import "NSBundle+MGLAdditions.h"
-const CGFloat MGLUserLocationAnnotationDotSize = 22.0;
-const CGFloat MGLUserLocationAnnotationHaloSize = 115.0;
-
-const CGFloat MGLUserLocationAnnotationPuckSize = 45.0;
-const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.6;
-
-@interface MGLUserLocationAnnotationView ()
-
-@property (nonatomic, readwrite) CALayer *haloLayer;
-
+@interface MGLUserLocationAnnotationView()
+@property (nonatomic, weak, nullable) MGLMapView *mapView;
+@property (nonatomic, weak, nullable) MGLUserLocation *userLocation;
+@property (nonatomic, weak) CALayer *hitTestLayer;
@end
-#pragma mark -
-
-@implementation MGLUserLocationAnnotationView
-{
- BOOL _puckModeActivated;
-
- CALayer *_puckDot;
- CAShapeLayer *_puckArrow;
-
- CALayer *_headingIndicatorLayer;
- CAShapeLayer *_headingIndicatorMaskLayer;
- CALayer *_accuracyRingLayer;
- CALayer *_dotBorderLayer;
- CALayer *_dotLayer;
-
- double _oldHeadingAccuracy;
- CLLocationAccuracy _oldHorizontalAccuracy;
- double _oldZoom;
- double _oldPitch;
-
+@implementation MGLUserLocationAnnotationView {
MGLCoordinateFormatter *_accessibilityCoordinateFormatter;
}
- (instancetype)initWithFrame:(CGRect)frame
{
- NSAssert(NO, @"No containing map view specified. Call -initInMapView: instead.");
- return self = [self init];
+ self = [super initWithFrame:frame];
+ if (self == nil) return nil;
+
+ self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable | UIAccessibilityTraitUpdatesFrequently;
+
+ _accessibilityCoordinateFormatter = [[MGLCoordinateFormatter alloc] init];
+ _accessibilityCoordinateFormatter.unitStyle = NSFormattingUnitStyleLong;
+
+ return self;
}
-- (instancetype)initInMapView:(MGLMapView *)mapView
+- (CALayer *)hitTestLayer
{
- CGFloat frameSize = (mapView.userTrackingMode == MGLUserTrackingModeFollowWithCourse) ? MGLUserLocationAnnotationPuckSize : MGLUserLocationAnnotationDotSize;
-
- if (self = [super initWithFrame:CGRectMake(0, 0, frameSize, frameSize)])
- {
- self.annotation = [[MGLUserLocation alloc] initWithMapView:mapView];
- _mapView = mapView;
- [self setupLayers];
- self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable | UIAccessibilityTraitUpdatesFrequently;
-
- _accessibilityCoordinateFormatter = [[MGLCoordinateFormatter alloc] init];
- _accessibilityCoordinateFormatter.unitStyle = NSFormattingUnitStyleLong;
- }
- return self;
+ return self.layer.presentationLayer;
}
-- (instancetype)initWithCoder:(NSCoder *)decoder
+- (void)update
{
- MGLMapView *mapView = [decoder valueForKey:@"mapView"];
- return [self initInMapView:mapView];
+ // Left blank intentionally. Subclasses should usually override this in order to update the annotation’s appearance.
}
- (BOOL)isAccessibilityElement
@@ -78,14 +48,14 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
- (NSString *)accessibilityLabel
{
- return self.annotation.title;
+ return self.userLocation.title;
}
- (NSString *)accessibilityValue
{
- if (self.annotation.subtitle)
+ if (self.userLocation.subtitle)
{
- return self.annotation.subtitle;
+ return self.userLocation.subtitle;
}
// Each arcminute of longitude is at most about 1 nmi, too small for low zoom levels.
@@ -127,475 +97,6 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
}
}
-- (void)setTintColor:(UIColor *)tintColor
-{
- if (_puckModeActivated)
- {
- _puckArrow.fillColor = [tintColor CGColor];
- }
- else
- {
- if (_accuracyRingLayer)
- {
- _accuracyRingLayer.backgroundColor = [tintColor CGColor];
- }
-
- _haloLayer.backgroundColor = [tintColor CGColor];
- _dotLayer.backgroundColor = [tintColor CGColor];
-
- _headingIndicatorLayer.contents = (__bridge id)[[self headingIndicatorTintedGradientImage] CGImage];
- }
-}
-
-- (CALayer *)hitTestLayer
-{
- // only the main dot should be interactive (i.e., exclude the accuracy ring and halo)
- return _dotBorderLayer ?: _puckDot;
-}
-
-- (void)setupLayers
-{
- if (CLLocationCoordinate2DIsValid(self.annotation.coordinate))
- {
- (_mapView.userTrackingMode == MGLUserTrackingModeFollowWithCourse) ? [self drawPuck] : [self drawDot];
- [self updatePitch];
- }
-}
-
-- (void)updatePitch
-{
- if (self.mapView.camera.pitch != _oldPitch)
- {
- CATransform3D t = CATransform3DRotate(CATransform3DIdentity, MGLRadiansFromDegrees(self.mapView.camera.pitch), 1.0, 0, 0);
- self.layer.sublayerTransform = t;
-
- [self updateFaux3DEffect];
-
- _oldPitch = self.mapView.camera.pitch;
- }
-}
-
-- (void)updateFaux3DEffect
-{
- CGFloat pitch = MGLRadiansFromDegrees(self.mapView.camera.pitch);
-
- if (_puckDot)
- {
- _puckDot.shadowOffset = CGSizeMake(0, fmaxf(pitch * 10.f, 1.f));
- _puckDot.shadowRadius = fmaxf(pitch * 5.f, 0.75f);
- }
-
- if (_dotBorderLayer)
- {
- _dotBorderLayer.shadowOffset = CGSizeMake(0.f, pitch * 10.f);
- _dotBorderLayer.shadowRadius = fmaxf(pitch * 5.f, 3.f);
- }
-
- if (_dotLayer)
- {
- _dotLayer.zPosition = pitch * 2.f;
- }
-}
-
-- (void)updateFrameWithSize:(CGFloat)size
-{
- CGSize newSize = CGSizeMake(size, size);
- if (CGSizeEqualToSize(self.frame.size, newSize))
- {
- return;
- }
-
- // Update frame size, keeping the existing center point.
- CGPoint oldCenter = self.center;
- CGRect newFrame = self.frame;
- newFrame.size = newSize;
- [self setFrame:newFrame];
- [self setCenter:oldCenter];
-}
-
-- (void)drawPuck
-{
- if ( ! _puckModeActivated)
- {
- self.layer.sublayers = nil;
-
- _headingIndicatorLayer = nil;
- _headingIndicatorMaskLayer = nil;
- _accuracyRingLayer = nil;
- _haloLayer = nil;
- _dotBorderLayer = nil;
- _dotLayer = nil;
-
- [self updateFrameWithSize:MGLUserLocationAnnotationPuckSize];
- }
-
- // background dot (white with black shadow)
- //
- if ( ! _puckDot)
- {
- _puckDot = [self circleLayerWithSize:MGLUserLocationAnnotationPuckSize];
- _puckDot.backgroundColor = [[UIColor whiteColor] CGColor];
- _puckDot.shadowColor = [[UIColor blackColor] CGColor];
- _puckDot.shadowOpacity = 0.25;
-
- if (self.mapView.camera.pitch)
- {
- [self updateFaux3DEffect];
- }
- else
- {
- _puckDot.shadowOffset = CGSizeMake(0, 1);
- _puckDot.shadowRadius = 0.75;
- }
-
- [self.layer addSublayer:_puckDot];
- }
-
- // arrow
- //
- if ( ! _puckArrow)
- {
- _puckArrow = [CAShapeLayer layer];
- _puckArrow.path = [[self puckArrow] CGPath];
- _puckArrow.fillColor = [_mapView.tintColor CGColor];
- _puckArrow.bounds = CGRectMake(0, 0, MGLUserLocationAnnotationArrowSize, MGLUserLocationAnnotationArrowSize);
- _puckArrow.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0);
- _puckArrow.shouldRasterize = YES;
- _puckArrow.rasterizationScale = [UIScreen mainScreen].scale;
- _puckArrow.drawsAsynchronously = YES;
-
- [self.layer addSublayer:_puckArrow];
- }
- if (self.annotation.location.course >= 0)
- {
- _puckArrow.affineTransform = CGAffineTransformRotate(CGAffineTransformIdentity, -MGLRadiansFromDegrees(self.mapView.direction - self.annotation.location.course));
- }
-
- if ( ! _puckModeActivated)
- {
- _puckModeActivated = YES;
-
- [self updateFaux3DEffect];
- }
-}
-
-- (UIBezierPath *)puckArrow
-{
- CGFloat max = MGLUserLocationAnnotationArrowSize;
-
- UIBezierPath *bezierPath = UIBezierPath.bezierPath;
- [bezierPath moveToPoint: CGPointMake(max * 0.5, 0)];
- [bezierPath addLineToPoint: CGPointMake(max * 0.1, max)];
- [bezierPath addLineToPoint: CGPointMake(max * 0.5, max * 0.65)];
- [bezierPath addLineToPoint: CGPointMake(max * 0.9, max)];
- [bezierPath addLineToPoint: CGPointMake(max * 0.5, 0)];
- [bezierPath closePath];
-
- return bezierPath;
-}
-
-- (void)drawDot
-{
- if (_puckModeActivated)
- {
- self.layer.sublayers = nil;
-
- _puckDot = nil;
- _puckArrow = nil;
-
- [self updateFrameWithSize:MGLUserLocationAnnotationDotSize];
- }
-
- BOOL showHeadingIndicator = _mapView.userTrackingMode == MGLUserTrackingModeFollowWithHeading;
-
- // update heading indicator
- //
- if (showHeadingIndicator)
- {
- _headingIndicatorLayer.hidden = NO;
-
- // heading indicator (tinted, semi-circle)
- //
- if ( ! _headingIndicatorLayer && self.annotation.heading.headingAccuracy)
- {
- 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];
- }
-
- // heading indicator accuracy mask (fan-shaped)
- //
- if ( ! _headingIndicatorMaskLayer && self.annotation.heading.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.annotation.heading.headingAccuracy;
-
- }
- else if (_oldHeadingAccuracy != self.annotation.heading.headingAccuracy)
- {
- // recalculate the clipping mask based on updated accuracy
- _headingIndicatorMaskLayer.path = [[self headingIndicatorClippingMask] CGPath];
-
- _oldHeadingAccuracy = self.annotation.heading.headingAccuracy;
- }
-
- if (self.annotation.heading.trueHeading >= 0)
- {
- _headingIndicatorLayer.affineTransform = CGAffineTransformRotate(CGAffineTransformIdentity, -MGLRadiansFromDegrees(self.mapView.direction - self.annotation.heading.trueHeading));
- }
- }
- 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.annotation.location.horizontalAccuracy))
- {
- CGFloat accuracyRingSize = [self calculateAccuracyRingSize];
-
- // only show the accuracy ring if it won't be obscured by the location dot
- if (accuracyRingSize > MGLUserLocationAnnotationDotSize + 15)
- {
- _accuracyRingLayer.hidden = NO;
- _accuracyRingLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize);
- _accuracyRingLayer.cornerRadius = accuracyRingSize / 2;
-
- // match the halo to the accuracy ring
- _haloLayer.bounds = _accuracyRingLayer.bounds;
- _haloLayer.cornerRadius = _accuracyRingLayer.cornerRadius;
- _haloLayer.shouldRasterize = NO;
- }
- else
- {
- _accuracyRingLayer.hidden = YES;
-
- _haloLayer.bounds = CGRectMake(0, 0, MGLUserLocationAnnotationHaloSize, MGLUserLocationAnnotationHaloSize);
- _haloLayer.cornerRadius = MGLUserLocationAnnotationHaloSize / 2.0;
- _haloLayer.shouldRasterize = YES;
- _haloLayer.rasterizationScale = [UIScreen mainScreen].scale;
- }
-
- // store accuracy and zoom so we're not redrawing unchanged location updates
- _oldHorizontalAccuracy = self.annotation.location.horizontalAccuracy;
- _oldZoom = self.mapView.zoomLevel;
- }
-
- // accuracy ring (circular, tinted, mostly-transparent)
- //
- if ( ! _accuracyRingLayer && self.annotation.location.horizontalAccuracy)
- {
- CGFloat accuracyRingSize = [self calculateAccuracyRingSize];
- _accuracyRingLayer = [self circleLayerWithSize:accuracyRingSize];
- _accuracyRingLayer.backgroundColor = [_mapView.tintColor CGColor];
- _accuracyRingLayer.opacity = 0.1;
- _accuracyRingLayer.shouldRasterize = NO;
- _accuracyRingLayer.allowsGroupOpacity = NO;
-
- [self.layer addSublayer:_accuracyRingLayer];
- }
-
- // expanding sonar-like pulse (circular, tinted, fades out)
- //
- if ( ! _haloLayer)
- {
- _haloLayer = [self circleLayerWithSize:MGLUserLocationAnnotationHaloSize];
- _haloLayer.backgroundColor = [_mapView.tintColor CGColor];
- _haloLayer.allowsGroupOpacity = NO;
- _haloLayer.zPosition = -0.1f;
-
- // set defaults for the animations
- CAAnimationGroup *animationGroup = [self loopingAnimationGroupWithDuration:3.0];
-
- // scale out radially with initial acceleration
- CAKeyframeAnimation *boundsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.xy"];
- boundsAnimation.values = @[@0, @0.35, @1];
- boundsAnimation.keyTimes = @[@0, @0.2, @1];
-
- // go transparent as scaled out, start semi-opaque
- CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
- opacityAnimation.values = @[@0.4, @0.4, @0];
- opacityAnimation.keyTimes = @[@0, @0.2, @1];
-
- animationGroup.animations = @[boundsAnimation, opacityAnimation];
-
- [_haloLayer addAnimation:animationGroup forKey:@"animateTransformAndOpacity"];
-
- [self.layer addSublayer:_haloLayer];
- }
-
- // background dot (white with black shadow)
- //
- if ( ! _dotBorderLayer)
- {
- _dotBorderLayer = [self circleLayerWithSize:MGLUserLocationAnnotationDotSize];
- _dotBorderLayer.backgroundColor = [[UIColor whiteColor] CGColor];
- _dotBorderLayer.shadowColor = [[UIColor blackColor] CGColor];
- _dotBorderLayer.shadowOpacity = 0.25;
-
- if (self.mapView.camera.pitch)
- {
- [self updateFaux3DEffect];
- }
- else
- {
- _dotBorderLayer.shadowOffset = CGSizeMake(0, 0);
- _dotBorderLayer.shadowRadius = 3;
- }
-
- [self.layer addSublayer:_dotBorderLayer];
- }
-
- // inner dot (pulsing, tinted)
- //
- if ( ! _dotLayer)
- {
- _dotLayer = [self circleLayerWithSize:MGLUserLocationAnnotationDotSize * 0.75];
- _dotLayer.backgroundColor = [_mapView.tintColor CGColor];
- _dotLayer.shouldRasterize = NO;
-
- // set defaults for the animations
- CAAnimationGroup *animationGroup = [self loopingAnimationGroupWithDuration:1.5];
- animationGroup.autoreverses = YES;
- animationGroup.fillMode = kCAFillModeBoth;
-
- // scale the dot up and down
- CABasicAnimation *pulseAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale.xy"];
- pulseAnimation.fromValue = @0.8;
- pulseAnimation.toValue = @1;
-
- // fade opacity in and out, subtly
- CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
- opacityAnimation.fromValue = @0.8;
- opacityAnimation.toValue = @1;
-
- animationGroup.animations = @[pulseAnimation, opacityAnimation];
-
- [_dotLayer addAnimation:animationGroup forKey:@"animateTransformAndOpacity"];
-
- [self.layer addSublayer:_dotLayer];
- }
-
- if (_puckModeActivated)
- {
- _puckModeActivated = NO;
-
- [self updateFaux3DEffect];
- }
-}
-
-- (CALayer *)circleLayerWithSize:(CGFloat)layerSize
-{
- CALayer *circleLayer = [CALayer layer];
- circleLayer.bounds = CGRectMake(0, 0, layerSize, layerSize);
- circleLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0);
- circleLayer.cornerRadius = layerSize / 2.0;
- circleLayer.shouldRasterize = YES;
- circleLayer.rasterizationScale = [UIScreen mainScreen].scale;
- circleLayer.drawsAsynchronously = YES;
-
- return circleLayer;
-}
-
-- (CAAnimationGroup *)loopingAnimationGroupWithDuration:(CGFloat)animationDuration
-{
- CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
- animationGroup.duration = animationDuration;
- animationGroup.repeatCount = INFINITY;
- animationGroup.removedOnCompletion = NO;
- animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
-
- return animationGroup;
-}
-
-- (CGFloat)calculateAccuracyRingSize
-{
- CGFloat latRadians = self.annotation.coordinate.latitude * M_PI / 180.0f;
- CGFloat pixelRadius = self.annotation.location.horizontalAccuracy / cos(latRadians) / [self.mapView metersPerPointAtLatitude:self.annotation.coordinate.latitude];
-
- return pixelRadius * 2;
-}
-
-- (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)[_mapView.tintColor CGColor],
- (id)[[_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.annotation.heading.headingAccuracy;
-
- // size the mask using exagerated accuracy, but keep within a good display range
- CGFloat clippingDegrees = 90 - (accuracy * 1.5);
- clippingDegrees = fmin(clippingDegrees, 55);
- clippingDegrees = fmax(clippingDegrees, 10);
-
- 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:(-180 + clippingDegrees) * M_PI / 180
- endAngle:-clippingDegrees * M_PI / 180
- clockwise:YES];
-
- [ovalPath addLineToPoint:CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect))];
- [ovalPath closePath];
-
- return ovalPath;
-}
-
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
// Allow mbgl to drive animation of this view’s bounds.
diff --git a/platform/ios/src/MGLUserLocationAnnotationView_Private.h b/platform/ios/src/MGLUserLocationAnnotationView_Private.h
new file mode 100644
index 0000000000..3e12beab34
--- /dev/null
+++ b/platform/ios/src/MGLUserLocationAnnotationView_Private.h
@@ -0,0 +1,15 @@
+#import "MGLUserLocationAnnotationView.h"
+#import "MGLUserLocation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class MGLMapView;
+
+@interface MGLUserLocationAnnotationView (Private)
+
+@property (nonatomic, weak, nullable) MGLUserLocation *userLocation;
+@property (nonatomic, weak, nullable) MGLMapView *mapView;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/ios/src/Mapbox.h b/platform/ios/src/Mapbox.h
index d6c05503e9..24925d169c 100644
--- a/platform/ios/src/Mapbox.h
+++ b/platform/ios/src/Mapbox.h
@@ -47,6 +47,7 @@ FOUNDATION_EXPORT const unsigned char MapboxVersionString[];
#import "MGLTilePyramidOfflineRegion.h"
#import "MGLTypes.h"
#import "MGLUserLocation.h"
+#import "MGLUserLocationAnnotationView.h"
#import "NSValue+MGLAdditions.h"
#import "MGLStyleAttributeValue.h"
#import "MGLStyleAttributeFunction.h"