diff --git a/CHANGES.md b/CHANGES.md index 42ead58bb..8305eb4d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,15 +6,13 @@ - Added conversion of I3dm batch table metadata to `EXT_structural_metadata` and `EXT_instance_features` extensions. - Added `CesiumIonClient::Connection::geocode` method for making geocoding queries against the Cesium ion geocoder API. +- Added `UrlTemplateRasterOverlay` for requesting raster tiles from services using a templated URL. +- `upsampleGltfForRasterOverlays` is now compatible with meshes using TRIANGLE_STRIP, TRIANGLE_FAN, or non-indexed TRIANGLES primitives. ##### Fixes :wrench: - Fixed a crash in `GltfWriter` that would happen when the `EXT_structural_metadata` `schema` property was null. -##### Additions :tada: - -- `upsampleGltfForRasterOverlays` is now compatible with meshes using TRIANGLE_STRIP, TRIANGLE_FAN, or non-indexed TRIANGLES primitives. - ### v0.43.0 - 2025-01-02 ##### Breaking Changes :mega: diff --git a/CesiumGeometry/include/CesiumGeometry/QuadtreeTileID.h b/CesiumGeometry/include/CesiumGeometry/QuadtreeTileID.h index fa8117624..49818b211 100644 --- a/CesiumGeometry/include/CesiumGeometry/QuadtreeTileID.h +++ b/CesiumGeometry/include/CesiumGeometry/QuadtreeTileID.h @@ -44,6 +44,19 @@ struct CESIUMGEOMETRY_API QuadtreeTileID final { return !(*this == other); } + /** + * @brief Computes the inverse x-coordinate of this tile ID. + * + * This will compute the inverse x-coordinate of this tile ID, based + * on the given tiling scheme, which provides the number of tiles + * in x-direction for the level of this tile ID. + * + * @param tilingScheme The {@link QuadtreeTilingScheme}. + * @return The inverted x-coordinate. + */ + uint32_t + computeInvertedX(const QuadtreeTilingScheme& tilingScheme) const noexcept; + /** * @brief Computes the inverse y-coordinate of this tile ID. * diff --git a/CesiumGeometry/src/QuadtreeTileID.cpp b/CesiumGeometry/src/QuadtreeTileID.cpp index 1445e7589..aca708092 100644 --- a/CesiumGeometry/src/QuadtreeTileID.cpp +++ b/CesiumGeometry/src/QuadtreeTileID.cpp @@ -7,6 +7,11 @@ #include namespace CesiumGeometry { +uint32_t QuadtreeTileID::computeInvertedX( + const QuadtreeTilingScheme& tilingScheme) const noexcept { + const uint32_t xTiles = tilingScheme.getNumberOfXTilesAtLevel(this->level); + return xTiles - this->x - 1; +} uint32_t QuadtreeTileID::computeInvertedY( const QuadtreeTilingScheme& tilingScheme) const noexcept { diff --git a/CesiumRasterOverlays/include/CesiumRasterOverlays/UrlTemplateRasterOverlay.h b/CesiumRasterOverlays/include/CesiumRasterOverlays/UrlTemplateRasterOverlay.h new file mode 100644 index 000000000..3ba81cd08 --- /dev/null +++ b/CesiumRasterOverlays/include/CesiumRasterOverlays/UrlTemplateRasterOverlay.h @@ -0,0 +1,136 @@ +#pragma once + +#include "IPrepareRasterOverlayRendererResources.h" +#include "Library.h" +#include "RasterOverlayTileProvider.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace CesiumRasterOverlays { + +/** + * @brief Options for URL template overlays. + */ +struct UrlTemplateRasterOverlayOptions { + /** + * @brief A credit for the data source, which is displayed on the canvas. + */ + std::optional credit; + + /** + * @brief The {@link CesiumGeospatial::Projection} that is used. + */ + std::optional projection; + + /** + * @brief The {@link CesiumGeometry::QuadtreeTilingScheme} specifying how + * the ellipsoidal surface is broken into tiles. + */ + std::optional tilingScheme; + + /** + * @brief The minimum level-of-detail supported by the imagery provider. + * + * Take care when specifying this that the number of tiles at the minimum + * level is small, such as four or less. A larger number is likely to + * result in rendering problems. + */ + uint32_t minimumLevel = 0; + + /** + * @brief The maximum level-of-detail supported by the imagery provider. + */ + uint32_t maximumLevel = 25; + + /** + * @brief Pixel width of image tiles. + */ + uint32_t tileWidth = 256; + + /** + * @brief Pixel height of image tiles. + */ + uint32_t tileHeight = 256; + + /** + * @brief The {@link CesiumGeometry::Rectangle}, in radians, covered by the + * image. + */ + std::optional coverageRectangle; +}; + +/** + * @brief A {@link RasterOverlay} accessing images from a templated URL. + */ +class CESIUMRASTEROVERLAYS_API UrlTemplateRasterOverlay final + : public RasterOverlay { +public: + /** + * @brief Creates a new instance. + * + * The following template parameters are supported in `url`: + * - `{x}` - The tile X coordinate in the tiling scheme, where 0 is the westernmost tile. + * - `{y}` - The tile Y coordinate in the tiling scheme, where 0 is the nothernmost tile. + * - `{z}` - The level of the tile in the tiling scheme, where 0 is the root of the quadtree pyramid. + * - `{reverseX}` - The tile X coordinate in the tiling scheme, where 0 is the easternmost tile. + * - `{reverseY}` - The tile Y coordinate in the tiling scheme, where 0 is the southernmost tile. + * - `{reverseZ}` - The tile Z coordinate in the tiling scheme, where 0 is equivalent to `urlTemplateOptions.maximumLevel`. + * - `{westDegrees}` - The western edge of the tile in geodetic degrees. + * - `{southDegrees}` - The southern edge of the tile in geodetic degrees. + * - `{eastDegrees}` - The eastern edge of the tile in geodetic degrees. + * - `{northDegrees}` - The northern edge of the tile in geodetic degrees. + * - `{minimumX}` - The minimum X coordinate of the tile's projected coordinates. + * - `{minimumY}` - The minimum Y coordinate of the tile's projected coordinates. + * - `{maximumX}` - The maximum X coordinate of the tile's projected coordinates. + * - `{maximumY}` - The maximum Y coordinate of the tile's projected coordinates. + * - `{width}` - The width of each tile in pixels. + * - `{height}` - The height of each tile in pixels. + * + * @param name The user-given name of this overlay layer. + * @param url The URL with template parameters. + * @param headers The headers. This is a list of pairs of strings of the + * form (Key,Value) that will be inserted as request headers internally. + * @param urlTemplateOptions The {@link UrlTemplateRasterOverlayOptions}. + * @param overlayOptions The {@link RasterOverlayOptions} for this instance. + */ + UrlTemplateRasterOverlay( + const std::string& name, + const std::string& url, + const std::vector& headers = {}, + const UrlTemplateRasterOverlayOptions& urlTemplateOptions = {}, + const RasterOverlayOptions& overlayOptions = {}) + : RasterOverlay(name, overlayOptions), + _url(url), + _headers(headers), + _options(urlTemplateOptions) {} + + virtual CesiumAsync::Future createTileProvider( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, + const std::shared_ptr& pCreditSystem, + const std::shared_ptr& + pPrepareRendererResources, + const std::shared_ptr& pLogger, + CesiumUtility::IntrusivePointer pOwner) + const override; + +private: + std::string _url; + std::vector _headers; + UrlTemplateRasterOverlayOptions _options; +}; +} // namespace CesiumRasterOverlays diff --git a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp index fdfb1c143..282dc4572 100644 --- a/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp +++ b/CesiumRasterOverlays/src/QuadtreeRasterOverlayTileProvider.cpp @@ -131,6 +131,7 @@ QuadtreeRasterOverlayTileProvider::QuadtreeRasterOverlayTileProvider( } } #endif + IntrusivePointer pLoadedImage; pLoadedImage.emplace( std::make_shared( diff --git a/CesiumRasterOverlays/src/UrlTemplateRasterOverlay.cpp b/CesiumRasterOverlays/src/UrlTemplateRasterOverlay.cpp new file mode 100644 index 000000000..629ab944e --- /dev/null +++ b/CesiumRasterOverlays/src/UrlTemplateRasterOverlay.cpp @@ -0,0 +1,195 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace CesiumAsync; +using namespace CesiumGeometry; +using namespace CesiumGeospatial; +using namespace CesiumUtility; + +namespace CesiumRasterOverlays { + +class UrlTemplateRasterOverlayTileProvider final + : public QuadtreeRasterOverlayTileProvider { +public: + UrlTemplateRasterOverlayTileProvider( + const IntrusivePointer& pOwner, + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, + std::optional credit, + const std::shared_ptr& + pPrepareRendererResources, + const std::shared_ptr& pLogger, + const CesiumGeospatial::Projection& projection, + const CesiumGeometry::QuadtreeTilingScheme& tilingScheme, + const CesiumGeometry::Rectangle& coverageRectangle, + const std::string& url, + const std::vector& headers, + uint32_t width, + uint32_t height, + uint32_t minimumLevel, + uint32_t maximumLevel) + : QuadtreeRasterOverlayTileProvider( + pOwner, + asyncSystem, + pAssetAccessor, + credit, + pPrepareRendererResources, + pLogger, + projection, + tilingScheme, + coverageRectangle, + minimumLevel, + maximumLevel, + width, + height), + _url(url), + _headers(headers) {} + + virtual ~UrlTemplateRasterOverlayTileProvider() = default; + +protected: + virtual CesiumAsync::Future loadQuadtreeTileImage( + const CesiumGeometry::QuadtreeTileID& tileID) const override { + + LoadTileImageFromUrlOptions options; + options.rectangle = this->getTilingScheme().tileToRectangle(tileID); + options.moreDetailAvailable = tileID.level < this->getMaximumLevel(); + + const GlobeRectangle unprojectedRect = + unprojectRectangleSimple(this->getProjection(), options.rectangle); + + const std::map + placeholdersMap{ + {"x", std::to_string(tileID.x)}, + {"y", std::to_string(tileID.y)}, + {"z", std::to_string(tileID.level)}, + {"reverseX", + std::to_string(tileID.computeInvertedX(this->getTilingScheme()))}, + {"reverseY", + std::to_string(tileID.computeInvertedY(this->getTilingScheme()))}, + {"reverseZ", + std::to_string(this->getMaximumLevel() - tileID.level)}, + {"westDegrees", + std::to_string(Math::radiansToDegrees(unprojectedRect.getWest()))}, + {"southDegrees", + std::to_string( + Math::radiansToDegrees(unprojectedRect.getSouth()))}, + {"eastDegrees", + std::to_string(Math::radiansToDegrees(unprojectedRect.getEast()))}, + {"northDegrees", + std::to_string( + Math::radiansToDegrees(unprojectedRect.getNorth()))}, + {"minimumY", std::to_string(options.rectangle.minimumY)}, + {"minimumX", std::to_string(options.rectangle.minimumX)}, + {"maximumY", std::to_string(options.rectangle.maximumY)}, + {"maximumX", std::to_string(options.rectangle.maximumX)}, + {"width", std::to_string(this->getWidth())}, + {"height", std::to_string(this->getHeight())}}; + + const std::string substitutedUrl = Uri::substituteTemplateParameters( + this->_url, + [&placeholdersMap](const std::string& placeholder) { + auto placeholderIt = placeholdersMap.find(placeholder); + if (placeholderIt != placeholdersMap.end()) { + return placeholderIt->second; + } + return std::string("[UNKNOWN PLACEHOLDER]"); + }); + return this->loadTileImageFromUrl( + substitutedUrl, + this->_headers, + std::move(options)); + } + +private: + std::string _url; + std::vector _headers; +}; + +CesiumAsync::Future +UrlTemplateRasterOverlay::createTileProvider( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor, + const std::shared_ptr& pCreditSystem, + const std::shared_ptr& + pPrepareRendererResources, + const std::shared_ptr& pLogger, + CesiumUtility::IntrusivePointer pOwner) const { + pOwner = pOwner ? pOwner : this; + + std::optional credit = std::nullopt; + if (pCreditSystem && this->_options.credit) { + credit = pCreditSystem->createCredit( + *this->_options.credit, + pOwner->getOptions().showCreditsOnScreen); + } + + CesiumGeospatial::Projection projection = _options.projection.value_or( + CesiumGeospatial::WebMercatorProjection(pOwner->getOptions().ellipsoid)); + CesiumGeospatial::GlobeRectangle tilingSchemeRectangle = + CesiumGeospatial::WebMercatorProjection::MAXIMUM_GLOBE_RECTANGLE; + + uint32_t rootTilesX = 1; + if (std::get_if(&projection)) { + tilingSchemeRectangle = + CesiumGeospatial::GeographicProjection::MAXIMUM_GLOBE_RECTANGLE; + rootTilesX = 2; + } + CesiumGeometry::Rectangle coverageRectangle = + _options.coverageRectangle.value_or( + projectRectangleSimple(projection, tilingSchemeRectangle)); + + CesiumGeometry::QuadtreeTilingScheme tilingScheme = + _options.tilingScheme.value_or(CesiumGeometry::QuadtreeTilingScheme( + coverageRectangle, + rootTilesX, + 1)); + + return asyncSystem + .createResolvedFuture( + new UrlTemplateRasterOverlayTileProvider( + pOwner, + asyncSystem, + pAssetAccessor, + credit, + pPrepareRendererResources, + pLogger, + projection, + tilingScheme, + coverageRectangle, + _url, + _headers, + _options.tileWidth, + _options.tileHeight, + _options.minimumLevel, + _options.maximumLevel)); +} + +} // namespace CesiumRasterOverlays diff --git a/CesiumRasterOverlays/test/TestUrlTemplateRasterOverlay.cpp b/CesiumRasterOverlays/test/TestUrlTemplateRasterOverlay.cpp new file mode 100644 index 000000000..28f02cfaf --- /dev/null +++ b/CesiumRasterOverlays/test/TestUrlTemplateRasterOverlay.cpp @@ -0,0 +1,90 @@ +#include "CesiumRasterOverlays/RasterOverlay.h" +#include "CesiumRasterOverlays/RasterOverlayTile.h" +#include "CesiumRasterOverlays/UrlTemplateRasterOverlay.h" + +#include +#include +#include +#include + +#include +#include + +#include + +using namespace CesiumAsync; +using namespace CesiumGeometry; +using namespace CesiumGeospatial; +using namespace CesiumGltf; +using namespace CesiumUtility; +using namespace CesiumNativeTests; +using namespace CesiumRasterOverlays; + +TEST_CASE("UrlTemplateRasterOverlay getTile") { + std::filesystem::path dataDir(CesiumRasterOverlays_TEST_DATA_DIR); + std::vector blackPng = readFile(dataDir / "black.png"); + + CesiumAsync::AsyncSystem asyncSystem{std::make_shared()}; + auto pAssetAccessor = std::make_shared( + std::map>()); + pAssetAccessor->mockCompletedRequests.insert( + {"http://example.com/0/0/0.png", + std::make_shared( + "GET", + "http://example.com/0/0/0.png", + HttpHeaders{}, + std::make_unique( + uint16_t(200), + "doesn't matter", + HttpHeaders{}, + blackPng))}); + + IntrusivePointer pOverlay = + new UrlTemplateRasterOverlay( + "Test", + "http://example.com/{x}/{y}/{z}.png"); + + IntrusivePointer pProvider = nullptr; + + pOverlay + ->createTileProvider( + asyncSystem, + pAssetAccessor, + nullptr, + nullptr, + spdlog::default_logger(), + nullptr) + .thenInMainThread( + [&pProvider](RasterOverlay::CreateTileProviderResult&& created) { + CHECK(created); + pProvider = *created; + }); + + asyncSystem.dispatchMainThreadTasks(); + + REQUIRE(pProvider); + REQUIRE(!pProvider->isPlaceholder()); + + Rectangle rectangle = + GeographicProjection::computeMaximumProjectedRectangle(Ellipsoid::WGS84); + IntrusivePointer pTile = + pProvider->getTile(rectangle, glm::dvec2(256)); + pProvider->loadTile(*pTile); + + while (pTile->getState() != RasterOverlayTile::LoadState::Loaded) { + asyncSystem.dispatchMainThreadTasks(); + } + + CHECK(pTile->getState() == RasterOverlayTile::LoadState::Loaded); + + REQUIRE(pTile->getImage()); + + const ImageAsset& image = *pTile->getImage(); + CHECK(image.width > 0); + CHECK(image.height > 0); + CHECK(image.pixelData.size() > 0); + CHECK(std::all_of( + image.pixelData.begin(), + image.pixelData.end(), + [](std::byte b) { return b == std::byte(0); })); +} diff --git a/CesiumRasterOverlays/test/data/black.png b/CesiumRasterOverlays/test/data/black.png new file mode 100644 index 000000000..fda58469b Binary files /dev/null and b/CesiumRasterOverlays/test/data/black.png differ