summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE.md31
-rw-r--r--gyp/platform-ios.gypi5
-rw-r--r--include/mbgl/ios/MGLMapView.h49
-rw-r--r--include/mbgl/ios/MGLTypes.h8
-rw-r--r--include/mbgl/ios/MGLUserLocation.h26
-rw-r--r--include/mbgl/ios/MapboxGL.h1
-rw-r--r--include/mbgl/platform/darwin/settings_nsuserdefaults.hpp5
-rw-r--r--ios/app/MBXViewController.mm77
-rw-r--r--platform/darwin/settings_nsuserdefaults.mm36
-rw-r--r--platform/ios/MGLMapView.mm495
-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
14 files changed, 913 insertions, 101 deletions
diff --git a/LICENSE.md b/LICENSE.md
index 845f311695..e5ed29c9ae 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -22,3 +22,34 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+===========================================================================
+
+Mapbox GL uses portions of the Mapbox iOS SDK, which was derived from the
+Route-Me open source project, including the Alpstein fork of it.
+
+The Route-Me license appears below.
+
+Copyright (c) 2008-2013, Route-Me Contributors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi
index de8745bf25..cb50e8b44a 100644
--- a/gyp/platform-ios.gypi
+++ b/gyp/platform-ios.gypi
@@ -22,6 +22,11 @@
'../include/mbgl/ios/MGLMapView.h',
'../platform/ios/MGLMapView.mm',
'../include/mbgl/ios/MGLAnnotation.h',
+ '../include/mbgl/ios/MGLUserLocation.h',
+ '../platform/ios/MGLUserLocation_Private.h',
+ '../platform/ios/MGLUserLocation.m',
+ '../platform/ios/MGLUserLocationAnnotationView.h',
+ '../platform/ios/MGLUserLocationAnnotationView.m',
'../include/mbgl/ios/MGLMetricsLocationManager.h',
'../platform/ios/MGLMetricsLocationManager.m',
'../include/mbgl/ios/MGLStyleFunctionValue.h',
diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h
index 0fbd498fc3..b41e2f5d72 100644
--- a/include/mbgl/ios/MGLMapView.h
+++ b/include/mbgl/ios/MGLMapView.h
@@ -1,6 +1,10 @@
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
+#import "MGLTypes.h"
+
+@class MGLUserLocation;
+
@protocol MGLMapViewDelegate;
@protocol MGLAnnotation;
@@ -234,6 +238,30 @@
* @param animated If `YES`, the callout view is animated offscreen. */
- (void)deselectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated;
+#pragma mark - Displaying the User's Location
+
+/** A Boolean value indicating whether the map may display the user location.
+
+ This property does not indicate whether the user’s position is actually visible on the map, only whether the map view is allowed to display it. To determine whether the user’s position is visible, use the userLocationVisible property. The default value of this property is `NO`.
+
+ Setting this property to `YES` causes the map view to use the Core Location framework to find the current location. As long as this property is `YES`, the map view continues to track the user’s location and update it periodically.
+
+ On iOS 8 and above, your app must specify a value for `NSLocationWhenInUseUsageDescription` in its `Info.plist` to satisfy the requirements of the underlying Core Location framework when enabling this property.
+ */
+@property (nonatomic, assign) BOOL showsUserLocation;
+
+/// Returns a Boolean value indicating whether the user currently sees the user location annotation.
+@property (nonatomic, assign, readonly, getter=isUserLocationVisible) BOOL userLocationVisible;
+
+/// Returns the annotation object indicating the user’s current location.
+@property (nonatomic, readonly) MGLUserLocation *userLocation;
+
+/** The mode used to track the user location. */
+@property (nonatomic, assign) MGLUserTrackingMode userTrackingMode;
+
+/** Whether the map view should display a heading calibration alert when necessary. The default value is `YES`. */
+@property (nonatomic, assign) BOOL displayHeadingCalibration;
+
#pragma mark - Debugging
/** @name Debugging */
@@ -333,6 +361,27 @@
// TODO
- (void)mapViewDidFinishRenderingMap:(MGLMapView *)mapView fullyRendered:(BOOL)fullyRendered;
+#pragma mark - Tracking the User Location
+
+/// Tells the delegate that the map view will begin tracking the user’s location.
+- (void)mapViewWillStartLocatingUser:(MGLMapView *)mapView;
+
+/// Tells the delegate that the map view has stopped tracking the user’s location.
+- (void)mapViewDidStopLocatingUser:(MGLMapView *)mapView;
+
+/// Tells the delegate that the map view has updated the user’s location to the given location.
+- (void)mapView:(MGLMapView *)mapView didUpdateUserLocation:(MGLUserLocation *)userLocation;
+
+/// Tells the delegate that the map view has failed to locate the user.
+- (void)mapView:(MGLMapView *)mapView didFailToLocateUserWithError:(NSError *)error;
+
+/**
+ Tells the delegate that the map view’s user tracking mode has changed.
+
+ This method is called after the map view asynchronously changes to reflect the new user tracking mode, for example by beginning to zoom or rotate.
+ */
+- (void)mapView:(MGLMapView *)mapView didChangeUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated;
+
#pragma mark - Managing Annotations
/** @name Managing Annotations */
diff --git a/include/mbgl/ios/MGLTypes.h b/include/mbgl/ios/MGLTypes.h
index 7a17445770..0d7ae5f6d1 100644
--- a/include/mbgl/ios/MGLTypes.h
+++ b/include/mbgl/ios/MGLTypes.h
@@ -15,3 +15,11 @@ extern NSString *const MGLStyleValueTypeFunctionMaximumZoom;
extern NSString *const MGLStyleValueTypeFunctionLinear;
extern NSString *const MGLStyleValueTypeFunctionExponential;
extern NSString *const MGLStyleValueTypeFunctionStops;
+
+/// The degree to which the map view tracks the user’s location.
+typedef NS_ENUM(NSUInteger, MGLUserTrackingMode)
+{
+ MGLUserTrackingModeNone = 0, ///< does not track the user’s location or heading
+ MGLUserTrackingModeFollow = 1, ///< tracks the user’s location
+ MGLUserTrackingModeFollowWithHeading = 2, ///< tracks the user’s location and heading
+};
diff --git a/include/mbgl/ios/MGLUserLocation.h b/include/mbgl/ios/MGLUserLocation.h
new file mode 100644
index 0000000000..fee3368889
--- /dev/null
+++ b/include/mbgl/ios/MGLUserLocation.h
@@ -0,0 +1,26 @@
+#import "MGLAnnotation.h"
+
+@interface MGLUserLocation : NSObject <MGLAnnotation>
+
+@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
+
+/** @name Determining the User’s Position */
+
+/** The current location of the device. (read-only)
+*
+* This property contains `nil` if the map view is not currently showing the user location or if the user’s location has not yet been determined. */
+@property (nonatomic, readonly) CLLocation *location;
+
+/** A Boolean value indicating whether the user’s location is currently being updated. (read-only) */
+@property (nonatomic, readonly, getter=isUpdating) BOOL updating; // FIXME
+
+/** The heading of the user location. (read-only)
+*
+* This property is `nil` if the user location tracking mode is not `RMUserTrackingModeFollowWithHeading`. */
+@property (nonatomic, readonly) CLHeading *heading;
+
+@property (nonatomic, copy) NSString *title;
+
+@property (nonatomic, copy) NSString *subtitle;
+
+@end
diff --git a/include/mbgl/ios/MapboxGL.h b/include/mbgl/ios/MapboxGL.h
index 237d493f31..48ea908bba 100644
--- a/include/mbgl/ios/MapboxGL.h
+++ b/include/mbgl/ios/MapboxGL.h
@@ -2,6 +2,7 @@
#import "MGLMapView.h"
#import "MGLStyleFunctionValue.h"
#import "MGLTypes.h"
+#import "MGLUserLocation.h"
#import "NSArray+MGLAdditions.h"
#import "NSDictionary+MGLAdditions.h"
#import "UIColor+MGLAdditions.h"
diff --git a/include/mbgl/platform/darwin/settings_nsuserdefaults.hpp b/include/mbgl/platform/darwin/settings_nsuserdefaults.hpp
index 3533e3da35..6c91fd3029 100644
--- a/include/mbgl/platform/darwin/settings_nsuserdefaults.hpp
+++ b/include/mbgl/platform/darwin/settings_nsuserdefaults.hpp
@@ -1,6 +1,8 @@
#ifndef MBGL_COMMON_SETTINGS_NSUSERDEFAULTS
#define MBGL_COMMON_SETTINGS_NSUSERDEFAULTS
+#import <mbgl/ios/MGLTypes.h>
+
namespace mbgl {
class Settings_NSUserDefaults {
@@ -16,6 +18,9 @@ public:
double zoom = 0;
double bearing = 0;
+ MGLUserTrackingMode userTrackingMode = MGLUserTrackingModeNone;
+ bool showsUserLocation = false;
+
bool debug = false;
};
diff --git a/ios/app/MBXViewController.mm b/ios/app/MBXViewController.mm
index 2c7bfd6b40..c9973bcb7d 100644
--- a/ios/app/MBXViewController.mm
+++ b/ios/app/MBXViewController.mm
@@ -21,10 +21,9 @@ static NSArray *const kStyleNames = @[
static NSString *const kStyleVersion = @"v7";
-@interface MBXViewController () <UIActionSheetDelegate, CLLocationManagerDelegate, MGLMapViewDelegate>
+@interface MBXViewController () <UIActionSheetDelegate, MGLMapViewDelegate>
@property (nonatomic) MGLMapView *mapView;
-@property (nonatomic) CLLocationManager *locationManager;
@end
@@ -66,11 +65,10 @@ mbgl::Settings_NSUserDefaults *settings = nullptr;
self.mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds accessToken:accessToken];
self.mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- [self.view addSubview:self.mapView];
-
self.mapView.viewControllerForLayoutGuides = self;
-
+ self.mapView.showsUserLocation = YES;
self.mapView.delegate = self;
+ [self.view addSubview:self.mapView];
self.view.tintColor = kTintColor;
self.navigationController.navigationBar.tintColor = kTintColor;
@@ -109,6 +107,8 @@ mbgl::Settings_NSUserDefaults *settings = nullptr;
settings->zoom = self.mapView.zoomLevel;
settings->bearing = self.mapView.direction;
settings->debug = self.mapView.isDebugActive;
+ settings->userTrackingMode = self.mapView.userTrackingMode;
+ settings->showsUserLocation = self.mapView.showsUserLocation;
settings->save();
}
}
@@ -119,6 +119,8 @@ mbgl::Settings_NSUserDefaults *settings = nullptr;
settings->load();
[self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(settings->latitude, settings->longitude) zoomLevel:settings->zoom animated:NO];
self.mapView.direction = settings->bearing;
+ self.mapView.userTrackingMode = settings->userTrackingMode;
+ self.mapView.showsUserLocation = settings->showsUserLocation;
[self.mapView setDebugActive:settings->debug];
}
}
@@ -254,40 +256,17 @@ mbgl::Settings_NSUserDefaults *settings = nullptr;
- (void)locateUser
{
- if ( ! self.locationManager)
+ if (self.mapView.userTrackingMode == MGLUserTrackingModeNone)
{
- self.locationManager = [CLLocationManager new];
- self.locationManager.delegate = self;
+ self.mapView.userTrackingMode = MGLUserTrackingModeFollow;
}
-
- if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied)
+ else if (self.mapView.userTrackingMode == MGLUserTrackingModeFollow)
{
- [[[UIAlertView alloc] initWithTitle:@"Authorization Denied"
- message:@"Please enable location services for this app in Privacy settings."
- delegate:nil
- cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
+ self.mapView.userTrackingMode = MGLUserTrackingModeFollowWithHeading;
}
else
{
-#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
- if ([CLLocationManager instancesRespondToSelector:@selector(requestWhenInUseAuthorization)])
- {
- if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse)
- {
- [self.locationManager startUpdatingLocation];
- }
- else
- {
- [_locationManager requestWhenInUseAuthorization];
- }
- }
- else
- {
- [self.locationManager startUpdatingLocation];
- }
-#else
- [self.locationManager startUpdatingLocation];
-#endif
+ self.mapView.userTrackingMode = MGLUserTrackingModeNone;
}
}
@@ -305,41 +284,9 @@ mbgl::Settings_NSUserDefaults *settings = nullptr;
}
}
-#pragma mark - CLLocationManagerDelegate
-
-- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
-{
- switch (status)
- {
- case kCLAuthorizationStatusAuthorizedAlways:
-#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
- case kCLAuthorizationStatusAuthorizedWhenInUse:
-#endif
- {
- [manager startUpdatingLocation];
- break;
- }
- default:
- {
- }
- }
-}
-
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"
-- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
-{
- CLLocation *latestLocation = locations.lastObject;
-
- if ([latestLocation distanceFromLocation:[[CLLocation alloc] initWithLatitude:self.mapView.centerCoordinate.latitude longitude:self.mapView.centerCoordinate.longitude]] > 100)
- {
- [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(latestLocation.coordinate.latitude, latestLocation.coordinate.longitude) zoomLevel:17 animated:YES];
- }
-
- [self.locationManager stopUpdatingLocation];
-}
-
#pragma mark - MGLMapViewDelegate
- (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id <MGLAnnotation>)annotation
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/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm
index 30edd87977..caaa4574e2 100644
--- a/platform/ios/MGLMapView.mm
+++ b/platform/ios/MGLMapView.mm
@@ -17,6 +17,8 @@
#import "MGLTypes.h"
#import "MGLStyleFunctionValue.h"
#import "MGLAnnotation.h"
+#import "MGLUserLocationAnnotationView.h"
+#import "MGLUserLocation_Private.h"
#import "SMCalloutView.h"
@@ -24,10 +26,11 @@
#import "NSArray+MGLAdditions.h"
#import "NSDictionary+MGLAdditions.h"
-#import <algorithm>
#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 {
@@ -61,12 +64,13 @@ extern NSString *const MGLStyleKeyBackground;
extern NSString *const MGLStyleValueFunctionAllowed;
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;
@@ -83,6 +87,8 @@ NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey";
@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, readonly) NSDictionary *allowedStyleTypes;
@property (nonatomic) CGPoint centerPoint;
@property (nonatomic) CGFloat scale;
@@ -372,7 +378,7 @@ 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 MBLocationManager for metrics
+ // setup dedicated location manager for metrics
[MGLMetricsLocationManager sharedManager];
// set initial position
@@ -450,6 +456,13 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
}
}
+- (void)setDelegate:(id<MGLMapViewDelegate>)delegate
+{
+ if (_delegate == delegate) return;
+
+ _delegate = delegate;
+}
+
#pragma mark - Layout -
- (void)setFrame:(CGRect)frame
@@ -603,13 +616,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 -
@@ -617,6 +631,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)handleCompassTapGesture:(id)sender
{
[self resetNorthAnimated:YES];
+
+ if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) self.userTrackingMode = MGLUserTrackingModeFollow;
}
#pragma clang diagnostic pop
@@ -632,6 +648,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
if (pan.state == UIGestureRecognizerStateBegan)
{
self.centerPoint = CGPointMake(0, 0);
+
+ self.userTrackingMode = MGLUserTrackingModeNone;
}
else if (pan.state == UIGestureRecognizerStateChanged)
{
@@ -641,6 +659,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)
{
@@ -708,6 +728,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
mbglMap->startScaling();
self.scale = mbglMap->getScale();
+
+ self.userTrackingMode = MGLUserTrackingModeNone;
}
else if (pinch.state == UIGestureRecognizerStateChanged)
{
@@ -742,6 +764,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
mbglMap->startRotating();
self.angle = [MGLMapView degreesToRadians:mbglMap->getBearing()] * -1;
+
+ self.userTrackingMode = MGLUserTrackingModeNone;
}
else if (rotate.state == UIGestureRecognizerStateChanged)
{
@@ -774,7 +798,18 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
[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;
@@ -889,7 +924,11 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
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));
@@ -918,7 +957,11 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
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));
@@ -950,6 +993,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
self.scale = mbglMap->getScale();
self.quickZoomStart = [quickZoom locationInView:quickZoom.view].y;
+
+ self.userTrackingMode = MGLUserTrackingModeNone;
}
else if (quickZoom.state == UIGestureRecognizerStateChanged)
{
@@ -985,7 +1030,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]);
}
-- (void) trackGestureEvent:(NSString *) gesture forRecognizer:(UIGestureRecognizer *) recognizer
+- (void)trackGestureEvent:(NSString *)gesture forRecognizer:(UIGestureRecognizer *)recognizer
{
// Send Map Zoom Event
CGPoint ptInView = CGPointMake([recognizer locationInView:recognizer.view].x, [recognizer locationInView:recognizer.view].y);
@@ -1028,6 +1073,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)resetNorthAnimated:(BOOL)animated
{
+ self.userTrackingMode = MGLUserTrackingModeNone;
+
CGFloat duration = (animated ? MGLAnimationDuration : 0);
mbglMap->setBearing(0, secondsAsDuration(duration));
@@ -1066,6 +1113,13 @@ 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);
@@ -1087,6 +1141,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated
{
+ self.userTrackingMode = MGLUserTrackingModeNone;
+
CGFloat duration = (animated ? MGLAnimationDuration : 0);
mbglMap->setLatLngZoom(coordinateToLatLng(centerCoordinate), zoomLevel, secondsAsDuration(duration));
@@ -1103,6 +1159,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated
{
+ self.userTrackingMode = MGLUserTrackingModeNone;
+
CGFloat duration = (animated ? MGLAnimationDuration : 0);
mbglMap->setZoom(zoomLevel, secondsAsDuration(duration));
@@ -1117,6 +1175,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;
@@ -1131,6 +1205,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr;
{
if ( ! animated && ! self.rotationAllowed) return;
+ self.userTrackingMode = MGLUserTrackingModeNone;
+
CGFloat duration = (animated ? MGLAnimationDuration : 0);
mbglMap->setBearing(direction * -1, secondsAsDuration(duration));
@@ -1720,7 +1796,7 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
for (size_t i = 0; i < annotationIDs.size(); ++i)
{
[self.annotationIDsByAnnotation setObject:@{ MGLAnnotationIDKey : @(annotationIDs[i]) }
- forKey:annotations[i]];
+ forKey:annotations[i]];
}
}
@@ -1746,7 +1822,8 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
{
assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]);
- annotationIDsToRemove.push_back([[[self.annotationIDsByAnnotation objectForKey:annotation] objectForKey:MGLAnnotationIDKey] unsignedIntValue]);
+ annotationIDsToRemove.push_back([[[self.annotationIDsByAnnotation objectForKey:annotation]
+ objectForKey:MGLAnnotationIDKey] unsignedIntValue]);
[self.annotationIDsByAnnotation removeObjectForKey:annotation];
if (annotation == self.selectedAnnotation)
@@ -1781,9 +1858,11 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
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;
@@ -1794,18 +1873,28 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
// build the callout
self.selectedAnnotationCalloutView = [self calloutViewForAnnotation:annotation];
- // determine symbol in use for point
- NSString *symbol = MGLDefaultStyleMarkerSymbolName;
- if ([self.delegate respondsToSelector:@selector(mapView:symbolNameForAnnotation:)])
+ CGRect calloutBounds;
+
+ if ([annotation isEqual:self.userLocation])
{
- symbol = [self.delegate mapView:self symbolNameForAnnotation:annotation];
+ CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self];
+ calloutBounds = CGRectMake(calloutAnchorPoint.x - 1, calloutAnchorPoint.y - 13, 0, 0);
}
- std::string symbolName([symbol UTF8String]);
+ 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);
- CGRect calloutBounds = CGRectMake(calloutAnchorPoint.x, calloutAnchorPoint.y + y, 0, 0);
+ // 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:)])
@@ -1857,6 +1946,8 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
if ([annotation respondsToSelector:@selector(title)]) calloutView.title = annotation.title;
if ([annotation respondsToSelector:@selector(subtitle)]) calloutView.subtitle = annotation.subtitle;
+ calloutView.tintColor = self.tintColor;
+
return calloutView;
}
@@ -1881,6 +1972,332 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
}
}
+#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;
+
+ 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
@@ -1958,6 +2375,9 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
case mbgl::MapChangeRegionWillChange:
case mbgl::MapChangeRegionWillChangeAnimated:
{
+ [self updateUserLocationAnnotationView];
+ [self updateCompass];
+
[self deselectAnnotation:self.selectedAnnotation animated:NO];
BOOL animated = ([change unsignedIntegerValue] == mbgl::MapChangeRegionWillChangeAnimated);
@@ -1992,6 +2412,9 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
}
case mbgl::MapChangeRegionIsChanging:
{
+ [self updateUserLocationAnnotationView];
+ [self updateCompass];
+
if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)])
{
[self.delegate mapViewRegionIsChanging:self];
@@ -2000,6 +2423,7 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
case mbgl::MapChangeRegionDidChange:
case mbgl::MapChangeRegionDidChangeAnimated:
{
+ [self updateUserLocationAnnotationView];
[self updateCompass];
if (self.pan.state == UIGestureRecognizerStateChanged ||
@@ -2065,6 +2489,25 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng)
}
}
+- (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;
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