Files
MapLibreParticleLab/src/mbgl/map/transform.cpp
2019-08-08 12:14:48 +03:00

598 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <mbgl/map/camera.hpp>
#include <mbgl/map/transform.hpp>
#include <mbgl/util/constants.hpp>
#include <mbgl/util/mat4.hpp>
#include <mbgl/util/math.hpp>
#include <mbgl/util/unitbezier.hpp>
#include <mbgl/util/interpolate.hpp>
#include <mbgl/util/chrono.hpp>
#include <mbgl/util/projection.hpp>
#include <mbgl/math/clamp.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/platform.hpp>
#include <cstdio>
#include <cmath>
namespace mbgl {
/** Converts the given angle (in radians) to be numerically close to the anchor angle, allowing it to be interpolated properly without sudden jumps. */
static double _normalizeAngle(double angle, double anchorAngle)
{
if (std::isnan(angle) || std::isnan(anchorAngle)) {
return 0;
}
angle = util::wrap(angle, -M_PI, M_PI);
if (angle == -M_PI) angle = M_PI;
double diff = std::abs(angle - anchorAngle);
if (std::abs(angle - util::M2PI - anchorAngle) < diff) {
angle -= util::M2PI;
}
if (std::abs(angle + util::M2PI - anchorAngle) < diff) {
angle += util::M2PI;
}
return angle;
}
Transform::Transform(MapObserver& observer_,
ConstrainMode constrainMode,
ViewportMode viewportMode)
: observer(observer_), state(constrainMode, viewportMode) {
}
#pragma mark - Map View
void Transform::resize(const Size size) {
if (size.isEmpty()) {
throw std::runtime_error("failed to resize: size is empty");
}
if (state.size == size) {
return;
}
observer.onCameraWillChange(MapObserver::CameraChangeMode::Immediate);
state.size = size;
state.constrain(state.scale, state.x, state.y);
observer.onCameraDidChange(MapObserver::CameraChangeMode::Immediate);
}
#pragma mark - Camera
CameraOptions Transform::getCameraOptions(const EdgeInsets& padding) const {
return state.getCameraOptions(padding);
}
/**
* Change any combination of center, zoom, bearing, and pitch, without
* a transition. The map will retain the current values for any options
* not included in `options`.
*/
void Transform::jumpTo(const CameraOptions& camera) {
easeTo(camera);
}
/**
* Change any combination of center, zoom, bearing, pitch and edgeInsets, with a
* smooth animation between old and new values. The map will retain the current
* values for any options not included in `options`.
*/
void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& animation) {
const EdgeInsets& padding = camera.padding.value_or(state.edgeInsets);
LatLng startLatLng = getLatLng(LatLng::Unwrapped);
const LatLng& unwrappedLatLng = camera.center.value_or(startLatLng);
const LatLng& latLng = state.bounds != LatLngBounds::unbounded() ? unwrappedLatLng : unwrappedLatLng.wrapped();
double zoom = camera.zoom.value_or(getZoom());
double bearing = camera.bearing ? -*camera.bearing * util::DEG2RAD : getBearing();
double pitch = camera.pitch ? *camera.pitch * util::DEG2RAD : getPitch();
if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch)) {
return;
}
if (state.bounds == LatLngBounds::unbounded()) {
if (isGestureInProgress()) {
// If gesture in progress, we transfer the wrap rounds from the end longitude into
// start, so the "scroll effect" of rounding the world is the same while assuring the
// end longitude remains wrapped.
const double wrap = unwrappedLatLng.longitude() - latLng.longitude();
startLatLng = LatLng(startLatLng.latitude(), startLatLng.longitude() - wrap);
} else {
// Find the shortest path otherwise.
startLatLng.unwrapForShortestPath(latLng);
}
}
const Point<double> startPoint = Projection::project(startLatLng, state.scale);
const Point<double> endPoint = Projection::project(latLng, state.scale);
// Constrain camera options.
zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
pitch = util::clamp(pitch, util::PITCH_MIN, util::PITCH_MAX);
// Minimize rotation by taking the shorter path around the circle.
bearing = _normalizeAngle(bearing, state.bearing);
state.bearing = _normalizeAngle(state.bearing, bearing);
Duration duration = animation.duration ? *animation.duration : Duration::zero();
const double startZoom = state.getZoom();
const double startBearing = state.bearing;
const double startPitch = state.pitch;
state.panning = unwrappedLatLng != startLatLng;
state.scaling = zoom != startZoom;
state.rotating = bearing != startBearing;
const EdgeInsets startEdgeInsets = state.edgeInsets;
startTransition(camera, animation, [=](double t) {
Point<double> framePoint = util::interpolate(startPoint, endPoint, t);
LatLng frameLatLng = Projection::unproject(framePoint, state.zoomScale(startZoom));
double frameZoom = util::interpolate(startZoom, zoom, t);
state.setLatLngZoom(frameLatLng, frameZoom);
if (bearing != startBearing) {
state.bearing = util::wrap(util::interpolate(startBearing, bearing, t), -M_PI, M_PI);
}
if (padding != startEdgeInsets) {
// Interpolate edge insets
state.edgeInsets = {
util::interpolate(startEdgeInsets.top(), padding.top(), t),
util::interpolate(startEdgeInsets.left(), padding.left(), t),
util::interpolate(startEdgeInsets.bottom(), padding.bottom(), t),
util::interpolate(startEdgeInsets.right(), padding.right(), t)
};
}
auto maxPitch = getMaxPitchForEdgeInsets(state.edgeInsets);
if (pitch != startPitch || maxPitch < startPitch) {
state.pitch = std::min(maxPitch, util::interpolate(startPitch, pitch, t));
}
}, duration);
}
/** 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 &camera, const AnimationOptions &animation) {
const EdgeInsets& padding = camera.padding.value_or(state.edgeInsets);
const LatLng& latLng = camera.center.value_or(getLatLng(LatLng::Unwrapped)).wrapped();
double zoom = camera.zoom.value_or(getZoom());
double bearing = camera.bearing ? -*camera.bearing * util::DEG2RAD : getBearing();
double pitch = camera.pitch ? *camera.pitch * util::DEG2RAD : getPitch();
if (std::isnan(zoom) || std::isnan(bearing) || std::isnan(pitch) || state.size.isEmpty()) {
return;
}
// Determine endpoints.
LatLng startLatLng = getLatLng(LatLng::Unwrapped).wrapped();
startLatLng.unwrapForShortestPath(latLng);
const Point<double> startPoint = Projection::project(startLatLng, state.scale);
const Point<double> endPoint = Projection::project(latLng, state.scale);
// Constrain camera options.
zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
pitch = util::clamp(pitch, util::PITCH_MIN, util::PITCH_MAX);
// Minimize rotation by taking the shorter path around the circle.
bearing = _normalizeAngle(bearing, state.bearing);
state.bearing = _normalizeAngle(state.bearing, bearing);
const double startZoom = state.scaleZoom(state.scale);
const double startBearing = state.bearing;
const double startPitch = state.pitch;
/// w₀: Initial visible span, measured in pixels at the initial scale.
/// Known henceforth as a <i>screenful</i>.
double w0 = std::max(state.size.width - padding.left() - padding.right(),
state.size.height - padding.top() - padding.bottom());
/// w₁: Final visible span, measured in pixels with respect to the initial
/// scale.
double w1 = w0 / state.zoomScale(zoom - startZoom);
/// Length of the flight path as projected onto the ground plane, measured
/// in pixels from the world image origin at the initial scale.
double u1 = ::hypot((endPoint - startPoint).x, (endPoint - startPoint).y);
/** ρ: 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>. A value of 1 would
produce a circular motion. */
double rho = 1.42;
if (animation.minZoom) {
double minZoom = util::min(*animation.minZoom, startZoom, zoom);
minZoom = util::clamp(minZoom, state.getMinZoom(), state.getMaxZoom());
/// w<sub>m</sub>: Maximum visible span, measured in pixels with respect
/// to the initial scale.
double wMax = w0 / state.zoomScale(minZoom - startZoom);
rho = std::sqrt(wMax / u1 * 2);
}
/// ρ²
double rho2 = rho * rho;
/** rᵢ: Returns the zoom-out factor at one end of the animation.
@param i 0 for the ascent or 1 for the descent. */
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);
};
/// r₀: Zoom-out factor during ascent.
double r0 = r(0);
double r1 = r(1);
// When u₀ = u₁, the optimal path doesn’t require both ascent and descent.
bool isClose = std::abs(u1) < 0.000001 || !std::isfinite(r0) || !std::isfinite(r1);
/** w(s): Returns the visible span on the ground, measured in pixels with
respect to the initial scale.
Assumes an angular field of view of 2 arctan ½ ≈ 53°. */
auto w = [=](double s) {
return (isClose ? std::exp((w1 < w0 ? -1 : 1) * rho * s)
: (std::cosh(r0) / std::cosh(r0 + rho * s)));
};
/// u(s): Returns the distance along the flight path as projected onto the
/// ground plane, measured in pixels from the world image origin at the
/// initial scale.
auto u = [=](double s) {
return (isClose ? 0.
: (w0 * (std::cosh(r0) * std::tanh(r0 + rho * s) - std::sinh(r0)) / rho2 / u1));
};
/// S: Total length of the flight path, measured in ρ-screenfuls.
double S = (isClose ? (std::abs(std::log(w1 / w0)) / rho)
: ((r1 - r0) / rho));
Duration duration;
if (animation.duration) {
duration = *animation.duration;
} else {
/// V: Average velocity, measured in ρ-screenfuls per second.
double velocity = 1.2;
if (animation.velocity) {
velocity = *animation.velocity / rho;
}
duration = std::chrono::duration_cast<Duration>(std::chrono::duration<double>(S / velocity));
}
if (duration == Duration::zero()) {
// Perform an instantaneous transition.
jumpTo(camera);
return;
}
const double startScale = state.scale;
state.panning = true;
state.scaling = true;
state.rotating = bearing != startBearing;
const EdgeInsets startEdgeInsets = state.edgeInsets;
startTransition(camera, animation, [=](double k) {
/// s: The distance traveled along the flight path, measured in
/// ρ-screenfuls.
double s = k * S;
double us = k == 1.0 ? 1.0 : u(s);
// Calculate the current point and zoom level along the flight path.
Point<double> framePoint = util::interpolate(startPoint, endPoint, us);
double frameZoom = startZoom + state.scaleZoom(1 / w(s));
// Zoom can be NaN if size is empty.
if (std::isnan(frameZoom)) {
frameZoom = zoom;
}
// Convert to geographic coordinates and set the new viewpoint.
LatLng frameLatLng = Projection::unproject(framePoint, startScale);
state.setLatLngZoom(frameLatLng, frameZoom);
if (bearing != startBearing) {
state.bearing = util::wrap(util::interpolate(startBearing, bearing, k), -M_PI, M_PI);
}
if (padding != startEdgeInsets) {
// Interpolate edge insets
state.edgeInsets = {
util::interpolate(startEdgeInsets.top(), padding.top(), us),
util::interpolate(startEdgeInsets.left(), padding.left(), us),
util::interpolate(startEdgeInsets.bottom(), padding.bottom(), us),
util::interpolate(startEdgeInsets.right(), padding.right(), us)
};
}
auto maxPitch = getMaxPitchForEdgeInsets(state.edgeInsets);
if (pitch != startPitch || maxPitch < startPitch) {
state.pitch = std::min(maxPitch, util::interpolate(startPitch, pitch, k));
}
}, duration);
}
#pragma mark - Position
void Transform::moveBy(const ScreenCoordinate& offset, const AnimationOptions& animation) {
ScreenCoordinate centerOffset = { offset.x, offset.y };
ScreenCoordinate pointOnScreen = state.edgeInsets.getCenter(state.size.width, state.size.height) - centerOffset;
// Use unwrapped LatLng to carry information about moveBy direction.
easeTo(CameraOptions().withCenter(screenCoordinateToLatLng(pointOnScreen, LatLng::Unwrapped)), animation);
}
LatLng Transform::getLatLng(LatLng::WrapMode wrap) const {
return state.getLatLng(wrap);
}
#pragma mark - Zoom
double Transform::getZoom() const {
return state.getZoom();
}
#pragma mark - Bounds
void Transform::setLatLngBounds(LatLngBounds bounds) {
if (!bounds.valid()) {
throw std::runtime_error("failed to set bounds: bounds are invalid");
}
state.setLatLngBounds(std::move(bounds));
}
void Transform::setMinZoom(const double minZoom) {
if (std::isnan(minZoom)) return;
state.setMinZoom(minZoom);
}
void Transform::setMaxZoom(const double maxZoom) {
if (std::isnan(maxZoom)) return;
state.setMaxZoom(maxZoom);
}
#pragma mark - Bearing
void Transform::rotateBy(const ScreenCoordinate& first, const ScreenCoordinate& second, const AnimationOptions& animation) {
ScreenCoordinate center = state.edgeInsets.getCenter(state.size.width, state.size.height);
const ScreenCoordinate offset = first - center;
const double distance = std::sqrt(std::pow(2, offset.x) + std::pow(2, offset.y));
// If the first click was too close to the center, move the center of rotation by 200 pixels
// in the direction of the click.
if (distance < 200) {
const double heightOffset = -200;
const double rotateBearing = std::atan2(offset.y, offset.x);
center.x = first.x + std::cos(rotateBearing) * heightOffset;
center.y = first.y + std::sin(rotateBearing) * heightOffset;
}
const double bearing = -(state.bearing + util::angle_between(first - center, second - center)) * util::RAD2DEG;
easeTo(CameraOptions().withBearing(bearing), animation);
}
double Transform::getBearing() const {
return state.bearing;
}
#pragma mark - Pitch
double Transform::getPitch() const {
return state.pitch;
}
#pragma mark - North Orientation
void Transform::setNorthOrientation(NorthOrientation orientation) {
state.orientation = orientation;
state.constrain(state.scale, state.x, state.y);
}
NorthOrientation Transform::getNorthOrientation() const {
return state.getNorthOrientation();
}
#pragma mark - Constrain mode
void Transform::setConstrainMode(mbgl::ConstrainMode mode) {
state.constrainMode = mode;
state.constrain(state.scale, state.x, state.y);
}
ConstrainMode Transform::getConstrainMode() const {
return state.getConstrainMode();
}
#pragma mark - Viewport mode
void Transform::setViewportMode(mbgl::ViewportMode mode) {
state.viewportMode = mode;
}
ViewportMode Transform::getViewportMode() const {
return state.getViewportMode();
}
#pragma mark - Projection mode
void Transform::setProjectionMode(const ProjectionMode& options) {
state.axonometric = options.axonometric.value_or(state.axonometric);
state.xSkew = options.xSkew.value_or(state.xSkew);
state.ySkew = options.ySkew.value_or(state.ySkew);
}
ProjectionMode Transform::getProjectionMode() const {
return ProjectionMode()
.withAxonometric(state.axonometric)
.withXSkew(state.xSkew)
.withYSkew(state.ySkew);
}
#pragma mark - Transition
void Transform::startTransition(const CameraOptions& camera,
const AnimationOptions& animation,
std::function<void(double)> frame,
const Duration& duration) {
if (transitionFinishFn) {
transitionFinishFn();
}
bool isAnimated = duration != Duration::zero();
observer.onCameraWillChange(isAnimated ? MapObserver::CameraChangeMode::Animated : MapObserver::CameraChangeMode::Immediate);
// Associate the anchor, if given, with a coordinate.
// Anchor and center points are mutually exclusive, with preference for the
// center point when both are set.
optional<ScreenCoordinate> anchor = camera.center ? nullopt : camera.anchor;
LatLng anchorLatLng;
if (anchor) {
anchor->y = state.size.height - anchor->y;
anchorLatLng = state.screenCoordinateToLatLng(*anchor);
}
transitionStart = Clock::now();
transitionDuration = duration;
transitionFrameFn = [isAnimated, animation, frame, anchor, anchorLatLng, this](const TimePoint now) {
float t = isAnimated ? (std::chrono::duration<float>(now - transitionStart) / transitionDuration) : 1.0;
if (t >= 1.0) {
frame(1.0);
} else {
util::UnitBezier ease = animation.easing ? *animation.easing : util::DEFAULT_TRANSITION_EASE;
frame(ease.solve(t, 0.001));
}
if (anchor) state.moveLatLng(anchorLatLng, *anchor);
// At t = 1.0, a DidChangeAnimated notification should be sent from finish().
if (t < 1.0) {
if (animation.transitionFrameFn) {
animation.transitionFrameFn(t);
}
observer.onCameraIsChanging();
return false;
} else {
// Indicate that we need to terminate this transition
return true;
}
};
transitionFinishFn = [isAnimated, animation, this] {
state.panning = false;
state.scaling = false;
state.rotating = false;
if (animation.transitionFinishFn) {
animation.transitionFinishFn();
}
observer.onCameraDidChange(isAnimated ? MapObserver::CameraChangeMode::Animated : MapObserver::CameraChangeMode::Immediate);
};
if (!isAnimated) {
auto update = std::move(transitionFrameFn);
auto finish = std::move(transitionFinishFn);
transitionFrameFn = nullptr;
transitionFinishFn = nullptr;
update(Clock::now());
finish();
}
}
bool Transform::inTransition() const {
return transitionFrameFn != nullptr;
}
void Transform::updateTransitions(const TimePoint& now) {
// Use a temporary function to ensure that the transitionFrameFn lambda is
// called only once per update.
// This addresses the symptoms of https://github.com/mapbox/mapbox-gl-native/issues/11180
// where setting a shape source to nil (or similar) in the `onCameraIsChanging`
// observer function causes `Map::Impl::onUpdate()` to be called which
// in turn calls this function (before the current iteration has completed),
// leading to an infinite loop. See https://github.com/mapbox/mapbox-gl-native/issues/5833
// for a similar, related, issue.
//
// By temporarily nulling the `transitionFrameFn` (and then restoring it
// after the temporary has been called) we stop this recursion.
//
// It's important to note that the scope of this change is stop the above
// crashes. It doesn't address any potential deeper issue (for example
// user error, how often and when transition callbacks are called).
auto transition = std::move(transitionFrameFn);
transitionFrameFn = nullptr;
if (transition && transition(now)) {
// If the transition indicates that it is complete, then we should call
// the finish lambda (going via a temporary as above)
auto finish = std::move(transitionFinishFn);
transitionFinishFn = nullptr;
transitionFrameFn = nullptr;
if (finish) {
finish();
}
} else if (!transitionFrameFn) {
// We have to check `transitionFrameFn` is nil here, since a new transition
// may have been triggered in a user callback (from the transition call
// above)
transitionFrameFn = std::move(transition);
}
}
void Transform::cancelTransitions() {
if (transitionFinishFn) {
transitionFinishFn();
}
transitionFrameFn = nullptr;
transitionFinishFn = nullptr;
}
void Transform::setGestureInProgress(bool inProgress) {
state.gestureInProgress = inProgress;
}
#pragma mark Conversion and projection
ScreenCoordinate Transform::latLngToScreenCoordinate(const LatLng& latLng) const {
ScreenCoordinate point = state.latLngToScreenCoordinate(latLng);
point.y = state.size.height - point.y;
return point;
}
LatLng Transform::screenCoordinateToLatLng(const ScreenCoordinate& point, LatLng::WrapMode wrapMode) const {
ScreenCoordinate flippedPoint = point;
flippedPoint.y = state.size.height - flippedPoint.y;
return state.screenCoordinateToLatLng(flippedPoint, wrapMode);
}
double Transform::getMaxPitchForEdgeInsets(const EdgeInsets &insets) const
{
double centerOffsetY = 0.5 * (insets.top() - insets.bottom()); // See TransformState::getCenterOffset.
const auto height = state.size.height;
assert(height);
// For details, see description at https://github.com/mapbox/mapbox-gl-native/pull/15195
// The definition of half of TransformState::fov with no inset, is: fov = arctan((height / 2) / (height * 1.5)).
// We use half of fov, as it is field of view above perspective center.
// With inset, this angle changes and tangentOfFovAboveCenterAngle = (h/2 + centerOffsetY) / (height * 1.5).
// 1.03 is a bit extra added to prevent parallel ground to viewport clipping plane.
const double tangentOfFovAboveCenterAngle = 1.03 * (height / 2.0 + centerOffsetY) / (1.5 * height);
const double fovAboveCenter = std::atan(tangentOfFovAboveCenterAngle);
return M_PI * 0.5 - fovAboveCenter;
// e.g. Maximum pitch of 60 degrees is when perspective center's offset from the top is 84% of screen height.
}
} // namespace mbgl