diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index 0f709d69496..4e6b2b8987a 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 fc70f3d9041..46c923c4092 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; @@ -135,6 +137,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. */ @@ -183,17 +187,22 @@ IB_DESIGNABLE /** 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; +@property (nonatomic) CGFloat pitch; /** 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; +* @param pitch The pitch of the map (measured in degrees) relative to top-down. +* @param animated Specify `YES` if you want the map view to animate the change to the new pitch or `NO` if you want the map to display the new pitch immediately. +* +* Changing the pitch tilts the map without changing the current center coordinate or zoom level. */ +- (void)setPitch:(CGFloat)pitch animated:(BOOL)animated; /** Resets the map pitch to head-on. */ - (IBAction)resetPitch; +@property (nonatomic, copy) MGLMapCamera *camera; + +- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated; + #pragma mark - Converting Map Coordinates /** @name 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..e2c053ce264 --- /dev/null +++ b/include/mbgl/map/camera.hpp @@ -0,0 +1,23 @@ +#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 bd8847d420f..a8310ffe105 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 @@ -97,6 +98,9 @@ class Map : private util::noncopyable { void cancelTransitions(); void setGestureInProgress(bool); + 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()); @@ -110,8 +114,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; @@ -124,7 +128,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/platform/ios/MGLMapCamera.mm b/platform/ios/MGLMapCamera.mm new file mode 100644 index 00000000000..c877e3ebb52 --- /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::sqrt(std::pow(centerMeters.northing - eyeMeters.northing, 2) + + std::pow(centerMeters.easting - eyeMeters.easting, 2)); + 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 decodeObjectForKey:@"centerLatitude"] doubleValue], + [[coder decodeObjectForKey:@"centerLongitude"] doubleValue]); + _altitude = [[coder decodeObjectForKey:@"altitude"] doubleValue]; + _pitch = [[coder decodeObjectForKey:@"pitch"] doubleValue]; + _heading = [[coder decodeObjectForKey:@"heading"] doubleValue]; + } + 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 4f3e9030744..875fac66632 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"; @@ -368,7 +371,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; @@ -1349,12 +1355,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); } @@ -1532,7 +1536,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 @@ -1544,11 +1548,14 @@ - (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)); + mbgl::CameraOptions options; + options.center = MGLLatLngFromLocationCoordinate2D(coordinate); + options.zoom = fmaxf(_mbglMap->getZoom(), self.currentMinimumZoom); + if (animated) + { + options.duration = secondsAsDuration(MGLAnimationDuration); + } + _mbglMap->easeTo(options); [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; } @@ -1564,18 +1571,42 @@ - (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 = zoomLevel; + if (direction >= 0) + { + options.angle = MGLRadiansFromDegrees(-direction); + } + if (animated) + { + options.duration = secondsAsDuration(MGLAnimationDuration); + } + _mbglMap->easeTo(options); [self unrotateIfNeededAnimated:animated]; [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; } ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingZoomLevel +{ + return [NSSet setWithObject:@"camera"]; +} + - (double)zoomLevel { return _mbglMap->getZoom(); @@ -1585,11 +1616,13 @@ - (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated { self.userTrackingMode = MGLUserTrackingModeNone; - CGFloat duration = (animated ? MGLAnimationDuration : 0); - - _mbglMap->setLatLngZoom(_mbglMap->getLatLng(), - fmaxf(zoomLevel, self.currentMinimumZoom), - secondsAsDuration(duration)); + mbgl::CameraOptions options; + options.zoom = fmaxf(zoomLevel, self.currentMinimumZoom); + if (animated) + { + options.duration = secondsAsDuration(MGLAnimationDuration); + } + _mbglMap->easeTo(options); [self unrotateIfNeededAnimated:animated]; @@ -1641,7 +1674,27 @@ - (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 { // NOTE: does not disrupt tracking mode CGFloat duration = animated ? MGLAnimationDuration : 0; @@ -1654,7 +1707,13 @@ - (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); + } + options.duration = secondsAsDuration(duration); + _mbglMap->easeTo(options); [self didChangeValueForKey:@"visibleCoordinateBounds"]; [self unrotateIfNeededAnimated:animated]; @@ -1662,6 +1721,11 @@ - (void)setVisibleCoordinates:(CLLocationCoordinate2D *)coordinates count:(NSUIn [self notifyMapChange:(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; } ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingDirection +{ + return [NSSet setWithObject:@"camera"]; +} + - (CLLocationDirection)direction { return mbgl::util::wrap(_mbglMap->getBearing(), 0., 360.); @@ -1685,23 +1749,120 @@ - (void)setDirection:(CLLocationDirection)direction [self setDirection:direction animated:NO]; } -- (double)pitch ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingPitch +{ + return [NSSet setWithObject:@"camera"]; +} + +- (CGFloat)pitch { return _mbglMap->getPitch(); } -- (void)setPitch:(double)pitch +- (void)setPitch:(CGFloat)pitch +{ + [self setPitch:pitch animated:NO]; +} + +- (void)setPitch:(CGFloat)pitch animated:(BOOL)animated { // constrain pitch to between 0º and 60º // - _mbglMap->setPitch(fmax(fmin(pitch, 60), 0)); + CGFloat duration = animated ? MGLAnimationDuration : 0; + _mbglMap->setPitch(mbgl::util::clamp(pitch, MGLMinimumPitch, MGLMaximumPitch), secondsAsDuration(duration)); //[self notifyMapChange:(mbgl::MapChangeRegionDidChange)]; } - (void)resetPitch { - [self setPitch:0]; + [self setPitch:0 animated:YES]; +} + +- (MGLMapCamera *)camera +{ + CGSize size = self.bounds.size; + mbgl::ProjectedMeters topLeftMeters; + CGPoint opposite; + if (size.width > size.height) // landscape + { + CLLocationCoordinate2D centerLeft = [self convertPoint:CGPointMake(0, size.height / 2) toCoordinateFromView:self]; + topLeftMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(centerLeft)); + opposite = CGPointMake(size.width, 0); + } + else // portrait + { + CLLocationCoordinate2D topCenter = [self convertPoint:CGPointMake(size.width / 2, 0) toCoordinateFromView:self]; + topLeftMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(topCenter)); + opposite = CGPointMake(0, size.height); + } + CLLocationCoordinate2D oppositeCoordinate = [self convertPoint:opposite toCoordinateFromView:self]; + mbgl::ProjectedMeters oppositeMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(oppositeCoordinate)); + CLLocationDistance distance = std::hypot(oppositeMeters.easting - topLeftMeters.easting, + oppositeMeters.northing - topLeftMeters.northing); + CLLocationDistance altitude = distance / std::tan(MGLAngularFieldOfView / 2.) * 0.5; + + CGFloat pitch = MGLDegreesFromRadians(_mbglMap->getPitch()); + + return [MGLMapCamera cameraLookingAtCenterCoordinate:self.centerCoordinate + fromDistance:altitude + pitch:pitch + heading:self.direction]; +} + ++ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCamera +{ + return [NSSet setWithObjects:@"longitude", @"latitude", @"centerCoordinate", @"zoomLevel", @"direction", @"pitch", nil]; +} + +- (void)setCamera:(MGLMapCamera *)camera +{ + [self setCamera:camera animated:NO]; +} + +- (void)setCamera:(MGLMapCamera *)camera animated:(BOOL)animated +{ + CGSize size = self.bounds.size; + mbgl::ProjectedMeters centerMeters = _mbglMap->projectedMetersForLatLng(MGLLatLngFromLocationCoordinate2D(camera.centerCoordinate)); + CLLocationDistance distance = camera.altitude * std::tan(MGLAngularFieldOfView / 2.); + mbgl::LatLng sw, ne; + if (size.width > size.height) // landscape + { + sw = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing, + centerMeters.easting - distance, + }); + ne = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing, + centerMeters.easting + distance, + }); + } + else // portrait + { + sw = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing - distance, + centerMeters.easting, + }); + ne = _mbglMap->latLngForProjectedMeters({ + centerMeters.northing + distance, + centerMeters.easting, + }); + } + mbgl::CameraOptions options = _mbglMap->cameraForLatLngBounds({ sw, ne }, {}); + + if (camera.heading >= 0) + { + options.angle = MGLRadiansFromDegrees(-camera.heading); + } + if (camera.pitch >= 0) + { + options.pitch = MGLRadiansFromDegrees(camera.pitch); + } + if (animated) + { + options.duration = secondsAsDuration(MGLAnimationDuration); + } + _mbglMap->easeTo(options); } - (CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(nullable UIView *)view @@ -2454,6 +2615,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) { @@ -2468,7 +2635,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 { @@ -2498,17 +2665,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; @@ -3055,7 +3216,7 @@ @implementation MGLMapView (IBAdditions) + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingLatitude { - return [NSSet setWithObject:@"centerCoordinate"]; + return [NSSet setWithObjects:@"centerCoordinate", @"camera", nil]; } - (double)latitude @@ -3081,7 +3242,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 065c33ce5f3..8e1d2e02654 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -9,6 +9,7 @@ #include #include +#include namespace mbgl { @@ -115,6 +116,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) { @@ -123,7 +136,9 @@ void Map::moveBy(double dx, double dy, const Duration& duration) { } void Map::setLatLng(LatLng latLng, const Duration& duration) { - transform->setLatLng(latLng, duration); + CameraOptions options; + options.duration = duration; + transform->setLatLng(latLng, options); update(Update::Repaint); } @@ -132,9 +147,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); } @@ -165,29 +182,32 @@ double Map::getZoom() const { } void Map::setLatLngZoom(LatLng latLng, double zoom, const Duration& duration) { - transform->setLatLngZoom(latLng, zoom, duration); + CameraOptions options; + options.duration = duration; + transform->setLatLngZoom(latLng, zoom, options); 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); @@ -215,7 +235,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() { @@ -250,7 +272,9 @@ void Map::rotateBy(double sx, double sy, double ex, double ey, const Duration& d } void Map::setBearing(double degrees, const Duration& duration) { - transform->setAngle(-degrees * M_PI / 180, duration); + CameraOptions options; + options.duration = duration; + transform->setAngle(-degrees * M_PI / 180, options); update(Update::Repaint); } @@ -264,15 +288,19 @@ double Map::getBearing() const { } void Map::resetNorth() { - transform->setAngle(0, std::chrono::milliseconds(500)); + CameraOptions options; + options.duration = std::chrono::milliseconds(500); + transform->setAngle(0, options); update(Update::Repaint); } #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) { + CameraOptions options; + options.duration = duration; + transform->setPitch(util::clamp(pitch, 0., 90.) * M_PI / 180, options); update(Update::Repaint); } diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 7ccc2ad4cc9..66a1abc809f 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -53,6 +54,35 @@ 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(const 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)); + + _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 +92,26 @@ 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); +void Transform::setLatLng(const LatLng latLng, CameraOptions options) { + options.center = latLng; + 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); +void Transform::setLatLngZoom(const LatLng latLng, const double zoom, CameraOptions options) { + options.center = latLng; + options.zoom = zoom; + easeTo(options); } @@ -206,13 +191,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 +220,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(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 +249,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); } } @@ -283,15 +295,17 @@ void Transform::rotateBy(const double start_x, const double start_y, const doubl const double ang = state.angle + util::angle_between(first_x, first_y, second_x, second_y); - _setAngle(ang, duration); + CameraOptions options; + options.duration = duration; + _setAngle(ang, options); } -void Transform::setAngle(const double new_angle, const Duration& duration) { +void Transform::setAngle(const double new_angle, CameraOptions options) { if (std::isnan(new_angle)) { return; } - _setAngle(new_angle, duration); + _setAngle(new_angle, options); } void Transform::setAngle(const double new_angle, const double cx, const double cy) { @@ -307,40 +321,17 @@ void Transform::setAngle(const double new_angle, const double cx, const double c _moveBy(dx, dy, Duration::zero()); } - _setAngle(new_angle, Duration::zero()); + CameraOptions options; + _setAngle(new_angle, options); if (cx >= 0 && cy >= 0) { _moveBy(-dx, -dy, Duration::zero()); } } -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); - } +void Transform::_setAngle(double new_angle, CameraOptions options) { + options.angle = new_angle; + easeTo(options); } double Transform::getAngle() const { @@ -349,8 +340,9 @@ double Transform::getAngle() const { #pragma mark - Pitch -void Transform::setPitch(double pitch) { - state.pitch = pitch; +void Transform::setPitch(double pitch, CameraOptions options) { + options.pitch = pitch; + easeTo(options); } double Transform::getPitch() const { @@ -359,7 +351,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 +362,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 +371,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 5d5a72d6d32..00ea259c014 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,10 +23,13 @@ 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()); - void setLatLngZoom(LatLng latLng, double zoom, const Duration& = Duration::zero()); + void setLatLng(LatLng latLng, CameraOptions options); + void setLatLngZoom(LatLng latLng, double zoom, CameraOptions options); inline const LatLng getLatLng() const { return state.getLatLng(); } // Zoom @@ -37,12 +41,12 @@ class Transform : private util::noncopyable { // Angle void rotateBy(double sx, double sy, double ex, double ey, const Duration& = Duration::zero()); - void setAngle(double angle, const Duration& = Duration::zero()); + void setAngle(double angle, CameraOptions options); void setAngle(double angle, double cx, double cy); double getAngle() const; // Pitch - void setPitch(double pitch); + void setPitch(double pitch, CameraOptions options); double getPitch() const; // Transitions @@ -59,13 +63,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 _setAngle(double angle, 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, CameraOptions options); View &view; TransformState state; - void startTransition(std::function frame, + void startTransition(std::function easing, + std::function frame, std::function finish, const Duration& duration);