diff --git a/CHANGES.md b/CHANGES.md index f8dc996af..e6f45097a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,12 @@ ### ? - ? +##### Additions :tada: + +- Added `CesiumIonClient::Connection::geocode` method for making geocoding queries against the Cesium ion geocoder API. + ##### Fixes :wrench: + - Fixed a bug in `thenPassThrough` that caused a compiler error when given a value by r-value refrence. ### v0.42.0 - 2024-12-02 diff --git a/CesiumIonClient/CMakeLists.txt b/CesiumIonClient/CMakeLists.txt index cb2621ed5..8976d6117 100644 --- a/CesiumIonClient/CMakeLists.txt +++ b/CesiumIonClient/CMakeLists.txt @@ -40,6 +40,7 @@ target_link_libraries(CesiumIonClient PUBLIC CesiumAsync CesiumUtility + CesiumGeospatial PRIVATE picosha2::picosha2 modp_b64::modp_b64 diff --git a/CesiumIonClient/include/CesiumIonClient/Connection.h b/CesiumIonClient/include/CesiumIonClient/Connection.h index 088522a87..8c634ab2d 100644 --- a/CesiumIonClient/include/CesiumIonClient/Connection.h +++ b/CesiumIonClient/include/CesiumIonClient/Connection.h @@ -3,6 +3,7 @@ #include "ApplicationData.h" #include "Assets.h" #include "Defaults.h" +#include "Geocoder.h" #include "Profile.h" #include "Response.h" #include "Token.h" @@ -291,6 +292,21 @@ class CESIUMASYNC_API Connection { const std::vector& newScopes, const std::optional>& newAllowedUrls) const; + /** + * @brief Makes a request to the ion geocoding service. + * + * A geocoding service is used to make a plain text query (like an address, + * city name, or landmark) and obtain information about where it's located. + * + * @param provider The ion geocoding provider to use. + * @param type The type of request to make. See {@link GeocoderRequestType} for more information. + * @param query The query to make. + */ + CesiumAsync::Future> geocode( + const GeocoderProviderType& provider, + const GeocoderRequestType& type, + const std::string& query); + /** * @brief Decodes a token ID from a token. * diff --git a/CesiumIonClient/include/CesiumIonClient/Geocoder.h b/CesiumIonClient/include/CesiumIonClient/Geocoder.h new file mode 100644 index 000000000..b5cb213e0 --- /dev/null +++ b/CesiumIonClient/include/CesiumIonClient/Geocoder.h @@ -0,0 +1,131 @@ +#pragma once + +#include "CesiumGeospatial/Cartographic.h" +#include "CesiumGeospatial/GlobeRectangle.h" + +#include + +#include +#include +#include + +namespace CesiumIonClient { + +/** + * @brief The supported types of requests to geocoding API. + */ +enum GeocoderRequestType { + /** + * @brief Perform a full search from a complete query. + */ + Search, + + /** + * @brief Perform a quick search based on partial input, such as while a user + * is typing. + * The search results may be less accurate or exhaustive than using {@link GeocoderRequestType::Search}. + */ + Autocomplete +}; + +/** + * @brief The supported providers that can be accessed through ion's geocoder + * API. + */ +enum GeocoderProviderType { + /** + * @brief Google geocoder, for use with Google data. + */ + Google, + + /** + * @brief Bing geocoder, for use with Bing data. + */ + Bing, + + /** + * @brief Use the default geocoder as set on the server. Used when neither + * Bing or Google data is used. + */ + Default +}; + +/** + * @brief A single feature (a location or region) obtained from a geocoder + * service. + */ +struct GeocoderFeature { + /** + * @brief The user-friendly display name of this feature. + */ + std::string displayName; + + /** + * @brief The region on the globe for this feature. + */ + std::variant + destination; + + GeocoderFeature( + std::string& displayName_, + CesiumGeospatial::GlobeRectangle& destination_) + : displayName(displayName_), destination(destination_) {} + GeocoderFeature( + std::string& displayName_, + CesiumGeospatial::Cartographic& destination_) + : displayName(displayName_), destination(destination_) {} + + /** + * @brief Returns a {@link CesiumGeospatial::GlobeRectangle} representing this feature. + * + * If the geocoder service returned a bounding box for this result, this will + * return the bounding box. If the geocoder service returned a coordinate for + * this result, this will return a zero-width rectangle at that coordinate. + */ + CesiumGeospatial::GlobeRectangle getGlobeRectangle() const; + + /** + * @brief Returns a {@link CesiumGeospatial::Cartographic} representing this feature. + * + * If the geocoder service returned a bounding box for this result, this will + * return the center of the bounding box. If the geocoder service returned a + * coordinate for this result, this will return the coordinate. + */ + CesiumGeospatial::Cartographic getCartographic() const; +}; + +/** + * @brief Attribution information for a query to a geocoder service. + */ +struct GeocoderAttribution { + /** + * @brief An HTML string containing the necessary attribution information. + */ + std::string html; + + /** + * @brief If true, the credit should be visible in the main credit container. + * Otherwise, it can appear in a popover. + */ + bool showOnScreen; + + GeocoderAttribution(std::string& html_, bool showOnScreen_) + : html(html_), showOnScreen(showOnScreen_) {} +}; + +/** + * @brief The result of making a request to a geocoder service. + */ +struct GeocoderResult { + /** + * @brief Any necessary attributions for this geocoder result. + */ + std::vector attributions; + + /** + * @brief The features obtained from this geocoder service, if any. + */ + std::vector features; +}; + +}; // namespace CesiumIonClient \ No newline at end of file diff --git a/CesiumIonClient/src/Connection.cpp b/CesiumIonClient/src/Connection.cpp index 661c522d3..374632017 100644 --- a/CesiumIonClient/src/Connection.cpp +++ b/CesiumIonClient/src/Connection.cpp @@ -1,10 +1,15 @@ #include "CesiumIonClient/Connection.h" +#include "CesiumGeospatial/BoundingRegion.h" +#include "CesiumGeospatial/Cartographic.h" +#include "CesiumGeospatial/GlobeRectangle.h" +#include "CesiumIonClient/Geocoder.h" #include "fillWithRandomBytes.h" #include "parseLinkHeader.h" #include #include +#include #include #include #include @@ -13,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -582,6 +588,85 @@ Defaults defaultsFromJson(const rapidjson::Document& json) { return defaults; } +GeocoderResult geocoderResultFromJson(const rapidjson::Document& json) { + GeocoderResult result; + + auto featuresMemberIt = json.FindMember("features"); + if (featuresMemberIt != json.MemberEnd() && + featuresMemberIt->value.IsArray()) { + auto featuresIt = featuresMemberIt->value.GetArray(); + for (auto& feature : featuresIt) { + const rapidjson::Value* pLabel = + rapidjson::Pointer("/properties/label").Get(feature); + if (!pLabel) { + SPDLOG_WARN("Missing label for geocoder feature"); + continue; + } + + std::string label(pLabel->GetString()); + auto bboxMemberIt = feature.FindMember("bbox"); + if (bboxMemberIt == feature.MemberEnd() || + !bboxMemberIt->value.IsArray()) { + // Could be a point value. + const rapidjson::Value* pCoordinates = + rapidjson::Pointer("/geometry/coordinates").Get(feature); + if (!pCoordinates) { + SPDLOG_WARN( + "Missing bbox and geometry.coordinates for geocoder feature"); + continue; + } + + if (!pCoordinates->IsArray() || pCoordinates->Size() != 2) { + SPDLOG_WARN("geometry.coordinates must be an array of size 2"); + continue; + } + + auto coordinatesArray = pCoordinates->GetArray(); + + CesiumGeospatial::Cartographic point = + CesiumGeospatial::Cartographic::fromDegrees( + JsonHelpers::getDoubleOrDefault(coordinatesArray[0], 0), + JsonHelpers::getDoubleOrDefault(coordinatesArray[1], 0)); + + result.features.emplace_back(label, point); + } else { + auto bboxIt = bboxMemberIt->value.GetArray(); + if (bboxIt.Size() != 4) { + SPDLOG_WARN("bbox property should have exactly four values"); + continue; + } + + CesiumGeospatial::GlobeRectangle rect = + CesiumGeospatial::GlobeRectangle::fromDegrees( + JsonHelpers::getDoubleOrDefault(bboxIt[0], 0), + JsonHelpers::getDoubleOrDefault(bboxIt[1], 0), + JsonHelpers::getDoubleOrDefault(bboxIt[2], 0), + JsonHelpers::getDoubleOrDefault(bboxIt[3], 0)); + + result.features.emplace_back(label, rect); + } + } + } + + auto attributionMemberIt = json.FindMember("attributions"); + if (attributionMemberIt != json.MemberEnd() && + attributionMemberIt->value.IsArray()) { + const auto& valueJson = attributionMemberIt->value; + + result.attributions.reserve(valueJson.Size()); + + for (rapidjson::SizeType i = 0; i < valueJson.Size(); ++i) { + const auto& element = valueJson[i]; + std::string html = JsonHelpers::getStringOrDefault(element, "html", ""); + bool showOnScreen = + !JsonHelpers::getBoolOrDefault(element, "collapsible", false); + result.attributions.emplace_back(html, showOnScreen); + } + } + + return result; +} + } // namespace Future> Connection::defaults() const { @@ -1179,3 +1264,64 @@ Connection::tokens(const std::string& url) const { return Response(pRequest, tokenListFromJson(d)); }); } + +CesiumAsync::Future> Connection::geocode( + const GeocoderProviderType& provider, + const GeocoderRequestType& type, + const std::string& query) { + const std::string endpointUrl = type == GeocoderRequestType::Autocomplete + ? "v1/geocode/autocomplete" + : "v1/geocode/search"; + std::string requestUrl = + CesiumUtility::Uri::resolve(this->_apiUrl, endpointUrl); + requestUrl = CesiumUtility::Uri::addQuery(requestUrl, "text", query); + + // Add provider type to url + switch (provider) { + case GeocoderProviderType::Bing: + requestUrl = CesiumUtility::Uri::addQuery(requestUrl, "geocoder", "BING"); + break; + case GeocoderProviderType::Google: + requestUrl = CesiumUtility::Uri::addQuery(requestUrl, "geocoder", "GOOGLE"); + break; + case GeocoderProviderType::Default: + requestUrl = + CesiumUtility::Uri::addQuery(requestUrl, "geocoder", "DEFAULT"); + break; + } + + return this->_pAssetAccessor + ->get( + this->_asyncSystem, + requestUrl, + {{"Accept", "application/json"}, + {"Authorization", "Bearer " + this->_accessToken}}) + .thenInMainThread( + [](std::shared_ptr&& pRequest) { + const IAssetResponse* pResponse = pRequest->response(); + if (!pResponse) { + return createEmptyResponse(); + } + + if (pResponse->statusCode() < 200 || + pResponse->statusCode() >= 300) { + return createErrorResponse(pResponse); + } + + rapidjson::Document d; + if (!parseJsonObject(pResponse, d)) { + return createJsonErrorResponse(pResponse, d); + } + if (!d.IsObject()) { + return createJsonTypeResponse( + pResponse, + "object"); + } + + return Response( + geocoderResultFromJson(d), + pResponse->statusCode(), + std::string(), + std::string()); + }); +} diff --git a/CesiumIonClient/src/Geocoder.cpp b/CesiumIonClient/src/Geocoder.cpp new file mode 100644 index 000000000..cca45a74b --- /dev/null +++ b/CesiumIonClient/src/Geocoder.cpp @@ -0,0 +1,43 @@ +#include "CesiumIonClient/Geocoder.h" + +#include "CesiumGeospatial/Cartographic.h" +#include "CesiumGeospatial/GlobeRectangle.h" + +namespace CesiumIonClient { + +CesiumGeospatial::GlobeRectangle GeocoderFeature::getGlobeRectangle() const { + struct Operation { + CesiumGeospatial::GlobeRectangle + operator()(const CesiumGeospatial::GlobeRectangle& rect) { + return rect; + } + + CesiumGeospatial::GlobeRectangle + operator()(const CesiumGeospatial::Cartographic& cartographic) { + return CesiumGeospatial::GlobeRectangle( + cartographic.longitude, + cartographic.latitude, + cartographic.longitude, + cartographic.latitude); + } + }; + + return std::visit(Operation{}, this->destination); +} + +CesiumGeospatial::Cartographic GeocoderFeature::getCartographic() const { + struct Operation { + CesiumGeospatial::Cartographic + operator()(const CesiumGeospatial::GlobeRectangle& rect) { + return rect.computeCenter(); + } + + CesiumGeospatial::Cartographic + operator()(const CesiumGeospatial::Cartographic& cartographic) { + return cartographic; + } + }; + + return std::visit(Operation{}, this->destination); +} +} // namespace CesiumIonClient \ No newline at end of file diff --git a/CesiumIonClient/src/Response.cpp b/CesiumIonClient/src/Response.cpp index 9db8bfeff..e84f9a0b7 100644 --- a/CesiumIonClient/src/Response.cpp +++ b/CesiumIonClient/src/Response.cpp @@ -3,6 +3,7 @@ #include "CesiumIonClient/ApplicationData.h" #include "CesiumIonClient/Assets.h" #include "CesiumIonClient/Defaults.h" +#include "CesiumIonClient/Geocoder.h" #include "CesiumIonClient/Profile.h" #include "CesiumIonClient/TokenList.h" #include "parseLinkHeader.h" @@ -79,5 +80,6 @@ template struct Response; template struct Response; template struct Response; template struct Response; +template struct Response; } // namespace CesiumIonClient diff --git a/CesiumIonClient/test/TestConnection.cpp b/CesiumIonClient/test/TestConnection.cpp index dd31df952..e55f00db1 100644 --- a/CesiumIonClient/test/TestConnection.cpp +++ b/CesiumIonClient/test/TestConnection.cpp @@ -1,3 +1,6 @@ +#include "CesiumGeospatial/Cartographic.h" +#include "CesiumIonClient/Geocoder.h" + #include #include #include @@ -100,3 +103,79 @@ TEST_CASE("CesiumIonClient::Connection on single-user mode") { CHECK(me.value->id == 0); CHECK(me.value->username == "ion-user"); } + +TEST_CASE("CesiumIonClient::Connection::geocode") { + std::shared_ptr pAssetAccessor = + std::make_shared( + std::map>()); + + std::unique_ptr pResponse = + std::make_unique( + static_cast(200), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + readFile( + std::filesystem::path(CesiumIonClient_TEST_DATA_DIR) / + "geocode.json")); + + std::shared_ptr pRequest = + std::make_shared( + "GET", + "doesn't matter", + CesiumAsync::HttpHeaders{}, + std::move(pResponse)); + + pAssetAccessor->mockCompletedRequests.insert( + {"https://example.com/v1/geocode/search?text=antarctica&geocoder=BING", + std::move(pRequest)}); + + AsyncSystem asyncSystem(std::make_shared()); + Connection connection( + asyncSystem, + pAssetAccessor, + "my access token", + CesiumIonClient::ApplicationData(), + "https://example.com/"); + + Future> futureGeocode = connection.geocode( + GeocoderProviderType::Bing, + GeocoderRequestType::Search, + "antarctica"); + Response geocode = + waitForFuture(asyncSystem, std::move(futureGeocode)); + + REQUIRE(geocode.value); + + CHECK(geocode.value->attributions.size() == 2); + CHECK(geocode.value->attributions[0].showOnScreen); + CHECK(!geocode.value->attributions[1].showOnScreen); + + CHECK(geocode.value->features.size() == 3); + CHECK(geocode.value->features[0].displayName == "Antarctica"); + CHECK( + geocode.value->features[0].getGlobeRectangle().getNorth() == + -1.05716816000174529); + + CHECK(geocode.value->features[1].displayName == "Antarctica, FL"); + CesiumGeospatial::Cartographic center( + -1.4217365374220714, + 0.4958794631292909); + CHECK(geocode.value->features[1].getCartographic() == center); + + CHECK(geocode.value->features[2].displayName == "Point Value"); + CesiumGeospatial::Cartographic point = + CesiumGeospatial::Cartographic::fromDegrees(-180, -90); + CHECK(geocode.value->features[2].getCartographic() == point); + CHECK( + geocode.value->features[2].getGlobeRectangle().getNorth() == + point.latitude); + CHECK( + geocode.value->features[2].getGlobeRectangle().getSouth() == + point.latitude); + CHECK( + geocode.value->features[2].getGlobeRectangle().getEast() == + point.longitude); + CHECK( + geocode.value->features[2].getGlobeRectangle().getWest() == + point.longitude); +} diff --git a/CesiumIonClient/test/data/geocode.json b/CesiumIonClient/test/data/geocode.json new file mode 100644 index 000000000..007e10db9 --- /dev/null +++ b/CesiumIonClient/test/data/geocode.json @@ -0,0 +1,62 @@ +{ + "attributions": [ + { + "html": "\"Cesium", + "collapsible": false + }, + { + "html": "\"Microsoft", + "collapsible": true + } + ], + "features": [ + { + "properties": { + "label": "Antarctica" + }, + "bbox": [ + -180, + -90, + 180, + -60.57127380371094 + ] + }, + { + "properties": { + "label": "Antarctica, FL" + }, + "bbox": [ + -81.46535855304613, + 28.407937666950808, + -81.45364779461012, + 28.41566310209216 + ] + }, + { + "properties": { + "label": "Point Value" + }, + "geometry": { + "coordinates": [ + -180, + -90 + ] + } + }, + { + "properties": {}, + "bbox": [ + -180, + -90, + 180, + -60.57127380371094 + ] + }, + { + "properties": { + "label": "Missing bbox test" + }, + "bbox": [] + } + ] +} \ No newline at end of file