Plumbed camera options all the way through to MGLMapView. Added a method that lets you specify a direction in addition to center point and zoom level. Added Map::jumpTo() for parity with mapbox-gl-js. Replaced usage of Map::setLatLng() and Map::setLatLngZoom() with Map::jumpTo() or Map::easeTo() within MGLMapView. Replaced MGLMapView.pitch with MGLMapCamera for setting all supported degrees of freedom simultaneously. Simultaneously move and rotate with course. Support customizable timing functions on iOS. iosapp now persists an archived MGLMapCamera instead of separate viewpoint properties and also synchronizes user defaults on termination. This change implements persistence entirely in Objective-C, eliminating the use of the Objective-C++ implementation. Fixes #1643, fixes #1834. Ref #1581.
409 lines
12 KiB
C++
409 lines
12 KiB
C++
#include <mbgl/map/camera.hpp>
|
|
#include <mbgl/map/transform.hpp>
|
|
#include <mbgl/map/view.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/platform/platform.hpp>
|
|
|
|
#include <cstdio>
|
|
|
|
using 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)
|
|
{
|
|
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(View &view_)
|
|
: view(view_)
|
|
{
|
|
}
|
|
|
|
#pragma mark - Map View
|
|
|
|
bool Transform::resize(const std::array<uint16_t, 2> size) {
|
|
if (state.width != size[0] || state.height != size[1]) {
|
|
|
|
view.notifyMapChange(MapChangeRegionWillChange);
|
|
|
|
state.width = size[0];
|
|
state.height = size[1];
|
|
state.constrain(state.scale, state.y);
|
|
|
|
view.notifyMapChange(MapChangeRegionDidChange);
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#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;
|
|
}
|
|
|
|
_moveBy(dx, dy, duration);
|
|
}
|
|
|
|
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);
|
|
|
|
CameraOptions options;
|
|
options.duration = duration;
|
|
_easeTo(options, state.scale, state.angle, x, y);
|
|
}
|
|
|
|
void Transform::setLatLng(const LatLng latLng, const Duration& duration) {
|
|
CameraOptions options;
|
|
options.center = latLng;
|
|
options.duration = duration;
|
|
easeTo(options);
|
|
}
|
|
|
|
void Transform::setLatLngZoom(const LatLng latLng, const double zoom, const Duration& duration) {
|
|
CameraOptions options;
|
|
options.center = latLng;
|
|
options.zoom = zoom;
|
|
options.duration = duration;
|
|
easeTo(options);
|
|
}
|
|
|
|
|
|
#pragma mark - Zoom
|
|
|
|
void Transform::scaleBy(const double ds, const double cx, const double cy, const Duration& duration) {
|
|
if (std::isnan(ds) || std::isnan(cx) || std::isnan(cy)) {
|
|
return;
|
|
}
|
|
|
|
// clamp scale to min/max values
|
|
double new_scale = state.scale * ds;
|
|
if (new_scale < state.min_scale) {
|
|
new_scale = state.min_scale;
|
|
} else if (new_scale > state.max_scale) {
|
|
new_scale = state.max_scale;
|
|
}
|
|
|
|
_setScale(new_scale, cx, cy, duration);
|
|
}
|
|
|
|
void Transform::setScale(const double scale, const double cx, const double cy,
|
|
const Duration& duration) {
|
|
if (std::isnan(scale) || std::isnan(cx) || std::isnan(cy)) {
|
|
return;
|
|
}
|
|
|
|
_setScale(scale, cx, cy, duration);
|
|
}
|
|
|
|
void Transform::setZoom(const double zoom, const Duration& duration) {
|
|
if (std::isnan(zoom)) {
|
|
return;
|
|
}
|
|
|
|
_setScale(std::pow(2.0, zoom), -1, -1, duration);
|
|
}
|
|
|
|
double Transform::getZoom() const {
|
|
return state.getZoom();
|
|
}
|
|
|
|
double Transform::getScale() const {
|
|
return state.scale;
|
|
}
|
|
|
|
void Transform::_setScale(double new_scale, double cx, double cy, const Duration& duration) {
|
|
// Ensure that we don't zoom in further than the maximum allowed.
|
|
if (new_scale < state.min_scale) {
|
|
new_scale = state.min_scale;
|
|
} else if (new_scale > state.max_scale) {
|
|
new_scale = state.max_scale;
|
|
}
|
|
|
|
// Zoom in on the center if we don't have click or gesture anchor coordinates.
|
|
if (cx < 0 || cy < 0) {
|
|
cx = static_cast<double>(state.width) / 2.0;
|
|
cy = static_cast<double>(state.height) / 2.0;
|
|
}
|
|
|
|
// Account for the x/y offset from the center (= where the user clicked or pinched)
|
|
const double factor = new_scale / state.scale;
|
|
const double dx = (cx - static_cast<double>(state.width) / 2.0) * (1.0 - factor);
|
|
const double dy = (cy - static_cast<double>(state.height) / 2.0) * (1.0 - factor);
|
|
|
|
// Account for angle
|
|
const double angle_sin = std::sin(-state.angle);
|
|
const double angle_cos = std::cos(-state.angle);
|
|
const double ax = angle_cos * dx - angle_sin * dy;
|
|
const double ay = angle_sin * dx + angle_cos * dy;
|
|
|
|
const double xn = state.x * factor + ax;
|
|
const double yn = state.y * factor + ay;
|
|
|
|
_setScaleXY(new_scale, xn, yn, 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 (!options.duration) {
|
|
options.duration = Duration::zero();
|
|
}
|
|
if (!options.duration || *options.duration == Duration::zero()) {
|
|
view.notifyMapChange(MapChangeRegionWillChange);
|
|
|
|
state.scale = scale;
|
|
state.x = x;
|
|
state.y = y;
|
|
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);
|
|
state.y = util::interpolate(startY, y, t);
|
|
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;
|
|
},
|
|
[=] {
|
|
state.panning = false;
|
|
state.scaling = false;
|
|
state.rotating = false;
|
|
view.notifyMapChange(MapChangeRegionDidChangeAnimated);
|
|
}, *options.duration);
|
|
}
|
|
}
|
|
|
|
#pragma mark - Angle
|
|
|
|
void Transform::rotateBy(const double start_x, const double start_y, const double end_x,
|
|
const double end_y, const Duration& duration) {
|
|
if (std::isnan(start_x) || std::isnan(start_y) || std::isnan(end_x) || std::isnan(end_y)) {
|
|
return;
|
|
}
|
|
|
|
double center_x = static_cast<double>(state.width) / 2.0, center_y = static_cast<double>(state.height) / 2.0;
|
|
|
|
const double begin_center_x = start_x - center_x;
|
|
const double begin_center_y = start_y - center_y;
|
|
|
|
const double beginning_center_dist =
|
|
std::sqrt(begin_center_x * begin_center_x + begin_center_y * begin_center_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 (beginning_center_dist < 200) {
|
|
const double offset_x = -200, offset_y = 0;
|
|
const double rotate_angle = std::atan2(begin_center_y, begin_center_x);
|
|
const double rotate_angle_sin = std::sin(rotate_angle);
|
|
const double rotate_angle_cos = std::cos(rotate_angle);
|
|
center_x = start_x + rotate_angle_cos * offset_x - rotate_angle_sin * offset_y;
|
|
center_y = start_y + rotate_angle_sin * offset_x + rotate_angle_cos * offset_y;
|
|
}
|
|
|
|
const double first_x = start_x - center_x, first_y = start_y - center_y;
|
|
const double second_x = end_x - center_x, second_y = end_y - center_y;
|
|
|
|
const double ang = state.angle + util::angle_between(first_x, first_y, second_x, second_y);
|
|
|
|
_setAngle(ang, duration);
|
|
}
|
|
|
|
void Transform::setAngle(const double new_angle, const Duration& duration) {
|
|
if (std::isnan(new_angle)) {
|
|
return;
|
|
}
|
|
|
|
_setAngle(new_angle, duration);
|
|
}
|
|
|
|
void Transform::setAngle(const double new_angle, const double cx, const double cy) {
|
|
if (std::isnan(new_angle) || std::isnan(cx) || std::isnan(cy)) {
|
|
return;
|
|
}
|
|
|
|
double dx = 0, dy = 0;
|
|
|
|
if (cx >= 0 && cy >= 0) {
|
|
dx = (static_cast<double>(state.width) / 2.0) - cx;
|
|
dy = (static_cast<double>(state.height) / 2.0) - cy;
|
|
_moveBy(dx, dy, Duration::zero());
|
|
}
|
|
|
|
_setAngle(new_angle);
|
|
|
|
if (cx >= 0 && cy >= 0) {
|
|
_moveBy(-dx, -dy, Duration::zero());
|
|
}
|
|
}
|
|
|
|
void Transform::_setAngle(double new_angle, const Duration& duration) {
|
|
CameraOptions options;
|
|
options.angle = new_angle;
|
|
options.duration = duration;
|
|
easeTo(options);
|
|
}
|
|
|
|
double Transform::getAngle() const {
|
|
return state.angle;
|
|
}
|
|
|
|
#pragma mark - Pitch
|
|
|
|
void Transform::setPitch(double pitch, const Duration& duration) {
|
|
CameraOptions options;
|
|
options.pitch = pitch;
|
|
options.duration = duration;
|
|
easeTo(options);
|
|
}
|
|
|
|
double Transform::getPitch() const {
|
|
return state.pitch;
|
|
}
|
|
|
|
#pragma mark - Transition
|
|
|
|
void Transform::startTransition(std::function<double(double)> easing,
|
|
std::function<Update(double)> frame,
|
|
std::function<void()> finish,
|
|
const Duration& duration) {
|
|
if (transitionFinishFn) {
|
|
transitionFinishFn();
|
|
}
|
|
|
|
transitionStart = Clock::now();
|
|
transitionDuration = duration;
|
|
|
|
transitionFrameFn = [easing, frame, this](const TimePoint now) {
|
|
float t = std::chrono::duration<float>(now - transitionStart) / transitionDuration;
|
|
if (t >= 1.0) {
|
|
Update result = frame(1.0);
|
|
transitionFinishFn();
|
|
transitionFrameFn = nullptr;
|
|
transitionFinishFn = nullptr;
|
|
return result;
|
|
} else {
|
|
return frame(easing(t));
|
|
}
|
|
};
|
|
|
|
transitionFinishFn = finish;
|
|
}
|
|
|
|
bool Transform::inTransition() const {
|
|
return transitionFrameFn != nullptr;
|
|
}
|
|
|
|
Update Transform::updateTransitions(const TimePoint& now) {
|
|
return transitionFrameFn ? transitionFrameFn(now) : Update::Nothing;
|
|
}
|
|
|
|
void Transform::cancelTransitions() {
|
|
if (transitionFinishFn) {
|
|
transitionFinishFn();
|
|
}
|
|
|
|
transitionFrameFn = nullptr;
|
|
transitionFinishFn = nullptr;
|
|
}
|
|
|
|
void Transform::setGestureInProgress(bool inProgress) {
|
|
state.gestureInProgress = inProgress;
|
|
}
|