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);
}
 static std::string convertToLatLngStr(const LatLng &loc, int prec) {
     std::string locStr = "";
     locStr += Utility::truncateDouble(loc.getLat(), prec);
     locStr += ",";
     locStr += Utility::truncateDouble(loc.getLng(), prec);
     return locStr;
 }    
 static Point<double> project_(const LatLng& latLng, double worldSize) {
     const double latitude = util::clamp(latLng.latitude(), -util::LATITUDE_MAX, util::LATITUDE_MAX);
     return Point<double> {
         util::LONGITUDE_MAX + latLng.longitude(),
         util::LONGITUDE_MAX - util::RAD2DEG * std::log(std::tan(M_PI / 4 + latitude * M_PI / util::DEGREES_MAX))
     } * worldSize / util::DEGREES_MAX;
 }
TEST(Transform, UnwrappedLatLng) {
    MockView view;
    Transform transform(view, ConstrainMode::HeightOnly);
    transform.resize({{ 1000, 1000 }});
    transform.setScale(2 << 9);
    transform.setPitch(0.9);
    transform.setLatLng(LatLng(38, -77));

    const TransformState& state = transform.getState();

    LatLng fromGetLatLng = state.getLatLng();
    ASSERT_DOUBLE_EQ(fromGetLatLng.latitude, 38);
    ASSERT_DOUBLE_EQ(fromGetLatLng.longitude, -77);

    LatLng fromScreenCoordinate = state.screenCoordinateToLatLng({ 500, 500 });
    ASSERT_NEAR(fromScreenCoordinate.latitude,   37.999999999999829, 0.0001); // 1.71E-13
    ASSERT_NEAR(fromScreenCoordinate.longitude, -76.999999999999773, 0.0001); // 2.27E-13

    LatLng wrappedForwards = state.screenCoordinateToLatLng(state.latLngToScreenCoordinate({ 38, 283 }));
    ASSERT_NEAR(wrappedForwards.latitude, 37.999999999999716, 0.0001); // 2.84E-13
    ASSERT_NEAR(wrappedForwards.longitude, 282.99999999988751, 0.0001); // 1.1249E-11
    wrappedForwards.wrap();
    ASSERT_NEAR(wrappedForwards.longitude, -77.000000000112493, 0.001); // 1.1249E-11

    LatLng wrappedBackwards = state.screenCoordinateToLatLng(state.latLngToScreenCoordinate({ 38, -437 }));
    ASSERT_NEAR(wrappedBackwards.latitude, wrappedForwards.latitude, 0.001);
    ASSERT_NEAR(wrappedBackwards.longitude, -436.99999999988728, 0.001); // 1.1272E-11
    wrappedBackwards.wrap();
    ASSERT_NEAR(wrappedBackwards.longitude, -76.99999999988728, 0.001); // 1.1272E-11
}
 LatLng constrain(const LatLng& p) const {
     if (contains(p)) {
         return p;
     }
     return LatLng {
         util::clamp(p.latitude(), sw.latitude(), ne.latitude()),
         util::clamp(p.longitude(), sw.longitude(), ne.longitude())
     };
 }
