summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
Diffstat (limited to 'platform')
-rw-r--r--platform/darwin/settings_nsuserdefaults.mm36
-rw-r--r--platform/default/glfw_view.cpp1
-rw-r--r--platform/ios/MGLMapView.mm1043
-rw-r--r--platform/ios/MGLMapboxEvents.m381
-rw-r--r--platform/ios/MGLMetricsLocationManager.m107
-rw-r--r--platform/ios/MGLUserLocation.m57
-rw-r--r--platform/ios/MGLUserLocationAnnotationView.h18
-rw-r--r--platform/ios/MGLUserLocationAnnotationView.m197
-rw-r--r--platform/ios/MGLUserLocation_Private.h9
-rw-r--r--platform/ios/resources/mapbox.pngbin5947 -> 1958 bytes
-rw-r--r--platform/ios/resources/mapbox@2x.pngbin10713 -> 4492 bytes
-rw-r--r--platform/ios/resources/mapbox@3x.pngbin14888 -> 7059 bytes
m---------platform/ios/vendor/SMCalloutView0
13 files changed, 1797 insertions, 52 deletions
diff --git a/platform/darwin/settings_nsuserdefaults.mm b/platform/darwin/settings_nsuserdefaults.mm
index b9b0e134bf..168cba172d 100644
--- a/platform/darwin/settings_nsuserdefaults.mm
+++ b/platform/darwin/settings_nsuserdefaults.mm
@@ -6,11 +6,15 @@ using namespace mbgl;
Settings_NSUserDefaults::Settings_NSUserDefaults()
{
- [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"longitude" : @(longitude),
- @"latitude" : @(latitude),
- @"zoom" : @(zoom),
- @"bearing" : @(bearing),
- @"debug" : @(debug) }];
+ [[NSUserDefaults standardUserDefaults] registerDefaults:@{
+ @"longitude" : @(longitude),
+ @"latitude" : @(latitude),
+ @"zoom" : @(zoom),
+ @"bearing" : @(bearing),
+ @"userTrackingMode" : @(userTrackingMode),
+ @"showsUserLocation" : @(showsUserLocation),
+ @"debug" : @(debug),
+ }];
load();
}
@@ -23,15 +27,27 @@ void Settings_NSUserDefaults::load()
zoom = [settings[@"zoom"] doubleValue];
bearing = [settings[@"bearing"] doubleValue];
debug = [settings[@"debug"] boolValue];
+
+ unsigned uncheckedTrackingMode = [settings[@"trackingMode"] unsignedIntValue];
+ if (uncheckedTrackingMode > MGLUserTrackingModeNone &&
+ uncheckedTrackingMode <= MGLUserTrackingModeFollowWithHeading)
+ {
+ userTrackingMode = (MGLUserTrackingMode)uncheckedTrackingMode;
+ }
+ showsUserLocation = [settings[@"showsUserLocation"] boolValue];
}
void Settings_NSUserDefaults::save()
{
- [[NSUserDefaults standardUserDefaults] setValuesForKeysWithDictionary:@{ @"longitude" : @(longitude),
- @"latitude" : @(latitude),
- @"zoom" : @(zoom),
- @"bearing" : @(bearing),
- @"debug" : @(debug) }];
+ [[NSUserDefaults standardUserDefaults] setValuesForKeysWithDictionary:@{
+ @"longitude" : @(longitude),
+ @"latitude" : @(latitude),
+ @"zoom" : @(zoom),
+ @"bearing" : @(bearing),
+ @"userTrackingMode" : @(userTrackingMode),
+ @"showsUserLocation" : @(showsUserLocation),
+ @"debug" : @(debug),
+ }];
[[NSUserDefaults standardUserDefaults] synchronize];
}
diff --git a/platform/default/glfw_view.cpp b/platform/default/glfw_view.cpp
index 601ae0ec2d..fd9c701f92 100644
--- a/platform/default/glfw_view.cpp
+++ b/platform/default/glfw_view.cpp
@@ -11,6 +11,7 @@ GLFWView::GLFWView(bool fullscreen_) : fullscreen(fullscreen_) {
}
GLFWView::~GLFWView() {
+ glfwDestroyWindow(window);
glfwTerminate();
}
diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm
index e18c5557d9..a4604bc580 100644
--- a/platform/ios/MGLMapView.mm
+++ b/platform/ios/MGLMapView.mm
@@ -14,6 +14,18 @@
#include <mbgl/storage/network_status.hpp>
#include <mbgl/util/geo.hpp>
+#import "MGLTypes.h"
+#import "MGLAnnotation.h"
+#import "MGLUserLocationAnnotationView.h"
+#import "MGLUserLocation_Private.h"
+
+#import "SMCalloutView.h"
+
+#import "MGLMapboxEvents.h"
+#import "MGLMetricsLocationManager.h"
+
+#import <algorithm>
+
// Returns the path to the default cache database on this system.
const std::string &defaultCacheDatabase() {
static const std::string path = []() -> std::string {
@@ -31,11 +43,18 @@ const std::string &defaultCacheDatabase() {
static dispatch_once_t loadGLExtensions;
+NSString *const MGLDefaultStyleName = @"Emerald";
+NSString *const MGLStyleVersion = @"v7";
+NSString *const MGLDefaultStyleMarkerSymbolName = @"default_marker";
+
NSTimeInterval const MGLAnimationDuration = 0.3;
+const CGSize MGLAnnotationUpdateViewportOutset = {150, 150};
+
+NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey";
#pragma mark - Private -
-@interface MGLMapView () <UIGestureRecognizerDelegate, GLKViewDelegate>
+@interface MGLMapView () <UIGestureRecognizerDelegate, GLKViewDelegate, CLLocationManagerDelegate>
@property (nonatomic) EAGLContext *context;
@property (nonatomic) GLKView *glView;
@@ -48,6 +67,12 @@ NSTimeInterval const MGLAnimationDuration = 0.3;
@property (nonatomic) UIRotationGestureRecognizer *rotate;
@property (nonatomic) UILongPressGestureRecognizer *quickZoom;
@property (nonatomic) NSMutableArray *bundledStyleNames;
+@property (nonatomic) NSMapTable *annotationIDsByAnnotation;
+@property (nonatomic) std::vector<uint32_t> annotationsNearbyLastTap;
+@property (nonatomic, weak) id <MGLAnnotation> selectedAnnotation;
+@property (nonatomic) SMCalloutView *selectedAnnotationCalloutView;
+@property (nonatomic) MGLUserLocationAnnotationView *userLocationAnnotationView;
+@property (nonatomic) CLLocationManager *locationManager;
@property (nonatomic) CGPoint centerPoint;
@property (nonatomic) CGFloat scale;
@property (nonatomic) CGFloat angle;
@@ -59,8 +84,6 @@ NSTimeInterval const MGLAnimationDuration = 0.3;
@implementation MGLMapView
-@synthesize bundledStyleNames=_bundledStyleNames;
-
#pragma mark - Setup & Teardown -
@dynamic debugActive;
@@ -77,7 +100,7 @@ MBGLView *mbglView = nullptr;
mbgl::SQLiteCache *mbglFileCache = nullptr;
mbgl::DefaultFileSource *mbglFileSource = nullptr;
-- (instancetype)initWithFrame:(CGRect)frame styleJSON:(NSString *)styleJSON accessToken:(NSString *)accessToken
+- (instancetype)initWithFrame:(CGRect)frame accessToken:(NSString *)accessToken styleJSON:(NSString *)styleJSON
{
self = [super initWithFrame:frame];
@@ -97,9 +120,22 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
return self;
}
+- (instancetype)initWithFrame:(CGRect)frame accessToken:(NSString *)accessToken bundledStyleNamed:(NSString *)styleName
+{
+ self = [super initWithFrame:frame];
+
+ if (self && [self commonInit])
+ {
+ if (accessToken) [self setAccessToken:accessToken];
+ if (styleName) [self useBundledStyleNamed:styleName];
+ }
+
+ return self;
+}
+
- (instancetype)initWithFrame:(CGRect)frame accessToken:(NSString *)accessToken
{
- return [self initWithFrame:frame styleJSON:nil accessToken:accessToken];
+ return [self initWithFrame:frame accessToken:accessToken styleJSON:nil];
}
- (instancetype)initWithCoder:(NSCoder *)decoder
@@ -118,7 +154,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
{
if (accessToken)
{
- mbglMap->setAccessToken((std::string)[accessToken cStringUsingEncoding:[NSString defaultCStringEncoding]]);
+ mbglMap->setAccessToken((std::string)[accessToken UTF8String]);
+ [[MGLMapboxEvents sharedManager] setToken:accessToken];
}
}
@@ -126,21 +163,19 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
{
if ( ! styleJSON)
{
- [self useBundledStyleNamed:@"bright-v7"];
+ [self useBundledStyleNamed:[[[MGLDefaultStyleName lowercaseString]
+ stringByAppendingString:@"-"]
+ stringByAppendingString:MGLStyleVersion]];
}
else
{
- if ([@(mbglMap->getStyleJSON().c_str()) length]) mbglMap->stop();
- mbglMap->setStyleJSON((std::string)[styleJSON cStringUsingEncoding:[NSString defaultCStringEncoding]]);
- mbglMap->start();
+ mbglMap->setStyleJSON((std::string)[styleJSON UTF8String]);
}
}
- (void)setStyleURL:(NSString *)filePathURL
{
- if ([@(mbglMap->getStyleJSON().c_str()) length]) mbglMap->stop();
mbglMap->setStyleURL(std::string("asset://") + [filePathURL UTF8String]);
- mbglMap->start();
}
- (BOOL)commonInit
@@ -159,6 +194,17 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
// setup accessibility
//
self.accessibilityLabel = @"Map";
+
+ // setup Metrics
+ MGLMapboxEvents *events = [MGLMapboxEvents sharedManager];
+ NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
+ NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
+ if (appName != nil) {
+ events.appName = appName;
+ }
+ if (appVersion != nil) {
+ events.appVersion = appVersion;
+ }
// create GL view
//
@@ -217,6 +263,12 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
Reachability* reachability = [Reachability reachabilityForInternetConnection];
[reachability startNotifier];
+ // setup annotations
+ //
+ _annotationIDsByAnnotation = [NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory];
+ std::string defaultSymbolName([MGLDefaultStyleMarkerSymbolName UTF8String]);
+ mbglMap->setDefaultPointAnnotationSymbol(defaultSymbolName);
+
// setup logo bug
//
_logoBug = [[UIImageView alloc] initWithImage:[MGLMapView resourceImageNamed:@"mapbox.png"]];
@@ -270,6 +322,10 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
doubleTap.numberOfTapsRequired = 2;
[self addGestureRecognizer:doubleTap];
+ UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTapGesture:)];
+ [singleTap requireGestureRecognizerToFail:doubleTap];
+ [self addGestureRecognizer:singleTap];
+
UITapGestureRecognizer *twoFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerTapGesture:)];
twoFingerTap.numberOfTouchesRequired = 2;
[twoFingerTap requireGestureRecognizerToFail:_pinch];
@@ -289,6 +345,9 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
+ // setup dedicated location manager for metrics
+ [MGLMetricsLocationManager sharedManager];
+
// set initial position
//
mbglMap->setLatLngZoom(mbgl::LatLng(0, 0), mbglMap->getMinZoom());
@@ -298,6 +357,31 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
_regionChangeDelegateQueue = [NSOperationQueue new];
_regionChangeDelegateQueue.maxConcurrentOperationCount = 1;
+ // start the main loop
+ mbglMap->start();
+
+
+ // Fire map.load on a background thread
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+
+ NSMutableDictionary *evt = [[NSMutableDictionary alloc] init];
+ [evt setValue:[[NSNumber alloc] initWithDouble:mbglMap->getLatLng().latitude] forKey:@"lat"];
+ [evt setValue:[[NSNumber alloc] initWithDouble:mbglMap->getLatLng().longitude] forKey:@"lng"];
+ [evt setValue:[[NSNumber alloc] initWithDouble:mbglMap->getZoom()] forKey:@"zoom"];
+ [evt setValue:[[NSNumber alloc] initWithBool:[[UIApplication sharedApplication] isRegisteredForRemoteNotifications]] forKey:@"enabled.push"];
+
+ NSString *email = @"Unknown";
+ Class MFMailComposeViewController = NSClassFromString(@"MFMailComposeViewController");
+ if (MFMailComposeViewController) {
+ SEL canSendMail = NSSelectorFromString(@"canSendMail");
+ BOOL sendMail = ((BOOL (*)(id, SEL))[MFMailComposeViewController methodForSelector:canSendMail])(MFMailComposeViewController, canSendMail);
+ email = [NSString stringWithFormat:@"%i", sendMail];
+ }
+ [evt setValue:email forKey:@"enabled.email"];
+
+ [[MGLMapboxEvents sharedManager] pushEvent:@"map.load" withAttributes:evt];
+ });
+
return YES;
}
@@ -339,6 +423,13 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
}
}
+- (void)setDelegate:(id<MGLMapViewDelegate>)delegate
+{
+ if (_delegate == delegate) return;
+
+ _delegate = delegate;
+}
+
#pragma mark - Layout -
- (void)setFrame:(CGRect)frame
@@ -475,6 +566,9 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)appDidBackground:(NSNotification *)notification
{
+ // Flush Any Events Still In Queue
+ [[MGLMapboxEvents sharedManager] flush];
+
mbglMap->stop();
[self.glView deleteDrawable];
@@ -489,13 +583,14 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)tintColorDidChange
{
- for (UIView *subview in self.subviews)
- {
- if ([subview respondsToSelector:@selector(setTintColor:)])
- {
- subview.tintColor = self.tintColor;
- }
- }
+ for (UIView *subview in self.subviews) [self updateTintColorForView:subview];
+}
+
+- (void)updateTintColorForView:(UIView *)view
+{
+ if ([view respondsToSelector:@selector(setTintColor:)]) view.tintColor = self.tintColor;
+
+ for (UIView *subview in view.subviews) [self updateTintColorForView:subview];
}
#pragma mark - Gestures -
@@ -503,12 +598,16 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)handleCompassTapGesture:(id)sender
{
[self resetNorthAnimated:YES];
+
+ if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) self.userTrackingMode = MGLUserTrackingModeFollow;
}
#pragma clang diagnostic pop
- (void)handlePanGesture:(UIPanGestureRecognizer *)pan
{
+ [self trackGestureEvent:@"Pan" forRecognizer:pan];
+
if ( ! self.isScrollEnabled) return;
mbglMap->cancelTransitions();
@@ -516,6 +615,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
if (pan.state == UIGestureRecognizerStateBegan)
{
self.centerPoint = CGPointMake(0, 0);
+
+ self.userTrackingMode = MGLUserTrackingModeNone;
}
else if (pan.state == UIGestureRecognizerStateChanged)
{
@@ -525,6 +626,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
mbglMap->moveBy(delta.x, delta.y);
self.centerPoint = CGPointMake(self.centerPoint.x + delta.x, self.centerPoint.y + delta.y);
+
+ [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
}
else if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled)
{
@@ -560,11 +663,27 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
[weakSelf notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
}];
}
+ else
+ {
+ [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)];
+ }
+
+ // Send Map Drag End Event
+ CGPoint ptInView = CGPointMake([pan locationInView:pan.view].x, [pan locationInView:pan.view].y);
+ CLLocationCoordinate2D coord = [self convertPoint:ptInView toCoordinateFromView:pan.view];
+ NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
+ [dict setValue:[[NSNumber alloc] initWithDouble:coord.latitude] forKey:@"lat"];
+ [dict setValue:[[NSNumber alloc] initWithDouble:coord.longitude] forKey:@"lng"];
+ [dict setValue:[[NSNumber alloc] initWithDouble:[self zoomLevel]] forKey:@"zoom"];
+
+ [[MGLMapboxEvents sharedManager] pushEvent:@"map.dragend" withAttributes:dict];
}
}
- (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinch
{
+ [self trackGestureEvent:@"Pinch" forRecognizer:pinch];
+
if ( ! self.isZoomEnabled) return;
if (mbglMap->getZoom() <= mbglMap->getMinZoom() && pinch.scale < 1) return;
@@ -576,6 +695,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
mbglMap->startScaling();
self.scale = mbglMap->getScale();
+
+ self.userTrackingMode = MGLUserTrackingModeNone;
}
else if (pinch.state == UIGestureRecognizerStateChanged)
{
@@ -593,12 +714,14 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
[self unrotateIfNeededAnimated:YES];
- [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
+ [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)];
}
}
- (void)handleRotateGesture:(UIRotationGestureRecognizer *)rotate
{
+ [self trackGestureEvent:@"Rotation" forRecognizer:rotate];
+
if ( ! self.isRotateEnabled) return;
mbglMap->cancelTransitions();
@@ -608,6 +731,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
mbglMap->startRotating();
self.angle = [MGLMapView degreesToRadians:mbglMap->getBearing()] * -1;
+
+ self.userTrackingMode = MGLUserTrackingModeNone;
}
else if (rotate.state == UIGestureRecognizerStateChanged)
{
@@ -631,17 +756,146 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
[self unrotateIfNeededAnimated:YES];
- [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
+ [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)];
+ }
+}
+
+- (void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap
+{
+ [self trackGestureEvent:@"SingleTap" forRecognizer:singleTap];
+
+ CGPoint tapPoint = [singleTap locationInView:self];
+
+ if (self.userLocationVisible && ! [self.selectedAnnotation isEqual:self.userLocation])
+ {
+ CGRect userLocationRect = CGRectMake(tapPoint.x - 15, tapPoint.y - 15, 30, 30);
+
+ if (CGRectContainsPoint(userLocationRect, [self convertCoordinate:self.userLocation.coordinate toPointToView:self]))
+ {
+ [self selectAnnotation:self.userLocation animated:YES];
+ return;
+ }
+ }
+
+ // tolerances based on touch size & typical marker aspect ratio
+ CGFloat toleranceWidth = 40;
+ CGFloat toleranceHeight = 60;
+
+ // setup a recognition area weighted 2/3 of the way above the point to account for average marker imagery
+ CGRect tapRect = CGRectMake(tapPoint.x - toleranceWidth / 2, tapPoint.y - 2 * toleranceHeight / 3, toleranceWidth, toleranceHeight);
+ CGPoint tapRectLowerLeft = CGPointMake(tapRect.origin.x, tapRect.origin.y + tapRect.size.height);
+ CGPoint tapRectUpperLeft = CGPointMake(tapRect.origin.x, tapRect.origin.y);
+ CGPoint tapRectUpperRight = CGPointMake(tapRect.origin.x + tapRect.size.width, tapRect.origin.y);
+ CGPoint tapRectLowerRight = CGPointMake(tapRect.origin.x + tapRect.size.width, tapRect.origin.y + tapRect.size.height);
+
+ // figure out what that means in coordinate space
+ CLLocationCoordinate2D coordinate;
+ mbgl::LatLngBounds tapBounds;
+
+ coordinate = [self convertPoint:tapRectLowerLeft toCoordinateFromView:self];
+ tapBounds.extend(mbgl::LatLng(coordinate.latitude, coordinate.longitude));
+
+ coordinate = [self convertPoint:tapRectUpperLeft toCoordinateFromView:self];
+ tapBounds.extend(mbgl::LatLng(coordinate.latitude, coordinate.longitude));
+
+ coordinate = [self convertPoint:tapRectUpperRight toCoordinateFromView:self];
+ tapBounds.extend(mbgl::LatLng(coordinate.latitude, coordinate.longitude));
+
+ coordinate = [self convertPoint:tapRectLowerRight toCoordinateFromView:self];
+ tapBounds.extend(mbgl::LatLng(coordinate.latitude, coordinate.longitude));
+
+ // query for nearby annotations
+ std::vector<uint32_t> nearbyAnnotations = mbglMap->getAnnotationsInBounds(tapBounds);
+
+ int32_t newSelectedAnnotationID = -1;
+
+ if (nearbyAnnotations.size())
+ {
+ // there is at least one nearby annotation; select one
+ //
+ // first, sort for comparison and iteration
+ std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end());
+
+ if (nearbyAnnotations == self.annotationsNearbyLastTap)
+ {
+ // the selection candidates haven't changed; cycle through them
+ if (self.selectedAnnotation &&
+ [[[self.annotationIDsByAnnotation objectForKey:self.selectedAnnotation]
+ objectForKey:MGLAnnotationIDKey] unsignedIntValue] == self.annotationsNearbyLastTap.back())
+ {
+ // the selected annotation is the last in the set; cycle back to the first
+ // note: this could be the selected annotation if only one in set
+ newSelectedAnnotationID = self.annotationsNearbyLastTap.front();
+ }
+ else if (self.selectedAnnotation)
+ {
+ // otherwise increment the selection through the candidates
+ uint32_t currentID = [[[self.annotationIDsByAnnotation objectForKey:self.selectedAnnotation] objectForKey:MGLAnnotationIDKey] unsignedIntValue];
+ auto result = std::find(self.annotationsNearbyLastTap.begin(), self.annotationsNearbyLastTap.end(), currentID);
+ auto distance = std::distance(self.annotationsNearbyLastTap.begin(), result);
+ newSelectedAnnotationID = self.annotationsNearbyLastTap[distance + 1];
+ }
+ else
+ {
+ // no current selection; select the first one
+ newSelectedAnnotationID = self.annotationsNearbyLastTap.front();
+ }
+ }
+ else
+ {
+ // start tracking a new set of nearby annotations
+ self.annotationsNearbyLastTap = nearbyAnnotations;
+
+ // select the first one
+ newSelectedAnnotationID = self.annotationsNearbyLastTap.front();
+ }
+ }
+ else
+ {
+ // there are no nearby annotations; deselect if necessary
+ newSelectedAnnotationID = -1;
+ }
+
+ if (newSelectedAnnotationID >= 0)
+ {
+ // find & select model object for selection
+ NSEnumerator *enumerator = self.annotationIDsByAnnotation.keyEnumerator;
+
+ while (id <MGLAnnotation> annotation = enumerator.nextObject)
+ {
+ if ([[[self.annotationIDsByAnnotation objectForKey:annotation] objectForKey:MGLAnnotationIDKey] integerValue] == newSelectedAnnotationID)
+ {
+ // only change selection status if not the currently selected annotation
+ if ( ! [annotation isEqual:self.selectedAnnotation])
+ {
+ [self selectAnnotation:annotation animated:YES];
+ }
+
+ // either way, we should stop enumerating
+ break;
+ }
+ }
+ }
+ else
+ {
+ // deselect any selected annotation
+ if (self.selectedAnnotation) [self deselectAnnotation:self.selectedAnnotation animated:YES];
}
}
- (void)handleDoubleTapGesture:(UITapGestureRecognizer *)doubleTap
{
+ [self trackGestureEvent:@"DoubleTap" forRecognizer:doubleTap];
+
if ( ! self.isZoomEnabled) return;
mbglMap->cancelTransitions();
- if (doubleTap.state == UIGestureRecognizerStateEnded)
+ if (doubleTap.state == UIGestureRecognizerStateBegan)
+ {
+ self.userTrackingMode = MGLUserTrackingModeNone;
+ }
+ else if (doubleTap.state == UIGestureRecognizerStateEnded)
{
mbglMap->scaleBy(2, [doubleTap locationInView:doubleTap.view].x, [doubleTap locationInView:doubleTap.view].y, secondsAsDuration(MGLAnimationDuration));
@@ -662,13 +916,19 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)handleTwoFingerTapGesture:(UITapGestureRecognizer *)twoFingerTap
{
+ [self trackGestureEvent:@"TwoFingerTap" forRecognizer:twoFingerTap];
+
if ( ! self.isZoomEnabled) return;
if (mbglMap->getZoom() == mbglMap->getMinZoom()) return;
mbglMap->cancelTransitions();
- if (twoFingerTap.state == UIGestureRecognizerStateEnded)
+ if (twoFingerTap.state == UIGestureRecognizerStateBegan)
+ {
+ self.userTrackingMode = MGLUserTrackingModeNone;
+ }
+ else if (twoFingerTap.state == UIGestureRecognizerStateEnded)
{
mbglMap->scaleBy(0.5, [twoFingerTap locationInView:twoFingerTap.view].x, [twoFingerTap locationInView:twoFingerTap.view].y, secondsAsDuration(MGLAnimationDuration));
@@ -689,6 +949,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)handleQuickZoomGesture:(UILongPressGestureRecognizer *)quickZoom
{
+ [self trackGestureEvent:@"QuickZoom" forRecognizer:quickZoom];
+
if ( ! self.isZoomEnabled) return;
mbglMap->cancelTransitions();
@@ -698,6 +960,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
self.scale = mbglMap->getScale();
self.quickZoomStart = [quickZoom locationInView:quickZoom.view].y;
+
+ self.userTrackingMode = MGLUserTrackingModeNone;
}
else if (quickZoom.state == UIGestureRecognizerStateChanged)
{
@@ -713,7 +977,16 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
{
[self unrotateIfNeededAnimated:YES];
- [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
+ [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)];
+ }
+}
+
+- (void)handleCalloutAccessoryTapGesture:(UITapGestureRecognizer *)tap
+{
+ if ([self.delegate respondsToSelector:@selector(mapView:annotation:calloutAccessoryControlTapped:)])
+ {
+ [self.delegate mapView:self annotation:self.selectedAnnotation
+ calloutAccessoryControlTapped:(UIControl *)tap.view];
}
}
@@ -724,6 +997,22 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]);
}
+- (void)trackGestureEvent:(NSString *)gesture forRecognizer:(UIGestureRecognizer *)recognizer
+{
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ // Send Map Zoom Event
+ CGPoint ptInView = CGPointMake([recognizer locationInView:recognizer.view].x, [recognizer locationInView:recognizer.view].y);
+ CLLocationCoordinate2D coord = [self convertPoint:ptInView toCoordinateFromView:recognizer.view];
+ NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
+ [dict setValue:[[NSNumber alloc] initWithDouble:coord.latitude] forKey:@"lat"];
+ [dict setValue:[[NSNumber alloc] initWithDouble:coord.longitude] forKey:@"lng"];
+ [dict setValue:[[NSNumber alloc] initWithDouble:[self zoomLevel]] forKey:@"zoom"];
+ [dict setValue:gesture forKey:@"gesture"];
+
+ [[MGLMapboxEvents sharedManager] pushEvent:@"map.click" withAttributes:dict];
+ });
+}
+
#pragma mark - Properties -
#pragma clang diagnostic push
@@ -753,6 +1042,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)resetNorthAnimated:(BOOL)animated
{
+ self.userTrackingMode = MGLUserTrackingModeNone;
+
CGFloat duration = (animated ? MGLAnimationDuration : 0);
mbglMap->setBearing(0, secondsAsDuration(duration));
@@ -766,6 +1057,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
{
if (finished)
{
+ [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)];
+
[UIView animateWithDuration:MGLAnimationDuration
animations:^
{
@@ -778,6 +1071,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)resetPosition
{
mbglMap->resetPosition();
+
+ [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)];
}
- (void)toggleDebug
@@ -787,11 +1082,20 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
#pragma mark - Geography -
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated preservingTracking:(BOOL)tracking
+{
+ self.userTrackingMode = (tracking ? self.userTrackingMode : MGLUserTrackingModeNone);
+
+ [self setCenterCoordinate:coordinate animated:animated];
+}
+
- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated
{
CGFloat duration = (animated ? MGLAnimationDuration : 0);
- mbglMap->setLatLng(mbgl::LatLng(coordinate.latitude, coordinate.longitude), secondsAsDuration(duration));
+ mbglMap->setLatLng(coordinateToLatLng(coordinate), secondsAsDuration(duration));
+
+ [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)];
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
@@ -801,18 +1105,20 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (CLLocationCoordinate2D)centerCoordinate
{
- mbgl::LatLng latLng = mbglMap->getLatLng();
-
- return CLLocationCoordinate2DMake(latLng.latitude, latLng.longitude);
+ return latLngToCoordinate(mbglMap->getLatLng());
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated
{
+ self.userTrackingMode = MGLUserTrackingModeNone;
+
CGFloat duration = (animated ? MGLAnimationDuration : 0);
- mbglMap->setLatLngZoom(mbgl::LatLng(centerCoordinate.latitude, centerCoordinate.longitude), zoomLevel, secondsAsDuration(duration));
+ mbglMap->setLatLngZoom(coordinateToLatLng(centerCoordinate), zoomLevel, secondsAsDuration(duration));
[self unrotateIfNeededAnimated:animated];
+
+ [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)];
}
- (double)zoomLevel
@@ -822,11 +1128,15 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated
{
+ self.userTrackingMode = MGLUserTrackingModeNone;
+
CGFloat duration = (animated ? MGLAnimationDuration : 0);
mbglMap->setZoom(zoomLevel, secondsAsDuration(duration));
[self unrotateIfNeededAnimated:animated];
+
+ [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)];
}
- (void)setZoomLevel:(double)zoomLevel
@@ -834,6 +1144,22 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
[self setZoomLevel:zoomLevel animated:NO];
}
+- (void)zoomToSouthWestCoordinate:(CLLocationCoordinate2D)southWestCoordinate northEastCoordinate:(CLLocationCoordinate2D)northEastCoordinate animated:(BOOL)animated
+{
+ // NOTE: does not disrupt tracking mode
+
+ CLLocationCoordinate2D center = CLLocationCoordinate2DMake((northEastCoordinate.latitude + southWestCoordinate.latitude) / 2, (northEastCoordinate.longitude + southWestCoordinate.longitude) / 2);
+
+ CGFloat scale = mbglMap->getScale();
+ CGFloat scaleX = mbglMap->getState().getWidth() / (northEastCoordinate.longitude - southWestCoordinate.longitude);
+ CGFloat scaleY = mbglMap->getState().getHeight() / (northEastCoordinate.latitude - southWestCoordinate.latitude);
+ CGFloat minZoom = mbglMap->getMinZoom();
+ CGFloat maxZoom = mbglMap->getMaxZoom();
+ CGFloat zoomLevel = MAX(MIN(log(scale * MIN(scaleX, scaleY)) / log(2), maxZoom), minZoom);
+
+ [self setCenterCoordinate:center zoomLevel:zoomLevel animated:animated];
+}
+
- (CLLocationDirection)direction
{
double direction = mbglMap->getBearing() * -1;
@@ -848,9 +1174,13 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
{
if ( ! animated && ! self.rotationAllowed) return;
+ self.userTrackingMode = MGLUserTrackingModeNone;
+
CGFloat duration = (animated ? MGLAnimationDuration : 0);
mbglMap->setBearing(direction * -1, secondsAsDuration(duration));
+
+ [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)];
}
- (void)setDirection:(CLLocationDirection)direction
@@ -866,9 +1196,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
//
convertedPoint.y = self.bounds.size.height - convertedPoint.y;
- mbgl::LatLng latLng = mbglMap->latLngForPixel(mbgl::vec2<double>(convertedPoint.x, convertedPoint.y));
-
- return CLLocationCoordinate2DMake(latLng.latitude, latLng.longitude);
+ return latLngToCoordinate(mbglMap->latLngForPixel(mbgl::vec2<double>(convertedPoint.x, convertedPoint.y)));
}
- (CGPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(UIView *)view
@@ -887,20 +1215,46 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
return mbglMap->getMetersPerPixelAtLatitude(latitude, self.zoomLevel);
}
+mbgl::LatLng coordinateToLatLng(CLLocationCoordinate2D coordinate)
+{
+ return mbgl::LatLng(coordinate.latitude, coordinate.longitude);
+}
+
+CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
+{
+ return CLLocationCoordinate2DMake(latLng.latitude, latLng.longitude);
+}
+
+- (mbgl::LatLngBounds)viewportBounds
+{
+ mbgl::LatLngBounds bounds;
+
+ bounds.extend(coordinateToLatLng(
+ [self convertPoint:CGPointMake(0, 0) toCoordinateFromView:self]));
+ bounds.extend(coordinateToLatLng(
+ [self convertPoint:CGPointMake(self.bounds.size.width, 0) toCoordinateFromView:self]));
+ bounds.extend(coordinateToLatLng(
+ [self convertPoint:CGPointMake(0, self.bounds.size.height) toCoordinateFromView:self]));
+ bounds.extend(coordinateToLatLng(
+ [self convertPoint:CGPointMake(self.bounds.size.width, self.bounds.size.height) toCoordinateFromView:self]));
+
+ return bounds;
+}
+
#pragma mark - Styling -
- (NSDictionary *)getRawStyle
{
const std::string styleJSON = mbglMap->getStyleJSON();
- return [NSJSONSerialization JSONObjectWithData:[@(styleJSON.c_str()) dataUsingEncoding:[NSString defaultCStringEncoding]] options:0 error:nil];
+ return [NSJSONSerialization JSONObjectWithData:[@(styleJSON.c_str()) dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
}
- (void)setRawStyle:(NSDictionary *)style
{
NSData *data = [NSJSONSerialization dataWithJSONObject:style options:0 error:nil];
- [self setStyleJSON:[[NSString alloc] initWithData:data encoding:[NSString defaultCStringEncoding]]];
+ [self setStyleJSON:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]];
}
- (NSArray *)bundledStyleNames
@@ -965,13 +1319,583 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
for (NSString *appliedClass in appliedClasses)
{
- newAppliedClasses.insert(newAppliedClasses.end(), [appliedClass cStringUsingEncoding:[NSString defaultCStringEncoding]]);
+ newAppliedClasses.insert(newAppliedClasses.end(), [appliedClass UTF8String]);
}
mbglMap->setDefaultTransitionDuration(secondsAsDuration(transitionDuration));
mbglMap->setClasses(newAppliedClasses);
}
+#pragma mark - Annotations -
+
+- (NSArray *)annotations
+{
+ if ([_annotationIDsByAnnotation count])
+ {
+ NSMutableArray *result = [NSMutableArray array];
+
+ NSEnumerator *keyEnumerator = [_annotationIDsByAnnotation keyEnumerator];
+ id <MGLAnnotation> annotation;
+
+ while (annotation = [keyEnumerator nextObject])
+ {
+ [result addObject:annotation];
+ }
+
+ return [NSArray arrayWithArray:result];
+ }
+
+ return nil;
+}
+
+- (void)addAnnotation:(id <MGLAnnotation>)annotation
+{
+ if ( ! annotation) return;
+
+ // The core bulk add API is efficient with respect to indexing and
+ // screen refreshes, thus we should defer to it even for individual adds.
+ //
+ [self addAnnotations:@[ annotation ]];
+}
+
+- (void)addAnnotations:(NSArray *)annotations
+{
+ if ( ! annotations) return;
+
+ std::vector<mbgl::LatLng> latLngs;
+ latLngs.reserve(annotations.count);
+
+ std::vector<std::string> symbols;
+ symbols.reserve(annotations.count);
+
+ BOOL delegateImplementsSymbolLookup = [self.delegate respondsToSelector:@selector(mapView:symbolNameForAnnotation:)];
+
+ for (id <MGLAnnotation> annotation in annotations)
+ {
+ assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]);
+
+ latLngs.push_back(coordinateToLatLng(annotation.coordinate));
+
+ NSString *symbolName = nil;
+
+ if (delegateImplementsSymbolLookup)
+ {
+ symbolName = [self.delegate mapView:self symbolNameForAnnotation:annotation];
+ }
+
+ symbols.push_back((symbolName ? [symbolName UTF8String] : ""));
+ }
+
+ std::vector<uint32_t> annotationIDs = mbglMap->addPointAnnotations(latLngs, symbols);
+
+ for (size_t i = 0; i < annotationIDs.size(); ++i)
+ {
+ [self.annotationIDsByAnnotation setObject:@{ MGLAnnotationIDKey : @(annotationIDs[i]) }
+ forKey:annotations[i]];
+ }
+}
+
+- (void)removeAnnotation:(id <MGLAnnotation>)annotation
+{
+ if ( ! annotation) return;
+
+ // The core bulk deletion API is efficient with respect to indexing
+ // and screen refreshes, thus we should defer to it even for
+ // individual deletes.
+ //
+ [self removeAnnotations:@[ annotation ]];
+}
+
+- (void)removeAnnotations:(NSArray *)annotations
+{
+ if ( ! annotations) return;
+
+ std::vector<uint32_t> annotationIDsToRemove;
+ annotationIDsToRemove.reserve(annotations.count);
+
+ for (id <MGLAnnotation> annotation in annotations)
+ {
+ assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]);
+
+ annotationIDsToRemove.push_back([[[self.annotationIDsByAnnotation objectForKey:annotation]
+ objectForKey:MGLAnnotationIDKey] unsignedIntValue]);
+ [self.annotationIDsByAnnotation removeObjectForKey:annotation];
+
+ if (annotation == self.selectedAnnotation)
+ {
+ [self deselectAnnotation:annotation animated:NO];
+ }
+ }
+
+ mbglMap->removeAnnotations(annotationIDsToRemove);
+}
+
+- (NSArray *)selectedAnnotations
+{
+ return (self.selectedAnnotation ? @[ self.selectedAnnotation ] : @[]);
+}
+
+- (void)setSelectedAnnotations:(NSArray *)selectedAnnotations
+{
+ if ( ! selectedAnnotations.count) return;
+
+ id <MGLAnnotation> firstAnnotation = selectedAnnotations[0];
+
+ assert([firstAnnotation conformsToProtocol:@protocol(MGLAnnotation)]);
+
+ if ( ! [self viewportBounds].contains(coordinateToLatLng(firstAnnotation.coordinate))) return;
+
+ [self selectAnnotation:firstAnnotation animated:NO];
+}
+
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated
+{
+ if ( ! annotation) return;
+
+ if ( ! [self viewportBounds].contains(coordinateToLatLng(annotation.coordinate))) return;
+
+ if (annotation == self.selectedAnnotation) return;
+
+ self.userTrackingMode = MGLUserTrackingModeNone;
+
+ [self deselectAnnotation:self.selectedAnnotation animated:NO];
+
+ self.selectedAnnotation = annotation;
+
+ if (annotation.title && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] &&
+ [self.delegate mapView:self annotationCanShowCallout:annotation])
+ {
+ // build the callout
+ self.selectedAnnotationCalloutView = [self calloutViewForAnnotation:annotation];
+
+ CGRect calloutBounds;
+
+ if ([annotation isEqual:self.userLocation])
+ {
+ CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self];
+ calloutBounds = CGRectMake(calloutAnchorPoint.x - 1, calloutAnchorPoint.y - 13, 0, 0);
+ }
+ else
+ {
+ // determine symbol in use for point
+ NSString *symbol = MGLDefaultStyleMarkerSymbolName;
+ if ([self.delegate respondsToSelector:@selector(mapView:symbolNameForAnnotation:)])
+ {
+ symbol = [self.delegate mapView:self symbolNameForAnnotation:annotation];
+ }
+ std::string symbolName([symbol UTF8String]);
+
+ // determine anchor point based on symbol
+ CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self];
+ double y = mbglMap->getTopOffsetPixelsForAnnotationSymbol(symbolName);
+ calloutBounds = CGRectMake(calloutAnchorPoint.x - 1, calloutAnchorPoint.y + y, 0, 0);
+ }
+
+ // consult delegate for left and/or right accessory views
+ if ([self.delegate respondsToSelector:@selector(mapView:leftCalloutAccessoryViewForAnnotation:)])
+ {
+ self.selectedAnnotationCalloutView.leftAccessoryView =
+ [self.delegate mapView:self leftCalloutAccessoryViewForAnnotation:annotation];
+
+ if ([self.selectedAnnotationCalloutView.leftAccessoryView isKindOfClass:[UIControl class]])
+ {
+ UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
+ action:@selector(handleCalloutAccessoryTapGesture:)];
+
+ [self.selectedAnnotationCalloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap];
+ }
+ }
+
+ if ([self.delegate respondsToSelector:@selector(mapView:rightCalloutAccessoryViewForAnnotation:)])
+ {
+ self.selectedAnnotationCalloutView.rightAccessoryView =
+ [self.delegate mapView:self rightCalloutAccessoryViewForAnnotation:annotation];
+
+ if ([self.selectedAnnotationCalloutView.rightAccessoryView isKindOfClass:[UIControl class]])
+ {
+ UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
+ action:@selector(handleCalloutAccessoryTapGesture:)];
+
+ [self.selectedAnnotationCalloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap];
+ }
+ }
+
+ // present popup
+ [self.selectedAnnotationCalloutView presentCalloutFromRect:calloutBounds
+ inView:self.glView
+ constrainedToView:self.glView
+ animated:animated];
+ }
+
+ // notify delegate
+ if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)])
+ {
+ [self.delegate mapView:self didSelectAnnotation:annotation];
+ }
+}
+
+- (SMCalloutView *)calloutViewForAnnotation:(id <MGLAnnotation>)annotation
+{
+ SMCalloutView *calloutView = [SMCalloutView platformCalloutView];
+
+ if ([annotation respondsToSelector:@selector(title)]) calloutView.title = annotation.title;
+ if ([annotation respondsToSelector:@selector(subtitle)]) calloutView.subtitle = annotation.subtitle;
+
+ calloutView.tintColor = self.tintColor;
+
+ return calloutView;
+}
+
+- (void)deselectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated
+{
+ if ( ! annotation) return;
+
+ if ([self.selectedAnnotation isEqual:annotation])
+ {
+ // dismiss popup
+ [self.selectedAnnotationCalloutView dismissCalloutAnimated:animated];
+
+ // clean up
+ self.selectedAnnotationCalloutView = nil;
+ self.selectedAnnotation = nil;
+ }
+
+ // notify delegate
+ if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)])
+ {
+ [self.delegate mapView:self didDeselectAnnotation:annotation];
+ }
+}
+
+#pragma mark - User Location -
+
+- (void)setShowsUserLocation:(BOOL)showsUserLocation
+{
+ if (showsUserLocation == _showsUserLocation) return;
+
+ _showsUserLocation = showsUserLocation;
+
+ if (showsUserLocation)
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewWillStartLocatingUser:)])
+ {
+ [self.delegate mapViewWillStartLocatingUser:self];
+ }
+
+ self.userLocationAnnotationView = [[MGLUserLocationAnnotationView alloc] initInMapView:self];
+
+ self.locationManager = [CLLocationManager new];
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
+ // enable iOS 8+ location authorization API
+ //
+ if ([CLLocationManager instancesRespondToSelector:@selector(requestWhenInUseAuthorization)])
+ {
+ BOOL hasLocationDescription = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"] ||
+ [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"];
+ NSAssert(hasLocationDescription,
+ @"For iOS 8 and above, your app must have a value for NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription in its Info.plist");
+ [self.locationManager requestWhenInUseAuthorization];
+ }
+#endif
+
+ self.locationManager.headingFilter = 5.0;
+ self.locationManager.delegate = self;
+ [self.locationManager startUpdatingLocation];
+ }
+ else
+ {
+ [self.locationManager stopUpdatingLocation];
+ [self.locationManager stopUpdatingHeading];
+ self.locationManager.delegate = nil;
+ self.locationManager = nil;
+
+ if ([self.delegate respondsToSelector:@selector(mapViewDidStopLocatingUser:)])
+ {
+ [self.delegate mapViewDidStopLocatingUser:self];
+ }
+
+ [self setUserTrackingMode:MGLUserTrackingModeNone animated:YES];
+
+ [self.userLocationAnnotationView removeFromSuperview];
+ self.userLocationAnnotationView = nil;
+ }
+}
+
+- (void)setUserLocationAnnotationView:(MGLUserLocationAnnotationView *)newAnnotationView
+{
+ if ( ! [newAnnotationView isEqual:_userLocationAnnotationView])
+ {
+ _userLocationAnnotationView = newAnnotationView;
+ }
+}
+
++ (NSSet *)keyPathsForValuesAffectingUserLocation
+{
+ return [NSSet setWithObject:@"userLocationAnnotationView"];
+}
+
+- (MGLUserLocation *)userLocation
+{
+ return self.userLocationAnnotationView.annotation;
+}
+
+- (BOOL)isUserLocationVisible
+{
+ if (self.userLocationAnnotationView)
+ {
+ CGPoint locationPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self];
+
+ CGRect locationRect = CGRectMake(locationPoint.x - self.userLocation.location.horizontalAccuracy,
+ locationPoint.y - self.userLocation.location.horizontalAccuracy,
+ self.userLocation.location.horizontalAccuracy * 2,
+ self.userLocation.location.horizontalAccuracy * 2);
+
+ return CGRectIntersectsRect([self bounds], locationRect);
+ }
+
+ return NO;
+}
+
+- (void)setUserTrackingMode:(MGLUserTrackingMode)mode
+{
+ [self setUserTrackingMode:mode animated:YES];
+}
+
+- (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated
+{
+ if (mode == _userTrackingMode) return;
+
+ if (mode == MGLUserTrackingModeFollowWithHeading && ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate))
+ {
+ mode = MGLUserTrackingModeNone;
+ }
+
+ _userTrackingMode = mode;
+
+ switch (_userTrackingMode)
+ {
+ case MGLUserTrackingModeNone:
+ default:
+ {
+ [self.locationManager stopUpdatingHeading];
+
+ break;
+ }
+ case MGLUserTrackingModeFollow:
+ {
+ self.showsUserLocation = YES;
+
+ [self.locationManager stopUpdatingHeading];
+
+ if (self.userLocationAnnotationView)
+ {
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [self locationManager:self.locationManager didUpdateToLocation:self.userLocation.location fromLocation:self.userLocation.location];
+ #pragma clang diagnostic pop
+ }
+
+ break;
+ }
+ case MGLUserTrackingModeFollowWithHeading:
+ {
+ self.showsUserLocation = YES;
+
+ if (self.zoomLevel < 3) [self setZoomLevel:3 animated:YES];
+
+ if (self.userLocationAnnotationView)
+ {
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [self locationManager:self.locationManager didUpdateToLocation:self.userLocation.location fromLocation:self.userLocation.location];
+ #pragma clang diagnostic pop
+ }
+
+ [self updateHeadingForDeviceOrientation];
+
+ [self.locationManager startUpdatingHeading];
+
+ break;
+ }
+ }
+
+ if ([self.delegate respondsToSelector:@selector(mapView:didChangeUserTrackingMode:animated:)])
+ {
+ [self.delegate mapView:self didChangeUserTrackingMode:_userTrackingMode animated:animated];
+ }
+}
+
+- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
+{
+ (void)manager;
+
+ if ( ! _showsUserLocation || ! newLocation || ! CLLocationCoordinate2DIsValid(newLocation.coordinate)) return;
+
+ if ([newLocation distanceFromLocation:oldLocation] || ! oldLocation)
+ {
+ self.userLocation.location = newLocation;
+
+ // deselect user if applicable since we don't do callout tracking yet
+ if ([self.selectedAnnotation isEqual:self.userLocation]) [self deselectAnnotation:self.userLocation animated:NO];
+
+ if ([self.delegate respondsToSelector:@selector(mapView:didUpdateUserLocation:)])
+ {
+ [self.delegate mapView:self didUpdateUserLocation:self.userLocation];
+ }
+ }
+
+ if (self.userTrackingMode != MGLUserTrackingModeNone)
+ {
+ // center on user location unless we're already centered there (or very close)
+ //
+ CGPoint mapCenterPoint = [self convertPoint:self.center fromView:self.superview];
+ CGPoint userLocationPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self];
+
+ if (std::abs(userLocationPoint.x - mapCenterPoint.x) > 1.0 || std::abs(userLocationPoint.y - mapCenterPoint.y) > 1.0)
+ {
+ if (round(self.zoomLevel) >= 10)
+ {
+ // at sufficient detail, just re-center the map; don't zoom
+ //
+ [self setCenterCoordinate:self.userLocation.location.coordinate animated:YES preservingTracking:YES];
+ }
+ else
+ {
+ // otherwise re-center and zoom in to near accuracy confidence
+ //
+ float delta = (newLocation.horizontalAccuracy / 110000) * 1.2; // approx. meter per degree latitude, plus some margin
+
+ CLLocationCoordinate2D desiredSouthWest = CLLocationCoordinate2DMake(newLocation.coordinate.latitude - delta,
+ newLocation.coordinate.longitude - delta);
+
+ CLLocationCoordinate2D desiredNorthEast = CLLocationCoordinate2DMake(newLocation.coordinate.latitude + delta,
+ newLocation.coordinate.longitude + delta);
+
+ CGFloat pixelRadius = fminf(self.bounds.size.width, self.bounds.size.height) / 2;
+
+ CLLocationCoordinate2D actualSouthWest = [self convertPoint:CGPointMake(userLocationPoint.x - pixelRadius,
+ userLocationPoint.y - pixelRadius)
+ toCoordinateFromView:self];
+
+ CLLocationCoordinate2D actualNorthEast = [self convertPoint:CGPointMake(userLocationPoint.x + pixelRadius,
+ userLocationPoint.y + pixelRadius)
+ toCoordinateFromView:self];
+
+ if (desiredNorthEast.latitude != actualNorthEast.latitude ||
+ desiredNorthEast.longitude != actualNorthEast.longitude ||
+ desiredSouthWest.latitude != actualSouthWest.latitude ||
+ desiredSouthWest.longitude != actualSouthWest.longitude)
+ {
+ // assumes we won't disrupt tracking mode
+ [self zoomToSouthWestCoordinate:desiredSouthWest northEastCoordinate:desiredNorthEast animated:YES];
+ }
+ }
+ }
+ }
+
+ self.userLocationAnnotationView.layer.hidden = ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate);
+
+ self.userLocationAnnotationView.haloLayer.hidden = ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate) ||
+ newLocation.horizontalAccuracy > 10;
+
+ [self updateUserLocationAnnotationView];
+}
+
+- (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager
+{
+ (void)manager;
+
+ if (self.displayHeadingCalibration) [self.locationManager performSelector:@selector(dismissHeadingCalibrationDisplay)
+ withObject:nil
+ afterDelay:10.0];
+
+ return self.displayHeadingCalibration;
+}
+
+- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
+{
+ (void)manager;
+
+ if ( ! _showsUserLocation || self.pan.state == UIGestureRecognizerStateBegan || newHeading.headingAccuracy < 0) return;
+
+ self.userLocation.heading = newHeading;
+
+ if ([self.delegate respondsToSelector:@selector(mapView:didUpdateUserLocation:)])
+ {
+ [self.delegate mapView:self didUpdateUserLocation:self.userLocation];
+
+ if ( ! _showsUserLocation) return;
+ }
+
+ CLLocationDirection headingDirection = (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading);
+
+ if (headingDirection > 0 && self.userTrackingMode == MGLUserTrackingModeFollowWithHeading)
+ {
+ mbglMap->setBearing(headingDirection, secondsAsDuration(MGLAnimationDuration));
+ }
+}
+
+- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
+{
+ (void)manager;
+
+ if (status == kCLAuthorizationStatusDenied || status == kCLAuthorizationStatusRestricted)
+ {
+ self.userTrackingMode = MGLUserTrackingModeNone;
+ self.showsUserLocation = NO;
+ }
+}
+
+- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
+{
+ (void)manager;
+
+ if ([error code] == kCLErrorDenied)
+ {
+ self.userTrackingMode = MGLUserTrackingModeNone;
+ self.showsUserLocation = NO;
+
+ if ([self.delegate respondsToSelector:@selector(mapView:didFailToLocateUserWithError:)])
+ {
+ [self.delegate mapView:self didFailToLocateUserWithError:error];
+ }
+ }
+}
+
+- (void)updateHeadingForDeviceOrientation
+{
+ if (self.locationManager)
+ {
+ // note that right/left device and interface orientations are opposites (see UIApplication.h)
+ //
+ switch ([[UIApplication sharedApplication] statusBarOrientation])
+ {
+ case (UIInterfaceOrientationLandscapeLeft):
+ {
+ self.locationManager.headingOrientation = CLDeviceOrientationLandscapeRight;
+ break;
+ }
+ case (UIInterfaceOrientationLandscapeRight):
+ {
+ self.locationManager.headingOrientation = CLDeviceOrientationLandscapeLeft;
+ break;
+ }
+ case (UIInterfaceOrientationPortraitUpsideDown):
+ {
+ self.locationManager.headingOrientation = CLDeviceOrientationPortraitUpsideDown;
+ break;
+ }
+ case (UIInterfaceOrientationPortrait):
+ default:
+ {
+ self.locationManager.headingOrientation = CLDeviceOrientationPortrait;
+ break;
+ }
+ }
+ }
+}
+
#pragma mark - Utility -
+ (CGFloat)degreesToRadians:(CGFloat)degrees
@@ -1049,6 +1973,11 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
case mbgl::MapChangeRegionWillChange:
case mbgl::MapChangeRegionWillChangeAnimated:
{
+ [self updateUserLocationAnnotationView];
+ [self updateCompass];
+
+ [self deselectAnnotation:self.selectedAnnotation animated:NO];
+
BOOL animated = ([change unsignedIntegerValue] == mbgl::MapChangeRegionWillChangeAnimated);
@synchronized (self.regionChangeDelegateQueue)
@@ -1079,9 +2008,20 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
}
break;
}
+ case mbgl::MapChangeRegionIsChanging:
+ {
+ [self updateUserLocationAnnotationView];
+ [self updateCompass];
+
+ if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)])
+ {
+ [self.delegate mapViewRegionIsChanging:self];
+ }
+ }
case mbgl::MapChangeRegionDidChange:
case mbgl::MapChangeRegionDidChangeAnimated:
{
+ [self updateUserLocationAnnotationView];
[self updateCompass];
if (self.pan.state == UIGestureRecognizerStateChanged ||
@@ -1147,6 +2087,25 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
}
}
+- (void)updateUserLocationAnnotationView
+{
+ if ( ! self.userLocationAnnotationView.superview) [self.glView addSubview:self.userLocationAnnotationView];
+
+ CGPoint userPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self];
+
+ if (CGRectContainsPoint(CGRectInset(self.bounds, -MGLAnnotationUpdateViewportOutset.width,
+ -MGLAnnotationUpdateViewportOutset.height), userPoint))
+ {
+ self.userLocationAnnotationView.center = userPoint;
+
+ [self.userLocationAnnotationView setupLayers];
+ }
+ else
+ {
+ self.userLocationAnnotationView.layer.hidden = YES;
+ }
+}
+
- (void)updateCompass
{
double degrees = mbglMap->getBearing() * -1;
@@ -1198,8 +2157,11 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)invalidate
{
- // This is run in the main/UI thread.
+ assert([[NSThread currentThread] isMainThread]);
+
[self.glView setNeedsDisplay];
+
+ [self notifyMapChange:@(mbgl::MapChangeRegionIsChanging)];
}
class MBGLView : public mbgl::View
@@ -1227,12 +2189,9 @@ class MBGLView : public mbgl::View
}
else
{
- dispatch_async(dispatch_get_main_queue(), ^
- {
- [nativeView performSelector:@selector(notifyMapChange:)
- withObject:@(change)
- afterDelay:0];
- });
+ assert([[NSThread currentThread] isMainThread]);
+
+ [nativeView notifyMapChange:@(change)];
}
}
diff --git a/platform/ios/MGLMapboxEvents.m b/platform/ios/MGLMapboxEvents.m
new file mode 100644
index 0000000000..8a037f18f5
--- /dev/null
+++ b/platform/ios/MGLMapboxEvents.m
@@ -0,0 +1,381 @@
+//
+// MapboxEvents.m
+// MapboxEvents
+//
+// Dynamic detection of ASIdentifierManager from Mixpanel
+// https://github.com/mixpanel/mixpanel-iphone/blob/master/LICENSE
+//
+// Created by Brad Leege on 3/5/15.
+// Copyright (c) 2015 Mapbox. All rights reserved.
+//
+
+#import "MGLMapboxEvents.h"
+#import <UIKit/UIKit.h>
+#import <CoreTelephony/CTTelephonyNetworkInfo.h>
+#import <CoreTelephony/CTCarrier.h>
+#include <sys/sysctl.h>
+#import <SystemConfiguration/CaptiveNetwork.h>
+
+@interface MGLMapboxEvents()
+
+@property (atomic) NSMutableArray *queue;
+@property (atomic) NSString *instance;
+@property (atomic) NSString *anonid;
+@property (atomic) NSTimer *timer;
+@property (atomic) NSString *userAgent;
+@property (atomic) dispatch_queue_t serialqPush;
+@property (atomic) dispatch_queue_t serialqFlush;
+
+@end
+
+@implementation MGLMapboxEvents
+
+static MGLMapboxEvents *sharedManager = nil;
+
+NSDateFormatter *rfc3339DateFormatter = nil;
+NSString *model;
+NSString *iOSVersion;
+NSString *carrier;
+NSNumber *scale;
+
+- (id) init {
+ self = [super init];
+ if (self) {
+
+ // Put Settings bundle into memory
+ NSString *settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
+ if(!settingsBundle) {
+ NSLog(@"Could not find Settings.bundle");
+ } else {
+ NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]];
+ NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"];
+ NSMutableDictionary *defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]];
+ for(NSDictionary *prefSpecification in preferences) {
+ NSString *key = [prefSpecification objectForKey:@"Key"];
+ if(key && [[prefSpecification allKeys] containsObject:@"DefaultValue"]) {
+ [defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key];
+ }
+ }
+
+ [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister];
+ }
+ NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier];
+ NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString];
+ _serialqPush = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.SERIALQPUSH", bundleID, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
+ _serialqFlush = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.SERIALQFLUSH", bundleID, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
+
+ // Configure Events Infrastructure
+ _queue = [[NSMutableArray alloc] init];
+ _flushAt = 20;
+ _flushAfter = 10000;
+ _api = @"https://api.tiles.mapbox.com";
+ _token = nil;
+ _instance = [[NSUUID UUID] UUIDString];
+ Class ASIdentifierManagerClass = NSClassFromString(@"ASIdentifierManager");
+ if (ASIdentifierManagerClass) {
+ SEL sharedManagerSelector = NSSelectorFromString(@"sharedManager");
+ id sharedManager = ((id (*)(id, SEL))[ASIdentifierManagerClass methodForSelector:sharedManagerSelector])(ASIdentifierManagerClass, sharedManagerSelector);
+ // Add check here
+ SEL isAdvertisingTrackingEnabledSelector = NSSelectorFromString(@"isAdvertisingTrackingEnabled");
+ BOOL trackingEnabled = ((BOOL (*)(id, SEL))[sharedManager methodForSelector:isAdvertisingTrackingEnabledSelector])(sharedManager, isAdvertisingTrackingEnabledSelector);
+ if (trackingEnabled) {
+ SEL advertisingIdentifierSelector = NSSelectorFromString(@"advertisingIdentifier");
+ NSUUID *uuid = ((NSUUID* (*)(id, SEL))[sharedManager methodForSelector:advertisingIdentifierSelector])(sharedManager, advertisingIdentifierSelector);
+ _anonid = [uuid UUIDString];
+ } else {
+ _anonid = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
+ }
+ } else {
+ _anonid = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
+ }
+
+ model = [self getSysInfoByName:"hw.machine"];
+ iOSVersion = [NSString stringWithFormat:@"%@ %@", [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion];
+ if ([UIScreen instancesRespondToSelector:@selector(nativeScale)]) {
+ scale = [[NSNumber alloc] initWithFloat:[UIScreen mainScreen].nativeScale];
+ } else {
+ scale = [[NSNumber alloc] initWithFloat:[UIScreen mainScreen].scale];
+ }
+ CTCarrier *carrierVendor = [[[CTTelephonyNetworkInfo alloc] init] subscriberCellularProvider];
+ carrier = [carrierVendor carrierName];
+
+ _userAgent = @"MapboxEventsiOS/1.0";
+
+ // 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':'ss'Z'"];
+ [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
+ }
+ return self;
+}
+
++ (id)sharedManager {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedManager = [[self alloc] init];
+ });
+ return sharedManager;
+}
+
+- (void) pushEvent:(NSString *)event withAttributes:(NSDictionary *)attributeDictionary {
+
+ // Opt Out Checking When Built
+ if (![[NSUserDefaults standardUserDefaults] boolForKey:@"mapbox_metrics_enabled_preference"]) {
+ [_queue removeAllObjects];
+ return;
+ }
+
+ // Add Metrics Disabled App Wide Check
+ if ([[NSUserDefaults standardUserDefaults] objectForKey:@"mapbox_metrics_disabled"] != nil) {
+ [_queue removeAllObjects];
+ return;
+ }
+
+ if (!event) {
+ return;
+ }
+
+ dispatch_async(_serialqPush, ^{
+
+ NSMutableDictionary *evt = [[NSMutableDictionary alloc] init];
+ // mapbox-events stock attributes
+ [evt setObject:event forKey:@"event"];
+ [evt setObject:[NSNumber numberWithInt:1] forKey:@"version"];
+ [evt setObject:[self formatDate:[NSDate date]] forKey:@"created"];
+ [evt setObject:self.instance forKey:@"instance"];
+ [evt setObject:self.anonid forKey:@"anonid"];
+
+ // mapbox-events-ios stock attributes
+ [evt setValue:[rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"];
+ [evt setValue:model forKey:@"model"];
+ [evt setValue:iOSVersion forKey:@"operatingSystem"];
+ [evt setValue:[self getDeviceOrientation] forKey:@"orientation"];
+ [evt setValue:[[NSNumber alloc] initWithFloat:(100 * [UIDevice currentDevice].batteryLevel)] forKey:@"batteryLevel"];
+ [evt setValue:scale forKey:@"resolution"];
+ [evt setValue:carrier forKey:@"carrier"];
+ [evt setValue:[self getCurrentCellularNetworkConnectionType] forKey:@"cellularNetworkType"];
+ [evt setValue:[self getWifiNetworkName] forKey:@"wifi"];
+ [evt setValue:[NSNumber numberWithInt:[self getContentSizeScale]] forKey:@"accessibilityFontScale"];
+
+ for (NSString *key in [attributeDictionary allKeys]) {
+ [evt setObject:[attributeDictionary valueForKey:key] forKey:key];
+ }
+
+ // Make Immutable Version
+ NSDictionary *finalEvent = [NSDictionary dictionaryWithDictionary:evt];
+
+ // Put On The Queue
+ [self.queue addObject:finalEvent];
+
+ // Has Flush Limit Been Reached?
+ if ((int)_queue.count >= (int)_flushAt) {
+ [self flush];
+ }
+
+ // Reset Timer (Initial Starting of Timer after first event is pushed)
+ [self startTimer];
+
+ });
+}
+
+- (void) flush {
+ if (_token == nil) {
+ return;
+ }
+
+ dispatch_async(_serialqFlush, ^{
+
+ int upper = (int)_flushAt;
+ if (_flushAt > [_queue count]) {
+ if ([_queue count] == 0) {
+ return;
+ }
+ upper = (int)[_queue count];
+ }
+
+ // Create Array of Events to push to the Server
+ NSRange theRange = NSMakeRange(0, upper);
+ NSArray *events = [_queue subarrayWithRange:theRange];
+
+ // Update Queue to remove events sent to server
+ [_queue removeObjectsInRange:theRange];
+
+ // Send Array of Events to Server
+ [self postEvents:events];
+ });
+}
+
+- (void) postEvents:(NSArray *)events {
+ // Setup URL Request
+ NSString *url = [NSString stringWithFormat:@"%@/events/v1?access_token=%@", _api, _token];
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
+ [request setValue:[self getUserAgent] forHTTPHeaderField:@"User-Agent"];
+ [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
+ [request setHTTPMethod:@"POST"];
+
+ // Convert Array of Dictionaries to JSON
+ if ([NSJSONSerialization isValidJSONObject:events]) {
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:events options:NSJSONWritingPrettyPrinted error:nil];
+ [request setHTTPBody:jsonData];
+
+ // Send non blocking HTTP Request to server
+ [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:nil];
+ }
+}
+
+- (void) startTimer {
+ // Stop Timer if it already exists
+ if (_timer) {
+ [_timer invalidate];
+ _timer = nil;
+ }
+
+ // Start New Timer
+ NSTimeInterval interval = (double)((NSInteger)_flushAfter);
+ _timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(flush) userInfo:nil repeats:YES];
+}
+
+- (NSString *) getUserAgent {
+
+ if (_appName != nil && _appVersion != nil && ([_userAgent rangeOfString:_appName].location == NSNotFound)) {
+ _userAgent = [NSString stringWithFormat:@"%@/%@ %@", _appName, _appVersion, _userAgent];
+ }
+ return _userAgent;
+}
+
+- (NSString *) formatDate:(NSDate *)date {
+ return [rfc3339DateFormatter stringFromDate:date];
+}
+
+- (NSString *) getDeviceOrientation {
+ switch ([UIDevice currentDevice].orientation) {
+ case UIDeviceOrientationUnknown:
+ return @"Unknown";
+ break;
+ case UIDeviceOrientationPortrait:
+ return @"Portrait";
+ break;
+ case UIDeviceOrientationPortraitUpsideDown:
+ return @"PortraitUpsideDown";
+ break;
+ case UIDeviceOrientationLandscapeLeft:
+ return @"LandscapeLeft";
+ break;
+ case UIDeviceOrientationLandscapeRight:
+ return @"LandscapeRight";
+ break;
+ case UIDeviceOrientationFaceUp:
+ return @"FaceUp";
+ break;
+ case UIDeviceOrientationFaceDown:
+ return @"FaceDown";
+ break;
+ default:
+ return @"Default - Unknown";
+ break;
+ }
+}
+
+- (int) getContentSizeScale {
+ NSString *sc = [UIApplication sharedApplication].preferredContentSizeCategory;
+
+ if ([sc isEqualToString:UIContentSizeCategoryExtraSmall]) {
+ return -3;
+ } else if ([sc isEqualToString:UIContentSizeCategorySmall]) {
+ return -2;
+ } else if ([sc isEqualToString:UIContentSizeCategoryMedium]) {
+ return -1;
+ } else if ([sc isEqualToString:UIContentSizeCategoryLarge]) {
+ return 0;
+ } else if ([sc isEqualToString:UIContentSizeCategoryExtraLarge]) {
+ return 1;
+ } else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraLarge]) {
+ return 2;
+ } else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) {
+ return 3;
+ } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityMedium]) {
+ return -11;
+ } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityLarge]) {
+ return 10;
+ } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) {
+ return 11;
+ } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) {
+ return 12;
+ } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) {
+ return 13;
+ }
+ return -9999;
+}
+
+
+- (NSString *)getSysInfoByName:(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;
+}
+
+- (NSString *) getWifiNetworkName {
+
+ NSString *ssid = @"";
+ CFArrayRef interfaces = CNCopySupportedInterfaces();
+ if (interfaces) {
+ NSDictionary *info = (__bridge NSDictionary *)CNCopyCurrentNetworkInfo(CFArrayGetValueAtIndex(interfaces, 0));
+ if (info) {
+ ssid = info[@"SSID"];
+ } else {
+ ssid = @"NONE";
+ }
+ } else {
+ ssid = @"NONE";
+ }
+
+ return ssid;
+}
+
+- (NSString *) getCurrentCellularNetworkConnectionType {
+ CTTelephonyNetworkInfo *telephonyInfo = [CTTelephonyNetworkInfo new];
+ NSString *radioTech = telephonyInfo.currentRadioAccessTechnology;
+
+ if (radioTech == nil) {
+ return @"NONE";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyGPRS]) {
+ return @"GPRS";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyEdge]) {
+ return @"EDGE";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyWCDMA]) {
+ return @"WCDMA";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyHSDPA]) {
+ return @"HSDPA";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyHSUPA]) {
+ return @"HSUPA";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyCDMA1x]) {
+ return @"CDMA1x";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyCDMAEVDORev0]) {
+ return @"CDMAEVDORev0";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyCDMAEVDORevA]) {
+ return @"CDMAEVDORevA";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyCDMAEVDORevB]) {
+ return @"CDMAEVDORevB";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyeHRPD]) {
+ return @"HRPD";
+ } else if ([radioTech isEqualToString:CTRadioAccessTechnologyLTE]) {
+ return @"LTE";
+ } else {
+ return @"Unknown";
+ }
+}
+
+
+
+@end \ No newline at end of file
diff --git a/platform/ios/MGLMetricsLocationManager.m b/platform/ios/MGLMetricsLocationManager.m
new file mode 100644
index 0000000000..19210fddca
--- /dev/null
+++ b/platform/ios/MGLMetricsLocationManager.m
@@ -0,0 +1,107 @@
+//
+// MBLocationManager.m
+// Hermes
+//
+// Dynamic Settings.bundle loading based on:
+// http://stackoverflow.com/questions/510216/can-you-make-the-settings-in-settings-bundle-default-even-if-you-dont-open-the
+//
+// Created by Brad Leege on 3/8/15.
+// Copyright (c) 2015 Mapbox. All rights reserved.
+//
+
+#import "MGLMetricsLocationManager.h"
+#import "CoreLocation/CoreLocation.h"
+#import "MGLMapboxEvents.h"
+
+@interface MGLMetricsLocationManager()
+
+@property (atomic) CLLocationManager *locationManager;
+
+@end
+
+@implementation MGLMetricsLocationManager
+
+static MGLMetricsLocationManager *sharedManager = nil;
+
+- (id) init {
+ if (self = [super init]) {
+ _locationManager = [[CLLocationManager alloc] init];
+ _locationManager.distanceFilter = 2;
+ _locationManager.desiredAccuracy = kCLLocationAccuracyBest;
+ _locationManager.pausesLocationUpdatesAutomatically = YES;
+ [_locationManager setDelegate:self];
+ }
+ return self;
+}
+
++ (id)sharedManager {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedManager = [[self alloc] init];
+ });
+ return sharedManager;
+}
+
+- (BOOL) isAuthorizedStatusDetermined {
+ return ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusNotDetermined);
+}
+
+- (void) requestAlwaysAuthorization {
+ if ([self.locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) {
+ [self.locationManager requestAlwaysAuthorization];
+ } else {
+ // This is iOS 7 or below so Starting Location Updates will trigger authorization request
+ [self startUpdatingLocation];
+ }
+}
+
+- (void) startUpdatingLocation {
+ [self.locationManager startUpdatingLocation];
+}
+
+- (void) stopUpdatingLocation {
+ [self.locationManager stopUpdatingLocation];
+}
+
+#pragma mark CLLocationManagerDelegate
+- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
+ // Iterate through locations to pass all data
+ for (CLLocation *loc in locations) {
+ NSMutableDictionary *evt = [[NSMutableDictionary alloc] init];
+ [evt setValue:[[NSNumber alloc] initWithDouble:loc.coordinate.latitude] forKey:@"lat"];
+ [evt setValue:[[NSNumber alloc] initWithDouble:loc.coordinate.longitude] forKey:@"lng"];
+ [[MGLMapboxEvents sharedManager] pushEvent:@"location" withAttributes:evt];
+ }
+}
+
+- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
+ NSString *newStatus = nil;
+ switch (status) {
+ case kCLAuthorizationStatusNotDetermined:
+ newStatus = @"User Hasn't Determined Yet";
+ break;
+ case kCLAuthorizationStatusRestricted:
+ newStatus = @"Restricted and Can't Be Changed By User";
+ break;
+ case kCLAuthorizationStatusDenied:
+ newStatus = @"User Explcitly Denied";
+ [[MGLMetricsLocationManager sharedManager] stopUpdatingLocation];
+ break;
+ case kCLAuthorizationStatusAuthorized:
+ newStatus = @"User Has Authorized / Authorized Always";
+ [[MGLMetricsLocationManager sharedManager] startUpdatingLocation];
+ break;
+ // case kCLAuthorizationStatusAuthorizedAlways:
+ // newStatus = @"Not Determined";
+ // break;
+ case kCLAuthorizationStatusAuthorizedWhenInUse:
+ newStatus = @"User Has Authorized When In Use Only";
+ [[MGLMetricsLocationManager sharedManager] startUpdatingLocation];
+ break;
+ default:
+ newStatus = @"Unknown";
+ break;
+ }
+}
+
+@end
diff --git a/platform/ios/MGLUserLocation.m b/platform/ios/MGLUserLocation.m
new file mode 100644
index 0000000000..054dfa686d
--- /dev/null
+++ b/platform/ios/MGLUserLocation.m
@@ -0,0 +1,57 @@
+#import "MGLUserLocation_Private.h"
+
+@implementation MGLUserLocation
+{
+ CLLocationCoordinate2D _coordinate;
+}
+
+@synthesize coordinate = _coordinate;
+
+- (instancetype)init
+{
+ if (self = [super init])
+ {
+ _coordinate = CLLocationCoordinate2DMake(MAXFLOAT, MAXFLOAT);
+ }
+
+ return self;
+}
+
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
+{
+ return ! [key isEqualToString:@"location"] && ! [key isEqualToString:@"heading"];
+}
+
++ (NSSet *)keyPathsForValuesAffectingCoordinate
+{
+ return [NSSet setWithObject:@"location"];
+}
+
+- (void)setLocation:(CLLocation *)newLocation
+{
+ if ([newLocation distanceFromLocation:_location] && newLocation.coordinate.latitude != 0 &&
+ newLocation.coordinate.longitude != 0)
+ {
+ [self willChangeValueForKey:@"location"];
+ _location = newLocation;
+ _coordinate = _location.coordinate;
+ [self didChangeValueForKey:@"location"];
+ }
+}
+
+- (void)setHeading:(CLHeading *)newHeading
+{
+ if (newHeading.trueHeading != _heading.trueHeading)
+ {
+ [self willChangeValueForKey:@"heading"];
+ _heading = newHeading;
+ [self didChangeValueForKey:@"heading"];
+ }
+}
+
+- (NSString *)title
+{
+ if ( ! _title) return @"Current Location";
+}
+
+@end
diff --git a/platform/ios/MGLUserLocationAnnotationView.h b/platform/ios/MGLUserLocationAnnotationView.h
new file mode 100644
index 0000000000..c72d64ed8f
--- /dev/null
+++ b/platform/ios/MGLUserLocationAnnotationView.h
@@ -0,0 +1,18 @@
+#import <UIKit/UIKit.h>
+#import <CoreLocation/CoreLocation.h>
+
+#import "MGLUserLocation.h"
+
+@class MGLMapView;
+
+/** The MGLUserLocationAnnotationView class defines a specific type of annotation that identifies the user’s current location. You do not create instances of this class directly. Instead, you retrieve an existing MGLUserLocationAnnotationView object from the userLocation property of the map view displayed in your application. */
+@interface MGLUserLocationAnnotationView : UIView
+
+@property (nonatomic, weak) MGLMapView *mapView;
+@property (nonatomic) MGLUserLocation *annotation;
+@property (nonatomic, readonly) CALayer *haloLayer;
+
+- (instancetype)initInMapView:(MGLMapView *)mapView;
+- (void)setupLayers;
+
+@end
diff --git a/platform/ios/MGLUserLocationAnnotationView.m b/platform/ios/MGLUserLocationAnnotationView.m
new file mode 100644
index 0000000000..18c98bf1d0
--- /dev/null
+++ b/platform/ios/MGLUserLocationAnnotationView.m
@@ -0,0 +1,197 @@
+#import "MGLUserLocationAnnotationView.h"
+
+#import "MGLUserLocation_Private.h"
+#import "MGLAnnotation.h"
+#import "MGLMapView.h"
+
+const CGFloat MGLTrackingDotRingWidth = 24.0;
+
+@interface MGLUserLocationAnnotationView ()
+
+@property (nonatomic, readwrite) CALayer *haloLayer;
+
+@end
+
+#pragma mark -
+
+@implementation MGLUserLocationAnnotationView
+{
+ CALayer *_accuracyRingLayer;
+ CALayer *_dotBorderLayer;
+ CALayer *_dotLayer;
+}
+
+- (instancetype)initInMapView:(MGLMapView *)mapView
+{
+ if (self = [super init])
+ {
+ self.annotation = [[MGLUserLocation alloc] init];
+ _mapView = mapView;
+ [self setupLayers];
+ }
+ return self;
+}
+
+- (void)setTintColor:(UIColor *)tintColor
+{
+ UIImage *trackingDotHaloImage = [self trackingDotHaloImage];
+ _haloLayer.bounds = CGRectMake(0, 0, trackingDotHaloImage.size.width, trackingDotHaloImage.size.height);
+ _haloLayer.contents = (__bridge id)[trackingDotHaloImage CGImage];
+
+ UIImage *dotImage = [self dotImage];
+ _dotLayer.bounds = CGRectMake(0, 0, dotImage.size.width, dotImage.size.height);
+ _dotLayer.contents = (__bridge id)[dotImage CGImage];
+}
+
+- (void)setupLayers
+{
+ if (CLLocationCoordinate2DIsValid(self.annotation.coordinate))
+ {
+ if ( ! _accuracyRingLayer && self.annotation.location.horizontalAccuracy)
+ {
+ UIImage *accuracyRingImage = [self accuracyRingImage];
+ _accuracyRingLayer = [CALayer layer];
+ _haloLayer.bounds = CGRectMake(0, 0, accuracyRingImage.size.width, accuracyRingImage.size.height);
+ _haloLayer.contents = (__bridge id)[accuracyRingImage CGImage];
+ _haloLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
+
+ [self.layer addSublayer:_accuracyRingLayer];
+ }
+
+ if ( ! _haloLayer)
+ {
+ UIImage *haloImage = [self trackingDotHaloImage];
+ _haloLayer = [CALayer layer];
+ _haloLayer.bounds = CGRectMake(0, 0, haloImage.size.width, haloImage.size.height);
+ _haloLayer.contents = (__bridge id)[haloImage CGImage];
+ _haloLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
+
+ [CATransaction begin];
+
+ [CATransaction setAnimationDuration:3.5];
+ [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
+
+ // scale out radially
+ //
+ CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
+ boundsAnimation.repeatCount = MAXFLOAT;
+ boundsAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
+ boundsAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2.0, 2.0, 1.0)];
+ boundsAnimation.removedOnCompletion = NO;
+
+ [_haloLayer addAnimation:boundsAnimation forKey:@"animateScale"];
+
+ // go transparent as scaled out
+ //
+ CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ opacityAnimation.repeatCount = MAXFLOAT;
+ opacityAnimation.fromValue = [NSNumber numberWithFloat:1.0];
+ opacityAnimation.toValue = [NSNumber numberWithFloat:-1.0];
+ opacityAnimation.removedOnCompletion = NO;
+
+ [_haloLayer addAnimation:opacityAnimation forKey:@"animateOpacity"];
+
+ [CATransaction commit];
+
+ [self.layer addSublayer:_haloLayer];
+ }
+
+ // white dot background with shadow
+ //
+ if ( ! _dotBorderLayer)
+ {
+ CGRect rect = CGRectMake(0, 0, MGLTrackingDotRingWidth * 1.25, MGLTrackingDotRingWidth * 1.25);
+
+ UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]);
+ CGContextRef context = UIGraphicsGetCurrentContext();
+
+ CGContextSetShadow(context, CGSizeMake(0, 0), MGLTrackingDotRingWidth / 4.0);
+
+ CGContextSetFillColorWithColor(context, [[UIColor whiteColor] CGColor]);
+ CGContextFillEllipseInRect(context, CGRectMake((rect.size.width - MGLTrackingDotRingWidth) / 2.0, (rect.size.height - MGLTrackingDotRingWidth) / 2.0, MGLTrackingDotRingWidth, MGLTrackingDotRingWidth));
+
+ UIImage *whiteBackground = UIGraphicsGetImageFromCurrentImageContext();
+
+ UIGraphicsEndImageContext();
+
+ _dotBorderLayer = [CALayer layer];
+ _dotBorderLayer.bounds = CGRectMake(0, 0, whiteBackground.size.width, whiteBackground.size.height);
+ _dotBorderLayer.contents = (__bridge id)[whiteBackground CGImage];
+ _dotBorderLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
+ [self.layer addSublayer:_dotBorderLayer];
+ }
+
+ // pulsing, tinted dot sublayer
+ //
+ if ( ! _dotLayer)
+ {
+ UIImage *dotImage = [self dotImage];
+ _dotLayer = [CALayer layer];
+ _dotLayer.bounds = CGRectMake(0, 0, dotImage.size.width, dotImage.size.height);
+ _dotLayer.contents = (__bridge id)[dotImage CGImage];
+ _dotLayer.position = CGPointMake(super.layer.bounds.size.width / 2.0, super.layer.bounds.size.height / 2.0);
+
+ CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
+ animation.repeatCount = MAXFLOAT;
+ animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 1.0)];
+ animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.8, 0.8, 1.0)];
+ animation.removedOnCompletion = NO;
+ animation.autoreverses = YES;
+ animation.duration = 1.5;
+ animation.beginTime = CACurrentMediaTime() + 1.0;
+ animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
+
+ [_dotLayer addAnimation:animation forKey:@"animateTransform"];
+
+ [self.layer addSublayer:_dotLayer];
+ }
+ }
+}
+
+- (UIImage *)accuracyRingImage
+{
+ CGFloat latRadians = self.annotation.coordinate.latitude * M_PI / 180.0f;
+ CGFloat pixelRadius = self.annotation.location.horizontalAccuracy / cos(latRadians) / [self.mapView metersPerPixelAtLatitude:self.annotation.coordinate.latitude];
+ UIGraphicsBeginImageContextWithOptions(CGSizeMake(pixelRadius * 2, pixelRadius * 2), NO, [[UIScreen mainScreen] scale]);
+
+ CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), [[UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.7] CGColor]);
+ CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [[UIColor colorWithRed:0.378 green:0.552 blue:0.827 alpha:0.15] CGColor]);
+ CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 2.0);
+ CGContextStrokeEllipseInRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, pixelRadius * 2, pixelRadius * 2));
+
+ UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ return finalImage;
+}
+
+- (UIImage *)trackingDotHaloImage
+{
+ UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), NO, [[UIScreen mainScreen] scale]);
+ CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [[_mapView.tintColor colorWithAlphaComponent:0.75] CGColor]);
+ CGContextFillEllipseInRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, 100, 100));
+ UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ return finalImage;
+}
+
+- (UIImage *)dotImage
+{
+ CGFloat tintedWidth = MGLTrackingDotRingWidth * 0.7;
+
+ CGRect rect = CGRectMake(0, 0, tintedWidth, tintedWidth);
+
+ UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]);
+ CGContextRef context = UIGraphicsGetCurrentContext();
+
+ CGContextSetFillColorWithColor(context, [_mapView.tintColor CGColor]);
+ CGContextFillEllipseInRect(context, CGRectMake((rect.size.width - tintedWidth) / 2.0, (rect.size.height - tintedWidth) / 2.0, tintedWidth, tintedWidth));
+
+ UIImage *tintedForeground = UIGraphicsGetImageFromCurrentImageContext();
+
+ UIGraphicsEndImageContext();
+
+ return tintedForeground;
+}
+
+@end
diff --git a/platform/ios/MGLUserLocation_Private.h b/platform/ios/MGLUserLocation_Private.h
new file mode 100644
index 0000000000..d4f358bcbc
--- /dev/null
+++ b/platform/ios/MGLUserLocation_Private.h
@@ -0,0 +1,9 @@
+#import "MGLUserLocation.h"
+
+@interface MGLUserLocation (Private)
+
+@property (nonatomic, readwrite) CLLocationCoordinate2D coordinate;
+@property (nonatomic, readwrite) CLLocation *location;
+@property (nonatomic, readwrite) CLHeading *heading;
+
+@end
diff --git a/platform/ios/resources/mapbox.png b/platform/ios/resources/mapbox.png
index 01b5596b67..5a9da3fe39 100644
--- a/platform/ios/resources/mapbox.png
+++ b/platform/ios/resources/mapbox.png
Binary files differ
diff --git a/platform/ios/resources/mapbox@2x.png b/platform/ios/resources/mapbox@2x.png
index ff4ef2558b..194aa64da2 100644
--- a/platform/ios/resources/mapbox@2x.png
+++ b/platform/ios/resources/mapbox@2x.png
Binary files differ
diff --git a/platform/ios/resources/mapbox@3x.png b/platform/ios/resources/mapbox@3x.png
index 5a2afcb17b..d1260a16f3 100644
--- a/platform/ios/resources/mapbox@3x.png
+++ b/platform/ios/resources/mapbox@3x.png
Binary files differ
diff --git a/platform/ios/vendor/SMCalloutView b/platform/ios/vendor/SMCalloutView
new file mode 160000
+Subproject da691eceee57cdecce0235d2946552e105d8b7c