diff options
Diffstat (limited to 'platform/ios/src')
-rw-r--r-- | platform/ios/src/MGLFaux3DUserLocationAnnotationView.h | 7 | ||||
-rw-r--r-- | platform/ios/src/MGLFaux3DUserLocationAnnotationView.m | 510 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 52 | ||||
-rw-r--r-- | platform/ios/src/MGLUserLocationAnnotationView.h | 52 | ||||
-rw-r--r-- | platform/ios/src/MGLUserLocationAnnotationView.m | 543 | ||||
-rw-r--r-- | platform/ios/src/MGLUserLocationAnnotationView_Private.h | 15 | ||||
-rw-r--r-- | platform/ios/src/Mapbox.h | 1 |
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" |