diff --git a/CHANGES.md b/CHANGES.md index 72f6c55c8..59ec01f94 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,8 @@ - Renamed `ExtensionReaderContext` to `JsonReaderOptions`, and the `getExtensions` method on various JSON reader classes to `getOptions`. - `IExtensionJsonHandler` no longer derives from `IJsonHandler`. Instead, it has a new pure virtual method, `getHandler`, that must be implemented to allow clients to obtain the `IJsonHandler`. In almost all implementations, this should simply return `*this`. - In `SubtreeReader`, `SchemaReader`, and `TilesetReader`, the `readSubtree`, `readSchema`, and `readTileset` methods (respectively) have been renamed to `readFromJson` and return a templated `ReadJsonResult` instead of a bespoke result class. +- `TileExternalContent` is now heap allocated and stored in `TileContent` with a `std::unique_ptr`. +- The root `Tile` of a `Cesium3DTilesSelection::Tileset` now represents the tileset.json itself, and the `root` tile specified in the tileset.json is its only child. This makes the shape of the tile tree consistent between a standard top-level tileset and an external tileset embedded elsewhere in the tree. In both cases, the "tile" that represents the tileset.json itself has content of type `TileExternalContent`. ##### Additions :tada: @@ -15,6 +17,10 @@ - Added `ValueType` type alias to `ArrayJsonHandler`, for consistency with other JSON handlers. - Added an overload of `JsonReader::readJson` that takes a `rapidjson::Value` instead of a byte buffer. This allows a subtree of a `rapidjson::Document` to be easily and efficiently converted into statically-typed classes via `IJsonHandler`. - Added `*Reader` classes to `CesiumGltfReader` and `Cesium3DTilesReader` to allow each of the classes to be individually read from JSON. +- Added `getExternalContent` method to the `TileContent` class. +- `TileExternalContent` now holds the metadata (`schema`, `schemaUri`, `metadata`, and `groups`) stored in the tileset.json. +- Added `loadMetadata` and `getMetadata` methods to `Cesium3DTilesSelection::Tileset`. They provide access to `TilesetMetadata` instance representing the metadata associated with a tileset.json. +- Added `MetadataQuery` class to make it easier to find properties with specific semantics in `TilesetMetadata`. ##### Fixes :wrench: diff --git a/Cesium3DTiles/include/Cesium3DTiles/MetadataQuery.h b/Cesium3DTiles/include/Cesium3DTiles/MetadataQuery.h new file mode 100644 index 000000000..c0f5b3213 --- /dev/null +++ b/Cesium3DTiles/include/Cesium3DTiles/MetadataQuery.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace Cesium3DTiles { + +/** + * @brief Holds the details of a found property in a {@link MetadataEntity}. + * + * Because this structure holds _references_ to the original {@link Schema} and + * {@link MetadataEntity} instances, it will be invalided if either are + * destroyed or modified. Continuing to access this result in that scenario will + * result in undefined behavior. + */ +struct CESIUM3DTILES_API FoundMetadataProperty { + /** + * @brief A reference to the identifier of the class that contains the found + * property within the {@link Schema}. + */ + const std::string& classIdentifier; + + /** + * @brief A reference to the {@link Class} that contains the found property + * within the {@link Schema}. + */ + const Class& classDefinition; + + /** + * @brief A reference to the identifier of the found property within the + * {@link Schema}. + */ + const std::string& propertyIdentifier; + + /** + * @brief A reference to the {@link ClassProperty} describing the found + * property within the {@lnik Schema}. + */ + const ClassProperty& propertyDefinition; + + /** + * @brief A reference to the value of the found property within the + * {@link MetadataEntity}. + */ + const CesiumUtility::JsonValue& propertyValue; +}; + +/** + * @brief Convenience functions for querying {@link MetadataEntity} instances. + */ +class CESIUM3DTILES_API MetadataQuery { +public: + /** + * @brief Gets the first property with a given + * {@link ClassProperty::semantic}. + * + * @param schema The schema to use to look up semantics. + * @param entity The metadata entity to search for a property with the + * semantic. + * @param semantic The semantic to find. + * @return The details of the found property, or `std::nullopt` if a property + * with the given semantic does not exist. + */ + static std::optional findFirstPropertyWithSemantic( + const Schema& schema, + const MetadataEntity& entity, + const std::string& semantic); +}; + +} // namespace Cesium3DTiles diff --git a/Cesium3DTiles/src/MetadataQuery.cpp b/Cesium3DTiles/src/MetadataQuery.cpp new file mode 100644 index 000000000..5705dd845 --- /dev/null +++ b/Cesium3DTiles/src/MetadataQuery.cpp @@ -0,0 +1,38 @@ +#include + +namespace Cesium3DTiles { + +std::optional +MetadataQuery::findFirstPropertyWithSemantic( + const Schema& schema, + const MetadataEntity& entity, + const std::string& semantic) { + auto classIt = schema.classes.find(entity.classProperty); + if (classIt == schema.classes.end()) { + return std::nullopt; + } + + const Cesium3DTiles::Class& klass = classIt->second; + + for (auto it = entity.properties.begin(); it != entity.properties.end(); + ++it) { + const std::pair& property = *it; + auto propertyIt = klass.properties.find(property.first); + if (propertyIt == klass.properties.end()) + continue; + + const ClassProperty& classProperty = propertyIt->second; + if (classProperty.semantic == semantic) { + return FoundMetadataProperty{ + classIt->first, + classIt->second, + it->first, + propertyIt->second, + it->second}; + } + } + + return std::nullopt; +} + +} // namespace Cesium3DTiles diff --git a/Cesium3DTiles/test/TestMetadataQuery.cpp b/Cesium3DTiles/test/TestMetadataQuery.cpp new file mode 100644 index 000000000..6c6b8c839 --- /dev/null +++ b/Cesium3DTiles/test/TestMetadataQuery.cpp @@ -0,0 +1,55 @@ +#include + +#include + +using namespace Cesium3DTiles; +using namespace CesiumUtility; + +TEST_CASE("MetadataQuery") { + SECTION("findFirstPropertyWithSemantic") { + Schema schema{}; + Class& classDefinition = + schema.classes.emplace("someClass", Class()).first->second; + + ClassProperty& classProperty1 = + classDefinition.properties.emplace("someProperty", ClassProperty()) + .first->second; + classProperty1.type = ClassProperty::Type::SCALAR; + classProperty1.componentType = ClassProperty::ComponentType::FLOAT64; + + ClassProperty& classProperty2 = + classDefinition.properties + .emplace("somePropertyWithSemantic", ClassProperty()) + .first->second; + classProperty2.type = ClassProperty::Type::STRING; + classProperty2.semantic = "SOME_SEMANTIC"; + + MetadataEntity withoutSemantic; + withoutSemantic.classProperty = "someClass"; + withoutSemantic.properties.emplace("someProperty", JsonValue(3.0)); + + MetadataEntity withSemantic = withoutSemantic; + withSemantic.properties.emplace( + "somePropertyWithSemantic", + JsonValue("the value")); + + std::optional foundProperty1 = + MetadataQuery::findFirstPropertyWithSemantic( + schema, + withoutSemantic, + "SOME_SEMANTIC"); + CHECK(!foundProperty1); + + std::optional foundProperty2 = + MetadataQuery::findFirstPropertyWithSemantic( + schema, + withSemantic, + "SOME_SEMANTIC"); + REQUIRE(foundProperty2); + CHECK(foundProperty2->classIdentifier == "someClass"); + CHECK(&foundProperty2->classDefinition == &classDefinition); + CHECK(foundProperty2->propertyIdentifier == "somePropertyWithSemantic"); + CHECK(&foundProperty2->propertyDefinition == &classProperty2); + CHECK(foundProperty2->propertyValue.getStringOrDefault("") == "the value"); + } +} diff --git a/Cesium3DTilesSelection/CMakeLists.txt b/Cesium3DTilesSelection/CMakeLists.txt index 793a7da73..e0736a313 100644 --- a/Cesium3DTilesSelection/CMakeLists.txt +++ b/Cesium3DTilesSelection/CMakeLists.txt @@ -43,6 +43,8 @@ target_include_directories( target_link_libraries(Cesium3DTilesSelection PUBLIC + Cesium3DTiles + Cesium3DTilesReader CesiumAsync CesiumGeospatial CesiumGeometry diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h index 7e0a9f25f..3071acb29 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h @@ -115,7 +115,7 @@ class CESIUM3DTILESSELECTION_API Tile final { */ Tile( TilesetContentLoader* pLoader, - TileExternalContent externalContent) noexcept; + std::unique_ptr&& externalContent) noexcept; /** * @brief Construct a tile with an empty content and a loader that is diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileContent.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileContent.h index 981b1c085..c7d1c487c 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileContent.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TileContent.h @@ -3,12 +3,14 @@ #include "CreditSystem.h" #include "Library.h" #include "RasterOverlayDetails.h" +#include "TilesetMetadata.h" #include #include #include #include +#include namespace Cesium3DTilesSelection { /** @@ -47,7 +49,12 @@ struct CESIUM3DTILESSELECTION_API TileEmptyContent {}; * external tileset. When this tile is loaded, all the tiles in the * external tileset will become children of this external content tile */ -struct CESIUM3DTILESSELECTION_API TileExternalContent {}; +struct CESIUM3DTILESSELECTION_API TileExternalContent { + /** + * @brief The metadata associated with this tileset. + */ + TilesetMetadata metadata; +}; /** * @brief A content tag that indicates a tile has a glTF model content and @@ -198,7 +205,7 @@ class CESIUM3DTILESSELECTION_API TileContent { using TileContentKindImpl = std::variant< TileUnknownContent, TileEmptyContent, - TileExternalContent, + std::unique_ptr, std::unique_ptr>; public: @@ -218,7 +225,7 @@ class CESIUM3DTILESSELECTION_API TileContent { * @brief Construct an external content for a tile whose content * points to an external tileset */ - TileContent(TileExternalContent content); + TileContent(std::unique_ptr&& content); /** * @brief Set an unknown content tag for a tile. This constructor @@ -236,7 +243,7 @@ class CESIUM3DTILESSELECTION_API TileContent { * @brief Set an external content for a tile whose content * points to an external tileset */ - void setContentKind(TileExternalContent content); + void setContentKind(std::unique_ptr&& content); /** * @brief Set a glTF model content for a tile @@ -267,21 +274,27 @@ class CESIUM3DTILESSELECTION_API TileContent { /** * @brief Get the {@link TileRenderContent} which stores the glTF model * and render resources of the tile - * - * @return The {@link TileRenderContent} which stores the glTF model - * and render resources of the tile */ const TileRenderContent* getRenderContent() const noexcept; /** * @brief Get the {@link TileRenderContent} which stores the glTF model * and render resources of the tile - * - * @return The {@link TileRenderContent} which stores the glTF model - * and render resources of the tile */ TileRenderContent* getRenderContent() noexcept; + /** + * @brief Get the {@link TileExternalContent} which stores the details of + * the external tileset. + */ + const TileExternalContent* getExternalContent() const noexcept; + + /** + * @brief Get the {@link TileExternalContent} which stores the details of + * the external tileset. + */ + TileExternalContent* getExternalContent() noexcept; + private: TileContentKindImpl _contentKind; }; diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index c0de9c35d..5d959320f 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -22,6 +22,7 @@ namespace Cesium3DTilesSelection { class TilesetContentManager; +class TilesetMetadata; /** * @brief A & getAsyncDestructionCompleteEvent(); - /** * @brief Destroys this tileset. * @@ -93,6 +87,20 @@ class CESIUM3DTILESSELECTION_API Tileset final { */ ~Tileset() noexcept; + /** + * @brief A future that resolves when this Tileset has been destroyed (i.e. + * its destructor has been called) and all async operations that it was + * executing have completed. + */ + CesiumAsync::SharedFuture& getAsyncDestructionCompleteEvent(); + + /** + * @brief A future that resolves when the details of the root tile of this + * tileset are available. The root tile's content (e.g., 3D model), however, + * will not necessarily be loaded yet. + */ + CesiumAsync::SharedFuture& getRootTileAvailableEvent(); + /** * @brief Get tileset credits. */ @@ -207,6 +215,49 @@ class CESIUM3DTILESSELECTION_API Tileset final { */ int64_t getTotalDataBytes() const noexcept; + /** + * @brief Gets the {@link TilesetMetadata} associated with the main or + * external tileset.json that contains a given tile. If the metadata is not + * yet loaded, this method returns nullptr. + * + * If this tileset's root tile is not yet available, this method returns + * nullptr. + * + * If the tileset has a {@link TilesetMetadata::schemaUri}, it will not + * necessarily have been loaded yet. + * + * If the provided tile is not the root tile of a tileset.json, this method + * walks up the {@link Tile::getParent} chain until it finds the closest + * root and then returns the metadata associated with the corresponding + * tileset.json. + * + * Consider calling {@link loadMetadata} instead, which will return a future + * that only resolves after the root tile is loaded and the `schemaUri`, if + * any, has been resolved. + * + * @param pTile The tile. If this parameter is nullptr, the metadata for the + * main tileset.json is returned. + * @return The found metadata, or nullptr if the root tile is not yet loaded. + */ + const TilesetMetadata* getMetadata(const Tile* pTile = nullptr) const; + + /** + * @brief Asynchronously loads the metadata associated with the main + * tileset.json. + * + * Before the returned future resolves, the root tile of this tileset will be + * loaded and the {@link TilesetMetadata::schemaUri} will be loaded if one + * has been specified. + * + * If the tileset or `schemaUri` fail to load, the returned future will + * reject. + * + * @return A shared future that resolves to the loaded metadata. Once this + * future resolves, {@link getMetadata} can be used to synchronously obtain + * the same metadata instance. + */ + CesiumAsync::Future loadMetadata(); + private: /** * @brief The result of traversing one branch of the tile hierarchy. diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetMetadata.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetMetadata.h new file mode 100644 index 000000000..db5b811d7 --- /dev/null +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/TilesetMetadata.h @@ -0,0 +1,77 @@ +#pragma once + +#include "Library.h" + +#include +#include +#include + +#include +#include +#include + +namespace CesiumAsync { +class AsyncSystem; +class IAssetAccessor; +} // namespace CesiumAsync + +namespace Cesium3DTilesSelection { + +/** + * @brief Holds the metadata associated with a {@link Tileset} or an external + * tileset. + */ +class CESIUM3DTILESSELECTION_API TilesetMetadata { +public: + ~TilesetMetadata() noexcept; + + /** + * @brief An object defining the structure of metadata classes and enums. When + * this is defined, then `schemaUri` shall be undefined. + */ + std::optional schema; + + /** + * @brief The URI (or IRI) of the external schema file. When this is defined, + * then `schema` shall be undefined. + */ + std::optional schemaUri; + + /** + * @brief An array of groups that tile content may belong to. Each element of + * this array is a metadata entity that describes the group. The tile content + * `group` property is an index into this array. + */ + std::vector groups; + + /** + * @brief A metadata entity that is associated with this tileset. + */ + std::optional metadata; + + /** + * @brief Asynchronously loads the {@link schema} from the {@link schemaUri}. + * If the {@link schemaUri} does not contain a value, this method does + * nothing and returns an already-resolved future. + * + * Calling this method multiple times will return the same shared future each + * time, unless the {@link schemaUri} is changed. In that case, when this + * method is called, the previous load is canceled and the new one begins. + * + * @param asyncSystem The async system used to do work in threads. + * @param pAssetAccessor The asset accessor used to request the schema from + * the schemaUri. + * @return A future that resolves when the schema has been loaded from the + * schemaUri. + */ + CesiumAsync::SharedFuture& loadSchemaUri( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor); + +private: + std::optional> _loadingFuture; + std::optional _loadingSchemaUri; + std::shared_ptr _pLoadingCanceled; +}; + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/Tile.cpp b/Cesium3DTilesSelection/src/Tile.cpp index 87df7636c..5db390341 100644 --- a/Cesium3DTilesSelection/src/Tile.cpp +++ b/Cesium3DTilesSelection/src/Tile.cpp @@ -21,12 +21,12 @@ Tile::Tile(TilesetContentLoader* pLoader) noexcept Tile::Tile( TilesetContentLoader* pLoader, - TileExternalContent externalContent) noexcept + std::unique_ptr&& externalContent) noexcept : Tile( TileConstructorImpl{}, TileLoadState::ContentLoaded, pLoader, - externalContent) {} + TileContent(std::move(externalContent))) {} Tile::Tile( TilesetContentLoader* pLoader, diff --git a/Cesium3DTilesSelection/src/TileContent.cpp b/Cesium3DTilesSelection/src/TileContent.cpp index e039dd071..2d357563c 100644 --- a/Cesium3DTilesSelection/src/TileContent.cpp +++ b/Cesium3DTilesSelection/src/TileContent.cpp @@ -78,7 +78,8 @@ TileContent::TileContent() : _contentKind{TileUnknownContent{}} {} TileContent::TileContent(TileEmptyContent content) : _contentKind{content} {} -TileContent::TileContent(TileExternalContent content) : _contentKind{content} {} +TileContent::TileContent(std::unique_ptr&& content) + : _contentKind{std::move(content)} {} void TileContent::setContentKind(TileUnknownContent content) { _contentKind = content; @@ -88,8 +89,9 @@ void TileContent::setContentKind(TileEmptyContent content) { _contentKind = content; } -void TileContent::setContentKind(TileExternalContent content) { - _contentKind = content; +void TileContent::setContentKind( + std::unique_ptr&& content) { + _contentKind = std::move(content); } void TileContent::setContentKind(std::unique_ptr&& content) { @@ -105,7 +107,8 @@ bool TileContent::isEmptyContent() const noexcept { } bool TileContent::isExternalContent() const noexcept { - return std::holds_alternative(this->_contentKind); + return std::holds_alternative>( + this->_contentKind); } bool TileContent::isRenderContent() const noexcept { @@ -132,4 +135,25 @@ TileRenderContent* TileContent::getRenderContent() noexcept { return nullptr; } + +const TileExternalContent* TileContent::getExternalContent() const noexcept { + const std::unique_ptr* pExternalContent = + std::get_if>(&this->_contentKind); + if (pExternalContent) { + return pExternalContent->get(); + } + + return nullptr; +} + +TileExternalContent* TileContent::getExternalContent() noexcept { + std::unique_ptr* pExternalContent = + std::get_if>(&this->_contentKind); + if (pExternalContent) { + return pExternalContent->get(); + } + + return nullptr; +} + } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 7e006a55f..31873f20a 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -86,10 +87,6 @@ Tileset::Tileset( ionAccessToken, ionAssetEndpointUrl)} {} -CesiumAsync::SharedFuture& Tileset::getAsyncDestructionCompleteEvent() { - return this->_pTilesetContentManager->getAsyncDestructionCompleteEvent(); -} - Tileset::~Tileset() noexcept { this->_pTilesetContentManager->unloadAll(); if (this->_externals.pTileOcclusionProxyPool) { @@ -97,6 +94,14 @@ Tileset::~Tileset() noexcept { } } +CesiumAsync::SharedFuture& Tileset::getAsyncDestructionCompleteEvent() { + return this->_pTilesetContentManager->getAsyncDestructionCompleteEvent(); +} + +CesiumAsync::SharedFuture& Tileset::getRootTileAvailableEvent() { + return this->_pTilesetContentManager->getRootTileAvailableEvent(); +} + const std::vector& Tileset::getTilesetCredits() const noexcept { return this->_pTilesetContentManager->getTilesetCredits(); } @@ -453,6 +458,61 @@ int64_t Tileset::getTotalDataBytes() const noexcept { return this->_pTilesetContentManager->getTotalDataUsed(); } +const TilesetMetadata* Tileset::getMetadata(const Tile* pTile) const { + if (pTile == nullptr) { + pTile = this->getRootTile(); + } + + while (pTile != nullptr) { + const TileExternalContent* pExternal = + pTile->getContent().getExternalContent(); + if (pExternal) + return &pExternal->metadata; + pTile = pTile->getParent(); + } + + return nullptr; +} + +CesiumAsync::Future Tileset::loadMetadata() { + return this->getRootTileAvailableEvent().thenInMainThread( + [pManager = this->_pTilesetContentManager, + pAssetAccessor = this->_externals.pAssetAccessor, + asyncSystem = + this->getAsyncSystem()]() -> Future { + Tile* pRoot = pManager->getRootTile(); + assert(pRoot); + + TileExternalContent* pExternal = + pRoot->getContent().getExternalContent(); + if (!pExternal) { + return asyncSystem.createResolvedFuture( + nullptr); + } + + TilesetMetadata& metadata = pExternal->metadata; + if (!metadata.schemaUri) { + // No schema URI, so the metadata is ready to go. + return asyncSystem.createResolvedFuture( + &metadata); + } + + return metadata.loadSchemaUri(asyncSystem, pAssetAccessor) + .thenInMainThread( + [pManager, pAssetAccessor]() -> const TilesetMetadata* { + Tile* pRoot = pManager->getRootTile(); + assert(pRoot); + + TileExternalContent* pExternal = + pRoot->getContent().getExternalContent(); + if (!pExternal) { + return nullptr; + } + return &pExternal->metadata; + }); + }); +} + static void markTileNonRendered( TileSelectionState::Result lastResult, Tile& tile, @@ -781,13 +841,16 @@ Tileset::TraversalDetails Tileset::_visitTileIfNeeded( this->_frustumCull(tile, frameState, cullWithChildrenBounds, cullResult); this->_fogCull(frameState, distances, cullResult); - if (this->_options.forbidHoles && !cullResult.shouldVisit && - tile.getRefine() == TileRefine::Replace && - tile.getUnconditionallyRefine()) { + if (!cullResult.shouldVisit && tile.getUnconditionallyRefine()) { // Unconditionally refined tiles must always be visited in forbidHoles // mode, because we need to load this tile's descendants before we can - // render any of its siblings. - cullResult.shouldVisit = true; + // render any of its siblings. An unconditionally refined root tile must be + // visited as well, otherwise we won't load anything at all. + if ((this->_options.forbidHoles && + tile.getRefine() == TileRefine::Replace) || + tile.getParent() == nullptr) { + cullResult.shouldVisit = true; + } } if (!cullResult.shouldVisit) { diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index bf7b7e0c2..ee315ddfe 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -37,8 +37,9 @@ struct ContentKindSetter { tileContent.setContentKind(content); } - void operator()(TileExternalContent content) { - tileContent.setContentKind(content); + void operator()(TileExternalContent&& content) { + tileContent.setContentKind( + std::make_unique(std::move(content))); } void operator()(CesiumGltf::Model&& model) { @@ -598,7 +599,12 @@ TilesetContentManager::TilesetContentManager( _tilesDataUsed{0}, _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ - this->_destructionCompletePromise.getFuture().share()} {} + this->_destructionCompletePromise.getFuture().share()}, + _rootTileAvailablePromise{externals.asyncSystem.createPromise()}, + _rootTileAvailableFuture{ + this->_rootTileAvailablePromise.getFuture().share()} { + this->_rootTileAvailablePromise.resolve(); +} TilesetContentManager::TilesetContentManager( const TilesetExternals& externals, @@ -622,7 +628,10 @@ TilesetContentManager::TilesetContentManager( _tilesDataUsed{0}, _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ - this->_destructionCompletePromise.getFuture().share()} { + this->_destructionCompletePromise.getFuture().share()}, + _rootTileAvailablePromise{externals.asyncSystem.createPromise()}, + _rootTileAvailableFuture{ + this->_rootTileAvailablePromise.getFuture().share()} { if (!url.empty()) { this->notifyTileStartLoading(nullptr); @@ -720,13 +729,16 @@ TilesetContentManager::TilesetContentManager( TilesetLoadType::TilesetJson, errorCallback, std::move(result)); + thiz->_rootTileAvailablePromise.resolve(); }) .catchInMainThread([thiz](std::exception&& e) { thiz->notifyTileDoneLoading(nullptr); SPDLOG_LOGGER_ERROR( thiz->_externals.pLogger, - "An unexpected error occurs when loading tile: {}", + "An unexpected error occurred when loading tile: {}", e.what()); + thiz->_rootTileAvailablePromise.reject( + std::runtime_error("Root tile failed to load.")); }); } } @@ -755,7 +767,10 @@ TilesetContentManager::TilesetContentManager( _tilesDataUsed{0}, _destructionCompletePromise{externals.asyncSystem.createPromise()}, _destructionCompleteFuture{ - this->_destructionCompletePromise.getFuture().share()} { + this->_destructionCompletePromise.getFuture().share()}, + _rootTileAvailablePromise{externals.asyncSystem.createPromise()}, + _rootTileAvailableFuture{ + this->_rootTileAvailablePromise.getFuture().share()} { if (ionAssetID > 0) { auto authorizationChangeListener = [this]( const std::string& header, @@ -792,13 +807,16 @@ TilesetContentManager::TilesetContentManager( TilesetLoadType::CesiumIon, errorCallback, std::move(result)); + thiz->_rootTileAvailablePromise.resolve(); }) .catchInMainThread([thiz](std::exception&& e) { thiz->notifyTileDoneLoading(nullptr); SPDLOG_LOGGER_ERROR( thiz->_externals.pLogger, - "An unexpected error occurs when loading tile: {}", + "An unexpected error occurred when loading tile: {}", e.what()); + thiz->_rootTileAvailablePromise.reject( + std::runtime_error("Root tile failed to load.")); }); } } @@ -808,6 +826,11 @@ TilesetContentManager::getAsyncDestructionCompleteEvent() { return this->_destructionCompleteFuture; } +CesiumAsync::SharedFuture& +TilesetContentManager::getRootTileAvailableEvent() { + return this->_rootTileAvailableFuture; +} + TilesetContentManager::~TilesetContentManager() noexcept { assert(this->_tileLoadsInProgress == 0); this->unloadAll(); diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index 355fd82a2..273db7cb5 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -51,6 +51,13 @@ class TilesetContentManager */ CesiumAsync::SharedFuture& getAsyncDestructionCompleteEvent(); + /** + * @brief A future that resolves when the details of the root tile of this + * tileset are available. The root tile's content (e.g., 3D model), however, + * will not necessarily be loaded yet. + */ + CesiumAsync::SharedFuture& getRootTileAvailableEvent(); + ~TilesetContentManager() noexcept; void loadTileContent(Tile& tile, const TilesetOptions& tilesetOptions); @@ -146,5 +153,8 @@ class TilesetContentManager CesiumAsync::Promise _destructionCompletePromise; CesiumAsync::SharedFuture _destructionCompleteFuture; + + CesiumAsync::Promise _rootTileAvailablePromise; + CesiumAsync::SharedFuture _rootTileAvailableFuture; }; } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp index f61d07172..e6433575d 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp @@ -4,6 +4,9 @@ #include "ImplicitQuadtreeLoader.h" #include "logTileLoadResult.h" +#include +#include +#include #include #include #include @@ -30,10 +33,13 @@ struct ExternalContentInitializer { std::shared_ptr> pExternalTilesetLoaders; TilesetJsonLoader* tilesetJsonLoader; + TileExternalContent externalContent; void operator()(Tile& tile) { - TileContent& content = tile.getContent(); - if (content.isExternalContent()) { + TileExternalContent* pExternalContent = + tile.getContent().getExternalContent(); + if (pExternalContent) { + *pExternalContent = std::move(externalContent); std::unique_ptr& pExternalRoot = pExternalTilesetLoaders->pRootTile; if (pExternalRoot) { // propagate all the external tiles to be the children of this tile @@ -511,7 +517,7 @@ std::optional parseTileJsonRecursively( if (implicitTilingJson) { // mark this tile as external - Tile tile{¤tLoader, TileExternalContent{}}; + Tile tile{¤tLoader, std::make_unique()}; tile.setTileID(""); tile.setTransform(tileTransform); tile.setBoundingVolume(tileBoundingVolume); @@ -618,31 +624,42 @@ TilesetContentLoaderResult parseTilesetJson( ErrorList{}}; } -TilesetContentLoaderResult parseTilesetJson( - const std::shared_ptr& pLogger, +void parseTilesetMetadata( const std::string& baseUrl, - const gsl::span& tilesetJsonBinary, - const glm::dmat4& parentTransform, - TileRefine parentRefine) { - rapidjson::Document tilesetJson; - tilesetJson.Parse( - reinterpret_cast(tilesetJsonBinary.data()), - tilesetJsonBinary.size()); - if (tilesetJson.HasParseError()) { - TilesetContentLoaderResult result; - result.errors.emplaceError(fmt::format( - "Error when parsing tileset JSON, error code {} at byte offset {}", - tilesetJson.GetParseError(), - tilesetJson.GetErrorOffset())); - return result; + const rapidjson::Document& tilesetJson, + TileExternalContent& externalContent) { + auto schemaIt = tilesetJson.FindMember("schema"); + if (schemaIt != tilesetJson.MemberEnd()) { + Cesium3DTilesReader::SchemaReader schemaReader; + auto schemaResult = schemaReader.readFromJson(schemaIt->value); + if (schemaResult.value) { + externalContent.metadata.schema = std::move(*schemaResult.value); + } } - return parseTilesetJson( - pLogger, - baseUrl, - tilesetJson, - parentTransform, - parentRefine); + auto schemaUriIt = tilesetJson.FindMember("schemaUri"); + if (schemaUriIt != tilesetJson.MemberEnd() && schemaUriIt->value.IsString()) { + externalContent.metadata.schemaUri = + CesiumUtility::Uri::resolve(baseUrl, schemaUriIt->value.GetString()); + } + + const auto metadataIt = tilesetJson.FindMember("metadata"); + if (metadataIt != tilesetJson.MemberEnd()) { + Cesium3DTilesReader::MetadataEntityReader metadataReader; + auto metadataResult = metadataReader.readFromJson(metadataIt->value); + if (metadataResult.value) { + externalContent.metadata.metadata = std::move(*metadataResult.value); + } + } + + const auto groupsIt = tilesetJson.FindMember("groups"); + if (groupsIt != tilesetJson.MemberEnd()) { + Cesium3DTilesReader::GroupMetadataReader groupMetadataReader; + auto groupsResult = groupMetadataReader.readArrayFromJson(groupsIt->value); + if (groupsResult.value) { + externalContent.metadata.groups = std::move(*groupsResult.value); + } + } } TileLoadResult parseExternalTilesetInWorkerThread( @@ -657,6 +674,19 @@ TileLoadResult parseExternalTilesetInWorkerThread( const auto& responseData = pResponse->data(); const auto& tileUrl = pCompletedRequest->url(); + rapidjson::Document tilesetJson; + tilesetJson.Parse( + reinterpret_cast(responseData.data()), + responseData.size()); + if (tilesetJson.HasParseError()) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Error when parsing tileset JSON, error code {} at byte offset {}", + tilesetJson.GetParseError(), + tilesetJson.GetErrorOffset()); + return TileLoadResult::createFailedResult(std::move(pCompletedRequest)); + } + // Save the parsed external tileset into custom data. // We will propagate it back to tile later in the main // thread @@ -664,10 +694,16 @@ TileLoadResult parseExternalTilesetInWorkerThread( parseTilesetJson( pLogger, tileUrl, - responseData, + tilesetJson, tileTransform, tileRefine); + // Populate the root tile with metadata + parseTilesetMetadata( + tileUrl, + tilesetJson, + externalContentInitializer.externalContent); + // check and log any errors const auto& errors = externalTilesetLoader.errors; if (errors) { @@ -692,12 +728,13 @@ TileLoadResult parseExternalTilesetInWorkerThread( std::move(externalContentInitializer), TileLoadResultState::Success}; } + } // namespace TilesetJsonLoader::TilesetJsonLoader( const std::string& baseUrl, CesiumGeometry::Axis upAxis) - : _baseUrl{baseUrl}, _upAxis{upAxis} {} + : _baseUrl{baseUrl}, _upAxis{upAxis}, _children{} {} CesiumAsync::Future> TilesetJsonLoader::createLoader( @@ -731,12 +768,26 @@ TilesetJsonLoader::createLoader( return result; } - return parseTilesetJson( + gsl::span data = pResponse->data(); + + rapidjson::Document tilesetJson; + tilesetJson.Parse( + reinterpret_cast(data.data()), + data.size()); + if (tilesetJson.HasParseError()) { + TilesetContentLoaderResult result; + result.errors.emplaceError(fmt::format( + "Error when parsing tileset JSON, error code {} at byte offset " + "{}", + tilesetJson.GetParseError(), + tilesetJson.GetErrorOffset())); + return result; + } + + return TilesetJsonLoader::createLoader( pLogger, pCompletedRequest->url(), - pResponse->data(), - glm::dmat4(1.0), - TileRefine::Replace); + tilesetJson); }); } @@ -744,12 +795,37 @@ TilesetContentLoaderResult TilesetJsonLoader::createLoader( const std::shared_ptr& pLogger, const std::string& tilesetJsonUrl, const rapidjson::Document& tilesetJson) { - return parseTilesetJson( + TilesetContentLoaderResult result = parseTilesetJson( pLogger, tilesetJsonUrl, tilesetJson, glm::dmat4(1.0), TileRefine::Replace); + + // Create a root tile to represent the tileset.json itself. + std::vector children; + children.emplace_back(std::move(*result.pRootTile)); + + result.pRootTile = std::make_unique( + children[0].getLoader(), + std::make_unique()); + + result.pRootTile->setTileID(""); + result.pRootTile->setTransform(children[0].getTransform()); + result.pRootTile->setBoundingVolume(children[0].getBoundingVolume()); + result.pRootTile->setUnconditionallyRefine(); + result.pRootTile->setRefine(children[0].getRefine()); + result.pRootTile->createChildTiles(std::move(children)); + + // Populate the root tile with metadata + TileExternalContent* pExternal = + result.pRootTile->getContent().getExternalContent(); + assert(pExternal); + if (pExternal) { + parseTilesetMetadata(tilesetJsonUrl, tilesetJson, *pExternal); + } + + return result; } CesiumAsync::Future @@ -771,7 +847,7 @@ TilesetJsonLoader::loadTileContent(const TileLoadInput& loadInput) { const glm::dmat4& tileTransform = tile.getTransform(); TileRefine tileRefine = tile.getRefine(); - ExternalContentInitializer externalContentInitializer{nullptr, this}; + ExternalContentInitializer externalContentInitializer{nullptr, this, {}}; const auto& asyncSystem = loadInput.asyncSystem; const auto& pAssetAccessor = loadInput.pAssetAccessor; diff --git a/Cesium3DTilesSelection/src/TilesetMetadata.cpp b/Cesium3DTilesSelection/src/TilesetMetadata.cpp new file mode 100644 index 000000000..a42c76eea --- /dev/null +++ b/Cesium3DTilesSelection/src/TilesetMetadata.cpp @@ -0,0 +1,93 @@ +#include +#include +#include +#include +#include +#include + +using namespace CesiumAsync; +using namespace Cesium3DTilesReader; +using namespace CesiumUtility; + +namespace Cesium3DTilesSelection { + +TilesetMetadata::~TilesetMetadata() noexcept { + if (this->_pLoadingCanceled) { + *this->_pLoadingCanceled = true; + } +} + +SharedFuture& TilesetMetadata::loadSchemaUri( + const AsyncSystem& asyncSystem, + const std::shared_ptr& pAssetAccessor) { + if (!this->_loadingFuture || this->_loadingSchemaUri != this->schemaUri) { + this->_loadingSchemaUri = this->schemaUri; + + if (this->_pLoadingCanceled) { + *this->_pLoadingCanceled = true; + this->_pLoadingCanceled.reset(); + } + + if (!this->schemaUri) { + this->_loadingFuture = asyncSystem.createResolvedFuture().share(); + } else { + std::shared_ptr pLoadingCanceled = std::make_shared(false); + this->_pLoadingCanceled = pLoadingCanceled; + this->_loadingFuture = + pAssetAccessor->get(asyncSystem, *this->schemaUri) + .thenInMainThread([pLoadingCanceled, this, asyncSystem]( + std::shared_ptr&& pRequest) { + Promise promise = asyncSystem.createPromise(); + + if (*pLoadingCanceled) { + promise.reject(std::runtime_error(fmt::format( + "Loading of schema URI {} was canceled.", + pRequest->url()))); + return promise.getFuture(); + } + + const IAssetResponse* pResponse = pRequest->response(); + if (!pResponse) { + promise.reject(std::runtime_error(fmt::format( + "Did not receive a valid response for schema URI {}", + pRequest->url()))); + return promise.getFuture(); + } + + uint16_t statusCode = pResponse->statusCode(); + if (statusCode != 0 && + (statusCode < 200 || statusCode >= 300)) { + promise.reject(std::runtime_error(fmt::format( + "Received status code {} for schema URI {}.", + statusCode, + pRequest->url()))); + return promise.getFuture(); + } + + SchemaReader reader; + auto result = reader.readFromJson(pResponse->data()); + if (!result.value) { + std::string errors = + CesiumUtility::joinToString(result.errors, "\n - "); + if (!errors.empty()) { + errors = " Errors:\n - " + errors; + } + promise.reject(std::runtime_error(fmt::format( + "Error reading Schema from {}.{}", + pRequest->url(), + errors))); + } + + this->schema = std::move(*result.value); + + promise.resolve(); + return promise.getFuture(); + }) + .share(); + } + } + + return *this->_loadingFuture; +} + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp index 925abb554..9755229ea 100644 --- a/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetContentManager.cpp @@ -219,8 +219,10 @@ TEST_CASE("Test the manager can be initialized with correct loaders") { CHECK(manager.getNumberOfTilesLoaded() == 1); // check root - const Tile* pRootTile = manager.getRootTile(); - CHECK(pRootTile); + const Tile* pTilesetJson = manager.getRootTile(); + REQUIRE(pTilesetJson); + REQUIRE(pTilesetJson->getChildren().size() == 1); + const Tile* pRootTile = &pTilesetJson->getChildren()[0]; CHECK(std::get(pRootTile->getTileID()) == "parent.b3dm"); CHECK(pRootTile->getGeometricError() == 70.0); CHECK(pRootTile->getRefine() == TileRefine::Add); diff --git a/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp index be993bf03..64f370bdf 100644 --- a/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetJsonLoader.cpp @@ -118,10 +118,13 @@ TEST_CASE("Test creating tileset json loader") { CHECK(!loaderResult.errors.hasErrors()); // check root tile - auto pRootTile = loaderResult.pRootTile.get(); - CHECK(pRootTile != nullptr); - CHECK(pRootTile->getParent() == nullptr); - CHECK(pRootTile->getChildren().size() == 4); + auto pTilesetJson = loaderResult.pRootTile.get(); + REQUIRE(pTilesetJson); + REQUIRE(pTilesetJson->getChildren().size() == 1); + CHECK(pTilesetJson->getParent() == nullptr); + auto pRootTile = &pTilesetJson->getChildren()[0]; + CHECK(pRootTile->getParent() == pTilesetJson); + REQUIRE(pRootTile->getChildren().size() == 4); CHECK(pRootTile->getGeometricError() == 70.0); CHECK(pRootTile->getRefine() == TileRefine::Replace); CHECK(std::get(pRootTile->getTileID()) == "parent.b3dm"); @@ -182,9 +185,12 @@ TEST_CASE("Test creating tileset json loader") { CHECK(!loaderResult.errors.hasErrors()); // check root tile - auto pRootTile = loaderResult.pRootTile.get(); - CHECK(pRootTile != nullptr); - CHECK(pRootTile->getParent() == nullptr); + auto pTilesetJson = loaderResult.pRootTile.get(); + REQUIRE(pTilesetJson != nullptr); + CHECK(pTilesetJson->getParent() == nullptr); + REQUIRE(pTilesetJson->getChildren().size() == 1); + auto pRootTile = &pTilesetJson->getChildren()[0]; + CHECK(pRootTile->getParent() == pTilesetJson); CHECK(pRootTile->getChildren().size() == 4); CHECK(pRootTile->getGeometricError() == 70.0); CHECK(pRootTile->getRefine() == TileRefine::Add); @@ -225,9 +231,10 @@ TEST_CASE("Test creating tileset json loader") { testDataPath / "MultipleKindsOfTilesets" / "SphereBoundingVolumeTileset.json"); - CHECK(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); - const Tile& rootTile = *loaderResult.pRootTile; + const Tile& rootTile = loaderResult.pRootTile->getChildren()[0]; const CesiumGeometry::BoundingSphere& sphere = std::get(rootTile.getBoundingVolume()); CHECK(sphere.getCenter() == glm::dvec3(0.0, 0.0, 10.0)); @@ -242,9 +249,10 @@ TEST_CASE("Test creating tileset json loader") { testDataPath / "MultipleKindsOfTilesets" / "BoxBoundingVolumeTileset.json"); - CHECK(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); - const Tile& rootTile = *loaderResult.pRootTile; + const Tile& rootTile = loaderResult.pRootTile->getChildren()[0]; const CesiumGeometry::OrientedBoundingBox& box = std::get( rootTile.getBoundingVolume()); @@ -261,8 +269,10 @@ TEST_CASE("Test creating tileset json loader") { "NoBoundingVolumeTileset.json"); CHECK(!loaderResult.errors.hasErrors()); - CHECK(loaderResult.pRootTile); - CHECK(loaderResult.pRootTile->getChildren().empty()); + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + CHECK(pRootTile->getChildren().empty()); // check loader up axis CHECK(loaderResult.pLoader->getUpAxis() == CesiumGeometry::Axis::Y); @@ -274,10 +284,12 @@ TEST_CASE("Test creating tileset json loader") { "NoGeometricErrorTileset.json"); CHECK(!loaderResult.errors.hasErrors()); - CHECK(loaderResult.pRootTile); - CHECK(loaderResult.pRootTile->getGeometricError() == Approx(70.0)); - CHECK(loaderResult.pRootTile->getChildren().size() == 4); - for (const Tile& child : loaderResult.pRootTile->getChildren()) { + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + CHECK(pRootTile->getGeometricError() == Approx(70.0)); + CHECK(pRootTile->getChildren().size() == 4); + for (const Tile& child : pRootTile->getChildren()) { CHECK(child.getGeometricError() == Approx(35.0)); } @@ -291,11 +303,13 @@ TEST_CASE("Test creating tileset json loader") { "NoCapitalizedRefineTileset.json"); CHECK(!loaderResult.errors.hasErrors()); - CHECK(loaderResult.pRootTile); - CHECK(loaderResult.pRootTile->getGeometricError() == Approx(70.0)); - CHECK(loaderResult.pRootTile->getRefine() == TileRefine::Add); - CHECK(loaderResult.pRootTile->getChildren().size() == 4); - for (const Tile& child : loaderResult.pRootTile->getChildren()) { + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + CHECK(pRootTile->getGeometricError() == Approx(70.0)); + CHECK(pRootTile->getRefine() == TileRefine::Add); + CHECK(pRootTile->getChildren().size() == 4); + for (const Tile& child : pRootTile->getChildren()) { CHECK(child.getGeometricError() == Approx(5.0)); CHECK(child.getRefine() == TileRefine::Replace); } @@ -310,10 +324,12 @@ TEST_CASE("Test creating tileset json loader") { "ScaleGeometricErrorTileset.json"); CHECK(!loaderResult.errors.hasErrors()); - CHECK(loaderResult.pRootTile); - CHECK(loaderResult.pRootTile->getGeometricError() == Approx(210.0)); - CHECK(loaderResult.pRootTile->getChildren().size() == 4); - for (const Tile& child : loaderResult.pRootTile->getChildren()) { + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + CHECK(pRootTile->getGeometricError() == Approx(210.0)); + CHECK(pRootTile->getChildren().size() == 4); + for (const Tile& child : pRootTile->getChildren()) { CHECK(child.getGeometricError() == Approx(15.0)); } @@ -325,11 +341,13 @@ TEST_CASE("Test creating tileset json loader") { auto loaderResult = createLoader( testDataPath / "MultipleKindsOfTilesets" / "EmptyTileTileset.json"); CHECK(!loaderResult.errors.hasErrors()); - CHECK(loaderResult.pRootTile); - CHECK(loaderResult.pRootTile->getGeometricError() == Approx(70.0)); - CHECK(loaderResult.pRootTile->getChildren().size() == 1); + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + CHECK(pRootTile->getGeometricError() == Approx(70.0)); + CHECK(pRootTile->getChildren().size() == 1); - const Tile& child = loaderResult.pRootTile->getChildren().front(); + const Tile& child = pRootTile->getChildren().front(); CHECK(child.isEmptyContent()); // check loader up axis @@ -341,19 +359,19 @@ TEST_CASE("Test creating tileset json loader") { testDataPath / "MultipleKindsOfTilesets" / "QuadtreeImplicitTileset.json"); CHECK(!loaderResult.errors.hasErrors()); - CHECK(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile); CHECK(loaderResult.pRootTile->isExternalContent()); - CHECK(loaderResult.pRootTile->getChildren().size() == 1); - CHECK( - loaderResult.pRootTile->getTransform() == glm::dmat4(glm::dmat3(2.0))); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + CHECK(pRootTile->isExternalContent()); + REQUIRE(pRootTile->getChildren().size() == 1); + CHECK(pRootTile->getTransform() == glm::dmat4(glm::dmat3(2.0))); - const Tile& child = loaderResult.pRootTile->getChildren().front(); + const Tile& child = pRootTile->getChildren().front(); CHECK(child.getLoader() != loaderResult.pLoader.get()); - CHECK( - child.getGeometricError() == - loaderResult.pRootTile->getGeometricError()); - CHECK(child.getRefine() == loaderResult.pRootTile->getRefine()); - CHECK(child.getTransform() == loaderResult.pRootTile->getTransform()); + CHECK(child.getGeometricError() == pRootTile->getGeometricError()); + CHECK(child.getRefine() == pRootTile->getRefine()); + CHECK(child.getTransform() == pRootTile->getTransform()); CHECK( std::get(child.getTileID()) == CesiumGeometry::QuadtreeTileID(0, 0, 0)); @@ -364,23 +382,41 @@ TEST_CASE("Test creating tileset json loader") { testDataPath / "MultipleKindsOfTilesets" / "OctreeImplicitTileset.json"); CHECK(!loaderResult.errors.hasErrors()); - CHECK(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile); CHECK(loaderResult.pRootTile->isExternalContent()); - CHECK(loaderResult.pRootTile->getChildren().size() == 1); - CHECK( - loaderResult.pRootTile->getTransform() == glm::dmat4(glm::dmat3(2.0))); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + CHECK(pRootTile->isExternalContent()); + REQUIRE(pRootTile->getChildren().size() == 1); + CHECK(pRootTile->getTransform() == glm::dmat4(glm::dmat3(2.0))); - const Tile& child = loaderResult.pRootTile->getChildren().front(); + const Tile& child = pRootTile->getChildren().front(); CHECK(child.getLoader() != loaderResult.pLoader.get()); - CHECK( - child.getGeometricError() == - loaderResult.pRootTile->getGeometricError()); - CHECK(child.getRefine() == loaderResult.pRootTile->getRefine()); - CHECK(child.getTransform() == loaderResult.pRootTile->getTransform()); + CHECK(child.getGeometricError() == pRootTile->getGeometricError()); + CHECK(child.getRefine() == pRootTile->getRefine()); + CHECK(child.getTransform() == pRootTile->getTransform()); CHECK( std::get(child.getTileID()) == CesiumGeometry::OctreeTileID(0, 0, 0, 0)); } + + SECTION("Tileset with metadata") { + auto loaderResult = + createLoader(testDataPath / "WithMetadata" / "tileset.json"); + + CHECK(!loaderResult.errors.hasErrors()); + REQUIRE(loaderResult.pLoader); + REQUIRE(loaderResult.pRootTile); + + TileExternalContent* pExternal = + loaderResult.pRootTile->getContent().getExternalContent(); + REQUIRE(pExternal); + + const TilesetMetadata& metadata = pExternal->metadata; + const std::optional& schema = metadata.schema; + REQUIRE(schema); + CHECK(schema->id == "foo"); + } } TEST_CASE("Test loading individual tile of tileset json") { @@ -389,17 +425,19 @@ TEST_CASE("Test loading individual tile of tileset json") { SECTION("Load tile that has render content") { auto loaderResult = createLoader(testDataPath / "ReplaceTileset" / "tileset.json"); - CHECK(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); - const auto& tileID = - std::get(loaderResult.pRootTile->getTileID()); + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + + const auto& tileID = std::get(pRootTile->getTileID()); CHECK(tileID == "parent.b3dm"); // check tile content auto tileLoadResult = loadTileContent( testDataPath / "ReplaceTileset" / tileID, *loaderResult.pLoader, - *loaderResult.pRootTile); + *pRootTile); CHECK( std::holds_alternative(tileLoadResult.contentKind)); CHECK(tileLoadResult.updatedBoundingVolume == std::nullopt); @@ -412,17 +450,19 @@ TEST_CASE("Test loading individual tile of tileset json") { auto loaderResult = createLoader(testDataPath / "AddTileset" / "tileset.json"); - CHECK(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); - const auto& tileID = - std::get(loaderResult.pRootTile->getTileID()); + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + + const auto& tileID = std::get(pRootTile->getTileID()); CHECK(tileID == "tileset2.json"); // check tile content auto tileLoadResult = loadTileContent( testDataPath / "AddTileset" / tileID, *loaderResult.pLoader, - *loaderResult.pRootTile); + *pRootTile); CHECK(tileLoadResult.updatedBoundingVolume == std::nullopt); CHECK(tileLoadResult.updatedContentBoundingVolume == std::nullopt); CHECK(std::holds_alternative( @@ -431,11 +471,12 @@ TEST_CASE("Test loading individual tile of tileset json") { CHECK(tileLoadResult.tileInitializer); // check tile is really an external tile - loaderResult.pRootTile->getContent().setContentKind( - std::get(tileLoadResult.contentKind)); - tileLoadResult.tileInitializer(*loaderResult.pRootTile); - const auto& children = loaderResult.pRootTile->getChildren(); - CHECK(children.size() == 1); + pRootTile->getContent().setContentKind( + std::make_unique( + std::get(tileLoadResult.contentKind))); + tileLoadResult.tileInitializer(*pRootTile); + const auto& children = pRootTile->getChildren(); + REQUIRE(children.size() == 1); const Tile& parentB3dmTile = children[0]; CHECK(std::get(parentB3dmTile.getTileID()) == "parent.b3dm"); @@ -461,11 +502,14 @@ TEST_CASE("Test loading individual tile of tileset json") { auto loaderResult = createLoader(testDataPath / "ImplicitTileset" / "tileset_1.1.json"); - CHECK(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile); CHECK(loaderResult.pRootTile->isExternalContent()); - CHECK(loaderResult.pRootTile->getChildren().size() == 1); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + REQUIRE(pRootTile->getChildren().size() == 1); - auto& implicitTile = loaderResult.pRootTile->getChildren().front(); + auto& implicitTile = pRootTile->getChildren().front(); const auto& tileID = std::get(implicitTile.getTileID()); CHECK(tileID == CesiumGeometry::QuadtreeTileID(0, 0, 0)); @@ -557,11 +601,14 @@ TEST_CASE("Test loading individual tile of tileset json") { auto loaderResult = createLoader(testDataPath / "ImplicitTileset" / "tileset_1.0.json"); - CHECK(loaderResult.pRootTile); + REQUIRE(loaderResult.pRootTile); CHECK(loaderResult.pRootTile->isExternalContent()); - CHECK(loaderResult.pRootTile->getChildren().size() == 1); + REQUIRE(loaderResult.pRootTile->getChildren().size() == 1); + + auto pRootTile = &loaderResult.pRootTile->getChildren()[0]; + REQUIRE(pRootTile->getChildren().size() == 1); - auto& implicitTile = loaderResult.pRootTile->getChildren().front(); + auto& implicitTile = pRootTile->getChildren().front(); const auto& tileID = std::get(implicitTile.getTileID()); CHECK(tileID == CesiumGeometry::QuadtreeTileID(0, 0, 0)); diff --git a/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp b/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp index 90811bdc6..f8784addc 100644 --- a/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp +++ b/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorithm.cpp @@ -7,6 +7,7 @@ #include "SimplePrepareRendererResource.h" #include "SimpleTaskProcessor.h" +#include #include #include @@ -169,7 +170,12 @@ TEST_CASE("Test replace refinement for render") { initializeTileset(tileset); // check the tiles status - const Tile* root = tileset.getRootTile(); + const Tile* pTilesetJson = tileset.getRootTile(); + REQUIRE(pTilesetJson); + REQUIRE(pTilesetJson->getChildren().size() == 1); + + const Tile* root = &pTilesetJson->getChildren()[0]; + REQUIRE(root->getState() == TileLoadState::ContentLoading); for (const auto& child : root->getChildren()) { REQUIRE(child.getState() == TileLoadState::Unloaded); @@ -208,7 +214,7 @@ TEST_CASE("Test replace refinement for render") { REQUIRE(result.tilesFadingOut.size() == 0); - REQUIRE(result.tilesVisited == 1); + REQUIRE(result.tilesVisited == 2); REQUIRE(result.workerThreadTileLoadQueueLength == 0); REQUIRE(result.mainThreadTileLoadQueueLength == 0); REQUIRE(result.tilesCulled == 0); @@ -269,7 +275,7 @@ TEST_CASE("Test replace refinement for render") { REQUIRE(result.tilesFadingOut.size() == 0); - REQUIRE(result.tilesVisited == 5); + REQUIRE(result.tilesVisited == 6); REQUIRE(result.workerThreadTileLoadQueueLength == 4); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -293,7 +299,7 @@ TEST_CASE("Test replace refinement for render") { REQUIRE(result.tilesFadingOut.size() == 1); - REQUIRE(result.tilesVisited == 5); + REQUIRE(result.tilesVisited == 6); REQUIRE(result.workerThreadTileLoadQueueLength == 0); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -342,7 +348,7 @@ TEST_CASE("Test replace refinement for render") { REQUIRE(result.tilesFadingOut.size() == 0); - REQUIRE(result.tilesVisited == 6); + REQUIRE(result.tilesVisited == 7); REQUIRE(result.workerThreadTileLoadQueueLength == 5); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -384,7 +390,7 @@ TEST_CASE("Test replace refinement for render") { REQUIRE(result.tilesFadingOut.size() == 1); - REQUIRE(result.tilesVisited == 6); + REQUIRE(result.tilesVisited == 7); REQUIRE(result.workerThreadTileLoadQueueLength == 0); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -437,7 +443,7 @@ TEST_CASE("Test replace refinement for render") { REQUIRE(result.tilesFadingOut.size() == 1); - REQUIRE(result.tilesVisited == 5); + REQUIRE(result.tilesVisited == 6); REQUIRE(result.workerThreadTileLoadQueueLength == 0); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -467,7 +473,7 @@ TEST_CASE("Test replace refinement for render") { REQUIRE(result.tilesFadingOut.size() == 0); - REQUIRE(result.tilesVisited == 5); + REQUIRE(result.tilesVisited == 6); REQUIRE(result.workerThreadTileLoadQueueLength == 4); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -496,7 +502,7 @@ TEST_CASE("Test replace refinement for render") { REQUIRE(result.tilesFadingOut.size() == 1); - REQUIRE(result.tilesVisited == 5); + REQUIRE(result.tilesVisited == 6); REQUIRE(result.workerThreadTileLoadQueueLength == 0); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -551,7 +557,11 @@ TEST_CASE("Test additive refinement") { // root is external tileset. Since its content is loading, we won't know if it // has children or not - const Tile* root = tileset.getRootTile(); + const Tile* pTilesetJson = tileset.getRootTile(); + REQUIRE(pTilesetJson); + REQUIRE(pTilesetJson->getChildren().size() == 1); + + const Tile* root = &pTilesetJson->getChildren()[0]; REQUIRE(root->getState() == TileLoadState::ContentLoading); REQUIRE(root->getChildren().size() == 0); @@ -581,11 +591,11 @@ TEST_CASE("Test additive refinement") { } REQUIRE(result.tilesToRenderThisFrame.size() == 1); - REQUIRE(result.tilesToRenderThisFrame.front() == root); + REQUIRE(result.tilesToRenderThisFrame.front() == pTilesetJson); REQUIRE(result.tilesFadingOut.size() == 0); - REQUIRE(result.tilesVisited == 6); + REQUIRE(result.tilesVisited == 7); REQUIRE(result.workerThreadTileLoadQueueLength == 5); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -627,13 +637,14 @@ TEST_CASE("Test additive refinement") { } } - REQUIRE(result.tilesToRenderThisFrame.size() == 2); - REQUIRE(result.tilesToRenderThisFrame[0] == root); - REQUIRE(result.tilesToRenderThisFrame[1] == &parentB3DM); + REQUIRE(result.tilesToRenderThisFrame.size() == 3); + REQUIRE(result.tilesToRenderThisFrame[0] == pTilesetJson); + REQUIRE(result.tilesToRenderThisFrame[1] == root); + REQUIRE(result.tilesToRenderThisFrame[2] == &parentB3DM); REQUIRE(result.tilesFadingOut.size() == 0); - REQUIRE(result.tilesVisited == 7); + REQUIRE(result.tilesVisited == 8); REQUIRE(result.workerThreadTileLoadQueueLength == 1); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -643,11 +654,11 @@ TEST_CASE("Test additive refinement") { { ViewUpdateResult result = tileset.updateView({viewState}); - REQUIRE(result.tilesToRenderThisFrame.size() == 7); + REQUIRE(result.tilesToRenderThisFrame.size() == 8); REQUIRE(result.tilesFadingOut.size() == 0); - REQUIRE(result.tilesVisited == 7); + REQUIRE(result.tilesVisited == 8); REQUIRE(result.workerThreadTileLoadQueueLength == 0); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -700,7 +711,11 @@ TEST_CASE("Render any tiles even when one of children can't be rendered for " initializeTileset(tileset); ViewState viewState = zoomToTileset(tileset); - Tile* root = tileset.getRootTile(); + Tile* pTilesetJson = tileset.getRootTile(); + REQUIRE(pTilesetJson); + REQUIRE(pTilesetJson->getChildren().size() == 1); + Tile* root = &pTilesetJson->getChildren()[0]; + REQUIRE(!doesTileMeetSSE(viewState, *root, tileset)); REQUIRE(root->getState() == TileLoadState::ContentLoading); REQUIRE(root->getChildren().size() == 3); @@ -714,9 +729,9 @@ TEST_CASE("Render any tiles even when one of children can't be rendered for " CHECK(child.getState() == TileLoadState::ContentLoading); } - REQUIRE(result.tilesToRenderThisFrame.size() == 1); + REQUIRE(result.tilesToRenderThisFrame.size() == 2); REQUIRE(result.tilesFadingOut.size() == 0); - REQUIRE(result.tilesVisited == 4); + REQUIRE(result.tilesVisited == 5); REQUIRE(result.workerThreadTileLoadQueueLength == 3); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -738,9 +753,9 @@ TEST_CASE("Render any tiles even when one of children can't be rendered for " REQUIRE(child.isRenderable()); } - REQUIRE(result.tilesToRenderThisFrame.size() == 4); + REQUIRE(result.tilesToRenderThisFrame.size() == 5); REQUIRE(result.tilesFadingOut.size() == 0); - REQUIRE(result.tilesVisited == 4); + REQUIRE(result.tilesVisited == 5); REQUIRE(result.workerThreadTileLoadQueueLength == 0); REQUIRE(result.tilesCulled == 0); REQUIRE(result.culledTilesVisited == 0); @@ -793,7 +808,10 @@ TEST_CASE("Test multiple frustums") { initializeTileset(tileset); // check the tiles status - const Tile* root = tileset.getRootTile(); + const Tile* pTilesetJson = tileset.getRootTile(); + REQUIRE(pTilesetJson); + REQUIRE(pTilesetJson->getChildren().size() == 1); + const Tile* root = &pTilesetJson->getChildren()[0]; REQUIRE(root->getState() == TileLoadState::ContentLoading); for (const auto& child : root->getChildren()) { REQUIRE(child.getState() == TileLoadState::Unloaded); @@ -856,7 +874,7 @@ TEST_CASE("Test multiple frustums") { REQUIRE(result.tilesFadingOut.size() == 1); REQUIRE(*result.tilesFadingOut.begin() == root); - REQUIRE(result.tilesVisited == 5); + REQUIRE(result.tilesVisited == 6); REQUIRE(result.workerThreadTileLoadQueueLength == 0); REQUIRE(result.tilesCulled == 0); } @@ -905,7 +923,7 @@ TEST_CASE("Test multiple frustums") { // The grand child and the second child are the only ones rendered. // The third and fourth children of the root are culled. REQUIRE(result.tilesToRenderThisFrame.size() == 2); - REQUIRE(result.tilesVisited == 4); + REQUIRE(result.tilesVisited == 5); REQUIRE( std::find( result.tilesToRenderThisFrame.begin(), @@ -1028,7 +1046,11 @@ TEST_CASE("Can load example tileset.json from 3DTILES_bounding_volume_S2 " tilesetExternals.asyncSystem.dispatchMainThreadTasks(); } - const Tile* pRoot = tileset.getRootTile(); + const Tile* pTilesetJson = tileset.getRootTile(); + REQUIRE(pTilesetJson); + REQUIRE(pTilesetJson->getChildren().size() == 1); + const Tile* pRoot = &pTilesetJson->getChildren()[0]; + const S2CellBoundingVolume* pS2 = std::get_if(&pRoot->getBoundingVolume()); REQUIRE(pS2); @@ -1070,6 +1092,378 @@ TEST_CASE("Can load example tileset.json from 3DTILES_bounding_volume_S2 " REQUIRE(pGreatGrandchild->getChildren().empty()); } +TEST_CASE("Makes metadata available once root tile is loaded") { + Cesium3DTilesSelection::registerAllTileContentTypes(); + + std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR; + testDataPath = testDataPath / "WithMetadata"; + std::vector files{ + "tileset.json", + "external-tileset.json", + "parent.b3dm", + "ll.b3dm", + "lr.b3dm", + "ul.b3dm", + "ur.b3dm"}; + + std::map> + mockCompletedRequests; + for (const auto& file : files) { + std::unique_ptr mockCompletedResponse = + std::make_unique( + static_cast(200), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + readFile(testDataPath / file)); + mockCompletedRequests.insert( + {file, + std::make_shared( + "GET", + file, + CesiumAsync::HttpHeaders{}, + std::move(mockCompletedResponse))}); + } + + std::shared_ptr mockAssetAccessor = + std::make_shared(std::move(mockCompletedRequests)); + TilesetExternals tilesetExternals{ + mockAssetAccessor, + std::make_shared(), + AsyncSystem(std::make_shared()), + nullptr}; + + // create tileset and call updateView() to give it a chance to load + Tileset tileset(tilesetExternals, "tileset.json"); + initializeTileset(tileset); + + Tile* pRoot = tileset.getRootTile(); + REQUIRE(pRoot); + + TileExternalContent* pExternal = pRoot->getContent().getExternalContent(); + REQUIRE(pExternal); + + const TilesetMetadata& metadata = pExternal->metadata; + const std::optional& schema = metadata.schema; + REQUIRE(schema); + CHECK(schema->id == "foo"); +} + +TEST_CASE("Makes metadata available on external tilesets") { + Cesium3DTilesSelection::registerAllTileContentTypes(); + + std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR; + testDataPath = testDataPath / "WithMetadata"; + std::vector files{ + "tileset.json", + "external-tileset.json", + "parent.b3dm", + "ll.b3dm", + "lr.b3dm", + "ul.b3dm", + "ur.b3dm"}; + + std::map> + mockCompletedRequests; + for (const auto& file : files) { + std::unique_ptr mockCompletedResponse = + std::make_unique( + static_cast(200), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + readFile(testDataPath / file)); + mockCompletedRequests.insert( + {file, + std::make_shared( + "GET", + file, + CesiumAsync::HttpHeaders{}, + std::move(mockCompletedResponse))}); + } + + std::shared_ptr mockAssetAccessor = + std::make_shared(std::move(mockCompletedRequests)); + TilesetExternals tilesetExternals{ + mockAssetAccessor, + std::make_shared(), + AsyncSystem(std::make_shared()), + nullptr}; + + // create tileset and call updateView() to give it a chance to load + Tileset tileset(tilesetExternals, "tileset.json"); + initializeTileset(tileset); + + Tile* pTilesetJson = tileset.getRootTile(); + REQUIRE(pTilesetJson); + REQUIRE(pTilesetJson->getChildren().size() == 1); + + Tile* pRoot = &pTilesetJson->getChildren()[0]; + REQUIRE(pRoot); + REQUIRE(pRoot->getChildren().size() == 5); + Tile* pExternal = &pRoot->getChildren()[4]; + + TileExternalContent* pExternalContent = nullptr; + + for (int i = 0; i < 10 && pExternalContent == nullptr; ++i) { + ViewState zoomToTileViewState = zoomToTile(*pExternal); + tileset.updateView({zoomToTileViewState}); + pExternalContent = pExternal->getContent().getExternalContent(); + } + + REQUIRE(pExternalContent); + + REQUIRE(pExternalContent->metadata.groups.size() == 2); + CHECK(pExternalContent->metadata.groups[0].classProperty == "someClass"); + CHECK(pExternalContent->metadata.groups[1].classProperty == "someClass"); +} + +TEST_CASE("Allows access to material variants") { + Cesium3DTilesSelection::registerAllTileContentTypes(); + + std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR; + testDataPath = testDataPath / "MaterialVariants"; + std::vector files{"tileset.json", "parent.b3dm"}; + + std::map> + mockCompletedRequests; + for (const auto& file : files) { + std::unique_ptr mockCompletedResponse = + std::make_unique( + static_cast(200), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + readFile(testDataPath / file)); + mockCompletedRequests.insert( + {file, + std::make_shared( + "GET", + file, + CesiumAsync::HttpHeaders{}, + std::move(mockCompletedResponse))}); + } + + std::shared_ptr mockAssetAccessor = + std::make_shared(std::move(mockCompletedRequests)); + TilesetExternals tilesetExternals{ + mockAssetAccessor, + std::make_shared(), + AsyncSystem(std::make_shared()), + nullptr}; + + // create tileset and call updateView() to give it a chance to load + Tileset tileset(tilesetExternals, "tileset.json"); + initializeTileset(tileset); + + const TilesetMetadata* pMetadata = tileset.getMetadata(); + REQUIRE(pMetadata); + REQUIRE(pMetadata->schema); + REQUIRE(pMetadata->metadata); + + std::optional found1 = + Cesium3DTiles::MetadataQuery::findFirstPropertyWithSemantic( + *pMetadata->schema, + *pMetadata->metadata, + "MATERIAL_VARIANTS"); + REQUIRE(found1); + CHECK(found1->classIdentifier == "MaterialVariants"); + CHECK(found1->classDefinition.properties.size() == 1); + CHECK(found1->propertyIdentifier == "material_variants"); + CHECK( + found1->propertyDefinition.description == + "Names of material variants to be expected in the glTF assets"); + REQUIRE(found1->propertyValue.isArray()); + + std::vector variants = + found1->propertyValue.getArrayOfStrings(""); + REQUIRE(variants.size() == 4); + CHECK(variants[0] == "RGB"); + CHECK(variants[1] == "RRR"); + CHECK(variants[2] == "GGG"); + CHECK(variants[3] == "BBB"); + + std::vector> variantsByGroup; + for (const Cesium3DTiles::GroupMetadata& group : pMetadata->groups) { + std::optional found2 = + Cesium3DTiles::MetadataQuery::findFirstPropertyWithSemantic( + *pMetadata->schema, + group, + "MATERIAL_VARIANTS"); + REQUIRE(found2); + REQUIRE(found2->propertyValue.isArray()); + + variantsByGroup.emplace_back(found2->propertyValue.getArrayOfStrings("")); + } + + std::vector> expected = { + {"RGB", "RRR"}, + {"GGG", "BBB"}}; + + CHECK(variantsByGroup == expected); +} + +TEST_CASE("Allows access to material variants in an external schema") { + Cesium3DTilesSelection::registerAllTileContentTypes(); + + std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR; + testDataPath = testDataPath / "MaterialVariants"; + std::vector files{ + "tileset-external-schema.json", + "schema.json", + "parent.b3dm"}; + + std::map> + mockCompletedRequests; + for (const auto& file : files) { + std::unique_ptr mockCompletedResponse = + std::make_unique( + static_cast(200), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + readFile(testDataPath / file)); + mockCompletedRequests.insert( + {file, + std::make_shared( + "GET", + file, + CesiumAsync::HttpHeaders{}, + std::move(mockCompletedResponse))}); + } + + std::shared_ptr mockAssetAccessor = + std::make_shared(std::move(mockCompletedRequests)); + TilesetExternals tilesetExternals{ + mockAssetAccessor, + std::make_shared(), + AsyncSystem(std::make_shared()), + nullptr}; + + Tileset tileset(tilesetExternals, "tileset-external-schema.json"); + + // getMetadata returns nullptr before the root tile is loaded. + CHECK(tileset.getMetadata() == nullptr); + + bool wasCalled = false; + tileset.loadMetadata().thenInMainThread( + [&wasCalled](const TilesetMetadata* pMetadata) { + wasCalled = true; + REQUIRE(pMetadata); + REQUIRE(pMetadata->schema); + REQUIRE(pMetadata->metadata); + + std::optional found1 = + Cesium3DTiles::MetadataQuery::findFirstPropertyWithSemantic( + *pMetadata->schema, + *pMetadata->metadata, + "MATERIAL_VARIANTS"); + REQUIRE(found1); + CHECK(found1->classIdentifier == "MaterialVariants"); + CHECK(found1->classDefinition.properties.size() == 1); + CHECK(found1->propertyIdentifier == "material_variants"); + CHECK( + found1->propertyDefinition.description == + "Names of material variants to be expected in the glTF assets"); + REQUIRE(found1->propertyValue.isArray()); + + std::vector variants = + found1->propertyValue.getArrayOfStrings(""); + REQUIRE(variants.size() == 4); + CHECK(variants[0] == "RGB"); + CHECK(variants[1] == "RRR"); + CHECK(variants[2] == "GGG"); + CHECK(variants[3] == "BBB"); + + std::vector> variantsByGroup; + for (const Cesium3DTiles::GroupMetadata& group : pMetadata->groups) { + std::optional found2 = + Cesium3DTiles::MetadataQuery::findFirstPropertyWithSemantic( + *pMetadata->schema, + group, + "MATERIAL_VARIANTS"); + REQUIRE(found2); + REQUIRE(found2->propertyValue.isArray()); + variantsByGroup.emplace_back( + found2->propertyValue.getArrayOfStrings("")); + } + + std::vector> expected = { + {"RGB", "RRR"}, + {"GGG", "BBB"}}; + + CHECK(variantsByGroup == expected); + }); + + CHECK(!wasCalled); + initializeTileset(tileset); + CHECK(wasCalled); +} + +TEST_CASE("Future from loadSchema rejects if schemaUri can't be loaded") { + Cesium3DTilesSelection::registerAllTileContentTypes(); + + std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR; + testDataPath = testDataPath / "MaterialVariants"; + std::vector files{"tileset-external-schema.json", "parent.b3dm"}; + + std::map> + mockCompletedRequests; + for (const auto& file : files) { + std::unique_ptr mockCompletedResponse = + std::make_unique( + static_cast(200), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + readFile(testDataPath / file)); + mockCompletedRequests.insert( + {file, + std::make_shared( + "GET", + file, + CesiumAsync::HttpHeaders{}, + std::move(mockCompletedResponse))}); + } + + mockCompletedRequests.insert( + {"schema.json", + std::make_shared( + "GET", + "schema.json", + CesiumAsync::HttpHeaders{}, + std::make_unique( + static_cast(404), + "doesn't matter", + CesiumAsync::HttpHeaders{}, + std::vector()))}); + + std::shared_ptr mockAssetAccessor = + std::make_shared(std::move(mockCompletedRequests)); + TilesetExternals tilesetExternals{ + mockAssetAccessor, + std::make_shared(), + AsyncSystem(std::make_shared()), + nullptr}; + + Tileset tileset(tilesetExternals, "tileset-external-schema.json"); + + // getMetadata returns nullptr before the root tile is loaded. + CHECK(tileset.getMetadata() == nullptr); + + bool wasResolved = false; + bool wasRejected = false; + tileset.loadMetadata() + .thenInMainThread( + [&wasResolved](const TilesetMetadata*) { wasResolved = true; }) + .catchInMainThread([&wasRejected](const std::exception& exception) { + CHECK(std::string(exception.what()).find("") != std::string::npos); + wasRejected = true; + }); + + CHECK(!wasResolved); + CHECK(!wasRejected); + + initializeTileset(tileset); + CHECK(!wasResolved); + CHECK(wasRejected); +} + namespace { void runUnconditionallyRefinedTestCase(const TilesetOptions& options) { @@ -1261,8 +1655,8 @@ TEST_CASE("Additive-refined tiles are added to the tilesFadingOut array") { updateResult = tileset.updateView({viewState}); } - // All three tiles should be rendered. - CHECK(updateResult.tilesToRenderThisFrame.size() == 3); + // All three tiles (plus the tileset.json) should be rendered. + CHECK(updateResult.tilesToRenderThisFrame.size() == 4); // Zoom way out std::optional position = viewState.getPositionCartographic(); @@ -1278,7 +1672,8 @@ TEST_CASE("Additive-refined tiles are added to the tilesFadingOut array") { viewState.getVerticalFieldOfView()); updateResult = tileset.updateView({zoomedOut}); - // Only the root tile is visible now, and the other two are fading out. - CHECK(updateResult.tilesToRenderThisFrame.size() == 1); + // Only the root tile (plus the tileset.json) is visible now, and the other + // two are fading out. + CHECK(updateResult.tilesToRenderThisFrame.size() == 2); CHECK(updateResult.tilesFadingOut.size() == 2); } diff --git a/Cesium3DTilesSelection/test/data/MaterialVariants/parent.b3dm b/Cesium3DTilesSelection/test/data/MaterialVariants/parent.b3dm new file mode 100644 index 000000000..8cb958955 Binary files /dev/null and b/Cesium3DTilesSelection/test/data/MaterialVariants/parent.b3dm differ diff --git a/Cesium3DTilesSelection/test/data/MaterialVariants/schema.json b/Cesium3DTilesSelection/test/data/MaterialVariants/schema.json new file mode 100644 index 000000000..3a7004d29 --- /dev/null +++ b/Cesium3DTilesSelection/test/data/MaterialVariants/schema.json @@ -0,0 +1,14 @@ +{ + "classes": { + "MaterialVariants": { + "properties": { + "material_variants": { + "type": "STRING", + "array": true, + "description": "Names of material variants to be expected in the glTF assets", + "semantic": "MATERIAL_VARIANTS" + } + } + } + } +} diff --git a/Cesium3DTilesSelection/test/data/MaterialVariants/tileset-external-schema.json b/Cesium3DTilesSelection/test/data/MaterialVariants/tileset-external-schema.json new file mode 100644 index 000000000..6fcbf8ff0 --- /dev/null +++ b/Cesium3DTilesSelection/test/data/MaterialVariants/tileset-external-schema.json @@ -0,0 +1,76 @@ +{ + "asset": { + "version": "1.0", + "tilesetVersion": "1.2.3" + }, + "extras": { + "name": "Sample Tileset" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 85.41026367992163 + } + }, + "schemaUri": "schema.json", + "metadata": { + "class": "MaterialVariants", + "properties": { + "material_variants": ["RGB", "RRR", "GGG", "BBB"] + } + }, + "groups": [ + { + "class": "MaterialVariants", + "properties": { + "material_variants": ["RGB", "RRR"] + } + }, + { + "class": "MaterialVariants", + "properties": { + "material_variants": ["GGG", "BBB"] + } + } + ], + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "ADD", + "content": { + "uri": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + } + } +} diff --git a/Cesium3DTilesSelection/test/data/MaterialVariants/tileset.json b/Cesium3DTilesSelection/test/data/MaterialVariants/tileset.json new file mode 100644 index 000000000..c162854f2 --- /dev/null +++ b/Cesium3DTilesSelection/test/data/MaterialVariants/tileset.json @@ -0,0 +1,89 @@ +{ + "asset": { + "version": "1.0", + "tilesetVersion": "1.2.3" + }, + "extras": { + "name": "Sample Tileset" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 85.41026367992163 + } + }, + "schema": { + "classes": { + "MaterialVariants": { + "properties": { + "material_variants": { + "type": "STRING", + "array": true, + "description": "Names of material variants to be expected in the glTF assets", + "semantic": "MATERIAL_VARIANTS" + } + } + } + } + }, + "metadata": { + "class": "MaterialVariants", + "properties": { + "material_variants": ["RGB", "RRR", "GGG", "BBB"] + } + }, + "groups": [ + { + "class": "MaterialVariants", + "properties": { + "material_variants": ["RGB", "RRR"] + } + }, + { + "class": "MaterialVariants", + "properties": { + "material_variants": ["GGG", "BBB"] + } + } + ], + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "ADD", + "content": { + "uri": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + } + } +} diff --git a/Cesium3DTilesSelection/test/data/WithMetadata/README.md b/Cesium3DTilesSelection/test/data/WithMetadata/README.md new file mode 100644 index 000000000..a57c35345 --- /dev/null +++ b/Cesium3DTilesSelection/test/data/WithMetadata/README.md @@ -0,0 +1,2 @@ +The contents of this directory are from CesiumJS: +https://github.com/CesiumGS/cesium/tree/4f0d17c/Apps/SampleData/Cesium3DTiles/Tilesets/Tileset diff --git a/Cesium3DTilesSelection/test/data/WithMetadata/external-tileset.json b/Cesium3DTilesSelection/test/data/WithMetadata/external-tileset.json new file mode 100644 index 000000000..4ae1a25f2 --- /dev/null +++ b/Cesium3DTilesSelection/test/data/WithMetadata/external-tileset.json @@ -0,0 +1,67 @@ +{ + "asset": { + "version": "1.0", + "tilesetVersion": "1.2.3" + }, + "extras": { + "name": "Sample Tileset" + }, + "schema": { + "id": "bar", + "name": "Bar", + "description": "A description of Bar.", + "version": "3.2.1", + "classes": { + "someClass": { + "name": "Some Class!", + "description": "A description of Some Class.", + "properties": { + "someProperty": { + "name": "Some Property", + "description": "A description of Some Property.", + "type": "STRING", + "semantic": "GREAT" + } + } + } + } + }, + "groups": [ + { + "class": "someClass", + "properties": { + "someProperty": "test" + } + }, + { + "class": "someClass", + "properties": { + "someProperty": "another" + } + } + ], + "metadata": { + "class": "someClass", + "properties": { + "someProperty": "foo" + } + }, + "geometricError": 35, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 35, + "refine": "REPLACE", + "content": { + "uri": "parent.b3dm" + } + } +} diff --git a/Cesium3DTilesSelection/test/data/WithMetadata/ll.b3dm b/Cesium3DTilesSelection/test/data/WithMetadata/ll.b3dm new file mode 100644 index 000000000..df79fe32f Binary files /dev/null and b/Cesium3DTilesSelection/test/data/WithMetadata/ll.b3dm differ diff --git a/Cesium3DTilesSelection/test/data/WithMetadata/lr.b3dm b/Cesium3DTilesSelection/test/data/WithMetadata/lr.b3dm new file mode 100644 index 000000000..e273cb03a Binary files /dev/null and b/Cesium3DTilesSelection/test/data/WithMetadata/lr.b3dm differ diff --git a/Cesium3DTilesSelection/test/data/WithMetadata/parent.b3dm b/Cesium3DTilesSelection/test/data/WithMetadata/parent.b3dm new file mode 100644 index 000000000..8cb958955 Binary files /dev/null and b/Cesium3DTilesSelection/test/data/WithMetadata/parent.b3dm differ diff --git a/Cesium3DTilesSelection/test/data/WithMetadata/tileset.json b/Cesium3DTilesSelection/test/data/WithMetadata/tileset.json new file mode 100644 index 000000000..8d5452a90 --- /dev/null +++ b/Cesium3DTilesSelection/test/data/WithMetadata/tileset.json @@ -0,0 +1,160 @@ +{ + "asset": { + "version": "1.0", + "tilesetVersion": "1.2.3" + }, + "extras": { + "name": "Sample Tileset" + }, + "schema": { + "id": "foo", + "name": "Foo", + "description": "A description of Foo.", + "version": "1.0.0", + "classes": { + "someClass": { + "name": "Some Class!", + "description": "A description of Some Class.", + "properties": { + "someProperty": { + "name": "Some Property", + "description": "A description of Some Property.", + "type": "STRING", + "semantic": "GREAT" + } + } + } + } + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 85.41026367992163 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "ADD", + "content": { + "uri": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "uri": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "uri": "lr.b3dm" + }, + "extras": { + "id": "Special Tile" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "uri": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "uri": "ul.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 35, + "content": { + "uri": "external-tileset.json" + } + } + ] + } +} diff --git a/Cesium3DTilesSelection/test/data/WithMetadata/ul.b3dm b/Cesium3DTilesSelection/test/data/WithMetadata/ul.b3dm new file mode 100644 index 000000000..b7a4c1622 Binary files /dev/null and b/Cesium3DTilesSelection/test/data/WithMetadata/ul.b3dm differ diff --git a/Cesium3DTilesSelection/test/data/WithMetadata/ur.b3dm b/Cesium3DTilesSelection/test/data/WithMetadata/ur.b3dm new file mode 100644 index 000000000..9ae74c7b0 Binary files /dev/null and b/Cesium3DTilesSelection/test/data/WithMetadata/ur.b3dm differ diff --git a/CesiumNativeTests/CMakeLists.txt b/CesiumNativeTests/CMakeLists.txt index e9c65c68f..89604034c 100644 --- a/CesiumNativeTests/CMakeLists.txt +++ b/CesiumNativeTests/CMakeLists.txt @@ -4,6 +4,7 @@ configure_cesium_library(cesium-native-tests) # Add tests here, ensure they define the TEST_SOURCES / TEST_HEADERS # properties. set(cesium_native_targets + Cesium3DTiles Cesium3DTilesReader Cesium3DTilesWriter Cesium3DTilesSelection diff --git a/CesiumUtility/include/CesiumUtility/JsonValue.h b/CesiumUtility/include/CesiumUtility/JsonValue.h index 47773ed14..26c305e51 100644 --- a/CesiumUtility/include/CesiumUtility/JsonValue.h +++ b/CesiumUtility/include/CesiumUtility/JsonValue.h @@ -421,6 +421,17 @@ class CESIUMUTILITY_API JsonValue final { return std::get(this->value); } + /** + * @brief Gets an array of strings from the value. + * + * @param defaultString The default string to include in the array for an + * element that is not a string. + * @return The array of strings, or an empty array if this value is not an + * array at all. + */ + [[nodiscard]] std::vector + getArrayOfStrings(const std::string& defaultString) const; + /** * @brief Gets the bool from the value. * @return The bool. diff --git a/CesiumUtility/src/JsonValue.cpp b/CesiumUtility/src/JsonValue.cpp index 6c0c8515c..2323e632c 100644 --- a/CesiumUtility/src/JsonValue.cpp +++ b/CesiumUtility/src/JsonValue.cpp @@ -1,5 +1,7 @@ #include "CesiumUtility/JsonValue.h" +#include + namespace CesiumUtility { const JsonValue* JsonValue::getValuePtrForKey(const std::string& key) const { @@ -31,3 +33,19 @@ JsonValue* JsonValue::getValuePtrForKey(const std::string& key) { } } // namespace CesiumUtility +std::vector CesiumUtility::JsonValue::getArrayOfStrings( + const std::string& defaultString) const { + if (!this->isArray()) + return std::vector(); + + const JsonValue::Array& array = this->getArray(); + std::vector result(array.size()); + std::transform( + array.begin(), + array.end(), + result.begin(), + [&defaultString](const JsonValue& arrayValue) { + return arrayValue.getStringOrDefault(defaultString); + }); + return result; +}