Beispiel #6
0
webViewBridge::webViewBridge(LatLng latLng, int zoom, QString maptype, QObject *parent)
 : QObject(parent)
{
 this->_latLng = latLng;
 this->_lat = latLng.lat();
 this->_lon = latLng.lon();
 this->_zoom = zoom;
 this->maptype = maptype;
 _instance = this;
}
ScreenCoordinate Transform::latLngToScreenCoordinate(const LatLng& latLng) const {
    // If the center and point longitudes are not in the same side of the
    // antimeridian, we unwrap the point longitude so it would be seen if
    // e.g. the next antimeridian side is visible.
    LatLng unwrappedLatLng = latLng.wrapped();
    unwrappedLatLng.unwrapForShortestPath(getLatLng());
    ScreenCoordinate point = state.latLngToScreenCoordinate(unwrappedLatLng);
    point.y = state.height - point.y;
    return point;
}
    static ProjectedMeters projectedMetersForLatLng(const LatLng& latLng) {
        const double constrainedLatitude = util::clamp(latLng.latitude(), -util::LATITUDE_MAX, util::LATITUDE_MAX);
        const double constrainedLongitude = util::clamp(latLng.longitude(), -util::LONGITUDE_MAX, util::LONGITUDE_MAX);

        const double m = 1 - 1e-15;
        const double f = util::clamp(std::sin(util::DEG2RAD * constrainedLatitude), -m, m);

        const double easting  = util::EARTH_RADIUS_M * constrainedLongitude * util::DEG2RAD;
        const double northing = 0.5 * util::EARTH_RADIUS_M * std::log((1 + f) / (1 - f));

        return ProjectedMeters(northing, easting);
    }
 // compute pick up distance
 static double computePickupDistance_savingsConstr(
         const time_t masterETA, const double masterOrigLat, const double masterOrigLng, 
         const time_t masterETD, const double masterDestLat, const double masterDestLng, 
         const time_t minionReqTime, const double minionOrigLat, const double minionOrigLng, 
         int inclInitDistExtendedMatches
 ) {                
     double pickupDist = 0.0;
     
     bool isExtended = (masterETA <= minionReqTime);
     
     // case 1: non-extended match
     if( !isExtended ) {
         pickupDist = Utility::computeGreatCircleDistance(masterOrigLat, masterOrigLng, minionOrigLat, minionOrigLng);
     }
     
     // case 2: extended match
     else {
         // get the master location at time of minion request
         LatLng estLocAtMinReq = Utility::estLocationByLinearProxy(minionReqTime, masterETA, masterOrigLat, masterOrigLng, masterETD, masterDestLat, masterDestLng);
         
         // compute dist between master origin and curr location
         const double distFromOrigToCurrLoc = (inclInitDistExtendedMatches) ? Utility::computeGreatCircleDistance(masterOrigLat, masterOrigLng, estLocAtMinReq.getLat(), estLocAtMinReq.getLng()) : 0.0;
         
         // compute dist between curr location and minion origin
         const double distFromCurrLocToMinion = Utility::computeGreatCircleDistance(estLocAtMinReq.getLat(), estLocAtMinReq.getLng(), minionOrigLat, minionOrigLng);
         
         pickupDist = distFromOrigToCurrLoc + distFromCurrLocToMinion;               
     }
     
     return pickupDist;
 }
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 mapbox-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);
}
void MapSnapshotter::Impl::snapshot(ActorRef<MapSnapshotter::Callback> callback) {
    map.renderStill([this, callback = std::move(callback)] (std::exception_ptr error) mutable {

        // Create lambda that captures the current transform state
        // and can be used to translate for geographic to screen
        // coordinates
        assert (frontend.getTransformState());
        PointForFn pointForFn { [=, center=map.getLatLng(), transformState = *frontend.getTransformState()] (const LatLng& latLng) {
            LatLng unwrappedLatLng = latLng.wrapped();
            unwrappedLatLng.unwrapForShortestPath(center);
            Transform transform { transformState };
            return transform.latLngToScreenCoordinate(unwrappedLatLng);
        }};

        // Create lambda that captures the current transform state
        // and can be used to translate for geographic to screen
        // coordinates
        assert (frontend.getTransformState());
        LatLngForFn latLngForFn { [=, transformState = *frontend.getTransformState()] (const ScreenCoordinate& screenCoordinate) {
            Transform transform { transformState };
            return transform.screenCoordinateToLatLng(screenCoordinate);
        }};

        // Collect all source attributions
        std::vector<std::string> attributions;
        for (auto source : map.getStyle().getSources()) {
            auto attribution = source->getAttribution();
            if (attribution) {
                attributions.push_back(*attribution);
            }
        }

        // Invoke callback
        callback.invoke(
                &MapSnapshotter::Callback::operator(),
                error,
                error ? PremultipliedImage() : frontend.readStillImage(),
                std::move(attributions),
                std::move(pointForFn),
                std::move(latLngForFn)
        );
    });
}
void TransformState::setLatLngZoom(const LatLng& latLng, double zoom) {
    LatLng constrained = latLng;
    if (bounds) {
        constrained = bounds->constrain(latLng);
    }

    double newScale = util::clamp(zoomScale(zoom), min_scale, max_scale);
    const double newWorldSize = newScale * util::tileSize;
    Bc = newWorldSize / util::DEGREES_MAX;
    Cc = newWorldSize / util::M2PI;

    const double m = 1 - 1e-15;
    const double f = util::clamp(std::sin(util::DEG2RAD * constrained.latitude()), -m, m);

    ScreenCoordinate point = {
        -constrained.longitude() * Bc,
        0.5 * Cc * std::log((1 + f) / (1 - f)),
    };
    setScalePoint(newScale, point);
}
 //static double getPickupDistanceAtTimeOfMinionRequest(Request * pRequest, OpenTrip * pOpenTrip) {
 static double getPickupDistanceAtTimeOfMinionRequest_maxPickupConstr(
         const time_t minionReqTime,         // request time
         const double minionPickupLat,       // request lat
         const double minionPickupLng,       // request lng
         const time_t masterPickupTime,      // previous event time
         const double masterPickupLat,       // previous event start lat
         const double masterPickupLng,       // previous event start lng
         const time_t masterDropoffTime,     // next event time
         const double masterDropLat,         // next event end lat
         const double masterDropLng,         // next event end lng
         const time_t masterDispatchTime,    // dispatch time
         const double masterDispatchLat,     // dispatch lat
         const double masterDispatchLng      // dispatch lng
         ) {
     
     // case 1: pickup has NOT occurred (non-extended)
     if( minionReqTime <= masterPickupTime ) {
         
         // pickup distance from current location to minion origin
         LatLng estMasterLocation = Utility::estLocationByLinearProxy(minionReqTime, masterDispatchTime, masterDispatchLat, masterDispatchLng, masterPickupTime, masterPickupLat, masterPickupLng);
         double pickupDistance_driverToMaster = Utility::computeGreatCircleDistance(estMasterLocation.getLat(), estMasterLocation.getLng(), masterPickupLat, masterPickupLng);
         
         // pickup distance from master to minion
         double pickupDistance_masterToMinion = Utility::computeGreatCircleDistance(masterPickupLat, masterPickupLng, minionPickupLat, minionPickupLng);
         
         double pickupDistance_total = pickupDistance_driverToMaster + pickupDistance_masterToMinion;
         return pickupDistance_total;
     }
     
     // case 2: pickup HAS occurred (extended)
     else {
         // estimate location     
         LatLng estLocAtTimeOfRequest = Utility::estLocationByLinearProxy(minionReqTime, masterPickupTime, masterPickupLat, masterPickupLng, masterDropoffTime, masterDropLat, masterDropLng);
         
         // pickup distance is 
         double pickupDistance = Utility::computeGreatCircleDistance(estLocAtTimeOfRequest.getLat(), estLocAtTimeOfRequest.getLng(), minionPickupLat, minionPickupLng);
         
         return pickupDistance;
     }                
 }
 static double getPickupDistance_maxPickupConstr_route(Route * pRoute, Request * pRequest, int pickupOrder) {
     
     // ensure there are two existing pickups
     assert( pRoute->getPickupEvents()->size() == 2 );
     
     const time_t reqTime = pRequest->getReqTime();
             
     const Event * pDispatch = pRoute->getDispatchEvent();
     RouteEvent * pFirstSchedPick  = pRoute->getPickupEvents()->front();
     RouteEvent * pSecondSchedPick = pRoute->getPickupEvents()->back();
     
     // case 1: the request is to be the first pickup in the new route
     if ( pickupOrder == 1 ) {
         // 1.A the request occurs BEFORE the first sched pickup
         if( reqTime < pFirstSchedPick->getEventTime() ) {
             LatLng currLoc = Utility::estLocationByLinearProxy(reqTime, pDispatch->timeT, pDispatch->lat, pDispatch->lng, pFirstSchedPick->getEventTime(), pFirstSchedPick->getLat(), pFirstSchedPick->getLng());
             double distToCurrReq = Utility::computeGreatCircleDistance(currLoc.getLat(), currLoc.getLng(), pRequest->getActualPickupEvent()->lat, pRequest->getActualPickupEvent()->lng);
             return distToCurrReq;
         }
         // 1.B the request already occurred and request is in between first and second scheduled pickup
         else if( (pFirstSchedPick->getEventTime() <= reqTime) && (reqTime <= pSecondSchedPick->getEventTime()) ) {
             LatLng currLoc = Utility::estLocationByLinearProxy(reqTime, pFirstSchedPick->getEventTime(), pFirstSchedPick->getLat(), pFirstSchedPick->getLng(), pSecondSchedPick->getEventTime(), pSecondSchedPick->getLat(), pSecondSchedPick->getLng());
             double distToCurrReq = Utility::computeGreatCircleDistance(currLoc.getLat(), currLoc.getLng(), pRequest->getActualPickupEvent()->lat, pRequest->getActualPickupEvent()->lng);
             return distToCurrReq;
         }
     }
     
     // case 2: the request is not to be the first pickup in the new route
     else {
         // get prior pickup event
         RouteEvent * pPriorPickup = pRoute->getPickupEvents()->at(pickupOrder-2); // e.g. if the 2nd pickup, then the prior is the 1st pickup which is index 0
         
         // 2.A: the request occurs BEFORE the pickup of the prior event
         if( reqTime < pPriorPickup->getEventTime() ) {
             double distToCurrReq = Utility::computeGreatCircleDistance(pPriorPickup->getLat(), pPriorPickup->getLng(), pRequest->getActualPickupEvent()->lat, pRequest->getActualPickupEvent()->lng);
             return distToCurrReq;
         }
         
         // 2.B: the requests occurs AFTER pickup of the prior event
         else {
             RouteEvent * pNextEvent = (pickupOrder == 2) ? pRoute->getPickupEvents()->back() : pRoute->getDropoffEvents()->front();
             LatLng currLoc = Utility::estLocationByLinearProxy(reqTime, pPriorPickup->getEventTime(), pPriorPickup->getLat(), pPriorPickup->getLng(), pNextEvent->getEventTime(), pNextEvent->getLat(), pNextEvent->getLng());
             double distToCurrReq = Utility::computeGreatCircleDistance(currLoc.getLat(), currLoc.getLng(), pRequest->getActualPickupEvent()->lat, pRequest->getActualPickupEvent()->lng);
             return distToCurrReq;
         }
         
     }
     
     cout << "** unhandled case when checking for pickup distance to minion req **\n" << endl;
     exit(1);
     return 0.0;
 }
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(M_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(-45.0 * util::DEG2RAD, 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(10.0 * util::DEG2RAD, 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(15.0 * util::DEG2RAD, 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(20.0 * util::DEG2RAD, 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(Projection, Boundaries) {
    LatLng sw { -90.0, -180.0 };
    LatLng ne { 90.0, 180.0 };

    const double minScale = std::pow(2, 0);
    const double maxScale = std::pow(2, util::DEFAULT_MAX_ZOOM);

    Point<double> projected {};
    LatLng unprojected {};

    projected = Projection::project(sw, minScale);
    EXPECT_DOUBLE_EQ(projected.x, 0.0);
    EXPECT_DOUBLE_EQ(projected.y, util::tileSize);

    unprojected = Projection::unproject(projected, minScale);
    EXPECT_DOUBLE_EQ(unprojected.latitude(), -util::LATITUDE_MAX);
    EXPECT_DOUBLE_EQ(unprojected.longitude(), sw.longitude());

    projected = Projection::project(sw, maxScale);
    EXPECT_DOUBLE_EQ(projected.x, 0.0);
    EXPECT_DOUBLE_EQ(projected.y, util::tileSize * maxScale);

    unprojected = Projection::unproject(projected, maxScale);
    EXPECT_DOUBLE_EQ(unprojected.latitude(), -util::LATITUDE_MAX);
    EXPECT_DOUBLE_EQ(unprojected.longitude(), sw.longitude());

    projected = Projection::project(ne, minScale);
    EXPECT_DOUBLE_EQ(projected.x, util::tileSize);
    ASSERT_NEAR(projected.y, 0.0, 1e-10);

    unprojected = Projection::unproject(projected, minScale);
    EXPECT_DOUBLE_EQ(unprojected.latitude(), util::LATITUDE_MAX);
    EXPECT_DOUBLE_EQ(unprojected.longitude(), ne.longitude());

    projected = Projection::project(ne, maxScale);
    EXPECT_DOUBLE_EQ(projected.x, util::tileSize * maxScale);
    ASSERT_NEAR(projected.y, 0.0, 1e-6);

    unprojected = Projection::unproject(projected, maxScale);
    EXPECT_DOUBLE_EQ(unprojected.latitude(), util::LATITUDE_MAX);
    EXPECT_DOUBLE_EQ(unprojected.longitude(), ne.longitude());
}
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::unbounded());
    ASSERT_EQ(transform.getLatLng(), nullIsland);

    // Invalid bounds.
    try {
        transform.setLatLngBounds(LatLngBounds::empty());
        ASSERT_TRUE(false) << "Should throw";
    } catch (...) {
        ASSERT_EQ(transform.getState().getLatLngBounds(), LatLngBounds::unbounded());
    }

    transform.jumpTo(CameraOptions().withCenter(sanFrancisco));
    ASSERT_EQ(transform.getLatLng(), sanFrancisco);

    // 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_EQ(transform.getLatLng().latitude(), sanFrancisco.latitude());
    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, 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);
}
const std::array<float, 2> RenderHillshadeLayer::getLatRange(const UnwrappedTileID& id) {
   const LatLng latlng0 = LatLng(id);
   const LatLng latlng1 = LatLng(UnwrappedTileID(id.canonical.z, id.canonical.x, id.canonical.y + 1));
   return {{ (float)latlng0.latitude(), (float)latlng1.latitude() }};
}
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);
}
 PointAnnotation(const LatLng& position_, const std::string& icon_ = "")
     : position(position_.wrapped()), icon(icon_) {}
