summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
authorJason Wray <friedbunny@users.noreply.github.com>2016-08-16 15:16:59 -0400
committerGitHub <noreply@github.com>2016-08-16 15:16:59 -0400
commitf489ec2ee131725b42162edf0414241f9c6310d1 (patch)
tree87d1621831a3386f49a6d4a47362c59e9a338d21 /platform
parent01b94de107de875a6889258e72e5e060c0de0881 (diff)
downloadqtlocation-mapboxgl-f489ec2ee131725b42162edf0414241f9c6310d1.tar.gz
[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.
Diffstat (limited to 'platform')
-rw-r--r--platform/ios/CHANGELOG.md1
-rw-r--r--platform/ios/app/MBXUserLocationAnnotationView.h5
-rw-r--r--platform/ios/app/MBXUserLocationAnnotationView.m165
-rw-r--r--platform/ios/app/MBXViewController.m24
-rw-r--r--platform/ios/ios.xcodeproj/project.pbxproj52
-rw-r--r--platform/ios/jazzy.yml1
-rw-r--r--platform/ios/src/MGLFaux3DUserLocationAnnotationView.h7
-rw-r--r--platform/ios/src/MGLFaux3DUserLocationAnnotationView.m510
-rw-r--r--platform/ios/src/MGLMapView.mm52
-rw-r--r--platform/ios/src/MGLUserLocationAnnotationView.h52
-rw-r--r--platform/ios/src/MGLUserLocationAnnotationView.m543
-rw-r--r--platform/ios/src/MGLUserLocationAnnotationView_Private.h15
-rw-r--r--platform/ios/src/Mapbox.h1
13 files changed, 868 insertions, 560 deletions
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 <Mapbox/Mapbox.h>
+
+@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 <Mapbox/Mapbox.h>
@@ -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<MGLAnnotation>)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 = "<group>"; };
353933FD1D3FB7DD003F57D7 /* MGLSymbolStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLSymbolStyleLayer.h; sourceTree = "<group>"; };
353D23951D0B0DFE002BE09D /* MGLAnnotationViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLAnnotationViewTests.m; sourceTree = "<group>"; };
+ 354B83941D2E873E005D9406 /* MGLUserLocationAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLUserLocationAnnotationView.h; sourceTree = "<group>"; };
+ 354B83951D2E873E005D9406 /* MGLUserLocationAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLUserLocationAnnotationView.m; sourceTree = "<group>"; };
+ 354B839A1D2E9B48005D9406 /* MBXUserLocationAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MBXUserLocationAnnotationView.h; sourceTree = "<group>"; };
+ 354B839B1D2E9B48005D9406 /* MBXUserLocationAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MBXUserLocationAnnotationView.m; sourceTree = "<group>"; };
354D42DB1D4919F900F400A1 /* NSValue+MGLStyleAttributeAdditions_Private.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = "NSValue+MGLStyleAttributeAdditions_Private.hpp"; sourceTree = "<group>"; };
35599DE91D46F14E0048254D /* MGLStyleAttributeFunction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLStyleAttributeFunction.h; sourceTree = "<group>"; };
35599DEA1D46F14E0048254D /* MGLStyleAttributeFunction.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLStyleAttributeFunction.mm; sourceTree = "<group>"; };
@@ -508,6 +518,7 @@
3593E51F1D529C29006D9365 /* MGLStyleAttribute.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MGLStyleAttribute.hpp; sourceTree = "<group>"; };
3593E5201D529C29006D9365 /* MGLStyleAttribute.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLStyleAttribute.mm; sourceTree = "<group>"; };
3593E5251D529EDC006D9365 /* UIColor+MGLStyleAttributeAdditions_Private.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = "UIColor+MGLStyleAttributeAdditions_Private.hpp"; sourceTree = "<group>"; };
+ 359F57451D2FDBD5005217F1 /* MGLUserLocationAnnotationView_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MGLUserLocationAnnotationView_Private.h; sourceTree = "<group>"; };
35CE61801D4165D9004F2359 /* UIColor+MGLAdditions.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = "UIColor+MGLAdditions.hpp"; sourceTree = "<group>"; };
35CE61811D4165D9004F2359 /* UIColor+MGLAdditions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "UIColor+MGLAdditions.mm"; sourceTree = "<group>"; };
35D13AB51D3D15E300AFB4E0 /* MGLStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLStyleLayer.h; sourceTree = "<group>"; };
@@ -642,8 +653,8 @@
DA88484A1CBAFB9800AB86E3 /* MGLMapView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapView.mm; sourceTree = "<group>"; };
DA88484B1CBAFB9800AB86E3 /* MGLUserLocation_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLUserLocation_Private.h; sourceTree = "<group>"; };
DA88484C1CBAFB9800AB86E3 /* MGLUserLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLUserLocation.m; sourceTree = "<group>"; };
- DA88484D1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLUserLocationAnnotationView.h; sourceTree = "<group>"; };
- DA88484E1CBAFB9800AB86E3 /* MGLUserLocationAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLUserLocationAnnotationView.m; sourceTree = "<group>"; };
+ DA88484D1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLFaux3DUserLocationAnnotationView.h; sourceTree = "<group>"; };
+ DA88484E1CBAFB9800AB86E3 /* MGLFaux3DUserLocationAnnotationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLFaux3DUserLocationAnnotationView.m; sourceTree = "<group>"; };
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 = "<group>"; };
DA8848641CBAFCC100AB86E3 /* Compass@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Compass@2x.png"; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 <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"