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..894c534f8530 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,258 @@ #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 ); + } + +#if 0 + if ( json.contains( "children" ) ) + { + for ( const auto &childJson : json["children"] ) + { + addNodeFromJson( childJson, node ); + } + } + + if ( parent ) + parent->addChild( node ); +#endif + 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 +394,15 @@ void QgsCesiumTilesDataProviderSharedData::setTilesetContent( const QString &til QgsDebugError( QStringLiteral( "unsupported boundingVolume format" ) ); } } + + mIndex = QgsTiledMeshIndex( + new QgsCesiumTiledMeshIndex( + mTileset, + rootPath, + authCfg, + headers + ) + ); } } @@ -221,18 +472,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 +611,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()