diff options
Diffstat (limited to 'platform/ios/src')
-rw-r--r-- | platform/ios/src/MGLAPIClient.h | 15 | ||||
-rw-r--r-- | platform/ios/src/MGLAPIClient.m | 202 | ||||
-rw-r--r-- | platform/ios/src/MGLAnnotationView.h | 8 | ||||
-rw-r--r-- | platform/ios/src/MGLAnnotationView.mm | 2 | ||||
-rw-r--r-- | platform/ios/src/MGLCalloutView.h | 27 | ||||
-rw-r--r-- | platform/ios/src/MGLLocationManager.h | 25 | ||||
-rw-r--r-- | platform/ios/src/MGLLocationManager.m | 175 | ||||
-rw-r--r-- | platform/ios/src/MGLMapAccessibilityElement.mm | 4 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView+IBAdditions.h | 1 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.h | 131 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 393 | ||||
-rw-r--r-- | platform/ios/src/MGLMapboxEvents.h | 39 | ||||
-rw-r--r-- | platform/ios/src/MGLMapboxEvents.m | 831 | ||||
-rw-r--r-- | platform/ios/src/MGLTelemetryConfig.h | 2 | ||||
-rw-r--r-- | platform/ios/src/Mapbox-Prefix.pch | 1 | ||||
-rw-r--r-- | platform/ios/src/Mapbox.h | 5 | ||||
-rw-r--r-- | platform/ios/src/UIColor+MGLAdditions.h | 7 | ||||
-rw-r--r-- | platform/ios/src/UIColor+MGLAdditions.mm | 49 |
18 files changed, 483 insertions, 1434 deletions
diff --git a/platform/ios/src/MGLAPIClient.h b/platform/ios/src/MGLAPIClient.h deleted file mode 100644 index 4e5ea3b5e0..0000000000 --- a/platform/ios/src/MGLAPIClient.h +++ /dev/null @@ -1,15 +0,0 @@ -#import <Foundation/Foundation.h> - -#import "MGLMapboxEvents.h" -#import "MGLTypes.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface MGLAPIClient : NSObject <NSURLSessionDelegate> - -- (void)postEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler; -- (void)postEvent:(MGLMapboxEventAttributes *)event completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLAPIClient.m b/platform/ios/src/MGLAPIClient.m deleted file mode 100644 index 8a987d76d8..0000000000 --- a/platform/ios/src/MGLAPIClient.m +++ /dev/null @@ -1,202 +0,0 @@ -#import "MGLAPIClient.h" -#import "NSBundle+MGLAdditions.h" -#import "NSData+MGLAdditions.h" -#import "MGLAccountManager.h" - -static NSString * const MGLAPIClientUserAgentBase = @"MapboxEventsiOS"; -static NSString * const MGLAPIClientBaseURL = @"https://events.mapbox.com"; -static NSString * const MGLAPIClientEventsPath = @"events/v2"; - -static NSString * const MGLAPIClientHeaderFieldUserAgentKey = @"User-Agent"; -static NSString * const MGLAPIClientHeaderFieldContentTypeKey = @"Content-Type"; -static NSString * const MGLAPIClientHeaderFieldContentTypeValue = @"application/json"; -static NSString * const MGLAPIClientHeaderFieldContentEncodingKey = @"Content-Encoding"; -static NSString * const MGLAPIClientHTTPMethodPost = @"POST"; - -@interface MGLAPIClient () - -@property (nonatomic, copy) NSURLSession *session; -@property (nonatomic, copy) NSURL *baseURL; -@property (nonatomic, copy) NSData *digicertCert_2016; -@property (nonatomic, copy) NSData *geoTrustCert_2016; -@property (nonatomic, copy) NSData *digicertCert_2017; -@property (nonatomic, copy) NSData *geoTrustCert_2017; -@property (nonatomic, copy) NSData *testServerCert; -@property (nonatomic, copy) NSString *userAgent; -@property (nonatomic) BOOL usesTestServer; - -@end - -@implementation MGLAPIClient - -- (instancetype)init { - self = [super init]; - if (self) { - _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] - delegate:self delegateQueue:nil]; - [self loadCertificates]; - [self setupBaseURL]; - [self setupUserAgent]; - } - return self; -} - -#pragma mark Public API - -- (void)postEvents:(nonnull NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler { - __block NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:[self requestForEvents:events] - completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; - NSError *statusError = nil; - if (httpResponse.statusCode >= 400) { - NSString *description = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"API_CLIENT_400_DESC", nil, nil, @"The session data task failed. Original request was: %@", nil), dataTask.originalRequest]; - NSString *reason = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"API_CLIENT_400_REASON", nil, nil, @"The status code was %ld", nil), (long)httpResponse.statusCode]; - NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description, - NSLocalizedFailureReasonErrorKey: reason}; - statusError = [NSError errorWithDomain:MGLErrorDomain code:1 userInfo:userInfo]; - } - if (completionHandler) { - error = error ?: statusError; - completionHandler(error); - } - dataTask = nil; - }]; - [dataTask resume]; -} - -- (void)postEvent:(nonnull MGLMapboxEventAttributes *)event completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler { - [self postEvents:@[event] completionHandler:completionHandler]; -} - -#pragma mark Utilities - -- (NSURLRequest *)requestForEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { - NSString *path = [NSString stringWithFormat:@"%@?access_token=%@", MGLAPIClientEventsPath, [MGLAccountManager accessToken]]; - NSURL *url = [NSURL URLWithString:path relativeToURL:self.baseURL]; - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - [request setValue:self.userAgent forHTTPHeaderField:MGLAPIClientHeaderFieldUserAgentKey]; - [request setValue:MGLAPIClientHeaderFieldContentTypeValue forHTTPHeaderField:MGLAPIClientHeaderFieldContentTypeKey]; - [request setHTTPMethod:MGLAPIClientHTTPMethodPost]; - - NSData *jsonData = [self serializedDataForEvents:events]; - - // Compressing less than 3 events can have a negative impact on the size. - if (events.count > 2) { - NSData *compressedData = [jsonData mgl_compressedData]; - [request setValue:@"deflate" forHTTPHeaderField:MGLAPIClientHeaderFieldContentEncodingKey]; - [request setHTTPBody:compressedData]; - } - - // Set JSON data if events.count were less than 3 or something went wrong with compressing HTTP body data. - if (!request.HTTPBody) { - [request setValue:nil forHTTPHeaderField:MGLAPIClientHeaderFieldContentEncodingKey]; - [request setHTTPBody:jsonData]; - } - - return [request copy]; -} - -- (void)setupBaseURL { - NSString *testServerURLString = [[NSUserDefaults standardUserDefaults] stringForKey:@"MGLTelemetryTestServerURL"]; - NSURL *testServerURL = [NSURL URLWithString:testServerURLString]; - if (testServerURL && [testServerURL.scheme isEqualToString:@"https"]) { - self.baseURL = testServerURL; - self.usesTestServer = YES; - } else { - self.baseURL = [NSURL URLWithString:MGLAPIClientBaseURL]; - } -} - -- (void)loadCertificates { - NSData *certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_com-geotrust_2016"]; - self.geoTrustCert_2016 = certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_com-digicert_2016"]; - self.digicertCert_2016 = certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_com-geotrust_2017"]; - self.geoTrustCert_2017 = certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_com-digicert_2017"]; - self.digicertCert_2017 = certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_staging"]; - self.testServerCert = certificate; -} - -- (void)loadCertificate:(NSData **)certificate withResource:(NSString *)resource { - NSBundle *frameworkBundle = [NSBundle mgl_frameworkBundle]; - NSString *cerPath = [frameworkBundle pathForResource:resource ofType:@"der"]; - if (cerPath != nil) { - *certificate = [NSData dataWithContentsOfFile:cerPath]; - } -} - -- (void)setupUserAgent { - NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; - NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - NSString *appBuildNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; - NSString *semanticVersion = [NSBundle mgl_frameworkInfoDictionary][@"MGLSemanticVersionString"]; - NSString *shortVersion = [NSBundle mgl_frameworkInfoDictionary][@"CFBundleShortVersionString"]; - NSString *sdkVersion = semanticVersion ?: shortVersion; - _userAgent = [NSString stringWithFormat:@"%@/%@/%@ %@/%@", appName, appVersion, appBuildNumber, MGLAPIClientUserAgentBase, sdkVersion]; -} - -#pragma mark - JSON Serialization - -- (NSData *)serializedDataForEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { - return [NSJSONSerialization dataWithJSONObject:events options:0 error:nil]; -} - -#pragma mark NSURLSessionDelegate - -- (BOOL)evaluateCertificateWithCertificateData:(NSData *)certificateData keyCount:(CFIndex)keyCount serverTrust:(SecTrustRef)serverTrust challenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { - for (int lc = 0; lc < keyCount; lc++) { - SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc); - NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); - if ([remoteCertificateData isEqualToData:certificateData]) { - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - return YES; - } - } - return NO; -} - -- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { - - if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { - SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust]; - SecTrustResultType trustResult; - - // Validate the certificate chain with the device's trust store anyway this *might* use revocation checking - SecTrustEvaluate(serverTrust, &trustResult); - - BOOL found = NO; // For clarity; we start in a state where the challange has not been completed and no certificate has been found - - if (trustResult == kSecTrustResultUnspecified) { - // Look for a pinned certificate in the server's certificate chain - CFIndex numKeys = SecTrustGetCertificateCount(serverTrust); - - // Check certs in the following order: digicert 2016, digicert 2017, geotrust 2016, geotrust 2017 - found = [self evaluateCertificateWithCertificateData:self.digicertCert_2016 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - if (!found) { - found = [self evaluateCertificateWithCertificateData:self.digicertCert_2017 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - } - if (!found) { - found = [self evaluateCertificateWithCertificateData:self.geoTrustCert_2016 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - } - if (!found) { - found = [self evaluateCertificateWithCertificateData:self.geoTrustCert_2017 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - } - - // If challenge can't be completed with any of the above certs, then try the test server if the app is configured to use the test server - if (!found && _usesTestServer) { - found = [self evaluateCertificateWithCertificateData:self.testServerCert keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - } - } - - if (!found) { - // No certificate was found so cancel the connection. - completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - } - } -} - -@end diff --git a/platform/ios/src/MGLAnnotationView.h b/platform/ios/src/MGLAnnotationView.h index 8006ec3e40..57d97e56c1 100644 --- a/platform/ios/src/MGLAnnotationView.h +++ b/platform/ios/src/MGLAnnotationView.h @@ -169,12 +169,12 @@ MGL_EXPORT value of this property is `NO` or the map’s pitch is zero, the annotation view remains the same size regardless of its position on-screen. - The default value of this property is `YES`. Set this property to `NO` if the - view’s legibility is important. + The default value of this property is `NO`. Keep this property set to `NO` if + the view’s legibility is important. @note Scaling many on-screen annotation views can contribute to poor map - performance. Consider disabling this property if your use case involves - hundreds or thousands of annotation views. + performance. Consider keeping this property disabled if your use case + involves hundreds or thousands of annotation views. */ @property (nonatomic, assign) BOOL scalesWithViewingDistance; diff --git a/platform/ios/src/MGLAnnotationView.mm b/platform/ios/src/MGLAnnotationView.mm index 4585479582..1c53ba507a 100644 --- a/platform/ios/src/MGLAnnotationView.mm +++ b/platform/ios/src/MGLAnnotationView.mm @@ -45,7 +45,7 @@ _lastAppliedScaleTransform = CATransform3DIdentity; _annotation = annotation; _reuseIdentifier = [reuseIdentifier copy]; - _scalesWithViewingDistance = YES; + _scalesWithViewingDistance = NO; _enabled = YES; } diff --git a/platform/ios/src/MGLCalloutView.h b/platform/ios/src/MGLCalloutView.h index 0481a39680..689b159fd7 100644 --- a/platform/ios/src/MGLCalloutView.h +++ b/platform/ios/src/MGLCalloutView.h @@ -39,7 +39,14 @@ NS_ASSUME_NONNULL_BEGIN Presents a callout view by adding it to `view` and pointing at the given rect of `view`’s bounds. Constrains the callout to the bounds of the given view. */ -- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated; +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated __attribute__((unavailable("Use -presentCalloutFromRect:inView:constrainedToRect:animated: instead."))); + + +/** + Presents a callout view by adding it to `view` and pointing at the given rect + of `view`’s bounds. Constrains the callout to the rect in the space of `view`. + */ +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated; /** Dismisses the callout view. @@ -49,6 +56,24 @@ NS_ASSUME_NONNULL_BEGIN @optional /** + If implemented, should provide margins to expand the rect the callout is presented from. + + These are used to determine positioning. Currently only the top and bottom properties of the return + value are used. For example, `{ .top = -50.0, .left = -10.0, .bottom = 0.0, .right = -10.0 }` indicates + a 50 point margin above the presentation origin rect (and 10 point margins to the left and the right) + in which the callout is assumed to be displayed. + + There are no assumed defaults for these margins, as they should be calculated from the callout that + is to be presented. For example, `SMCalloutView` generates the top margin from the callout height, but + the left and right margins from a minimum width that the callout should have. + + @param rect Rect that the callout is presented from. This should be the same as the one passed in + `-[MGLCalloutView presentCalloutFromRect:inView:constrainedToRect:animated:]` + @return `UIEdgeInsets` representing the margins. Values should be negative. + */ +- (UIEdgeInsets)marginInsetsHintForPresentationFromRect:(CGRect)rect NS_SWIFT_NAME(marginInsetsHintForPresentation(from:)); + +/** A Boolean value indicating whether the callout view should be anchored to the corresponding annotation. You can adjust the callout view’s precise location by overriding -[UIView setCenter:]. The callout view will not be anchored to the diff --git a/platform/ios/src/MGLLocationManager.h b/platform/ios/src/MGLLocationManager.h deleted file mode 100644 index ea23801813..0000000000 --- a/platform/ios/src/MGLLocationManager.h +++ /dev/null @@ -1,25 +0,0 @@ -#import <Foundation/Foundation.h> -#import <CoreLocation/CoreLocation.h> - -@protocol MGLLocationManagerDelegate; - -@interface MGLLocationManager : NSObject <CLLocationManagerDelegate> - -@property (nonatomic, weak) id<MGLLocationManagerDelegate> delegate; - -- (void)startUpdatingLocation; -- (void)stopUpdatingLocation; - -@end - -@protocol MGLLocationManagerDelegate <NSObject> - -@optional - -- (void)locationManager:(MGLLocationManager *)locationManager didUpdateLocations:(NSArray *)locations; -- (void)locationManagerDidStartLocationUpdates:(MGLLocationManager *)locationManager; -- (void)locationManagerBackgroundLocationUpdatesDidTimeout:(MGLLocationManager *)locationManager; -- (void)locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:(MGLLocationManager *)locationManager; -- (void)locationManagerDidStopLocationUpdates:(MGLLocationManager *)locationManager; - -@end diff --git a/platform/ios/src/MGLLocationManager.m b/platform/ios/src/MGLLocationManager.m deleted file mode 100644 index 85ef4ca489..0000000000 --- a/platform/ios/src/MGLLocationManager.m +++ /dev/null @@ -1,175 +0,0 @@ -#import "MGLLocationManager.h" -#import "MGLTelemetryConfig.h" -#import <UIKit/UIKit.h> - -static const NSTimeInterval MGLLocationManagerHibernationTimeout = 300.0; -static const NSTimeInterval MGLLocationManagerHibernationPollInterval = 5.0; -static const CLLocationDistance MGLLocationManagerDistanceFilter = 5.0; -static NSString * const MGLLocationManagerRegionIdentifier = @"MGLLocationManagerRegionIdentifier.fence.center"; - -@interface MGLLocationManager () - -@property (nonatomic) CLLocationManager *standardLocationManager; -@property (nonatomic) BOOL hostAppHasBackgroundCapability; -@property (nonatomic, getter=isUpdatingLocation) BOOL updatingLocation; -@property (nonatomic) NSDate *backgroundLocationServiceTimeoutAllowedDate; -@property (nonatomic) NSTimer *backgroundLocationServiceTimeoutTimer; - -@end - -@implementation MGLLocationManager - -- (instancetype)init { - self = [super init]; - if (self) { - NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"]; - _hostAppHasBackgroundCapability = [backgroundModes containsObject:@"location"]; - } - return self; -} - -- (void)startUpdatingLocation { - if ([self isUpdatingLocation]) { - return; - } - - [self configurePassiveStandardLocationManager]; - [self startLocationServices]; -} - -- (void)stopUpdatingLocation { - if ([self isUpdatingLocation]) { - [self.standardLocationManager stopUpdatingLocation]; - [self.standardLocationManager stopMonitoringSignificantLocationChanges]; - self.updatingLocation = NO; - if ([self.delegate respondsToSelector:@selector(locationManagerDidStopLocationUpdates:)]) { - [self.delegate locationManagerDidStopLocationUpdates:self]; - } - } - if(self.standardLocationManager.monitoredRegions.count > 0) { - for(CLRegion *region in self.standardLocationManager.monitoredRegions) { - if([region.identifier isEqualToString:MGLLocationManagerRegionIdentifier]) { - [self.standardLocationManager stopMonitoringForRegion:region]; - } - } - } -} - -#pragma mark - Utilities - -- (void)configurePassiveStandardLocationManager { - if (!self.standardLocationManager) { - CLLocationManager *standardLocationManager = [[CLLocationManager alloc] init]; - standardLocationManager.delegate = self; - standardLocationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers; - standardLocationManager.distanceFilter = MGLLocationManagerDistanceFilter; - self.standardLocationManager = standardLocationManager; - } -} - -- (void)startLocationServices { - CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus]; -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000 - BOOL authorizedAlways = authorizationStatus == kCLAuthorizationStatusAuthorizedAlways; -#else - BOOL authorizedAlways = authorizationStatus == kCLAuthorizationStatusAuthorized; -#endif - if (authorizedAlways || authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse) { - // If the host app can run in the background with `always` location permissions then allow background - // updates and start the significant location change service and background timeout timer - if (self.hostAppHasBackgroundCapability && authorizedAlways) { - [self.standardLocationManager startMonitoringSignificantLocationChanges]; - [self startBackgroundTimeoutTimer]; - // On iOS 9 and above also allow background location updates - if ([self.standardLocationManager respondsToSelector:@selector(allowsBackgroundLocationUpdates)]) { - self.standardLocationManager.allowsBackgroundLocationUpdates = YES; - } - } - - [self.standardLocationManager startUpdatingLocation]; - self.updatingLocation = YES; - if ([self.delegate respondsToSelector:@selector(locationManagerDidStartLocationUpdates:)]) { - [self.delegate locationManagerDidStartLocationUpdates:self]; - } - } -} - -- (void)timeoutAllowedCheck { - if (self.backgroundLocationServiceTimeoutAllowedDate == nil) { - return; - } - - if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive || - [UIApplication sharedApplication].applicationState == UIApplicationStateInactive ) { - [self startBackgroundTimeoutTimer]; - return; - } - - NSTimeInterval timeIntervalSinceTimeoutAllowed = [[NSDate date] timeIntervalSinceDate:self.backgroundLocationServiceTimeoutAllowedDate]; - if (timeIntervalSinceTimeoutAllowed > 0) { - [self.standardLocationManager stopUpdatingLocation]; - self.backgroundLocationServiceTimeoutAllowedDate = nil; - if ([self.delegate respondsToSelector:@selector(locationManagerBackgroundLocationUpdatesDidTimeout:)]) { - [self.delegate locationManagerBackgroundLocationUpdatesDidTimeout:self]; - } - } -} - -- (void)startBackgroundTimeoutTimer { - [self.backgroundLocationServiceTimeoutTimer invalidate]; - self.backgroundLocationServiceTimeoutAllowedDate = [[NSDate date] dateByAddingTimeInterval:MGLLocationManagerHibernationTimeout]; - self.backgroundLocationServiceTimeoutTimer = [NSTimer scheduledTimerWithTimeInterval:MGLLocationManagerHibernationPollInterval target:self selector:@selector(timeoutAllowedCheck) userInfo:nil repeats:YES]; -} - -- (void)establishRegionMonitoringForLocation:(CLLocation *)location { - CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:location.coordinate radius:MGLTelemetryConfig.sharedConfig.MGLLocationManagerHibernationRadius identifier:MGLLocationManagerRegionIdentifier]; - region.notifyOnEntry = NO; - region.notifyOnExit = YES; - [self.standardLocationManager startMonitoringForRegion:region]; -} - -#pragma mark - CLLocationManagerDelegate - -- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { - switch (status) { -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000 - case kCLAuthorizationStatusAuthorizedAlways: -#else - case kCLAuthorizationStatusAuthorized: -#endif - case kCLAuthorizationStatusAuthorizedWhenInUse: - [self startUpdatingLocation]; - break; - default: - [self stopUpdatingLocation]; - break; - } -} - -- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations { - CLLocation *location = locations.lastObject; - if (location.speed > 0.0) { - [self startBackgroundTimeoutTimer]; - } - if (self.standardLocationManager.monitoredRegions.count == 0 || location.horizontalAccuracy < MGLTelemetryConfig.sharedConfig.MGLLocationManagerHibernationRadius) { - [self establishRegionMonitoringForLocation:location]; - } - if ([self.delegate respondsToSelector:@selector(locationManager:didUpdateLocations:)]) { - [self.delegate locationManager:self didUpdateLocations:locations]; - } -} - -- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region { - [self startBackgroundTimeoutTimer]; - [self.standardLocationManager startUpdatingLocation]; -} - -- (void)locationManagerDidPauseLocationUpdates:(CLLocationManager *)manager { - if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { - if ([self.delegate respondsToSelector:@selector(locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:)]) { - [self.delegate locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:self]; - } - } -} - -@end diff --git a/platform/ios/src/MGLMapAccessibilityElement.mm b/platform/ios/src/MGLMapAccessibilityElement.mm index 79dcda4054..8bce38a145 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.mm +++ b/platform/ios/src/MGLMapAccessibilityElement.mm @@ -4,7 +4,7 @@ #import "MGLFeature.h" #import "MGLGeometry_Private.h" -#import "MGLVectorSource_Private.h" +#import "MGLVectorTileSource_Private.h" #import "NSBundle+MGLAdditions.h" #import "NSOrthography+MGLAdditions.h" @@ -48,7 +48,7 @@ if (self = [super initWithAccessibilityContainer:container]) { _feature = feature; - NSString *languageCode = [MGLVectorSource preferredMapboxStreetsLanguage]; + NSString *languageCode = [MGLVectorTileSource preferredMapboxStreetsLanguage]; NSString *nameAttribute = [NSString stringWithFormat:@"name_%@", languageCode]; NSString *name = [feature attributeForKey:nameAttribute]; diff --git a/platform/ios/src/MGLMapView+IBAdditions.h b/platform/ios/src/MGLMapView+IBAdditions.h index 6d5351df2b..64016e8319 100644 --- a/platform/ios/src/MGLMapView+IBAdditions.h +++ b/platform/ios/src/MGLMapView+IBAdditions.h @@ -44,6 +44,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) IBInspectable BOOL allowsTilting; @property (nonatomic) IBInspectable BOOL showsUserLocation; @property (nonatomic) IBInspectable BOOL showsHeading; +@property (nonatomic) IBInspectable BOOL showsScale; @end diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index 3c5aa2c122..765f0f932b 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -189,13 +189,7 @@ MGL_EXPORT IB_DESIGNABLE */ @property (nonatomic, readonly, nullable) MGLStyle *style; -/** - URLs of the styles bundled with the library. - - @deprecated Call the relevant class method of `MGLStyle` for the URL of a - particular default style. - */ -@property (nonatomic, readonly) NS_ARRAY_OF(NSURL *) *bundledStyleURLs __attribute__((deprecated("Call the relevant class method of MGLStyle for the URL of a particular default style."))); +@property (nonatomic, readonly) NS_ARRAY_OF(NSURL *) *bundledStyleURLs __attribute__((unavailable("Call the relevant class method of MGLStyle for the URL of a particular default style."))); /** URL of the style currently displayed in the receiver. @@ -224,11 +218,20 @@ MGL_EXPORT IB_DESIGNABLE the server, calling this method does not necessarily ensure that the map view reflects those changes. */ -- (IBAction)reloadStyle:(id)sender; +- (IBAction)reloadStyle:(nullable id)sender; + +/** + A Boolean value indicating whether the map may display scale information. + + The scale bar may not be shown at all zoom levels. The view controlled by this + property is available at `scaleBar`. The default value of this property is + `NO`. + */ +@property (nonatomic, assign) BOOL showsScale; /** A control indicating the scale of the map. The scale bar is positioned in the - upper-left corner. The scale bar is hidden by default. + upper-left corner. Enable the scale bar via `showsScale`. */ @property (nonatomic, readonly) UIView *scaleBar; @@ -283,17 +286,13 @@ MGL_EXPORT IB_DESIGNABLE */ - (IBAction)showAttribution:(id)sender; -/// :nodoc: Support for style classes has been removed. This property always returns an empty array. -@property (nonatomic) NS_ARRAY_OF(NSString *) *styleClasses __attribute__((deprecated("This property is non-functional."))); +@property (nonatomic) NS_ARRAY_OF(NSString *) *styleClasses __attribute__((unavailable("Support for style classes has been removed."))); -/// :nodoc: Support for style classes has been removed. This property always returns NO. -- (BOOL)hasStyleClass:(NSString *)styleClass __attribute__((deprecated("This method is non-functional."))); +- (BOOL)hasStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed."))); -/// :nodoc: Support for style classes has been removed. This property is a no-op. -- (void)addStyleClass:(NSString *)styleClass __attribute__((deprecated("This method is non-functional."))); +- (void)addStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed."))); -/// :nodoc: Support for style classes has been removed. This property is a no-op. -- (void)removeStyleClass:(NSString *)styleClass __attribute__((deprecated("This method is non-functional."))); +- (void)removeStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed."))); #pragma mark Displaying the User’s Location @@ -494,6 +493,19 @@ MGL_EXPORT IB_DESIGNABLE @property(nonatomic, getter=isPitchEnabled) BOOL pitchEnabled; /** + A Boolean value that determines whether the user will receive haptic feedback + for certain interactions with the map. + + When this property is set to `YES`, the default, a `UIImpactFeedbackStyleLight` + haptic feedback event be played when the user rotates the map to due north + (0°). + + This feature requires a device that supports haptic feedback, running iOS 10 or + newer. + */ +@property(nonatomic, getter=isHapticFeedbackEnabled) BOOL hapticFeedbackEnabled; + +/** A floating-point value that determines the rate of deceleration after the user lifts their finger. @@ -669,11 +681,10 @@ MGL_EXPORT IB_DESIGNABLE want to animate the change, call `-setVisibleCoordinateBounds:animated:` instead. - If a longitude is less than −180 degrees or greater than 180 degrees, the visible - bounds straddles the antimeridian or international date line. - - For example, a visible bounds that stretches from Tokyo to San Francisco would have - coordinates of (35.68476, -220.24257) and (37.78428, -122.41310). + If a longitude is less than −180 degrees or greater than 180 degrees, the + visible bounds straddles the antimeridian or international date line. For + example, if both Tokyo and San Francisco are visible, the visible bounds might + extend from (35.68476, −220.24257) to (37.78428, −122.41310). */ @property (nonatomic) MGLCoordinateBounds visibleCoordinateBounds; @@ -681,11 +692,10 @@ MGL_EXPORT IB_DESIGNABLE Changes the receiver’s viewport to fit the given coordinate bounds, optionally animating the change. - To make the visible bounds go across the antimeridian or international date line, - specify some longitudes less than −180 degrees or greater than 180 degrees. - - For example, a visible bounds that stretches from Tokyo to San Francisco would have - coordinates of (35.68476, -220.24257) and (37.78428, -122.41310). + To bring both sides of the antimeridian or international date line into view, + specify some longitudes less than −180 degrees or greater than 180 degrees. For + example, to show both Tokyo and San Francisco simultaneously, you could set the + visible bounds to extend from (35.68476, −220.24257) to (37.78428, −122.41310). @param bounds The bounds that the viewport will show in its entirety. @param animated Specify `YES` to animate the change by smoothly scrolling @@ -696,6 +706,11 @@ MGL_EXPORT IB_DESIGNABLE /** Changes the receiver’s viewport to fit the given coordinate bounds and optionally some additional padding on each side. + + To bring both sides of the antimeridian or international date line into view, + specify some longitudes less than −180 degrees or greater than 180 degrees. For + example, to show both Tokyo and San Francisco simultaneously, you could set the + visible bounds to extend from (35.68476, −220.24257) to (37.78428, −122.41310). @param bounds The bounds that the viewport will show in its entirety. @param insets The minimum padding (in screen points) that will be visible @@ -708,6 +723,11 @@ MGL_EXPORT IB_DESIGNABLE /** Changes the receiver’s viewport to fit all of the given coordinates and optionally some additional padding on each side. + + To bring both sides of the antimeridian or international date line into view, + specify some longitudes less than −180 degrees or greater than 180 degrees. For + example, to show both Tokyo and San Francisco simultaneously, you could set the + visible coordinates to (35.68476, −220.24257) and (37.78428, −122.41310). @param coordinates The coordinates that the viewport will show. @param count The number of coordinates. This number must not be greater than @@ -722,6 +742,11 @@ MGL_EXPORT IB_DESIGNABLE /** Changes the receiver’s viewport to fit all of the given coordinates and optionally some additional padding on each side. + + To bring both sides of the antimeridian or international date line into view, + specify some longitudes less than −180 degrees or greater than 180 degrees. For + example, to show both Tokyo and San Francisco simultaneously, you could set the + visible coordinates to (35.68476, −220.24257) and (37.78428, −122.41310). @param coordinates The coordinates that the viewport will show. @param count The number of coordinates. This number must not be greater than @@ -1004,6 +1029,9 @@ MGL_EXPORT IB_DESIGNABLE /** Converts a rectangle in the given view’s coordinate system to a geographic bounding box. + + If a longitude is less than −180 degrees or greater than 180 degrees, the + bounding box straddles the antimeridian or international date line. @param rect The rectangle to convert. @param view The view in whose coordinate system the rectangle is expressed. @@ -1172,17 +1200,32 @@ MGL_EXPORT IB_DESIGNABLE Assigning a new array to this property selects only the first annotation in the array. + + If the annotation is of type `MGLPointAnnotation` and is offscreen, the camera + will animate to bring the annotation and its callout just on screen. If you + need finer control, consider using `-selectAnnotation:animated:`. + + @note In versions prior to `4.0.0` if the annotation was offscreen it was not + selected. */ @property (nonatomic, copy) NS_ARRAY_OF(id <MGLAnnotation>) *selectedAnnotations; /** - Selects an annotation and displays a callout view for it. + Selects an annotation and displays its callout view. - If the given annotation is not visible within the current viewport, this - method has no effect. + The `animated` parameter determines whether the map is panned to bring the + annotation on-screen, specifically: + + | `animated` parameter | Effect | + |------------------|--------| + | `NO` | The annotation is selected, and the callout is presented. However the map is not panned to bring the annotation or callout onscreen. The presentation of the callout is animated. | + | `YES` | The annotation is selected, and the callout is presented. If the annotation is offscreen *and* is of type `MGLPointAnnotation`, the map is panned so that the annotation and its callout are brought just onscreen. The annotation is *not* centered within the viewport. | @param annotation The annotation object to select. - @param animated If `YES`, the callout view is animated into position. + @param animated If `YES`, the annotation and callout view are animated on-screen. + + @note In versions prior to `4.0.0` selecting an offscreen annotation did not + change the camera. */ - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated; @@ -1286,9 +1329,9 @@ MGL_EXPORT IB_DESIGNABLE Each object in the returned array represents a feature rendered by the current style and provides access to attributes specified by the relevant map content sources. The returned array includes features loaded by - `MGLShapeSource` and `MGLVectorSource` objects but does not include anything - from `MGLRasterSource` objects, or from image, video, or canvas sources, which - are unsupported by this SDK. + `MGLShapeSource` and `MGLVectorTileSource` objects but does not include + anything from `MGLRasterTileSource` objects, or from video or canvas sources, + which are unsupported by this SDK. The returned features are drawn by a style layer in the current style. For example, suppose the current style uses the @@ -1320,7 +1363,7 @@ MGL_EXPORT IB_DESIGNABLE Only visible features are returned. To obtain features regardless of visibility, use the - `-[MGLVectorSource featuresInSourceLayersWithIdentifiers:predicate:]` and + `-[MGLVectorTileSource featuresInSourceLayersWithIdentifiers:predicate:]` and `-[MGLShapeSource featuresMatchingPredicate:]` methods on the relevant sources. The returned features may also include features corresponding to annotations. @@ -1388,9 +1431,9 @@ MGL_EXPORT IB_DESIGNABLE Each object in the returned array represents a feature rendered by the current style and provides access to attributes specified by the relevant map content sources. The returned array includes features loaded by - `MGLShapeSource` and `MGLVectorSource` objects but does not include anything - from `MGLRasterSource` objects, or from image, video, or canvas sources, which - are unsupported by this SDK. + `MGLShapeSource` and `MGLVectorTileSource` objects but does not include + anything from `MGLRasterTileSource` objects, or from video or canvas sources, + which are unsupported by this SDK. The returned features are drawn by a style layer in the current style. For example, suppose the current style uses the @@ -1423,7 +1466,7 @@ MGL_EXPORT IB_DESIGNABLE Only visible features are returned. To obtain features regardless of visibility, use the - `-[MGLVectorSource featuresInSourceLayersWithIdentifiers:predicate:]` and + `-[MGLVectorTileSource featuresInSourceLayersWithIdentifiers:predicate:]` and `-[MGLShapeSource featuresMatchingPredicate:]` methods on the relevant sources. @note Layer identifiers are not guaranteed to exist across styles or different @@ -1431,8 +1474,8 @@ MGL_EXPORT IB_DESIGNABLE style URL to an explicitly versioned style using a convenience method like `+[MGLStyle outdoorsStyleURLWithVersion:]`, `MGLMapView`’s “Style URL” inspectable in Interface Builder, or a manually constructed `NSURL`. This - approach also avoids layer identifer name changes that will occur in the default - style’s layers over time. + approach also avoids layer identifer name changes that will occur in the + default style’s layers over time. @note Layer identifiers are not guaranteed to exist across styles or different versions of the same style. Applications that use this API must first set @@ -1462,11 +1505,11 @@ MGL_EXPORT IB_DESIGNABLE */ @property (nonatomic) MGLMapDebugMaskOptions debugMask; -@property (nonatomic, getter=isDebugActive) BOOL debugActive __attribute__((deprecated("Use -debugMask and -setDebugMask:."))); +@property (nonatomic, getter=isDebugActive) BOOL debugActive __attribute__((unavailable("Use -debugMask and -setDebugMask:."))); -- (void)toggleDebug __attribute__((deprecated("Use -setDebugMask:."))); +- (void)toggleDebug __attribute__((unavailable("Use -setDebugMask:."))); -- (void)emptyMemoryCache __attribute__((deprecated)); +- (void)emptyMemoryCache __attribute__((unavailable)); @end diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 6be6a71f66..b7d0974872 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -41,7 +41,7 @@ #import "MGLGeometry_Private.h" #import "MGLMultiPoint_Private.h" #import "MGLOfflineStorage_Private.h" -#import "MGLVectorSource_Private.h" +#import "MGLVectorTileSource_Private.h" #import "MGLFoundation_Private.h" #import "MGLRendererFrontend.h" #import "MGLRendererConfiguration.h" @@ -66,6 +66,7 @@ #import "MGLStyle_Private.h" #import "MGLStyleLayer_Private.h" #import "MGLMapboxEvents.h" +#import "MMEConstants.h" #import "MGLSDKUpdateChecker.h" #import "MGLCompactCalloutView.h" #import "MGLAnnotationContainerView.h" @@ -143,6 +144,9 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1; const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10); +/// Padding to edge of view that an offscreen annotation must have when being brought onscreen (by being selected) +const UIEdgeInsets MGLMapViewOffscreenAnnotationPadding = UIEdgeInsetsMake(-20.0f, -20.0f, -20.0f, -20.0f); + /// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationTagNotFound = UINT32_MAX }; @@ -227,6 +231,7 @@ public: @property (nonatomic) CGFloat quickZoomStart; @property (nonatomic, getter=isDormant) BOOL dormant; @property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed; +@property (nonatomic) BOOL shouldTriggerHapticFeedbackForCompass; @property (nonatomic) MGLMapViewProxyAccessibilityElement *mapViewProxyAccessibilityElement; @property (nonatomic) MGLAnnotationContainerView *annotationContainerView; @property (nonatomic) MGLUserLocation *userLocation; @@ -244,8 +249,6 @@ public: BOOL _opaque; - NS_MUTABLE_ARRAY_OF(NSURL *) *_bundledStyleURLs; - MGLAnnotationTagContextMap _annotationContextsByAnnotationTag; MGLAnnotationObjectTagMap _annotationTagsByAnnotation; @@ -270,7 +273,7 @@ public: CADisplayLink *_displayLink; BOOL _needsDisplayRefresh; - NSUInteger _changeDelimiterSuppressionDepth; + NSInteger _changeDelimiterSuppressionDepth; /// Center coordinate of the pinch gesture on the previous iteration of the gesture. CLLocationCoordinate2D _previousPinchCenterCoordinate; @@ -524,6 +527,8 @@ public: [_twoFingerTap requireGestureRecognizerToFail:_twoFingerDrag]; [self addGestureRecognizer:_twoFingerTap]; + _hapticFeedbackEnabled = YES; + _decelerationRate = MGLMapViewDecelerationRateNormal; _quickZoom = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleQuickZoomGesture:)]; @@ -568,7 +573,8 @@ public: _targetCoordinate = kCLLocationCoordinate2DInvalid; if ([UIApplication sharedApplication].applicationState != UIApplicationStateBackground) { - [MGLMapboxEvents pushEvent:MGLEventTypeMapLoad withAttributes:@{}]; + [MGLMapboxEvents pushTurnstileEvent]; + [MGLMapboxEvents pushEvent:MMEEventTypeMapLoad withAttributes:@{}]; } } @@ -970,8 +976,8 @@ public: - (void)layoutSubviews { // Calling this here instead of in the scale bar itself because if this is done in the - // scale bar instance, it triggers a call to this this `layoutSubviews` method that - // calls `_mbglMap->setSize()` just below that triggers rendering update which triggers + // scale bar instance, it triggers a call to this `layoutSubviews` method that calls + // `_mbglMap->setSize()` just below that triggers rendering update which triggers // another scale bar update which causes a rendering update loop and a major performace // degradation. The only time the scale bar's intrinsic content size _must_ invalidated // is here as a reaction to this object's view dimension changes. @@ -1220,7 +1226,8 @@ public: [self validateLocationServices]; - [MGLMapboxEvents pushEvent:MGLEventTypeMapLoad withAttributes:@{}]; + [MGLMapboxEvents pushTurnstileEvent]; + [MGLMapboxEvents pushEvent:MMEEventTypeMapLoad withAttributes:@{}]; } } @@ -1332,7 +1339,7 @@ public: if (pan.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGesturePanStart forRecognizer:pan]; + [self trackGestureEvent:MMEEventGesturePanStart forRecognizer:pan]; self.userTrackingMode = MGLUserTrackingModeNone; @@ -1380,10 +1387,10 @@ public: CLLocationCoordinate2D panCoordinate = [self convertPoint:pointInView toCoordinateFromView:pan.view]; int zoom = round([self zoomLevel]); - [MGLMapboxEvents pushEvent:MGLEventTypeMapDragEnd withAttributes:@{ - MGLEventKeyLatitude: @(panCoordinate.latitude), - MGLEventKeyLongitude: @(panCoordinate.longitude), - MGLEventKeyZoomLevel: @(zoom) + [MGLMapboxEvents pushEvent:MMEEventTypeMapDragEnd withAttributes:@{ + MMEEventKeyLatitude: @(panCoordinate.latitude), + MMEEventKeyLongitude: @(panCoordinate.longitude), + MMEEventKeyZoomLevel: @(zoom) }]; } @@ -1402,7 +1409,7 @@ public: if (pinch.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGesturePinchStart forRecognizer:pinch]; + [self trackGestureEvent:MMEEventGesturePinchStart forRecognizer:pinch]; self.scale = powf(2, _mbglMap->getZoom()); @@ -1503,7 +1510,7 @@ public: if (rotate.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGestureRotateStart forRecognizer:rotate]; + [self trackGestureEvent:MMEEventGestureRotateStart forRecognizer:rotate]; self.angle = MGLRadiansFromDegrees(_mbglMap->getBearing()) * -1; @@ -1512,6 +1519,8 @@ public: self.userTrackingMode = MGLUserTrackingModeFollow; } + self.shouldTriggerHapticFeedbackForCompass = NO; + [self notifyGestureDidBegin]; } else if (rotate.state == UIGestureRecognizerStateChanged) @@ -1534,6 +1543,22 @@ public: } [self cameraIsChanging]; + + // Trigger a light haptic feedback event when the user rotates to due north. + if (@available(iOS 10.0, *)) + { + if (self.isHapticFeedbackEnabled && fabs(newDegrees) <= 1 && self.shouldTriggerHapticFeedbackForCompass) + { + UIImpactFeedbackGenerator *hapticFeedback = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; + [hapticFeedback impactOccurred]; + + self.shouldTriggerHapticFeedbackForCompass = NO; + } + else if (fabs(newDegrees) > 1) + { + self.shouldTriggerHapticFeedbackForCompass = YES; + } + } } else if (rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled) { @@ -1575,7 +1600,7 @@ public: { return; } - [self trackGestureEvent:MGLEventGestureSingleTap forRecognizer:singleTap]; + [self trackGestureEvent:MMEEventGestureSingleTap forRecognizer:singleTap]; if (self.mapViewProxyAccessibilityElement.accessibilityElementIsFocused) { @@ -1601,7 +1626,7 @@ public: { CGPoint calloutPoint = [singleTap locationInView:self]; CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint]; - [self selectAnnotation:annotation animated:YES calloutPositioningRect:positionRect]; + [self selectAnnotation:annotation moveOnscreen:YES animateSelection:YES calloutPositioningRect:positionRect]; } else if (self.selectedAnnotation) { @@ -1697,7 +1722,7 @@ public: if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) { - [self trackGestureEvent:MGLEventGestureDoubleTap forRecognizer:doubleTap]; + [self trackGestureEvent:MMEEventGestureDoubleTap forRecognizer:doubleTap]; mbgl::ScreenCoordinate center(gesturePoint.x, gesturePoint.y); _mbglMap->setZoom(newZoom, center, MGLDurationFromTimeInterval(MGLAnimationDuration)); @@ -1728,7 +1753,7 @@ public: if (twoFingerTap.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGestureTwoFingerSingleTap forRecognizer:twoFingerTap]; + [self trackGestureEvent:MMEEventGestureTwoFingerSingleTap forRecognizer:twoFingerTap]; [self notifyGestureDidBegin]; } @@ -1767,7 +1792,7 @@ public: if (quickZoom.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGestureQuickZoom forRecognizer:quickZoom]; + [self trackGestureEvent:MMEEventGestureQuickZoom forRecognizer:quickZoom]; self.scale = powf(2, _mbglMap->getZoom()); @@ -1812,7 +1837,7 @@ public: if (twoFingerDrag.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGesturePitchStart forRecognizer:twoFingerDrag]; + [self trackGestureEvent:MMEEventGesturePitchStart forRecognizer:twoFingerDrag]; [self notifyGestureDidBegin]; } @@ -2017,11 +2042,11 @@ public: CLLocationCoordinate2D gestureCoordinate = [self convertPoint:pointInView toCoordinateFromView:recognizer.view]; int zoom = round([self zoomLevel]); - [MGLMapboxEvents pushEvent:MGLEventTypeMapTap withAttributes:@{ - MGLEventKeyLatitude: @(gestureCoordinate.latitude), - MGLEventKeyLongitude: @(gestureCoordinate.longitude), - MGLEventKeyZoomLevel: @(zoom), - MGLEventKeyGestureID: gestureID + [MGLMapboxEvents pushEvent:MMEEventTypeMapTap withAttributes:@{ + MMEEventKeyLatitude: @(gestureCoordinate.latitude), + MMEEventKeyLongitude: @(gestureCoordinate.longitude), + MMEEventKeyZoomLevel: @(zoom), + MMEEventKeyGestureID: gestureID }]; } @@ -2293,16 +2318,6 @@ public: MGLMapDebugCollisionBoxesMask) : 0; } -- (BOOL)isDebugActive -{ - return self.debugMask; -} - -- (void)toggleDebug -{ - self.debugActive = !self.debugActive; -} - - (void)resetNorth { [self resetNorthAnimated:YES]; @@ -2325,11 +2340,6 @@ public: heading:heading]; } -- (void)emptyMemoryCache -{ - _rendererFrontend->reduceMemoryUse(); -} - - (void)setZoomEnabled:(BOOL)zoomEnabled { _zoomEnabled = zoomEnabled; @@ -2357,6 +2367,17 @@ public: self.twoFingerDrag.enabled = pitchEnabled; } +- (void)setShowsScale:(BOOL)showsScale +{ + _showsScale = showsScale; + self.scaleBar.hidden = !showsScale; + + if (showsScale) + { + [self updateScaleBar]; + } +} + #pragma mark - Accessibility - - (NSString *)accessibilityValue @@ -3439,36 +3460,24 @@ public: /// bounding box. - (mbgl::LatLngBounds)convertRect:(CGRect)rect toLatLngBoundsFromView:(nullable UIView *)view { - mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty(); - bounds.extend([self convertPoint:rect.origin toLatLngFromView:view]); - bounds.extend([self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMinY(rect) } toLatLngFromView:view]); - bounds.extend([self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view]); - bounds.extend([self convertPoint:{ CGRectGetMinX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view]); - - // The world is wrapping if a point just outside the bounds is also within - // the rect. - mbgl::LatLng outsideLatLng; - if (bounds.west() > -180) - { - outsideLatLng = { - (bounds.south() + bounds.north()) / 2, - bounds.west() - 1, - }; - } - else if (bounds.east() < 180) - { - outsideLatLng = { - (bounds.south() + bounds.north()) / 2, - bounds.east() + 1, - }; - } - - // If the world is wrapping, extend the bounds to cover all longitudes. - if (CGRectContainsPoint(rect, [self convertLatLng:outsideLatLng toPointToView:view])) - { - bounds.extend(mbgl::LatLng(bounds.south(), -180)); - bounds.extend(mbgl::LatLng(bounds.south(), 180)); - } + auto bounds = mbgl::LatLngBounds::empty(); + auto topLeft = [self convertPoint:{ CGRectGetMinX(rect), CGRectGetMinY(rect) } toLatLngFromView:view]; + auto topRight = [self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMinY(rect) } toLatLngFromView:view]; + auto bottomRight = [self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view]; + auto bottomLeft = [self convertPoint:{ CGRectGetMinX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view]; + + // If the bounds straddles the antimeridian, unwrap it so that one side + // extends beyond ±180° longitude. + auto center = [self convertPoint:{ CGRectGetMidX(rect), CGRectGetMidY(rect) } toLatLngFromView:view]; + topLeft.unwrapForShortestPath(center); + topRight.unwrapForShortestPath(center); + bottomRight.unwrapForShortestPath(center); + bottomLeft.unwrapForShortestPath(center); + + bounds.extend(topLeft); + bounds.extend(topRight); + bounds.extend(bottomRight); + bounds.extend(bottomLeft); return bounds; } @@ -3478,11 +3487,6 @@ public: return mbgl::Projection::getMetersPerPixelAtLatitude(latitude, self.zoomLevel); } -- (CLLocationDistance)metersPerPixelAtLatitude:(CLLocationDegrees)latitude -{ - return [self metersPerPointAtLatitude:latitude]; -} - #pragma mark - Camera Change Reason - - (void)resetCameraChangeReason @@ -3490,70 +3494,6 @@ public: self.cameraChangeReasonBitmask = MGLCameraChangeReasonNone; } -#pragma mark - Styling - - -- (NS_ARRAY_OF(NSURL *) *)bundledStyleURLs -{ - if ( ! _bundledStyleURLs) - { - _bundledStyleURLs = [NSMutableArray array]; - for (NSUInteger i = 0; i < mbgl::util::default_styles::numOrderedStyles; i++) - { - NSURL *styleURL = [NSURL URLWithString:@(mbgl::util::default_styles::orderedStyles[i].url)]; - [_bundledStyleURLs addObject:styleURL]; - } - } - - return [NSArray arrayWithArray:_bundledStyleURLs]; -} - -- (nullable NSString *)styleID -{ - [NSException raise:@"Method unavailable" format: - @"%s has been replaced by -[MGLMapView styleURL].", - __PRETTY_FUNCTION__]; - return nil; -} - -- (void)setStyleID:(nullable NSString *)styleID -{ - [NSException raise:@"Method unavailable" format: - @"%s has been replaced by -[MGLMapView setStyleURL:].\n\n" - @"If you previously set this style ID in a storyboard inspectable, select the MGLMapView in Interface Builder and delete the “styleID” entry from the User Defined Runtime Attributes section of the Identity inspector. " - @"Then go to the Attributes inspector and enter “mapbox://styles/%@” into the “Style URL” field.", - __PRETTY_FUNCTION__, styleID]; -} - -- (NS_ARRAY_OF(NSString *) *)styleClasses -{ - return [self.style styleClasses]; -} - -- (void)setStyleClasses:(NS_ARRAY_OF(NSString *) *)appliedClasses -{ - [self setStyleClasses:appliedClasses transitionDuration:0]; -} - -- (void)setStyleClasses:(NS_ARRAY_OF(NSString *) *)appliedClasses transitionDuration:(NSTimeInterval)transitionDuration -{ - [self.style setStyleClasses:appliedClasses transitionDuration:transitionDuration]; -} - -- (BOOL)hasStyleClass:(NSString *)styleClass -{ - return [self.style hasStyleClass:styleClass]; -} - -- (void)addStyleClass:(NSString *)styleClass -{ - [self.style addStyleClass:styleClass]; -} - -- (void)removeStyleClass:(NSString *)styleClass -{ - [self.style removeStyleClass:styleClass]; -} - #pragma mark - Annotations - - (nullable NS_ARRAY_OF(id <MGLAnnotation>) *)annotations @@ -4174,51 +4114,48 @@ public: MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound; if (nearbyAnnotations.size()) { - // The annotation tags need to be stable in order to compare them with - // the remembered tags. - std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end()); - + // The first selection in the cycle should be the one nearest to the + // tap. Also the annotation tags need to be stable in order to compare them with + // the remembered tags _annotationsNearbyLastTap. + CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self]; + std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, + coordinateA.longitude - currentCoordinate.longitude); + CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, + coordinateB.longitude - currentCoordinate.longitude); + return deltaA < deltaB; + }); + if (nearbyAnnotations == _annotationsNearbyLastTap) { - // The first selection in the cycle should be the one nearest to the - // tap. - CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self]; - std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { - CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; - CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; - CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, - coordinateA.longitude - currentCoordinate.longitude); - CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, - coordinateB.longitude - currentCoordinate.longitude); - return deltaA < deltaB; - }); - // The last time we persisted a set of annotations, we had the same // set of annotations as we do now. Cycle through them. if (_selectedAnnotationTag == MGLAnnotationTagNotFound - || _selectedAnnotationTag == _annotationsNearbyLastTap.back()) + || _selectedAnnotationTag == nearbyAnnotations.back()) { // Either no annotation is selected or the last annotation in // the set was selected. Wrap around to the first annotation in // the set. - hitAnnotationTag = _annotationsNearbyLastTap.front(); + hitAnnotationTag = nearbyAnnotations.front(); } else { - auto result = std::find(_annotationsNearbyLastTap.begin(), - _annotationsNearbyLastTap.end(), + auto result = std::find(nearbyAnnotations.begin(), + nearbyAnnotations.end(), _selectedAnnotationTag); - if (result == _annotationsNearbyLastTap.end()) + if (result == nearbyAnnotations.end()) { // An annotation from this set hasn’t been selected before. // Select the first (nearest) one. - hitAnnotationTag = _annotationsNearbyLastTap.front(); + hitAnnotationTag = nearbyAnnotations.front(); } else { // Step to the next annotation in the set. - auto distance = std::distance(_annotationsNearbyLastTap.begin(), result); - hitAnnotationTag = _annotationsNearbyLastTap[distance + 1]; + auto distance = std::distance(nearbyAnnotations.begin(), result); + hitAnnotationTag = nearbyAnnotations[distance + 1]; } } } @@ -4230,7 +4167,7 @@ public: { _annotationsNearbyLastTap = nearbyAnnotations; } - + // Choose the first nearby annotation. if (nearbyAnnotations.size()) { @@ -4259,6 +4196,12 @@ public: }); } + +- (BOOL)isBringingAnnotationOnscreenSupportedForAnnotation:(id<MGLAnnotation>)annotation animated:(BOOL)animated { + // Consider delegating + return animated && [annotation isKindOfClass:[MGLPointAnnotation class]]; +} + - (id <MGLAnnotation>)selectedAnnotation { if (_userLocationAnnotationIsSelected) @@ -4299,20 +4242,16 @@ public: if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) return; - // Select the annotation if it’s visible. - if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) - { - [self selectAnnotation:firstAnnotation animated:NO]; - } + [self selectAnnotation:firstAnnotation animated:YES]; } - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero]; - [self selectAnnotation:annotation animated:animated calloutPositioningRect:positioningRect]; + [self selectAnnotation:annotation moveOnscreen:animated animateSelection:YES calloutPositioningRect:positioningRect]; } -- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect +- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveOnscreen:(BOOL)moveOnscreen animateSelection:(BOOL)animateSelection calloutPositioningRect:(CGRect)calloutPositioningRect { if ( ! annotation) return; @@ -4336,22 +4275,34 @@ public: MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); annotationView = annotationContext.annotationView; if (annotationView && annotationView.enabled) { - // Annotations represented by views use the view frame as the positioning rect. - calloutPositioningRect = annotationView.frame; - [annotationView.superview bringSubviewToFront:annotationView]; - [annotationView setSelected:YES animated:animated]; + // Annotations represented by views use the view frame as the positioning rect. + calloutPositioningRect = annotationView.frame; + [annotationView.superview bringSubviewToFront:annotationView]; + + [annotationView setSelected:YES animated:animateSelection]; } } self.selectedAnnotation = annotation; + // Determine if we're allowed to move this offscreen annotation on screen, even though we've asked it to + if (moveOnscreen) { + moveOnscreen = [self isBringingAnnotationOnscreenSupportedForAnnotation:annotation animated:animateSelection]; + } + + CGRect expandedPositioningRect = UIEdgeInsetsInsetRect(calloutPositioningRect, MGLMapViewOffscreenAnnotationPadding); + + // Used for callout positioning, and moving offscreen annotations onscreen. + CGRect constrainedRect = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + UIView <MGLCalloutView> *calloutView = nil; + if ([annotation respondsToSelector:@selector(title)] && annotation.title && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] && [self.delegate mapView:self annotationCanShowCallout:annotation]) { // build the callout - UIView <MGLCalloutView> *calloutView; if ([self.delegate respondsToSelector:@selector(mapView:calloutViewForAnnotation:)]) { id providedCalloutView = [self.delegate mapView:self calloutViewForAnnotation:annotation]; @@ -4409,13 +4360,51 @@ public: // set annotation delegate to handle taps on the callout view calloutView.delegate = self; - // present popup - [calloutView presentCalloutFromRect:calloutPositioningRect - inView:self.glView - constrainedToView:self.glView - animated:animated]; + // If the callout view provides inset (outset) information, we can use it to expand our positioning + // rect, which we then use to help move the annotation on-screen if want need to. + if (moveOnscreen && [calloutView respondsToSelector:@selector(marginInsetsHintForPresentationFromRect:)]) { + UIEdgeInsets margins = [calloutView marginInsetsHintForPresentationFromRect:calloutPositioningRect]; + expandedPositioningRect = UIEdgeInsetsInsetRect(expandedPositioningRect, margins); + } + } + + if (moveOnscreen) + { + moveOnscreen = NO; + + // Need to consider the content insets. + CGRect bounds = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + // Any one of these cases should trigger a move onscreen + if (CGRectGetMinX(calloutPositioningRect) < CGRectGetMinX(bounds)) + { + constrainedRect.origin.x = expandedPositioningRect.origin.x; + moveOnscreen = YES; + } + else if (CGRectGetMaxX(calloutPositioningRect) > CGRectGetMaxX(bounds)) + { + constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width; + moveOnscreen = YES; + } + + if (CGRectGetMinY(calloutPositioningRect) < CGRectGetMinY(bounds)) + { + constrainedRect.origin.y = expandedPositioningRect.origin.y; + moveOnscreen = YES; + } + else if (CGRectGetMaxY(calloutPositioningRect) > CGRectGetMaxY(bounds)) + { + constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height; + moveOnscreen = YES; + } } + // Remember, calloutView can be nil here. + [calloutView presentCalloutFromRect:calloutPositioningRect + inView:self.glView + constrainedToRect:constrainedRect + animated:animateSelection]; + // notify delegate if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) { @@ -4426,6 +4415,13 @@ public: { [self.delegate mapView:self didSelectAnnotationView:annotationView]; } + + if (moveOnscreen) + { + CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect)); + CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self]; + [self setCenterCoordinate:centerCoord animated:animateSelection]; + } } - (MGLCompactCalloutView *)calloutViewForAnnotation:(id <MGLAnnotation>)annotation @@ -4616,6 +4612,7 @@ public: animated:animated]; } + #pragma mark Annotation Image Delegate - (void)annotationImageNeedsRedisplay:(MGLAnnotationImage *)annotationImage @@ -4625,11 +4622,6 @@ public: NSString *fallbackReuseIdentifier = MGLDefaultStyleMarkerSymbolName; NSString *fallbackIconIdentifier = [MGLAnnotationSpritePrefix stringByAppendingString:fallbackReuseIdentifier]; - // Remove the old icon from the style. - if ( ! [iconIdentifier isEqualToString:fallbackIconIdentifier]) { - _mbglMap->removeAnnotationImage(iconIdentifier.UTF8String); - } - if (annotationImage.image) { // Add the new icon to the style. @@ -4746,12 +4738,8 @@ public: userLocationAnnotationView = (MGLUserLocationAnnotationView *)[self.delegate mapView:self viewForAnnotation:self.userLocation]; if (userLocationAnnotationView && ! [userLocationAnnotationView isKindOfClass:MGLUserLocationAnnotationView.class]) { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - NSLog(@"Ignoring user location annotation view with type %@. User location annotation view must be a kind of MGLUserLocationAnnotationView. This warning is only shown once and will become an error in a future version.", NSStringFromClass(userLocationAnnotationView.class)); - }); - - userLocationAnnotationView = nil; + [NSException raise:@"MGLUserLocationAnnotationTypeException" + format:@"User location annotation view must be a kind of MGLUserLocationAnnotationView. %@", userLocationAnnotationView.debugDescription]; } } @@ -5455,10 +5443,7 @@ public: } [self updateCompass]; - - if (!self.scaleBar.hidden) { - [(MGLScaleBar *)self.scaleBar setMetersPerPoint:[self metersPerPointAtLatitude:self.centerCoordinate.latitude]]; - } + [self updateScaleBar]; if ([self.delegate respondsToSelector:@selector(mapView:regionIsChangingWithReason:)]) { @@ -5476,6 +5461,7 @@ public: } [self updateCompass]; + [self updateScaleBar]; if ( ! [self isSuppressingChangeDelimiters]) { @@ -5920,6 +5906,17 @@ public: } } +- (void)updateScaleBar +{ + // Use the `hidden` property (instead of `self.showsScale`) so that we don't + // break developers who still rely on the <4.0.0 approach of directly + // setting this property. + if ( ! self.scaleBar.hidden) + { + [(MGLScaleBar *)self.scaleBar setMetersPerPoint:[self metersPerPointAtLatitude:self.centerCoordinate.latitude]]; + } +} + + (UIImage *)resourceImageNamed:(NSString *)imageName { UIImage *image = [UIImage imageNamed:imageName diff --git a/platform/ios/src/MGLMapboxEvents.h b/platform/ios/src/MGLMapboxEvents.h index 98f59ffd3f..cbac578798 100644 --- a/platform/ios/src/MGLMapboxEvents.h +++ b/platform/ios/src/MGLMapboxEvents.h @@ -1,44 +1,17 @@ #import <Foundation/Foundation.h> - -#import "MGLTypes.h" +#import "MMEEventsManager.h" NS_ASSUME_NONNULL_BEGIN -// Event types -extern NSString *const MGLEventTypeAppUserTurnstile; -extern NSString *const MGLEventTypeMapLoad; -extern NSString *const MGLEventTypeMapTap; -extern NSString *const MGLEventTypeMapDragEnd; -extern NSString *const MGLEventTypeLocation; - -// Event keys -extern NSString *const MGLEventKeyLatitude; -extern NSString *const MGLEventKeyLongitude; -extern NSString *const MGLEventKeyZoomLevel; -extern NSString *const MGLEventKeyGestureID; - -// Gestures -extern NSString *const MGLEventGestureSingleTap; -extern NSString *const MGLEventGestureDoubleTap; -extern NSString *const MGLEventGestureTwoFingerSingleTap; -extern NSString *const MGLEventGestureQuickZoom; -extern NSString *const MGLEventGesturePanStart; -extern NSString *const MGLEventGesturePinchStart; -extern NSString *const MGLEventGestureRotateStart; -extern NSString *const MGLEventGesturePitchStart; - -typedef NS_DICTIONARY_OF(NSString *, id) MGLMapboxEventAttributes; -typedef NS_MUTABLE_DICTIONARY_OF(NSString *, id) MGLMutableMapboxEventAttributes; - @interface MGLMapboxEvents : NSObject -+ (nullable instancetype)sharedManager; ++ (nullable instancetype)sharedInstance; -// You must call these methods from the main thread. -// -+ (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary; -+ (void)ensureMetricsOptoutExists; ++ (void)setupWithAccessToken:(NSString *)accessToken; ++ (void)pushTurnstileEvent; ++ (void)pushEvent:(NSString *)event withAttributes:(MMEMapboxEventAttributes *)attributeDictionary; + (void)flush; ++ (void)ensureMetricsOptoutExists; @end diff --git a/platform/ios/src/MGLMapboxEvents.m b/platform/ios/src/MGLMapboxEvents.m index 273af5b3bc..05f291d8a0 100644 --- a/platform/ios/src/MGLMapboxEvents.m +++ b/platform/ios/src/MGLMapboxEvents.m @@ -1,646 +1,166 @@ #import "MGLMapboxEvents.h" -#import <UIKit/UIKit.h> -#import <CoreLocation/CoreLocation.h> -#import "MGLAccountManager.h" +#import "NSBundle+MGLAdditions.h" #import "NSProcessInfo+MGLAdditions.h" -#import "NSException+MGLAdditions.h" -#import "MGLAPIClient.h" -#import "MGLLocationManager.h" -#import "MGLTelemetryConfig.h" -#include <mbgl/storage/reachability.h> -#include <sys/sysctl.h> +static NSString * const MGLAPIClientUserAgentBase = @"mapbox-maps-ios"; +static NSString * const MGLMapboxAccountType = @"MGLMapboxAccountType"; +static NSString * const MGLMapboxMetricsEnabled = @"MGLMapboxMetricsEnabled"; +static NSString * const MGLMapboxMetricsDebugLoggingEnabled = @"MGLMapboxMetricsDebugLoggingEnabled"; +static NSString * const MGLTelemetryAccessToken = @"MGLTelemetryAccessToken"; +static NSString * const MGLTelemetryBaseURL = @"MGLTelemetryBaseURL"; -// Event types -NSString *const MGLEventTypeAppUserTurnstile = @"appUserTurnstile"; -NSString *const MGLEventTypeMapLoad = @"map.load"; -NSString *const MGLEventTypeMapTap = @"map.click"; -NSString *const MGLEventTypeMapDragEnd = @"map.dragend"; -NSString *const MGLEventTypeLocation = @"location"; -NSString *const MGLEventTypeLocalDebug = @"debug"; +@interface MGLMapboxEvents () -// Gestures -NSString *const MGLEventGestureSingleTap = @"SingleTap"; -NSString *const MGLEventGestureDoubleTap = @"DoubleTap"; -NSString *const MGLEventGestureTwoFingerSingleTap = @"TwoFingerTap"; -NSString *const MGLEventGestureQuickZoom = @"QuickZoom"; -NSString *const MGLEventGesturePanStart = @"Pan"; -NSString *const MGLEventGesturePinchStart = @"Pinch"; -NSString *const MGLEventGestureRotateStart = @"Rotation"; -NSString *const MGLEventGesturePitchStart = @"Pitch"; - -// Event keys -NSString *const MGLEventKeyLatitude = @"lat"; -NSString *const MGLEventKeyLongitude = @"lng"; -NSString *const MGLEventKeyZoomLevel = @"zoom"; -NSString *const MGLEventKeySpeed = @"speed"; -NSString *const MGLEventKeyCourse = @"course"; -NSString *const MGLEventKeyGestureID = @"gesture"; -NSString *const MGLEventHorizontalAccuracy = @"horizontalAccuracy"; -NSString *const MGLEventKeyLocalDebugDescription = @"debug.description"; - -static NSString *const MGLEventKeyEvent = @"event"; -static NSString *const MGLEventKeyCreated = @"created"; -static NSString *const MGLEventKeyVendorID = @"userId"; -static NSString *const MGLEventKeyModel = @"model"; -static NSString *const MGLEventKeyEnabledTelemetry = @"enabled.telemetry"; -static NSString *const MGLEventKeyOperatingSystem = @"operatingSystem"; -static NSString *const MGLEventKeyResolution = @"resolution"; -static NSString *const MGLEventKeyAccessibilityFontScale = @"accessibilityFontScale"; -static NSString *const MGLEventKeyOrientation = @"orientation"; -static NSString *const MGLEventKeyPluggedIn = @"pluggedIn"; -static NSString *const MGLEventKeyWifi = @"wifi"; -static NSString *const MGLEventKeySource = @"source"; -static NSString *const MGLEventKeySessionId = @"sessionId"; -static NSString *const MGLEventKeyApplicationState = @"applicationState"; -static NSString *const MGLEventKeyAltitude = @"altitude"; - -static NSString *const MGLMapboxAccountType = @"MGLMapboxAccountType"; -static NSString *const MGLMapboxMetricsEnabled = @"MGLMapboxMetricsEnabled"; - -// SDK event source -static NSString *const MGLEventSource = @"mapbox"; - -// Event application state -static NSString *const MGLApplicationStateForeground = @"Foreground"; -static NSString *const MGLApplicationStateBackground = @"Background"; -static NSString *const MGLApplicationStateInactive = @"Inactive"; -static NSString *const MGLApplicationStateUnknown = @"Unknown"; - -const NSUInteger MGLMaximumEventsPerFlush = 180; -const NSTimeInterval MGLFlushInterval = 180; - -@interface MGLMapboxEventsData : NSObject - -@property (nonatomic) NSString *vendorId; -@property (nonatomic) NSString *model; -@property (nonatomic) NSString *iOSVersion; -@property (nonatomic) CGFloat scale; - -@end - -@implementation MGLMapboxEventsData - -- (instancetype)init { - if (self = [super init]) { - _vendorId = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; - _model = [self sysInfoByName:"hw.machine"]; - _iOSVersion = [NSString stringWithFormat:@"%@ %@", [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion]; - if ([UIScreen instancesRespondToSelector:@selector(nativeScale)]) { - _scale = [UIScreen mainScreen].nativeScale; - } else { - _scale = [UIScreen mainScreen].scale; - } - } - return self; -} - -- (NSString *)sysInfoByName:(char *)typeSpecifier { - size_t size; - sysctlbyname(typeSpecifier, NULL, &size, NULL, 0); - - char *answer = malloc(size); - sysctlbyname(typeSpecifier, answer, &size, NULL, 0); - - NSString *results = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding]; - - free(answer); - return results; -} +@property (nonatomic) MMEEventsManager *eventsManager; +@property (nonatomic) NSURL *baseURL; +@property (nonatomic, copy) NSString *accessToken; @end +@implementation MGLMapboxEvents -@interface MGLMapboxEvents () <MGLLocationManagerDelegate> - -@property (nonatomic) MGLMapboxEventsData *data; -@property (nonatomic, copy) NSString *appBundleId; -@property (nonatomic, readonly) NSString *instanceID; -@property (nonatomic, copy) NSString *dateForDebugLogFile; -@property (nonatomic) NSDateFormatter *rfc3339DateFormatter; -@property (nonatomic) MGLAPIClient *apiClient; -@property (nonatomic) BOOL usesTestServer; -@property (nonatomic) BOOL canEnableDebugLogging; -@property (nonatomic, getter=isPaused) BOOL paused; -@property (nonatomic) NS_MUTABLE_ARRAY_OF(MGLMapboxEventAttributes *) *eventQueue; -@property (nonatomic) dispatch_queue_t serialQueue; -@property (nonatomic) dispatch_queue_t debugLogSerialQueue; -@property (nonatomic) MGLLocationManager *locationManager; -@property (nonatomic) NSTimer *timer; -@property (nonatomic) NSDate *instanceIDRotationDate; -@property (nonatomic) NSDate *nextTurnstileSendDate; -@property (nonatomic) NSNumber *currentAccountTypeValue; -@property (nonatomic) BOOL currentMetricsEnabledValue; - -@end - -@implementation MGLMapboxEvents { - NSString *_instanceID; - UIBackgroundTaskIdentifier _backgroundTaskIdentifier; -} + (void)initialize { if (self == [MGLMapboxEvents class]) { NSBundle *bundle = [NSBundle mainBundle]; NSNumber *accountTypeNumber = [bundle objectForInfoDictionaryKey:MGLMapboxAccountType]; - [[NSUserDefaults standardUserDefaults] registerDefaults:@{ - MGLMapboxAccountType: accountTypeNumber ?: @0, - MGLMapboxMetricsEnabled: @YES, - @"MGLMapboxMetricsDebugLoggingEnabled": @NO, - }]; + [[NSUserDefaults standardUserDefaults] registerDefaults:@{MGLMapboxAccountType: accountTypeNumber ?: @0, + MGLMapboxMetricsEnabled: @YES, + MGLMapboxMetricsDebugLoggingEnabled: @NO}]; } } -+ (BOOL)isEnabled { -#if TARGET_OS_SIMULATOR - return NO; -#else - BOOL isLowPowerModeEnabled = NO; - if ([NSProcessInfo instancesRespondToSelector:@selector(isLowPowerModeEnabled)]) { - isLowPowerModeEnabled = [[NSProcessInfo processInfo] isLowPowerModeEnabled]; ++ (nullable instancetype)sharedInstance { + if (NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent) { + return nil; } - return ([[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabled] && - [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType] == 0 && - !isLowPowerModeEnabled); -#endif -} - - -- (BOOL)debugLoggingEnabled { - return (self.canEnableDebugLogging && - [[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]); + + static dispatch_once_t onceToken; + static MGLMapboxEvents *_sharedInstance; + dispatch_once(&onceToken, ^{ + _sharedInstance = [[self alloc] init]; + }); + return _sharedInstance; } -- (instancetype) init { +- (instancetype)init { self = [super init]; if (self) { - [MGLTelemetryConfig.sharedConfig configurationFromKey:[[NSUserDefaults standardUserDefaults] objectForKey:MGLMapboxMetricsProfile]]; + _eventsManager = [[MMEEventsManager alloc] init]; + _eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsDebugLoggingEnabled]; + _eventsManager.accountType = [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType]; + _eventsManager.metricsEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabled]; - _currentAccountTypeValue = @0; - _currentMetricsEnabledValue = YES; - - _appBundleId = [[NSBundle mainBundle] bundleIdentifier]; - _apiClient = [[MGLAPIClient alloc] init]; - - NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString]; - _serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); - - _locationManager = [[MGLLocationManager alloc] init]; - _locationManager.delegate = self; - _paused = YES; - [self resumeMetricsCollection]; - - // Events Control - _eventQueue = [[NSMutableArray alloc] init]; - - // Setup Date Format - _rfc3339DateFormatter = [[NSDateFormatter alloc] init]; - NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; - - [_rfc3339DateFormatter setLocale:enUSPOSIXLocale]; - [_rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"]; - // Clear Any System TimeZone Cache - [NSTimeZone resetSystemTimeZone]; - [_rfc3339DateFormatter setTimeZone:[NSTimeZone systemTimeZone]]; - - // Configure logging - if ([self isProbablyAppStoreBuild]) { - self.canEnableDebugLogging = NO; - - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]) { - NSLog(@"Telemetry logging is only enabled in non-app store builds."); - } - } else { - self.canEnableDebugLogging = YES; + // It is possible for the shared instance of this class to be created because of a call to + // +[MGLAccountManager load] early on in the app lifecycle of the host application. + // If user default values for access token and base URL are available, they are stored here + // on local properties so that they can be applied later once MMEEventsManager is fully initialized + // (once -[MMEEventsManager initializeWithAccessToken:userAgentBase:hostSDKVersion:] is called. + // Normally, the telem access token and base URL are not set this way. However, overriding these values + // with user defaults can be useful for testing with an alternative (test) backend system. + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) { + self.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken]; } - - // Watch for changes to telemetry settings by the user - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidEnterBackgroundNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidBecomeActiveNotification object:nil]; - - // Watch for Low Power Mode change events - if (&NSProcessInfoPowerStateDidChangeNotification != NULL) { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:NSProcessInfoPowerStateDidChangeNotification object:nil]; + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) { + self.baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]]; } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil]; } return self; } -// Called implicitly from any public class convenience methods. -// May return nil if this feature is disabled. -// -+ (nullable instancetype)sharedManager { - if (NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent) { - return nil; - } - static dispatch_once_t onceToken; - static MGLMapboxEvents *_sharedManager; - dispatch_once(&onceToken, ^{ - _sharedManager = [[self alloc] init]; - }); - return _sharedManager; -} - - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; - [self pauseMetricsCollection]; } -- (NSString *)instanceID { - if (self.instanceIDRotationDate && [[NSDate date] timeIntervalSinceDate:self.instanceIDRotationDate] >= 0) { - _instanceID = nil; +- (void)userDefaultsDidChange:(NSNotification *)notification { + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateNonDisablingConfigurationValues]; + [self updateDisablingConfigurationValuesWithNotification:notification]; + }); +} + +- (void)updateNonDisablingConfigurationValues { + self.eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]; + + // It is possible for `MGLTelemetryAccessToken` to have been set yet `userDefaultsDidChange:` + // is called before `setupWithAccessToken:` is called. + // In that case, setting the access token here will have no effect. In practice, that's fine + // because the access token value will be resolved when `setupWithAccessToken:` is called eventually + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) { + self.eventsManager.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken]; } - if (!_instanceID) { - _instanceID = [[NSUUID UUID] UUIDString]; - NSTimeInterval twentyFourHourTimeInterval = 24 * 3600; - self.instanceIDRotationDate = [[NSDate date] dateByAddingTimeInterval:twentyFourHourTimeInterval]; + + // It is possible for `MGLTelemetryBaseURL` to have been set yet `userDefaultsDidChange:` + // is called before setupWithAccessToken: is called. + // In that case, setting the base URL here will have no effect. In practice, that's fine + // because the base URL value will be resolved when `setupWithAccessToken:` is called eventually + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) { + NSURL *baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]]; + self.eventsManager.baseURL = baseURL; } - return _instanceID; } -- (void)userDefaultsDidChange:(NSNotification *)notification { - +- (void)updateDisablingConfigurationValuesWithNotification:(NSNotification *)notification { // Guard against over calling pause / resume if the values this implementation actually - // cares about have not changed - + // cares about have not changed. We guard because the pause and resume method checks CoreLocation's + // authorization status and that can drag on the main thread if done too many times (e.g. if the host + // app heavily uses the user defaults API and this method is called very frequently) if ([[notification object] respondsToSelector:@selector(objectForKey:)]) { NSUserDefaults *userDefaults = [notification object]; - NSNumber *accountType = [userDefaults objectForKey:MGLMapboxAccountType]; - BOOL metricsEnabled = [[userDefaults objectForKey:MGLMapboxMetricsEnabled] boolValue]; - - if (![accountType isEqualToNumber:self.currentAccountTypeValue] || metricsEnabled != self.currentMetricsEnabledValue) { - [self pauseOrResumeMetricsCollectionIfRequired]; - self.currentAccountTypeValue = accountType; - self.currentMetricsEnabledValue = metricsEnabled; - } - } - -} - -- (void)pauseOrResumeMetricsCollectionIfRequired { - - // [CLLocationManager authorizationStatus] has been found to block in some cases so - // dispatch the call to a non-UI thread - dispatch_async(self.serialQueue, ^{ - CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + NSInteger accountType = [userDefaults integerForKey:MGLMapboxAccountType]; + BOOL metricsEnabled = [userDefaults boolForKey:MGLMapboxMetricsEnabled]; - // Checking application state must be done on the main thread for safety and - // to avoid a thread sanitizer error - dispatch_async(dispatch_get_main_queue(), ^{ - UIApplication *application = [UIApplication sharedApplication]; - UIApplicationState state = application.applicationState; - - // Prevent blue status bar when host app has `when in use` permission only and it is not in foreground - if (status == kCLAuthorizationStatusAuthorizedWhenInUse && state == UIApplicationStateBackground) { - if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) { - _backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{ - [application endBackgroundTask:_backgroundTaskIdentifier]; - _backgroundTaskIdentifier = UIBackgroundTaskInvalid; - }]; - [self flush]; - } - [self pauseMetricsCollection]; - return; - } + if (accountType != self.eventsManager.accountType || metricsEnabled != self.eventsManager.metricsEnabled) { + self.eventsManager.accountType = accountType; + self.eventsManager.metricsEnabled = metricsEnabled; - // Toggle pause based on current pause state, user opt-out state, and low-power state. - BOOL enabled = [[self class] isEnabled]; - if (self.paused && enabled) { - [self resumeMetricsCollection]; - } else if (!self.paused && !enabled) { - [self flush]; - [self pauseMetricsCollection]; - } - }); - }); -} - -- (void)pauseMetricsCollection { - if (self.paused) { - return; - } - - self.paused = YES; - [self.timer invalidate]; - self.timer = nil; - [self.eventQueue removeAllObjects]; - self.data = nil; - - [self.locationManager stopUpdatingLocation]; -} - -- (void)resumeMetricsCollection { - if (!self.paused || ![[self class] isEnabled]) { - return; - } - - self.paused = NO; - self.data = [[MGLMapboxEventsData alloc] init]; - - [self.locationManager startUpdatingLocation]; -} - -+ (void)flush { - [[MGLMapboxEvents sharedManager] flush]; -} - -- (void)flush { - if ([MGLAccountManager accessToken] == nil) { - return; - } - - NSArray *events = [NSArray arrayWithArray:self.eventQueue]; - [self.eventQueue removeAllObjects]; - - [self postEvents:events]; - - if (self.timer) { - [self.timer invalidate]; - self.timer = nil; - } - - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"flush"}]; -} - -- (void)pushTurnstileEvent { - if (self.nextTurnstileSendDate && [[NSDate date] timeIntervalSinceDate:self.nextTurnstileSendDate] < 0) { - return; - } - - NSString *vendorID = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; - if (!vendorID) { - return; - } - - NSDictionary *turnstileEventAttributes = @{MGLEventKeyEvent: MGLEventTypeAppUserTurnstile, - MGLEventKeyCreated: [self.rfc3339DateFormatter stringFromDate:[NSDate date]], - MGLEventKeyVendorID: vendorID, - MGLEventKeyEnabledTelemetry: @([[self class] isEnabled])}; - - if ([MGLAccountManager accessToken] == nil) { - return; - } - - __weak __typeof__(self) weakSelf = self; - [self.apiClient postEvent:turnstileEventAttributes completionHandler:^(NSError * _Nullable error) { - __strong __typeof__(weakSelf) strongSelf = weakSelf; - if (error) { - [strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error", - @"error": error}]; - return; + [self.eventsManager pauseOrResumeMetricsCollectionIfRequired]; } - [strongSelf writeEventToLocalDebugLog:turnstileEventAttributes]; - [strongSelf updateNextTurnstileSendDate]; - }]; -} - -- (void)updateNextTurnstileSendDate { - // Find the time a day from now (sometime tomorrow) - NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; - NSDateComponents *dayComponent = [[NSDateComponents alloc] init]; - dayComponent.day = 1; - NSDate *sometimeTomorrow = [calendar dateByAddingComponents:dayComponent toDate:[NSDate date] options:0]; - - // Find the start of tomorrow and use that as the next turnstile send date. The effect of this is that - // turnstile events can be sent as much as once per calendar day and always at the start of a session - // when a map load happens. - NSDate *startOfTomorrow = nil; - [calendar rangeOfUnit:NSCalendarUnitDay startDate:&startOfTomorrow interval:nil forDate:sometimeTomorrow]; - self.nextTurnstileSendDate = startOfTomorrow; -} - -+ (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - [[MGLMapboxEvents sharedManager] pushEvent:event withAttributes:attributeDictionary]; -} - -- (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - if (!event) { - return; - } - - if ([event isEqualToString:MGLEventTypeMapLoad]) { - [self pushTurnstileEvent]; - } - - if (self.paused) { - return; - } - - MGLMapboxEventAttributes *fullyFormedEvent = [self fullyFormedEventForEvent:event withAttributes:attributeDictionary]; - if (fullyFormedEvent) { - [self.eventQueue addObject:fullyFormedEvent]; - [self writeEventToLocalDebugLog:fullyFormedEvent]; - // Has Flush Limit Been Reached? - if (self.eventQueue.count >= MGLMaximumEventsPerFlush) { - [self flush]; - } else if (self.eventQueue.count == 1) { - // If this is first new event on queue start timer, - [self startTimer]; - } - } else { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Unknown event", - @"eventName": event, - @"event.attributes": attributeDictionary}]; - } -} - -#pragma mark Events - -- (MGLMapboxEventAttributes *)fullyFormedEventForEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - if ([event isEqualToString:MGLEventTypeMapLoad]) { - return [self mapLoadEventWithAttributes:attributeDictionary]; - } else if ([event isEqualToString:MGLEventTypeMapTap]) { - return [self mapClickEventWithAttributes:attributeDictionary]; - } else if ([event isEqualToString:MGLEventTypeMapDragEnd]) { - return [self mapDragEndEventWithAttributes:attributeDictionary]; - } else if ([event isEqualToString:MGLEventTypeLocation]) { - return [self locationEventWithAttributes:attributeDictionary]; } - return nil; -} - -- (MGLMapboxEventAttributes *)locationEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - MGLMutableMapboxEventAttributes *attributes = [NSMutableDictionary dictionary]; - attributes[MGLEventKeyEvent] = MGLEventTypeLocation; - attributes[MGLEventKeySource] = MGLEventSource; - attributes[MGLEventKeySessionId] = self.instanceID; - attributes[MGLEventKeyOperatingSystem] = self.data.iOSVersion; - NSString *currentApplicationState = [self applicationState]; - if (![currentApplicationState isEqualToString:MGLApplicationStateUnknown]) { - attributes[MGLEventKeyApplicationState] = currentApplicationState; - } - - return [self eventForAttributes:attributes attributeDictionary:attributeDictionary]; -} - -- (MGLMapboxEventAttributes *)mapLoadEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - MGLMutableMapboxEventAttributes *attributes = [NSMutableDictionary dictionary]; - attributes[MGLEventKeyEvent] = MGLEventTypeMapLoad; - attributes[MGLEventKeyCreated] = [self.rfc3339DateFormatter stringFromDate:[NSDate date]]; - attributes[MGLEventKeyVendorID] = self.data.vendorId; - attributes[MGLEventKeyModel] = self.data.model; - attributes[MGLEventKeyOperatingSystem] = self.data.iOSVersion; - attributes[MGLEventKeyResolution] = @(self.data.scale); - attributes[MGLEventKeyAccessibilityFontScale] = @([self contentSizeScale]); - attributes[MGLEventKeyOrientation] = [self deviceOrientation]; - attributes[MGLEventKeyWifi] = @([[MGLReachability reachabilityForLocalWiFi] isReachableViaWiFi]); - - return [self eventForAttributes:attributes attributeDictionary:attributeDictionary]; -} - -- (MGLMapboxEventAttributes *)mapClickEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - MGLMutableMapboxEventAttributes *attributes = [self interactionEvent]; - attributes[MGLEventKeyEvent] = MGLEventTypeMapTap; - return [self eventForAttributes:attributes attributeDictionary:attributeDictionary]; -} - -- (MGLMapboxEventAttributes *)mapDragEndEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - MGLMutableMapboxEventAttributes *attributes = [self interactionEvent]; - attributes[MGLEventKeyEvent] = MGLEventTypeMapDragEnd; - - return [self eventForAttributes:attributes attributeDictionary:attributeDictionary]; } -- (MGLMutableMapboxEventAttributes *)interactionEvent { - MGLMutableMapboxEventAttributes *attributes = [NSMutableDictionary dictionary]; - attributes[MGLEventKeyCreated] = [self.rfc3339DateFormatter stringFromDate:[NSDate date]]; - attributes[MGLEventKeyOrientation] = [self deviceOrientation]; - attributes[MGLEventKeyWifi] = @([[MGLReachability reachabilityForLocalWiFi] isReachableViaWiFi]); - - return attributes; -} - -- (MGLMapboxEventAttributes *)eventForAttributes:(MGLMutableMapboxEventAttributes *)attributes attributeDictionary:(MGLMapboxEventAttributes *)attributeDictionary { - [attributes addEntriesFromDictionary:attributeDictionary]; - - return [attributes copy]; -} - -// Called implicitly from public use of +flush. -// -- (void)postEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { - if (self.paused) { - return; ++ (void)setupWithAccessToken:(NSString *)accessToken { + NSString *semanticVersion = [NSBundle mgl_frameworkInfoDictionary][@"MGLSemanticVersionString"]; + NSString *shortVersion = [NSBundle mgl_frameworkInfoDictionary][@"CFBundleShortVersionString"]; + NSString *sdkVersion = semanticVersion ?: shortVersion; + + // It is possible that an alternative access token was already set on this instance when the class was loaded + // Use it if it exists + NSString *resolvedAccessToken = [MGLMapboxEvents sharedInstance].accessToken ?: accessToken; + + [[[self sharedInstance] eventsManager] initializeWithAccessToken:resolvedAccessToken userAgentBase:MGLAPIClientUserAgentBase hostSDKVersion:sdkVersion]; + + // It is possible that an alternative base URL was set on this instance when the class was loaded + // Use it if it exists + if ([MGLMapboxEvents sharedInstance].baseURL) { + [[MGLMapboxEvents sharedInstance] eventsManager].baseURL = [MGLMapboxEvents sharedInstance].baseURL; } - - __weak __typeof__(self) weakSelf = self; - dispatch_async(self.serialQueue, ^{ - __strong __typeof__(weakSelf) strongSelf = weakSelf; - [self.apiClient postEvents:events completionHandler:^(NSError * _Nullable error) { - if (error) { - [strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error", - @"error": error}]; - } else { - [strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"post", - @"debug.eventsCount": @(events.count)}]; - } - [[UIApplication sharedApplication] endBackgroundTask:_backgroundTaskIdentifier]; - _backgroundTaskIdentifier = UIBackgroundTaskInvalid; - }]; - }); -} - -- (void)startTimer { - [self.timer invalidate]; - self.timer = [NSTimer scheduledTimerWithTimeInterval:MGLFlushInterval - target:self - selector:@selector(flush) - userInfo:nil - repeats:YES]; } -- (NSString *)deviceOrientation { - NSString *result; - - switch ([UIDevice currentDevice].orientation) { - case UIDeviceOrientationUnknown: - result = @"Unknown"; - break; - case UIDeviceOrientationPortrait: - result = @"Portrait"; - break; - case UIDeviceOrientationPortraitUpsideDown: - result = @"PortraitUpsideDown"; - break; - case UIDeviceOrientationLandscapeLeft: - result = @"LandscapeLeft"; - break; - case UIDeviceOrientationLandscapeRight: - result = @"LandscapeRight"; - break; - case UIDeviceOrientationFaceUp: - result = @"FaceUp"; - break; - case UIDeviceOrientationFaceDown: - result = @"FaceDown"; - break; - default: - result = @"Default - Unknown"; - break; - } - - return result; ++ (void)pushTurnstileEvent { + [[[self sharedInstance] eventsManager] sendTurnstileEvent]; } -- (NSString *)applicationState { - switch ([UIApplication sharedApplication].applicationState) { - case UIApplicationStateActive: - return MGLApplicationStateForeground; - case UIApplicationStateInactive: - return MGLApplicationStateInactive; - case UIApplicationStateBackground: - return MGLApplicationStateBackground; - default: - return MGLApplicationStateUnknown; - } ++ (void)pushEvent:(NSString *)event withAttributes:(MMEMapboxEventAttributes *)attributeDictionary { + [[[self sharedInstance] eventsManager] enqueueEventWithName:event attributes:attributeDictionary]; } -- (NSInteger)contentSizeScale { - NSInteger result = -9999; - - NSString *sc = [UIApplication sharedApplication].preferredContentSizeCategory; - - if ([sc isEqualToString:UIContentSizeCategoryExtraSmall]) { - result = -3; - } else if ([sc isEqualToString:UIContentSizeCategorySmall]) { - result = -2; - } else if ([sc isEqualToString:UIContentSizeCategoryMedium]) { - result = -1; - } else if ([sc isEqualToString:UIContentSizeCategoryLarge]) { - result = 0; - } else if ([sc isEqualToString:UIContentSizeCategoryExtraLarge]) { - result = 1; - } else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraLarge]) { - result = 2; - } else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { - result = 3; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityMedium]) { - result = -11; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { - result = 10; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { - result = 11; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { - result = 12; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { - result = 13; - } - - return result; ++ (void)flush { + [[[self sharedInstance] eventsManager] flush]; } + (void)ensureMetricsOptoutExists { NSNumber *shownInAppNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMapboxMetricsEnabledSettingShownInApp"]; BOOL metricsEnabledSettingShownInAppFlag = [shownInAppNumber boolValue]; - + if (!metricsEnabledSettingShownInAppFlag && [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType] == 0) { // Opt-out is not configured in UI, so check for Settings.bundle id defaultEnabledValue; NSString *appSettingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"]; - + if (appSettingsBundle) { // Dynamic Settings.bundle loading based on http://stackoverflow.com/a/510329/2094275 NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[appSettingsBundle stringByAppendingPathComponent:@"Root.plist"]]; @@ -651,7 +171,7 @@ const NSTimeInterval MGLFlushInterval = 180; } } } - + if (!defaultEnabledValue) { [NSException raise:@"Telemetry opt-out missing" format: @"End users must be able to opt out of Mapbox Telemetry in your app, either inside Settings (via Settings.bundle) or inside this app. " @@ -663,153 +183,4 @@ const NSTimeInterval MGLFlushInterval = 180; } } -#pragma mark CLLocationManagerUtilityDelegate - -- (void)locationManager:(MGLLocationManager *)locationManager didUpdateLocations:(NSArray *)locations { - for (CLLocation *loc in locations) { - double accuracy = 10000000; - double lat = floor(loc.coordinate.latitude * accuracy) / accuracy; - double lng = floor(loc.coordinate.longitude * accuracy) / accuracy; - double horizontalAccuracy = round(loc.horizontalAccuracy); - NSString *formattedDate = [self.rfc3339DateFormatter stringFromDate:loc.timestamp]; - [MGLMapboxEvents pushEvent:MGLEventTypeLocation withAttributes:@{MGLEventKeyCreated: formattedDate, - MGLEventKeyLatitude: @(lat), - MGLEventKeyLongitude: @(lng), - MGLEventKeyAltitude: @(round(loc.altitude)), - MGLEventHorizontalAccuracy: @(horizontalAccuracy)}]; - } -} - -- (void)locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:(MGLLocationManager *)locationManager { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerAutoPause"}]; -} - -- (void)locationManagerBackgroundLocationUpdatesDidTimeout:(MGLLocationManager *)locationManager { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerTimeout"}]; -} - -- (void)locationManagerDidStartLocationUpdates:(MGLLocationManager *)locationManager { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerStartUpdates"}]; -} - -- (void)locationManagerDidStopLocationUpdates:(MGLLocationManager *)locationManager { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"locationManager.locationManagerStopUpdates"}]; -} - -#pragma mark MGLMapboxEvents Debug - -- (void)pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - if (![self debugLoggingEnabled]) { - return; - } - - if (!event) { - return; - } - - MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:attributeDictionary]; - [evt setObject:event forKey:@"event"]; - [evt setObject:[self.rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"]; - [evt setValue:[self applicationState] forKey:@"applicationState"]; - [evt setValue:@([[self class] isEnabled]) forKey:@"telemetryEnabled"]; - [evt setObject:self.instanceID forKey:@"instance"]; - - MGLMapboxEventAttributes *finalEvent = [NSDictionary dictionaryWithDictionary:evt]; - [self writeEventToLocalDebugLog:finalEvent]; -} - -- (void)writeEventToLocalDebugLog:(MGLMapboxEventAttributes *)event { - if (![self debugLoggingEnabled]) { - return; - } - - NSLog(@"%@", [self stringForDebugEvent:event]); - - if (!self.dateForDebugLogFile) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy'-'MM'-'dd"]; - [dateFormatter setTimeZone:[NSTimeZone systemTimeZone]]; - self.dateForDebugLogFile = [dateFormatter stringFromDate:[NSDate date]]; - } - - if (!self.debugLogSerialQueue) { - NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString]; - self.debugLogSerialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.debugLog", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); - } - - dispatch_async(self.debugLogSerialQueue, ^{ - if ([NSJSONSerialization isValidJSONObject:event]) { - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:nil]; - - NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - jsonString = [jsonString stringByAppendingString:@",\n"]; - - NSString *logFilePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"telemetry_log-%@.json", self.dateForDebugLogFile]]; - - NSFileManager *fileManager = [[NSFileManager alloc] init]; - if ([fileManager fileExistsAtPath:logFilePath]) { - NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath]; - [fileHandle seekToEndOfFile]; - [fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]]; - } else { - [fileManager createFileAtPath:logFilePath contents:[jsonString dataUsingEncoding:NSUTF8StringEncoding] attributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication }]; - } - } - }); -} - -- (NSString *)stringForDebugEvent:(MGLMapboxEventAttributes *)event { - // redact potentially sensitive location details from system console log - if ([event[@"event"] isEqualToString:MGLEventTypeLocation]) { - MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:event]; - [evt setObject:@"<redacted>" forKey:@"lat"]; - [evt setObject:@"<redacted>" forKey:@"lng"]; - event = evt; - } - - return [NSString stringWithFormat:@"Mapbox Telemetry event %@", event]; -} - -- (BOOL)isProbablyAppStoreBuild { -#if TARGET_IPHONE_SIMULATOR - return NO; -#else - // BugshotKit by Marco Arment https://github.com/marcoarment/BugshotKit/ - // Adapted from https://github.com/blindsightcorp/BSMobileProvision - - NSString *binaryMobileProvision = [NSString stringWithContentsOfFile:[NSBundle.mainBundle pathForResource:@"embedded" ofType:@"mobileprovision"] encoding:NSISOLatin1StringEncoding error:NULL]; - if (!binaryMobileProvision) { - return YES; // no provision - } - - NSScanner *scanner = [NSScanner scannerWithString:binaryMobileProvision]; - NSString *plistString; - if (![scanner scanUpToString:@"<plist" intoString:nil] || ! [scanner scanUpToString:@"</plist>" intoString:&plistString]) { - return YES; // no XML plist found in provision - } - plistString = [plistString stringByAppendingString:@"</plist>"]; - - NSData *plistdata_latin1 = [plistString dataUsingEncoding:NSISOLatin1StringEncoding]; - NSError *error = nil; - NSDictionary *mobileProvision = [NSPropertyListSerialization propertyListWithData:plistdata_latin1 options:NSPropertyListImmutable format:NULL error:&error]; - if (error) { - return YES; // unknown plist format - } - - if (!mobileProvision || ! mobileProvision.count) { - return YES; // no entitlements - } - - if (mobileProvision[@"ProvisionsAllDevices"]) { - return NO; // enterprise provisioning - } - - if (mobileProvision[@"ProvisionedDevices"] && [mobileProvision[@"ProvisionedDevices"] count]) { - return NO; // development or ad-hoc - } - - return YES; // expected development/enterprise/ad-hoc entitlements not found -#endif -} - @end diff --git a/platform/ios/src/MGLTelemetryConfig.h b/platform/ios/src/MGLTelemetryConfig.h index 527d344291..96e525c969 100644 --- a/platform/ios/src/MGLTelemetryConfig.h +++ b/platform/ios/src/MGLTelemetryConfig.h @@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN extern NSString *const MGLMapboxMetricsProfile; -+ (nullable instancetype)sharedConfig; +@property (class, nullable, nonatomic, readonly) MGLTelemetryConfig *sharedConfig; - (void)configurationFromKey:(NSString *)key; diff --git a/platform/ios/src/Mapbox-Prefix.pch b/platform/ios/src/Mapbox-Prefix.pch new file mode 100644 index 0000000000..6754020861 --- /dev/null +++ b/platform/ios/src/Mapbox-Prefix.pch @@ -0,0 +1 @@ +#import "MMENamespacedDependencies.h" diff --git a/platform/ios/src/Mapbox.h b/platform/ios/src/Mapbox.h index 11720ac68e..20417dbbd4 100644 --- a/platform/ios/src/Mapbox.h +++ b/platform/ios/src/Mapbox.h @@ -51,11 +51,10 @@ FOUNDATION_EXPORT MGL_EXPORT const unsigned char MapboxVersionString[]; #import "MGLOpenGLStyleLayer.h" #import "MGLSource.h" #import "MGLTileSource.h" -#import "MGLVectorSource.h" +#import "MGLVectorTileSource.h" #import "MGLShapeSource.h" -#import "MGLAbstractShapeSource.h" #import "MGLComputedShapeSource.h" -#import "MGLRasterSource.h" +#import "MGLRasterTileSource.h" #import "MGLRasterDEMSource.h" #import "MGLImageSource.h" #import "MGLTilePyramidOfflineRegion.h" diff --git a/platform/ios/src/UIColor+MGLAdditions.h b/platform/ios/src/UIColor+MGLAdditions.h index ea415d9db9..60cfe1c58b 100644 --- a/platform/ios/src/UIColor+MGLAdditions.h +++ b/platform/ios/src/UIColor+MGLAdditions.h @@ -12,3 +12,10 @@ + (UIColor *)mgl_colorWithColor:(mbgl::Color)color; @end + +@interface NSExpression (MGLColorAdditions) + ++ (NSExpression *)mgl_expressionForRGBComponents:(NSArray<NSExpression *> *)components; ++ (NSExpression *)mgl_expressionForRGBAComponents:(NSArray<NSExpression *> *)components; + +@end diff --git a/platform/ios/src/UIColor+MGLAdditions.mm b/platform/ios/src/UIColor+MGLAdditions.mm index 41c066c206..9ca39acda4 100644 --- a/platform/ios/src/UIColor+MGLAdditions.mm +++ b/platform/ios/src/UIColor+MGLAdditions.mm @@ -21,3 +21,52 @@ } @end + +@implementation NSExpression (MGLColorAdditions) + ++ (NSExpression *)mgl_expressionForRGBComponents:(NSArray<NSExpression *> *)components { + if (UIColor *color = [self mgl_colorWithRGBComponents:components]) { + return [NSExpression expressionForConstantValue:color]; + } + + NSExpression *color = [NSExpression expressionForConstantValue:[UIColor class]]; + NSExpression *alpha = [NSExpression expressionForConstantValue:@1.0]; + return [NSExpression expressionForFunction:color + selectorName:@"colorWithRed:green:blue:alpha:" + arguments:[components arrayByAddingObject:alpha]]; +} + ++ (NSExpression *)mgl_expressionForRGBAComponents:(NSArray<NSExpression *> *)components { + if (UIColor *color = [self mgl_colorWithRGBComponents:components]) { + return [NSExpression expressionForConstantValue:color]; + } + + NSExpression *color = [NSExpression expressionForConstantValue:[UIColor class]]; + return [NSExpression expressionForFunction:color + selectorName:@"colorWithRed:green:blue:alpha:" + arguments:components]; +} + ++ (UIColor *)mgl_colorWithRGBComponents:(NSArray<NSExpression *> *)components { + if (components.count < 3 || components.count > 4) { + return nil; + } + + for (NSExpression *component in components) { + if (component.expressionType != NSConstantValueExpressionType) { + return nil; + } + + NSNumber *number = (NSNumber *)component.constantValue; + if (![number isKindOfClass:[NSNumber class]]) { + return nil; + } + } + + return [UIColor colorWithRed:[components[0].constantValue doubleValue] / 255.0 + green:[components[1].constantValue doubleValue] / 255.0 + blue:[components[2].constantValue doubleValue] / 255.0 + alpha:components.count == 3 ? [components[3].constantValue doubleValue] : 1.0]; +} + +@end |