[core] Refined and commented flyTo
Rewrote the flyTo implementation to more closely match GL JS’s implementation and the paper on which it is based. Rewrote CameraOptions documentation. Only document units for generic types like double. The semantics of LatLng and Duration are already baked into the types; one just needs to look up the types’ definitions. Also, the […) is set notation, so the braces are supposed to be mismatched. Fixes #3296.
This commit is contained in:
committed by
Minh Nguyễn
parent
925687ab06
commit
a68589b6c7
@@ -11,16 +11,53 @@
|
||||
|
||||
namespace mbgl {
|
||||
|
||||
/** Various options for describing the viewpoint of a map, along with parameters
|
||||
for transitioning to the viewpoint with animation. All fields are optional;
|
||||
the default values of transition options depend on how this struct is used.
|
||||
*/
|
||||
struct CameraOptions {
|
||||
mapbox::util::optional<LatLng> center; // Map center (Degrees)
|
||||
mapbox::util::optional<double> zoom; // Map zoom level Positive Numbers > 0 and < 18
|
||||
mapbox::util::optional<double> angle; // Map rotation bearing in Radians counter-clockwise from north. The value is wrapped to [−π rad, π rad]
|
||||
mapbox::util::optional<double> pitch; // Map angle in degrees at which the camera is looking to ground (Radians)
|
||||
mapbox::util::optional<Duration> duration; // Animation time length (Nanoseconds)
|
||||
// Viewpoint options
|
||||
|
||||
/** Coordinate at the center of the map. */
|
||||
mapbox::util::optional<LatLng> center;
|
||||
|
||||
/** Zero-based zoom level. Constrained to the minimum and maximum zoom
|
||||
levels. */
|
||||
mapbox::util::optional<double> zoom;
|
||||
|
||||
/** Bearing, measured in radians counterclockwise from true north. Wrapped
|
||||
to [−π rad, π rad). */
|
||||
mapbox::util::optional<double> angle;
|
||||
|
||||
/** Pitch toward the horizon measured in radians, with 0 rad resulting in a
|
||||
two-dimensional map. */
|
||||
mapbox::util::optional<double> pitch;
|
||||
|
||||
// Transition options
|
||||
|
||||
/** Time to animate to the viewpoint defined herein. */
|
||||
mapbox::util::optional<Duration> duration;
|
||||
|
||||
/** Average velocity of a flyTo() transition, measured in distance units per
|
||||
second. */
|
||||
mapbox::util::optional<double> speed;
|
||||
|
||||
/** The relative amount of zooming that takes place along the flight path of
|
||||
a flyTo() transition. A high value maximizes zooming for an exaggerated
|
||||
animation, while a low value minimizes zooming for something closer to
|
||||
easeTo(). */
|
||||
mapbox::util::optional<double> curve;
|
||||
|
||||
/** The easing timing curve of the transition. */
|
||||
mapbox::util::optional<mbgl::util::UnitBezier> easing;
|
||||
|
||||
/** A function that is called on each frame of the transition, just before a
|
||||
screen update, except on the last frame. The first parameter indicates
|
||||
the elapsed time as a percentage of the duration. */
|
||||
std::function<void(double)> transitionFrameFn;
|
||||
|
||||
/** A function that is called once on the last frame of the transition, just
|
||||
before the corresponding screen update. */
|
||||
std::function<void()> transitionFinishFn;
|
||||
};
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ void Transform::easeTo(const CameraOptions& options) {
|
||||
return;
|
||||
}
|
||||
|
||||
double new_scale = std::pow(2.0, zoom);
|
||||
double new_scale = state.zoomScale(zoom);
|
||||
|
||||
const double s = new_scale * util::tileSize;
|
||||
state.Bc = s / 360;
|
||||
@@ -148,9 +148,9 @@ void Transform::setLatLng(const LatLng& latLng, const PrecisionPoint& point, con
|
||||
float rowDiff = coordAtPoint.row - coord.row;
|
||||
|
||||
auto newLatLng = state.coordinateToLatLng({
|
||||
coordCenter.column - columnDiff,
|
||||
coordCenter.row - rowDiff,
|
||||
coordCenter.zoom
|
||||
coordCenter.column - columnDiff,
|
||||
coordCenter.row - rowDiff,
|
||||
coordCenter.zoom
|
||||
});
|
||||
|
||||
setLatLng(newLatLng, duration);
|
||||
@@ -324,11 +324,15 @@ void Transform::_easeTo(const CameraOptions& options, double new_scale, double n
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flying animation to a specified location/zoom/bearing with automatic curve.
|
||||
*/
|
||||
/** This method implements an “optimal path” animation, as detailed in:
|
||||
|
||||
Van Wijk, Jarke J.; Nuij, Wim A. A. “Smooth and efficient zooming and
|
||||
panning.” INFOVIS ’03. pp. 15–22.
|
||||
<https://www.win.tue.nl/~vanwijk/zoompan.pdf#page=5>.
|
||||
|
||||
Where applicable, local variable documentation begins with the associated
|
||||
variable or function in van Wijk (2003). */
|
||||
void Transform::flyTo(const CameraOptions &options) {
|
||||
|
||||
CameraOptions flyOptions(options);
|
||||
LatLng latLng = options.center ? *options.center : getLatLng();
|
||||
LatLng startLatLng = getLatLng();
|
||||
@@ -340,101 +344,126 @@ void Transform::flyTo(const CameraOptions &options) {
|
||||
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 PrecisionPoint startPoint = {
|
||||
state.lngX(startLatLng.longitude),
|
||||
state.latY(startLatLng.latitude),
|
||||
};
|
||||
const PrecisionPoint endPoint = {
|
||||
state.lngX(latLng.longitude),
|
||||
state.latY(latLng.latitude),
|
||||
};
|
||||
|
||||
const double m = 1 - 1e-15;
|
||||
const double f = ::fmin(::fmax(std::sin(util::DEG2RAD * latLng.latitude), -m), m);
|
||||
// Minimize rotation by taking the shorter path around the circle.
|
||||
double normalizedAngle = _normalizeAngle(angle, state.angle);
|
||||
state.angle = _normalizeAngle(state.angle, normalizedAngle);
|
||||
|
||||
double xn = -latLng.longitude * state.Bc;
|
||||
double yn = 0.5 * state.Cc * std::log((1 + f) / (1 - f));
|
||||
const double startZoom = state.scaleZoom(state.scale);
|
||||
const double startAngle = state.angle;
|
||||
const double startPitch = state.pitch;
|
||||
|
||||
view.notifyMapChange(MapChangeRegionWillChangeAnimated);
|
||||
|
||||
const double startZ = state.scaleZoom(state.scale);
|
||||
const double startA = state.angle;
|
||||
const double startP = state.pitch;
|
||||
state.panning = true;
|
||||
state.scaling = true;
|
||||
state.rotating = true;
|
||||
|
||||
/** ρ: The relative amount of zooming that takes place along the flight
|
||||
path. A high value maximizes zooming for an exaggerated animation, while
|
||||
a low value minimizes zooming for something closer to easeTo().
|
||||
|
||||
1.42 is the average value selected by participants in the user study in
|
||||
van Wijk (2003). A value of 6<sup>¼</sup> would be equivalent to the
|
||||
root mean squared average velocity, V<sub>RMS</sub>. */
|
||||
double rho = flyOptions.curve ? *flyOptions.curve : 1.42;
|
||||
/// w₀: Initial visible range.
|
||||
double w0 = std::max(state.width, state.height);
|
||||
double w1 = w0 / new_scale;
|
||||
double u1 = ::hypot(xn, yn);
|
||||
/// w₁: Target visible range.
|
||||
double w1 = w0 / state.zoomScale(zoom - startZoom);
|
||||
/// Length of the flight path as projected onto the ground plane.
|
||||
double u1 = ::hypot((endPoint - startPoint).x, (endPoint - startPoint).y);
|
||||
/// ρ²
|
||||
double rho2 = rho * rho;
|
||||
|
||||
/// rᵢ
|
||||
auto r = [=](double i) {
|
||||
/// bᵢ
|
||||
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) {
|
||||
// When u₀ = u₁, the optimal path doesn’t require both ascent and descent.
|
||||
bool isClose = std::abs(u1) < 0.000001;
|
||||
// Bail if the path is too short.
|
||||
if (isClose && std::abs(w0 - w1) < 0.000001) {
|
||||
return;
|
||||
}
|
||||
|
||||
/// r₀: rᵢ where i = 0.
|
||||
double r0 = r(0);
|
||||
/** w(s): Visible range on the ground, proportional to the scale, measured
|
||||
in world coordinates.
|
||||
|
||||
Assumes an angular field of view of 2 arctan ½ ≈ 53°. */
|
||||
auto w = [=](double s) {
|
||||
return (is_close ? std::exp((w1 < w0 ? -1 : 1) * rho * s)
|
||||
return (isClose ? std::exp((w1 < w0 ? -1 : 1) * rho * s)
|
||||
: (std::cosh(r0) / std::cosh(r0 + rho * s)));
|
||||
};
|
||||
/// u(s): Distance along the flight path as projected onto the ground plane.
|
||||
auto u = [=](double s) {
|
||||
return (is_close ? 0.
|
||||
: (w0 * ((std::cosh(r0) * std::tanh(r0 + rho * s) - std::sinh(r0)) / rho2) / u1));
|
||||
return (isClose ? 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)
|
||||
/// S: Total length of the flight path.
|
||||
double S = (isClose ? (std::abs(std::log(w1 / w0)) / rho)
|
||||
: ((r(1) - r0) / rho));
|
||||
|
||||
Duration duration;
|
||||
if (flyOptions.duration) {
|
||||
duration = *flyOptions.duration;
|
||||
} else {
|
||||
double speed = flyOptions.speed ? *flyOptions.speed : 1.2;
|
||||
/// V: Average velocity.
|
||||
double velocity = flyOptions.speed ? *flyOptions.speed : 1.2;
|
||||
duration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
|
||||
std::chrono::duration<double, std::chrono::seconds::period>(S / speed));
|
||||
std::chrono::duration<double, std::chrono::seconds::period>(S / velocity));
|
||||
}
|
||||
if (duration == Duration::zero()) {
|
||||
// Atomic transition.
|
||||
jumpTo(options);
|
||||
return;
|
||||
}
|
||||
|
||||
view.notifyMapChange(MapChangeRegionWillChangeAnimated);
|
||||
|
||||
const double startWorldSize = state.worldSize();
|
||||
state.Bc = startWorldSize / 360;
|
||||
state.Cc = startWorldSize / util::M2PI;
|
||||
|
||||
state.panning = true;
|
||||
state.scaling = true;
|
||||
state.rotating = angle != startAngle;
|
||||
|
||||
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) {
|
||||
/// s: The distance traveled along the flight path.
|
||||
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;
|
||||
// Calculate the current point and zoom level along the flight path.
|
||||
PrecisionPoint framePoint = startPoint + (endPoint - startPoint) * us;
|
||||
double frameZoom = startZoom + state.scaleZoom(1 / w(s));
|
||||
|
||||
//Now calculate desired zoom
|
||||
double desiredZoom = startZ + state.scaleZoom(1 / w(s));
|
||||
double desiredScale = state.zoomScale(desiredZoom);
|
||||
state.scale = ::fmax(::fmin(desiredScale, state.max_scale), state.min_scale);
|
||||
// Convert to geographic coordinates and set the new viewpoint.
|
||||
LatLng frameLatLng = {
|
||||
state.yLat(framePoint.y, startWorldSize),
|
||||
state.xLng(framePoint.x, startWorldSize),
|
||||
};
|
||||
state.setLatLngZoom(frameLatLng, frameZoom);
|
||||
|
||||
//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 = ::fmin(::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);
|
||||
if (angle != startAngle) {
|
||||
state.angle = util::wrap(util::interpolate(startAngle, normalizedAngle, k), -M_PI, M_PI);
|
||||
}
|
||||
if (pitch != startP) {
|
||||
state.pitch = util::clamp(util::interpolate(startP, pitch, k), 0., 60.);
|
||||
if (pitch != startPitch) {
|
||||
state.pitch = util::clamp(util::interpolate(startPitch, pitch, k), 0., 60.);
|
||||
}
|
||||
|
||||
// At k = 1.0, a DidChangeAnimated notification should be sent from finish().
|
||||
if (k < 1.0) {
|
||||
if (options.transitionFrameFn) {
|
||||
|
||||
@@ -26,6 +26,8 @@ public:
|
||||
|
||||
void jumpTo(const CameraOptions&);
|
||||
void easeTo(const CameraOptions&);
|
||||
/** Smoothly zoom out, pan, and zoom back into the given camera along a
|
||||
great circle, as though the viewer is aboard a supersonic jetcopter. */
|
||||
void flyTo(const CameraOptions&);
|
||||
|
||||
// Position
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <mbgl/util/box.hpp>
|
||||
#include <mbgl/util/tile_coordinate.hpp>
|
||||
#include <mbgl/util/interpolate.hpp>
|
||||
#include <mbgl/util/math.hpp>
|
||||
|
||||
using namespace mbgl;
|
||||
|
||||
@@ -370,4 +371,32 @@ void TransformState::constrain(double& scale_, double& x_, double& y_) const {
|
||||
y_ = std::max(-max_y, std::min(y_, max_y));
|
||||
}
|
||||
|
||||
void TransformState::setLatLngZoom(const LatLng &latLng, double zoom) {
|
||||
double newScale = zoomScale(zoom);
|
||||
const double newWorldSize = newScale * util::tileSize;
|
||||
Bc = newWorldSize / 360;
|
||||
Cc = newWorldSize / util::M2PI;
|
||||
|
||||
const double m = 1 - 1e-15;
|
||||
const double f = util::clamp(std::sin(util::DEG2RAD * latLng.latitude), -m, m);
|
||||
|
||||
PrecisionPoint point = {
|
||||
-latLng.longitude * Bc,
|
||||
0.5 * Cc * std::log((1 + f) / (1 - f)),
|
||||
};
|
||||
setScalePoint(newScale, point);
|
||||
}
|
||||
|
||||
void TransformState::setScalePoint(const double newScale, const PrecisionPoint &point) {
|
||||
double constrainedScale = newScale;
|
||||
PrecisionPoint constrainedPoint = point;
|
||||
constrain(constrainedScale, constrainedPoint.x, constrainedPoint.y);
|
||||
|
||||
scale = constrainedScale;
|
||||
x = constrainedPoint.x;
|
||||
y = constrainedPoint.y;
|
||||
Bc = worldSize() / 360;
|
||||
Cc = worldSize() / util::M2PI;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -100,6 +100,9 @@ private:
|
||||
|
||||
mat4 coordinatePointMatrix(double z) const;
|
||||
mat4 getPixelMatrix() const;
|
||||
|
||||
void setLatLngZoom(const LatLng &latLng, double zoom);
|
||||
void setScalePoint(const double scale, const PrecisionPoint &point);
|
||||
|
||||
private:
|
||||
ConstrainMode constrainMode;
|
||||
|
||||
Reference in New Issue
Block a user