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()) }; }
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); }
//------------------------------------------------------------------------ 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; }
//------------------------------------------------------------------------ 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); }