613 lines
22 KiB
Plaintext
613 lines
22 KiB
Plaintext
#import "MLNFaux3DUserLocationAnnotationView.h"
|
|
|
|
#import "MLNLocationManager_Private.h"
|
|
#import "MLNMapViewDelegate.h"
|
|
#import "MLNMapView_Private.h"
|
|
#import "MLNUserLocation.h"
|
|
#import "MLNUserLocationAnnotationViewStyle.h"
|
|
#import "MLNUserLocationHeadingArrowLayer.h"
|
|
#import "MLNUserLocationHeadingBeamLayer.h"
|
|
#import "MLNUserLocationHeadingIndicator.h"
|
|
|
|
const CGFloat MLNUserLocationAnnotationDotSize = 22.0;
|
|
const CGFloat MLNUserLocationAnnotationHaloSize = 115.0;
|
|
|
|
const CGFloat MLNUserLocationAnnotationPuckSize = 45.0;
|
|
const CGFloat MLNUserLocationAnnotationArrowSize = MLNUserLocationAnnotationPuckSize * 0.5;
|
|
|
|
const CGFloat MLNUserLocationHeadingUpdateThreshold = 0.01;
|
|
|
|
const CGFloat MLNUserLocationApproximateZoomThreshold = 7.0;
|
|
|
|
@implementation MLNFaux3DUserLocationAnnotationView {
|
|
BOOL _puckModeActivated;
|
|
BOOL _approximateModeActivated;
|
|
|
|
CALayer *_puckDot;
|
|
CAShapeLayer *_puckArrow;
|
|
|
|
CALayer<MLNUserLocationHeadingIndicator> *_headingIndicatorLayer;
|
|
CALayer *_accuracyRingLayer;
|
|
CALayer *_dotBorderLayer;
|
|
CALayer *_dotLayer;
|
|
CALayer *_haloLayer;
|
|
|
|
CALayer *_approximateLayer;
|
|
|
|
CLLocationDirection _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 == MLNUserTrackingModeFollowWithCourse)
|
|
? MLNUserLocationAnnotationPuckSize
|
|
: MLNUserLocationAnnotationDotSize;
|
|
[self updateFrameWithSize:frameSize];
|
|
}
|
|
|
|
if (CLLocationCoordinate2DIsValid(self.userLocation.coordinate)) {
|
|
if (@available(iOS 14, *)) {
|
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
|
|
if (![self.mapView.locationManager respondsToSelector:@selector(accuracyAuthorization)] ||
|
|
self.mapView.locationManager.accuracyAuthorization ==
|
|
CLAccuracyAuthorizationFullAccuracy) {
|
|
[self drawPreciseLocationPuck];
|
|
} else {
|
|
[self drawApproximate];
|
|
[self updatePitch];
|
|
}
|
|
#endif
|
|
} else {
|
|
[self drawPreciseLocationPuck];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)drawPreciseLocationPuck {
|
|
if (_approximateModeActivated) {
|
|
[_approximateLayer removeFromSuperlayer];
|
|
_approximateLayer = nil;
|
|
|
|
_approximateModeActivated = NO;
|
|
}
|
|
(self.mapView.userTrackingMode == MLNUserTrackingModeFollowWithCourse) ? [self drawPuck]
|
|
: [self drawDot];
|
|
[self updatePitch];
|
|
_haloLayer.hidden = !CLLocationCoordinate2DIsValid(self.mapView.userLocation.coordinate) ||
|
|
self.mapView.userLocation.location.horizontalAccuracy > 10;
|
|
}
|
|
|
|
- (void)setTintColor:(UIColor *)tintColor {
|
|
UIColor *puckArrowFillColor = tintColor;
|
|
UIColor *puckArrowStrokeColor = tintColor;
|
|
|
|
UIColor *approximateFillColor = tintColor;
|
|
|
|
UIColor *accuracyFillColor = tintColor;
|
|
UIColor *haloFillColor = tintColor;
|
|
UIColor *dotFillColor = tintColor;
|
|
UIColor *headingFillColor = tintColor;
|
|
|
|
if ([self.mapView.delegate
|
|
respondsToSelector:@selector(mapViewStyleForDefaultUserLocationAnnotationView:)]) {
|
|
MLNUserLocationAnnotationViewStyle *style =
|
|
[self.mapView.delegate mapViewStyleForDefaultUserLocationAnnotationView:self.mapView];
|
|
|
|
puckArrowFillColor = style.puckArrowFillColor ? style.puckArrowFillColor : puckArrowFillColor;
|
|
|
|
if (@available(iOS 14, *)) {
|
|
approximateFillColor =
|
|
style.approximateHaloFillColor ? style.approximateHaloFillColor : approximateFillColor;
|
|
}
|
|
|
|
haloFillColor = style.haloFillColor ? style.haloFillColor : haloFillColor;
|
|
dotFillColor = style.puckFillColor ? style.puckFillColor : dotFillColor;
|
|
headingFillColor = style.puckFillColor ? style.puckFillColor : headingFillColor;
|
|
}
|
|
|
|
if (_puckModeActivated) {
|
|
_puckArrow.fillColor = [puckArrowFillColor CGColor];
|
|
_puckArrow.strokeColor = [puckArrowStrokeColor CGColor];
|
|
} else if (_approximateModeActivated) {
|
|
_approximateLayer.backgroundColor = [approximateFillColor CGColor];
|
|
} else {
|
|
_accuracyRingLayer.backgroundColor = [accuracyFillColor CGColor];
|
|
_haloLayer.backgroundColor = [haloFillColor CGColor];
|
|
_dotLayer.backgroundColor = [dotFillColor CGColor];
|
|
[_headingIndicatorLayer updateTintColor:[headingFillColor CGColor]];
|
|
}
|
|
}
|
|
|
|
- (void)updatePitch {
|
|
if (self.mapView.camera.pitch != _oldPitch) {
|
|
// disable implicit animation
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
|
|
CATransform3D t = CATransform3DRotate(
|
|
CATransform3DIdentity, MLNRadiansFromDegrees(self.mapView.camera.pitch), 1.0, 0, 0);
|
|
self.layer.sublayerTransform = t;
|
|
|
|
[self updateFaux3DEffect];
|
|
|
|
[CATransaction commit];
|
|
|
|
_oldPitch = self.mapView.camera.pitch;
|
|
}
|
|
}
|
|
|
|
- (void)updateFaux3DEffect {
|
|
CGFloat pitch = MLNRadiansFromDegrees(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;
|
|
_accuracyRingLayer = nil;
|
|
_haloLayer = nil;
|
|
_dotBorderLayer = nil;
|
|
_dotLayer = nil;
|
|
|
|
[self updateFrameWithSize:MLNUserLocationAnnotationPuckSize];
|
|
}
|
|
|
|
UIColor *arrowColor = self.mapView.tintColor;
|
|
UIColor *puckShadowColor = UIColor.blackColor;
|
|
CGFloat shadowOpacity = 0.25;
|
|
|
|
if ([self.mapView.delegate
|
|
respondsToSelector:@selector(mapViewStyleForDefaultUserLocationAnnotationView:)]) {
|
|
MLNUserLocationAnnotationViewStyle *style =
|
|
[self.mapView.delegate mapViewStyleForDefaultUserLocationAnnotationView:self.mapView];
|
|
arrowColor = style.puckArrowFillColor ? style.puckArrowFillColor : arrowColor;
|
|
puckShadowColor = style.puckShadowColor ? style.puckShadowColor : puckShadowColor;
|
|
shadowOpacity = style.puckShadowOpacity;
|
|
}
|
|
|
|
// background dot (white with black shadow)
|
|
//
|
|
if (!_puckDot) {
|
|
_puckDot = [self circleLayerWithSize:MLNUserLocationAnnotationPuckSize];
|
|
_puckDot.backgroundColor = [[UIColor whiteColor] CGColor];
|
|
_puckDot.shadowColor = [puckShadowColor CGColor];
|
|
_puckDot.shadowOpacity = shadowOpacity;
|
|
_puckDot.shadowPath = [[UIBezierPath bezierPathWithOvalInRect:_puckDot.bounds] CGPath];
|
|
|
|
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 = [arrowColor CGColor];
|
|
_puckArrow.bounds = CGRectMake(0, 0, round(MLNUserLocationAnnotationArrowSize),
|
|
round(MLNUserLocationAnnotationArrowSize));
|
|
_puckArrow.position = CGPointMake(CGRectGetMidX(super.bounds), CGRectGetMidY(super.bounds));
|
|
_puckArrow.shouldRasterize = YES;
|
|
_puckArrow.rasterizationScale = [UIScreen mainScreen].scale;
|
|
_puckArrow.drawsAsynchronously = YES;
|
|
|
|
_puckArrow.lineJoin = @"round";
|
|
_puckArrow.lineWidth = 1.f;
|
|
_puckArrow.strokeColor = _puckArrow.fillColor;
|
|
|
|
[self.layer addSublayer:_puckArrow];
|
|
}
|
|
if (self.userLocation.location.course >= 0) {
|
|
_puckArrow.affineTransform = CGAffineTransformRotate(
|
|
CGAffineTransformIdentity,
|
|
-MLNRadiansFromDegrees(self.mapView.direction - self.userLocation.location.course));
|
|
}
|
|
|
|
if (!_puckModeActivated) {
|
|
_puckModeActivated = YES;
|
|
|
|
[self updateFaux3DEffect];
|
|
}
|
|
}
|
|
|
|
- (UIBezierPath *)puckArrow {
|
|
CGFloat max = MLNUserLocationAnnotationArrowSize;
|
|
|
|
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:MLNUserLocationAnnotationDotSize];
|
|
}
|
|
|
|
UIColor *haloColor = self.mapView.tintColor;
|
|
UIColor *puckBackgroundColor = self.mapView.tintColor;
|
|
UIColor *puckShadowColor = UIColor.blackColor;
|
|
CGFloat shadowOpacity = 0.25;
|
|
|
|
if ([self.mapView.delegate
|
|
respondsToSelector:@selector(mapViewStyleForDefaultUserLocationAnnotationView:)]) {
|
|
MLNUserLocationAnnotationViewStyle *style =
|
|
[self.mapView.delegate mapViewStyleForDefaultUserLocationAnnotationView:self.mapView];
|
|
haloColor = style.haloFillColor ? style.haloFillColor : haloColor;
|
|
puckBackgroundColor = style.puckFillColor ? style.puckFillColor : puckBackgroundColor;
|
|
puckShadowColor = style.puckShadowColor ? style.puckShadowColor : puckShadowColor;
|
|
shadowOpacity = style.puckShadowOpacity;
|
|
}
|
|
|
|
// heading indicator (tinted, beam or arrow)
|
|
//
|
|
BOOL headingTrackingModeEnabled =
|
|
self.mapView.userTrackingMode == MLNUserTrackingModeFollowWithHeading;
|
|
BOOL showHeadingIndicator = self.mapView.showsUserHeadingIndicator || headingTrackingModeEnabled;
|
|
|
|
if (showHeadingIndicator) {
|
|
_headingIndicatorLayer.hidden = NO;
|
|
CLLocationDirection headingAccuracy = self.userLocation.heading.headingAccuracy;
|
|
|
|
if (([_headingIndicatorLayer isMemberOfClass:[MLNUserLocationHeadingBeamLayer class]] &&
|
|
!headingTrackingModeEnabled) ||
|
|
([_headingIndicatorLayer isMemberOfClass:[MLNUserLocationHeadingArrowLayer class]] &&
|
|
headingTrackingModeEnabled)) {
|
|
[_headingIndicatorLayer removeFromSuperlayer];
|
|
_headingIndicatorLayer = nil;
|
|
_oldHeadingAccuracy = -1;
|
|
}
|
|
|
|
if (!_headingIndicatorLayer && headingAccuracy) {
|
|
if (headingTrackingModeEnabled) {
|
|
_headingIndicatorLayer =
|
|
[[MLNUserLocationHeadingBeamLayer alloc] initWithUserLocationAnnotationView:self];
|
|
[_headingIndicatorLayer updateTintColor:[haloColor CGColor]];
|
|
[self.layer insertSublayer:_headingIndicatorLayer below:_dotBorderLayer];
|
|
} else {
|
|
_headingIndicatorLayer =
|
|
[[MLNUserLocationHeadingArrowLayer alloc] initWithUserLocationAnnotationView:self];
|
|
[_headingIndicatorLayer updateTintColor:[puckBackgroundColor CGColor]];
|
|
[self.layer addSublayer:_headingIndicatorLayer];
|
|
_headingIndicatorLayer.zPosition = 1;
|
|
}
|
|
}
|
|
|
|
if (_oldHeadingAccuracy != headingAccuracy) {
|
|
[_headingIndicatorLayer updateHeadingAccuracy:headingAccuracy];
|
|
_oldHeadingAccuracy = headingAccuracy;
|
|
}
|
|
|
|
CLLocationDirection headingDirection =
|
|
(self.userLocation.heading.trueHeading >= 0 ? self.userLocation.heading.trueHeading
|
|
: self.userLocation.heading.magneticHeading);
|
|
if (headingDirection >= 0) {
|
|
CGFloat rotation = -MLNRadiansFromDegrees(self.mapView.direction - headingDirection);
|
|
|
|
// Don't rotate if the change is imperceptible.
|
|
if (fabs(rotation) > MLNUserLocationHeadingUpdateThreshold) {
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
|
|
_headingIndicatorLayer.affineTransform =
|
|
CGAffineTransformRotate(CGAffineTransformIdentity, rotation);
|
|
|
|
[CATransaction commit];
|
|
}
|
|
}
|
|
} else {
|
|
[_headingIndicatorLayer removeFromSuperlayer];
|
|
_headingIndicatorLayer = 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:self.mapView.zoomLevel];
|
|
|
|
// only show the accuracy ring if it won't be obscured by the location dot
|
|
if (accuracyRingSize > MLNUserLocationAnnotationDotSize + 15) {
|
|
_accuracyRingLayer.hidden = NO;
|
|
|
|
// disable implicit animation of the accuracy ring, unless triggered by a change in accuracy
|
|
BOOL shouldDisableActions =
|
|
_oldHorizontalAccuracy == self.userLocation.location.horizontalAccuracy;
|
|
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:shouldDisableActions];
|
|
|
|
_accuracyRingLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize);
|
|
_accuracyRingLayer.cornerRadius = accuracyRingSize / 2.0;
|
|
|
|
// match the halo to the accuracy ring
|
|
_haloLayer.bounds = _accuracyRingLayer.bounds;
|
|
_haloLayer.cornerRadius = _accuracyRingLayer.cornerRadius;
|
|
_haloLayer.shouldRasterize = NO;
|
|
|
|
[CATransaction commit];
|
|
} else {
|
|
_accuracyRingLayer.hidden = YES;
|
|
|
|
_haloLayer.bounds =
|
|
CGRectMake(0, 0, MLNUserLocationAnnotationHaloSize, MLNUserLocationAnnotationHaloSize);
|
|
_haloLayer.cornerRadius = MLNUserLocationAnnotationHaloSize / 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:self.mapView.zoomLevel];
|
|
_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:MLNUserLocationAnnotationHaloSize];
|
|
_haloLayer.backgroundColor = [haloColor 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:MLNUserLocationAnnotationDotSize];
|
|
_dotBorderLayer.backgroundColor = [[UIColor whiteColor] CGColor];
|
|
_dotBorderLayer.shadowColor = [puckShadowColor CGColor];
|
|
_dotBorderLayer.shadowOpacity = shadowOpacity;
|
|
_dotBorderLayer.shadowPath =
|
|
[[UIBezierPath bezierPathWithOvalInRect:_dotBorderLayer.bounds] CGPath];
|
|
|
|
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:MLNUserLocationAnnotationDotSize * 0.75];
|
|
_dotLayer.backgroundColor = [puckBackgroundColor CGColor];
|
|
|
|
// 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];
|
|
}
|
|
}
|
|
|
|
- (void)drawApproximate {
|
|
if (!_approximateModeActivated) {
|
|
self.layer.sublayers = nil;
|
|
|
|
_headingIndicatorLayer = nil;
|
|
_dotBorderLayer = nil;
|
|
_dotLayer = nil;
|
|
_accuracyRingLayer = nil;
|
|
_haloLayer = nil;
|
|
_puckDot = nil;
|
|
_puckArrow = nil;
|
|
|
|
_approximateModeActivated = YES;
|
|
}
|
|
|
|
UIColor *backgroundColor = self.mapView.tintColor;
|
|
UIColor *strokeColor = UIColor.blackColor;
|
|
CGFloat borderSize = 2.0;
|
|
CGFloat opacity = 0.25;
|
|
|
|
if ([self.mapView.delegate
|
|
respondsToSelector:@selector(mapViewStyleForDefaultUserLocationAnnotationView:)]) {
|
|
MLNUserLocationAnnotationViewStyle *style =
|
|
[self.mapView.delegate mapViewStyleForDefaultUserLocationAnnotationView:self.mapView];
|
|
if (@available(iOS 14, *)) {
|
|
backgroundColor =
|
|
style.approximateHaloFillColor ? style.approximateHaloFillColor : backgroundColor;
|
|
strokeColor =
|
|
style.approximateHaloBorderColor ? style.approximateHaloBorderColor : strokeColor;
|
|
opacity = style.approximateHaloOpacity;
|
|
borderSize = style.approximateHaloBorderWidth;
|
|
}
|
|
}
|
|
|
|
// approximate ring
|
|
if (!_approximateLayer && self.userLocation.location.horizontalAccuracy) {
|
|
CGFloat accuracyRingSize =
|
|
[self calculateAccuracyRingSize:MAX(self.mapView.zoomLevel,
|
|
MLNUserLocationApproximateZoomThreshold)];
|
|
_approximateLayer = [self circleLayerWithSize:accuracyRingSize];
|
|
_approximateLayer.backgroundColor = [backgroundColor CGColor];
|
|
_approximateLayer.opacity = opacity;
|
|
_approximateLayer.shouldRasterize = NO;
|
|
_approximateLayer.allowsGroupOpacity = NO;
|
|
_approximateLayer.borderWidth = borderSize;
|
|
_approximateLayer.borderColor = [strokeColor CGColor];
|
|
|
|
[self.layer addSublayer:_approximateLayer];
|
|
}
|
|
|
|
// update approximate ring (if zoom or horizontal accuracy have changed)
|
|
if (_approximateLayer &&
|
|
(_oldZoom != self.mapView.zoomLevel ||
|
|
_oldHorizontalAccuracy != self.userLocation.location.horizontalAccuracy)) {
|
|
if (self.mapView.zoomLevel < MLNUserLocationApproximateZoomThreshold) {
|
|
borderSize = 1.0;
|
|
}
|
|
_approximateLayer.borderWidth = borderSize;
|
|
|
|
if (self.mapView.zoomLevel >= MLNUserLocationApproximateZoomThreshold) {
|
|
CGFloat accuracyRingSize = [self calculateAccuracyRingSize:self.mapView.zoomLevel];
|
|
|
|
_approximateLayer.hidden = NO;
|
|
|
|
// disable implicit animation of the accuracy ring, unless triggered by a change in accuracy
|
|
BOOL shouldDisableActions =
|
|
_oldHorizontalAccuracy == self.userLocation.location.horizontalAccuracy;
|
|
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:shouldDisableActions];
|
|
|
|
_approximateLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize);
|
|
_approximateLayer.cornerRadius = accuracyRingSize / 2.0;
|
|
|
|
[CATransaction commit];
|
|
}
|
|
|
|
// store accuracy and zoom so we're not redrawing unchanged location updates
|
|
_oldHorizontalAccuracy = self.userLocation.location.horizontalAccuracy;
|
|
_oldZoom = self.mapView.zoomLevel;
|
|
}
|
|
}
|
|
|
|
- (CALayer *)circleLayerWithSize:(CGFloat)layerSize {
|
|
layerSize = round(layerSize);
|
|
|
|
CALayer *circleLayer = [CALayer layer];
|
|
circleLayer.bounds = CGRectMake(0, 0, layerSize, layerSize);
|
|
circleLayer.position = CGPointMake(CGRectGetMidX(super.bounds), CGRectGetMidY(super.bounds));
|
|
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:(double)zoomLevel {
|
|
// diameter in screen points
|
|
return round(self.userLocation.location.horizontalAccuracy /
|
|
[self.mapView metersPerPointAtLatitude:self.userLocation.coordinate.latitude
|
|
zoomLevel:zoomLevel] *
|
|
2.0);
|
|
}
|
|
|
|
@end
|