#include #include #include #include #include #include #include #include #include #include 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 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(state.width) / 2.0; cy = static_cast(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(state.width) / 2.0) * (1.0 - factor); const double dy = (cy - static_cast(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(state.width) / 2.0, center_y = static_cast(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(state.width) / 2.0) - cx; dy = (static_cast(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 easing, std::function frame, std::function finish, const Duration& duration) { if (transitionFinishFn) { transitionFinishFn(); } transitionStart = Clock::now(); transitionDuration = duration; transitionFrameFn = [easing, frame, this](const TimePoint now) { float t = std::chrono::duration(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; }