From f489ec2ee131725b42162edf0414241f9c6310d1 Mon Sep 17 00:00:00 2001 From: Jason Wray Date: Tue, 16 Aug 2016 15:16:59 -0400 Subject: [ios] Refactored user location annotation into a customizable class (#5882) A new class, `MGLUserLocationAnnotationView`, has been added that inherits from `MGLAnnotationView`. Use a subclass of `MGLUserLocationAnnotationView` to customize the appearance of the user location annotation. Use your custom view with the `MGLMapView.userLocation` annotation via the `-mapView:viewForAnnotation:` delegate method. --- platform/ios/CHANGELOG.md | 1 + platform/ios/app/MBXUserLocationAnnotationView.h | 5 + platform/ios/app/MBXUserLocationAnnotationView.m | 165 +++++++ platform/ios/app/MBXViewController.m | 24 +- platform/ios/ios.xcodeproj/project.pbxproj | 52 +- platform/ios/jazzy.yml | 1 + .../ios/src/MGLFaux3DUserLocationAnnotationView.h | 7 + .../ios/src/MGLFaux3DUserLocationAnnotationView.m | 510 +++++++++++++++++++ platform/ios/src/MGLMapView.mm | 52 +- platform/ios/src/MGLUserLocationAnnotationView.h | 52 +- platform/ios/src/MGLUserLocationAnnotationView.m | 543 +-------------------- .../src/MGLUserLocationAnnotationView_Private.h | 15 + platform/ios/src/Mapbox.h | 1 + 13 files changed, 868 insertions(+), 560 deletions(-) create mode 100644 platform/ios/app/MBXUserLocationAnnotationView.h create mode 100644 platform/ios/app/MBXUserLocationAnnotationView.m create mode 100644 platform/ios/src/MGLFaux3DUserLocationAnnotationView.h create mode 100644 platform/ios/src/MGLFaux3DUserLocationAnnotationView.m create mode 100644 platform/ios/src/MGLUserLocationAnnotationView_Private.h (limited to 'platform/ios') diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index e4f8653a40..49e6369b6f 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -5,6 +5,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT ## master * A new runtime styling API allows you to adjust the style and content of the base map dynamically. All the options available in [Mapbox Studio](https://www.mapbox.com/studio/) are now exposed via MGLStyle and subclasses of MGLStyleLayer and MGLSource. ([#5727](https://github.com/mapbox/mapbox-gl-native/pull/5727)) +* The user location annotation is now customizable via the newly added `MGLUserLocationAnnotationView` class. ([#5882](https://github.com/mapbox/mapbox-gl-native/pull/5882)) * Simulator architecture slices are included in the included dSYM file, allowing you to symbolicate crashes that occur in the Simulator. ([#5740](https://github.com/mapbox/mapbox-gl-native/pull/5740)) * As the user zooms in, tiles from lower zoom levels are scaled up until tiles for higher zoom levels are loaded. ([#5143](https://github.com/mapbox/mapbox-gl-native/pull/5143)) * Fixed an issue causing the wrong annotation view to be selected when tapping an annotation view with a center offset applied. ([#5931](https://github.com/mapbox/mapbox-gl-native/pull/5931)) diff --git a/platform/ios/app/MBXUserLocationAnnotationView.h b/platform/ios/app/MBXUserLocationAnnotationView.h new file mode 100644 index 0000000000..39ed729d2b --- /dev/null +++ b/platform/ios/app/MBXUserLocationAnnotationView.h @@ -0,0 +1,5 @@ +#import + +@interface MBXUserLocationAnnotationView : MGLUserLocationAnnotationView + +@end diff --git a/platform/ios/app/MBXUserLocationAnnotationView.m b/platform/ios/app/MBXUserLocationAnnotationView.m new file mode 100644 index 0000000000..a0347a174f --- /dev/null +++ b/platform/ios/app/MBXUserLocationAnnotationView.m @@ -0,0 +1,165 @@ +#import "MBXUserLocationAnnotationView.h" + +const CGFloat MBXUserLocationDotSize = 10; + +@implementation MBXUserLocationAnnotationView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self == nil) return nil; + self.backgroundColor = [UIColor clearColor]; + return self; +} + +- (void)update +{ + [self updateFrameWithSize:self.intrinsicContentSize]; + [self setNeedsDisplay]; +} + + +- (CGSize)intrinsicContentSize +{ + CGSize carSize = CGSizeMake(30, 60); + return (self.mapView.userTrackingMode == MGLUserTrackingModeFollowWithCourse) ? carSize : [self dotSize]; +} + +- (CGSize)dotSize +{ + CGFloat minDotSize = 30; + CGFloat dotSize = MAX(minDotSize, self.accuracyInPoints); + return CGSizeMake(dotSize, dotSize); +} + +- (void)updateFrameWithSize:(CGSize)size +{ + if (CGSizeEqualToSize(self.frame.size, size)) return; + + // Update frame size, keeping the existing center point. + CGRect newFrame = self.frame; + CGPoint oldCenter = self.center; + newFrame.size = size; + self.frame = newFrame; + self.center = oldCenter; +} + +- (CGFloat)accuracyInPoints +{ + CGFloat metersPerPoint = [self.mapView metersPerPointAtLatitude:self.userLocation.location.coordinate.latitude]; + return self.userLocation.location.horizontalAccuracy / metersPerPoint; +} + +- (void)drawRect:(CGRect)rect +{ + (self.mapView.userTrackingMode == MGLUserTrackingModeFollowWithCourse) ? [self drawCar] : [self drawDot]; +} + +- (void)drawDot +{ + // Accuracy + CGFloat accuracy = self.accuracyInPoints; + + CGFloat center = self.bounds.size.width / 2.0 - accuracy / 2.0; + UIBezierPath *accuracyPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(center, center, accuracy, accuracy)]; + UIColor *accuracyColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:.4]; + [accuracyColor setFill]; + [accuracyPath fill]; + + // Dot + center = self.bounds.size.width / 2.0 - MBXUserLocationDotSize / 2.0; + UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(center, center, MBXUserLocationDotSize, MBXUserLocationDotSize)]; + [UIColor.greenColor setFill]; + [ovalPath fill]; + + [UIColor.blackColor setStroke]; + ovalPath.lineWidth = 1; + [ovalPath stroke]; + + // Accuracy text + UIFont *font = [UIFont systemFontOfSize:11]; + [[NSString stringWithFormat:@"%.0f", accuracy] + drawAtPoint:CGPointZero withAttributes:@{NSFontAttributeName: font, + NSBackgroundColorAttributeName: [UIColor colorWithWhite:0 alpha:.5], + NSForegroundColorAttributeName: [UIColor whiteColor]}]; +} + +- (void)drawCar +{ + UIColor* fillColor = [UIColor colorWithRed: 0 green: 0 blue: 0 alpha: 1]; + UIColor* strokeColor = [UIColor colorWithRed: 0.592 green: 0.592 blue: 0.592 alpha: 1]; + UIColor* fillColor2 = [UIColor colorWithRed: 1 green: 1 blue: 1 alpha: 1]; + + UIBezierPath* bezier2Path = [UIBezierPath bezierPath]; + [bezier2Path moveToPoint: CGPointMake(30, 7.86)]; + [bezier2Path addLineToPoint: CGPointMake(30, 52.66)]; + [bezier2Path addCurveToPoint: CGPointMake(0, 52.66) controlPoint1: CGPointMake(30, 62.05) controlPoint2: CGPointMake(0, 62.84)]; + [bezier2Path addCurveToPoint: CGPointMake(0, 7.86) controlPoint1: CGPointMake(0, 42.48) controlPoint2: CGPointMake(0, 17.89)]; + [bezier2Path addCurveToPoint: CGPointMake(30, 7.86) controlPoint1: CGPointMake(-0, -2.17) controlPoint2: CGPointMake(30, -3.05)]; + [bezier2Path closePath]; + bezier2Path.usesEvenOddFillRule = YES; + + [fillColor setFill]; + [bezier2Path fill]; + + UIBezierPath* bezier3Path = [UIBezierPath bezierPath]; + [bezier3Path moveToPoint: CGPointMake(30, 7.86)]; + [bezier3Path addLineToPoint: CGPointMake(30, 52.66)]; + [bezier3Path addCurveToPoint: CGPointMake(0, 52.66) controlPoint1: CGPointMake(30, 62.05) controlPoint2: CGPointMake(0, 62.84)]; + [bezier3Path addCurveToPoint: CGPointMake(0, 7.86) controlPoint1: CGPointMake(0, 42.48) controlPoint2: CGPointMake(0, 17.89)]; + [bezier3Path addCurveToPoint: CGPointMake(30, 7.86) controlPoint1: CGPointMake(0, -2.17) controlPoint2: CGPointMake(30, -3.05)]; + [bezier3Path closePath]; + [strokeColor setStroke]; + bezier3Path.lineWidth = 1; + [bezier3Path stroke]; + + UIBezierPath* bezier4Path = [UIBezierPath bezierPath]; + [bezier4Path moveToPoint: CGPointMake(15.56, 4.26)]; + [bezier4Path addCurveToPoint: CGPointMake(26, 6) controlPoint1: CGPointMake(21, 4.26) controlPoint2: CGPointMake(26, 6)]; + [bezier4Path addCurveToPoint: CGPointMake(23, 21) controlPoint1: CGPointMake(26, 6) controlPoint2: CGPointMake(29, 17)]; + [bezier4Path addCurveToPoint: CGPointMake(16, 21) controlPoint1: CGPointMake(20.03, 22.98) controlPoint2: CGPointMake(16, 21)]; + [bezier4Path addCurveToPoint: CGPointMake(7, 21) controlPoint1: CGPointMake(16, 21) controlPoint2: CGPointMake(9.02, 23.53)]; + [bezier4Path addCurveToPoint: CGPointMake(4, 6) controlPoint1: CGPointMake(3, 16) controlPoint2: CGPointMake(4, 6)]; + [bezier4Path addCurveToPoint: CGPointMake(15.56, 4.26) controlPoint1: CGPointMake(4, 6) controlPoint2: CGPointMake(10.12, 4.26)]; + [bezier4Path closePath]; + bezier4Path.usesEvenOddFillRule = YES; + + [fillColor2 setFill]; + [bezier4Path fill]; + + UIBezierPath* rectanglePath = [UIBezierPath bezierPath]; + [rectanglePath moveToPoint: CGPointMake(25, 46)]; + [rectanglePath addCurveToPoint: CGPointMake(21, 55) controlPoint1: CGPointMake(31, 46) controlPoint2: CGPointMake(28.5, 55)]; + [rectanglePath addCurveToPoint: CGPointMake(9, 55) controlPoint1: CGPointMake(13.5, 55) controlPoint2: CGPointMake(14, 55)]; + [rectanglePath addCurveToPoint: CGPointMake(5, 46) controlPoint1: CGPointMake(4, 55) controlPoint2: CGPointMake(0, 46)]; + [rectanglePath addCurveToPoint: CGPointMake(25, 46) controlPoint1: CGPointMake(10, 46) controlPoint2: CGPointMake(19, 46)]; + [rectanglePath closePath]; + [UIColor.whiteColor setFill]; + [rectanglePath fill]; + + UIBezierPath* bezierPath = [UIBezierPath bezierPath]; + [UIColor.whiteColor setFill]; + [bezierPath fill]; + + UIBezierPath* rectangle2Path = [UIBezierPath bezierPath]; + [rectangle2Path moveToPoint: CGPointMake(2, 35)]; + [rectangle2Path addCurveToPoint: CGPointMake(4.36, 35) controlPoint1: CGPointMake(2, 39) controlPoint2: CGPointMake(4.36, 35)]; + [rectangle2Path addCurveToPoint: CGPointMake(4.36, 22) controlPoint1: CGPointMake(4.36, 35) controlPoint2: CGPointMake(5.55, 26)]; + [rectangle2Path addCurveToPoint: CGPointMake(2, 22) controlPoint1: CGPointMake(3.18, 18) controlPoint2: CGPointMake(2, 22)]; + [rectangle2Path addCurveToPoint: CGPointMake(2, 35) controlPoint1: CGPointMake(2, 22) controlPoint2: CGPointMake(2, 31)]; + [rectangle2Path closePath]; + [UIColor.whiteColor setFill]; + [rectangle2Path fill]; + + UIBezierPath* rectangle3Path = [UIBezierPath bezierPath]; + [rectangle3Path moveToPoint: CGPointMake(28, 35)]; + [rectangle3Path addCurveToPoint: CGPointMake(25.64, 35) controlPoint1: CGPointMake(28, 39) controlPoint2: CGPointMake(25.64, 35)]; + [rectangle3Path addCurveToPoint: CGPointMake(25.64, 22) controlPoint1: CGPointMake(25.64, 35) controlPoint2: CGPointMake(24.45, 26)]; + [rectangle3Path addCurveToPoint: CGPointMake(28, 22) controlPoint1: CGPointMake(26.82, 18) controlPoint2: CGPointMake(28, 22)]; + [rectangle3Path addCurveToPoint: CGPointMake(28, 35) controlPoint1: CGPointMake(28, 22) controlPoint2: CGPointMake(28, 31)]; + [rectangle3Path closePath]; + [UIColor.whiteColor setFill]; + [rectangle3Path fill]; +} + +@end diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index f1eed9625b..4c98dc75fb 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -4,6 +4,7 @@ #import "MBXCustomCalloutView.h" #import "MBXOfflinePacksTableViewController.h" #import "MBXAnnotationView.h" +#import "MBXUserLocationAnnotationView.h" #import "MGLFillStyleLayer.h" #import @@ -42,6 +43,7 @@ static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXVie @property (nonatomic) IBOutlet MGLMapView *mapView; @property (nonatomic) NSInteger styleIndex; @property (nonatomic) BOOL debugLoggingEnabled; +@property (nonatomic) BOOL customUserLocationAnnnotationEnabled; @end @@ -201,7 +203,10 @@ static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXVie @"Start World Tour", @"Add Custom Callout Point", @"Remove Annotations", - @"Runtime styling", + @"Runtime Styling", + ((_customUserLocationAnnnotationEnabled) + ? @"Disable Custom User Dot" + : @"Enable Custom User Dot"), nil]; if (self.debugLoggingEnabled) @@ -283,6 +288,12 @@ static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXVie { [self testRuntimeStyling]; } + else if (buttonIndex == actionSheet.firstOtherButtonIndex + 17) + { + _customUserLocationAnnnotationEnabled = !_customUserLocationAnnnotationEnabled; + self.mapView.showsUserLocation = NO; + self.mapView.userTrackingMode = MGLUserTrackingModeFollow; + } else if (buttonIndex == actionSheet.numberOfButtons - 2 && self.debugLoggingEnabled) { NSString *fileContents = [NSString stringWithContentsOfFile:[self telemetryDebugLogfilePath] encoding:NSUTF8StringEncoding error:nil]; @@ -697,6 +708,17 @@ static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXVie - (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id)annotation { + if (annotation == mapView.userLocation) + { + if (_customUserLocationAnnnotationEnabled) + { + MBXUserLocationAnnotationView *annotationView = [[MBXUserLocationAnnotationView alloc] initWithFrame:CGRectZero]; + annotationView.frame = CGRectMake(0, 0, annotationView.intrinsicContentSize.width, annotationView.intrinsicContentSize.height); + return annotationView; + } + + return nil; + } // Use GL backed pins for dropped pin annotations if ([annotation isKindOfClass:[MBXDroppedPinAnnotation class]] || [annotation isKindOfClass:[MBXSpriteBackedAnnotation class]]) { diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index f822e587d4..65e0d00dea 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -78,6 +78,11 @@ 353933FE1D3FB7DD003F57D7 /* MGLSymbolStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 353933FD1D3FB7DD003F57D7 /* MGLSymbolStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 353933FF1D3FB7DD003F57D7 /* MGLSymbolStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 353933FD1D3FB7DD003F57D7 /* MGLSymbolStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 353D23961D0B0DFE002BE09D /* MGLAnnotationViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 353D23951D0B0DFE002BE09D /* MGLAnnotationViewTests.m */; }; + 354B83961D2E873E005D9406 /* MGLUserLocationAnnotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = 354B83941D2E873E005D9406 /* MGLUserLocationAnnotationView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 354B83971D2E873E005D9406 /* MGLUserLocationAnnotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = 354B83941D2E873E005D9406 /* MGLUserLocationAnnotationView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 354B83981D2E873E005D9406 /* MGLUserLocationAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 354B83951D2E873E005D9406 /* MGLUserLocationAnnotationView.m */; }; + 354B83991D2E873E005D9406 /* MGLUserLocationAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 354B83951D2E873E005D9406 /* MGLUserLocationAnnotationView.m */; }; + 354B839C1D2E9B48005D9406 /* MBXUserLocationAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 354B839B1D2E9B48005D9406 /* MBXUserLocationAnnotationView.m */; }; 354D42DC1D4919F900F400A1 /* NSValue+MGLStyleAttributeAdditions_Private.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 354D42DB1D4919F900F400A1 /* NSValue+MGLStyleAttributeAdditions_Private.hpp */; }; 354D42DD1D4919F900F400A1 /* NSValue+MGLStyleAttributeAdditions_Private.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 354D42DB1D4919F900F400A1 /* NSValue+MGLStyleAttributeAdditions_Private.hpp */; }; 35599DEB1D46F14E0048254D /* MGLStyleAttributeFunction.h in Headers */ = {isa = PBXBuildFile; fileRef = 35599DE91D46F14E0048254D /* MGLStyleAttributeFunction.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -109,6 +114,7 @@ 3593E5241D529C29006D9365 /* MGLStyleAttribute.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3593E5201D529C29006D9365 /* MGLStyleAttribute.mm */; }; 3593E5261D529EDC006D9365 /* UIColor+MGLStyleAttributeAdditions_Private.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3593E5251D529EDC006D9365 /* UIColor+MGLStyleAttributeAdditions_Private.hpp */; }; 3593E5271D529EDC006D9365 /* UIColor+MGLStyleAttributeAdditions_Private.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3593E5251D529EDC006D9365 /* UIColor+MGLStyleAttributeAdditions_Private.hpp */; }; + 359F57461D2FDDA6005217F1 /* MGLUserLocationAnnotationView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 359F57451D2FDBD5005217F1 /* MGLUserLocationAnnotationView_Private.h */; }; 35CE61821D4165D9004F2359 /* UIColor+MGLAdditions.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 35CE61801D4165D9004F2359 /* UIColor+MGLAdditions.hpp */; }; 35CE61831D4165D9004F2359 /* UIColor+MGLAdditions.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 35CE61801D4165D9004F2359 /* UIColor+MGLAdditions.hpp */; }; 35CE61841D4165D9004F2359 /* UIColor+MGLAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 35CE61811D4165D9004F2359 /* UIColor+MGLAdditions.mm */; }; @@ -249,8 +255,8 @@ DA8848591CBAFB9800AB86E3 /* MGLMapView.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA88484A1CBAFB9800AB86E3 /* MGLMapView.mm */; }; DA88485A1CBAFB9800AB86E3 /* MGLUserLocation_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DA88484B1CBAFB9800AB86E3 /* MGLUserLocation_Private.h */; }; DA88485B1CBAFB9800AB86E3 /* MGLUserLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = DA88484C1CBAFB9800AB86E3 /* MGLUserLocation.m */; }; - DA88485C1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = DA88484D1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.h */; }; - DA88485D1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA88484E1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.m */; }; + DA88485C1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.h in Headers */ = {isa = PBXBuildFile; fileRef = DA88484D1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.h */; }; + DA88485D1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA88484E1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.m */; }; DA8848601CBAFC2E00AB86E3 /* Mapbox.h in Headers */ = {isa = PBXBuildFile; fileRef = DA88485E1CBAFC2E00AB86E3 /* Mapbox.h */; settings = {ATTRIBUTES = (Public, ); }; }; DA88486D1CBAFCC100AB86E3 /* Compass.png in Resources */ = {isa = PBXBuildFile; fileRef = DA8848631CBAFCC100AB86E3 /* Compass.png */; }; DA88486E1CBAFCC100AB86E3 /* Compass@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DA8848641CBAFCC100AB86E3 /* Compass@2x.png */; }; @@ -321,7 +327,7 @@ DAA4E4311CBB730400178DFB /* MGLMapboxEvents.m in Sources */ = {isa = PBXBuildFile; fileRef = DA8848491CBAFB9800AB86E3 /* MGLMapboxEvents.m */; }; DAA4E4321CBB730400178DFB /* MGLMapView.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA88484A1CBAFB9800AB86E3 /* MGLMapView.mm */; }; DAA4E4331CBB730400178DFB /* MGLUserLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = DA88484C1CBAFB9800AB86E3 /* MGLUserLocation.m */; }; - DAA4E4341CBB730400178DFB /* MGLUserLocationAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA88484E1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.m */; }; + DAA4E4341CBB730400178DFB /* MGLFaux3DUserLocationAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA88484E1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.m */; }; DAA4E4351CBB730400178DFB /* SMCalloutView.m in Sources */ = {isa = PBXBuildFile; fileRef = DA88488A1CBB037E00AB86E3 /* SMCalloutView.m */; }; DAABF73D1CBC59BB005B1825 /* libmbgl-core.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAABF73B1CBC59BB005B1825 /* libmbgl-core.a */; }; DABCABAC1CB80692000A7C39 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DABCABAB1CB80692000A7C39 /* main.m */; }; @@ -488,6 +494,10 @@ 353933FA1D3FB7C0003F57D7 /* MGLRasterStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLRasterStyleLayer.h; sourceTree = ""; }; 353933FD1D3FB7DD003F57D7 /* MGLSymbolStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLSymbolStyleLayer.h; sourceTree = ""; }; 353D23951D0B0DFE002BE09D /* MGLAnnotationViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLAnnotationViewTests.m; sourceTree = ""; }; + 354B83941D2E873E005D9406 /* MGLUserLocationAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLUserLocationAnnotationView.h; sourceTree = ""; }; + 354B83951D2E873E005D9406 /* MGLUserLocationAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLUserLocationAnnotationView.m; sourceTree = ""; }; + 354B839A1D2E9B48005D9406 /* MBXUserLocationAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBXUserLocationAnnotationView.h; sourceTree = ""; }; + 354B839B1D2E9B48005D9406 /* MBXUserLocationAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBXUserLocationAnnotationView.m; sourceTree = ""; }; 354D42DB1D4919F900F400A1 /* NSValue+MGLStyleAttributeAdditions_Private.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = "NSValue+MGLStyleAttributeAdditions_Private.hpp"; sourceTree = ""; }; 35599DE91D46F14E0048254D /* MGLStyleAttributeFunction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLStyleAttributeFunction.h; sourceTree = ""; }; 35599DEA1D46F14E0048254D /* MGLStyleAttributeFunction.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLStyleAttributeFunction.mm; sourceTree = ""; }; @@ -508,6 +518,7 @@ 3593E51F1D529C29006D9365 /* MGLStyleAttribute.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MGLStyleAttribute.hpp; sourceTree = ""; }; 3593E5201D529C29006D9365 /* MGLStyleAttribute.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLStyleAttribute.mm; sourceTree = ""; }; 3593E5251D529EDC006D9365 /* UIColor+MGLStyleAttributeAdditions_Private.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = "UIColor+MGLStyleAttributeAdditions_Private.hpp"; sourceTree = ""; }; + 359F57451D2FDBD5005217F1 /* MGLUserLocationAnnotationView_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MGLUserLocationAnnotationView_Private.h; sourceTree = ""; }; 35CE61801D4165D9004F2359 /* UIColor+MGLAdditions.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = "UIColor+MGLAdditions.hpp"; sourceTree = ""; }; 35CE61811D4165D9004F2359 /* UIColor+MGLAdditions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "UIColor+MGLAdditions.mm"; sourceTree = ""; }; 35D13AB51D3D15E300AFB4E0 /* MGLStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLStyleLayer.h; sourceTree = ""; }; @@ -642,8 +653,8 @@ DA88484A1CBAFB9800AB86E3 /* MGLMapView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapView.mm; sourceTree = ""; }; DA88484B1CBAFB9800AB86E3 /* MGLUserLocation_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLUserLocation_Private.h; sourceTree = ""; }; DA88484C1CBAFB9800AB86E3 /* MGLUserLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLUserLocation.m; sourceTree = ""; }; - DA88484D1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLUserLocationAnnotationView.h; sourceTree = ""; }; - DA88484E1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLUserLocationAnnotationView.m; sourceTree = ""; }; + DA88484D1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLFaux3DUserLocationAnnotationView.h; sourceTree = ""; }; + DA88484E1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLFaux3DUserLocationAnnotationView.m; sourceTree = ""; }; DA88485E1CBAFC2E00AB86E3 /* Mapbox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Mapbox.h; path = src/Mapbox.h; sourceTree = SOURCE_ROOT; }; DA8848631CBAFCC100AB86E3 /* Compass.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Compass.png; sourceTree = ""; }; DA8848641CBAFCC100AB86E3 /* Compass@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Compass@2x.png"; sourceTree = ""; }; @@ -914,6 +925,8 @@ 40FDA76A1CCAAA6800442548 /* MBXAnnotationView.m */, DA1DC9661CB6C6B7006E619F /* MBXCustomCalloutView.h */, DA1DC9671CB6C6B7006E619F /* MBXCustomCalloutView.m */, + 354B839A1D2E9B48005D9406 /* MBXUserLocationAnnotationView.h */, + 354B839B1D2E9B48005D9406 /* MBXUserLocationAnnotationView.m */, DA1DC9681CB6C6B7006E619F /* MBXOfflinePacksTableViewController.h */, DA1DC9691CB6C6B7006E619F /* MBXOfflinePacksTableViewController.m */, DA1DC9531CB6C1C2006E619F /* MBXViewController.h */, @@ -1257,20 +1270,23 @@ 40EDA1BD1CFE0D4A00D9EA68 /* MGLAnnotationContainerView.h */, 404326881D5B9B1A007111BD /* MGLAnnotationContainerView_Private.h */, 40EDA1BE1CFE0D4A00D9EA68 /* MGLAnnotationContainerView.m */, - 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */, - 4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */, - 4018B1C41CDC277F00F666AF /* MGLAnnotationView.mm */, - DA8848341CBAFB8500AB86E3 /* MGLAnnotationImage.h */, DA8848401CBAFB9800AB86E3 /* MGLAnnotationImage_Private.h */, + DA8848341CBAFB8500AB86E3 /* MGLAnnotationImage.h */, DA8848411CBAFB9800AB86E3 /* MGLAnnotationImage.m */, + 4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */, + 4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */, + 4018B1C41CDC277F00F666AF /* MGLAnnotationView.mm */, DA8848351CBAFB8500AB86E3 /* MGLCalloutView.h */, DA8848441CBAFB9800AB86E3 /* MGLCompactCalloutView.h */, DA8848451CBAFB9800AB86E3 /* MGLCompactCalloutView.m */, - DA8848391CBAFB8500AB86E3 /* MGLUserLocation.h */, + DA88484D1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.h */, + DA88484E1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.m */, DA88484B1CBAFB9800AB86E3 /* MGLUserLocation_Private.h */, + DA8848391CBAFB8500AB86E3 /* MGLUserLocation.h */, DA88484C1CBAFB9800AB86E3 /* MGLUserLocation.m */, - DA88484D1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.h */, - DA88484E1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.m */, + 359F57451D2FDBD5005217F1 /* MGLUserLocationAnnotationView_Private.h */, + 354B83941D2E873E005D9406 /* MGLUserLocationAnnotationView.h */, + 354B83951D2E873E005D9406 /* MGLUserLocationAnnotationView.m */, ); name = Annotations; sourceTree = ""; @@ -1323,6 +1339,7 @@ DA88485A1CBAFB9800AB86E3 /* MGLUserLocation_Private.h in Headers */, DA27C24F1CBB4C11000B0ECD /* MGLAccountManager_Private.h in Headers */, DA8847FC1CBAFA5100AB86E3 /* MGLStyle.h in Headers */, + 354B83961D2E873E005D9406 /* MGLUserLocationAnnotationView.h in Headers */, DA8847F01CBAFA5100AB86E3 /* MGLAnnotation.h in Headers */, DA88483E1CBAFB8500AB86E3 /* MGLMapView+MGLCustomStyleLayerAdditions.h in Headers */, 4018B1CA1CDC288E00F666AF /* MGLAnnotationView.h in Headers */, @@ -1372,12 +1389,13 @@ DA737EE11D056A4E005BDA16 /* MGLMapViewDelegate.h in Headers */, DA8848851CBB033F00AB86E3 /* FABKitProtocol.h in Headers */, DA88481B1CBAFA6200AB86E3 /* MGLGeometry_Private.h in Headers */, - DA88485C1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.h in Headers */, 350098CA1D482D9C004B2AF0 /* NSArray+MGLStyleAttributeAdditions.h in Headers */, 350098AF1D47E6F4004B2AF0 /* UIColor+MGLStyleAttributeAdditions.h in Headers */, + DA88485C1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.h in Headers */, DA8848871CBB033F00AB86E3 /* Fabric.h in Headers */, 350098D81D4830D5004B2AF0 /* NSString+MGLStyleAttributeAdditions_Private.hpp in Headers */, 35305D4A1D22AA6A0007D005 /* NSData+MGLAdditions.h in Headers */, + 359F57461D2FDDA6005217F1 /* MGLUserLocationAnnotationView_Private.h in Headers */, DA8848841CBB033F00AB86E3 /* FABAttributes.h in Headers */, 3538AA171D541C43008EC33D /* MGLStyleFilter.h in Headers */, DA8847FD1CBAFA5100AB86E3 /* MGLTilePyramidOfflineRegion.h in Headers */, @@ -1444,6 +1462,7 @@ DABFB8631CBE99E500D62B32 /* MGLOfflineRegion.h in Headers */, DA35A2B21CCA141D00E826B2 /* MGLCompassDirectionFormatter.h in Headers */, DABFB8731CBE9A9900D62B32 /* Mapbox.h in Headers */, + 354B83971D2E873E005D9406 /* MGLUserLocationAnnotationView.h in Headers */, DABFB86B1CBE99E500D62B32 /* MGLTilePyramidOfflineRegion.h in Headers */, 4018B1CB1CDC288E00F666AF /* MGLAnnotationView.h in Headers */, DABFB85F1CBE99E500D62B32 /* MGLGeometry.h in Headers */, @@ -1750,6 +1769,7 @@ buildActionMask = 2147483647; files = ( DA1DC9971CB6E046006E619F /* main.m in Sources */, + 354B839C1D2E9B48005D9406 /* MBXUserLocationAnnotationView.m in Sources */, DA1DC9991CB6E054006E619F /* MBXAppDelegate.m in Sources */, DA1DC96B1CB6C6B7006E619F /* MBXOfflinePacksTableViewController.m in Sources */, DA1DC96A1CB6C6B7006E619F /* MBXCustomCalloutView.m in Sources */, @@ -1790,10 +1810,11 @@ files = ( 35136D391D42271A00C20EFD /* MGLBackgroundStyleLayer.mm in Sources */, 350098D51D4830A6004B2AF0 /* NSString+MGLStyleAttributeAdditions.mm in Sources */, - DA88485D1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.m in Sources */, 3593E5231D529C29006D9365 /* MGLStyleAttribute.mm in Sources */, 350098B11D47E6F4004B2AF0 /* UIColor+MGLStyleAttributeAdditions.mm in Sources */, DAED38651D62D0FC00D7640F /* NSURL+MGLAdditions.m in Sources */, + 354B83981D2E873E005D9406 /* MGLUserLocationAnnotationView.m in Sources */, + DA88485D1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.m in Sources */, DAD165701CF41981001FF4B9 /* MGLFeature.mm in Sources */, 350098C31D48149E004B2AF0 /* NSNumber+MGLStyleAttributeAdditions.mm in Sources */, 40EDA1C11CFE0E0500D9EA68 /* MGLAnnotationContainerView.m in Sources */, @@ -1853,6 +1874,7 @@ files = ( 35136D3A1D42271A00C20EFD /* MGLBackgroundStyleLayer.mm in Sources */, 350098D61D4830A6004B2AF0 /* NSString+MGLStyleAttributeAdditions.mm in Sources */, + 354B83991D2E873E005D9406 /* MGLUserLocationAnnotationView.m in Sources */, DAA4E4221CBB730400178DFB /* MGLPointAnnotation.m in Sources */, 3593E5241D529C29006D9365 /* MGLStyleAttribute.mm in Sources */, 350098B21D47E6F4004B2AF0 /* UIColor+MGLStyleAttributeAdditions.mm in Sources */, @@ -1895,7 +1917,7 @@ DAA4E4321CBB730400178DFB /* MGLMapView.mm in Sources */, DAA4E41E1CBB730400178DFB /* MGLMapCamera.mm in Sources */, 4018B1C81CDC287F00F666AF /* MGLAnnotationView.mm in Sources */, - DAA4E4341CBB730400178DFB /* MGLUserLocationAnnotationView.m in Sources */, + DAA4E4341CBB730400178DFB /* MGLFaux3DUserLocationAnnotationView.m in Sources */, DAA4E4311CBB730400178DFB /* MGLMapboxEvents.m in Sources */, DAA4E4231CBB730400178DFB /* MGLPolygon.mm in Sources */, 35D13AC61D3D19DD00AFB4E0 /* MGLFillStyleLayer.mm in Sources */, diff --git a/platform/ios/jazzy.yml b/platform/ios/jazzy.yml index ce09b8b39d..f1c5d28070 100644 --- a/platform/ios/jazzy.yml +++ b/platform/ios/jazzy.yml @@ -40,6 +40,7 @@ custom_categories: - MGLShape - MGLShapeCollection - MGLUserLocation + - MGLUserLocationAnnotationView - name: Map Data children: - MGLFeature 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 +#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 #import +#import #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)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" -- cgit v1.2.1