diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index 516d5e83223..6234f4d885e 100644 --- a/gyp/platform-ios.gypi +++ b/gyp/platform-ios.gypi @@ -20,6 +20,8 @@ '../include/mbgl/ios/Mapbox.h', '../platform/ios/MGLMapboxEvents.h', '../platform/ios/MGLMapboxEvents.m', + '../include/mbgl/ios/MGLMapCamera.h', + '../platform/ios/MGLMapCamera.mm', '../include/mbgl/ios/MGLMapView.h', '../include/mbgl/ios/MGLMapView+IBAdditions.h', '../platform/ios/MGLMapView.mm', diff --git a/include/mbgl/ios/MGLMapCamera.h b/include/mbgl/ios/MGLMapCamera.h new file mode 100644 index 00000000000..68f3923fd32 --- /dev/null +++ b/include/mbgl/ios/MGLMapCamera.h @@ -0,0 +1,36 @@ +#import "Mapbox.h" + +#pragma once + +NS_ASSUME_NONNULL_BEGIN + +/** An `MGLMapCamera` object represents a viewpoint from which the user observes some point on an `MGLMapView`. */ +@interface MGLMapCamera : NSObject + +/** Coordinate at the center of the map view. */ +@property (nonatomic) CLLocationCoordinate2D centerCoordinate; + +/** Heading measured in degrees clockwise from true north. */ +@property (nonatomic) CLLocationDirection heading; + +/** Pitch toward the horizon measured in degrees, with 0 degrees resulting in a two-dimensional map. */ +@property (nonatomic) CGFloat pitch; + +/** Meters above ground level. */ +@property (nonatomic) CLLocationDistance altitude; + +/** Returns a new camera with all properties set to 0. */ ++ (instancetype)camera; + ++ (instancetype)cameraLookingAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + fromEyeCoordinate:(CLLocationCoordinate2D)eyeCoordinate + eyeAltitude:(CLLocationDistance)eyeAltitude; + ++ (instancetype)cameraLookingAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + fromDistance:(CLLocationDistance)distance + pitch:(CGFloat)pitch + heading:(CLLocationDirection)heading; + +@end + +NS_ASSUME_NONNULL_END diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index ba332d98453..da500f1ef66 100644 --- a/include/mbgl/ios/MGLMapView.h +++ b/include/mbgl/ios/MGLMapView.h @@ -1,4 +1,5 @@ #import "MGLGeometry.h" +#import "MGLMapCamera.h" #import #import @@ -6,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN @class MGLAnnotationImage; +@class MGLMapCamera; @class MGLUserLocation; @class MGLPolyline; @class MGLPolygon; @@ -136,6 +138,8 @@ IB_DESIGNABLE * @param animated Specify `YES` if you want the map view to animate scrolling and zooming to the new location or `NO` if you want the map to display the new location immediately. */ - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated; +- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction animated:(BOOL)animated; + /** The coordinate bounds visible in the receiver’s viewport. * * Changing the value of this property updates the receiver immediately. If you want to animate the change, call `setVisibleCoordinateBounds:animated:` instead. */ @@ -181,19 +185,19 @@ IB_DESIGNABLE /** Resets the map rotation to a northern heading. */ - (IBAction)resetNorth; -/** The pitch of the map (measured in degrees). - * - * The default value `0` shows a completely flat map. Maximum value is `60`. */ -@property (nonatomic) double pitch; +/** A camera representing the current viewpoint of the map. */ +@property (nonatomic, copy) MGLMapCamera *camera; -/** Changes the pitch of the map. - * @param pitch The pitch of the map (measured in degrees) relative to top-down. - * - * Changing the pitch tilts the map without changing the current center coordinate or zoom level. */ -- (void)setPitch:(double)pitch; +/** Moves the viewpoint to a different location with respect to the map with an optional transition animation. +* @param camera The new viewpoint. +* @param animated Specify `YES` if you want the map view to animate the change to the new viewpoint or `NO` if you want the map to display the new viewpoint immediately. */ +- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated; -/** Resets the map pitch to head-on. */ -- (IBAction)resetPitch; +/** Moves the viewpoint to a different location with respect to the map with an optional transition duration and timing function. +* @param camera The new viewpoint. +* @param duration The amount of time, measured in seconds, that the transition animation should take. Specify `0` to jump to the new viewpoint instantaneously. +* @param function A timing function used for the animation. Set this parameter to `nil` for a transition that matches most system animations. If the duration is `0`, this parameter is ignored. */ +- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function; #pragma mark - Converting Map Coordinates diff --git a/include/mbgl/ios/Mapbox.h b/include/mbgl/ios/Mapbox.h index 401a62e82e1..f05f0c84294 100644 --- a/include/mbgl/ios/Mapbox.h +++ b/include/mbgl/ios/Mapbox.h @@ -1,6 +1,7 @@ #import "MGLAccountManager.h" #import "MGLAnnotation.h" #import "MGLAnnotationImage.h" +#import "MGLMapCamera.h" #import "MGLGeometry.h" #import "MGLMapView.h" #import "MGLMultiPoint.h" diff --git a/include/mbgl/map/camera.hpp b/include/mbgl/map/camera.hpp new file mode 100644 index 00000000000..bd0b353baea --- /dev/null +++ b/include/mbgl/map/camera.hpp @@ -0,0 +1,22 @@ +#ifndef MBGL_MAP_CAMERA +#define MBGL_MAP_CAMERA + +#include +#include +#include +#include + +namespace mbgl { + +struct CameraOptions { + mapbox::util::optional center; + mapbox::util::optional zoom; + mapbox::util::optional angle; + mapbox::util::optional pitch; + mapbox::util::optional duration; + mapbox::util::optional easing; +}; + +} + +#endif /* MBGL_MAP_CAMERA */ diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index ddf86045ee2..4cd9293c9bf 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -2,6 +2,7 @@ #define MBGL_MAP_MAP #include +#include #include #include #include @@ -94,6 +95,10 @@ class Map : private util::noncopyable { void cancelTransitions(); void setGestureInProgress(bool); + // Camera + void jumpTo(CameraOptions options); + void easeTo(CameraOptions options); + // Position void moveBy(double dx, double dy, const Duration& = Duration::zero()); void setLatLng(LatLng latLng, const Duration& = Duration::zero()); @@ -107,8 +112,8 @@ class Map : private util::noncopyable { void setZoom(double zoom, const Duration& = Duration::zero()); double getZoom() const; void setLatLngZoom(LatLng latLng, double zoom, const Duration& = Duration::zero()); - void fitBounds(LatLngBounds bounds, EdgeInsets padding, const Duration& duration = Duration::zero()); - void fitBounds(AnnotationSegment segment, EdgeInsets padding, const Duration& duration = Duration::zero()); + CameraOptions cameraForLatLngBounds(LatLngBounds bounds, EdgeInsets padding); + CameraOptions cameraForLatLngs(std::vector latLngs, EdgeInsets padding); void resetZoom(); double getMinZoom() const; double getMaxZoom() const; @@ -121,7 +126,7 @@ class Map : private util::noncopyable { void resetNorth(); // Pitch - void setPitch(double pitch); + void setPitch(double pitch, const Duration& = Duration::zero()); double getPitch() const; // Size diff --git a/src/mbgl/util/optional.hpp b/include/mbgl/util/optional.hpp similarity index 100% rename from src/mbgl/util/optional.hpp rename to include/mbgl/util/optional.hpp diff --git a/src/mbgl/util/unitbezier.hpp b/include/mbgl/util/unitbezier.hpp similarity index 100% rename from src/mbgl/util/unitbezier.hpp rename to include/mbgl/util/unitbezier.hpp diff --git a/ios/app/MBXViewController.mm b/ios/app/MBXViewController.mm index af25fa4dc5c..b323717702b 100644 --- a/ios/app/MBXViewController.mm +++ b/ios/app/MBXViewController.mm @@ -2,8 +2,6 @@ #import -#import - #import static UIColor *const kTintColor = [UIColor colorWithRed:0.120 green:0.550 blue:0.670 alpha:1.000]; @@ -26,10 +24,20 @@ @interface MBXViewController () @implementation MBXViewController -mbgl::Settings_NSUserDefaults *settings = nullptr; - #pragma mark - Setup ++ (void)initialize +{ + if (self == [MBXViewController class]) + { + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + @"userTrackingMode": @(MGLUserTrackingModeNone), + @"showsUserLocation": @NO, + @"debug": @NO, + }]; + } +} + - (id)init { self = [super init]; @@ -38,6 +46,7 @@ - (id)init { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveState:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(restoreState:) name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveState:) name:UIApplicationWillTerminateNotification object:nil]; } return self; @@ -73,7 +82,6 @@ - (void)viewDidLoad target:self action:@selector(locateUser)]; - settings = new mbgl::Settings_NSUserDefaults(); [self restoreState:nil]; if ( ! settings->showsUserLocation) @@ -86,30 +94,37 @@ - (void)viewDidLoad - (void)saveState:(__unused NSNotification *)notification { - if (self.mapView && settings) + if (self.mapView) { - settings->longitude = self.mapView.centerCoordinate.longitude; - settings->latitude = self.mapView.centerCoordinate.latitude; - settings->zoom = self.mapView.zoomLevel; - settings->bearing = self.mapView.direction; - settings->pitch = self.mapView.pitch; - settings->debug = self.mapView.isDebugActive; - settings->userTrackingMode = self.mapView.userTrackingMode; - settings->showsUserLocation = self.mapView.showsUserLocation; - settings->save(); + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSData *archivedCamera = [NSKeyedArchiver archivedDataWithRootObject:self.mapView.camera]; + [defaults setObject:archivedCamera forKey:@"camera"]; + [defaults setInteger:self.mapView.userTrackingMode forKey:@"userTrackingMode"]; + [defaults setBool:self.mapView.showsUserLocation forKey:@"showsUserLocation"]; + [defaults setBool:self.mapView.debugActive forKey:@"debug"]; + [defaults synchronize]; } } - (void)restoreState:(__unused NSNotification *)notification { - if (self.mapView && settings) { - settings->load(); - [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(settings->latitude, settings->longitude) zoomLevel:settings->zoom animated:NO]; - self.mapView.direction = settings->bearing; - self.mapView.pitch = settings->pitch; - self.mapView.userTrackingMode = settings->userTrackingMode; - self.mapView.showsUserLocation = settings->showsUserLocation; - [self.mapView setDebugActive:settings->debug]; + if (self.mapView) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSData *archivedCamera = [defaults objectForKey:@"camera"]; + MGLMapCamera *camera = archivedCamera ? [NSKeyedUnarchiver unarchiveObjectWithData:archivedCamera] : nil; + if (camera) + { + self.mapView.camera = camera; + } + NSInteger uncheckedTrackingMode = [defaults integerForKey:@"userTrackingMode"]; + if (uncheckedTrackingMode >= 0 && + (NSUInteger)uncheckedTrackingMode >= MGLUserTrackingModeNone && + (NSUInteger)uncheckedTrackingMode <= MGLUserTrackingModeFollowWithCourse) + { + self.mapView.userTrackingMode = (MGLUserTrackingMode)uncheckedTrackingMode; + } + self.mapView.showsUserLocation = [defaults boolForKey:@"showsUserLocation"]; + self.mapView.debugActive = [defaults boolForKey:@"debug"]; } } @@ -339,12 +354,7 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; - if (settings) - { - [self saveState:nil]; - delete settings; - settings = nullptr; - } + [self saveState:nil]; } #pragma mark - MGLMapViewDelegate diff --git a/ios/app/mapboxgl-app.gypi b/ios/app/mapboxgl-app.gypi index 34381ca1586..8407ea7da2b 100644 --- a/ios/app/mapboxgl-app.gypi +++ b/ios/app/mapboxgl-app.gypi @@ -32,7 +32,6 @@ './MBXAppDelegate.m', './MBXViewController.h', './MBXViewController.mm', - '../../platform/darwin/settings_nsuserdefaults.mm', ], 'xcode_settings': { diff --git a/platform/ios/MGLMapCamera.mm b/platform/ios/MGLMapCamera.mm new file mode 100644 index 00000000000..d04e46fa909 --- /dev/null +++ b/platform/ios/MGLMapCamera.mm @@ -0,0 +1,101 @@ +#import "MGLMapCamera.h" + +#include + +@implementation MGLMapCamera + ++ (BOOL)supportsSecureCoding +{ + return YES; +} + ++ (instancetype)camera +{ + return [[self alloc] init]; +} + ++ (instancetype)cameraLookingAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + fromEyeCoordinate:(CLLocationCoordinate2D)eyeCoordinate + eyeAltitude:(CLLocationDistance)eyeAltitude +{ + mbgl::LatLng centerLatLng = mbgl::LatLng(centerCoordinate.latitude, centerCoordinate.longitude); + mbgl::LatLng eyeLatLng = mbgl::LatLng(eyeCoordinate.latitude, eyeCoordinate.longitude); + + mbgl::ProjectedMeters centerMeters = mbgl::Projection::projectedMetersForLatLng(centerLatLng); + mbgl::ProjectedMeters eyeMeters = mbgl::Projection::projectedMetersForLatLng(eyeLatLng); + CLLocationDirection heading = std::atan((centerMeters.northing - eyeMeters.northing) / + (centerMeters.easting - eyeMeters.easting)); + + double groundDistance = std::hypot(centerMeters.northing - eyeMeters.northing, + centerMeters.easting - eyeMeters.easting); + CGFloat pitch = std::atan(eyeAltitude / groundDistance); + + return [[self alloc] initWithCenterCoordinate:centerCoordinate + altitude:eyeAltitude + pitch:pitch + heading:heading]; +} + ++ (instancetype)cameraLookingAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + fromDistance:(CLLocationDistance)distance + pitch:(CGFloat)pitch + heading:(CLLocationDirection)heading +{ + return [[self alloc] initWithCenterCoordinate:centerCoordinate + altitude:distance + pitch:(CGFloat)pitch + heading:heading]; +} + +- (instancetype)initWithCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + altitude:(CLLocationDistance)altitude + pitch:(CGFloat)pitch + heading:(CLLocationDirection)heading +{ + if (self = [super init]) + { + _centerCoordinate = centerCoordinate; + _altitude = altitude; + _pitch = pitch; + _heading = heading; + } + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + if (self = [super init]) + { + _centerCoordinate = CLLocationCoordinate2DMake([coder decodeDoubleForKey:@"centerLatitude"], + [coder decodeDoubleForKey:@"centerLongitude"]); + _altitude = [coder decodeDoubleForKey:@"altitude"]; + _pitch = [coder decodeDoubleForKey:@"pitch"]; + _heading = [coder decodeDoubleForKey:@"heading"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeDouble:_centerCoordinate.latitude forKey:@"centerLatitude"]; + [coder encodeDouble:_centerCoordinate.longitude forKey:@"centerLongitude"]; + [coder encodeDouble:_altitude forKey:@"altitude"]; + [coder encodeDouble:_pitch forKey:@"pitch"]; + [coder encodeDouble:_heading forKey:@"heading"]; +} + +- (id)copyWithZone:(nullable NSZone *)zone +{ + return [[[self class] allocWithZone:zone] initWithCenterCoordinate:_centerCoordinate + altitude:_altitude + pitch:_pitch + heading:_heading]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", + self, _centerCoordinate.latitude, _centerCoordinate.longitude, _altitude, _heading, _pitch]; +} + +@end diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index 959f294f076..e509b5c8d84 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -48,6 +48,9 @@ const NSTimeInterval MGLAnimationDuration = 0.3; const CGSize MGLAnnotationUpdateViewportOutset = {150, 150}; const CGFloat MGLMinimumZoom = 3; +const CGFloat MGLMinimumPitch = 0; +const CGFloat MGLMaximumPitch = 60; +const CLLocationDegrees MGLAngularFieldOfView = M_PI / 6.; NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey"; NSString *const MGLAnnotationSymbolKey = @"MGLAnnotationSymbolKey"; @@ -67,6 +70,18 @@ CLLocationDegrees MGLDegreesFromRadians(CGFloat radians) return radians * 180 / M_PI; } +mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction *function) +{ + if ( ! function) + { + function = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]; + } + float p1[2], p2[2]; + [function getControlPointAtIndex:0 values:p1]; + [function getControlPointAtIndex:1 values:p2]; + return { p1[0], p1[1], p2[0], p2[1] }; +} + #pragma mark - Private - @interface MGLMapView () @@ -368,7 +383,10 @@ - (void)commonInit // set initial position // - _mbglMap->setLatLngZoom(mbgl::LatLng(0, 0), _mbglMap->getMinZoom()); + mbgl::CameraOptions options; + options.center = mbgl::LatLng(0, 0); + options.zoom = _mbglMap->getMinZoom(); + _mbglMap->jumpTo(options); _pendingLatitude = NAN; _pendingLongitude = NAN; @@ -1347,12 +1365,10 @@ - (void)handleTwoFingerDragGesture:(UIPanGestureRecognizer *)twoFingerDrag else if (twoFingerDrag.state == UIGestureRecognizerStateBegan || twoFingerDrag.state == UIGestureRecognizerStateChanged) { CGFloat gestureDistance = CGPoint([twoFingerDrag translationInView:twoFingerDrag.view]).y; - double currentPitch = _mbglMap->getPitch(); - double minPitch = 0; - double maxPitch = 60.0; - double slowdown = 20.0; + CGFloat currentPitch = _mbglMap->getPitch(); + CGFloat slowdown = 20.0; - double pitchNew = fmax(fmin(currentPitch - (gestureDistance / slowdown), maxPitch), minPitch); + CGFloat pitchNew = mbgl::util::clamp(currentPitch - (gestureDistance / slowdown), MGLMinimumPitch, MGLMaximumPitch); _mbglMap->setPitch(pitchNew); } @@ -1530,7 +1546,7 @@ - (void)emptyMemoryCache + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate { - return [NSSet setWithObjects:@"latitude", @"longitude", nil]; + return [NSSet setWithObjects:@"latitude", @"longitude", @"camera", nil]; } - (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated preservingTracking:(BOOL)tracking @@ -1542,13 +1558,7 @@ - (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)an - (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated { - CGFloat duration = (animated ? MGLAnimationDuration : 0); - - _mbglMap->setLatLngZoom(MGLLatLngFromLocationCoordinate2D(coordinate), - fmaxf(_mbglMap->getZoom(), self.currentMinimumZoom), - secondsAsDuration(duration)); - - [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; + [self setCenterCoordinate:coordinate zoomLevel:self.zoomLevel animated:animated]; } - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate @@ -1562,36 +1572,46 @@ - (CLLocationCoordinate2D)centerCoordinate } - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated +{ + [self setCenterCoordinate:centerCoordinate zoomLevel:zoomLevel direction:self.direction animated:animated]; +} + +- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction animated:(BOOL)animated { self.userTrackingMode = MGLUserTrackingModeNone; - CGFloat duration = (animated ? MGLAnimationDuration : 0); + [self _setCenterCoordinate:centerCoordinate zoomLevel:zoomLevel direction:direction animated:animated]; +} - _mbglMap->setLatLngZoom(MGLLatLngFromLocationCoordinate2D(centerCoordinate), zoomLevel, secondsAsDuration(duration)); +- (void)_setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction animated:(BOOL)animated +{ + mbgl::CameraOptions options; + options.center = MGLLatLngFromLocationCoordinate2D(centerCoordinate); + options.zoom = fmaxf(zoomLevel, self.currentMinimumZoom); + if (direction >= 0) + { + options.angle = MGLRadiansFromDegrees(-direction); + } + if (animated) + { + options.duration = secondsAsDuration(MGLAnimationDuration); + options.easing = MGLUnitBezierForMediaTimingFunction(nil); + } + _mbglMap->easeTo(options); [self unrotateIfNeededAnimated:animated]; [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; } -- (double)zoomLevel ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomLevel { - return _mbglMap->getZoom(); + return [NSSet setWithObject:@"camera"]; } -- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated +- (double)zoomLevel { - self.userTrackingMode = MGLUserTrackingModeNone; - - CGFloat duration = (animated ? MGLAnimationDuration : 0); - - _mbglMap->setLatLngZoom(_mbglMap->getLatLng(), - fmaxf(zoomLevel, self.currentMinimumZoom), - secondsAsDuration(duration)); - - [self unrotateIfNeededAnimated:animated]; - - [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; + return _mbglMap->getZoom(); } - (void)setZoomLevel:(double)zoomLevel @@ -1599,6 +1619,11 @@ - (void)setZoomLevel:(double)zoomLevel [self setZoomLevel:zoomLevel animated:NO]; } +- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated +{ + [self setCenterCoordinate:self.centerCoordinate zoomLevel:zoomLevel animated:animated]; +} + MGLCoordinateBounds MGLCoordinateBoundsFromLatLngBounds(mbgl::LatLngBounds latLngBounds) { return MGLCoordinateBoundsMake(MGLLocationCoordinate2DFromLatLng(latLngBounds.sw), @@ -1639,11 +1664,34 @@ - (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEd animated:animated]; } +- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction animated:(BOOL)animated +{ + CLLocationCoordinate2D coordinates[] = { + {bounds.ne.latitude, bounds.sw.longitude}, + bounds.sw, + {bounds.sw.latitude, bounds.ne.longitude}, + bounds.ne, + }; + [self setVisibleCoordinates:coordinates + count:sizeof(coordinates) / sizeof(coordinates[0]) + edgePadding:insets + direction:direction + animated:animated]; +} + - (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated +{ + [self setVisibleCoordinates:coordinates count:count edgePadding:insets direction:self.direction animated:animated]; +} + +- (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction animated:(BOOL)animated +{ + [self setVisibleCoordinates:coordinates count:count edgePadding:insets direction:direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil]; +} + +- (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUInteger)count edgePadding:(UIEdgeInsets)insets direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(CAMediaTimingFunction *)function { // NOTE: does not disrupt tracking mode - CGFloat duration = animated ? MGLAnimationDuration : 0; - [self willChangeValueForKey:@"visibleCoordinateBounds"]; mbgl::EdgeInsets mbglInsets = {insets.top, insets.left, insets.bottom, insets.right}; mbgl::AnnotationSegment segment; @@ -1652,12 +1700,27 @@ - (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUIn { segment.push_back({coordinates[i].latitude, coordinates[i].longitude}); } - _mbglMap->fitBounds(segment, mbglInsets, secondsAsDuration(duration)); + mbgl::CameraOptions options = _mbglMap->cameraForLatLngs(segment, mbglInsets); + if (direction >= 0) + { + options.angle = MGLRadiansFromDegrees(-direction); + } + if (duration > 0) + { + options.duration = secondsAsDuration(duration); + options.easing = MGLUnitBezierForMediaTimingFunction(function); + } + _mbglMap->easeTo(options); [self didChangeValueForKey:@"visibleCoordinateBounds"]; - [self unrotateIfNeededAnimated:animated]; + [self unrotateIfNeededAnimated:duration > 0]; - [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; + [self notifyMapChange:(duration > 0 ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingDirection +{ + return [NSSet setWithObject:@"camera"]; } - (CLLocationDirection)direction @@ -1683,23 +1746,139 @@ - (void)setDirection:(CLLocationDirection)direction [self setDirection:direction animated:NO]; } -- (double)pitch ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingPitch { - return _mbglMap->getPitch(); + return [NSSet setWithObject:@"camera"]; } -- (void)setPitch:(double)pitch ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCamera { - // constrain pitch to between 0º and 60º - // - _mbglMap->setPitch(fmax(fmin(pitch, 60), 0)); + return [NSSet setWithObjects:@"longitude", @"latitude", @"centerCoordinate", @"zoomLevel", @"direction", nil]; +} + +- (MGLMapCamera *)camera +{ + CGRect frame = self.frame; + CGPoint edgePoint; + // Constrain by the shorter of the two axes. + if (frame.size.width > frame.size.height) // landscape + { + edgePoint = CGPointMake(0, frame.size.height / 2.); + } + else // portrait + { + edgePoint = CGPointMake(frame.size.width / 2., 0); + } + CLLocationCoordinate2D edgeCoordinate = [self convertPoint:edgePoint toCoordinateFromView:self]; + mbgl::ProjectedMeters edgeMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(edgeCoordinate)); + + // Because we constrain the zoom level vertically in portrait orientation, + // the visible medial span is affected by pitch: the distance from the + // center point to the near edge is less than than distance from the center + // point to the far edge. Average the two distances. + mbgl::ProjectedMeters nearEdgeMeters; + if (frame.size.width > frame.size.height) + { + nearEdgeMeters = edgeMeters; + } + else + { + CGPoint nearEdgePoint = CGPointMake(frame.size.width / 2., frame.size.height); + CLLocationCoordinate2D nearEdgeCoordinate = [self convertPoint:nearEdgePoint toCoordinateFromView:self]; + nearEdgeMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(nearEdgeCoordinate)); + } + + // The opposite side is the distance between the center and one edge. + CLLocationCoordinate2D centerCoordinate = self.centerCoordinate; + mbgl::ProjectedMeters centerMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate)); + CLLocationDistance centerToEdge = std::hypot(centerMeters.easting - edgeMeters.easting, + centerMeters.northing - edgeMeters.northing); + CLLocationDistance centerToNearEdge = std::hypot(centerMeters.easting - nearEdgeMeters.easting, + centerMeters.northing - nearEdgeMeters.northing); + CLLocationDistance altitude = (centerToEdge + centerToNearEdge) / 2 / std::tan(MGLAngularFieldOfView / 2.); - //[self notifyMapChange:(mbgl::MapChangeRegionDidChange)]; + CGFloat pitch = _mbglMap->getPitch(); + + return [MGLMapCamera cameraLookingAtCenterCoordinate:centerCoordinate + fromDistance:altitude + pitch:pitch + heading:self.direction]; } -- (void)resetPitch +- (void)setCamera:(MGLMapCamera *)camera { - [self setPitch:0]; + [self setCamera:camera animated:NO]; +} + +- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated +{ + [self setCamera:camera withDuration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil]; +} + +- (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(CAMediaTimingFunction *)function +{ + // The opposite side is the distance between the center and one edge. + mbgl::LatLng centerLatLng = MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate); + mbgl::ProjectedMeters centerMeters = _mbglMap->projectedMetersForLatLng(centerLatLng); + CLLocationDistance centerToEdge = camera.altitude * std::tan(MGLAngularFieldOfView / 2.); + + double angle = -1; + if (camera.heading >= 0) + { + angle = MGLRadiansFromDegrees(-camera.heading); + } + double pitch = -1; + if (camera.pitch >= 0) + { + pitch = MGLRadiansFromDegrees(mbgl::util::clamp(camera.pitch, MGLMinimumPitch, MGLMaximumPitch)); + } + + // Make a visible bounds that extends in the constrained direction (the + // shorter of the two axes). + CGRect frame = self.frame; + mbgl::LatLng sw, ne; + if (frame.size.width > frame.size.height) // landscape + { + sw = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing - centerToEdge * std::sin(angle), + centerMeters.easting - centerToEdge * std::cos(angle), + }); + ne = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing + centerToEdge * std::sin(angle), + centerMeters.easting + centerToEdge * std::cos(angle), + }); + } + else // portrait + { + sw = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing - centerToEdge * std::cos(-angle) + centerToEdge * std::cos(-angle) * std::sin(pitch) / 2, + centerMeters.easting - centerToEdge * std::sin(-angle) + centerToEdge * std::sin(-angle) * std::sin(pitch) / 2, + }); + ne = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing + centerToEdge * std::cos(-angle) - centerToEdge * std::cos(-angle) * std::sin(pitch) / 2, + centerMeters.easting + centerToEdge * std::sin(-angle) - centerToEdge * std::sin(-angle) * std::sin(pitch) / 2, + }); + } + + // Fit the viewport to the bounds. Correct the center in case pitch should + // cause the visual center to lie above the screen center. + mbgl::CameraOptions options = _mbglMap->cameraForLatLngs({ sw, ne }, {}); + options.center = centerLatLng; + + if (camera.heading >= 0) + { + options.angle = angle; + } + if (pitch >= 0) + { + options.pitch = pitch; + } + if (duration > 0) + { + options.duration = secondsAsDuration(duration); + options.easing = MGLUnitBezierForMediaTimingFunction(function); + } + _mbglMap->easeTo(options); } - (CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(nullable UIView *)view @@ -2482,6 +2661,12 @@ - (void)locationManager:(__unused CLLocationManager *)manager didUpdateLocations [self.delegate mapView:self didUpdateUserLocation:self.userLocation]; } } + + CLLocationDirection course = self.userLocation.location.course; + if (course < 0 || self.userTrackingMode != MGLUserTrackingModeFollowWithCourse) + { + course = -1; + } if (self.userTrackingMode != MGLUserTrackingModeNone) { @@ -2496,7 +2681,7 @@ - (void)locationManager:(__unused CLLocationManager *)manager didUpdateLocations { // at sufficient detail, just re-center the map; don't zoom // - [self setCenterCoordinate:self.userLocation.location.coordinate animated:YES preservingTracking:YES]; + [self _setCenterCoordinate:self.userLocation.location.coordinate zoomLevel:self.zoomLevel direction:course animated:YES]; } else { @@ -2526,17 +2711,11 @@ - (void)locationManager:(__unused CLLocationManager *)manager didUpdateLocations desiredSouthWest.longitude != actualSouthWest.longitude) { // assumes we won't disrupt tracking mode - [self setVisibleCoordinateBounds:MGLCoordinateBoundsMake(desiredSouthWest, desiredNorthEast) animated:YES]; + [self setVisibleCoordinateBounds:MGLCoordinateBoundsMake(desiredSouthWest, desiredNorthEast) edgePadding:UIEdgeInsetsZero direction:course animated:YES]; } } } } - - CLLocationDirection course = self.userLocation.location.course; - if (course >= 0 && self.userTrackingMode == MGLUserTrackingModeFollowWithCourse) - { - _mbglMap->setBearing(course); - } self.userLocationAnnotationView.haloLayer.hidden = ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate) || newLocation.horizontalAccuracy > 10; @@ -3088,7 +3267,7 @@ @implementation MGLMapView (IBAdditions) + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLatitude { - return [NSSet setWithObject:@"centerCoordinate"]; + return [NSSet setWithObjects:@"centerCoordinate", @"camera", nil]; } - (double)latitude @@ -3114,7 +3293,7 @@ - (void)setLatitude:(double)latitude + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLongitude { - return [NSSet setWithObject:@"centerCoordinate"]; + return [NSSet setWithObjects:@"centerCoordinate", @"camera", nil]; } - (double)longitude diff --git a/src/mbgl/map/camera.cpp b/src/mbgl/map/camera.cpp new file mode 100644 index 00000000000..4a45e904f84 --- /dev/null +++ b/src/mbgl/map/camera.cpp @@ -0,0 +1 @@ +#include diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index 82ab72db2d3..9195f6b583a 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -9,6 +9,7 @@ #include #include +#include namespace mbgl { @@ -114,6 +115,18 @@ void Map::setGestureInProgress(bool inProgress) { update(Update::Repaint); } +#pragma mark - + +void Map::jumpTo(CameraOptions options) { + transform->jumpTo(options); + update(Update::Repaint); +} + +void Map::easeTo(CameraOptions options) { + transform->easeTo(options); + update(options.zoom ? Update::Zoom : Update::Repaint); +} + #pragma mark - Position void Map::moveBy(double dx, double dy, const Duration& duration) { @@ -131,9 +144,11 @@ LatLng Map::getLatLng() const { } void Map::resetPosition() { - transform->setAngle(0); - transform->setLatLng(LatLng(0, 0)); - transform->setZoom(0); + CameraOptions options; + options.angle = 0; + options.center = LatLng(0, 0); + options.zoom = 0; + transform->jumpTo(options); update(Update::Zoom); } @@ -168,25 +183,26 @@ void Map::setLatLngZoom(LatLng latLng, double zoom, const Duration& duration) { update(Update::Zoom); } -void Map::fitBounds(LatLngBounds bounds, EdgeInsets padding, const Duration& duration) { +CameraOptions Map::cameraForLatLngBounds(LatLngBounds bounds, EdgeInsets padding) { AnnotationSegment segment = { {bounds.ne.latitude, bounds.sw.longitude}, bounds.sw, {bounds.sw.latitude, bounds.ne.longitude}, bounds.ne, }; - fitBounds(segment, padding, duration); + return cameraForLatLngs(segment, padding); } -void Map::fitBounds(AnnotationSegment segment, EdgeInsets padding, const Duration& duration) { - if (segment.empty()) { - return; +CameraOptions Map::cameraForLatLngs(std::vector latLngs, EdgeInsets padding) { + CameraOptions options; + if (latLngs.empty()) { + return options; } // Calculate the bounds of the possibly rotated shape with respect to the viewport. vec2<> nePixel = {-INFINITY, -INFINITY}; vec2<> swPixel = {INFINITY, INFINITY}; - for (LatLng latLng : segment) { + for (LatLng latLng : latLngs) { vec2<> pixel = pixelForLatLng(latLng); swPixel.x = std::min(swPixel.x, pixel.x); nePixel.x = std::max(nePixel.x, pixel.x); @@ -214,7 +230,9 @@ void Map::fitBounds(AnnotationSegment segment, EdgeInsets padding, const Duratio vec2<> centerPixel = (paddedNEPixel + paddedSWPixel) * 0.5; LatLng centerLatLng = latLngForPixel(centerPixel); - setLatLngZoom(centerLatLng, zoom, duration); + options.center = centerLatLng; + options.zoom = zoom; + return options; } void Map::resetZoom() { @@ -270,8 +288,8 @@ void Map::resetNorth() { #pragma mark - Pitch -void Map::setPitch(double pitch) { - transform->setPitch(std::min(pitch, 60.0) * M_PI / 180); +void Map::setPitch(double pitch, const Duration& duration) { + transform->setPitch(util::clamp(pitch, 0., 60.) * M_PI / 180, duration); update(Update::Repaint); } diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 60c55c3f0ce..bb559091492 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -53,6 +54,38 @@ bool Transform::resize(const std::array size) { #pragma mark - Position +void Transform::jumpTo(const CameraOptions options) { + CameraOptions jumpOptions = options; + jumpOptions.duration.reset(); + easeTo(jumpOptions); +} + +void Transform::easeTo(CameraOptions options) { + LatLng latLng = options.center ? *options.center : getLatLng(); + double zoom = options.zoom ? *options.zoom : getZoom(); + double angle = options.angle ? *options.angle : getAngle(); + if (std::isnan(latLng.latitude) || std::isnan(latLng.longitude) || std::isnan(zoom)) { + return; + } + + double new_scale = std::pow(2.0, zoom); + + const double s = new_scale * util::tileSize; + state.Bc = s / 360; + state.Cc = s / util::M2PI; + + const double m = 1 - 1e-15; + const double f = std::fmin(std::fmax(std::sin(util::DEG2RAD * latLng.latitude), -m), m); + + double xn = -latLng.longitude * state.Bc; + double yn = 0.5 * state.Cc * std::log((1 + f) / (1 - f)); + + options.center.reset(); + options.zoom.reset(); + options.angle.reset(); + _easeTo(options, new_scale, angle, xn, yn); +} + void Transform::moveBy(const double dx, const double dy, const Duration& duration) { if (std::isnan(dx) || std::isnan(dy)) { return; @@ -62,71 +95,30 @@ void Transform::moveBy(const double dx, const double dy, const Duration& duratio } void Transform::_moveBy(const double dx, const double dy, const Duration& duration) { + double x = state.x + std::cos(state.angle) * dx + std::sin( state.angle) * dy; double y = state.y + std::cos(state.angle) * dy + std::sin(-state.angle) * dx; state.constrain(state.scale, y); - - if (duration == Duration::zero()) { - view.notifyMapChange(MapChangeRegionWillChange); - - state.x = x; - state.y = y; - - view.notifyMapChange(MapChangeRegionDidChange); - } else { - view.notifyMapChange(MapChangeRegionWillChangeAnimated); - - const double startX = state.x; - const double startY = state.y; - state.panning = true; - - startTransition( - [=](double t) { - state.x = util::interpolate(startX, x, t); - state.y = util::interpolate(startY, y, t); - view.notifyMapChange(MapChangeRegionIsChanging); - return Update::Repaint; - }, - [=] { - state.panning = false; - view.notifyMapChange(MapChangeRegionDidChangeAnimated); - }, duration); - } + + CameraOptions options; + options.duration = duration; + _easeTo(options, state.scale, state.angle, x, y); } void Transform::setLatLng(const LatLng latLng, const Duration& duration) { - if (std::isnan(latLng.latitude) || std::isnan(latLng.longitude)) { - return; - } - - const double m = 1 - 1e-15; - const double f = ::fmin(::fmax(std::sin(util::DEG2RAD * latLng.latitude), -m), m); - - double xn = -latLng.longitude * state.Bc; - double yn = 0.5 * state.Cc * std::log((1 + f) / (1 - f)); - - _setScaleXY(state.scale, xn, yn, duration); + CameraOptions options; + options.center = latLng; + options.duration = duration; + easeTo(options); } void Transform::setLatLngZoom(const LatLng latLng, const double zoom, const Duration& duration) { - if (std::isnan(latLng.latitude) || std::isnan(latLng.longitude) || std::isnan(zoom)) { - return; - } - - double new_scale = std::pow(2.0, zoom); - - const double s = new_scale * util::tileSize; - state.Bc = s / 360; - state.Cc = s / util::M2PI; - - const double m = 1 - 1e-15; - const double f = ::fmin(::fmax(std::sin(util::DEG2RAD * latLng.latitude), -m), m); - - double xn = -latLng.longitude * state.Bc; - double yn = 0.5 * state.Cc * std::log((1 + f) / (1 - f)); - - _setScaleXY(new_scale, xn, yn, duration); + CameraOptions options; + options.center = latLng; + options.zoom = zoom; + options.duration = duration; + easeTo(options); } @@ -206,13 +198,27 @@ void Transform::_setScale(double new_scale, double cx, double cy, const Duration void Transform::_setScaleXY(const double new_scale, const double xn, const double yn, const Duration& duration) { + CameraOptions options; + options.duration = duration; + _easeTo(options, new_scale, state.angle, xn, yn); +} + +void Transform::_easeTo(CameraOptions options, const double new_scale, const double new_angle, const double xn, const double yn) { + Update update = state.scale == new_scale ? Update::Repaint : Update::Zoom; double scale = new_scale; double x = xn; double y = yn; state.constrain(scale, y); + + double angle = _normalizeAngle(new_angle, state.angle); + state.angle = _normalizeAngle(state.angle, angle); + double pitch = options.pitch ? *options.pitch : state.pitch; - if (duration == Duration::zero()) { + if (!options.duration) { + options.duration = Duration::zero(); + } + if (!options.duration || *options.duration == Duration::zero()) { view.notifyMapChange(MapChangeRegionWillChange); state.scale = scale; @@ -221,18 +227,28 @@ void Transform::_setScaleXY(const double new_scale, const double xn, const doubl const double s = state.scale * util::tileSize; state.Bc = s / 360; state.Cc = s / util::M2PI; + + state.angle = angle; + state.pitch = pitch; view.notifyMapChange(MapChangeRegionDidChange); } else { view.notifyMapChange(MapChangeRegionWillChangeAnimated); const double startS = state.scale; + const double startA = state.angle; + const double startP = state.pitch; const double startX = state.x; const double startY = state.y; state.panning = true; state.scaling = true; + state.rotating = true; startTransition( + [=](double t) { + util::UnitBezier ease = options.easing ? *options.easing : util::UnitBezier(0, 0, 0.25, 1); + return ease.solve(t, 0.001); + }, [=](double t) { state.scale = util::interpolate(startS, scale, t); state.x = util::interpolate(startX, x, t); @@ -240,14 +256,17 @@ void Transform::_setScaleXY(const double new_scale, const double xn, const doubl const double s = state.scale * util::tileSize; state.Bc = s / 360; state.Cc = s / util::M2PI; + state.angle = util::wrap(util::interpolate(startA, angle, t), -M_PI, M_PI); + state.pitch = util::interpolate(startP, pitch, t); view.notifyMapChange(MapChangeRegionIsChanging); - return Update::Zoom; + return update; }, [=] { state.panning = false; state.scaling = false; + state.rotating = false; view.notifyMapChange(MapChangeRegionDidChangeAnimated); - }, duration); + }, *options.duration); } } @@ -307,7 +326,7 @@ void Transform::setAngle(const double new_angle, const double cx, const double c _moveBy(dx, dy, Duration::zero()); } - _setAngle(new_angle, Duration::zero()); + _setAngle(new_angle); if (cx >= 0 && cy >= 0) { _moveBy(-dx, -dy, Duration::zero()); @@ -315,32 +334,10 @@ void Transform::setAngle(const double new_angle, const double cx, const double c } void Transform::_setAngle(double new_angle, const Duration& duration) { - double angle = _normalizeAngle(new_angle, state.angle); - state.angle = _normalizeAngle(state.angle, angle); - - if (duration == Duration::zero()) { - view.notifyMapChange(MapChangeRegionWillChange); - - state.angle = angle; - - view.notifyMapChange(MapChangeRegionDidChange); - } else { - view.notifyMapChange(MapChangeRegionWillChangeAnimated); - - const double startA = state.angle; - state.rotating = true; - - startTransition( - [=](double t) { - state.angle = util::wrap(util::interpolate(startA, angle, t), -M_PI, M_PI); - view.notifyMapChange(MapChangeRegionIsChanging); - return Update::Repaint; - }, - [=] { - state.rotating = false; - view.notifyMapChange(MapChangeRegionDidChangeAnimated); - }, duration); - } + CameraOptions options; + options.angle = new_angle; + options.duration = duration; + easeTo(options); } double Transform::getAngle() const { @@ -349,8 +346,11 @@ double Transform::getAngle() const { #pragma mark - Pitch -void Transform::setPitch(double pitch) { - state.pitch = pitch; +void Transform::setPitch(double pitch, const Duration& duration) { + CameraOptions options; + options.pitch = pitch; + options.duration = duration; + easeTo(options); } double Transform::getPitch() const { @@ -359,7 +359,8 @@ double Transform::getPitch() const { #pragma mark - Transition -void Transform::startTransition(std::function frame, +void Transform::startTransition(std::function easing, + std::function frame, std::function finish, const Duration& duration) { if (transitionFinishFn) { @@ -369,7 +370,7 @@ void Transform::startTransition(std::function frame, transitionStart = Clock::now(); transitionDuration = duration; - transitionFrameFn = [frame, this](const TimePoint now) { + transitionFrameFn = [easing, frame, this](const TimePoint now) { float t = std::chrono::duration(now - transitionStart) / transitionDuration; if (t >= 1.0) { Update result = frame(1.0); @@ -378,8 +379,7 @@ void Transform::startTransition(std::function frame, transitionFinishFn = nullptr; return result; } else { - util::UnitBezier ease(0, 0, 0.25, 1); - return frame(ease.solve(t, 0.001)); + return frame(easing(t)); } }; diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 16719834495..56001fad81d 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -2,6 +2,7 @@ #define MBGL_MAP_TRANSFORM #include +#include #include #include #include @@ -22,6 +23,9 @@ class Transform : private util::noncopyable { // Map view bool resize(std::array size); + void jumpTo(const CameraOptions options); + void easeTo(const CameraOptions options); + // Position void moveBy(double dx, double dy, const Duration& = Duration::zero()); void setLatLng(LatLng latLng, const Duration& = Duration::zero()); @@ -42,7 +46,7 @@ class Transform : private util::noncopyable { double getAngle() const; // Pitch - void setPitch(double pitch); + void setPitch(double pitch, const Duration& = Duration::zero()); double getPitch() const; // Transitions @@ -60,13 +64,16 @@ class Transform : private util::noncopyable { void _moveBy(double dx, double dy, const Duration& = Duration::zero()); void _setScale(double scale, double cx, double cy, const Duration& = Duration::zero()); void _setScaleXY(double new_scale, double xn, double yn, const Duration& = Duration::zero()); + void _easeTo(CameraOptions options, const double new_scale, const double new_angle, + const double xn, const double yn); void _setAngle(double angle, const Duration& = Duration::zero()); View &view; TransformState state; - void startTransition(std::function frame, + void startTransition(std::function easing, + std::function frame, std::function finish, const Duration& duration);