@@ -405,6 +405,22 @@ void Transform::flyTo(const CameraOptions& inputCamera,
|
||||
|
||||
void Transform::moveBy(const ScreenCoordinate& offset, const AnimationOptions& animation) {
|
||||
ScreenCoordinate centerOffset = {offset.x, offset.y};
|
||||
|
||||
// Reduce the offset so that it never goes past the horizon. If it goes past
|
||||
// the horizon, the pan direction is opposite of the intended direction.
|
||||
const double pitch = state.getPitch();
|
||||
const double offsetLength = std::hypot(offset.x, offset.y);
|
||||
if (pitch > 0.0 && offsetLength > 0.0) {
|
||||
const double cameraToCenter = 0.5 * static_cast<double>(state.getSize().height) /
|
||||
std::tan(state.getFieldOfView() / 2.0);
|
||||
const double pixelsToHorizon = std::abs(cameraToCenter / std::tan(pitch));
|
||||
constexpr double horizonFactor = 0.75; // must be < 1 to keep the offset short of the horizon
|
||||
const double scale = pixelsToHorizon * horizonFactor / offsetLength;
|
||||
if (scale < 1.0) {
|
||||
centerOffset = {offset.x * scale, offset.y * scale};
|
||||
}
|
||||
}
|
||||
|
||||
ScreenCoordinate pointOnScreen = state.getEdgeInsets().getCenter(state.getSize().width, state.getSize().height) -
|
||||
centerOffset;
|
||||
// Use unwrapped LatLng to carry information about moveBy direction.
|
||||
|
||||
@@ -378,6 +378,32 @@ TEST(Transform, MoveBy) {
|
||||
ASSERT_NEAR(0.0, trueCenter.longitude(), 1.1);
|
||||
}
|
||||
|
||||
// On a pitched map a large (fling-sized) pan offset must be reduced so it can
|
||||
// never reach the horizon -- otherwise the unprojected pan direction flips and the
|
||||
// pan reverses (or lands in the tile-less void). See maplibre-native#3105.
|
||||
TEST(Transform, MoveByPitchedDoesNotCrossHorizon) {
|
||||
Transform transform;
|
||||
transform.resize({1000, 1000});
|
||||
const auto start = CameraOptions().withCenter(LatLng{0.0, 0.0}).withZoom(10.0).withPitch(60.0);
|
||||
|
||||
// Panning "up" the screen on a north-up pitched map moves the center north.
|
||||
transform.jumpTo(start);
|
||||
transform.moveBy({0.0, 300.0});
|
||||
const double moderateLat = transform.getLatLng().latitude();
|
||||
|
||||
// A fling-sized offset toward the horizon must keep moving the center the same
|
||||
// way (north) -- before the fix it reversed south and stay bounded (clamped
|
||||
// short of the horizon).
|
||||
transform.jumpTo(start);
|
||||
transform.moveBy({0.0, 100000.0});
|
||||
const double flingLat = transform.getLatLng().latitude();
|
||||
|
||||
EXPECT_GT(moderateLat, 0.0); // moved north
|
||||
EXPECT_GT(flingLat, 0.0); // still north; not reversed
|
||||
EXPECT_GE(flingLat, moderateLat); // fling goes at least as far as the moderate pan
|
||||
EXPECT_LT(flingLat, util::LATITUDE_MAX); // but is bounded, not off near the horizon
|
||||
}
|
||||
|
||||
TEST(Transform, Antimeridian) {
|
||||
Transform transform;
|
||||
transform.resize({1000, 1000});
|
||||
|
||||
Reference in New Issue
Block a user