Merge from main

This commit is contained in:
Ashley Rogers 2024-11-21 15:48:02 -05:00
commit 3832758a15
26 changed files with 513 additions and 276 deletions

View File

@ -12,10 +12,15 @@
- Added support for `EXT_accessor_additional_types` in `AccessorView`.
- Added `EllipsoidTilesetLoader` that will generate a tileset by tesselating the surface of an ellipsoid, producing a simple globe tileset without any terrain features.
- The `schemaUri` property in the `EXT_structural_metadata` glTF extension is now supported, allowing structural metadata schemas to be loaded from URIs rather than being embedded in the glTF itself.
- Added `getHeightSampler` method to `TilesetContentLoader`, allowing loaders to optionally provide a custom, more efficient means of querying heights using the `ITilesetHeightSampler` interface.
##### Fixes :wrench:
- Updated the CMake install process to install the vcpkg-built Debug binaries in Debug builds. Previously the Release binaries were installed instead.
- Fixed a crash that would occur for raster overlays attempting to dereference a null `CreditSystem`.
- Fixed a bug where an empty `extensions` object would get written if an `ExtensibleObject` only had unregistered extensions.
- Tightened the tolerance of `IntersectionTests::rayTriangleParametric`, allowing it to find intersections with smaller triangles.
- Fixed a bug that could cause `GltfUtilities::intersectRayGltfModel` to crash when the model contains a primitive whose position accessor does not have min/max values.
### v0.41.0 - 2024-11-01

View File

