#include #include #include #include #include #include #include #include using namespace std::numbers; using namespace mbgl; TEST(Transform, InvalidZoom) { Transform transform; transform.resize({1, 1}); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(0, transform.getZoom()); transform.jumpTo(CameraOptions().withZoom(1.0)); ASSERT_DOUBLE_EQ(1, transform.getZoom()); const double invalid = NAN; transform.jumpTo(CameraOptions().withZoom(invalid)); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(1, transform.getZoom()); transform.jumpTo(CameraOptions().withCenter(LatLng()).withZoom(invalid)); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(1, transform.getZoom()); transform.jumpTo(CameraOptions().withZoom(transform.getState().getMaxZoom() + 0.1)); ASSERT_DOUBLE_EQ(transform.getZoom(), transform.getState().getMaxZoom()); // Executing flyTo with an empty size causes frameZoom to be NaN. transform.flyTo(CameraOptions() .withCenter(LatLng{util::LATITUDE_MAX, util::LONGITUDE_MAX}) .withZoom(transform.getState().getMaxZoom())); transform.updateTransitions(transform.getTransitionStart() + transform.getTransitionDuration()); ASSERT_DOUBLE_EQ(transform.getZoom(), transform.getState().getMaxZoom()); // Executing flyTo with maximum zoom level to the same zoom level causes // frameZoom to be bigger than maximum zoom. transform.resize(Size{100, 100}); transform.flyTo(CameraOptions() .withCenter(LatLng{util::LATITUDE_MAX, util::LONGITUDE_MAX}) .withZoom(transform.getState().getMaxZoom())); transform.updateTransitions(transform.getTransitionStart() + transform.getTransitionDuration()); ASSERT_TRUE(transform.getState().valid()); ASSERT_DOUBLE_EQ(transform.getState().getMaxZoom(), transform.getZoom()); } TEST(Transform, InvalidBearing) { Transform transform; transform.resize({1, 1}); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(0, transform.getZoom()); transform.jumpTo(CameraOptions().withZoom(1.0).withBearing(2.0)); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(1, transform.getZoom()); ASSERT_DOUBLE_EQ(util::deg2rad(-2.0), transform.getBearing()); const double invalid = NAN; transform.jumpTo(CameraOptions().withBearing(invalid)); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(1, transform.getZoom()); ASSERT_DOUBLE_EQ(util::deg2rad(-2.0), transform.getBearing()); } TEST(Transform, IntegerZoom) { Transform transform; transform.resize({1, 1}); auto checkIntegerZoom = [&transform](uint8_t zoomInt, double zoom) { transform.jumpTo(CameraOptions().withZoom(zoom)); ASSERT_NEAR(transform.getZoom(), zoom, 1e-8); ASSERT_EQ(transform.getState().getIntegerZoom(), zoomInt); ASSERT_NEAR(transform.getState().getZoomFraction(), zoom - zoomInt, 1e-8); }; for (uint8_t zoomInt = 0; zoomInt < 20; ++zoomInt) { for (uint32_t percent = 0; percent < 100; ++percent) { double zoom = zoomInt + (0.01 * percent); checkIntegerZoom(zoomInt, zoom); } } // Special case zoom 20. checkIntegerZoom(20, 20.0); } TEST(Transform, PerspectiveProjection) { LatLng loc; Transform transform; transform.resize({1000, 1000}); // 0.9 rad ~ 51.56620156 deg transform.jumpTo(CameraOptions().withCenter(LatLng{38.0, -77.0}).withZoom(10.0).withPitch(51.56620156)); // expected values are from maplibre-gl-js loc = transform.getLatLng(); ASSERT_DOUBLE_EQ(-77, loc.longitude()); ASSERT_DOUBLE_EQ(38, loc.latitude()); loc = transform.getState().screenCoordinateToLatLng({0, 1000}); ASSERT_NEAR(-77.59198961199148, loc.longitude(), 1e-6); ASSERT_NEAR(38.74661326302018, loc.latitude(), 1e-6); loc = transform.getState().screenCoordinateToLatLng({1000, 0}); ASSERT_NEAR(-76.75823239205641, loc.longitude(), 1e-6); ASSERT_NEAR(37.692872969426375, loc.latitude(), 1e-6); ScreenCoordinate point = transform.getState().latLngToScreenCoordinate({38.74661326302018, -77.59198961199148}); ASSERT_NEAR(point.x, 0.0, 1e-5); ASSERT_NEAR(point.y, 1000.0, 1e-4); point = transform.getState().latLngToScreenCoordinate({37.692872969426375, -76.75823239205641}); ASSERT_NEAR(point.x, 1000.0, 1e-5); ASSERT_NEAR(point.y, 0.0, 1e-4); mbgl::vec4 p; point = transform.getState().latLngToScreenCoordinate({37.692872969426375, -76.75823239205641}, p); ASSERT_NEAR(point.x, 1000.0, 1e-5); ASSERT_NEAR(point.y, 0.0, 1e-4); ASSERT_GT(p[3], 0.0); transform.jumpTo(CameraOptions().withCenter(LatLng{38.0, -77.0}).withZoom(18.0).withPitch(51.56620156)); point = transform.getState().latLngToScreenCoordinate({7.692872969426375, -76.75823239205641}, p); ASSERT_LT(p[3], 0.0); } TEST(Transform, UnwrappedLatLng) { Transform transform; transform.resize({1000, 1000}); // 0.9 rad ~ 51.56620156 deg transform.jumpTo(CameraOptions().withCenter(LatLng{38.0, -77.0}).withZoom(10.0).withPitch(51.56620156)); const TransformState& state = transform.getState(); LatLng fromGetLatLng = state.getLatLng(); ASSERT_DOUBLE_EQ(fromGetLatLng.latitude(), 38.0); ASSERT_DOUBLE_EQ(fromGetLatLng.longitude(), -77.0); LatLng fromScreenCoordinate = state.screenCoordinateToLatLng({500, 500}); ASSERT_NEAR(fromScreenCoordinate.latitude(), 38.0, 1e-8); ASSERT_NEAR(fromScreenCoordinate.longitude(), -77.0, 1e-8); LatLng wrappedRightwards = state.screenCoordinateToLatLng(state.latLngToScreenCoordinate({38, 283})); ASSERT_NEAR(wrappedRightwards.latitude(), 38.0, 1e-8); ASSERT_NEAR(wrappedRightwards.longitude(), 283.0, 1e-8); wrappedRightwards.wrap(); ASSERT_NEAR(wrappedRightwards.longitude(), -77.0, 1e-8); LatLng wrappedLeftwards = state.screenCoordinateToLatLng(state.latLngToScreenCoordinate({38, -437})); ASSERT_DOUBLE_EQ(wrappedLeftwards.latitude(), wrappedRightwards.latitude()); ASSERT_NEAR(wrappedLeftwards.longitude(), -437.0, 1e-8); wrappedLeftwards.wrap(); ASSERT_NEAR(wrappedLeftwards.longitude(), -77.0, 1e-8); } TEST(Transform, ConstrainHeightOnly) { Transform transform(TransformObserver::nullObserver(), ConstrainMode::HeightOnly); transform.resize({2, 2}); transform.jumpTo(CameraOptions().withCenter(LatLngBounds::world().southwest()).withZoom(util::MAX_ZOOM)); ASSERT_NEAR(-util::LATITUDE_MAX, transform.getLatLng().latitude(), 1e-7); ASSERT_NEAR(-util::LONGITUDE_MAX, transform.getLatLng().longitude(), 1e-7); transform.jumpTo(CameraOptions().withCenter(LatLngBounds::world().northeast())); ASSERT_NEAR(util::LATITUDE_MAX, transform.getLatLng().latitude(), 1e-7); ASSERT_NEAR(-util::LONGITUDE_MAX, transform.getLatLng().longitude(), 1e-7); } TEST(Transform, ConstrainWidthAndHeight) { Transform transform(TransformObserver::nullObserver(), ConstrainMode::WidthAndHeight); transform.resize({2, 2}); transform.jumpTo(CameraOptions().withCenter(LatLngBounds::world().southwest()).withZoom(util::MAX_ZOOM)); ASSERT_NEAR(-util::LATITUDE_MAX, transform.getLatLng().latitude(), 1e-7); ASSERT_NEAR(-util::LONGITUDE_MAX, transform.getLatLng().longitude(), 1e-6); transform.jumpTo(CameraOptions().withCenter(LatLngBounds::world().northeast())); ASSERT_NEAR(util::LATITUDE_MAX, transform.getLatLng().latitude(), 1e-7); ASSERT_NEAR(-util::LONGITUDE_MAX, transform.getLatLng().longitude(), 1e-6); } TEST(Transform, Anchor) { Transform transform; transform.resize({1000, 1000}); const LatLng latLng{10, -100}; const ScreenCoordinate anchorPoint = {150, 150}; transform.jumpTo(CameraOptions().withCenter(latLng).withZoom(10.0)); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(10, transform.getZoom()); ASSERT_DOUBLE_EQ(0, transform.getBearing()); const LatLng anchorLatLng = transform.getState().screenCoordinateToLatLng(anchorPoint); ASSERT_NE(latLng.latitude(), anchorLatLng.latitude()); ASSERT_NE(latLng.longitude(), anchorLatLng.longitude()); transform.jumpTo(CameraOptions().withCenter(latLng).withZoom(3.0)); ASSERT_DOUBLE_EQ(3, transform.getZoom()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withZoom(3.5)); ASSERT_DOUBLE_EQ(3.5, transform.getZoom()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withZoom(5.5).withAnchor(anchorPoint)); ASSERT_DOUBLE_EQ(5.5, transform.getZoom()); ASSERT_NE(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_NE(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withCenter(latLng).withZoom(3.0)); ASSERT_DOUBLE_EQ(3, transform.getZoom()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withZoom(5.0)); ASSERT_DOUBLE_EQ(5, transform.getZoom()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withZoom(7.0).withAnchor(anchorPoint)); ASSERT_DOUBLE_EQ(7, transform.getZoom()); ASSERT_NE(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_NE(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withCenter(latLng).withZoom(2.0)); ASSERT_DOUBLE_EQ(2, transform.getZoom()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withZoom(4.0)); ASSERT_DOUBLE_EQ(4, transform.getZoom()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withZoom(8.0).withAnchor(anchorPoint)); ASSERT_DOUBLE_EQ(8, transform.getZoom()); ASSERT_NE(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_NE(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withCenter(latLng).withZoom(10.0).withBearing(-45.0)); ASSERT_DOUBLE_EQ(pi / 4, transform.getBearing()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withBearing(0.0)); ASSERT_DOUBLE_EQ(0, transform.getBearing()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withBearing(45.0).withAnchor(anchorPoint)); ASSERT_DOUBLE_EQ(util::deg2rad(-45.0), transform.getBearing()); // Anchor coordinates are imprecise because we are converting from an integer pixel. ASSERT_NEAR(anchorLatLng.latitude(), transform.getLatLng().latitude(), 0.5); ASSERT_NEAR(anchorLatLng.longitude(), transform.getLatLng().longitude(), 0.5); transform.jumpTo(CameraOptions().withCenter(latLng).withZoom(10.0).withPitch(10.0)); ASSERT_DOUBLE_EQ(util::deg2rad(10.0), transform.getPitch()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withPitch(15.0)); ASSERT_DOUBLE_EQ(util::deg2rad(15.0), transform.getPitch()); ASSERT_DOUBLE_EQ(latLng.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng.longitude(), transform.getLatLng().longitude()); transform.jumpTo(CameraOptions().withPitch(20.0).withAnchor(anchorPoint)); ASSERT_DOUBLE_EQ(util::deg2rad(20.0), transform.getPitch()); // Anchor coordinates are imprecise because we are converting from an integer pixel. ASSERT_NEAR(anchorLatLng.latitude(), transform.getLatLng().latitude(), 0.5); ASSERT_NEAR(anchorLatLng.longitude(), transform.getLatLng().longitude(), 0.5); } TEST(Transform, Padding) { Transform transform; transform.resize({1000, 1000}); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); CameraOptions nonPaddedCameraOptions = CameraOptions().withCenter(LatLng{10, -100}).withZoom(10.0); transform.jumpTo(nonPaddedCameraOptions); const LatLng trueCenter = transform.getLatLng(); ASSERT_DOUBLE_EQ(10, trueCenter.latitude()); ASSERT_DOUBLE_EQ(-100, trueCenter.longitude()); ASSERT_DOUBLE_EQ(10, transform.getZoom()); const LatLng screenCenter = transform.screenCoordinateToLatLng({ 1000.0 / 2.0, 1000.0 / 2.0, }); const LatLng upperHalfCenter = transform.screenCoordinateToLatLng({ 1000.0 / 2.0, 1000.0 * 0.25, }); EdgeInsets padding(1000.0 / 2.0, 0, 0, 0); // CameraOption center and zoom don't change when padding changes: center of // viewport remains the same as padding defines viwport center offset in rendering. CameraOptions paddedOptions = CameraOptions().withPadding(padding); transform.jumpTo(paddedOptions); const LatLng theSameCenter = transform.getLatLng(); ASSERT_DOUBLE_EQ(trueCenter.latitude(), theSameCenter.latitude()); ASSERT_DOUBLE_EQ(trueCenter.longitude(), theSameCenter.longitude()); // However, LatLng is now at the center of lower half - verify conversion // from screen coordinate to LatLng. const LatLng paddedLowerHalfScreenCenter = transform.screenCoordinateToLatLng({ 1000.0 / 2.0, 1000.0 * 0.75, }); ASSERT_NEAR(screenCenter.latitude(), paddedLowerHalfScreenCenter.latitude(), 1e-10); ASSERT_NEAR(screenCenter.longitude(), paddedLowerHalfScreenCenter.longitude(), 1e-10); // LatLng previously in upper half center, should now be under screen center. const LatLng paddedScreenCenter = transform.screenCoordinateToLatLng({ 1000.0 / 2.0, 1000.0 / 2.0, }); ASSERT_NEAR(upperHalfCenter.latitude(), paddedScreenCenter.latitude(), 1e-10); ASSERT_NEAR(upperHalfCenter.longitude(), paddedScreenCenter.longitude(), 1e-10); } TEST(Transform, MoveBy) { Transform transform; transform.resize({1000, 1000}); transform.jumpTo(CameraOptions().withCenter(LatLng()).withZoom(10.0)); LatLng trueCenter = transform.getLatLng(); ASSERT_DOUBLE_EQ(0, trueCenter.latitude()); ASSERT_DOUBLE_EQ(0, trueCenter.longitude()); ASSERT_DOUBLE_EQ(10, transform.getZoom()); for (uint8_t x = 0; x < 20; ++x) { bool odd = x % 2; bool forward = x % 10; LatLng coordinate = transform.screenCoordinateToLatLng({odd ? 400. : 600., forward ? 400. : 600}); transform.moveBy({odd ? 100. : -100., forward ? 100. : -100}); trueCenter = transform.getLatLng(); ASSERT_NEAR(coordinate.latitude(), trueCenter.latitude(), 1e-8); ASSERT_NEAR(coordinate.longitude(), trueCenter.longitude(), 1e-8); } // We have ~1.1 precision loss for each coordinate for 20 rounds of moveBy. ASSERT_NEAR(0.0, trueCenter.latitude(), 1.1); ASSERT_NEAR(0.0, trueCenter.longitude(), 1.1); } TEST(Transform, Antimeridian) { Transform transform; transform.resize({1000, 1000}); transform.jumpTo(CameraOptions().withCenter(LatLng()).withZoom(1.0)); // San Francisco const LatLng coordinateSanFrancisco{37.7833, -122.4167}; ScreenCoordinate pixelSF = transform.latLngToScreenCoordinate(coordinateSanFrancisco); ASSERT_DOUBLE_EQ(151.79249437176432, pixelSF.x); ASSERT_DOUBLE_EQ(383.76720782527661, pixelSF.y); transform.jumpTo(CameraOptions().withCenter(LatLng{0.0, -181.0})); ScreenCoordinate pixelSFLongest = transform.latLngToScreenCoordinate(coordinateSanFrancisco); ASSERT_DOUBLE_EQ(-357.36306616412816, pixelSFLongest.x); ASSERT_DOUBLE_EQ(pixelSF.y, pixelSFLongest.y); LatLng unwrappedSF = coordinateSanFrancisco.wrapped(); unwrappedSF.unwrapForShortestPath(transform.getLatLng()); ScreenCoordinate pixelSFShortest = transform.latLngToScreenCoordinate(unwrappedSF); ASSERT_DOUBLE_EQ(666.63694385219173, pixelSFShortest.x); ASSERT_DOUBLE_EQ(pixelSF.y, pixelSFShortest.y); transform.jumpTo(CameraOptions().withCenter(LatLng{0.0, 179.0})); pixelSFShortest = transform.latLngToScreenCoordinate(coordinateSanFrancisco); ASSERT_DOUBLE_EQ(pixelSFLongest.x, pixelSFShortest.x); ASSERT_DOUBLE_EQ(pixelSFLongest.y, pixelSFShortest.y); // Waikiri const LatLng coordinateWaikiri{-16.9310, 179.9787}; transform.jumpTo(CameraOptions().withCenter(coordinateWaikiri).withZoom(10.0)); ScreenCoordinate pixelWaikiri = transform.latLngToScreenCoordinate(coordinateWaikiri); ASSERT_DOUBLE_EQ(500, pixelWaikiri.x); ASSERT_DOUBLE_EQ(500, pixelWaikiri.y); transform.jumpTo(CameraOptions().withCenter(LatLng{coordinateWaikiri.latitude(), 180.0213})); ScreenCoordinate pixelWaikiriLongest = transform.latLngToScreenCoordinate(coordinateWaikiri); ASSERT_DOUBLE_EQ(524725.96438108233, pixelWaikiriLongest.x); ASSERT_DOUBLE_EQ(pixelWaikiri.y, pixelWaikiriLongest.y); LatLng unwrappedWaikiri = coordinateWaikiri.wrapped(); unwrappedWaikiri.unwrapForShortestPath(transform.getLatLng()); ScreenCoordinate pixelWaikiriShortest = transform.latLngToScreenCoordinate(unwrappedWaikiri); ASSERT_DOUBLE_EQ(437.95925272648344, pixelWaikiriShortest.x); ASSERT_DOUBLE_EQ(pixelWaikiri.y, pixelWaikiriShortest.y); LatLng coordinateFromPixel = transform.screenCoordinateToLatLng(pixelWaikiriLongest); ASSERT_NEAR(coordinateWaikiri.latitude(), coordinateFromPixel.latitude(), 1e-4); ASSERT_NEAR(coordinateWaikiri.longitude(), coordinateFromPixel.longitude(), 1e-4); transform.jumpTo(CameraOptions().withCenter(LatLng{coordinateWaikiri.latitude(), 180.0213})); pixelWaikiriShortest = transform.latLngToScreenCoordinate(coordinateWaikiri); ASSERT_DOUBLE_EQ(pixelWaikiriLongest.x, pixelWaikiriShortest.x); ASSERT_DOUBLE_EQ(pixelWaikiriLongest.y, pixelWaikiriShortest.y); coordinateFromPixel = transform.screenCoordinateToLatLng(pixelWaikiriShortest); ASSERT_NEAR(coordinateWaikiri.latitude(), coordinateFromPixel.latitude(), 1e-4); ASSERT_NEAR(coordinateWaikiri.longitude(), coordinateFromPixel.longitude(), 1e-4); } TEST(Transform, Camera) { Transform transform; transform.resize({1000, 1000}); const LatLng latLng1{45, 135}; CameraOptions cameraOptions1 = CameraOptions().withCenter(latLng1).withZoom(20.0); transform.jumpTo(cameraOptions1); ASSERT_DOUBLE_EQ(latLng1.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng1.longitude(), transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(20, transform.getZoom()); const LatLng latLng2{-45, -135}; CameraOptions cameraOptions2 = CameraOptions().withCenter(latLng2).withZoom(10.0); transform.jumpTo(cameraOptions2); ASSERT_DOUBLE_EQ(latLng2.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng2.longitude(), transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(10, transform.getZoom()); AnimationOptions easeOptions(Seconds(1)); easeOptions.transitionFrameFn = [&](double t) { ASSERT_TRUE(t >= 0 && t <= 1); ASSERT_GE(latLng1.latitude(), transform.getLatLng().latitude()); ASSERT_LE(latLng1.longitude(), transform.getLatLng().longitude()); }; easeOptions.transitionFinishFn = [&]() { ASSERT_DOUBLE_EQ(latLng1.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng1.longitude(), transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(20, transform.getZoom()); }; transform.easeTo(cameraOptions1, easeOptions); ASSERT_TRUE(transform.inTransition()); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(250)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(500)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(750)); transform.updateTransitions(transform.getTransitionStart() + transform.getTransitionDuration()); ASSERT_FALSE(transform.inTransition()); AnimationOptions flyOptions(Seconds(1)); flyOptions.transitionFrameFn = [&](double t) { ASSERT_TRUE(t >= 0 && t <= 1); ASSERT_LE(latLng2.latitude(), transform.getLatLng().latitude()); ASSERT_GE(latLng2.longitude(), transform.getLatLng(LatLng::Unwrapped).longitude()); }; flyOptions.transitionFinishFn = [&]() { // XXX Fix precision loss in flyTo: // https://github.com/mapbox/mapbox-gl-native/issues/4298 ASSERT_DOUBLE_EQ(latLng2.latitude(), transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(latLng2.longitude(), transform.getLatLng().longitude()); ASSERT_NEAR(10.0, transform.getZoom(), 1e-5); }; transform.flyTo(cameraOptions2, flyOptions); ASSERT_TRUE(transform.inTransition()); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(250)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(500)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(750)); transform.updateTransitions(transform.getTransitionStart() + transform.getTransitionDuration()); ASSERT_FALSE(transform.inTransition()); // Anchor and center points are mutually exclusive. CameraOptions camera; camera.center = LatLng{0, 0}; camera.anchor = ScreenCoordinate{0, 0}; // top-left camera.zoom = transform.getState().getMaxZoom(); transform.easeTo(camera, AnimationOptions(Seconds(1))); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(250)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(500)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(750)); transform.updateTransitions(transform.getTransitionStart() + transform.getTransitionDuration()); ASSERT_DOUBLE_EQ(transform.getLatLng().latitude(), 0); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), 0); } TEST(Transform, ProjectionMode) { Transform transform; transform.setProjectionMode(ProjectionMode().withAxonometric(true).withXSkew(1.0).withYSkew(0.0)); auto options = transform.getProjectionMode(); EXPECT_TRUE(*options.axonometric); EXPECT_EQ(*options.xSkew, 1.0); EXPECT_EQ(*options.ySkew, 0.0); } TEST(Transform, IsPanning) { Transform transform; AnimationOptions easeOptions(Seconds(1)); easeOptions.transitionFrameFn = [&transform](double) { ASSERT_TRUE(transform.getState().isPanning()); }; transform.resize({1000, 1000}); transform.easeTo(CameraOptions().withCenter(LatLng(0, 360.0)), easeOptions); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(250)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(500)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(750)); transform.updateTransitions(transform.getTransitionStart() + transform.getTransitionDuration()); } TEST(Transform, DefaultTransform) { struct TransformObserver : public mbgl::TransformObserver { void onCameraWillChange(MapObserver::CameraChangeMode) final { cameraWillChangeCallback(); }; void onCameraDidChange(MapObserver::CameraChangeMode) final { cameraDidChangeCallback(); }; std::function cameraWillChangeCallback; std::function cameraDidChangeCallback; }; uint32_t cameraWillChangeCount = 0; uint32_t cameraDidChangeCount = 0; TransformObserver observer; observer.cameraWillChangeCallback = [&cameraWillChangeCount]() { cameraWillChangeCount++; }; observer.cameraDidChangeCallback = [&cameraDidChangeCount]() { cameraDidChangeCount++; }; Transform transform(observer); const TransformState& state = transform.getState(); ASSERT_FALSE(state.valid()); LatLng nullIsland, latLng = {}; ScreenCoordinate center, point = {}; const uint32_t min = 0; const uint32_t max = 65535; // Cannot assign invalid sizes. std::vector invalidSizes = {{}, {min, max}, {max, min}}; for (const Size& size : invalidSizes) { try { transform.resize(size); ASSERT_TRUE(false) << "Should throw"; } catch (...) { ASSERT_TRUE(size.isEmpty()); } } Size validSize{max, max}; ASSERT_FALSE(validSize.isEmpty()); try { transform.resize(validSize); ASSERT_EQ(cameraWillChangeCount, 1u); ASSERT_EQ(cameraDidChangeCount, 1u); } catch (...) { ASSERT_TRUE(false) << "Should not throw"; } ASSERT_TRUE(state.valid()); // Double resize try { transform.resize(validSize); ASSERT_EQ(cameraWillChangeCount, 1u); ASSERT_EQ(cameraDidChangeCount, 1u); } catch (...) { ASSERT_TRUE(false) << "Should not throw"; } center = {max / 2., max / 2.}; latLng = state.screenCoordinateToLatLng(center); ASSERT_NEAR(latLng.latitude(), nullIsland.latitude(), 1e-8); ASSERT_NEAR(latLng.longitude(), nullIsland.longitude(), 1e-8); point = state.latLngToScreenCoordinate(nullIsland); ASSERT_DOUBLE_EQ(point.x, center.x); ASSERT_DOUBLE_EQ(point.y, center.y); // Constrain to screen while resizing transform.resize({1000, 500}); transform.setLatLngBounds(LatLngBounds::hull({40.0, -10.0}, {70.0, 40.0})); transform.setConstrainMode(ConstrainMode::Screen); // Request impossible zoom AnimationOptions easeOptions(Seconds(1)); transform.easeTo(CameraOptions().withCenter(LatLng{56, 11}).withZoom(1), easeOptions); ASSERT_TRUE(transform.inTransition()); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(250)); // Rotate the screen during a transition (resize it) transform.resize({500, 1000}); // The resize while constraining to screen should have stopped the transition and updated the state ASSERT_FALSE(transform.inTransition()); ASSERT_NEAR(transform.getLatLng().longitude(), 8.22103, 1e-4); ASSERT_NEAR(transform.getLatLng().latitude(), 46.6905, 1e-4); ASSERT_NEAR(transform.getState().getScale(), 38.1529, 1e-4); } TEST(Transform, LatLngBounds) { const LatLng nullIsland{}; const LatLng sanFrancisco{37.7749, -122.4194}; Transform transform; transform.resize({1000, 1000}); transform.jumpTo(CameraOptions().withCenter(LatLng()).withZoom(transform.getState().getMaxZoom())); // Default bounds. ASSERT_EQ(transform.getState().getLatLngBounds(), LatLngBounds()); ASSERT_EQ(transform.getLatLng(), nullIsland); // Invalid bounds. try { transform.setLatLngBounds(LatLngBounds::empty()); ASSERT_TRUE(false) << "Should throw"; } catch (...) { ASSERT_EQ(transform.getState().getLatLngBounds(), LatLngBounds()); } transform.jumpTo(CameraOptions().withCenter(sanFrancisco)); ASSERT_NEAR(transform.getLatLng().latitude(), sanFrancisco.latitude(), 1e-8); ASSERT_NEAR(transform.getLatLng().longitude(), sanFrancisco.longitude(), 1e-8); // Single location. transform.setLatLngBounds(LatLngBounds::singleton(sanFrancisco)); ASSERT_EQ(transform.getLatLng(), sanFrancisco); // -1 | 0 | +1 // ┌───┬───┰───┬───┰───┬───┐ // │ │ ┃• │ ┃ │ │ // ├───┼───╂───┼───╂───┼───┤ // │ │ ┃▓▓▓│▓▓▓┃ │ │ // └───┴───┸───┴───┸───┴───┘ transform.setLatLngBounds(LatLngBounds::hull({-90.0, -180.0}, {0.0, 180.0})); transform.jumpTo(CameraOptions().withCenter(sanFrancisco)); ASSERT_EQ(transform.getLatLng().latitude(), 0.0); ASSERT_EQ(transform.getLatLng().longitude(), sanFrancisco.longitude()); // Try crossing the antimeridian from the left. transform.jumpTo(CameraOptions().withCenter(LatLng{0.0, -200.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -180.0); // Try crossing the antimeridian from the right. transform.jumpTo(CameraOptions().withCenter(LatLng{0.0, 200.0})); ASSERT_DOUBLE_EQ(transform.getLatLng(LatLng::Unwrapped).longitude(), 180.0); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -180.0); // -1 | 0 | +1 // ┌───┬───┰───┬───┰───┬───┐ // │ │ ┃• │▓▓▓┃ │ │ // ├───┼───╂───┼───╂───┼───┤ // │ │ ┃ │▓▓▓┃ │ │ // └───┴───┸───┴───┸───┴───┘ transform.setLatLngBounds(LatLngBounds::hull({-90.0, 0.0}, {90.0, 180.0})); transform.jumpTo(CameraOptions().withCenter(sanFrancisco)); ASSERT_NEAR(transform.getLatLng().latitude(), sanFrancisco.latitude(), 1e-8); ASSERT_EQ(transform.getLatLng().longitude(), 0.0); // -1 | 0 | +1 // ┌───┬───┰───┬───┰───┬───┐ // │ │ ┃• │ ┃ │ │ // ├───┼───╂───┼───╂───┼───┤ // │ │ ┃ │▓▓▓┃ │ │ // └───┴───┸───┴───┸───┴───┘ transform.setLatLngBounds(LatLngBounds::hull({-90.0, 0.0}, {0.0, 180.0})); transform.jumpTo(CameraOptions().withCenter(sanFrancisco)); ASSERT_EQ(transform.getLatLng().latitude(), 0.0); ASSERT_EQ(transform.getLatLng().longitude(), 0.0); // -1 | 0 | +1 // ┌───┬───┰───┬───┰───┬───┐ // │ │ ┃ │ ▓┃▓ │ │ // ├───┼───╂───┼───╂───┼───┤ // │ │ ┃ │ ┃ │ │ // └───┴───┸───┴───┸───┴───┘ LatLng inside{45.0, 150.0}; transform.setLatLngBounds(LatLngBounds::hull({0.0, 120.0}, {90.0, 240.0})); transform.jumpTo(CameraOptions().withCenter(inside)); ASSERT_EQ(transform.getLatLng().latitude(), inside.latitude()); ASSERT_EQ(transform.getLatLng().longitude(), inside.longitude()); transform.jumpTo(CameraOptions().withCenter(LatLng{0.0, 140.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), 140.0); transform.jumpTo(CameraOptions().withCenter(LatLng{0.0, 160.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), 160.0); // Constrain latitude only. transform.jumpTo(CameraOptions().withCenter(LatLng{-45.0, inside.longitude()})); ASSERT_EQ(transform.getLatLng().latitude(), 0.0); ASSERT_EQ(transform.getLatLng().longitude(), inside.longitude()); // Crossing the antimeridian, within bounds. transform.jumpTo(CameraOptions().withCenter(LatLng{inside.latitude(), 181.0})); ASSERT_EQ(transform.getLatLng().longitude(), -179.0); // Crossing the antimeridian, outside bounds. transform.jumpTo(CameraOptions().withCenter(inside)); transform.jumpTo(CameraOptions().withCenter(LatLng{inside.latitude(), 250.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -120.0); // Constrain to the left edge. transform.jumpTo(CameraOptions().withCenter(LatLng{inside.latitude(), 119.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), 120.0); // Simulate swipe to the left. mbgl::AnimationOptions easeOptions(mbgl::Seconds(1)); easeOptions.transitionFrameFn = [&](double /* t */) { ASSERT_NEAR(transform.getLatLng().longitude(), 120.0, 1e-4); }; easeOptions.transitionFinishFn = [&]() { ASSERT_NEAR(transform.getLatLng().longitude(), 120.0, 1e-4); }; transform.moveBy(ScreenCoordinate{-500, -500}, easeOptions); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(0)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(250)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(500)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(750)); transform.updateTransitions(transform.getTransitionStart() + transform.getTransitionDuration()); // Constrain to the right edge. transform.jumpTo(CameraOptions().withCenter(LatLng{inside.latitude(), 241.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -120.0); // Simulate swipe to the right. easeOptions.transitionFrameFn = [&](double /* t */) { ASSERT_NEAR(transform.getLatLng().longitude(), -120.0, 1e-4); }; easeOptions.transitionFinishFn = [&]() { ASSERT_NEAR(transform.getLatLng().longitude(), -120.0, 1e-4); }; transform.moveBy(ScreenCoordinate{500, 500}, easeOptions); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(0)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(250)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(500)); transform.updateTransitions(transform.getTransitionStart() + Milliseconds(750)); transform.updateTransitions(transform.getTransitionStart() + transform.getTransitionDuration()); // -1 | 0 | +1 // ┌───┬───┰───┬───┰───┬───┐ // │ │ ┃ │ ┃ │ │ // ├───┼───╂───┼───╂───┼───┤ // │ │ ▓┃▓ │ ┃ │ │ // └───┴───┸───┴───┸───┴───┘ inside = LatLng{-45.0, -150.0}; transform.setLatLngBounds(LatLngBounds::hull({-90.0, -240.0}, {0.0, -120.0})); transform.jumpTo(CameraOptions().withCenter(inside)); ASSERT_DOUBLE_EQ(transform.getLatLng().latitude(), inside.latitude()); ASSERT_EQ(transform.getLatLng().longitude(), inside.longitude()); transform.jumpTo(CameraOptions().withCenter(LatLng{0.0, -140.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -140.0); transform.jumpTo(CameraOptions().withCenter(LatLng{0.0, -160.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -160.0); // Constrain latitude only. transform.jumpTo(CameraOptions().withCenter(LatLng{45.0, inside.longitude()})); ASSERT_EQ(transform.getLatLng().latitude(), 0.0); ASSERT_EQ(transform.getLatLng().longitude(), inside.longitude()); // Crossing the antimeridian, within bounds. transform.jumpTo(CameraOptions().withCenter(LatLng{inside.latitude(), -181.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().latitude(), inside.latitude()); ASSERT_EQ(transform.getLatLng().longitude(), 179.0); // Crossing the antimeridian, outside bounds. transform.jumpTo(CameraOptions().withCenter(inside)); transform.jumpTo(CameraOptions().withCenter(LatLng{inside.latitude(), -250.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), 120.0); // Constrain to the left edge. transform.jumpTo(CameraOptions().withCenter(LatLng{inside.latitude(), -119.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -120.0); transform.moveBy(ScreenCoordinate{-500, 0}); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -120.0); // Constrain to the right edge. transform.jumpTo(CameraOptions().withCenter(LatLng{inside.latitude(), -241.0})); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), 120.0); transform.moveBy(ScreenCoordinate{500, 0}); ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), 120.0); } TEST(Transform, ConstrainScreenToBounds) { Transform transform; transform.resize({500, 500}); transform.setLatLngBounds(LatLngBounds::hull({40.0, -10.0}, {70.0, 40.0})); transform.setConstrainMode(ConstrainMode::Screen); // Request impossible zoom transform.easeTo(CameraOptions().withCenter(LatLng{56, 11}).withZoom(1)); ASSERT_NEAR(transform.getZoom(), 2.81378, 1e-4); // Request impossible center left transform.easeTo(CameraOptions().withCenter(LatLng{56, -65}).withZoom(4)); ASSERT_NEAR(transform.getLatLng().longitude(), 0.98632, 1e-4); ASSERT_NEAR(transform.getLatLng().latitude(), 56.0, 1e-4); // Request impossible center top transform.easeTo(CameraOptions().withCenter(LatLng{80, 11}).withZoom(4)); ASSERT_NEAR(transform.getLatLng().longitude(), 11.0, 1e-4); ASSERT_NEAR(transform.getLatLng().latitude(), 65.88603, 1e-4); // Request impossible center right transform.easeTo(CameraOptions().withCenter(LatLng{56, 50}).withZoom(4)); ASSERT_NEAR(transform.getLatLng().longitude(), 29.01367, 1e-4); ASSERT_NEAR(transform.getLatLng().latitude(), 56.0, 1e-4); // Request impossible center bottom transform.easeTo(CameraOptions().withCenter(LatLng{30, 11}).withZoom(4)); ASSERT_NEAR(transform.getLatLng().longitude(), 11.0, 1e-4); ASSERT_NEAR(transform.getLatLng().latitude(), 47.89217, 1e-4); // Request impossible center with anchor transform.easeTo(CameraOptions().withAnchor(ScreenCoordinate{250, 250}).withCenter(LatLng{56, -65}).withZoom(4)); ASSERT_NEAR(transform.getLatLng().longitude(), 0.98632, 1e-4); ASSERT_NEAR(transform.getLatLng().latitude(), 56.0, 1e-4); // Request impossible center (anchor) transform.easeTo(CameraOptions().withAnchor(ScreenCoordinate{250, 250}).withZoom(4)); ASSERT_NEAR(transform.getLatLng().longitude(), 0.98632, 1e-4); ASSERT_NEAR(transform.getLatLng().latitude(), 56.0, 1e-4); // Fly to impossible center transform.flyTo(CameraOptions().withCenter(LatLng{56, -65}).withZoom(4)); ASSERT_NEAR(transform.getZoom(), 4.0, 1e-4); ASSERT_NEAR(transform.getLatLng().longitude(), 0.98632, 1e-4); ASSERT_NEAR(transform.getLatLng().latitude(), 56.0, 1e-4); // Fly to impossible center and zoom transform.flyTo(CameraOptions().withCenter(LatLng{56, -65}).withZoom(2)); ASSERT_NEAR(transform.getZoom(), 4.0, 1e-4); ASSERT_NEAR(transform.getLatLng().longitude(), 0.98632, 1e-4); ASSERT_NEAR(transform.getLatLng().latitude(), 56.0, 1e-4); } TEST(Transform, InvalidPitch) { Transform transform; transform.resize({1, 1}); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(0, transform.getZoom()); ASSERT_DOUBLE_EQ(0, transform.getPitch()); transform.jumpTo(CameraOptions().withZoom(1.0).withPitch(45)); ASSERT_DOUBLE_EQ(1, transform.getZoom()); ASSERT_DOUBLE_EQ(util::deg2rad(45), transform.getPitch()); const double invalid = NAN; transform.jumpTo(CameraOptions().withPitch(invalid)); ASSERT_DOUBLE_EQ(util::deg2rad(45), transform.getPitch()); transform.jumpTo(CameraOptions().withPitch(60)); ASSERT_DOUBLE_EQ(util::deg2rad(60), transform.getPitch()); } TEST(Transform, MinMaxPitch) { Transform transform; transform.resize({1, 1}); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); ASSERT_DOUBLE_EQ(0, transform.getZoom()); ASSERT_DOUBLE_EQ(0, transform.getPitch()); transform.jumpTo(CameraOptions().withZoom(1.0).withPitch(60)); ASSERT_DOUBLE_EQ(1, transform.getZoom()); ASSERT_DOUBLE_EQ(transform.getState().getMaxPitch(), transform.getPitch()); ASSERT_DOUBLE_EQ(util::deg2rad(60), transform.getPitch()); transform.setMaxPitch(70); transform.jumpTo(CameraOptions().withPitch(70)); ASSERT_DOUBLE_EQ(transform.getState().getMaxPitch(), transform.getPitch()); ASSERT_DOUBLE_EQ(util::deg2rad(60), transform.getPitch()); transform.setMaxPitch(45); transform.jumpTo(CameraOptions().withPitch(60)); ASSERT_DOUBLE_EQ(transform.getState().getMaxPitch(), transform.getPitch()); ASSERT_DOUBLE_EQ(util::deg2rad(45), transform.getPitch()); transform.jumpTo(CameraOptions().withPitch(0)); ASSERT_DOUBLE_EQ(transform.getState().getMinPitch(), transform.getPitch()); ASSERT_DOUBLE_EQ(0, transform.getPitch()); transform.setMinPitch(-10); transform.jumpTo(CameraOptions().withPitch(-10)); ASSERT_DOUBLE_EQ(transform.getState().getMinPitch(), transform.getPitch()); ASSERT_DOUBLE_EQ(0, transform.getPitch()); transform.setMinPitch(15); transform.jumpTo(CameraOptions().withPitch(0)); ASSERT_DOUBLE_EQ(transform.getState().getMinPitch(), transform.getPitch()); ASSERT_DOUBLE_EQ(util::deg2rad(15), transform.getPitch()); transform.setMinPitch(45); ASSERT_DOUBLE_EQ(util::deg2rad(45), transform.getState().getMinPitch()); transform.setMaxPitch(45); ASSERT_DOUBLE_EQ(util::deg2rad(45), transform.getState().getMaxPitch()); transform.setMaxPitch(10); ASSERT_DOUBLE_EQ(util::deg2rad(45), transform.getState().getMaxPitch()); transform.setMinPitch(60); ASSERT_DOUBLE_EQ(util::deg2rad(45), transform.getState().getMinPitch()); } static const double abs_double_error = 1e-5; MATCHER_P(Vec3NearEquals1E5, vec, "") { return std::fabs(vec[0] - arg[0]) <= abs_double_error && std::fabs(vec[1] - arg[1]) <= abs_double_error && std::fabs(vec[2] - arg[2]) <= abs_double_error; } TEST(Transform, FreeCameraOptionsInvalidSize) { Transform transform; FreeCameraOptions options; options.orientation = vec4{{1.0, 1.0, 1.0, 1.0}}; options.position = vec3{{0.1, 0.2, 0.3}}; transform.setFreeCameraOptions(options); const auto updatedOrientation = transform.getFreeCameraOptions().orientation.value(); const auto updatedPosition = transform.getFreeCameraOptions().position.value(); EXPECT_DOUBLE_EQ(0.0, updatedOrientation[0]); EXPECT_DOUBLE_EQ(0.0, updatedOrientation[1]); EXPECT_DOUBLE_EQ(0.0, updatedOrientation[2]); EXPECT_DOUBLE_EQ(1.0, updatedOrientation[3]); EXPECT_THAT(updatedPosition, Vec3NearEquals1E5(vec3{{0.0, 0.0, 0.0}})); } TEST(Transform, FreeCameraOptionsNanInput) { Transform transform; transform.resize({100, 100}); FreeCameraOptions options; options.position = vec3{{0.5, 0.5, 0.25}}; transform.setFreeCameraOptions(options); options.position = vec3{{0.0, 0.0, NAN}}; transform.setFreeCameraOptions(options); EXPECT_EQ((vec3{{0.5, 0.5, 0.25}}), transform.getFreeCameraOptions().position.value()); // Only the invalid parameter should be discarded options.position = vec3{{0.3, 0.1, 0.2}}; options.orientation = vec4{{NAN, 0.0, NAN, 0.0}}; transform.setFreeCameraOptions(options); EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.3, 0.1, 0.2}})); EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation.value()); } TEST(Transform, FreeCameraOptionsInvalidZ) { Transform transform; transform.resize({100, 100}); FreeCameraOptions options; // Invalid z-value (<= 0.0 || > 1) should be clamped to respect both min&max zoom values options.position = vec3{{0.1, 0.1, 0.0}}; transform.setFreeCameraOptions(options); EXPECT_DOUBLE_EQ(transform.getState().getMaxZoom(), transform.getState().getZoom()); EXPECT_GT(transform.getFreeCameraOptions().position.value()[2], 0.0); options.position = vec3{{0.5, 0.2, 123.456}}; transform.setFreeCameraOptions(options); EXPECT_DOUBLE_EQ(transform.getState().getMinZoom(), transform.getState().getZoom()); EXPECT_LE(transform.getFreeCameraOptions().position.value()[2], 1.0); } TEST(Transform, FreeCameraOptionsInvalidOrientation) { // Invalid orientations that cannot be clamped into a valid range Transform transform; transform.resize({100, 100}); FreeCameraOptions options; options.orientation = vec4{{0.0, 0.0, 0.0, 0.0}}; transform.setFreeCameraOptions(options); EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation); // Gimbal lock. Both forward and up vectors are on xy-plane options.orientation = Quaternion::fromAxisAngle(vec3{{0.0, 1.0, 0.0}}, pi / 2).m; transform.setFreeCameraOptions(options); EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation); // Camera is upside down options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, pi / 2 + pi / 4).m; transform.setFreeCameraOptions(options); EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation); } TEST(Transform, FreeCameraOptionsSetOrientation) { Transform transform; transform.resize({100, 100}); FreeCameraOptions options; options.orientation = Quaternion::identity.m; transform.setFreeCameraOptions(options); EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getPitch()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getX()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getY()); options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, util::deg2rad(-60.0)).m; transform.setFreeCameraOptions(options); EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing()); EXPECT_DOUBLE_EQ(util::deg2rad(60.0), transform.getState().getPitch()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getX()); EXPECT_DOUBLE_EQ(206.0, transform.getState().getY()); options.orientation = Quaternion::fromAxisAngle(vec3{{0.0, 0.0, 1.0}}, util::deg2rad(56.0)).m; transform.setFreeCameraOptions(options); EXPECT_DOUBLE_EQ(util::deg2rad(-56.0), transform.getState().getBearing()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getPitch()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getX()); EXPECT_NEAR(152.192378, transform.getState().getY(), 1e-6); options.orientation = Quaternion::fromEulerAngles(0.0, 0.0, util::deg2rad(-179.0)) .multiply(Quaternion::fromEulerAngles(util::deg2rad(-30.0), 0.0, 0.0)) .m; transform.setFreeCameraOptions(options); EXPECT_DOUBLE_EQ(util::deg2rad(179.0), transform.getState().getBearing()); EXPECT_DOUBLE_EQ(util::deg2rad(30.0), transform.getState().getPitch()); EXPECT_NEAR(1.308930, transform.getState().getX(), 1e-6); EXPECT_NEAR(56.813889, transform.getState().getY(), 1e-6); } static std::tuple rotatedFrame(const std::array& quaternion) { Quaternion q(quaternion); return std::make_tuple( q.transform({{1.0, 0.0, 0.0}}), q.transform({{0.0, -1.0, 0.0}}), q.transform({{0.0, 0.0, -1.0}})); } TEST(Transform, FreeCameraOptionsClampPitch) { Transform transform; transform.resize({100, 100}); FreeCameraOptions options; vec3 right, up, forward; options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, util::deg2rad(-85.0)).m; transform.setFreeCameraOptions(options); EXPECT_DOUBLE_EQ(util::PITCH_MAX, transform.getState().getPitch()); std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value()); EXPECT_THAT(right, Vec3NearEquals1E5(vec3{{1.0, 0.0, 0.0}})); EXPECT_THAT(up, Vec3NearEquals1E5(vec3{{0, -0.5, 0.866025}})); EXPECT_THAT(forward, Vec3NearEquals1E5(vec3{{0, -0.866025, -0.5}})); } TEST(Transform, FreeCameraOptionsClampToBounds) { Transform transform; transform.resize({100, 100}); transform.setConstrainMode(ConstrainMode::WidthAndHeight); transform.jumpTo(CameraOptions().withZoom(8.56)); FreeCameraOptions options; // Place camera to an arbitrary position looking away from the map options.position = vec3{{-100.0, -10000.0, 1000.0}}; options.orientation = Quaternion::fromEulerAngles(util::deg2rad(-45.0), 0.0, 0.0).m; transform.setFreeCameraOptions(options); // Map center should be clamped to width/2 pixels away from map borders EXPECT_DOUBLE_EQ(206.0, transform.getState().getX()); EXPECT_DOUBLE_EQ(206.0, transform.getState().getY()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing()); EXPECT_DOUBLE_EQ(util::deg2rad(45.0), transform.getState().getPitch()); vec3 right, up, forward; std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value()); EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.0976562, 0.304816, 0.20716}})); EXPECT_THAT(right, Vec3NearEquals1E5(vec3{{1.0, 0.0, 0.0}})); EXPECT_THAT(up, Vec3NearEquals1E5(vec3{{0, -0.707107, 0.707107}})); EXPECT_THAT(forward, Vec3NearEquals1E5(vec3{{0, -0.707107, -0.707107}})); } TEST(Transform, FreeCameraOptionsInvalidState) { Transform transform; // Invalid size EXPECT_DOUBLE_EQ(0.0, transform.getState().getX()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getY()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing()); EXPECT_DOUBLE_EQ(0.0, transform.getState().getPitch()); const auto options = transform.getFreeCameraOptions(); EXPECT_THAT(options.position.value(), Vec3NearEquals1E5(vec3{{0.0, 0.0, 0.0}})); } TEST(Transform, FreeCameraOptionsOrientationRoll) { Transform transform; FreeCameraOptions options; transform.resize({100, 100}); const auto orientationWithoutRoll = Quaternion::fromEulerAngles(-pi / 4, 0.0, 0.0); const auto orientationWithRoll = orientationWithoutRoll.multiply(Quaternion::fromEulerAngles(0.0, 0.0, pi / 4)); options.orientation = orientationWithRoll.m; transform.setFreeCameraOptions(options); options = transform.getFreeCameraOptions(); EXPECT_NEAR(options.orientation.value()[0], orientationWithoutRoll.x, 1e-9); EXPECT_NEAR(options.orientation.value()[1], orientationWithoutRoll.y, 1e-9); EXPECT_NEAR(options.orientation.value()[2], orientationWithoutRoll.z, 1e-9); EXPECT_NEAR(options.orientation.value()[3], orientationWithoutRoll.w, 1e-9); EXPECT_NEAR(util::deg2rad(45.0), transform.getState().getPitch(), 1e-9); EXPECT_NEAR(0.0, transform.getState().getBearing(), 1e-9); EXPECT_NEAR(0.0, transform.getState().getX(), 1e-9); EXPECT_NEAR(150.0, transform.getState().getY(), 1e-9); } TEST(Transform, FreeCameraOptionsStateSynchronization) { Transform transform; transform.resize({100, 100}); vec3 right, up, forward; transform.jumpTo(CameraOptions().withPitch(0.0).withBearing(0.0)); std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value()); EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.5, 0.5, 0.29296875}})); EXPECT_THAT(right, Vec3NearEquals1E5(vec3{{1.0, 0.0, 0.0}})); EXPECT_THAT(up, Vec3NearEquals1E5(vec3{{0.0, -1.0, 0.0}})); EXPECT_THAT(forward, Vec3NearEquals1E5(vec3{{0.0, 0.0, -1.0}})); transform.jumpTo(CameraOptions().withCenter(LatLng{60.1699, 24.9384})); EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.569273, 0.289453, 0.292969}})); transform.jumpTo(CameraOptions().withPitch(20.0).withBearing(77.0).withCenter(LatLng{-20.0, 20.0})); EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.457922, 0.57926, 0.275301}})); // Invalid pitch transform.jumpTo(CameraOptions().withPitch(-10.0).withBearing(0.0)); std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value()); EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.555556, 0.556719, 0.292969}})); EXPECT_THAT(right, Vec3NearEquals1E5(vec3{{1.0, 0.0, 0.0}})); EXPECT_THAT(up, Vec3NearEquals1E5(vec3{{0.0, -1.0, 0.0}})); EXPECT_THAT(forward, Vec3NearEquals1E5(vec3{{0.0, 0.0, -1.0}})); transform.jumpTo(CameraOptions().withPitch(85.0).withBearing(0.0).withCenter(LatLng{-80.0, 0.0})); std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value()); EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals1E5(vec3{{0.5, 1.14146, 0.146484}})); EXPECT_THAT(right, Vec3NearEquals1E5(vec3{{1.0, 0.0, 0.0}})); EXPECT_THAT(up, Vec3NearEquals1E5(vec3{{0, -0.5, 0.866025}})); EXPECT_THAT(forward, Vec3NearEquals1E5(vec3{{0, -0.866025, -0.5}})); }