From 52d83118e77afa03a45b1ea6420f083771ebf0ba Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 26 Jul 2023 13:46:34 +1000 Subject: [PATCH] Introduce QgsTiledMeshIndex This is an equivalent of the point cloud index class, with some notable differences: - The class is designed to be thread safe. There's a shallow copy QgsTiledMeshIndex class, which contains an implicitly shared QgsAbstractTiledMeshIndex object. Tiled mesh data providers will be accompanied by a concrete class of QgsAbstractTiledMeshIndex. - The QgsTiledMeshIndex class takes care of thread safety by protecting the underlying QgsAbstractTiledMeshIndex via a read/write lock - Callers can request tiled mesh nodes from the index by calling QgsTiledMeshIndex::getNodes along with a QgsTiledMeshRequest object. This will return the tree structure of nodes matching the request. Currently only geometric error based filtering is supported by QgsTiledMeshRequest, but bounding box based filtering will also be introduced. - Clients can request tile content by calling QgsTiledMeshIndex::retrieveContent. This will either return a cached version of the content (if available), or retrieve and cache the content for future retrieval. Currently this uses a custom caching mechanism, but this will be replaced in future with QgsTileDownloadManager. (Currently QgsTileDownloadManager lacks support for blocking gets which is required by the index to avoid use of local event loops) The api is implemented by the cesium tiles data provider, which builds the index dynamically as required from the underlying cesium tiles tileset.json definitions. (The node index is NOT built upfront in order to minimize memory requirements for large tilesets). Currently the cesium index implementation only handles single-file tilesets, with support for sub tilesets to be introduced later. --- .../tiledmesh/qgstiledmeshdataprovider.sip.in | 11 + .../tiledmesh/qgstiledmeshindex.sip.in | 79 ++ .../tiledmesh/qgstiledmeshrequest.sip.in | 77 ++ python/core/core_auto.sip | 2 + src/core/CMakeLists.txt | 4 + .../tiledmesh/qgscesiumtilesdataprovider.cpp | 259 +++++- .../tiledmesh/qgscesiumtilesdataprovider.h | 26 +- src/core/tiledmesh/qgstiledmeshdataprovider.h | 12 + src/core/tiledmesh/qgstiledmeshindex.cpp | 103 +++ src/core/tiledmesh/qgstiledmeshindex.h | 159 ++++ src/core/tiledmesh/qgstiledmeshrequest.cpp | 31 + src/core/tiledmesh/qgstiledmeshrequest.h | 86 ++ tests/src/python/CMakeLists.txt | 1 + .../python/test_qgscesiumtiledmeshlayer.py | 761 +++++++++++++++++- tests/src/python/test_qgstiledmeshrequest.py | 44 + 15 files changed, 1627 insertions(+), 28 deletions(-) create mode 100644 python/core/auto_generated/tiledmesh/qgstiledmeshindex.sip.in create mode 100644 python/core/auto_generated/tiledmesh/qgstiledmeshrequest.sip.in create mode 100644 src/core/tiledmesh/qgstiledmeshindex.cpp create mode 100644 src/core/tiledmesh/qgstiledmeshindex.h create mode 100644 src/core/tiledmesh/qgstiledmeshrequest.cpp create mode 100644 src/core/tiledmesh/qgstiledmeshrequest.h create mode 100644 tests/src/python/test_qgstiledmeshrequest.py diff --git a/python/core/auto_generated/tiledmesh/qgstiledmeshdataprovider.sip.in b/python/core/auto_generated/tiledmesh/qgstiledmeshdataprovider.sip.in index 7e21b50e9e11..015c387d2802 100644 --- a/python/core/auto_generated/tiledmesh/qgstiledmeshdataprovider.sip.in +++ b/python/core/auto_generated/tiledmesh/qgstiledmeshdataprovider.sip.in @@ -79,6 +79,17 @@ This corresponds to the root node bounding volume. Coordinates in the returned volume are in the :py:func:`~QgsTiledMeshDataProvider.meshCrs` reference system, not the :py:func:`QgsDataProvider.crs()` system. %End + virtual QgsTiledMeshIndex index() const = 0; +%Docstring +Returns the provider's tile index. + +This is a shallow copy, implicitly shared container for an underlying :py:class:`QgsAbstractTiledMeshIndex` +implementation. + +The index is thread safe and can be used safely across multiple threads or transferred between +threads. +%End + }; /************************************************************************ diff --git a/python/core/auto_generated/tiledmesh/qgstiledmeshindex.sip.in b/python/core/auto_generated/tiledmesh/qgstiledmeshindex.sip.in new file mode 100644 index 000000000000..83fb0bf3cc3c --- /dev/null +++ b/python/core/auto_generated/tiledmesh/qgstiledmeshindex.sip.in @@ -0,0 +1,79 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/tiledmesh/qgstiledmeshindex.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + +class QgsTiledMeshIndex +{ +%Docstring(signature="appended") +An index for tiled mesh data providers. + +This is a shallow copy, implicitly shared container for an underlying :py:class:`QgsAbstractTiledMeshIndex` +implementation. + +The class is thread safe and can be used safely across multiple threads or transferred between +threads. + +.. versionadded:: 3.34 +%End + +%TypeHeaderCode +#include "qgstiledmeshindex.h" +%End + public: + + + ~QgsTiledMeshIndex(); + + QgsTiledMeshIndex( const QgsTiledMeshIndex &other ); +%Docstring +Copy constructor +%End + + bool isValid() const; +%Docstring +Returns ``True`` if the index is valid. +%End + + QgsTiledMeshNode rootNode() const; +%Docstring +Returns the root node for the index. +%End + + QgsTiledMeshNode *getNodes( const QgsTiledMeshRequest &request ) /Factory/; +%Docstring +Returns a node tree containing all nodes matching the given ``request``. + +Caller takes ownership of the returned node. + +May return ``None`` if no data satisfies the request. +%End + + QByteArray retrieveContent( const QString &uri, QgsFeedback *feedback = 0 ); +%Docstring +Retrieves index content for the specified ``uri``. + +The content is cached within the index, so multiple calls are potentially cost-free. + +The optional ``feedback`` argument can be used the cancel the request early. +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/tiledmesh/qgstiledmeshindex.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/tiledmesh/qgstiledmeshrequest.sip.in b/python/core/auto_generated/tiledmesh/qgstiledmeshrequest.sip.in new file mode 100644 index 000000000000..fe4d4288eace --- /dev/null +++ b/python/core/auto_generated/tiledmesh/qgstiledmeshrequest.sip.in @@ -0,0 +1,77 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/tiledmesh/qgstiledmeshrequest.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsTiledMeshRequest +{ +%Docstring(signature="appended") + +Tiled mesh data request. + +.. versionadded:: 3.34 +%End + +%TypeHeaderCode +#include "qgstiledmeshrequest.h" +%End + public: + + QgsTiledMeshRequest(); + + double requiredGeometricError() const; +%Docstring +Returns the required geometric error treshold for the returned nodes, in +mesh CRS units. + +If the error is 0 then no geometric error filtering will be applied. + +.. seealso:: :py:func:`setRequiredGeometricError` +%End + + void setRequiredGeometricError( double error ); +%Docstring +Sets the required geometric ``error`` treshold for the returned nodes, in +mesh CRS units. + +If the ``error`` is 0 then no geometric error filtering will be applied. + +.. seealso:: :py:func:`requiredGeometricError` +%End + + void setFeedback( QgsFeedback *feedback ); +%Docstring +Attach a ``feedback`` object that can be queried regularly by the request to check +if it should be canceled. + +Ownership of ``feedback`` is NOT transferred, and the caller must take care that it exists +for the lifetime of the request. + +.. seealso:: :py:func:`feedback` +%End + + QgsFeedback *feedback() const; +%Docstring +Returns the feedback object that can be queried regularly by the request to check +if it should be canceled, if set. + +.. seealso:: :py:func:`setFeedback` +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/tiledmesh/qgstiledmeshrequest.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index ced73587780c..6eac34cfe9a8 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -714,8 +714,10 @@ %Include auto_generated/tiledmesh/qgscesiumutils.sip %Include auto_generated/tiledmesh/qgstiledmeshboundingvolume.sip %Include auto_generated/tiledmesh/qgstiledmeshdataprovider.sip +%Include auto_generated/tiledmesh/qgstiledmeshindex.sip %Include auto_generated/tiledmesh/qgstiledmeshlayer.sip %Include auto_generated/tiledmesh/qgstiledmeshnode.sip +%Include auto_generated/tiledmesh/qgstiledmeshrequest.sip %Include auto_generated/sensor/qgssensormodel.sip %Include auto_generated/sensor/qgssensormanager.sip %Include auto_generated/sensor/qgssensorregistry.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 7d4e4f78628c..6e43fb042e64 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -342,10 +342,12 @@ set(QGIS_CORE_SRCS tiledmesh/qgstiledmeshconnection.cpp tiledmesh/qgstiledmeshdataitems.cpp tiledmesh/qgstiledmeshdataprovider.cpp + tiledmesh/qgstiledmeshindex.cpp tiledmesh/qgstiledmeshlayer.cpp tiledmesh/qgstiledmeshlayerrenderer.cpp tiledmesh/qgstiledmeshnode.cpp tiledmesh/qgstiledmeshprovidermetadata.cpp + tiledmesh/qgstiledmeshrequest.cpp sensor/qgssensormodel.cpp sensor/qgssensormanager.cpp @@ -1910,10 +1912,12 @@ set(QGIS_CORE_HDRS tiledmesh/qgstiledmeshconnection.h tiledmesh/qgstiledmeshdataprovider.h tiledmesh/qgstiledmeshdataitems.h + tiledmesh/qgstiledmeshindex.h tiledmesh/qgstiledmeshlayer.h tiledmesh/qgstiledmeshlayerrenderer.h tiledmesh/qgstiledmeshnode.h tiledmesh/qgstiledmeshprovidermetadata.h + tiledmesh/qgstiledmeshrequest.h sensor/qgssensormodel.h sensor/qgssensormanager.h diff --git a/src/core/tiledmesh/qgscesiumtilesdataprovider.cpp b/src/core/tiledmesh/qgscesiumtilesdataprovider.cpp index 55dd94302e26..34af6903c347 100644 --- a/src/core/tiledmesh/qgscesiumtilesdataprovider.cpp +++ b/src/core/tiledmesh/qgscesiumtilesdataprovider.cpp @@ -28,6 +28,9 @@ #include "qgsorientedbox3d.h" #include "qgstiledmeshboundingvolume.h" #include "qgscoordinatetransform.h" +#include "qgstiledmeshnode.h" +#include "qgstiledmeshindex.h" +#include "qgstiledmeshrequest.h" #include #include @@ -35,19 +38,246 @@ #include #include #include +#include ///@cond PRIVATE #define PROVIDER_KEY QStringLiteral( "cesiumtiles" ) #define PROVIDER_DESCRIPTION QStringLiteral( "Cesium 3D Tiles data provider" ) + +class QgsCesiumTiledMeshIndex final : public QgsAbstractTiledMeshIndex +{ + public: + + QgsCesiumTiledMeshIndex( + const json &tileset, + const QString &rootPath, + const QString &authCfg, + const QgsHttpHeaders &headers ); + + std::unique_ptr< QgsTiledMeshNode > nodeFromJson( const json &node, const QgsMatrix4x4 *parentTransform ); + + const QgsTiledMeshNode *rootNode() const final; + QgsTiledMeshNode *getNodes( const QgsTiledMeshRequest &request ) final; + + protected: + + QByteArray fetchContent( const QString &uri, QgsFeedback *feedback = nullptr ) final; + + private: + json mTileset; + QString mRootPath; + std::unique_ptr< QgsTiledMeshNode > mRootNode; + QString mAuthCfg; + QgsHttpHeaders mHeaders; + +}; + +class QgsCesiumTilesDataProviderSharedData +{ + public: + QgsCesiumTilesDataProviderSharedData(); + void initialize( const QString &tileset, + const QString &rootPath, + const QgsCoordinateTransformContext &transformContext, + const QString &authCfg, + const QgsHttpHeaders &headers ); + + QgsCoordinateReferenceSystem mLayerCrs; + QgsCoordinateReferenceSystem mMeshCrs; + std::unique_ptr< QgsAbstractTiledMeshNodeBoundingVolume > mBoundingVolume; + + QgsRectangle mExtent; + nlohmann::json mTileset; + QgsDoubleRange mZRange; + + QgsTiledMeshIndex mIndex; + + QReadWriteLock mMutex; + + +}; + + +// +// QgsCesiumTiledMeshIndex +// + +QgsCesiumTiledMeshIndex::QgsCesiumTiledMeshIndex( const json &tileset, const QString &rootPath, const QString &authCfg, const QgsHttpHeaders &headers ) + : mTileset( tileset ) + , mRootPath( rootPath ) + , mAuthCfg( authCfg ) + , mHeaders( headers ) +{ + mRootNode = nodeFromJson( tileset[ "root" ], nullptr ); +} + +std::unique_ptr< QgsTiledMeshNode > QgsCesiumTiledMeshIndex::nodeFromJson( const json &json, const QgsMatrix4x4 *parentTransform ) +{ + std::unique_ptr< QgsTiledMeshNode > node = std::make_unique< QgsTiledMeshNode >(); + + const auto &boundingVolume = json[ "boundingVolume" ]; + if ( boundingVolume.contains( "region" ) ) + { + const QgsBox3D rootRegion = QgsCesiumUtils::parseRegion( boundingVolume[ "region" ] ); + if ( !rootRegion.isNull() ) + { + node->setBoundingVolume( new QgsTiledMeshNodeBoundingVolumeRegion( rootRegion ) ); + } + } + else if ( boundingVolume.contains( "box" ) ) + { + const QgsOrientedBox3D bbox = QgsCesiumUtils::parseBox( boundingVolume["box"] ); + if ( !bbox.isNull() ) + { + node->setBoundingVolume( new QgsTiledMeshNodeBoundingVolumeBox( bbox ) ); + } + } + else if ( boundingVolume.contains( "sphere" ) ) + { + const QgsSphere sphere = QgsCesiumUtils::parseSphere( boundingVolume["sphere"] ); + if ( !sphere.isNull() ) + { + node->setBoundingVolume( new QgsTiledMeshNodeBoundingVolumeSphere( sphere ) ); + } + } + else + { + QgsDebugError( QStringLiteral( "unsupported boundingVolume format" ) ); + } + + node->setGeometricError( json["geometricError"].get< double >() ); + if ( json["refine"] == "ADD" ) + node->setRefinementProcess( Qgis::TileRefinementProcess::Additive ); + else if ( json["refine"] == "REPLACE" ) + node->setRefinementProcess( Qgis::TileRefinementProcess::Replacement ); + + + if ( json.contains( "content" ) && !json["content"].is_null() ) + { + const auto &contentJson = json["content"]; + + // sometimes URI, sometimes URL... + if ( contentJson.contains( "uri" ) && !contentJson["uri"].is_null() ) + { + node->setContentUri( mRootPath + '/' + QString::fromStdString( contentJson["uri"].get() ) ); + } + else if ( contentJson.contains( "url" ) && !contentJson["url"].is_null() ) + { + node->setContentUri( mRootPath + '/' + QString::fromStdString( contentJson["url"].get() ) ); + } + } + + if ( json.contains( "transform" ) && !json["transform"].is_null() ) + { + const auto &transformJson = json["transform"]; + QgsMatrix4x4 transform; + double *ptr = transform.data(); + for ( int i = 0; i < 16; ++i ) + ptr[i] = transformJson[i].get(); + + if ( parentTransform ) + { + node->setTransform( *parentTransform * transform ); + } + else + { + node->setTransform( transform ); + } + } + else if ( parentTransform ) + { + node->setTransform( *parentTransform ); + } + + return node; +} + +const QgsTiledMeshNode *QgsCesiumTiledMeshIndex::rootNode() const +{ + return mRootNode.get(); +} + +QgsTiledMeshNode *QgsCesiumTiledMeshIndex::getNodes( const QgsTiledMeshRequest &request ) +{ + if ( !mRootNode ) + return nullptr; + + std::function< QgsTiledMeshNode * ( const json &nodeJson, QgsTiledMeshNode * )> traverseNode; + traverseNode = [&request, &traverseNode, this]( const json & nodeJson, QgsTiledMeshNode * parent ) -> QgsTiledMeshNode * + { + std::unique_ptr< QgsTiledMeshNode > newNode; + if ( parent ) + { + const QgsMatrix4x4 parentTransform = parent->transform(); + newNode = nodeFromJson( nodeJson, &parentTransform ); + } + else + { + newNode = nodeFromJson( nodeJson, nullptr ); + } + + if ( parent ) + parent->addChild( newNode.get() ); + + // TODO -- if node has content json, fetch it now and parse + + if ( request.requiredGeometricError() <= 0 || newNode->geometricError() <= 0 || newNode->geometricError() > request.requiredGeometricError() ) + { + // haven't traversed deep enough down this node, we need to explore children + if ( nodeJson.contains( "children" ) ) + { + for ( const auto &childJson : nodeJson["children"] ) + { + if ( request.feedback() && request.feedback()->isCanceled() ) + break; + + traverseNode( childJson, newNode.get() ); + } + } + } + + return newNode.release(); + }; + + return traverseNode( mTileset[ "root" ], nullptr ); +} + +QByteArray QgsCesiumTiledMeshIndex::fetchContent( const QString &uri, QgsFeedback *feedback ) +{ + // TODO -- error reporting? + if ( uri.startsWith( "http" ) ) + { + QNetworkRequest networkRequest = QNetworkRequest( QUrl( uri ) ); + mHeaders.updateNetworkRequest( networkRequest ); + const QgsNetworkReplyContent reply = QgsNetworkAccessManager::instance()->blockingGet( + networkRequest, mAuthCfg, false, feedback ); + return reply.content(); + } + else if ( QFile::exists( uri ) ) + { + QFile file( uri ); + if ( file.open( QIODevice::ReadOnly ) ) + { + return file.readAll(); + } + } + return QByteArray(); +} + + // // QgsCesiumTilesDataProviderSharedData // -QgsCesiumTilesDataProviderSharedData::QgsCesiumTilesDataProviderSharedData() = default; +QgsCesiumTilesDataProviderSharedData::QgsCesiumTilesDataProviderSharedData() + : mIndex( QgsTiledMeshIndex( nullptr ) ) +{ -void QgsCesiumTilesDataProviderSharedData::setTilesetContent( const QString &tileset, const QgsCoordinateTransformContext &transformContext ) +} + +void QgsCesiumTilesDataProviderSharedData::initialize( const QString &tileset, const QString &rootPath, const QgsCoordinateTransformContext &transformContext, const QString &authCfg, const QgsHttpHeaders &headers ) { mTileset = json::parse( tileset.toStdString() ); @@ -152,6 +382,15 @@ void QgsCesiumTilesDataProviderSharedData::setTilesetContent( const QString &til QgsDebugError( QStringLiteral( "unsupported boundingVolume format" ) ); } } + + mIndex = QgsTiledMeshIndex( + new QgsCesiumTiledMeshIndex( + mTileset, + rootPath, + authCfg, + headers + ) + ); } } @@ -221,18 +460,21 @@ bool QgsCesiumTilesDataProvider::init() } const QgsNetworkReplyContent content = networkRequest.reply(); - mShared->setTilesetContent( content.content(), transformContext() ); + + const QString base = tileSetUri.left( tileSetUri.lastIndexOf( '/' ) ); + mShared->initialize( content.content(), base, transformContext(), mAuthCfg, mHeaders ); } else { // try uri as a local file - if ( QFileInfo::exists( dataSourceUri( ) ) ) + const QFileInfo fi( dataSourceUri() ); + if ( fi.exists() ) { QFile file( dataSourceUri( ) ); if ( file.open( QIODevice::ReadOnly | QIODevice::Text ) ) { const QByteArray raw = file.readAll(); - mShared->setTilesetContent( raw, transformContext() ); + mShared->initialize( raw, fi.path(), transformContext(), mAuthCfg, mHeaders ); } else { @@ -357,6 +599,13 @@ const QgsAbstractTiledMeshNodeBoundingVolume *QgsCesiumTilesDataProvider::boundi return mShared ? mShared->mBoundingVolume.get() : nullptr; } +QgsTiledMeshIndex QgsCesiumTilesDataProvider::index() const +{ + QGIS_PROTECT_QOBJECT_THREAD_ACCESS + + return mShared ? mShared->mIndex : QgsTiledMeshIndex( nullptr ); +} + // // QgsCesiumTilesProviderMetadata diff --git a/src/core/tiledmesh/qgscesiumtilesdataprovider.h b/src/core/tiledmesh/qgscesiumtilesdataprovider.h index ee25a54f3462..614d5f1e49e1 100644 --- a/src/core/tiledmesh/qgscesiumtilesdataprovider.h +++ b/src/core/tiledmesh/qgscesiumtilesdataprovider.h @@ -23,35 +23,15 @@ #include "qgstiledmeshdataprovider.h" #include "qgis.h" #include "qgsprovidermetadata.h" -#include "qgsrange.h" -#include "nlohmann/json.hpp" #define SIP_NO_FILE class QgsAbstractTiledMeshNodeBoundingVolume; class QgsCoordinateTransformContext; +class QgsCesiumTilesDataProviderSharedData; ///@cond PRIVATE -class QgsCesiumTilesDataProviderSharedData -{ - public: - QgsCesiumTilesDataProviderSharedData(); - void setTilesetContent( const QString &tileset, const QgsCoordinateTransformContext &transformContext ); - - QgsCoordinateReferenceSystem mLayerCrs; - QgsCoordinateReferenceSystem mMeshCrs; - std::unique_ptr< QgsAbstractTiledMeshNodeBoundingVolume > mBoundingVolume; - - QgsRectangle mExtent; - nlohmann::json mTileset; - QgsDoubleRange mZRange; - - QReadWriteLock mMutex; - - -}; - class CORE_EXPORT QgsCesiumTilesDataProvider final: public QgsTiledMeshDataProvider { Q_OBJECT @@ -75,7 +55,8 @@ class CORE_EXPORT QgsCesiumTilesDataProvider final: public QgsTiledMeshDataProvi QString description() const final; QString htmlMetadata() const final; const QgsCoordinateReferenceSystem meshCrs() const final; - const QgsAbstractTiledMeshNodeBoundingVolume *boundingVolume() const override; + const QgsAbstractTiledMeshNodeBoundingVolume *boundingVolume() const final; + QgsTiledMeshIndex index() const final; private: @@ -114,3 +95,4 @@ class QgsCesiumTilesProviderMetadata : public QgsProviderMetadata ///@endcond #endif // QGSCESIUMTILESDATAPROVIDER_H + diff --git a/src/core/tiledmesh/qgstiledmeshdataprovider.h b/src/core/tiledmesh/qgstiledmeshdataprovider.h index ea195a73c83d..3f2c50811479 100644 --- a/src/core/tiledmesh/qgstiledmeshdataprovider.h +++ b/src/core/tiledmesh/qgstiledmeshdataprovider.h @@ -24,6 +24,7 @@ #include "qgis.h" class QgsAbstractTiledMeshNodeBoundingVolume; +class QgsTiledMeshIndex; /** * \ingroup core @@ -90,6 +91,17 @@ class CORE_EXPORT QgsTiledMeshDataProvider: public QgsDataProvider */ virtual const QgsAbstractTiledMeshNodeBoundingVolume *boundingVolume() const = 0; + /** + * Returns the provider's tile index. + * + * This is a shallow copy, implicitly shared container for an underlying QgsAbstractTiledMeshIndex + * implementation. + * + * The index is thread safe and can be used safely across multiple threads or transferred between + * threads. + */ + virtual QgsTiledMeshIndex index() const = 0; + }; #endif // QGSTILEDMESHDATAPROVIDER_H diff --git a/src/core/tiledmesh/qgstiledmeshindex.cpp b/src/core/tiledmesh/qgstiledmeshindex.cpp new file mode 100644 index 000000000000..45a40461579f --- /dev/null +++ b/src/core/tiledmesh/qgstiledmeshindex.cpp @@ -0,0 +1,103 @@ +/*************************************************************************** + qgstiledmeshindex.cpp + -------------------- + begin : June 2023 + copyright : (C) 2023 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstiledmeshindex.h" +#include "qgsfeedback.h" +#include "qgsreadwritelocker.h" +#include "qgstiledmeshnode.h" + +// +// QgsAbstractTiledMeshIndex +// + +QgsAbstractTiledMeshIndex::QgsAbstractTiledMeshIndex() +{ + mContentCache.setMaxCost( 50 ); +} + +QgsAbstractTiledMeshIndex::~QgsAbstractTiledMeshIndex() = default; + +QByteArray QgsAbstractTiledMeshIndex::retrieveContent( const QString &uri, QgsFeedback *feedback ) +{ + QgsReadWriteLocker locker( mLock, QgsReadWriteLocker::Read ); + if ( QByteArray *cachedData = mContentCache.object( uri ) ) + { + return *cachedData; + } + + locker.unlock(); + + const QByteArray res = fetchContent( uri, feedback ); + locker.changeMode( QgsReadWriteLocker::Write ); + mContentCache.insert( uri, new QByteArray( res ) ); + return res; +} + +// +// QgsTiledMeshIndex +// + +QgsTiledMeshIndex::QgsTiledMeshIndex( QgsAbstractTiledMeshIndex *index ) + : mIndex( index ) +{ + +} + +QgsTiledMeshIndex::~QgsTiledMeshIndex() = default; + +QgsTiledMeshIndex::QgsTiledMeshIndex( const QgsTiledMeshIndex &other ) + : mIndex( other.mIndex ) +{ + +} + +QgsTiledMeshIndex &QgsTiledMeshIndex::operator=( const QgsTiledMeshIndex &other ) +{ + mIndex = other.mIndex; + return *this; +} + +bool QgsTiledMeshIndex::isValid() const +{ + return static_cast< bool >( mIndex.get() ); +} + +QgsTiledMeshNode QgsTiledMeshIndex::rootNode() const +{ + if ( !mIndex ) + return QgsTiledMeshNode(); + + QgsReadWriteLocker locker( mIndex->mLock, QgsReadWriteLocker::Read ); + return *mIndex->rootNode(); +} + +QgsTiledMeshNode *QgsTiledMeshIndex::getNodes( const QgsTiledMeshRequest &request ) +{ + if ( !mIndex ) + return nullptr; + + QgsReadWriteLocker locker( mIndex->mLock, QgsReadWriteLocker::Write ); + return mIndex->getNodes( request ); +} + +QByteArray QgsTiledMeshIndex::retrieveContent( const QString &uri, QgsFeedback *feedback ) +{ + if ( !mIndex ) + return QByteArray(); + + return mIndex->retrieveContent( uri, feedback ); +} diff --git a/src/core/tiledmesh/qgstiledmeshindex.h b/src/core/tiledmesh/qgstiledmeshindex.h new file mode 100644 index 000000000000..3bb396bcbaa2 --- /dev/null +++ b/src/core/tiledmesh/qgstiledmeshindex.h @@ -0,0 +1,159 @@ +/*************************************************************************** + qgstiledmeshindex.h + -------------------- + begin : June 2023 + copyright : (C) 2023 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ****************************************************************** + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSTILEDMESHINDEX_H +#define QGSTILEDMESHINDEX_H + +#include "qgis_core.h" +#include "qgis.h" + +#include +#include + +class QgsTiledMeshNode; +class QgsFeedback; +class QgsTiledMeshRequest; + +#ifndef SIP_RUN + +/** + * \ingroup core + * \brief An abstract base class for tiled mesh data provider indices. + * + * \since QGIS 3.34 + */ +class CORE_EXPORT QgsAbstractTiledMeshIndex +{ + public: + + QgsAbstractTiledMeshIndex(); + virtual ~QgsAbstractTiledMeshIndex(); + + QgsAbstractTiledMeshIndex( const QgsAbstractTiledMeshIndex &other ) = delete; + QgsAbstractTiledMeshIndex &operator=( const QgsAbstractTiledMeshIndex &other ) = delete; + + /** + * Returns the root node for the index. + */ + virtual const QgsTiledMeshNode *rootNode() const = 0; + + /** + * Returns a node tree containing all nodes matching the given \a request. + * + * Caller takes ownership of the returned node. + * + * May return NULLPTR if no data satisfies the request. + */ + virtual QgsTiledMeshNode *getNodes( const QgsTiledMeshRequest &request ) = 0; + + /** + * Retrieves index content for the specified \a uri. + * + * The content is cached within the index, so multiple calls are potentially cost-free. + * + * The optional \a feedback argument can be used the cancel the request early. + */ + QByteArray retrieveContent( const QString &uri, QgsFeedback *feedback = nullptr ); + + mutable QReadWriteLock mLock; + + protected: + + /** + * Fetches index content for the specified \a uri. + * + * This must be implemented in subclasses to retrieve the corresponding binary content, + * including performing any necessary network requests. + * + * The optional \a feedback argument can be used the cancel the request early. + */ + virtual QByteArray fetchContent( const QString &uri, QgsFeedback *feedback = nullptr ) = 0; + + private: + + QCache< QString, QByteArray > mContentCache; + +}; +#endif + +/** + * \ingroup core + * \brief An index for tiled mesh data providers. + * + * This is a shallow copy, implicitly shared container for an underlying QgsAbstractTiledMeshIndex + * implementation. + * + * The class is thread safe and can be used safely across multiple threads or transferred between + * threads. + * + * \since QGIS 3.34 + */ +class CORE_EXPORT QgsTiledMeshIndex +{ + public: + + /** + * Constructor for QgsTiledMeshIndex. + * + * The specified \a index implementation will be transferred to the index. + * + * \note Not available in Python bindings. + */ + explicit QgsTiledMeshIndex( QgsAbstractTiledMeshIndex *index SIP_TRANSFER = nullptr ) SIP_SKIP; + + ~QgsTiledMeshIndex(); + + //! Copy constructor + QgsTiledMeshIndex( const QgsTiledMeshIndex &other ); + QgsTiledMeshIndex &operator=( const QgsTiledMeshIndex &other ); + + /** + * Returns TRUE if the index is valid. + */ + bool isValid() const; + + /** + * Returns the root node for the index. + */ + QgsTiledMeshNode rootNode() const; + + /** + * Returns a node tree containing all nodes matching the given \a request. + * + * Caller takes ownership of the returned node. + * + * May return NULLPTR if no data satisfies the request. + */ + QgsTiledMeshNode *getNodes( const QgsTiledMeshRequest &request ) SIP_FACTORY; + + /** + * Retrieves index content for the specified \a uri. + * + * The content is cached within the index, so multiple calls are potentially cost-free. + * + * The optional \a feedback argument can be used the cancel the request early. + */ + QByteArray retrieveContent( const QString &uri, QgsFeedback *feedback = nullptr ); + + private: + + std::shared_ptr mIndex; +}; + + +#endif // QGSTILEDMESHINDEX_H diff --git a/src/core/tiledmesh/qgstiledmeshrequest.cpp b/src/core/tiledmesh/qgstiledmeshrequest.cpp new file mode 100644 index 000000000000..27718fa75128 --- /dev/null +++ b/src/core/tiledmesh/qgstiledmeshrequest.cpp @@ -0,0 +1,31 @@ +/*************************************************************************** + qgstiledmeshrequest.cpp + -------------------- + begin : June 2023 + copyright : (C) 2023 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstiledmeshrequest.h" + +QgsTiledMeshRequest::QgsTiledMeshRequest() = default; + +void QgsTiledMeshRequest::setFeedback( QgsFeedback *feedback ) +{ + mFeedback = feedback; +} + +QgsFeedback *QgsTiledMeshRequest::feedback() const +{ + return mFeedback; +} + diff --git a/src/core/tiledmesh/qgstiledmeshrequest.h b/src/core/tiledmesh/qgstiledmeshrequest.h new file mode 100644 index 000000000000..2383dc09f007 --- /dev/null +++ b/src/core/tiledmesh/qgstiledmeshrequest.h @@ -0,0 +1,86 @@ +/*************************************************************************** + qgstiledmeshrequest.h + -------------------- + begin : June 2023 + copyright : (C) 2023 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ****************************************************************** + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSTILEDMESHREQUEST_H +#define QGSTILEDMESHREQUEST_H + +#include "qgis_core.h" +#include "qgis.h" + +class QgsFeedback; + +/** + * \ingroup core + * + * \brief Tiled mesh data request. + * + * \since QGIS 3.34 + */ +class CORE_EXPORT QgsTiledMeshRequest +{ + public: + + QgsTiledMeshRequest(); + + /** + * Returns the required geometric error treshold for the returned nodes, in + * mesh CRS units. + * + * If the error is 0 then no geometric error filtering will be applied. + * + * \see setRequiredGeometricError() + */ + double requiredGeometricError() const { return mRequiredGeometricError; } + + /** + * Sets the required geometric \a error treshold for the returned nodes, in + * mesh CRS units. + * + * If the \a error is 0 then no geometric error filtering will be applied. + * + * \see requiredGeometricError() + */ + void setRequiredGeometricError( double error ) { mRequiredGeometricError = error; } + + /** + * Attach a \a feedback object that can be queried regularly by the request to check + * if it should be canceled. + * + * Ownership of \a feedback is NOT transferred, and the caller must take care that it exists + * for the lifetime of the request. + * + * \see feedback() + */ + void setFeedback( QgsFeedback *feedback ); + + /** + * Returns the feedback object that can be queried regularly by the request to check + * if it should be canceled, if set. + * + * \see setFeedback() + */ + QgsFeedback *feedback() const; + + private: + + QgsFeedback *mFeedback = nullptr; + double mRequiredGeometricError = 0; +}; + + +#endif // QGSTILEDMESHREQUEST_H diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index fc0f51af8403..b6f17d575267 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -397,6 +397,7 @@ ADD_PYTHON_TEST(PyQgsTextFormatWidget test_qgstextformatwidget.py) ADD_PYTHON_TEST(PyQgsTiledMeshBoundingVolume test_qgstiledmeshboundingvolume.py) ADD_PYTHON_TEST(PyQgsTiledMeshLayer test_qgstiledmeshlayer.py) ADD_PYTHON_TEST(PyQgsTiledMeshNode test_qgstiledmeshnode.py) +ADD_PYTHON_TEST(PyQgsTiledMeshRequest test_qgstiledmeshrequest.py) ADD_PYTHON_TEST(PyQgsTiles test_qgstiles.py) ADD_PYTHON_TEST(PyQgsTreeWidgetItem test_qgstreewidgetitem.py) ADD_PYTHON_TEST(PyQgsUnitTypes test_qgsunittypes.py) diff --git a/tests/src/python/test_qgscesiumtiledmeshlayer.py b/tests/src/python/test_qgscesiumtiledmeshlayer.py index 051af7caa5ca..39320b84e689 100644 --- a/tests/src/python/test_qgscesiumtiledmeshlayer.py +++ b/tests/src/python/test_qgscesiumtiledmeshlayer.py @@ -14,9 +14,12 @@ import qgis # NOQA from qgis.core import ( + Qgis, QgsTiledMeshLayer, QgsCoordinateReferenceSystem, - QgsMatrix4x4 + QgsMatrix4x4, + QgsOrientedBox3D, + QgsTiledMeshRequest ) from qgis.testing import start_app, unittest from utilities import unitTestDataPath @@ -241,6 +244,762 @@ def test_source_bounding_sphere(self): self.assertIn('e575c6f1', layer.dataProvider().htmlMetadata()) self.assertIn('-2,658.68 - 4,056.37', layer.dataProvider().htmlMetadata()) + def test_index(self): + with tempfile.TemporaryDirectory() as temp_dir: + tmp_file = os.path.join(temp_dir, 'tileset.json') + with open(tmp_file, 'wt', encoding='utf-8') as f: + f.write(""" + { + "asset": { + "version": "1.0" + }, + "geometricError": 100.0, + "root": { + "transform": [ + -0.45434427515502945, + -0.8908261781255931, + 0.0, + 0.0, + 0.7960963970658078, + -0.4060296490606728, + 0.4487431901015397, + 0.0, + -0.39975218099804105, + 0.20388389943743965, + 0.8936607574116106, + 0.0, + -5061037.787957486, + 2571460.026087591, + -2824903.437545935, + 1.0 + ], + "boundingVolume": { + "box": [ + -1.45782, + 0.265355, + 7.44958, + 94.1946, + 0.0, + 0.0, + 0.0, + -14.9309, + 0.0, + 0.0, + 0.0, + 75.0565 + ] + }, + "geometricError": 100.0, + "refine": "ADD", + "content": null, + "children": [ + { + "transform": null, + "boundingVolume": { + "box": [ + 44.4926, + -2.87012, + 39.2136, + 45.9504, + 0.0, + 0.0, + 0.0, + -8.28534, + 0.0, + 0.0, + 0.0, + 31.8188 + ] + }, + "geometricError": 9.1, + "refine": "ADD", + "content": { + "uri": "LOD-2/Mesh-XR-YR.b3dm" + }, + "children": [ + { + "transform": null, + "boundingVolume": { + "box": [ + 44.4926, + -2.89708, + 39.2136, + 45.9504, + 0.0, + 0.0, + 0.0, + -8.31229, + 0.0, + 0.0, + 0.0, + 31.8188 + ] + }, + "geometricError": 3.0, + "refine": "ADD", + "content": { + "uri": "LOD-1/Mesh-XR-YR.b3dm" + }, + "children": [ + { + "transform": null, + "boundingVolume": { + "box": [ + 44.4926, + -2.89708, + 39.2136, + 45.9504, + 0.0, + 0.0, + 0.0, + -8.31229, + 0.0, + 0.0, + 0.0, + 31.8188 + ] + }, + "geometricError": 0.0, + "refine": "ADD", + "content": { + "uri": "LOD-0/Mesh-XR-YR.b3dm" + }, + "children": [] + } + ] + } + ] + }, + { + "transform": null, + "boundingVolume": { + "box": [ + -48.5551, + 5.67839, + 44.9504, + 47.0973, + 0.0, + 0.0, + 0.0, + -9.5179, + 0.0, + 0.0, + 0.0, + 37.5556 + ] + }, + "geometricError": 9.1, + "refine": "ADD", + "content": { + "uri": "LOD-2/Mesh-XL-YR.b3dm" + }, + "children": [ + { + "transform": null, + "boundingVolume": { + "box": [ + -48.5551, + 5.7113, + 44.9504, + 47.0973, + 0.0, + 0.0, + 0.0, + -9.48498, + 0.0, + 0.0, + 0.0, + 37.5556 + ] + }, + "geometricError": 3.0, + "refine": "ADD", + "content": { + "uri": "LOD-1/Mesh-XL-YR.b3dm" + }, + "children": [ + { + "transform": null, + "boundingVolume": { + "box": [ + -48.5551, + 5.7113, + 44.9504, + 47.0973, + 0.0, + 0.0, + 0.0, + -9.48498, + 0.0, + 0.0, + 0.0, + 37.5556 + ] + }, + "geometricError": 0.0, + "refine": "ADD", + "content": { + "uri": "LOD-0/Mesh-XL-YR.b3dm" + }, + "children": [] + } + ] + } + ] + }, + { + "transform": null, + "boundingVolume": { + "box": [ + 45.6395, + -2.2089, + -30.1061, + 47.0973, + 0.0, + 0.0, + 0.0, + -12.4567, + 0.0, + 0.0, + 0.0, + 37.5008 + ] + }, + "geometricError": 9.0, + "refine": "ADD", + "content": { + "uri": "LOD-2/Mesh-XR-YL.b3dm" + }, + "children": [ + { + "transform": null, + "boundingVolume": { + "box": [ + 45.6395, + -2.2089, + -30.1061, + 47.0973, + 0.0, + 0.0, + 0.0, + -12.4567, + 0.0, + 0.0, + 0.0, + 37.5008 + ] + }, + "geometricError": 3.0, + "refine": "ADD", + "content": { + "uri": "LOD-1/Mesh-XR-YL.b3dm" + }, + "children": [ + { + "transform": null, + "boundingVolume": { + "box": [ + 45.6395, + -2.2089, + -30.1061, + 47.0973, + 0.0, + 0.0, + 0.0, + -12.4567, + 0.0, + 0.0, + 0.0, + 37.5008 + ] + }, + "geometricError": 0.0, + "refine": "ADD", + "content": { + "uri": "LOD-0/Mesh-XR-YL.b3dm" + }, + "children": [] + } + ] + } + ] + }, + { + "transform": null, + "boundingVolume": { + "box": [ + -47.7663, + 0.128709, + -29.9984, + 46.3085, + 0.0, + 0.0, + 0.0, + -9.23776, + 0.0, + 0.0, + 0.0, + 37.3932 + ] + }, + "geometricError": 9.1, + "refine": "ADD", + "content": { + "uri": "LOD-2/Mesh-XL-YL.b3dm" + }, + "children": [ + { + "transform": null, + "boundingVolume": { + "box": [ + -47.7663, + 0.067987, + -29.9984, + 46.3085, + 0.0, + 0.0, + 0.0, + -9.29848, + 0.0, + 0.0, + 0.0, + 37.3932 + ] + }, + "geometricError": 3.0, + "refine": "REPLACE", + "content": { + "uri": "LOD-1/Mesh-XL-YL.b3dm" + }, + "children": [ + { + "transform": null, + "boundingVolume": { + "box": [ + -47.7663, + 0.067987, + -29.9984, + 46.3085, + 0.0, + 0.0, + 0.0, + -9.29848, + 0.0, + 0.0, + 0.0, + 37.3932 + ] + }, + "geometricError": 0.0, + "refine": "ADD", + "content": { + "uri": "LOD-0/Mesh-XL-YL.b3dm" + }, + "children": [] + } + ] + } + ] + } + ] + } +}""") + + layer = QgsTiledMeshLayer(tmp_file, 'my layer', + 'cesiumtiles') + self.assertTrue(layer.dataProvider().isValid()) + + index = layer.dataProvider().index() + self.assertTrue(index.isValid()) + + self.assertEqual(index.rootNode().boundingVolume().box(), + QgsOrientedBox3D([-1.45782, 0.265355, 7.44958], [94.1946, 0, 0, 0, -14.9309, 0, 0, 0, 75.0565])) + + # children should not be populated in advance + self.assertEqual(len(index.rootNode().children()), 0) + + # get all nodes + root_node = index.getNodes(QgsTiledMeshRequest()) + self.assertFalse(root_node.parentNode()) + self.assertFalse(root_node.contentUri()) + self.assertEqual(root_node.geometricError(), 100.0) + self.assertEqual(root_node.refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.boundingVolume().box(), + QgsOrientedBox3D([-1.45782, 0.265355, 7.44958], [94.1946, 0, 0, 0, -14.9309, 0, 0, 0, 75.0565])) + + self.assertEqual(len(root_node.children()), 4) + self.assertEqual(root_node.children()[0].parentNode(), root_node) + self.assertEqual(root_node.children()[0].contentUri(), temp_dir + '/LOD-2/Mesh-XR-YR.b3dm') + self.assertEqual(root_node.children()[0].geometricError(), 9.1) + self.assertEqual(root_node.children()[0].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[0].boundingVolume().box(), + QgsOrientedBox3D([44.4926, -2.87012, 39.2136], [45.9504, 0, 0, 0, -8.28534, 0, 0, 0, 31.8188])) + + self.assertEqual(len(root_node.children()[0].children()), 1) + self.assertEqual(root_node.children()[0].children()[0].parentNode(), root_node.children()[0]) + self.assertEqual(root_node.children()[0].children()[0].contentUri(), temp_dir + '/LOD-1/Mesh-XR-YR.b3dm') + self.assertEqual(root_node.children()[0].children()[0].geometricError(), 3) + self.assertEqual(root_node.children()[0].children()[0].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[0].children()[0].boundingVolume().box(), + QgsOrientedBox3D([44.4926, -2.89708, 39.2136], [45.9504, 0, 0, 0, -8.31229, 0, 0, 0, 31.8188])) + self.assertEqual(len(root_node.children()[0].children()[0].children()), 1) + self.assertEqual(root_node.children()[0].children()[0].children()[0].parentNode(), root_node.children()[0].children()[0]) + self.assertEqual(root_node.children()[0].children()[0].children()[0].contentUri(), temp_dir + '/LOD-0/Mesh-XR-YR.b3dm') + self.assertEqual(root_node.children()[0].children()[0].children()[0].geometricError(), 0) + self.assertEqual(root_node.children()[0].children()[0].children()[0].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[0].children()[0].children()[0].boundingVolume().box(), + QgsOrientedBox3D([44.4926, -2.89708, 39.2136], [45.9504, 0, 0, 0, -8.31229, 0, 0, 0, 31.8188])) + self.assertFalse( + len(root_node.children()[0].children()[0].children()[0].children())) + + self.assertEqual(root_node.children()[1].parentNode(), root_node) + self.assertEqual(root_node.children()[1].contentUri(), + temp_dir + '/LOD-2/Mesh-XL-YR.b3dm') + self.assertEqual(root_node.children()[1].geometricError(), 9.1) + self.assertEqual(root_node.children()[1].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[1].boundingVolume().box(), + QgsOrientedBox3D([-48.5551, 5.67839, 44.9504], [47.0973, 0, 0, 0, -9.5179, 0, 0, 0, 37.5556])) + + self.assertEqual(len(root_node.children()[1].children()), 1) + self.assertEqual(root_node.children()[1].children()[0].parentNode(), root_node.children()[1]) + self.assertEqual(root_node.children()[1].children()[0].contentUri(), temp_dir + '/LOD-1/Mesh-XL-YR.b3dm') + self.assertEqual(root_node.children()[1].children()[0].geometricError(), 3) + self.assertEqual(root_node.children()[1].children()[0].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[1].children()[0].boundingVolume().box(), + QgsOrientedBox3D([-48.5551, 5.7113, 44.9504], [47.0973, 0, 0, 0, -9.48498, 0, 0, 0, 37.5556])) + self.assertEqual(len(root_node.children()[1].children()[0].children()), 1) + self.assertEqual(root_node.children()[1].children()[0].children()[0].parentNode(), root_node.children()[1].children()[0]) + self.assertEqual(root_node.children()[1].children()[0].children()[0].contentUri(), temp_dir + '/LOD-0/Mesh-XL-YR.b3dm') + self.assertEqual(root_node.children()[1].children()[0].children()[0].geometricError(), 0) + self.assertEqual(root_node.children()[1].children()[0].children()[0].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[1].children()[0].children()[0].boundingVolume().box(), + QgsOrientedBox3D([-48.5551, 5.7113, 44.9504], [47.0973, 0, 0, 0, -9.48498, 0, 0, 0, 37.5556])) + self.assertFalse( + len(root_node.children()[1].children()[0].children()[0].children())) + + self.assertEqual(root_node.children()[2].parentNode(), root_node) + self.assertEqual(root_node.children()[2].contentUri(), + temp_dir + '/LOD-2/Mesh-XR-YL.b3dm') + self.assertEqual(root_node.children()[2].geometricError(), 9.0) + self.assertEqual(root_node.children()[2].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[2].boundingVolume().box(), + QgsOrientedBox3D([45.6395, -2.2089, -30.1061], [47.0973, 0, 0, 0, -12.4567, 0, 0, 0, 37.5008])) + self.assertEqual(len(root_node.children()[2].children()), 1) + self.assertEqual(root_node.children()[2].children()[0].parentNode(), root_node.children()[2]) + self.assertEqual(root_node.children()[2].children()[0].contentUri(), temp_dir + '/LOD-1/Mesh-XR-YL.b3dm') + self.assertEqual(root_node.children()[2].children()[0].geometricError(), 3) + self.assertEqual(root_node.children()[2].children()[0].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[2].children()[0].boundingVolume().box(), + QgsOrientedBox3D([45.6395, -2.2089, -30.1061], [47.0973, 0, 0, 0, -12.4567, 0, 0, 0, 37.5008])) + self.assertEqual(len(root_node.children()[2].children()[0].children()), 1) + self.assertEqual(root_node.children()[2].children()[0].children()[0].parentNode(), root_node.children()[2].children()[0]) + self.assertEqual(root_node.children()[2].children()[0].children()[0].contentUri(), temp_dir + '/LOD-0/Mesh-XR-YL.b3dm') + self.assertEqual(root_node.children()[2].children()[0].children()[0].geometricError(), 0) + self.assertEqual(root_node.children()[2].children()[0].children()[0].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[2].children()[0].children()[0].boundingVolume().box(), + QgsOrientedBox3D([45.6395, -2.2089, -30.1061], [47.0973, 0, 0, 0, -12.4567, 0, 0, 0, 37.5008])) + self.assertFalse( + len(root_node.children()[2].children()[0].children()[0].children())) + + self.assertEqual(root_node.children()[3].parentNode(), root_node) + self.assertEqual(root_node.children()[3].contentUri(), + temp_dir + '/LOD-2/Mesh-XL-YL.b3dm') + self.assertEqual(root_node.children()[3].geometricError(), 9.1) + self.assertEqual(root_node.children()[3].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[3].boundingVolume().box(), + QgsOrientedBox3D([-47.7663, 0.128709, -29.9984], [46.3085, 0, 0, 0, -9.23776, 0, 0, 0, 37.3932])) + self.assertEqual(len(root_node.children()[3].children()), 1) + self.assertEqual(root_node.children()[3].children()[0].parentNode(), root_node.children()[3]) + self.assertEqual(root_node.children()[3].children()[0].contentUri(), temp_dir + '/LOD-1/Mesh-XL-YL.b3dm') + self.assertEqual(root_node.children()[3].children()[0].geometricError(), 3) + self.assertEqual(root_node.children()[3].children()[0].refinementProcess(), Qgis.TileRefinementProcess.Replacement) + self.assertEqual(root_node.children()[3].children()[0].boundingVolume().box(), + QgsOrientedBox3D([-47.7663, 0.067987, -29.9984], [46.3085, 0, 0, 0, -9.29848, 0, 0, 0, 37.3932])) + self.assertEqual(len(root_node.children()[3].children()[0].children()), 1) + self.assertEqual(root_node.children()[3].children()[0].children()[0].parentNode(), root_node.children()[3].children()[0]) + self.assertEqual(root_node.children()[3].children()[0].children()[0].contentUri(), temp_dir + '/LOD-0/Mesh-XL-YL.b3dm') + self.assertEqual(root_node.children()[3].children()[0].children()[0].geometricError(), 0) + self.assertEqual(root_node.children()[3].children()[0].children()[0].refinementProcess(), Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[3].children()[0].children()[0].boundingVolume().box(), + QgsOrientedBox3D([-47.7663, 0.067987, -29.9984], [46.3085, 0, 0, 0, -9.29848, 0, 0, 0, 37.3932])) + self.assertFalse( + len(root_node.children()[3].children()[0].children()[0].children())) + root_node = index.getNodes(QgsTiledMeshRequest()) + self.assertFalse(root_node.parentNode()) + self.assertFalse(root_node.contentUri()) + self.assertEqual(root_node.geometricError(), 100.0) + self.assertEqual(root_node.refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.boundingVolume().box(), + QgsOrientedBox3D([-1.45782, 0.265355, 7.44958], + [94.1946, 0, 0, 0, -14.9309, 0, + 0, 0, 75.0565])) + + self.assertEqual(len(root_node.children()), 4) + self.assertEqual(root_node.children()[0].parentNode(), root_node) + self.assertEqual(root_node.children()[0].contentUri(), + temp_dir + '/LOD-2/Mesh-XR-YR.b3dm') + self.assertEqual(root_node.children()[0].geometricError(), 9.1) + self.assertEqual(root_node.children()[0].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[0].boundingVolume().box(), + QgsOrientedBox3D([44.4926, -2.87012, 39.2136], + [45.9504, 0, 0, 0, -8.28534, 0, + 0, 0, 31.8188])) + + self.assertEqual(len(root_node.children()[0].children()), 1) + self.assertEqual( + root_node.children()[0].children()[0].parentNode(), + root_node.children()[0]) + self.assertEqual( + root_node.children()[0].children()[0].contentUri(), + temp_dir + '/LOD-1/Mesh-XR-YR.b3dm') + self.assertEqual( + root_node.children()[0].children()[0].geometricError(), 3) + self.assertEqual( + root_node.children()[0].children()[0].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual( + root_node.children()[0].children()[0].boundingVolume().box(), + QgsOrientedBox3D([44.4926, -2.89708, 39.2136], + [45.9504, 0, 0, 0, -8.31229, 0, 0, 0, + 31.8188])) + self.assertEqual( + len(root_node.children()[0].children()[0].children()), 1) + self.assertEqual(root_node.children()[0].children()[0].children()[ + 0].parentNode(), + root_node.children()[0].children()[0]) + self.assertEqual(root_node.children()[0].children()[0].children()[ + 0].contentUri(), + temp_dir + '/LOD-0/Mesh-XR-YR.b3dm') + self.assertEqual(root_node.children()[0].children()[0].children()[ + 0].geometricError(), 0) + self.assertEqual(root_node.children()[0].children()[0].children()[ + 0].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[0].children()[0].children()[ + 0].boundingVolume().box(), + QgsOrientedBox3D([44.4926, -2.89708, 39.2136], + [45.9504, 0, 0, 0, -8.31229, 0, + 0, 0, 31.8188])) + self.assertFalse( + len(root_node.children()[0].children()[0].children()[ + 0].children())) + + self.assertEqual(root_node.children()[1].parentNode(), root_node) + self.assertEqual(root_node.children()[1].contentUri(), + temp_dir + '/LOD-2/Mesh-XL-YR.b3dm') + self.assertEqual(root_node.children()[1].geometricError(), 9.1) + self.assertEqual(root_node.children()[1].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[1].boundingVolume().box(), + QgsOrientedBox3D([-48.5551, 5.67839, 44.9504], + [47.0973, 0, 0, 0, -9.5179, 0, 0, + 0, 37.5556])) + + self.assertEqual(len(root_node.children()[1].children()), 1) + self.assertEqual( + root_node.children()[1].children()[0].parentNode(), + root_node.children()[1]) + self.assertEqual( + root_node.children()[1].children()[0].contentUri(), + temp_dir + '/LOD-1/Mesh-XL-YR.b3dm') + self.assertEqual( + root_node.children()[1].children()[0].geometricError(), 3) + self.assertEqual( + root_node.children()[1].children()[0].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual( + root_node.children()[1].children()[0].boundingVolume().box(), + QgsOrientedBox3D([-48.5551, 5.7113, 44.9504], + [47.0973, 0, 0, 0, -9.48498, 0, 0, 0, + 37.5556])) + self.assertEqual( + len(root_node.children()[1].children()[0].children()), 1) + self.assertEqual(root_node.children()[1].children()[0].children()[ + 0].parentNode(), + root_node.children()[1].children()[0]) + self.assertEqual(root_node.children()[1].children()[0].children()[ + 0].contentUri(), + temp_dir + '/LOD-0/Mesh-XL-YR.b3dm') + self.assertEqual(root_node.children()[1].children()[0].children()[ + 0].geometricError(), 0) + self.assertEqual(root_node.children()[1].children()[0].children()[ + 0].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[1].children()[0].children()[ + 0].boundingVolume().box(), + QgsOrientedBox3D([-48.5551, 5.7113, 44.9504], + [47.0973, 0, 0, 0, -9.48498, 0, + 0, 0, 37.5556])) + self.assertFalse( + len(root_node.children()[1].children()[0].children()[ + 0].children())) + + self.assertEqual(root_node.children()[2].parentNode(), root_node) + self.assertEqual(root_node.children()[2].contentUri(), + temp_dir + '/LOD-2/Mesh-XR-YL.b3dm') + self.assertEqual(root_node.children()[2].geometricError(), 9.0) + self.assertEqual(root_node.children()[2].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[2].boundingVolume().box(), + QgsOrientedBox3D([45.6395, -2.2089, -30.1061], + [47.0973, 0, 0, 0, -12.4567, 0, + 0, 0, 37.5008])) + self.assertEqual(len(root_node.children()[2].children()), 1) + self.assertEqual( + root_node.children()[2].children()[0].parentNode(), + root_node.children()[2]) + self.assertEqual( + root_node.children()[2].children()[0].contentUri(), + temp_dir + '/LOD-1/Mesh-XR-YL.b3dm') + self.assertEqual( + root_node.children()[2].children()[0].geometricError(), 3) + self.assertEqual( + root_node.children()[2].children()[0].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual( + root_node.children()[2].children()[0].boundingVolume().box(), + QgsOrientedBox3D([45.6395, -2.2089, -30.1061], + [47.0973, 0, 0, 0, -12.4567, 0, 0, 0, + 37.5008])) + self.assertEqual( + len(root_node.children()[2].children()[0].children()), 1) + self.assertEqual(root_node.children()[2].children()[0].children()[ + 0].parentNode(), + root_node.children()[2].children()[0]) + self.assertEqual(root_node.children()[2].children()[0].children()[ + 0].contentUri(), + temp_dir + '/LOD-0/Mesh-XR-YL.b3dm') + self.assertEqual(root_node.children()[2].children()[0].children()[ + 0].geometricError(), 0) + self.assertEqual(root_node.children()[2].children()[0].children()[ + 0].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[2].children()[0].children()[ + 0].boundingVolume().box(), + QgsOrientedBox3D([45.6395, -2.2089, -30.1061], + [47.0973, 0, 0, 0, -12.4567, 0, + 0, 0, 37.5008])) + self.assertFalse( + len(root_node.children()[2].children()[0].children()[ + 0].children())) + + self.assertEqual(root_node.children()[3].parentNode(), root_node) + self.assertEqual(root_node.children()[3].contentUri(), + temp_dir + '/LOD-2/Mesh-XL-YL.b3dm') + self.assertEqual(root_node.children()[3].geometricError(), 9.1) + self.assertEqual(root_node.children()[3].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[3].boundingVolume().box(), + QgsOrientedBox3D([-47.7663, 0.128709, -29.9984], + [46.3085, 0, 0, 0, -9.23776, 0, + 0, 0, 37.3932])) + self.assertEqual(len(root_node.children()[3].children()), 1) + self.assertEqual( + root_node.children()[3].children()[0].parentNode(), + root_node.children()[3]) + self.assertEqual( + root_node.children()[3].children()[0].contentUri(), + temp_dir + '/LOD-1/Mesh-XL-YL.b3dm') + self.assertEqual( + root_node.children()[3].children()[0].geometricError(), 3) + self.assertEqual( + root_node.children()[3].children()[0].refinementProcess(), + Qgis.TileRefinementProcess.Replacement) + self.assertEqual( + root_node.children()[3].children()[0].boundingVolume().box(), + QgsOrientedBox3D([-47.7663, 0.067987, -29.9984], + [46.3085, 0, 0, 0, -9.29848, 0, 0, 0, + 37.3932])) + self.assertEqual( + len(root_node.children()[3].children()[0].children()), 1) + self.assertEqual(root_node.children()[3].children()[0].children()[ + 0].parentNode(), + root_node.children()[3].children()[0]) + self.assertEqual(root_node.children()[3].children()[0].children()[ + 0].contentUri(), + temp_dir + '/LOD-0/Mesh-XL-YL.b3dm') + self.assertEqual(root_node.children()[3].children()[0].children()[ + 0].geometricError(), 0) + self.assertEqual(root_node.children()[3].children()[0].children()[ + 0].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[3].children()[0].children()[ + 0].boundingVolume().box(), + QgsOrientedBox3D([-47.7663, 0.067987, -29.9984], + [46.3085, 0, 0, 0, -9.29848, 0, + 0, 0, 37.3932])) + self.assertFalse( + len(root_node.children()[3].children()[0].children()[ + 0].children())) + + # request with geometric error set + request = QgsTiledMeshRequest() + request.setRequiredGeometricError(11) + + root_node = index.getNodes(request) + self.assertFalse(root_node.parentNode()) + self.assertFalse(root_node.contentUri()) + self.assertEqual(root_node.geometricError(), 100.0) + self.assertEqual(root_node.refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.boundingVolume().box(), + QgsOrientedBox3D([-1.45782, 0.265355, 7.44958], + [94.1946, 0, 0, 0, -14.9309, 0, + 0, 0, 75.0565])) + + self.assertEqual(len(root_node.children()), 4) + self.assertEqual(root_node.children()[0].parentNode(), root_node) + self.assertEqual(root_node.children()[0].contentUri(), + temp_dir + '/LOD-2/Mesh-XR-YR.b3dm') + self.assertEqual(root_node.children()[0].geometricError(), 9.1) + self.assertEqual(root_node.children()[0].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[0].boundingVolume().box(), + QgsOrientedBox3D([44.4926, -2.87012, 39.2136], + [45.9504, 0, 0, 0, -8.28534, 0, + 0, 0, 31.8188])) + + self.assertFalse(root_node.children()[0].children()) + + self.assertEqual(root_node.children()[1].parentNode(), root_node) + self.assertEqual(root_node.children()[1].contentUri(), + temp_dir + '/LOD-2/Mesh-XL-YR.b3dm') + self.assertEqual(root_node.children()[1].geometricError(), 9.1) + self.assertEqual(root_node.children()[1].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[1].boundingVolume().box(), + QgsOrientedBox3D([-48.5551, 5.67839, 44.9504], + [47.0973, 0, 0, 0, -9.5179, 0, 0, + 0, 37.5556])) + + self.assertFalse(root_node.children()[1].children()) + + self.assertEqual(root_node.children()[2].parentNode(), root_node) + self.assertEqual(root_node.children()[2].contentUri(), + temp_dir + '/LOD-2/Mesh-XR-YL.b3dm') + self.assertEqual(root_node.children()[2].geometricError(), 9.0) + self.assertEqual(root_node.children()[2].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[2].boundingVolume().box(), + QgsOrientedBox3D([45.6395, -2.2089, -30.1061], + [47.0973, 0, 0, 0, -12.4567, 0, + 0, 0, 37.5008])) + self.assertFalse(root_node.children()[2].children()) + + self.assertEqual(root_node.children()[3].parentNode(), root_node) + self.assertEqual(root_node.children()[3].contentUri(), + temp_dir + '/LOD-2/Mesh-XL-YL.b3dm') + self.assertEqual(root_node.children()[3].geometricError(), 9.1) + self.assertEqual(root_node.children()[3].refinementProcess(), + Qgis.TileRefinementProcess.Additive) + self.assertEqual(root_node.children()[3].boundingVolume().box(), + QgsOrientedBox3D([-47.7663, 0.128709, -29.9984], + [46.3085, 0, 0, 0, -9.23776, 0, + 0, 0, 37.3932])) + self.assertFalse(root_node.children()[3].children()) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgstiledmeshrequest.py b/tests/src/python/test_qgstiledmeshrequest.py new file mode 100644 index 000000000000..195a1c323f6e --- /dev/null +++ b/tests/src/python/test_qgstiledmeshrequest.py @@ -0,0 +1,44 @@ +"""QGIS Unit tests for QgsTiledMeshRequest + +From build dir, run: ctest -R QgsTiledMeshRequest -V + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = '(C) 2023 by Nyall Dawson' +__date__ = '26/07/2023' +__copyright__ = 'Copyright 2023, The QGIS Project' + +import math +import qgis # NOQA +from qgis.core import ( + QgsTiledMeshRequest, + QgsFeedback +) +import unittest +from qgis.testing import start_app, QgisTestCase + +from utilities import unitTestDataPath + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsTiledMeshRequest(QgisTestCase): + + def test_basic(self): + request = QgsTiledMeshRequest() + + request.setRequiredGeometricError(1.2) + self.assertEqual(request.requiredGeometricError(), 1.2) + + self.assertIsNone(request.feedback()) + feedback = QgsFeedback() + request.setFeedback(feedback) + self.assertEqual(request.feedback(), feedback) + + +if __name__ == '__main__': + unittest.main()