565 lines
20 KiB
C++
565 lines
20 KiB
C++
#include "CesiumIonTilesetLoader.h"
|
|
|
|
#include "LayerJsonTerrainLoader.h"
|
|
#include "TilesetJsonLoader.h"
|
|
|
|
#include <CesiumAsync/IAssetAccessor.h>
|
|
#include <CesiumAsync/IAssetResponse.h>
|
|
#include <CesiumUtility/JsonHelpers.h>
|
|
#include <CesiumUtility/Uri.h>
|
|
|
|
#include <rapidjson/document.h>
|
|
|
|
#include <unordered_map>
|
|
|
|
namespace Cesium3DTilesSelection {
|
|
namespace {
|
|
struct AssetEndpointAttribution {
|
|
std::string html;
|
|
bool collapsible = true;
|
|
};
|
|
|
|
struct AssetEndpoint {
|
|
std::string type;
|
|
std::string url;
|
|
std::string accessToken;
|
|
std::vector<AssetEndpointAttribution> attributions;
|
|
};
|
|
|
|
std::unordered_map<std::string, AssetEndpoint> endpointCache;
|
|
|
|
std::string createEndpointResource(
|
|
int64_t ionAssetID,
|
|
const std::string& ionAccessToken,
|
|
const std::string& ionAssetEndpointUrl) {
|
|
std::string ionUrl = fmt::format(
|
|
"{}v1/assets/{}/endpoint?access_token={}",
|
|
ionAssetEndpointUrl,
|
|
ionAssetID,
|
|
ionAccessToken);
|
|
return ionUrl;
|
|
}
|
|
|
|
/**
|
|
* @brief Tries to obtain the `accessToken` from the JSON of the
|
|
* given response.
|
|
*
|
|
* @param pIonResponse The response
|
|
* @return The access token if successful
|
|
*/
|
|
std::optional<std::string> getNewAccessToken(
|
|
const CesiumAsync::IAssetResponse* pIonResponse,
|
|
const std::shared_ptr<spdlog::logger>& pLogger) {
|
|
const gsl::span<const std::byte> data = pIonResponse->data();
|
|
rapidjson::Document ionResponse;
|
|
ionResponse.Parse(reinterpret_cast<const char*>(data.data()), data.size());
|
|
if (ionResponse.HasParseError()) {
|
|
SPDLOG_LOGGER_ERROR(
|
|
pLogger,
|
|
"Error when parsing Cesium ion response, error code {} at byte offset "
|
|
"{}",
|
|
ionResponse.GetParseError(),
|
|
ionResponse.GetErrorOffset());
|
|
return std::nullopt;
|
|
}
|
|
return CesiumUtility::JsonHelpers::getStringOrDefault(
|
|
ionResponse,
|
|
"accessToken",
|
|
"");
|
|
}
|
|
|
|
CesiumAsync::Future<TilesetContentLoaderResult<CesiumIonTilesetLoader>>
|
|
mainThreadLoadTilesetJsonFromAssetEndpoint(
|
|
const TilesetExternals& externals,
|
|
const AssetEndpoint& endpoint,
|
|
int64_t ionAssetID,
|
|
std::string ionAccessToken,
|
|
std::string ionAssetEndpointUrl,
|
|
CesiumIonTilesetLoader::AuthorizationHeaderChangeListener
|
|
headerChangeListener,
|
|
bool showCreditsOnScreen) {
|
|
std::vector<LoaderCreditResult> credits;
|
|
if (externals.pCreditSystem) {
|
|
credits.reserve(endpoint.attributions.size());
|
|
for (auto& endpointAttribution : endpoint.attributions) {
|
|
bool showOnScreen =
|
|
showCreditsOnScreen || !endpointAttribution.collapsible;
|
|
credits.push_back(
|
|
LoaderCreditResult{endpointAttribution.html, showOnScreen});
|
|
}
|
|
}
|
|
|
|
std::vector<CesiumAsync::IAssetAccessor::THeader> requestHeaders;
|
|
requestHeaders.emplace_back(
|
|
"Authorization",
|
|
"Bearer " + endpoint.accessToken);
|
|
|
|
return TilesetJsonLoader::createLoader(
|
|
externals,
|
|
endpoint.url,
|
|
requestHeaders)
|
|
.thenImmediately([credits = std::move(credits),
|
|
requestHeaders,
|
|
ionAssetID,
|
|
ionAccessToken = std::move(ionAccessToken),
|
|
ionAssetEndpointUrl = std::move(ionAssetEndpointUrl),
|
|
headerChangeListener = std::move(headerChangeListener)](
|
|
TilesetContentLoaderResult<TilesetJsonLoader>&&
|
|
tilesetJsonResult) mutable {
|
|
if (tilesetJsonResult.credits.empty()) {
|
|
tilesetJsonResult.credits = std::move(credits);
|
|
} else {
|
|
tilesetJsonResult.credits.insert(
|
|
tilesetJsonResult.credits.end(),
|
|
credits.begin(),
|
|
credits.end());
|
|
}
|
|
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader> result;
|
|
if (!tilesetJsonResult.errors) {
|
|
result.pLoader = std::make_unique<CesiumIonTilesetLoader>(
|
|
ionAssetID,
|
|
std::move(ionAccessToken),
|
|
std::move(ionAssetEndpointUrl),
|
|
std::move(tilesetJsonResult.pLoader),
|
|
std::move(headerChangeListener));
|
|
result.pRootTile = std::move(tilesetJsonResult.pRootTile);
|
|
result.credits = std::move(tilesetJsonResult.credits);
|
|
result.requestHeaders = std::move(requestHeaders);
|
|
}
|
|
result.errors = std::move(tilesetJsonResult.errors);
|
|
result.statusCode = tilesetJsonResult.statusCode;
|
|
return result;
|
|
});
|
|
}
|
|
|
|
CesiumAsync::Future<TilesetContentLoaderResult<CesiumIonTilesetLoader>>
|
|
mainThreadLoadLayerJsonFromAssetEndpoint(
|
|
const TilesetExternals& externals,
|
|
const TilesetContentOptions& contentOptions,
|
|
const AssetEndpoint& endpoint,
|
|
int64_t ionAssetID,
|
|
std::string ionAccessToken,
|
|
std::string ionAssetEndpointUrl,
|
|
CesiumIonTilesetLoader::AuthorizationHeaderChangeListener
|
|
headerChangeListener,
|
|
bool showCreditsOnScreen) {
|
|
std::vector<LoaderCreditResult> credits;
|
|
if (externals.pCreditSystem) {
|
|
credits.reserve(endpoint.attributions.size());
|
|
for (auto& endpointAttribution : endpoint.attributions) {
|
|
bool showOnScreen =
|
|
showCreditsOnScreen || !endpointAttribution.collapsible;
|
|
credits.push_back(
|
|
LoaderCreditResult{endpointAttribution.html, showOnScreen});
|
|
}
|
|
}
|
|
|
|
std::vector<CesiumAsync::IAssetAccessor::THeader> requestHeaders;
|
|
requestHeaders.emplace_back(
|
|
"Authorization",
|
|
"Bearer " + endpoint.accessToken);
|
|
|
|
std::string url =
|
|
CesiumUtility::Uri::resolve(endpoint.url, "layer.json", true);
|
|
|
|
return LayerJsonTerrainLoader::createLoader(
|
|
externals,
|
|
contentOptions,
|
|
url,
|
|
requestHeaders,
|
|
showCreditsOnScreen)
|
|
.thenImmediately([credits = std::move(credits),
|
|
requestHeaders,
|
|
ionAssetID,
|
|
ionAccessToken = std::move(ionAccessToken),
|
|
ionAssetEndpointUrl = std::move(ionAssetEndpointUrl),
|
|
headerChangeListener = std::move(headerChangeListener)](
|
|
TilesetContentLoaderResult<LayerJsonTerrainLoader>&&
|
|
tilesetJsonResult) mutable {
|
|
if (tilesetJsonResult.credits.empty()) {
|
|
tilesetJsonResult.credits = std::move(credits);
|
|
} else {
|
|
tilesetJsonResult.credits.insert(
|
|
tilesetJsonResult.credits.end(),
|
|
credits.begin(),
|
|
credits.end());
|
|
}
|
|
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader> result;
|
|
if (!tilesetJsonResult.errors) {
|
|
result.pLoader = std::make_unique<CesiumIonTilesetLoader>(
|
|
ionAssetID,
|
|
std::move(ionAccessToken),
|
|
std::move(ionAssetEndpointUrl),
|
|
std::move(tilesetJsonResult.pLoader),
|
|
std::move(headerChangeListener));
|
|
result.pRootTile = std::move(tilesetJsonResult.pRootTile);
|
|
result.credits = std::move(tilesetJsonResult.credits);
|
|
result.requestHeaders = std::move(requestHeaders);
|
|
}
|
|
result.errors = std::move(tilesetJsonResult.errors);
|
|
result.statusCode = tilesetJsonResult.statusCode;
|
|
return result;
|
|
});
|
|
}
|
|
|
|
CesiumAsync::Future<TilesetContentLoaderResult<CesiumIonTilesetLoader>>
|
|
mainThreadHandleEndpointResponse(
|
|
const TilesetExternals& externals,
|
|
std::shared_ptr<CesiumAsync::IAssetRequest>&& pRequest,
|
|
int64_t ionAssetID,
|
|
std::string&& ionAccessToken,
|
|
std::string&& ionAssetEndpointUrl,
|
|
const TilesetContentOptions& contentOptions,
|
|
CesiumIonTilesetLoader::AuthorizationHeaderChangeListener&&
|
|
headerChangeListener,
|
|
bool showCreditsOnScreen) {
|
|
const CesiumAsync::IAssetResponse* pResponse = pRequest->response();
|
|
const std::string& requestUrl = pRequest->url();
|
|
if (!pResponse) {
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader> result;
|
|
result.errors.emplaceError(
|
|
fmt::format("No response received for asset request {}", requestUrl));
|
|
return externals.asyncSystem.createResolvedFuture(std::move(result));
|
|
}
|
|
|
|
uint16_t statusCode = pResponse->statusCode();
|
|
if (statusCode < 200 || statusCode >= 300) {
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader> result;
|
|
result.errors.emplaceError(fmt::format(
|
|
"Received status code {} for asset response {}",
|
|
statusCode,
|
|
requestUrl));
|
|
result.statusCode = statusCode;
|
|
return externals.asyncSystem.createResolvedFuture(std::move(result));
|
|
}
|
|
|
|
const gsl::span<const std::byte> data = pResponse->data();
|
|
|
|
rapidjson::Document ionResponse;
|
|
ionResponse.Parse(reinterpret_cast<const char*>(data.data()), data.size());
|
|
|
|
if (ionResponse.HasParseError()) {
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader> result;
|
|
result.errors.emplaceError(fmt::format(
|
|
"Error when parsing Cesium ion response JSON, error code {} at byte "
|
|
"offset {}",
|
|
ionResponse.GetParseError(),
|
|
ionResponse.GetErrorOffset()));
|
|
return externals.asyncSystem.createResolvedFuture(std::move(result));
|
|
}
|
|
|
|
AssetEndpoint endpoint;
|
|
if (externals.pCreditSystem) {
|
|
const auto attributionsIt = ionResponse.FindMember("attributions");
|
|
if (attributionsIt != ionResponse.MemberEnd() &&
|
|
attributionsIt->value.IsArray()) {
|
|
|
|
for (const rapidjson::Value& attribution :
|
|
attributionsIt->value.GetArray()) {
|
|
AssetEndpointAttribution& endpointAttribution =
|
|
endpoint.attributions.emplace_back();
|
|
const auto html = attribution.FindMember("html");
|
|
if (html != attribution.MemberEnd() && html->value.IsString()) {
|
|
endpointAttribution.html = html->value.GetString();
|
|
}
|
|
auto collapsible = attribution.FindMember("collapsible");
|
|
if (collapsible != attribution.MemberEnd() &&
|
|
collapsible->value.IsBool()) {
|
|
endpointAttribution.collapsible = collapsible->value.GetBool();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string url =
|
|
CesiumUtility::JsonHelpers::getStringOrDefault(ionResponse, "url", "");
|
|
std::string accessToken = CesiumUtility::JsonHelpers::getStringOrDefault(
|
|
ionResponse,
|
|
"accessToken",
|
|
"");
|
|
|
|
std::string type =
|
|
CesiumUtility::JsonHelpers::getStringOrDefault(ionResponse, "type", "");
|
|
if (type == "TERRAIN") {
|
|
// For terrain resources, we need to append `/layer.json` to the end of the
|
|
// URL.
|
|
url = CesiumUtility::Uri::resolve(url, "layer.json", true);
|
|
endpoint.type = type;
|
|
endpoint.url = url;
|
|
endpoint.accessToken = accessToken;
|
|
endpointCache[requestUrl] = endpoint;
|
|
return mainThreadLoadLayerJsonFromAssetEndpoint(
|
|
externals,
|
|
contentOptions,
|
|
endpoint,
|
|
ionAssetID,
|
|
std::move(ionAccessToken),
|
|
std::move(ionAssetEndpointUrl),
|
|
std::move(headerChangeListener),
|
|
showCreditsOnScreen);
|
|
} else if (type == "3DTILES") {
|
|
endpoint.type = type;
|
|
endpoint.url = url;
|
|
endpoint.accessToken = accessToken;
|
|
endpointCache[requestUrl] = endpoint;
|
|
return mainThreadLoadTilesetJsonFromAssetEndpoint(
|
|
externals,
|
|
endpoint,
|
|
ionAssetID,
|
|
std::move(ionAccessToken),
|
|
std::move(ionAssetEndpointUrl),
|
|
std::move(headerChangeListener),
|
|
showCreditsOnScreen);
|
|
}
|
|
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader> result;
|
|
result.errors.emplaceError(
|
|
fmt::format("Received unsupported asset response type: {}", type));
|
|
return externals.asyncSystem.createResolvedFuture(std::move(result));
|
|
}
|
|
} // namespace
|
|
|
|
CesiumIonTilesetLoader::CesiumIonTilesetLoader(
|
|
int64_t ionAssetID,
|
|
std::string&& ionAccessToken,
|
|
std::string&& ionAssetEndpointUrl,
|
|
std::unique_ptr<TilesetContentLoader>&& pAggregatedLoader,
|
|
std::function<
|
|
void(const std::string& header, const std::string& headerValue)>&&
|
|
headerChangeListener)
|
|
: _refreshTokenState{TokenRefreshState::None},
|
|
_ionAssetID{ionAssetID},
|
|
_ionAccessToken{std::move(ionAccessToken)},
|
|
_ionAssetEndpointUrl{std::move(ionAssetEndpointUrl)},
|
|
_pAggregatedLoader{std::move(pAggregatedLoader)},
|
|
_headerChangeListener{std::move(headerChangeListener)} {}
|
|
|
|
CesiumAsync::Future<TileLoadResult>
|
|
CesiumIonTilesetLoader::loadTileContent(const TileLoadInput& loadInput) {
|
|
if (this->_refreshTokenState == TokenRefreshState::Loading) {
|
|
return loadInput.asyncSystem.createResolvedFuture(
|
|
TileLoadResult::createRetryLaterResult(nullptr));
|
|
} else if (this->_refreshTokenState == TokenRefreshState::Failed) {
|
|
return loadInput.asyncSystem.createResolvedFuture(
|
|
TileLoadResult::createFailedResult(nullptr));
|
|
}
|
|
|
|
const auto& asyncSystem = loadInput.asyncSystem;
|
|
const auto& pAssetAccessor = loadInput.pAssetAccessor;
|
|
const auto& pLogger = loadInput.pLogger;
|
|
|
|
// TODO: the way this is structured, requests already in progress
|
|
// with the old key might complete after the key has been updated,
|
|
// and there's nothing here clever enough to avoid refreshing the
|
|
// key _again_ in that instance.
|
|
auto refreshTokenInMainThread =
|
|
[this, pLogger, pAssetAccessor, asyncSystem]() {
|
|
this->refreshTokenInMainThread(pLogger, pAssetAccessor, asyncSystem);
|
|
};
|
|
|
|
return this->_pAggregatedLoader->loadTileContent(loadInput).thenImmediately(
|
|
[asyncSystem,
|
|
refreshTokenInMainThread = std::move(refreshTokenInMainThread)](
|
|
TileLoadResult&& result) mutable {
|
|
// check to see if we need to refresh token
|
|
if (result.pCompletedRequest) {
|
|
auto response = result.pCompletedRequest->response();
|
|
if (response->statusCode() == 401) {
|
|
// retry later
|
|
result.state = TileLoadResultState::RetryLater;
|
|
asyncSystem.runInMainThread(std::move(refreshTokenInMainThread));
|
|
}
|
|
}
|
|
|
|
return std::move(result);
|
|
});
|
|
}
|
|
|
|
TileChildrenResult
|
|
CesiumIonTilesetLoader::createTileChildren(const Tile& tile) {
|
|
auto pLoader = tile.getLoader();
|
|
return pLoader->createTileChildren(tile);
|
|
}
|
|
|
|
void CesiumIonTilesetLoader::refreshTokenInMainThread(
|
|
const std::shared_ptr<spdlog::logger>& pLogger,
|
|
const std::shared_ptr<CesiumAsync::IAssetAccessor>& pAssetAccessor,
|
|
const CesiumAsync::AsyncSystem& asyncSystem) {
|
|
if (this->_refreshTokenState == TokenRefreshState::Loading) {
|
|
return;
|
|
}
|
|
|
|
this->_refreshTokenState = TokenRefreshState::Loading;
|
|
|
|
std::string url = createEndpointResource(
|
|
this->_ionAssetID,
|
|
this->_ionAccessToken,
|
|
this->_ionAssetEndpointUrl);
|
|
pAssetAccessor->get(asyncSystem, url)
|
|
.thenInMainThread(
|
|
[this,
|
|
pLogger](std::shared_ptr<CesiumAsync::IAssetRequest>&& pIonRequest) {
|
|
const CesiumAsync::IAssetResponse* pIonResponse =
|
|
pIonRequest->response();
|
|
|
|
if (!pIonResponse) {
|
|
this->_refreshTokenState = TokenRefreshState::Failed;
|
|
return;
|
|
}
|
|
|
|
uint16_t statusCode = pIonResponse->statusCode();
|
|
if (statusCode >= 200 && statusCode < 300) {
|
|
auto accessToken = getNewAccessToken(pIonResponse, pLogger);
|
|
if (accessToken) {
|
|
this->_headerChangeListener(
|
|
"Authorization",
|
|
"Bearer " + *accessToken);
|
|
|
|
// update cache with new access token
|
|
auto cacheIt = endpointCache.find(pIonRequest->url());
|
|
if (cacheIt != endpointCache.end()) {
|
|
cacheIt->second.accessToken = accessToken.value();
|
|
}
|
|
|
|
this->_refreshTokenState = TokenRefreshState::Done;
|
|
return;
|
|
}
|
|
}
|
|
|
|
this->_refreshTokenState = TokenRefreshState::Failed;
|
|
});
|
|
}
|
|
|
|
CesiumAsync::Future<TilesetContentLoaderResult<CesiumIonTilesetLoader>>
|
|
CesiumIonTilesetLoader::createLoader(
|
|
const TilesetExternals& externals,
|
|
const TilesetContentOptions& contentOptions,
|
|
int64_t ionAssetID,
|
|
const std::string& ionAccessToken,
|
|
const std::string& ionAssetEndpointUrl,
|
|
const AuthorizationHeaderChangeListener& headerChangeListener,
|
|
bool showCreditsOnScreen) {
|
|
std::string ionUrl =
|
|
createEndpointResource(ionAssetID, ionAccessToken, ionAssetEndpointUrl);
|
|
auto cacheIt = endpointCache.find(ionUrl);
|
|
if (cacheIt != endpointCache.end()) {
|
|
const auto& endpoint = cacheIt->second;
|
|
if (endpoint.type == "TERRAIN") {
|
|
return mainThreadLoadLayerJsonFromAssetEndpoint(
|
|
externals,
|
|
contentOptions,
|
|
endpoint,
|
|
ionAssetID,
|
|
ionAccessToken,
|
|
ionAssetEndpointUrl,
|
|
headerChangeListener,
|
|
showCreditsOnScreen)
|
|
.thenInMainThread(
|
|
[externals,
|
|
contentOptions,
|
|
ionAssetID,
|
|
ionAccessToken,
|
|
ionAssetEndpointUrl,
|
|
headerChangeListener,
|
|
showCreditsOnScreen](
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader>&& result) {
|
|
return refreshTokenIfNeeded(
|
|
externals,
|
|
contentOptions,
|
|
ionAssetID,
|
|
ionAccessToken,
|
|
ionAssetEndpointUrl,
|
|
headerChangeListener,
|
|
showCreditsOnScreen,
|
|
std::move(result));
|
|
});
|
|
} else if (endpoint.type == "3DTILES") {
|
|
return mainThreadLoadTilesetJsonFromAssetEndpoint(
|
|
externals,
|
|
endpoint,
|
|
ionAssetID,
|
|
ionAccessToken,
|
|
ionAssetEndpointUrl,
|
|
headerChangeListener,
|
|
showCreditsOnScreen)
|
|
.thenInMainThread(
|
|
[externals,
|
|
contentOptions,
|
|
ionAssetID,
|
|
ionAccessToken,
|
|
ionAssetEndpointUrl,
|
|
headerChangeListener,
|
|
showCreditsOnScreen](
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader>&& result) {
|
|
return refreshTokenIfNeeded(
|
|
externals,
|
|
contentOptions,
|
|
ionAssetID,
|
|
ionAccessToken,
|
|
ionAssetEndpointUrl,
|
|
headerChangeListener,
|
|
showCreditsOnScreen,
|
|
std::move(result));
|
|
});
|
|
}
|
|
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader> result;
|
|
result.errors.emplaceError(fmt::format(
|
|
"Received unsupported asset response type: {}",
|
|
endpoint.type));
|
|
return externals.asyncSystem.createResolvedFuture(std::move(result));
|
|
} else {
|
|
return externals.pAssetAccessor->get(externals.asyncSystem, ionUrl)
|
|
.thenInMainThread(
|
|
[externals,
|
|
ionAssetID,
|
|
ionAccessToken = ionAccessToken,
|
|
ionAssetEndpointUrl = ionAssetEndpointUrl,
|
|
headerChangeListener = headerChangeListener,
|
|
showCreditsOnScreen,
|
|
contentOptions](std::shared_ptr<CesiumAsync::IAssetRequest>&&
|
|
pRequest) mutable {
|
|
return mainThreadHandleEndpointResponse(
|
|
externals,
|
|
std::move(pRequest),
|
|
ionAssetID,
|
|
std::move(ionAccessToken),
|
|
std::move(ionAssetEndpointUrl),
|
|
contentOptions,
|
|
std::move(headerChangeListener),
|
|
showCreditsOnScreen);
|
|
});
|
|
}
|
|
}
|
|
CesiumAsync::Future<TilesetContentLoaderResult<CesiumIonTilesetLoader>>
|
|
CesiumIonTilesetLoader::refreshTokenIfNeeded(
|
|
const TilesetExternals& externals,
|
|
const TilesetContentOptions& contentOptions,
|
|
int64_t ionAssetID,
|
|
const std::string& ionAccessToken,
|
|
const std::string& ionAssetEndpointUrl,
|
|
const AuthorizationHeaderChangeListener& headerChangeListener,
|
|
bool showCreditsOnScreen,
|
|
TilesetContentLoaderResult<CesiumIonTilesetLoader>&& result) {
|
|
if (result.errors.hasErrors()) {
|
|
if (result.statusCode == 401) {
|
|
endpointCache.erase(createEndpointResource(
|
|
ionAssetID,
|
|
ionAccessToken,
|
|
ionAssetEndpointUrl));
|
|
return CesiumIonTilesetLoader::createLoader(
|
|
externals,
|
|
contentOptions,
|
|
ionAssetID,
|
|
ionAccessToken,
|
|
ionAssetEndpointUrl,
|
|
headerChangeListener,
|
|
showCreditsOnScreen);
|
|
}
|
|
}
|
|
return externals.asyncSystem.createResolvedFuture(std::move(result));
|
|
}
|
|
} // namespace Cesium3DTilesSelection
|