diff --git a/CHANGELOG.md b/CHANGELOG.md index 3561c9f9e11..0d6f10b62fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ Known issues: - MGLMapCamera’s `altitude` values now match those of MKMapCamera. ([#3362](https://github.com/mapbox/mapbox-gl-native/pull/3362)) - MGLMapView properties like `centerCoordinate` and `camera` now offset the center to account for any translucent top or bottom bar. As a result, when user tracking is enabled and the map view is an immediate child of a view controller, the user dot is centered in the unobscured portion of the map view. To override this offset, modify the `contentInset` property; you may also need to set the containing view controller’s `automaticallyAdjustsScrollViewInsets` property to `NO`. ([#3583](https://github.com/mapbox/mapbox-gl-native/pull/3583)) - In user tracking mode, the user dot stays in a fixed position within MGLMapView while the map pans smoothly. A new property, `userLocationVerticalAlignment`, determines the user dot’s fixed position. ([#3589](https://github.com/mapbox/mapbox-gl-native/pull/3589)) +- When the user tracking mode is set to `MGLUserTrackingModeFollowWithCourse`, an optional `targetCoordinate` is kept within sight at all times as the user changes location. This property, in conjunction with the `userLocationVerticalAlignment` property, may be useful for displaying the user’s progress toward a waypoint. ([#3680](https://github.com/mapbox/mapbox-gl-native/pull/3680)) +- Heading or course tracking mode can now be enabled as soon as an MGLMapView is initialized. ([#3680](https://github.com/mapbox/mapbox-gl-native/pull/3680)) - Zooming and rotation gestures no longer disable user tracking mode. ([#3589](https://github.com/mapbox/mapbox-gl-native/pull/3589)) - User tracking mode starts out at a lower zoom level by default. ([#3589](https://github.com/mapbox/mapbox-gl-native/pull/3589)) - Fixed an issue with small map views not properly fitting annotations within bounds. (#[3407](https://github.com/mapbox/mapbox-gl-native/pull/3407)) diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index 2d28fd3a141..b27db4976a6 100644 --- a/include/mbgl/ios/MGLMapView.h +++ b/include/mbgl/ios/MGLMapView.h @@ -224,7 +224,12 @@ IB_DESIGNABLE @property (nonatomic, readonly, nullable) MGLUserLocation *userLocation; /** - The mode used to track the user location. + The mode used to track the user location. The default value is + `MGLUserTrackingModeNone`. + + Changing the value of this property updates the map view with an animated + transition. If you don’t want to animate the change, use the + `-setUserTrackingMode:animated:` method instead. */ @property (nonatomic, assign) MGLUserTrackingMode userTrackingMode; @@ -243,15 +248,70 @@ IB_DESIGNABLE /** The vertical alignment of the user location annotation within the receiver. The default value is `MGLAnnotationVerticalAlignmentCenter`. + + Changing the value of this property updates the map view with an animated + transition. If you don’t want to animate the change, use the + `-setUserLocationVerticalAlignment:animated:` method instead. */ @property (nonatomic, assign) MGLAnnotationVerticalAlignment userLocationVerticalAlignment; +/** + Sets the vertical alignment of the user location annotation within the + receiver, with an optional transition. + + @param alignment The vertical alignment of the user location annotation. + @param animated If `YES`, the user location annotation animates to its new + position within the map view. If `NO`, the user location annotation + instantaneously moves to its new position. + */ +- (void)setUserLocationVerticalAlignment:(MGLAnnotationVerticalAlignment)alignment animated:(BOOL)animated; + /** Whether the map view should display a heading calibration alert when necessary. The default value is `YES`. */ @property (nonatomic, assign) BOOL displayHeadingCalibration; +/** + The geographic coordinate that is the subject of observation as the user + location is being tracked. + + By default, this property is set to an invalid coordinate, indicating that + there is no target. In course tracking mode, the target forms one of two foci + in the viewport, the other being the user location annotation. Typically, this + property is set to a destination or waypoint in a real-time navigation scene. + As the user annotation moves toward the target, the map automatically zooms in + to fit both foci optimally within the viewport. + + This property has no effect if the `userTrackingMode` property is set to a + value other than `MGLUserTrackingModeFollowWithCourse`. + + Changing the value of this property updates the map view with an animated + transition. If you don’t want to animate the change, use the + `-setTargetCoordinate:animated:` method instead. + */ +@property (nonatomic, assign) CLLocationCoordinate2D targetCoordinate; + +/** + Sets the geographic coordinate that is the subject of observation as the user + location is being tracked, with an optional transition animation. + + By default, the target coordinate is set to an invalid coordinate, indicating + that there is no target. In course tracking mode, the target forms one of two + foci in the viewport, the other being the user location annotation. Typically, + the target is set to a destination or waypoint in a real-time navigation scene. + As the user annotation moves toward the target, the map automatically zooms in + to fit both foci optimally within the viewport. + + This method has no effect if the `userTrackingMode` property is set to a value + other than `MGLUserTrackingModeFollowWithCourse`. + + @param targetCoordinate The target coordinate to fit within the viewport. + @param animated If `YES`, the map animates to fit the target within the map + view. If `NO`, the map fits the target instantaneously. + */ +- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated; + #pragma mark Configuring How the User Interacts with the Map /** diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index 84af449fac5..1112ac6c934 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -130,6 +130,7 @@ class Map : private util::noncopyable { // Pitch void setPitch(double pitch, const Duration& = Duration::zero()); + void setPitch(double pitch, const PrecisionPoint&, const Duration& = Duration::zero()); double getPitch() const; // North Orientation diff --git a/platform/android/src/jni.cpp b/platform/android/src/jni.cpp index cc25ec4da05..cd013e4519c 100644 --- a/platform/android/src/jni.cpp +++ b/platform/android/src/jni.cpp @@ -680,11 +680,12 @@ jdouble JNICALL nativeGetPitch(JNIEnv *env, jobject obj, jlong nativeMapViewPtr) return nativeMapView->getMap().getPitch(); } -void JNICALL nativeSetPitch(JNIEnv *env, jobject obj, jlong nativeMapViewPtr, jdouble pitch, jlong duration) { +void JNICALL nativeSetPitch(JNIEnv *env, jobject obj, jlong nativeMapViewPtr, jdouble pitch, jlong milliseconds) { mbgl::Log::Debug(mbgl::Event::JNI, "nativeGetPitch"); assert(nativeMapViewPtr != 0); NativeMapView *nativeMapView = reinterpret_cast(nativeMapViewPtr); - nativeMapView->getMap().setPitch(pitch, mbgl::Milliseconds(duration)); + mbgl::Duration duration((mbgl::Milliseconds(milliseconds))); + nativeMapView->getMap().setPitch(pitch, duration); } void JNICALL nativeScaleBy(JNIEnv *env, jobject obj, jlong nativeMapViewPtr, jdouble ds, jdouble cx, diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 3fa5ba78b0d..e3016864194 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -67,6 +68,10 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { /// match a typical interval between user location updates. const NSTimeInterval MGLUserLocationAnimationDuration = 1.0; +/// Distance between the map view’s edge and that of the user location +/// annotation view. +const UIEdgeInsets MGLUserLocationAnnotationViewInset = UIEdgeInsetsMake(50, 0, 50, 0); + const CGSize MGLAnnotationUpdateViewportOutset = {150, 150}; const CGFloat MGLMinimumZoom = 3; @@ -443,6 +448,7 @@ - (void)commonInit _mbglMap->jumpTo(options); _pendingLatitude = NAN; _pendingLongitude = NAN; + _targetCoordinate = kCLLocationCoordinate2DInvalid; // metrics: map load event mbgl::LatLng latLng = _mbglMap->getLatLng(padding); @@ -851,8 +857,15 @@ - (void)setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated _contentInset = contentInset; - // Don’t call -setCenterCoordinate:, which resets the user tracking mode. - [self _setCenterCoordinate:oldCenter animated:animated]; + if (self.userTrackingMode == MGLUserTrackingModeNone) + { + // Don’t call -setCenterCoordinate:, which resets the user tracking mode. + [self _setCenterCoordinate:oldCenter animated:animated]; + } + else + { + [self didUpdateLocationWithUserTrackingAnimated:animated]; + } } /// Returns the frame of inset content within the map view. @@ -1415,8 +1428,13 @@ - (void)handleTwoFingerDragGesture:(UIPanGestureRecognizer *)twoFingerDrag CGFloat slowdown = 20.0; CGFloat pitchNew = currentPitch - (gestureDistance / slowdown); - - _mbglMap->setPitch(pitchNew); + + CGPoint centerPoint = self.contentCenter; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + centerPoint = self.userLocationAnnotationViewCenter; + } + _mbglMap->setPitch(pitchNew, centerPoint); [self notifyMapChange:mbgl::MapChangeRegionIsChanging]; } @@ -1756,7 +1774,14 @@ - (void)setZoomLevel:(double)zoomLevel - (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated { - [self setCenterCoordinate:self.centerCoordinate zoomLevel:zoomLevel animated:animated]; + if (zoomLevel == self.zoomLevel) return; + _mbglMap->cancelTransitions(); + + CGFloat duration = animated ? MGLAnimationDuration : 0; + + _mbglMap->setZoom(zoomLevel, + MGLEdgeInsetsFromNSEdgeInsets(self.contentInset), + MGLDurationInSeconds(duration)); } - (MGLCoordinateBounds)visibleCoordinateBounds @@ -1820,9 +1845,13 @@ - (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUIn - (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion { self.userTrackingMode = MGLUserTrackingModeNone; + [self _setVisibleCoordinates:coordinates count:count edgePadding:insets direction:direction duration:duration animationTimingFunction:function completionHandler:completion]; +} + +- (void)_setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion +{ _mbglMap->cancelTransitions(); - // NOTE: does not disrupt tracking mode [self willChangeValueForKey:@"visibleCoordinateBounds"]; mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(insets); padding += MGLEdgeInsetsFromNSEdgeInsets(self.contentInset); @@ -1871,8 +1900,7 @@ - (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated { if ( ! animated && ! self.rotationAllowed) return; - if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading || - self.userTrackingMode == MGLUserTrackingModeFollowWithCourse) + if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) { self.userTrackingMode = MGLUserTrackingModeFollow; } @@ -1886,10 +1914,19 @@ - (void)_setDirection:(CLLocationDirection)direction animated:(BOOL)animated _mbglMap->cancelTransitions(); CGFloat duration = animated ? MGLAnimationDuration : 0; - - _mbglMap->setBearing(direction, - MGLEdgeInsetsFromNSEdgeInsets(self.contentInset), - MGLDurationInSeconds(duration)); + + if (self.userTrackingMode == MGLUserTrackingModeNone) + { + _mbglMap->setBearing(direction, + MGLEdgeInsetsFromNSEdgeInsets(self.contentInset), + MGLDurationInSeconds(duration)); + } + else + { + CGPoint centerPoint = self.userLocationAnnotationViewCenter; + _mbglMap->setBearing(direction, { centerPoint.x, centerPoint.y }, + MGLDurationInSeconds(duration)); + } } - (void)setDirection:(CLLocationDirection)direction @@ -1943,7 +1980,7 @@ - (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration a return; } - mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera]; + mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera edgePadding:self.contentInset]; mbgl::AnimationOptions animationOptions; if (duration > 0) { @@ -1978,10 +2015,10 @@ - (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration { self.userTrackingMode = MGLUserTrackingModeNone; - [self _flyToCamera:camera withDuration:duration peakAltitude:peakAltitude completionHandler:completion]; + [self _flyToCamera:camera edgePadding:self.contentInset withDuration:duration peakAltitude:peakAltitude completionHandler:completion]; } -- (void)_flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion +- (void)_flyToCamera:(MGLMapCamera *)camera edgePadding:(UIEdgeInsets)insets withDuration:(NSTimeInterval)duration peakAltitude:(CLLocationDistance)peakAltitude completionHandler:(nullable void (^)(void))completion { _mbglMap->cancelTransitions(); if ([self.camera isEqual:camera]) @@ -1989,7 +2026,7 @@ - (void)_flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duratio return; } - mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera]; + mbgl::CameraOptions cameraOptions = [self cameraOptionsObjectForAnimatingToCamera:camera edgePadding:insets]; mbgl::AnimationOptions animationOptions; if (duration >= 0) { @@ -2018,11 +2055,14 @@ - (void)_flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duratio /// Returns a CameraOptions object that specifies parameters for animating to /// the given camera. -- (mbgl::CameraOptions)cameraOptionsObjectForAnimatingToCamera:(MGLMapCamera *)camera +- (mbgl::CameraOptions)cameraOptionsObjectForAnimatingToCamera:(MGLMapCamera *)camera edgePadding:(UIEdgeInsets)insets { mbgl::CameraOptions options; - options.center = MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate); - options.padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInset); + if (CLLocationCoordinate2DIsValid(camera.centerCoordinate)) + { + options.center = MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate); + } + options.padding = MGLEdgeInsetsFromNSEdgeInsets(insets); options.zoom = MGLZoomLevelForAltitude(camera.altitude, camera.pitch, camera.centerCoordinate.latitude, self.frame.size); @@ -2958,6 +2998,7 @@ - (void)setUserLocationAnnotationView:(MGLUserLocationAnnotationView *)newAnnota if ( ! [newAnnotationView isEqual:_userLocationAnnotationView]) { _userLocationAnnotationView = newAnnotationView; + [self updateUserLocationAnnotationView]; } } @@ -3005,11 +3046,7 @@ - (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated _userTrackingMode = mode; - if (_userTrackingMode == MGLUserTrackingModeNone - || _userTrackingMode == MGLUserTrackingModeFollowWithCourse) - { - self.userTrackingState = MGLUserTrackingStatePossible; - } + self.userTrackingState = animated ? MGLUserTrackingStatePossible : MGLUserTrackingStateChanged; switch (_userTrackingMode) { @@ -3044,7 +3081,6 @@ - (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated if (self.zoomLevel < self.currentMinimumZoom) { [self setZoomLevel:self.currentMinimumZoom animated:YES]; - _userTrackingMode = MGLUserTrackingModeFollowWithHeading; // reapply } if (self.userLocationAnnotationView) @@ -3067,11 +3103,38 @@ - (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated } - (void)setUserLocationVerticalAlignment:(MGLAnnotationVerticalAlignment)alignment +{ + [self setUserLocationVerticalAlignment:alignment animated:YES]; +} + +- (void)setUserLocationVerticalAlignment:(MGLAnnotationVerticalAlignment)alignment animated:(BOOL)animated { _userLocationVerticalAlignment = alignment; if (self.userTrackingMode != MGLUserTrackingModeNone) { - [self locationManager:self.locationManager didUpdateLocations:@[self.userLocation.location] animated:YES]; + [self locationManager:self.locationManager didUpdateLocations:@[self.userLocation.location] animated:animated]; + } +} + +- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate +{ + [self setTargetCoordinate:targetCoordinate animated:YES]; +} + +- (void)setTargetCoordinate:(CLLocationCoordinate2D)targetCoordinate animated:(BOOL)animated +{ + if (targetCoordinate.latitude != self.targetCoordinate.latitude + || targetCoordinate.longitude != self.targetCoordinate.longitude) + { + _targetCoordinate = targetCoordinate; + if (self.userTrackingMode == MGLUserTrackingModeFollowWithCourse) + { + self.userTrackingState = MGLUserTrackingStatePossible; + if (self.userLocation.location) + { + [self locationManager:self.locationManager didUpdateLocations:@[self.userLocation.location] animated:animated]; + } + } } } @@ -3087,9 +3150,10 @@ - (void)locationManager:(__unused CLLocationManager *)manager didUpdateLocations if ( ! _showsUserLocation || ! newLocation || ! CLLocationCoordinate2DIsValid(newLocation.coordinate)) return; - if (! oldLocation || ! CLLocationCoordinate2DIsValid(oldLocation.coordinate) || [newLocation distanceFromLocation:oldLocation]) + if (! oldLocation || ! CLLocationCoordinate2DIsValid(oldLocation.coordinate) || [newLocation distanceFromLocation:oldLocation] + || oldLocation.course != newLocation.course) { - if (self.userTrackingState != MGLUserTrackingStateBegan) + if ( ! oldLocation || ! CLLocationCoordinate2DIsValid(oldLocation.coordinate) || self.userTrackingState != MGLUserTrackingStateBegan) { self.userLocation.location = newLocation; } @@ -3100,84 +3164,192 @@ - (void)locationManager:(__unused CLLocationManager *)manager didUpdateLocations } } - CLLocationDirection course = self.userLocation.location.course; - if (course < 0 || self.userTrackingMode != MGLUserTrackingModeFollowWithCourse) + [self didUpdateLocationWithUserTrackingAnimated:animated]; + + self.userLocationAnnotationView.haloLayer.hidden = ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate) || + newLocation.horizontalAccuracy > 10; + + [self updateUserLocationAnnotationView]; +} + +- (void)didUpdateLocationWithUserTrackingAnimated:(BOOL)animated +{ + CLLocation *location = self.userLocation.location; + if ( ! _showsUserLocation || ! location + || ! CLLocationCoordinate2DIsValid(location.coordinate) + || self.userTrackingMode == MGLUserTrackingModeNone) { - course = -1; + return; } - - if (self.userTrackingMode != MGLUserTrackingModeNone) + + // If the user location annotation is already where it’s supposed to be, + // don’t change the viewport. + CGPoint correctPoint = self.userLocationAnnotationViewCenter; + CGPoint currentPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self]; + if (std::abs(currentPoint.x - correctPoint.x) <= 1.0 && std::abs(currentPoint.y - correctPoint.y) <= 1.0 + && self.userTrackingMode != MGLUserTrackingModeFollowWithCourse) { - // center on user location unless we're already centered there (or very close) - // - CGPoint correctPoint = self.userLocationAnnotationViewCenter; - CGPoint currentPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self]; + return; + } + + if (self.userTrackingMode == MGLUserTrackingModeFollowWithCourse + && CLLocationCoordinate2DIsValid(self.targetCoordinate)) + { + if (self.userTrackingState != MGLUserTrackingStateBegan) + { + // Keep both the user and the destination in view. + [self didUpdateLocationWithTargetAnimated:animated]; + } + } + else if (self.userTrackingState == MGLUserTrackingStatePossible) + { + // The first location update is often a great distance away from the + // current viewport, so fly there to provide additional context. + [self didUpdateLocationSignificantlyAnimated:animated]; + } + else if (self.userTrackingState == MGLUserTrackingStateChanged) + { + // Subsequent updates get a more subtle animation. + [self didUpdateLocationIncrementallyAnimated:animated]; + } + [self unrotateIfNeededAnimated:YES]; +} + +/// Changes the viewport based on an incremental location update. +- (void)didUpdateLocationIncrementallyAnimated:(BOOL)animated +{ + [self _setCenterCoordinate:self.userLocation.location.coordinate + edgePadding:self.edgePaddingForFollowing + zoomLevel:self.zoomLevel + direction:self.directionByFollowingWithCourse + duration:animated ? MGLUserLocationAnimationDuration : 0 + animationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear] + completionHandler:NULL]; +} - if (std::abs(currentPoint.x - correctPoint.x) > 1.0 || std::abs(currentPoint.y - correctPoint.y) > 1.0) +/// Changes the viewport based on a significant location update, such as the +/// first location update. +- (void)didUpdateLocationSignificantlyAnimated:(BOOL)animated +{ + self.userTrackingState = MGLUserTrackingStateBegan; + + MGLMapCamera *camera = self.camera; + camera.centerCoordinate = self.userLocation.location.coordinate; + camera.heading = self.directionByFollowingWithCourse; + if (self.zoomLevel < MGLMinimumZoomLevelForUserTracking) + { + camera.altitude = MGLAltitudeForZoomLevel(MGLDefaultZoomLevelForUserTracking, + camera.pitch, + camera.centerCoordinate.latitude, + self.frame.size); + } + + __weak MGLMapView *weakSelf = self; + [self _flyToCamera:camera + edgePadding:self.edgePaddingForFollowing + withDuration:animated ? -1 : 0 + peakAltitude:-1 + completionHandler:^{ + MGLMapView *strongSelf = weakSelf; + if (strongSelf.userTrackingState == MGLUserTrackingStateBegan) { - if (self.zoomLevel >= MGLMinimumZoomLevelForUserTracking) + strongSelf.userTrackingState = MGLUserTrackingStateChanged; + } + }]; +} + +/// Changes the viewport based on a location update in the presence of a target +/// coordinate that must also be displayed on the map concurrently. +- (void)didUpdateLocationWithTargetAnimated:(BOOL)animated +{ + BOOL firstUpdate = self.userTrackingState == MGLUserTrackingStatePossible; + void (^completion)(void); + if (animated && firstUpdate) + { + self.userTrackingState = MGLUserTrackingStateBegan; + __weak MGLMapView *weakSelf = self; + completion = ^{ + MGLMapView *strongSelf = weakSelf; + if (strongSelf.userTrackingState == MGLUserTrackingStateBegan) { - // at sufficient detail, just re-center the map; don't zoom - // - if (self.userTrackingState == MGLUserTrackingStateChanged) - { - // Ease incrementally to the new user location. - UIEdgeInsets insets = UIEdgeInsetsMake(correctPoint.y, correctPoint.x, - CGRectGetHeight(self.bounds) - correctPoint.y, - CGRectGetWidth(self.bounds) - correctPoint.x); - CAMediaTimingFunction *linearFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; - [self _setCenterCoordinate:self.userLocation.location.coordinate - edgePadding:insets - zoomLevel:self.zoomLevel - direction:course - duration:animated ? MGLUserLocationAnimationDuration : 0 - animationTimingFunction:linearFunction - completionHandler:NULL]; - } - else if (self.userTrackingState == MGLUserTrackingStatePossible) - { - // Fly to the first reported location, which may be far away - // from the current viewport. - self.userTrackingState = MGLUserTrackingStateBegan; - - MGLMapCamera *camera = self.camera; - camera.centerCoordinate = self.userLocation.location.coordinate; - camera.heading = course; - - __weak MGLMapView *weakSelf = self; - [self _flyToCamera:camera withDuration:animated ? -1 : 0 peakAltitude:-1 completionHandler:^{ - MGLMapView *strongSelf = weakSelf; - strongSelf.userTrackingState = MGLUserTrackingStateChanged; - }]; - } + strongSelf.userTrackingState = MGLUserTrackingStateChanged; } - else if (self.userTrackingState == MGLUserTrackingStatePossible) + }; + } + + CLLocationCoordinate2D foci[] = { + self.userLocation.location.coordinate, + self.targetCoordinate, + }; + UIEdgeInsets inset = self.edgePaddingForFollowingWithCourse; + if (self.userLocationVerticalAlignment == MGLAnnotationVerticalAlignmentCenter) + { + inset.bottom = CGRectGetMaxY(self.bounds) - CGRectGetMidY(self.contentFrame); + } + [self _setVisibleCoordinates:foci + count:sizeof(foci) / sizeof(foci[0]) + edgePadding:inset + direction:self.directionByFollowingWithCourse + duration:animated ? MGLUserLocationAnimationDuration : 0 + animationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear] + completionHandler:completion]; +} + +/// Returns the edge padding to apply when moving the map to a tracked location. +- (UIEdgeInsets)edgePaddingForFollowing +{ + // Center on user location unless we're already centered there (or very close). + CGPoint correctPoint = self.userLocationAnnotationViewCenter; + + // Shift the entire frame upward or downward to accommodate a shifted user + // location annotation view. + CGRect bounds = self.bounds; + CGRect boundsAroundCorrectPoint = CGRectOffset(bounds, + correctPoint.x - CGRectGetMidX(bounds), + correctPoint.y - CGRectGetMidY(bounds)); + return UIEdgeInsetsMake(CGRectGetMinY(boundsAroundCorrectPoint) - CGRectGetMinY(bounds), 0, + CGRectGetMaxY(bounds) - CGRectGetMaxY(boundsAroundCorrectPoint), 0); +} + +/// Returns the edge padding to apply during bifocal course tracking. +- (UIEdgeInsets)edgePaddingForFollowingWithCourse +{ + UIEdgeInsets inset = MGLUserLocationAnnotationViewInset; + inset.top += CGRectGetHeight(self.userLocationAnnotationView.frame); + inset.bottom += CGRectGetHeight(self.userLocationAnnotationView.frame); + return inset; +} + +/// Returns the direction the map should be turned to due to course tracking. +- (CLLocationDirection)directionByFollowingWithCourse +{ + CLLocationDirection direction = -1; + if (self.userTrackingMode == MGLUserTrackingModeFollowWithCourse) + { + if (CLLocationCoordinate2DIsValid(self.targetCoordinate)) + { + mbgl::LatLng userLatLng = MGLLatLngFromLocationCoordinate2D(self.userLocation.coordinate); + mbgl::LatLng targetLatLng = MGLLatLngFromLocationCoordinate2D(self.targetCoordinate); + mbgl::ProjectedMeters userMeters = mbgl::Projection::projectedMetersForLatLng(userLatLng); + mbgl::ProjectedMeters targetMeters = mbgl::Projection::projectedMetersForLatLng(targetLatLng); + double angle = atan2(targetMeters.easting - userMeters.easting, + targetMeters.northing - userMeters.northing); + direction = mbgl::util::wrap(MGLDegreesFromRadians(angle), 0., 360.); + } + else + { + direction = self.userLocation.location.course; + } + + if (direction >= 0) + { + if (self.userLocationVerticalAlignment == MGLAnnotationVerticalAlignmentTop) { - // otherwise re-center and zoom in to near accuracy confidence - // - self.userTrackingState = MGLUserTrackingStateBegan; - - MGLMapCamera *camera = self.camera; - camera.centerCoordinate = self.userLocation.location.coordinate; - camera.heading = course; - camera.altitude = MGLAltitudeForZoomLevel(MGLDefaultZoomLevelForUserTracking, camera.pitch, - camera.centerCoordinate.latitude, - self.frame.size); - - __weak MGLMapView *weakSelf = self; - [self _flyToCamera:camera withDuration:animated ? -1 : 0 peakAltitude:-1 completionHandler:^{ - MGLMapView *strongSelf = weakSelf; - strongSelf.userTrackingState = MGLUserTrackingStateChanged; - }]; + direction += 180; } - [self unrotateIfNeededAnimated:YES]; } } - - self.userLocationAnnotationView.haloLayer.hidden = ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate) || - newLocation.horizontalAccuracy > 10; - - [self updateUserLocationAnnotationView]; + return direction; } - (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager @@ -3204,7 +3376,8 @@ - (void)locationManager:(__unused CLLocationManager *)manager didUpdateHeading:( CLLocationDirection headingDirection = (newHeading.trueHeading >= 0 ? newHeading.trueHeading : newHeading.magneticHeading); - if (headingDirection >= 0 && self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) + if (headingDirection >= 0 && self.userTrackingMode == MGLUserTrackingModeFollowWithHeading + && self.userTrackingState != MGLUserTrackingStateBegan) { [self _setDirection:headingDirection animated:YES]; } @@ -3298,7 +3471,8 @@ - (void)unrotateIfNeededForGesture /// Rotate back to true north if the map view is zoomed too far out. - (void)unrotateIfNeededAnimated:(BOOL)animated { - if (self.direction != 0 && ! self.isRotationAllowed) + if (self.direction != 0 && ! self.isRotationAllowed + && self.userTrackingState != MGLUserTrackingStateBegan) { if (animated) { @@ -3438,7 +3612,7 @@ - (void)updateUserLocationAnnotationView { MGLUserLocationAnnotationView *annotationView = self.userLocationAnnotationView; if ( ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate)) { - annotationView.layer.hidden = YES; + annotationView.hidden = YES; return; } @@ -3459,7 +3633,7 @@ - (void)updateUserLocationAnnotationView -MGLAnnotationUpdateViewportOutset.height), userPoint)) { annotationView.center = userPoint; - annotationView.layer.hidden = NO; + annotationView.hidden = NO; [annotationView setupLayers]; if (_userLocationAnnotationIsSelected) @@ -3479,7 +3653,7 @@ - (void)updateUserLocationAnnotationView { // User has moved far enough outside of the viewport that showing it or // its callout would be useless. - annotationView.layer.hidden = YES; + annotationView.hidden = YES; if (_userLocationAnnotationIsSelected) { @@ -3488,10 +3662,15 @@ - (void)updateUserLocationAnnotationView } } -/// Intended center point of the user location annotation view. +/// Intended center point of the user location annotation view with respect to +/// the overall map view (but respecting the content inset). - (CGPoint)userLocationAnnotationViewCenter { - CGRect contentFrame = self.contentFrame; + CGRect contentFrame = UIEdgeInsetsInsetRect(self.contentFrame, self.edgePaddingForFollowingWithCourse); + if (CGRectIsEmpty(contentFrame)) + { + contentFrame = self.contentFrame; + } CGPoint center = CGPointMake(CGRectGetMidX(contentFrame), CGRectGetMidY(contentFrame)); // When tracking course, it’s more important to see the road ahead, so @@ -3500,10 +3679,10 @@ - (CGPoint)userLocationAnnotationViewCenter case MGLAnnotationVerticalAlignmentCenter: break; case MGLAnnotationVerticalAlignmentTop: - center.y = CGRectGetHeight(self.userLocationAnnotationView.frame); + center.y = CGRectGetMinY(contentFrame); break; case MGLAnnotationVerticalAlignmentBottom: - center.y = CGRectGetHeight(contentFrame) - CGRectGetHeight(self.userLocationAnnotationView.frame); + center.y = CGRectGetMaxY(contentFrame); break; } diff --git a/platform/ios/src/MGLUserLocationAnnotationView.m b/platform/ios/src/MGLUserLocationAnnotationView.m index 2e464168305..b59f4586b3e 100644 --- a/platform/ios/src/MGLUserLocationAnnotationView.m +++ b/platform/ios/src/MGLUserLocationAnnotationView.m @@ -179,6 +179,10 @@ - (void)drawPuck [self.layer addSublayer:_puckArrow]; } + if (self.annotation.location.course >= 0) + { + _puckArrow.affineTransform = CGAffineTransformRotate(CGAffineTransformIdentity, -MGLRadiansFromDegrees(self.mapView.direction - self.annotation.location.course)); + } if ( ! _puckModeActivated) { @@ -263,6 +267,10 @@ - (void)drawDot _oldHeadingAccuracy = self.annotation.heading.headingAccuracy; } + if (self.annotation.heading.trueHeading >= 0) + { + _headingIndicatorLayer.affineTransform = CGAffineTransformRotate(CGAffineTransformIdentity, -MGLRadiansFromDegrees(self.mapView.direction - self.annotation.heading.trueHeading)); + } } else { diff --git a/platform/osx/src/MGLMapView.mm b/platform/osx/src/MGLMapView.mm index 3bea3f11398..fe95427e524 100644 --- a/platform/osx/src/MGLMapView.mm +++ b/platform/osx/src/MGLMapView.mm @@ -1207,7 +1207,7 @@ - (void)handlePanGesture:(NSPanGestureRecognizer *)gestureRecognizer { [self didChangeValueForKey:@"direction"]; } if (self.pitchEnabled) { - _mbglMap->setPitch(_pitchAtBeginningOfGesture + delta.y / 5); + _mbglMap->setPitch(_pitchAtBeginningOfGesture + delta.y / 5, center); } } } else if (self.scrollEnabled) { diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index 51106db88b2..a45a9ffaff0 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -357,7 +357,11 @@ void Map::resetNorth(const Duration& duration) { #pragma mark - Pitch void Map::setPitch(double pitch, const Duration& duration) { - transform->setPitch(pitch * M_PI / 180, duration); + setPitch(pitch, {NAN, NAN}, duration); +} + +void Map::setPitch(double pitch, const PrecisionPoint& anchor, const Duration& duration) { + transform->setPitch(pitch * M_PI / 180, anchor, duration); update(Update::Repaint); } diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index b4743c0719c..208f9a089a8 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -523,12 +523,17 @@ double Transform::getAngle() const { #pragma mark - Pitch void Transform::setPitch(double pitch, const Duration& duration) { + setPitch(pitch, {NAN, NAN}, duration); +} + +void Transform::setPitch(double pitch, const PrecisionPoint& anchor, const Duration& duration) { if (std::isnan(pitch)) { return; } CameraOptions camera; camera.pitch = pitch; + camera.anchor = anchor; easeTo(camera, duration); } diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index bef6c6c87ec..8c9ea088856 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -102,7 +102,15 @@ class Transform : private util::noncopyable { double getAngle() const; // Pitch + /** Sets the pitch angle. + @param angle The new pitch angle, measured in radians toward the + horizon. */ void setPitch(double pitch, const Duration& = Duration::zero()); + /** Sets the pitch angle, keeping the given point fixed within the view. + @param angle The new pitch angle, measured in radians toward the + horizon. + @param anchor A point relative to the top-left corner of the view. */ + void setPitch(double pitch, const PrecisionPoint& anchor, const Duration& = Duration::zero()); double getPitch() const; // North Orientation