summaryrefslogtreecommitdiff
path: root/platform/ios/MGLUserLocationAnnotationView.m
diff options
context:
space:
mode:
Diffstat (limited to 'platform/ios/MGLUserLocationAnnotationView.m')
-rw-r--r--platform/ios/MGLUserLocationAnnotationView.m332
1 files changed, 230 insertions, 102 deletions
diff --git a/platform/ios/MGLUserLocationAnnotationView.m b/platform/ios/MGLUserLocationAnnotationView.m
index 67f360f68a..a6310a82c3 100644
--- a/platform/ios/MGLUserLocationAnnotationView.m
+++ b/platform/ios/MGLUserLocationAnnotationView.m
@@ -5,7 +5,8 @@
#import "MGLAnnotation.h"
#import "MGLMapView.h"
-const CGFloat MGLTrackingDotRingWidth = 24.0;
+const CGFloat MGLUserLocationAnnotationDotSize = 22.0;
+const CGFloat MGLUserLocationAnnotationHaloSize = 115.0;
@interface MGLUserLocationAnnotationView ()
@@ -17,9 +18,15 @@ const CGFloat MGLTrackingDotRingWidth = 24.0;
@implementation MGLUserLocationAnnotationView
{
+ CALayer *_headingIndicatorLayer;
+ CAShapeLayer *_headingIndicatorMaskLayer;
CALayer *_accuracyRingLayer;
CALayer *_dotBorderLayer;
CALayer *_dotLayer;
+
+ double _oldHeadingAccuracy;
+ CLLocationAccuracy _oldHorizontalAccuracy;
+ double _oldZoom;
}
- (instancetype)initWithFrame:(CGRect)frame
@@ -30,7 +37,7 @@ const CGFloat MGLTrackingDotRingWidth = 24.0;
- (instancetype)initInMapView:(MGLMapView *)mapView
{
- if (self = [super initWithFrame:CGRectMake(0, 0, MGLTrackingDotRingWidth, MGLTrackingDotRingWidth)])
+ if (self = [super initWithFrame:CGRectMake(0, 0, MGLUserLocationAnnotationDotSize, MGLUserLocationAnnotationDotSize)])
{
self.annotation = [[MGLUserLocation alloc] initWithMapView:mapView];
_mapView = mapView;
@@ -48,164 +55,285 @@ const CGFloat MGLTrackingDotRingWidth = 24.0;
- (void)setTintColor:(UIColor *)tintColor
{
- UIImage *trackingDotHaloImage = [self trackingDotHaloImage];
- _haloLayer.bounds = CGRectMake(0, 0, trackingDotHaloImage.size.width, trackingDotHaloImage.size.height);
- _haloLayer.contents = (__bridge id)[trackingDotHaloImage CGImage];
+ if (_accuracyRingLayer)
+ {
+ _accuracyRingLayer.backgroundColor = [tintColor CGColor];
+ }
+
+ _haloLayer.backgroundColor = [tintColor CGColor];
+ _dotLayer.backgroundColor = [tintColor CGColor];
- UIImage *dotImage = [self dotImage];
- _dotLayer.bounds = CGRectMake(0, 0, dotImage.size.width, dotImage.size.height);
- _dotLayer.contents = (__bridge id)[dotImage CGImage];
+ _headingIndicatorLayer.contents = (__bridge id)[[self headingIndicatorTintedGradientImage] CGImage];
}
- (void)setupLayers
{
if (CLLocationCoordinate2DIsValid(self.annotation.coordinate))
{
- if ( ! _accuracyRingLayer && self.annotation.location.horizontalAccuracy)
+ // update heading indicator
+ //
+ if (_headingIndicatorLayer)
{
- UIImage *accuracyRingImage = [self accuracyRingImage];
- _accuracyRingLayer = [CALayer layer];
- _haloLayer.bounds = CGRectMake(0, 0, accuracyRingImage.size.width, accuracyRingImage.size.height);
- _haloLayer.contents = (__bridge id)[accuracyRingImage CGImage];
- _haloLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
+ _headingIndicatorLayer.hidden = (_mapView.userTrackingMode == MGLUserTrackingModeFollowWithHeading) ? NO : YES;
- [self.layer addSublayer:_accuracyRingLayer];
+ 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 ( ! _haloLayer)
+ // heading indicator (tinted, semi-circle)
+ //
+ if ( ! _headingIndicatorLayer && self.annotation.heading.headingAccuracy)
{
- UIImage *haloImage = [self trackingDotHaloImage];
- _haloLayer = [CALayer layer];
- _haloLayer.bounds = CGRectMake(0, 0, haloImage.size.width, haloImage.size.height);
- _haloLayer.contents = (__bridge id)[haloImage CGImage];
- _haloLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
+ CGFloat headingIndicatorSize = MGLUserLocationAnnotationHaloSize;
- [CATransaction begin];
+ _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;
- [CATransaction setAnimationDuration:3.5];
- [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
-
- // scale out radially
- //
- CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
- boundsAnimation.repeatCount = MAXFLOAT;
- boundsAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
- boundsAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2.0, 2.0, 1.0)];
- boundsAnimation.removedOnCompletion = NO;
+ [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;
- [_haloLayer addAnimation:boundsAnimation forKey:@"animateScale"];
+ _oldHeadingAccuracy = self.annotation.heading.headingAccuracy;
+ }
+
+ // update accuracy ring
+ //
+ if (_accuracyRingLayer)
+ {
+ // FIX: This stops EVERYTHING... and that isn't necessarily necessary, now is it?
+ if (_oldZoom == self.mapView.zoomLevel && _oldHorizontalAccuracy == self.annotation.location.horizontalAccuracy)
+ {
+ return;
+ }
- // go transparent as scaled out
- //
- CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
- opacityAnimation.repeatCount = MAXFLOAT;
- opacityAnimation.fromValue = [NSNumber numberWithFloat:1.0];
- opacityAnimation.toValue = [NSNumber numberWithFloat:-1.0];
- opacityAnimation.removedOnCompletion = NO;
+ CGFloat accuracyRingSize = [self calculateAccuracyRingSize];
- [_haloLayer addAnimation:opacityAnimation forKey:@"animateOpacity"];
+ // 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;
+ }
- [CATransaction commit];
+ // 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:_haloLayer];
+ [self.layer addSublayer:_accuracyRingLayer];
}
- // white dot background with shadow
+ // expanding sonar-like pulse (circular, tinted, fades out)
//
- if ( ! _dotBorderLayer)
+ if ( ! _haloLayer)
{
- CGRect rect = CGRectMake(0, 0, MGLTrackingDotRingWidth * 1.5, MGLTrackingDotRingWidth * 1.5);
+ _haloLayer = [self circleLayerWithSize:MGLUserLocationAnnotationHaloSize];
+ _haloLayer.backgroundColor = [_mapView.tintColor CGColor];
+ _haloLayer.allowsGroupOpacity = NO;
- UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]);
- CGContextRef context = UIGraphicsGetCurrentContext();
+ // set defaults for the animations
+ CAAnimationGroup *animationGroup = [self loopingAnimationGroupWithDuration:3.0];
- CGContextSetShadow(context, CGSizeMake(0, 0), MGLTrackingDotRingWidth / 4.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];
- CGContextSetFillColorWithColor(context, [[UIColor whiteColor] CGColor]);
- CGContextFillEllipseInRect(context, CGRectMake((rect.size.width - MGLTrackingDotRingWidth) / 2.0, (rect.size.height - MGLTrackingDotRingWidth) / 2.0, MGLTrackingDotRingWidth, MGLTrackingDotRingWidth));
+ // 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];
- UIImage *whiteBackground = UIGraphicsGetImageFromCurrentImageContext();
+ animationGroup.animations = @[boundsAnimation, opacityAnimation];
- UIGraphicsEndImageContext();
+ [_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.shadowOffset = CGSizeMake(0, 0);
+ _dotBorderLayer.shadowRadius = 3;
+ _dotBorderLayer.shadowOpacity = 0.25;
- _dotBorderLayer = [CALayer layer];
- _dotBorderLayer.bounds = CGRectMake(0, 0, whiteBackground.size.width, whiteBackground.size.height);
- _dotBorderLayer.contents = (__bridge id)[whiteBackground CGImage];
- _dotBorderLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
[self.layer addSublayer:_dotBorderLayer];
}
-
- // pulsing, tinted dot sublayer
+
+ // inner dot (pulsing, tinted)
//
if ( ! _dotLayer)
{
- UIImage *dotImage = [self dotImage];
- _dotLayer = [CALayer layer];
- _dotLayer.bounds = CGRectMake(0, 0, dotImage.size.width, dotImage.size.height);
- _dotLayer.contents = (__bridge id)[dotImage CGImage];
- _dotLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
-
- CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
- animation.repeatCount = MAXFLOAT;
- animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 1.0)];
- animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.8, 0.8, 1.0)];
- animation.removedOnCompletion = NO;
- animation.autoreverses = YES;
- animation.duration = 1.5;
- animation.beginTime = CACurrentMediaTime() + 1.0;
- animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
-
- [_dotLayer addAnimation:animation forKey:@"animateTransform"];
+ _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];
}
}
}
-- (UIImage *)accuracyRingImage
+- (CALayer *)circleLayerWithSize:(CGFloat)layerSize
{
- CGFloat latRadians = self.annotation.coordinate.latitude * M_PI / 180.0f;
- CGFloat pixelRadius = self.annotation.location.horizontalAccuracy / cos(latRadians) / [self.mapView metersPerPixelAtLatitude:self.annotation.coordinate.latitude];
- UIGraphicsBeginImageContextWithOptions(CGSizeMake(pixelRadius * 2, pixelRadius * 2), NO, [[UIScreen mainScreen] scale]);
+ 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;
- CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), [[UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.7] CGColor]);
- CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [[UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.15] CGColor]);
- CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 2.0);
- CGContextStrokeEllipseInRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, pixelRadius * 2, pixelRadius * 2));
+ return circleLayer;
+}
+
+- (CAAnimationGroup *)loopingAnimationGroupWithDuration:(CGFloat)animationDuration
+{
+ CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
+ animationGroup.duration = animationDuration;
+ animationGroup.repeatCount = INFINITY;
+ animationGroup.removedOnCompletion = NO;
+ animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
- UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
- return finalImage;
+ return animationGroup;
}
-- (UIImage *)trackingDotHaloImage
+- (CGFloat)calculateAccuracyRingSize
{
- UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), NO, [[UIScreen mainScreen] scale]);
- CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [[_mapView.tintColor colorWithAlphaComponent:0.75] CGColor]);
- CGContextFillEllipseInRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, 100, 100));
- UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
+ CGFloat latRadians = self.annotation.coordinate.latitude * M_PI / 180.0f;
+ CGFloat pixelRadius = self.annotation.location.horizontalAccuracy / cos(latRadians) / [self.mapView metersPerPixelAtLatitude:self.annotation.coordinate.latitude];
- return finalImage;
+ return pixelRadius * 2;
}
-- (UIImage *)dotImage
+- (UIImage *)headingIndicatorTintedGradientImage
{
- CGFloat tintedWidth = MGLTrackingDotRingWidth * 0.7;
+ UIImage *image;
- CGRect rect = CGRectMake(0, 0, tintedWidth, tintedWidth);
+ CGFloat haloRadius = MGLUserLocationAnnotationHaloSize / 2.0;
- UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]);
+ UIGraphicsBeginImageContextWithOptions(CGSizeMake(MGLUserLocationAnnotationHaloSize, haloRadius), NO, 0);
+
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = UIGraphicsGetCurrentContext();
- CGContextSetFillColorWithColor(context, [_mapView.tintColor CGColor]);
- CGContextFillEllipseInRect(context, CGRectMake((rect.size.width - tintedWidth) / 2.0, (rect.size.height - tintedWidth) / 2.0, tintedWidth, tintedWidth));
+ // 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);
- UIImage *tintedForeground = UIGraphicsGetImageFromCurrentImageContext();
+ // 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,
+ nil);
+ image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
- return tintedForeground;
+ 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;
}
@end