@ -1,5 +1,7 @@
#pragma once
#include "ITilesetHeightSampler.h"
#include <Cesium3DTilesSelection/Tileset.h>
#include <CesiumGeometry/QuadtreeTilingScheme.h>
@ -8,7 +10,8 @@ namespace Cesium3DTilesSelection {
* @brief A loader that will generate a tileset by tesselating the surface of an
* ellipsoid, producing a simple globe tileset without any terrain features.
*/
class EllipsoidTilesetLoader : public TilesetContentLoader {
class EllipsoidTilesetLoader : public TilesetContentLoader,
public ITilesetHeightSampler {
public:
/**
* @brief Constructs a new instance.
@ -35,6 +38,12 @@ public:
const CesiumGeospatial::Ellipsoid& ellipsoid
CESIUM_DEFAULT_ELLIPSOID) override;
ITilesetHeightSampler* getHeightSampler() override;
CesiumAsync::Future<SampleHeightResult> sampleHeights(
const CesiumAsync::AsyncSystem& asyncSystem,
std::vector<CesiumGeospatial::Cartographic>&& positions) override;
private:
struct Geometry {
std::vector<uint16_t> indices;

View File

@ -0,0 +1,36 @@
#pragma once
#include "Library.h"
#include "SampleHeightResult.h"
#include <CesiumAsync/Future.h>
#include <CesiumGeospatial/Cartographic.h>
#include <vector>
namespace CesiumAsync {
class AsyncSystem;
}
namespace Cesium3DTilesSelection {
/**
* @brief An interface to query heights from a tileset that can do so
* efficiently without necessarily downloading individual tiles.
*/
class CESIUM3DTILESSELECTION_API ITilesetHeightSampler {
public:
/**
* @brief Queries the heights at a list of locations.
*
* @param asyncSystem The async system used to do work in threads.
* @param positions The positions at which to query heights. The height field
* of each {@link Cartographic} is ignored.
* @return A future that will be resolved when the heights have been queried.
*/
virtual CesiumAsync::Future<SampleHeightResult> sampleHeights(
const CesiumAsync::AsyncSystem& asyncSystem,
std::vector<CesiumGeospatial::Cartographic>&& positions) = 0;
};
} // namespace Cesium3DTilesSelection

View File

@ -6,6 +6,7 @@
#include "TileLoadResult.h"
#include "TilesetOptions.h"
#include <Cesium3DTilesSelection/SampleHeightResult.h>
#include <CesiumAsync/AsyncSystem.h>
#include <CesiumAsync/Future.h>
#include <CesiumAsync/IAssetAccessor.h>
@ -23,6 +24,7 @@
namespace Cesium3DTilesSelection {
class Tile;
class ITilesetHeightSampler;
/**
* @brief Store the parameters that are needed to load a tile
@ -145,5 +147,24 @@ public:
const Tile& tile,
const CesiumGeospatial::Ellipsoid& ellipsoid
CESIUM_DEFAULT_ELLIPSOID) = 0;
/**
* @brief Gets an interface that can be used to efficiently query heights from
* this tileset.
*
* Some loaders may be able to query heights very efficiently by using a web
* service or by using an analytical model, e.g., when the "terrain" is a
* simple ellipsoid.
*
* For loaders that have no particular way to query heights, this method will
* return `nullptr`, signaling that heights should be computed by downloading
* and sampling individual tiles.
*
* @return The interface that can be used to efficiently query heights from
* this loader, or `nullptr` if this loader has no particular way to do that.
* The returned instance must have a lifetime that is at least as long as the
* loader itself.
*/
virtual ITilesetHeightSampler* getHeightSampler() { return nullptr; }
};
} // namespace Cesium3DTilesSelection

View File

@ -69,7 +69,9 @@ TileChildrenResult EllipsoidTilesetLoader::createTileChildren(
const QuadtreeTileID* pParentID =
std::get_if<QuadtreeTileID>(&tile.getTileID());
if (pParentID) {
// Due to the use of uint32_t for QuadtreeTileID X and Y, we can only support
// through level 30.
if (pParentID && pParentID->level < 30) {
std::vector<Tile> children;
QuadtreeChildren childIDs =
ImplicitTilingUtilities::getChildren(*pParentID);
@ -87,6 +89,25 @@ TileChildrenResult EllipsoidTilesetLoader::createTileChildren(
return TileChildrenResult{{}, TileLoadResultState::Failed};
}
ITilesetHeightSampler* EllipsoidTilesetLoader::getHeightSampler() {
return this;
}
CesiumAsync::Future<SampleHeightResult> EllipsoidTilesetLoader::sampleHeights(
const CesiumAsync::AsyncSystem& asyncSystem,
std::vector<CesiumGeospatial::Cartographic>&& positions) {
SampleHeightResult result;
result.positions = std::move(positions);
result.sampleSuccess.resize(result.positions.size(), true);
for (Cartographic& position : result.positions) {
position.height = 0.0;
}
return asyncSystem.createResolvedFuture(std::move(result));
}
void EllipsoidTilesetLoader::createChildTile(
const Tile& parent,
std::vector<Tile>& children,
@ -120,7 +141,9 @@ EllipsoidTilesetLoader::Geometry
EllipsoidTilesetLoader::createGeometry(const Tile& tile) const {
static constexpr uint16_t resolution = 24;
std::vector<uint16_t> indices(6 * (resolution - 1) * (resolution - 1));
std::vector<uint16_t> indices;
indices.reserve(6 * (resolution - 1) * (resolution - 1));
std::vector<glm::vec3> vertices(resolution * resolution);
std::vector<glm::vec3> normals(vertices.size());

View File

@ -391,6 +391,7 @@ Tileset::updateView(const std::vector<ViewState>& frustums, float deltaTime) {
}
TilesetHeightRequest::processHeightRequests(
this->getAsyncSystem(),
*this->_pTilesetContentManager,
this->_options,
this->_loadedTiles,

View File

@ -3,6 +3,7 @@
#include "TileUtilities.h"
#include "TilesetContentManager.h"
#include <Cesium3DTilesSelection/ITilesetHeightSampler.h>
#include <Cesium3DTilesSelection/SampleHeightResult.h>
#include <CesiumGeometry/IntersectionTests.h>
#include <CesiumGeospatial/GlobeRectangle.h>
@ -200,6 +201,7 @@ void TilesetHeightQuery::findCandidateTiles(
}
/*static*/ void TilesetHeightRequest::processHeightRequests(
const AsyncSystem& asyncSystem,
TilesetContentManager& contentManager,
const TilesetOptions& options,
Tile::LoadedLinkedList& loadedTiles,
@ -214,6 +216,7 @@ void TilesetHeightQuery::findCandidateTiles(
for (auto it = heightRequests.begin(); it != heightRequests.end();) {
TilesetHeightRequest& request = *it;
if (!request.tryCompleteHeightRequest(
asyncSystem,
contentManager,
options,
loadedTiles,
@ -249,10 +252,35 @@ void Cesium3DTilesSelection::TilesetHeightRequest::failHeightRequests(
}
bool TilesetHeightRequest::tryCompleteHeightRequest(
const AsyncSystem& asyncSystem,
TilesetContentManager& contentManager,
const TilesetOptions& options,
Tile::LoadedLinkedList& loadedTiles,
std::set<Tile*>& tileLoadSet) {
// If this TilesetContentLoader supports direct height queries, use that
// instead of downloading tiles.
if (contentManager.getRootTile() &&
contentManager.getRootTile()->getLoader()) {
ITilesetHeightSampler* pSampler =
contentManager.getRootTile()->getLoader()->getHeightSampler();
if (pSampler) {
std::vector<Cartographic> positions;
positions.reserve(this->queries.size());
for (TilesetHeightQuery& query : this->queries) {
positions.emplace_back(query.inputPosition);
}
pSampler->sampleHeights(asyncSystem, std::move(positions))
.thenImmediately(
[promise = this->promise](SampleHeightResult&& result) {
promise.resolve(std::move(result));
});
return true;
}
}
// No direct height query possible, so download and sample tiles.
bool tileStillNeedsLoading = false;
std::vector<std::string> warnings;
for (TilesetHeightQuery& query : this->queries) {

View File

@ -126,6 +126,7 @@ struct TilesetHeightRequest {
* @brief Process a given list of height requests. This is called by the {@link Tileset}
* in every call to {@link Tileset::updateView}.
*
* @param asyncSystem The async system used to do work in threads.
* @param contentManager The content manager.
* @param options Options associated with the tileset.
* @param loadedTiles The linked list of loaded tiles, used to ensure that
@ -137,6 +138,7 @@ struct TilesetHeightRequest {
* height requests can complete are added to this vector.
*/
static void processHeightRequests(
const CesiumAsync::AsyncSystem& asyncSystem,
TilesetContentManager& contentManager,
const TilesetOptions& options,
Tile::LoadedLinkedList& loadedTiles,
@ -160,6 +162,7 @@ struct TilesetHeightRequest {
* @brief Tries to complete this height request. Returns false if further data
* still needs to be loaded and thus the request cannot yet complete.
*
* @param asyncSystem The async system used to do work in threads.
* @param contentManager The content manager.
* @param options Options associated with the tileset.
* @param loadedTiles The linked list of loaded tiles, used to ensure that
@ -169,6 +172,7 @@ struct TilesetHeightRequest {
* can complete.
*/
bool tryCompleteHeightRequest(
const CesiumAsync::AsyncSystem& asyncSystem,
TilesetContentManager& contentManager,
const TilesetOptions& options,
Tile::LoadedLinkedList& loadedTiles,

View File

@ -1,4 +1,5 @@
#include <Cesium3DTilesContent/registerAllTileContentTypes.h>
#include <Cesium3DTilesSelection/EllipsoidTilesetLoader.h>
#include <Cesium3DTilesSelection/Tileset.h>
#include <CesiumGeospatial/Cartographic.h>
#include <CesiumNativeTests/FileAccessor.h>
@ -232,4 +233,38 @@ TEST_CASE("Tileset height queries") {
CHECK(!results.sampleSuccess[0]);
CHECK(results.warnings[0].find("failed to load") != std::string::npos);
}
SECTION("ellipsoid tileset") {
std::unique_ptr<Tileset> pTileset =
EllipsoidTilesetLoader::createTileset(externals);
Future<SampleHeightResult> future = pTileset->sampleHeightMostDetailed(
{Cartographic::fromDegrees(-75.612559, 40.042183, 1.0)});
while (!future.isReady()) {
pTileset->updateView({});
}
SampleHeightResult results = future.waitInMainThread();
REQUIRE(results.warnings.size() == 0);
REQUIRE(results.positions.size() == 1);
REQUIRE(results.sampleSuccess.size() == 1);
CHECK(results.sampleSuccess[0]);
CHECK(Math::equalsEpsilon(
results.positions[0].longitude,
Math::degreesToRadians(-75.612559),
0.0,
Math::Epsilon4));
CHECK(Math::equalsEpsilon(
results.positions[0].latitude,
Math::degreesToRadians(40.042183),
0.0,
Math::Epsilon4));
CHECK(Math::equalsEpsilon(
results.positions[0].height,
0.0,
0.0,
Math::Epsilon4));
}
}

View File

@ -285,7 +285,7 @@ void writeExtensibleObject(
CesiumJsonWriter::JsonWriter& jsonWriter,
const CesiumJsonWriter::ExtensionWriterContext& context) {
if (!obj.extensions.empty()) {
if (hasRegisteredExtensions(obj, jsonWriter, context)) {
jsonWriter.Key("extensions");
writeJsonExtensions(obj, jsonWriter, context);
}

View File

@ -141,7 +141,7 @@ std::optional<double> IntersectionTests::rayTriangleParametric(
glm::dvec3 p = glm::cross(direction, edge1);
double det = glm::dot(edge0, p);
if (cullBackFaces) {
if (det < Math::Epsilon6)
if (det < Math::Epsilon8)
return std::nullopt;
glm::dvec3 tvec = origin - p0;
@ -158,7 +158,7 @@ std::optional<double> IntersectionTests::rayTriangleParametric(
} else {
if (glm::abs(det) < Math::Epsilon6)
if (glm::abs(det) < Math::Epsilon8)
return std::nullopt;
double invDet = 1.0 / det;

View File

@ -1265,22 +1265,25 @@ std::optional<glm::dvec3> intersectRayScenePrimitive(
glm::dmat4x4 worldToPrimitive = glm::inverse(primitiveToWorld);
CesiumGeometry::Ray transformedRay = ray.transform(worldToPrimitive);
// Ignore primitive if ray doesn't intersect bounding box
// Ignore primitive if we have an AABB from the accessor min/max and the ray
// doesn't intersect it.
const std::vector<double>& min = positionAccessor.min;
const std::vector<double>& max = positionAccessor.max;
std::optional<double> boxT =
CesiumGeometry::IntersectionTests::rayAABBParametric(
transformedRay,
CesiumGeometry::AxisAlignedBox(
min[0],
min[1],
min[2],
max[0],
max[1],
max[2]));
if (!boxT)
return std::optional<glm::dvec3>();
if (min.size() >= 3 && max.size() >= 3) {
std::optional<double> boxT =
CesiumGeometry::IntersectionTests::rayAABBParametric(
transformedRay,
CesiumGeometry::AxisAlignedBox(
min[0],
min[1],
min[2],
max[0],
max[1],
max[2]));
if (!boxT)
return std::optional<glm::dvec3>();
}
double tClosest = -1.0;

View File

@ -526,7 +526,7 @@ void writeExtensibleObject(
CesiumJsonWriter::JsonWriter& jsonWriter,
const CesiumJsonWriter::ExtensionWriterContext& context) {
if (!obj.extensions.empty()) {
if (hasRegisteredExtensions(obj, jsonWriter, context)) {
jsonWriter.Key("extensions");
writeJsonExtensions(obj, jsonWriter, context);
}

View File

@ -20,19 +20,38 @@ void writeJsonExtensions(
item.first,
item.second,
TExtended::TypeName);
if (!handler) {
if (context.getExtensionState(item.first) != ExtensionState::Disabled) {
jsonWriter.emplaceWarning(fmt::format(
"Encountered unregistered extension {}. This extension will be "
"ignored. To silence this warning, disable the extension with "
"ExtensionWriterContext::setExtensionState.",
item.first));
}
continue;
if (handler) {
jsonWriter.Key(item.first);
handler(item.second, jsonWriter, context);
}
jsonWriter.Key(item.first);
handler(item.second, jsonWriter, context);
}
jsonWriter.EndObject();
}
template <typename TExtended>
bool hasRegisteredExtensions(
const TExtended& obj,
JsonWriter& jsonWriter,
const ExtensionWriterContext& context) {
bool hasRegisteredExtensions = false;
for (const auto& item : obj.extensions) {
auto handler = context.createExtensionHandler(
item.first,
item.second,
TExtended::TypeName);
if (handler) {
hasRegisteredExtensions = true;
} else if (
context.getExtensionState(item.first) != ExtensionState::Disabled) {
jsonWriter.emplaceWarning(fmt::format(
"Encountered unregistered extension {}. This extension will be "
"ignored. To silence this warning, disable the extension with "
"ExtensionWriterContext::setExtensionState.",
item.first));
}
}
return hasRegisteredExtensions;
}
} // namespace CesiumJsonWriter

View File

@ -143,7 +143,7 @@ struct CESIUMQUANTIZEDMESHTERRAIN_API LayerSpec
return accum;
}
private:
protected:
/**
* @brief This class is not meant to be instantiated directly. Use {@link Layer} instead.
*/

View File

@ -147,7 +147,7 @@ void writeExtensibleObject(
CesiumJsonWriter::JsonWriter& jsonWriter,
const CesiumJsonWriter::ExtensionWriterContext& context) {
if (!obj.extensions.empty()) {
if (hasRegisteredExtensions(obj, jsonWriter, context)) {
jsonWriter.Key("extensions");
writeJsonExtensions(obj, jsonWriter, context);
}

View File

@ -6,9 +6,12 @@
#include <CesiumAsync/AsyncSystem.h>
#include <CesiumAsync/IAssetAccessor.h>
#include <CesiumAsync/SharedAssetDepot.h>
#include <CesiumGeometry/QuadtreeTileID.h>
#include <CesiumGeometry/QuadtreeTilingScheme.h>
#include <CesiumUtility/CreditSystem.h>
#include <CesiumUtility/Result.h>
#include <CesiumUtility/SharedAsset.h>
#include <list>
#include <memory>
@ -111,12 +114,26 @@ private:
virtual CesiumAsync::Future<LoadedRasterOverlayImage>
loadTileImage(RasterOverlayTile& overlayTile) override final;
struct LoadedQuadtreeImage {
struct LoadedQuadtreeImage
: public CesiumUtility::SharedAsset<LoadedQuadtreeImage> {
LoadedQuadtreeImage(
const std::shared_ptr<LoadedRasterOverlayImage>& pLoaded_,
const std::optional<CesiumGeometry::Rectangle>& subset_)
: pLoaded(pLoaded_), subset(subset_) {}
std::shared_ptr<LoadedRasterOverlayImage> pLoaded = nullptr;
std::optional<CesiumGeometry::Rectangle> subset = std::nullopt;
int64_t getSizeBytes() const {
int64_t accum = 0;
accum += sizeof(LoadedQuadtreeImage);
if (pLoaded) {
accum += pLoaded->getSizeBytes();
}
return accum;
}
};
CesiumAsync::SharedFuture<LoadedQuadtreeImage>
CesiumAsync::SharedFuture<CesiumUtility::ResultPointer<LoadedQuadtreeImage>>
getQuadtreeTile(const CesiumGeometry::QuadtreeTileID& tileID);
/**
@ -129,13 +146,12 @@ private:
* data that is required to cover the rectangle with the given geometric
* error.
*/
std::vector<CesiumAsync::SharedFuture<LoadedQuadtreeImage>>
std::vector<CesiumAsync::SharedFuture<
CesiumUtility::ResultPointer<LoadedQuadtreeImage>>>
mapRasterTilesToGeometryTile(
const CesiumGeometry::Rectangle& geometryRectangle,
const glm::dvec2 targetScreenPixels);
void unloadCachedTiles();
struct CombinedImageMeasurements {
CesiumGeometry::Rectangle rectangle;
int32_t widthPixels;
@ -146,12 +162,13 @@ private:
static CombinedImageMeasurements measureCombinedImage(
const CesiumGeometry::Rectangle& targetRectangle,
const std::vector<LoadedQuadtreeImage>& images);
const std::vector<CesiumUtility::ResultPointer<LoadedQuadtreeImage>>&
images);
static LoadedRasterOverlayImage combineImages(
const CesiumGeometry::Rectangle& targetRectangle,
const CesiumGeospatial::Projection& projection,
std::vector<LoadedQuadtreeImage>&& images);
std::vector<CesiumUtility::ResultPointer<LoadedQuadtreeImage>>&& images);
uint32_t _minimumLevel;
uint32_t _maximumLevel;
@ -159,22 +176,9 @@ private:
uint32_t _imageHeight;
CesiumGeometry::QuadtreeTilingScheme _tilingScheme;
struct CacheEntry {
CesiumGeometry::QuadtreeTileID tileID;
CesiumAsync::SharedFuture<LoadedQuadtreeImage> future;
};
// Tiles at the beginning of this list are the least recently used (oldest),
// while the tiles at the end are most recently used (newest).
using TileLeastRecentlyUsedList = std::list<CacheEntry>;
TileLeastRecentlyUsedList _tilesOldToRecent;
// Allows a Future to be looked up by quadtree tile ID.
std::unordered_map<
CesiumGeometry::QuadtreeTileID,
TileLeastRecentlyUsedList::iterator>
_tileLookup;
std::atomic<int64_t> _cachedBytes;
CesiumUtility::IntrusivePointer<CesiumAsync::SharedAssetDepot<
LoadedQuadtreeImage,
CesiumGeometry::QuadtreeTileID>>
_pTileDepot;
};
} // namespace CesiumRasterOverlays

View File

@ -61,6 +61,19 @@ struct CESIUMRASTEROVERLAYS_API LoadedRasterOverlayImage {
* the bounds of this image.
*/
bool moreDetailAvailable = false;
/**
* @brief Returns the size of this `LoadedRasterOverlayImage` in bytes.
*/
size_t getSizeBytes() const {
int64_t accum = 0;
accum += sizeof(LoadedRasterOverlayImage);
accum += this->credits.capacity() * sizeof(CesiumUtility::Credit);
if (this->pImage) {
accum += this->pImage->getSizeBytes();
}
return accum;
}
};
/**

View File

@ -266,13 +266,18 @@ namespace {
* @param pResource The JSON value for the resource
* @param pCreditSystem The `CreditSystem` that will create one credit for
* each attribution
* @return The `CreditAndCoverageAreas` objects that have been parsed
* @return The `CreditAndCoverageAreas` objects that have been parsed, or an
* empty vector if pCreditSystem is nullptr.
*/
std::vector<CreditAndCoverageAreas> collectCredits(
const rapidjson::Value* pResource,
const std::shared_ptr<CreditSystem>& pCreditSystem,
bool showCreditsOnScreen) {
std::vector<CreditAndCoverageAreas> credits;
if (!pCreditSystem) {
return credits;
}
const auto attributionsIt = pResource->FindMember("imageryProviders");
if (attributionsIt != pResource->MemberEnd() &&
attributionsIt->value.IsArray()) {

View File

@ -52,10 +52,89 @@ QuadtreeRasterOverlayTileProvider::QuadtreeRasterOverlayTileProvider(
_maximumLevel(maximumLevel),
_imageWidth(imageWidth),
_imageHeight(imageHeight),
_tilingScheme(tilingScheme),
_tilesOldToRecent(),
_tileLookup(),
_cachedBytes(0) {}
_tilingScheme(tilingScheme) {
auto loadParentTile = [this](const QuadtreeTileID& key)
-> Future<ResultPointer<LoadedQuadtreeImage>> {
const Rectangle rectangle = this->getTilingScheme().tileToRectangle(key);
const QuadtreeTileID parentID(key.level - 1, key.x >> 1, key.y >> 1);
return this->getQuadtreeTile(parentID).thenImmediately(
[rectangle](const ResultPointer<LoadedQuadtreeImage>& loaded) {
ResultPointer<LoadedQuadtreeImage> result(loaded.errors);
if (loaded.pValue) {
result.pValue.emplace(*loaded.pValue);
result.pValue->subset = rectangle;
}
return result;
});
};
this->_pTileDepot.emplace(std::function(
[pThis = this, loadParentTile](
const AsyncSystem& asyncSystem,
[[maybe_unused]] const std::shared_ptr<IAssetAccessor>&
pAssetAccessor,
const QuadtreeTileID& key)
-> Future<ResultPointer<LoadedQuadtreeImage>> {
return pThis->loadQuadtreeTileImage(key)
.catchImmediately([](std::exception&& e) {
// Turn an exception into an error.
LoadedRasterOverlayImage result;
result.errorList.emplaceError(e.what());
return result;
})
.thenImmediately(
[loadParentTile,
key,
currentLevel = key.level,
minimumLevel = pThis->getMinimumLevel(),
asyncSystem](LoadedRasterOverlayImage&& loaded)
-> Future<ResultPointer<LoadedQuadtreeImage>> {
if (loaded.pImage && !loaded.errorList.hasErrors() &&
loaded.pImage->width > 0 && loaded.pImage->height > 0) {
#if SHOW_TILE_BOUNDARIES
// Highlight the edges in red to show tile boundaries.
std::span<uint32_t> pixels =
reintepretCastSpan<uint32_t, std::byte>(
loaded.image->pixelData);
for (int32_t j = 0; j < loaded.pImage->height; ++j) {
for (int32_t i = 0; i < loaded.pImage->width; ++i) {
if (i == 0 || j == 0 || i == loaded.pImage->width - 1 ||
j == loaded.pImage->height - 1) {
pixels[j * loaded.pImage->width + i] = 0xFF0000FF;
}
}
}
#endif
IntrusivePointer<LoadedQuadtreeImage> pLoadedImage;
pLoadedImage.emplace(
std::make_shared<LoadedRasterOverlayImage>(
std::move(loaded)),
std::nullopt);
return asyncSystem.createResolvedFuture(
ResultPointer<LoadedQuadtreeImage>(pLoadedImage));
}
// Tile failed to load, try loading the parent tile instead.
// We can only initiate a new tile request from the main
// thread, though.
if (currentLevel > minimumLevel) {
return asyncSystem.runInMainThread([key, loadParentTile]() {
return loadParentTile(key);
});
} else {
// No parent available, so return the original failed
// result.
IntrusivePointer<LoadedQuadtreeImage> pLoadedImage;
pLoadedImage.emplace(
std::make_shared<LoadedRasterOverlayImage>(
std::move(loaded)),
std::nullopt);
return asyncSystem.createResolvedFuture(
ResultPointer<LoadedQuadtreeImage>(pLoadedImage));
}
});
}));
}
uint32_t QuadtreeRasterOverlayTileProvider::computeLevelFromTargetScreenPixels(
const CesiumGeometry::Rectangle& rectangle,
@ -97,11 +176,12 @@ uint32_t QuadtreeRasterOverlayTileProvider::computeLevelFromTargetScreenPixels(
}
std::vector<CesiumAsync::SharedFuture<
QuadtreeRasterOverlayTileProvider::LoadedQuadtreeImage>>
ResultPointer<QuadtreeRasterOverlayTileProvider::LoadedQuadtreeImage>>>
QuadtreeRasterOverlayTileProvider::mapRasterTilesToGeometryTile(
const CesiumGeometry::Rectangle& geometryRectangle,
const glm::dvec2 targetScreenPixels) {
std::vector<CesiumAsync::SharedFuture<LoadedQuadtreeImage>> result;
std::vector<CesiumAsync::SharedFuture<ResultPointer<LoadedQuadtreeImage>>>
result;
const QuadtreeTilingScheme& imageryTilingScheme = this->getTilingScheme();
@ -271,7 +351,7 @@ QuadtreeRasterOverlayTileProvider::mapRasterTilesToGeometryTile(
continue;
}
CesiumAsync::SharedFuture<LoadedQuadtreeImage> pTile =
CesiumAsync::SharedFuture<ResultPointer<LoadedQuadtreeImage>> pTile =
this->getQuadtreeTile(QuadtreeTileID(level, i, j));
result.emplace_back(std::move(pTile));
}
@ -281,101 +361,13 @@ QuadtreeRasterOverlayTileProvider::mapRasterTilesToGeometryTile(
}
CesiumAsync::SharedFuture<
QuadtreeRasterOverlayTileProvider::LoadedQuadtreeImage>
ResultPointer<QuadtreeRasterOverlayTileProvider::LoadedQuadtreeImage>>
QuadtreeRasterOverlayTileProvider::getQuadtreeTile(
const CesiumGeometry::QuadtreeTileID& tileID) {
auto lookupIt = this->_tileLookup.find(tileID);
if (lookupIt != this->_tileLookup.end()) {
auto& cacheIt = lookupIt->second;
// Move this entry to the end, indicating it's most recently used.
this->_tilesOldToRecent.splice(
this->_tilesOldToRecent.end(),
this->_tilesOldToRecent,
cacheIt);
return cacheIt->future;
}
// We create this lambda here instead of where it's used below so that we
// don't need to pass `this` through a thenImmediately lambda, which would
// create the possibility of accidentally using this pointer to a
// non-thread-safe object from another thread and creating a (potentially very
// subtle) race condition.
auto loadParentTile = [tileID, this]() {
const Rectangle rectangle = this->getTilingScheme().tileToRectangle(tileID);
const QuadtreeTileID parentID(
tileID.level - 1,
tileID.x >> 1,
tileID.y >> 1);
return this->getQuadtreeTile(parentID).thenImmediately(
[rectangle](const LoadedQuadtreeImage& loaded) {
return LoadedQuadtreeImage{loaded.pLoaded, rectangle};
});
};
Future<LoadedQuadtreeImage> future =
this->loadQuadtreeTileImage(tileID)
.catchImmediately([](std::exception&& e) {
// Turn an exception into an error.
LoadedRasterOverlayImage result;
result.errorList.emplaceError(e.what());
return result;
})
.thenImmediately([&cachedBytes = this->_cachedBytes,
currentLevel = tileID.level,
minimumLevel = this->getMinimumLevel(),
asyncSystem = this->getAsyncSystem(),
loadParentTile = std::move(loadParentTile)](
LoadedRasterOverlayImage&& loaded) {
if (loaded.pImage && !loaded.errorList.hasErrors() &&
loaded.pImage->width > 0 && loaded.pImage->height > 0) {
// Successfully loaded, continue.
cachedBytes += int64_t(loaded.pImage->pixelData.size());
#if SHOW_TILE_BOUNDARIES
// Highlight the edges in red to show tile boundaries.
std::span<uint32_t> pixels =
reintepretCastSpan<uint32_t, std::byte>(
loaded.image->pixelData);
for (int32_t j = 0; j < loaded.pImage->height; ++j) {
for (int32_t i = 0; i < loaded.pImage->width; ++i) {
if (i == 0 || j == 0 || i == loaded.pImage->width - 1 ||
j == loaded.pImage->height - 1) {
pixels[j * loaded.pImage->width + i] = 0xFF0000FF;
}
}
}
#endif
return asyncSystem.createResolvedFuture(LoadedQuadtreeImage{
std::make_shared<LoadedRasterOverlayImage>(std::move(loaded)),
std::nullopt});
}
// Tile failed to load, try loading the parent tile instead.
// We can only initiate a new tile request from the main thread,
// though.
if (currentLevel > minimumLevel) {
return asyncSystem.runInMainThread(loadParentTile);
} else {
// No parent available, so return the original failed result.
return asyncSystem.createResolvedFuture(LoadedQuadtreeImage{
std::make_shared<LoadedRasterOverlayImage>(std::move(loaded)),
std::nullopt});
}
});
auto newIt = this->_tilesOldToRecent.emplace(
this->_tilesOldToRecent.end(),
CacheEntry{tileID, std::move(future).share()});
this->_tileLookup[tileID] = newIt;
SharedFuture<LoadedQuadtreeImage> result = newIt->future;
this->unloadCachedTiles();
return result;
return this->_pTileDepot->getOrCreate(
this->getAsyncSystem(),
this->getAssetAccessor(),
tileID);
}
namespace {
@ -445,95 +437,60 @@ QuadtreeRasterOverlayTileProvider::loadTileImage(
RasterOverlayTile& overlayTile) {
// Figure out which quadtree level we need, and which tiles from that level.
// Load each needed tile (or pull it from cache).
std::vector<CesiumAsync::SharedFuture<LoadedQuadtreeImage>> tiles =
this->mapRasterTilesToGeometryTile(
std::vector<CesiumAsync::SharedFuture<ResultPointer<LoadedQuadtreeImage>>>
tiles = this->mapRasterTilesToGeometryTile(
overlayTile.getRectangle(),
overlayTile.getTargetScreenPixels());
return this->getAsyncSystem()
.all(std::move(tiles))
.thenInWorkerThread([projection = this->getProjection(),
rectangle = overlayTile.getRectangle()](
std::vector<LoadedQuadtreeImage>&& images) {
// This set of images is only "useful" if at least one actually has
// image data, and that image data is _not_ from an ancestor. We can
// identify ancestor images because they have a `subset`.
const bool haveAnyUsefulImageData = std::any_of(
images.begin(),
images.end(),
[](const LoadedQuadtreeImage& image) {
return image.pLoaded->pImage && !image.subset.has_value();
});
.thenInWorkerThread(
[projection = this->getProjection(),
rectangle = overlayTile.getRectangle()](
std::vector<ResultPointer<LoadedQuadtreeImage>>&& images) {
// This set of images is only "useful" if at least one actually has
// image data, and that image data is _not_ from an ancestor. We can
// identify ancestor images because they have a `subset`.
const bool haveAnyUsefulImageData = std::any_of(
images.begin(),
images.end(),
[](const ResultPointer<LoadedQuadtreeImage>& image) {
return image.pValue && image.pValue->pLoaded->pImage &&
!image.pValue->subset.has_value();
});
if (!haveAnyUsefulImageData) {
// For non-useful sets of images, just return an empty image,
// signalling that the parent tile should be used instead.
// See https://github.com/CesiumGS/cesium-native/issues/316 for an
// edge case that is not yet handled. Be sure to pass through any
// errors and warnings.
ErrorList errors;
for (LoadedQuadtreeImage& image : images) {
if (image.pLoaded) {
errors.merge(image.pLoaded->errorList);
if (!haveAnyUsefulImageData) {
// For non-useful sets of images, just return an empty image,
// signalling that the parent tile should be used instead.
// See https://github.com/CesiumGS/cesium-native/issues/316 for an
// edge case that is not yet handled. Be sure to pass through any
// errors and warnings.
ErrorList errors;
for (ResultPointer<LoadedQuadtreeImage>& image : images) {
if (image.pValue->pLoaded) {
errors.merge(image.pValue->pLoaded->errorList);
}
errors.merge(image.errors);
}
return LoadedRasterOverlayImage{
new ImageAsset(),
Rectangle(),
{},
std::move(errors),
false};
}
}
return LoadedRasterOverlayImage{
new ImageAsset(),
Rectangle(),
{},
std::move(errors),
false};
}
return QuadtreeRasterOverlayTileProvider::combineImages(
rectangle,
projection,
std::move(images));
});
}
void QuadtreeRasterOverlayTileProvider::unloadCachedTiles() {
CESIUM_TRACE("QuadtreeRasterOverlayTileProvider::unloadCachedTiles");
const int64_t maxCacheBytes = this->getOwner().getOptions().subTileCacheBytes;
if (this->_cachedBytes <= maxCacheBytes) {
return;
}
auto it = this->_tilesOldToRecent.begin();
while (it != this->_tilesOldToRecent.end() &&
this->_cachedBytes > maxCacheBytes) {
const SharedFuture<LoadedQuadtreeImage>& future = it->future;
if (!future.isReady()) {
// Don't unload tiles that are still loading.
++it;
continue;
}
// Guaranteed not to block because isReady returned true.
const LoadedQuadtreeImage& image = future.wait();
std::shared_ptr<LoadedRasterOverlayImage> pImage = image.pLoaded;
this->_tileLookup.erase(it->tileID);
it = this->_tilesOldToRecent.erase(it);
// If this is the last use of this data, it will be freed when the shared
// pointer goes out of scope, so reduce the cachedBytes accordingly.
if (pImage.use_count() == 1) {
if (pImage->pImage) {
this->_cachedBytes -= int64_t(pImage->pImage->pixelData.size());
CESIUM_ASSERT(this->_cachedBytes >= 0);
}
}
}
return QuadtreeRasterOverlayTileProvider::combineImages(
rectangle,
projection,
std::move(images));
});
}
/*static*/ QuadtreeRasterOverlayTileProvider::CombinedImageMeasurements
QuadtreeRasterOverlayTileProvider::measureCombinedImage(
const Rectangle& targetRectangle,
const std::vector<LoadedQuadtreeImage>& images) {
const std::vector<ResultPointer<LoadedQuadtreeImage>>& images) {
// Find the image with the densest pixels, and use that to select the
// resolution of the target image.
@ -547,8 +504,11 @@ QuadtreeRasterOverlayTileProvider::measureCombinedImage(
double projectedHeightPerPixel = std::numeric_limits<double>::max();
int32_t channels = -1;
int32_t bytesPerChannel = -1;
for (const LoadedQuadtreeImage& image : images) {
const LoadedRasterOverlayImage& loaded = *image.pLoaded;
for (const ResultPointer<LoadedQuadtreeImage>& image : images) {
if (!image.pValue) {
continue;
}
const LoadedRasterOverlayImage& loaded = *image.pValue->pLoaded;
if (!loaded.pImage || loaded.pImage->width <= 0 ||
loaded.pImage->height <= 0) {
continue;
@ -567,15 +527,19 @@ QuadtreeRasterOverlayTileProvider::measureCombinedImage(
std::optional<Rectangle> combinedRectangle;
for (const LoadedQuadtreeImage& image : images) {
const LoadedRasterOverlayImage& loaded = *image.pLoaded;
for (const ResultPointer<LoadedQuadtreeImage>& image : images) {
if (!image.pValue) {
continue;
}
const LoadedRasterOverlayImage& loaded = *image.pValue->pLoaded;
if (!loaded.pImage || loaded.pImage->width <= 0 ||
loaded.pImage->height <= 0) {
continue;
}
// The portion of the source that we actually need to copy.
const Rectangle sourceSubset = image.subset.value_or(loaded.rectangle);
const Rectangle sourceSubset =
image.pValue->subset.value_or(loaded.rectangle);
// Find the bounds of the combined image.
// Intersect the loaded image's rectangle with the target rectangle.
@ -649,12 +613,13 @@ QuadtreeRasterOverlayTileProvider::measureCombinedImage(
QuadtreeRasterOverlayTileProvider::combineImages(
const Rectangle& targetRectangle,
const Projection& /* projection */,
std::vector<LoadedQuadtreeImage>&& images) {
std::vector<ResultPointer<LoadedQuadtreeImage>>&& images) {
ErrorList errors;
for (LoadedQuadtreeImage& image : images) {
if (image.pLoaded) {
errors.merge(std::move(image.pLoaded->errorList));
for (ResultPointer<LoadedQuadtreeImage>& image : images) {
if (image.pValue && image.pValue->pLoaded) {
errors.merge(std::move(image.pValue->pLoaded->errorList));
}
errors.merge(image.errors);
}
const CombinedImageMeasurements measurements =
@ -690,7 +655,10 @@ QuadtreeRasterOverlayTileProvider::combineImages(
target.width * target.height * target.channels * target.bytesPerChannel));
for (auto it = images.begin(); it != images.end(); ++it) {
const LoadedRasterOverlayImage& loaded = *it->pLoaded;
if (!it->pValue) {
continue;
}
const LoadedRasterOverlayImage& loaded = *it->pValue->pLoaded;
if (!loaded.pImage) {
continue;
}
@ -702,12 +670,15 @@ QuadtreeRasterOverlayTileProvider::combineImages(
result.rectangle,
*loaded.pImage,
loaded.rectangle,
it->subset);
it->pValue->subset);
}
size_t combinedCreditsCount = 0;
for (auto it = images.begin(); it != images.end(); ++it) {
const LoadedRasterOverlayImage& loaded = *it->pLoaded;
if (!it->pValue) {
continue;
}
const LoadedRasterOverlayImage& loaded = *it->pValue->pLoaded;
if (!loaded.pImage) {
continue;
}
@ -717,7 +688,10 @@ QuadtreeRasterOverlayTileProvider::combineImages(
result.credits.reserve(combinedCreditsCount);
for (auto it = images.begin(); it != images.end(); ++it) {
const LoadedRasterOverlayImage& loaded = *it->pLoaded;
if (!it->pValue) {
continue;
}
const LoadedRasterOverlayImage& loaded = *it->pValue->pLoaded;
if (!loaded.pImage) {
continue;
}

View File

@ -280,11 +280,12 @@ TileMapServiceRasterOverlay::createTileProvider(
pOwner = pOwner ? pOwner : this;
const std::optional<Credit> credit =
this->_options.credit ? std::make_optional(pCreditSystem->createCredit(
this->_options.credit.value(),
pOwner->getOptions().showCreditsOnScreen))
: std::nullopt;
std::optional<Credit> credit = std::nullopt;
if (pCreditSystem && this->_options.credit) {
credit = pCreditSystem->createCredit(
*this->_options.credit,
pOwner->getOptions().showCreditsOnScreen);
}
return getXmlDocument(asyncSystem, pAssetAccessor, xmlUrl, this->_headers)
.thenInMainThread(

View File

@ -270,10 +270,12 @@ WebMapServiceRasterOverlay::createTileProvider(
pOwner = pOwner ? pOwner : this;
const std::optional<Credit> credit =
this->_options.credit ? std::make_optional(pCreditSystem->createCredit(
this->_options.credit.value()))
: std::nullopt;
std::optional<Credit> credit = std::nullopt;
if (pCreditSystem && this->_options.credit) {
credit = pCreditSystem->createCredit(
*this->_options.credit,
pOwner->getOptions().showCreditsOnScreen);
}
return pAssetAccessor->get(asyncSystem, xmlUrlGetcapabilities, this->_headers)
.thenInMainThread(

View File

@ -193,11 +193,12 @@ WebMapTileServiceRasterOverlay::createTileProvider(
pOwner = pOwner ? pOwner : this;
const std::optional<Credit> credit =
this->_options.credit ? std::make_optional(pCreditSystem->createCredit(
this->_options.credit.value(),
pOwner->getOptions().showCreditsOnScreen))
: std::nullopt;
std::optional<Credit> credit = std::nullopt;
if (pCreditSystem && this->_options.credit) {
credit = pCreditSystem->createCredit(
*this->_options.credit,
pOwner->getOptions().showCreditsOnScreen);
}
bool hasError = false;
std::string errorMessage;

View File

@ -205,4 +205,56 @@ TEST_CASE("TileMapServiceRasterOverlay") {
REQUIRE(result);
}
SECTION("loads with credit") {
TileMapServiceRasterOverlayOptions options;
options.credit = "test credit";
IntrusivePointer<TileMapServiceRasterOverlay> pRasterOverlayWithCredit =
new TileMapServiceRasterOverlay("test", tmr, {}, options);
std::shared_ptr<CreditSystem> pCreditSystem =
std::make_shared<CreditSystem>();
RasterOverlay::CreateTileProviderResult result = waitForFuture(
asyncSystem,
pRasterOverlayWithCredit->createTileProvider(
asyncSystem,
pMockAssetAccessor,
pCreditSystem,
nullptr,
spdlog::default_logger(),
nullptr));
REQUIRE(result);
CesiumUtility::IntrusivePointer<RasterOverlayTileProvider> pTileProvider =
*result;
std::optional<Credit> maybeCredit = pTileProvider->getCredit();
REQUIRE(maybeCredit);
CHECK(pCreditSystem->getHtml(*maybeCredit) == "test credit");
}
SECTION("loads with credit and null credit system") {
TileMapServiceRasterOverlayOptions options;
options.credit = "test credit";
IntrusivePointer<TileMapServiceRasterOverlay> pRasterOverlayWithCredit =
new TileMapServiceRasterOverlay("test", tmr, {}, options);
RasterOverlay::CreateTileProviderResult result = waitForFuture(
asyncSystem,
pRasterOverlayWithCredit->createTileProvider(
asyncSystem,
pMockAssetAccessor,
nullptr,
nullptr,
spdlog::default_logger(),
nullptr));
REQUIRE(result);
CesiumUtility::IntrusivePointer<RasterOverlayTileProvider> pTileProvider =
*result;
CHECK(!pTileProvider->getCredit());
}
}

View File

@ -293,7 +293,8 @@ function generate(options, schema, writers) {
CesiumJsonReader::JsonReaderOptions _options;
};
} // namespace ${readerNamespace}`;
} // namespace ${readerNamespace}
`;
const readerHeaderOutputDir = path.join(
readerOutputDir,

View File

@ -59,10 +59,10 @@ function generateCombinedWriter(options) {
#include <CesiumJsonWriter/JsonWriter.h>
${writers
.map((writer) => {
return writer.writeInclude;
})
.join("\n")}
.map((writer) => {
return writer.writeInclude;
})
.join("\n")}
// NOLINTNEXTLINE(misc-include-cleaner)
#include <CesiumJsonWriter/writeJsonExtensions.h>
@ -78,10 +78,10 @@ function generateCombinedWriter(options) {
namespace {
${writers
.map((writer) => {
return writer.writeJsonDeclaration;
})
.join("\n")}
.map((writer) => {
return writer.writeJsonDeclaration;
})
.join("\n")}
// Forward declaration to avoid circular dependency since some properties
// are vector of unordered_map and others are unordered_map of vector
@ -96,7 +96,7 @@ function generateCombinedWriter(options) {
const CesiumUtility::IntrusivePointer<T>& ptr,
CesiumJsonWriter::JsonWriter& jsonWriter,
const CesiumJsonWriter::ExtensionWriterContext& context) {
writeJson(*ptr, jsonWriter, context);
writeJson(*ptr, jsonWriter, context);
}
[[maybe_unused]] void writeJson(
@ -196,7 +196,7 @@ function generateCombinedWriter(options) {
CesiumJsonWriter::JsonWriter& jsonWriter,
const CesiumJsonWriter::ExtensionWriterContext& context) {
if (!obj.extensions.empty()) {
if (hasRegisteredExtensions(obj, jsonWriter, context)) {
jsonWriter.Key("extensions");
writeJsonExtensions(obj, jsonWriter, context);
}
@ -246,10 +246,10 @@ function generateCombinedWriter(options) {
} // namespace
${writers
.map((writer) => {
return writer.writeDefinition;
})
.join("\n")}
.map((writer) => {
return writer.writeDefinition;
})
.join("\n")}
} // namespace ${writerNamespace}
`;