/** 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 LatLng latLng = camera.center.value_or(getLatLng()).wrapped();
    double zoom = camera.zoom.value_or(getZoom());
    double angle = camera.angle.value_or(getAngle());
    double pitch = camera.pitch.value_or(getPitch());

    if (!latLng || std::isnan(zoom)) {
        return;
    }
    
    // Determine endpoints.
    EdgeInsets padding;
    if (camera.padding) padding = *camera.padding;
    LatLng startLatLng = getLatLng(padding).wrapped();
    startLatLng.unwrapForShortestPath(latLng);

    const ScreenCoordinate startPoint = {
        state.lngX(startLatLng.longitude),
        state.latY(startLatLng.latitude),
    };
    const ScreenCoordinate endPoint = {
        state.lngX(latLng.longitude),
        state.latY(latLng.latitude),
    };
    ScreenCoordinate center = getScreenCoordinate(padding);
    center.y = state.height - center.y;
    
    // Constrain camera options.
    zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
    pitch = util::clamp(pitch, 0., util::PITCH_MAX);
    
    // Minimize rotation by taking the shorter path around the circle.
    angle = _normalizeAngle(angle, state.angle);
    state.angle = _normalizeAngle(state.angle, angle);
    
    const double startZoom = state.scaleZoom(state.scale);
    const double startAngle = state.angle;
    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 = padding ? std::max(state.width, state.height)
                        : std::max(state.width - padding.left - padding.right,
                                   state.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);
    };
    
    // When u₀ = u₁, the optimal path doesn’t require both ascent and descent.
    bool isClose = std::abs(u1) < 0.000001;
    // Perform a more or less instantaneous transition if the path is too short.
    if (isClose && std::abs(w0 - w1) < 0.000001) {
        easeTo(camera, animation);
        return;
    }
    
    /// r₀: Zoom-out factor during ascent.
    double r0 = r(0);
    /** 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)
                : ((r(1) - 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 startWorldSize = state.worldSize();
    state.Bc = startWorldSize / util::DEGREES_MAX;
    state.Cc = startWorldSize / util::M2PI;
    
    state.panning = true;
    state.scaling = true;
    state.rotating = angle != startAngle;
    
    startTransition(camera, animation, [=](double k) {
        /// s: The distance traveled along the flight path, measured in
        /// ρ-screenfuls.
        double s = k * S;
        double us = u(s);
        
        // Calculate the current point and zoom level along the flight path.
        ScreenCoordinate framePoint = util::interpolate(startPoint, endPoint, us);
        double frameZoom = startZoom + state.scaleZoom(1 / w(s));
        
        // Convert to geographic coordinates and set the new viewpoint.
        LatLng frameLatLng = {
            state.yLat(framePoint.y, startWorldSize),
            state.xLng(framePoint.x, startWorldSize),
        };
        state.setLatLngZoom(frameLatLng, frameZoom);
        
        if (angle != startAngle) {
            state.angle = util::wrap(util::interpolate(startAngle, angle, k), -M_PI, M_PI);
        }
        if (pitch != startPitch) {
            state.pitch = util::interpolate(startPitch, pitch, k);
        }
        
        if (padding) {
            state.moveLatLng(frameLatLng, center);
        }
        return Update::RecalculateStyle;
    }, duration);
}
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);
}
/**
 * Change any combination of center, zoom, bearing, and pitch, 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 LatLng unwrappedLatLng = camera.center.value_or(getLatLng());
    const LatLng latLng = unwrappedLatLng.wrapped();
    double zoom = camera.zoom.value_or(getZoom());
    double angle = camera.angle.value_or(getAngle());
    double pitch = camera.pitch.value_or(getPitch());

    if (!latLng || std::isnan(zoom)) {
        return;
    }

    // Determine endpoints.
    EdgeInsets padding;
    if (camera.padding) padding = *camera.padding;
    LatLng startLatLng = getLatLng(padding);
    // If gesture in progress, we transfer the world rounds from the end
    // longitude into start, so we can guarantee the "scroll effect" of rounding
    // the world while assuring the end longitude remains wrapped.
    if (isGestureInProgress()) startLatLng.longitude -= unwrappedLatLng.longitude - latLng.longitude;
    // Find the shortest path otherwise.
    else startLatLng.unwrapForShortestPath(latLng);

    const ScreenCoordinate startPoint = {
        state.lngX(startLatLng.longitude),
        state.latY(startLatLng.latitude),
    };
    const ScreenCoordinate endPoint = {
        state.lngX(latLng.longitude),
        state.latY(latLng.latitude),
    };
    ScreenCoordinate center = getScreenCoordinate(padding);
    center.y = state.height - center.y;
    
    // Constrain camera options.
    zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
    const double scale = state.zoomScale(zoom);
    pitch = util::clamp(pitch, 0., util::PITCH_MAX);
    
    Update update = state.getZoom() == zoom ? Update::Repaint : Update::RecalculateStyle;
    
    // Minimize rotation by taking the shorter path around the circle.
    angle = _normalizeAngle(angle, state.angle);
    state.angle = _normalizeAngle(state.angle, angle);

    Duration duration = animation.duration ? *animation.duration : Duration::zero();
    
    const double startWorldSize = state.worldSize();
    state.Bc = startWorldSize / util::DEGREES_MAX;
    state.Cc = startWorldSize / util::M2PI;
    
    const double startScale = state.scale;
    const double startAngle = state.angle;
    const double startPitch = state.pitch;
    state.panning = latLng != startLatLng;
    state.scaling = scale != startScale;
    state.rotating = angle != startAngle;

    startTransition(camera, animation, [=](double t) {
        ScreenCoordinate framePoint = util::interpolate(startPoint, endPoint, t);
        LatLng frameLatLng = {
            state.yLat(framePoint.y, startWorldSize),
            state.xLng(framePoint.x, startWorldSize)
        };
        double frameScale = util::interpolate(startScale, scale, t);
        state.setLatLngZoom(frameLatLng, state.scaleZoom(frameScale));
        
        if (angle != startAngle) {
            state.angle = util::wrap(util::interpolate(startAngle, angle, t), -M_PI, M_PI);
        }
        if (pitch != startPitch) {
            state.pitch = util::interpolate(startPitch, pitch, t);
        }
        
        if (padding) {
            state.moveLatLng(frameLatLng, center);
        }
        return update;
    }, duration);
}
Beispiel #25
0
//------------------------------------------------------------------------
double PolylineEncoder::distance(const LatLng &p0, const LatLng &p1, const LatLng &p2) 
{
  double out = 0.0;
  if (p1.lat() == p2.lat() && p1.lng() == p2.lng()) {
    out = hdist(p2, p0);
  }
  else {
    double dlat = (p2.lat()-p1.lat());
    double dlng = (p2.lng()-p1.lng());
    double u = ((p0.lat()-p1.lat())*dlat+(p0.lng()-p1.lng())*dlng)/(dlat*dlat + dlng*dlng);
  
    if (u <= 0) {
      out = hdist(p0, p1);
    }
    else if(u >= 1) {
      out = hdist(p0, p2);
    }
    else {
      out = hdist(p0, LatLng(p1.lat() + u*dlat, p1.lng() + u*dlng));
    }
  }
  return out;
}
NearestEdgeInfo GraphStorage::GetNearestEdge(const LatLng &source, const double &radius) const
{
    // Get all nodes within a radius
    vector<int> nodes = kd_tree.RadiusSearch(source, radius);

    // Get all edges within that radius
    // We want to get a set of edges and avoid adding
    // the same A->B and B->A edge twice
    set<Edge, function<bool(const Edge &, const Edge &)>>
                edges(
                    // Custom compare function to avoid adding A->B and B->A edges twice
                    [](const Edge &e1, const Edge &e2)
                    {
                        const int x = min(e1.u, e1.v);
                        const int y = min(e2.u, e2.v);
                        if (x < y)
                            return true;
                        else if (y > x)
                            return false;
                        else
                            return max(e1.u, e1.v) < max(e2.u, e2.v);
                    }
                );

    // Fill in a set of (undirected) edges
    // For each node
    for (int i = 0; i < nodes.size(); ++i)
    {
        const int A = nodes[i];

        // For each edge that starts from this node
        for (int j = 0; j < adjacency_lists[A].size(); ++j)
        {
            const auto &e = adjacency_lists[A][j];
            if (e.is_startpoint) // only startpoints
            {
                const int B = e.v;
                if (e.edge_type == Edge::BACKWARD_EDGE)
                    edges.insert(Edge(B, A, e.w, e.edge_type));
                else
                    edges.insert(Edge(A, B, e.w, e.edge_type));
            }
        }
    }

    auto current_best = NearestEdgeInfo();

    if (edges.empty())
    {
        return current_best;
    }

    // Find nearest edge
    const double    source_lat    = source.getLat();
    const double    source_lng    = source.getLng();
    double          best_distance = numeric_limits<double>::max();
    for (const auto &e : edges)
    {
        double clamped_ratio;
        LatLng projected_point;

        tie(clamped_ratio, projected_point) = LatLng::ProjectOnSegment(source_lat, source_lng,
                                                                       coordinates[e.u], coordinates[e.v]);
        const double current_distance = LatLng::DistanceInMeters(source_lat, source_lng,
                                                                 projected_point.getLat(), projected_point.getLng());
        // Update nearest edge information
        if (current_distance < best_distance)
        {
            best_distance = current_distance;
            current_best  = NearestEdgeInfo(e, projected_point, clamped_ratio, true);
        }
    }
    return current_best;
}
Beispiel #27
0
//------------------------------------------------------------------------
static double hdist(const LatLng &a, const LatLng &b) {
  return hypotenuse(a.lat()-b.lat(), a.lng()-b.lng());
}
 void extend(const LatLng& point) {
     sw = LatLng(std::min(point.latitude(), sw.latitude()),
                 std::min(point.longitude(), sw.longitude()));
     ne = LatLng(std::max(point.latitude(), ne.latitude()),
                 std::max(point.longitude(), ne.longitude()));
 }
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);
}