diff --git a/LICENSE.md b/LICENSE.md index 845f3116950..e5ed29c9ae4 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 de8745bf259..cb50e8b44a5 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 0fbd498fc38..b41e2f5d722 100644 --- a/include/mbgl/ios/MGLMapView.h +++ b/include/mbgl/ios/MGLMapView.h @@ -1,6 +1,10 @@ #import #import +#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 )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 7a174457709..0d7ae5f6d1c 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 00000000000..fee33688893 --- /dev/null +++ b/include/mbgl/ios/MGLUserLocation.h @@ -0,0 +1,26 @@ +#import "MGLAnnotation.h" + +@interface MGLUserLocation : NSObject + +@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 237d493f310..48ea908bbaa 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 3533e3da35c..6c91fd30299 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 + namespace mbgl { class Settings_NSUserDefaults { @@ -16,6 +18,9 @@ class Settings_NSUserDefaults { 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 2c7bfd6b409..c9973bcb7de 100644 --- a/ios/app/MBXViewController.mm +++ b/ios/app/MBXViewController.mm @@ -21,10 +21,9 @@ static NSString *const kStyleVersion = @"v7"; -@interface MBXViewController () +@interface MBXViewController () @property (nonatomic) MGLMapView *mapView; -@property (nonatomic) CLLocationManager *locationManager; @end @@ -66,11 +65,10 @@ - (void)viewDidLoad 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 @@ - (void)saveState:(NSNotification *)notification 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 @@ - (void)restoreState:(NSNotification *)notification 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 @@ - (void)cycleStyles - (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 @@ - (void)dealloc } } -#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 )annotation diff --git a/platform/darwin/settings_nsuserdefaults.mm b/platform/darwin/settings_nsuserdefaults.mm index b9b0e134bfc..168cba172d7 100644 --- a/platform/darwin/settings_nsuserdefaults.mm +++ b/platform/darwin/settings_nsuserdefaults.mm @@ -6,11 +6,15 @@ 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 @@ 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 30edd87977d..caaa4574e2a 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 #import "MGLMapboxEvents.h" #import "MGLMetricsLocationManager.h" +#import + // 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 MGLStyleValueFunctionAllowed; NSTimeInterval const MGLAnimationDuration = 0.3; +const CGSize MGLAnnotationUpdateViewportOutset = {150, 150}; NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey"; #pragma mark - Private - -@interface MGLMapView () +@interface MGLMapView () @property (nonatomic) EAGLContext *context; @property (nonatomic) GLKView *glView; @@ -83,6 +87,8 @@ @interface MGLMapView () @property (nonatomic) std::vector annotationsNearbyLastTap; @property (nonatomic, weak) id 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 @@ - (BOOL)commonInit [[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 @@ - (void)dealloc } } +- (void)setDelegate:(id)delegate +{ + if (_delegate == delegate) return; + + _delegate = delegate; +} + #pragma mark - Layout - - (void)setFrame:(CGRect)frame @@ -603,13 +616,14 @@ - (void)appWillForeground:(NSNotification *)notification - (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 @@ - (void)tintColorDidChange - (void)handleCompassTapGesture:(id)sender { [self resetNorthAnimated:YES]; + + if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) self.userTrackingMode = MGLUserTrackingModeFollow; } #pragma clang diagnostic pop @@ -632,6 +648,8 @@ - (void)handlePanGesture:(UIPanGestureRecognizer *)pan if (pan.state == UIGestureRecognizerStateBegan) { self.centerPoint = CGPointMake(0, 0); + + self.userTrackingMode = MGLUserTrackingModeNone; } else if (pan.state == UIGestureRecognizerStateChanged) { @@ -641,6 +659,8 @@ - (void)handlePanGesture:(UIPanGestureRecognizer *)pan 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 @@ - (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinch mbglMap->startScaling(); self.scale = mbglMap->getScale(); + + self.userTrackingMode = MGLUserTrackingModeNone; } else if (pinch.state == UIGestureRecognizerStateChanged) { @@ -742,6 +764,8 @@ - (void)handleRotateGesture:(UIRotationGestureRecognizer *)rotate mbglMap->startRotating(); self.angle = [MGLMapView degreesToRadians:mbglMap->getBearing()] * -1; + + self.userTrackingMode = MGLUserTrackingModeNone; } else if (rotate.state == UIGestureRecognizerStateChanged) { @@ -774,7 +798,18 @@ - (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; @@ -889,7 +924,11 @@ - (void)handleDoubleTapGesture:(UITapGestureRecognizer *)doubleTap 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 @@ - (void)handleTwoFingerTapGesture:(UITapGestureRecognizer *)twoFingerTap 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 @@ - (void)handleQuickZoomGesture:(UILongPressGestureRecognizer *)quickZoom self.scale = mbglMap->getScale(); self.quickZoomStart = [quickZoom locationInView:quickZoom.view].y; + + self.userTrackingMode = MGLUserTrackingModeNone; } else if (quickZoom.state == UIGestureRecognizerStateChanged) { @@ -985,7 +1030,7 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecogni 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 @@ - (void)resetNorth - (void)resetNorthAnimated:(BOOL)animated { + self.userTrackingMode = MGLUserTrackingModeNone; + CGFloat duration = (animated ? MGLAnimationDuration : 0); mbglMap->setBearing(0, secondsAsDuration(duration)); @@ -1066,6 +1113,13 @@ - (void)toggleDebug #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 @@ - (CLLocationCoordinate2D)centerCoordinate - (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 @@ - (double)zoomLevel - (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated { + self.userTrackingMode = MGLUserTrackingModeNone; + CGFloat duration = (animated ? MGLAnimationDuration : 0); mbglMap->setZoom(zoomLevel, secondsAsDuration(duration)); @@ -1117,6 +1175,22 @@ - (void)setZoomLevel:(double)zoomLevel [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 @@ - (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated { if ( ! animated && ! self.rotationAllowed) return; + self.userTrackingMode = MGLUserTrackingModeNone; + CGFloat duration = (animated ? MGLAnimationDuration : 0); mbglMap->setBearing(direction * -1, secondsAsDuration(duration)); @@ -1720,7 +1796,7 @@ - (void)addAnnotations:(NSArray *)annotations 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 @@ - (void)removeAnnotations:(NSArray *)annotations { 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 @@ - (void)selectAnnotation:(id )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; @@ -1794,18 +1873,28 @@ - (void)selectAnnotation:(id )annotation animated:(BOOL)animated // 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 @@ - (SMCalloutView *)calloutViewForAnnotation:(id )annotation 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 @@ - (void)deselectAnnotation:(id )annotation animated:(BOOL)animate } } +#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 @@ - (void)notifyMapChange:(NSNumber *)change 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 @@ - (void)notifyMapChange:(NSNumber *)change } case mbgl::MapChangeRegionIsChanging: { + [self updateUserLocationAnnotationView]; + [self updateCompass]; + if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)]) { [self.delegate mapViewRegionIsChanging:self]; @@ -2000,6 +2423,7 @@ - (void)notifyMapChange:(NSNumber *)change case mbgl::MapChangeRegionDidChange: case mbgl::MapChangeRegionDidChangeAnimated: { + [self updateUserLocationAnnotationView]; [self updateCompass]; if (self.pan.state == UIGestureRecognizerStateChanged || @@ -2065,6 +2489,25 @@ - (void)notifyMapChange:(NSNumber *)change } } +- (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 00000000000..054dfa686d2 --- /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 00000000000..c72d64ed8fd --- /dev/null +++ b/platform/ios/MGLUserLocationAnnotationView.h @@ -0,0 +1,18 @@ +#import +#import + +#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 00000000000..18c98bf1d0c --- /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 00000000000..d4f358bcbcc --- /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