summaryrefslogtreecommitdiff
path: root/platform/ios/MGLMapView.mm
diff options
context:
space:
mode:
Diffstat (limited to 'platform/ios/MGLMapView.mm')
-rw-r--r--platform/ios/MGLMapView.mm495
1 files changed, 469 insertions, 26 deletions
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;