diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index 010b16d5e91..2a5e649fef8 100644 --- a/include/mbgl/ios/MGLMapView.h +++ b/include/mbgl/ios/MGLMapView.h @@ -218,6 +218,14 @@ IB_DESIGNABLE * @param completion The block to execute after the animation finishes. */ - (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion; +#pragma mark - flyTo + +/** Uses a ballistic parabolic motion to "fly" the viewpoint to a different location with respect to the map with an optional transition duration. + * @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 completion The block to execute after the animation finishes. */ +- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion; + #pragma mark - Converting Map Coordinates /** @name Converting Map Coordinates */ diff --git a/include/mbgl/map/map.hpp b/include/mbgl/map/map.hpp index a6a6b5dede7..8057837546d 100644 --- a/include/mbgl/map/map.hpp +++ b/include/mbgl/map/map.hpp @@ -100,6 +100,7 @@ class Map : private util::noncopyable { // Camera void jumpTo(const CameraOptions&); void easeTo(const CameraOptions&); + void flyTo(const CameraOptions&); // Position void moveBy(const PrecisionPoint&, const Duration& = Duration::zero()); diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index 8e57fc4321c..f71f933c8e2 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -1850,6 +1850,16 @@ - (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration a } - (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion +{ + [self _setCamera:camera withDuration:duration animationTimingFunction:function completionHandler:completion useFly:NO]; +} + +- (void)flyToCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration completionHandler:(nullable void (^)(void))completion +{ + [self _setCamera:camera withDuration:duration animationTimingFunction:nil completionHandler:completion useFly:YES]; +} + +- (void)_setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion useFly:(BOOL)fly { _mbglMap->cancelTransitions(); @@ -1922,7 +1932,11 @@ - (void)setCamera:(MGLMapCamera *)camera withDuration:(NSTimeInterval)duration a }); }; } - _mbglMap->easeTo(options); + if (fly) { + _mbglMap->flyTo(options); + } else { + _mbglMap->easeTo(options); + } } - (CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(nullable UIView *)view diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index 0d6bdf9c7e8..1896e7d29bb 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -156,6 +156,12 @@ void Map::easeTo(const CameraOptions& options) { transform->easeTo(options); update(options.zoom ? Update::Zoom : Update::Repaint); } + + +void Map::flyTo(const CameraOptions& options) { + transform->flyTo(options); + update(options.zoom ? Update::Zoom : Update::Repaint); +} #pragma mark - Position diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index 7f0363c709c..8f5c1b0f587 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -313,6 +313,107 @@ void Transform::_easeTo(const CameraOptions& options, double new_scale, double n } } +void Transform::flyTo(const CameraOptions &options) { + CameraOptions flyOptions(options); + LatLng latLng = options.center ? *options.center : getLatLng(); + LatLng startLatLng = getLatLng(); + double zoom = flyOptions.zoom ? *flyOptions.zoom : getZoom(); + double angle = flyOptions.angle ? *flyOptions.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 scaled_tile_size = new_scale * util::tileSize; + state.Bc = scaled_tile_size / 360; + state.Cc = scaled_tile_size / 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)); + + view.notifyMapChange(MapChangeRegionWillChangeAnimated); + + const double startS = state.scale; + const double startA = state.angle; + state.panning = true; + state.scaling = true; + state.rotating = true; + + const double rho = 1.42; + double w0 = std::max(state.width, state.height); + double w1 = w0 / new_scale; + double u1 = std::hypot(xn, yn); + double rho2 = rho * rho; + + auto r = [=](double i) { + double b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1); + return std::log(std::sqrt(b * b + 1) - b); + }; + + bool is_close = std::abs(u1) < 0.000001; + if (is_close && std::abs(w0 - w1) < 0.000001) { + return; + } + + double r0 = r(0); + auto w = [=](double s) { + return (is_close ? std::exp((w1 < w0 ? -1 : 1) * rho * s) + : (std::cosh(r0) / std::cosh(r0 + rho * s))); + }; + auto u = [=](double s) { + return (is_close ? 0. + : (w0 * ((std::cosh(r0) * std::tanh(r0 + rho * s) - std::sinh(r0)) / rho2) / u1)); + }; + double S = (is_close ? (std::abs(std::log(w1 / w0)) / rho) + : ((r(1) - r0) / rho)); + + if (!flyOptions.duration) { + flyOptions.duration = Duration::zero(); + } + startTransition( + [=](double t) { + util::UnitBezier ease = flyOptions.easing ? *flyOptions.easing : util::UnitBezier(0, 0, 0.25, 1); + return ease.solve(t, 0.001); + }, + [=](double k) { + double s = k * S; + double us = u(s); + + //First calculate the desired latlng + double desiredLat = startLatLng.latitude + (latLng.latitude - startLatLng.latitude) * us; + double desiredLng = startLatLng.longitude + (latLng.longitude - startLatLng.longitude) * us; + + //Now calculate desired zoom + state.scale = startS - w(s); + + //Now set values + const double new_scaled_tile_size = state.scale * util::tileSize; + state.Bc = new_scaled_tile_size / 360; + state.Cc = new_scaled_tile_size / util::M2PI; + + const double f2 = std::fmin(std::fmax(std::sin(util::DEG2RAD * desiredLat), -m), m); + state.x = -desiredLng * state.Bc; + state.y = 0.5 * state.Cc * std::log((1 + f2) / (1 - f2)); + + if (angle != startA) { + state.angle = util::wrap(util::interpolate(startA, angle, k), -M_PI, M_PI); + } + + view.notifyMapChange(MapChangeRegionIsChanging); + return Update::Zoom; + }, + [=] { + state.panning = false; + state.scaling = false; + state.rotating = false; + view.notifyMapChange(MapChangeRegionDidChangeAnimated); + }, *flyOptions.duration); +}; + #pragma mark - Angle void Transform::rotateBy(const PrecisionPoint& first, const PrecisionPoint& second, const Duration& duration) { diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 914fa164da7..391b8c8d653 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -26,6 +26,7 @@ class Transform : private util::noncopyable { void jumpTo(const CameraOptions&); void easeTo(const CameraOptions&); + void flyTo(const CameraOptions&); // Position void moveBy(const PrecisionPoint&, const Duration& = Duration::zero());