Compare commits

...

16 Commits

Author SHA1 Message Date
Nyall Dawson
66499106a7
Merge 2c26d2f0dde07534bd801b6ed8bdaf6d0554d927 into 8779a4ea7161a303bfd2ebb556c651f4f36cb807 2025-10-02 14:55:53 +02:00
github-actions[bot]
8779a4ea71 auto-fix pre-commit issues 2025-10-02 11:25:13 +00:00
Martin Dobias
5912a6a69d Review from Benoit 2025-10-02 13:24:05 +02:00
Martin Dobias
bbbe278b22 fix windows build 2025-10-02 13:24:05 +02:00
Martin Dobias
412045d752 get rid of duplicate code 2025-10-02 13:24:05 +02:00
Martin Dobias
cb744707bd fixes 2025-10-02 13:24:05 +02:00
Martin Dobias
6edfbdd66c review from Stefanos 2025-10-02 13:24:05 +02:00
Martin Dobias
f6b55063c4 Add tests for I3S data provider and its index implementation 2025-10-02 13:24:05 +02:00
Martin Dobias
8c0d221f22 Make I3S parsing a bit more robust 2025-10-02 13:24:05 +02:00
Martin Dobias
4f33b6703f fix provider registry test 2025-10-02 13:24:05 +02:00
Martin Dobias
4b6afa446d sip, doc, unity, indentation fixes 2025-10-02 13:24:05 +02:00
Martin Dobias
cbe1a74224 Add ESRI I3S data provider + 2D/3D rendering of it 2025-10-02 13:24:05 +02:00
Nyall Dawson
2c26d2f0dd
Test mask updates 2025-10-02 09:15:16 +10:00
Nyall Dawson
408b461f18
Add test masks 2025-10-02 09:14:17 +10:00
Nyall Dawson
c8aac03410
Test mask update 2025-10-02 09:14:17 +10:00
Nyall Dawson
52f0b15807
[3d] Improve quality of billboard rendering
Add transparency layer to QgsPoint3DBillboardMaterial entities,
so that they get rendered in the transparent pipeline instead
of the opaque pipeline. Adjust alpha discard threshold in billboard
fragment shader to permit semi-opaque pixel rendering.

Pros: nicely smoothed billboard symbol rendering, without harsh
aliased edges. Billboard symbols can use semi-transparent colors.

Cons: Introduces ordering issues for billboards, where billboards
which should be shown behind others will appear in front from
some angles (i.e. the same issue as we get for 3d polygon objects
with transparency)
2025-10-02 09:14:13 +10:00
27 changed files with 2184 additions and 17 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -885,6 +885,14 @@ void Qgs3DMapScene::finalizeNewEntity( Qt3DCore::QEntity *newEntity )
}
}
}
else if ( QgsPoint3DBillboardMaterial *billboardMaterial = qobject_cast<QgsPoint3DBillboardMaterial *>( material ) )
{
Qt3DCore::QEntity *entity = qobject_cast<Qt3DCore::QEntity *>( billboardMaterial->parent() );
if ( entity && !entity->components().contains( transparentLayer ) )
{
entity->addComponent( transparentLayer );
}
}
else
{
// This handles the phong material with data defined properties, the textured case and point (instanced) symbols.

View File

@ -22,6 +22,7 @@
#include "qgscesiumutils.h"
#include "qgscoordinatetransform.h"
#include "qgsgeotransform.h"
#include "qgsgltfutils.h"
#include "qgsgltf3dutils.h"
#include "qgsquantizedmeshtiles.h"
#include "qgsraycastingutils_p.h"
@ -124,7 +125,7 @@ void QgsTiledSceneChunkLoader::start()
errors.append( QStringLiteral( "Failed to parse tile from '%1'" ).arg( uri ) );
}
}
else if ( format == "cesiumtiles" )
else if ( format == QLatin1String( "cesiumtiles" ) )
{
const QgsCesiumUtils::TileContents tileContent = QgsCesiumUtils::extractGltfFromTileContent( content );
if ( tileContent.gltf.isEmpty() )
@ -132,6 +133,21 @@ void QgsTiledSceneChunkLoader::start()
entityTransform.tileTransform.translate( tileContent.rtcCenter );
mEntity = QgsGltf3DUtils::gltfToEntity( tileContent.gltf, entityTransform, uri, &errors );
}
else if ( format == QLatin1String( "draco" ) )
{
QgsGltfUtils::I3SNodeContext i3sContext;
i3sContext.initFromTile( tile, mFactory.mLayerCrs, mFactory.mBoundsTransform.sourceCrs(), mFactory.mRenderContext.transformContext() );
QString dracoLoadError;
tinygltf::Model model;
if ( !QgsGltfUtils::loadDracoModel( content, i3sContext, model, &dracoLoadError ) )
{
errors.append( dracoLoadError );
return;
}
mEntity = QgsGltf3DUtils::parsedGltfToEntity( model, entityTransform, QString(), &errors );
}
else
return; // unsupported tile content type
@ -173,11 +189,19 @@ Qt3DCore::QEntity *QgsTiledSceneChunkLoader::createEntity( Qt3DCore::QEntity *pa
///
QgsTiledSceneChunkLoaderFactory::QgsTiledSceneChunkLoaderFactory( const Qgs3DRenderContext &context, const QgsTiledSceneIndex &index, QgsCoordinateReferenceSystem tileCrs, double zValueScale, double zValueOffset )
QgsTiledSceneChunkLoaderFactory::QgsTiledSceneChunkLoaderFactory(
const Qgs3DRenderContext &context,
const QgsTiledSceneIndex &index,
QgsCoordinateReferenceSystem tileCrs,
QgsCoordinateReferenceSystem layerCrs,
double zValueScale,
double zValueOffset
)
: mRenderContext( context )
, mIndex( index )
, mZValueScale( zValueScale )
, mZValueOffset( zValueOffset )
, mLayerCrs( layerCrs )
{
mBoundsTransform = QgsCoordinateTransform( tileCrs, context.crs(), context.transformContext() );
}
@ -346,8 +370,17 @@ void QgsTiledSceneChunkLoaderFactory::prepareChildren( QgsChunkNode *node )
///
QgsTiledSceneLayerChunkedEntity::QgsTiledSceneLayerChunkedEntity( Qgs3DMapSettings *map, const QgsTiledSceneIndex &index, QgsCoordinateReferenceSystem tileCrs, double maximumScreenError, bool showBoundingBoxes, double zValueScale, double zValueOffset )
: QgsChunkedEntity( map, maximumScreenError, new QgsTiledSceneChunkLoaderFactory( Qgs3DRenderContext::fromMapSettings( map ), index, tileCrs, zValueScale, zValueOffset ), true )
QgsTiledSceneLayerChunkedEntity::QgsTiledSceneLayerChunkedEntity(
Qgs3DMapSettings *map,
const QgsTiledSceneIndex &index,
QgsCoordinateReferenceSystem tileCrs,
QgsCoordinateReferenceSystem layerCrs,
double maximumScreenError,
bool showBoundingBoxes,
double zValueScale,
double zValueOffset
)
: QgsChunkedEntity( map, maximumScreenError, new QgsTiledSceneChunkLoaderFactory( Qgs3DRenderContext::fromMapSettings( map ), index, tileCrs, layerCrs, zValueScale, zValueOffset ), true )
, mIndex( index )
{
setShowBoundingBoxes( showBoundingBoxes );

View File

@ -84,8 +84,12 @@ class QgsTiledSceneChunkLoaderFactory : public QgsChunkLoaderFactory
Q_OBJECT
public:
QgsTiledSceneChunkLoaderFactory(
const Qgs3DRenderContext &context, const QgsTiledSceneIndex &index, QgsCoordinateReferenceSystem tileCrs,
double zValueScale, double zValueOffset
const Qgs3DRenderContext &context,
const QgsTiledSceneIndex &index,
QgsCoordinateReferenceSystem tileCrs,
QgsCoordinateReferenceSystem layerCrs,
double zValueScale,
double zValueOffset
);
virtual QgsChunkLoader *createChunkLoader( QgsChunkNode *node ) const override;
@ -104,6 +108,7 @@ class QgsTiledSceneChunkLoaderFactory : public QgsChunkLoaderFactory
double mZValueScale = 1.0;
double mZValueOffset = 0;
QgsCoordinateTransform mBoundsTransform;
QgsCoordinateReferenceSystem mLayerCrs;
QSet<long long> mPendingHierarchyFetches;
QSet<long long> mFutureHierarchyFetches;
};
@ -123,7 +128,7 @@ class QgsTiledSceneLayerChunkedEntity : public QgsChunkedEntity
{
Q_OBJECT
public:
explicit QgsTiledSceneLayerChunkedEntity( Qgs3DMapSettings *map, const QgsTiledSceneIndex &index, QgsCoordinateReferenceSystem tileCrs, double maximumScreenError, bool showBoundingBoxes, double zValueScale, double zValueOffset );
explicit QgsTiledSceneLayerChunkedEntity( Qgs3DMapSettings *map, const QgsTiledSceneIndex &index, QgsCoordinateReferenceSystem tileCrs, QgsCoordinateReferenceSystem layerCrs, double maximumScreenError, bool showBoundingBoxes, double zValueScale, double zValueOffset );
~QgsTiledSceneLayerChunkedEntity();

View File

@ -66,7 +66,7 @@ Qt3DCore::QEntity *QgsTiledSceneLayer3DRenderer::createEntity( Qgs3DMapSettings
QgsTiledSceneIndex index = tsl->dataProvider()->index();
return new QgsTiledSceneLayerChunkedEntity( map, index, tsl->dataProvider()->sceneCrs(), maximumScreenError(), showBoundingBoxes(), qgis::down_cast<const QgsTiledSceneLayerElevationProperties *>( tsl->elevationProperties() )->zScale(), qgis::down_cast<const QgsTiledSceneLayerElevationProperties *>( tsl->elevationProperties() )->zOffset() );
return new QgsTiledSceneLayerChunkedEntity( map, index, tsl->dataProvider()->sceneCrs(), tsl->dataProvider()->crs(), maximumScreenError(), showBoundingBoxes(), qgis::down_cast<const QgsTiledSceneLayerElevationProperties *>( tsl->elevationProperties() )->zScale(), qgis::down_cast<const QgsTiledSceneLayerElevationProperties *>( tsl->elevationProperties() )->zOffset() );
}
void QgsTiledSceneLayer3DRenderer::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const

View File

@ -7,9 +7,8 @@ out vec4 fragColor;
void main(void)
{
fragColor = texture(tex0, vec2(UV.x, 1.0f - UV.y));
if (fragColor.a < 0.5)
if (fragColor.a < 0.01)
discard;
}

View File

@ -401,6 +401,7 @@ set(QGIS_CORE_SRCS
tiledscene/qgscesiumtilesdataprovider.cpp
tiledscene/qgscesiumutils.cpp
tiledscene/qgsesrii3sdataprovider.cpp
tiledscene/qgsgltfutils.cpp
tiledscene/qgsquantizedmeshdataprovider.cpp
tiledscene/qgsquantizedmeshtiles.cpp
@ -2079,6 +2080,7 @@ set(QGIS_CORE_HDRS
tiledscene/qgscesiumtilesdataprovider.h
tiledscene/qgscesiumutils.h
tiledscene/qgsesrii3sdataprovider.h
tiledscene/qgsgltfutils.h
tiledscene/qgsquantizedmeshdataprovider.h
tiledscene/qgsquantizedmeshtiles.h

View File

@ -41,6 +41,7 @@
#include "qgsvtpkvectortiledataprovider.h"
#include "qgscesiumtilesdataprovider.h"
#include "qgsesrii3sdataprovider.h"
#include "qgstiledsceneprovidermetadata.h"
#ifdef HAVE_EPT
@ -245,6 +246,9 @@ void QgsProviderRegistry::init()
metadata = new QgsQuantizedMeshProviderMetadata();
mProviders[ metadata->key() ] = metadata;
metadata = new QgsEsriI3SProviderMetadata();
mProviders[ metadata->key() ] = metadata;
}
#ifdef HAVE_STATIC_PROVIDERS

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,96 @@
/***************************************************************************
qgsesrii3sdataprovider.h
--------------------------------------
Date : July 2025
Copyright : (C) 2025 by Martin Dobias
Email : wonder dot sk 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 QGSESRII3SDATAPROVIDER_H
#define QGSESRII3SDATAPROVIDER_H
#include "qgis_core.h"
#include "qgstiledscenedataprovider.h"
#include "qgis.h"
#include "qgsprovidermetadata.h"
#define SIP_NO_FILE
class QgsEsriI3SDataProviderSharedData;
///@cond PRIVATE
/**
* \ingroup core
* Data provider implementation for Esri I3S
* \since QGIS 4.0
*/
class CORE_EXPORT QgsEsriI3SDataProvider final: public QgsTiledSceneDataProvider
{
Q_OBJECT
public:
//! Constructor for QgsEsriI3SDataProvider
QgsEsriI3SDataProvider( const QString &uri,
const QgsDataProvider::ProviderOptions &providerOptions,
Qgis::DataProviderReadFlags flags = Qgis::DataProviderReadFlags() );
QgsEsriI3SDataProvider( const QgsEsriI3SDataProvider &other );
QgsEsriI3SDataProvider &operator=( const QgsEsriI3SDataProvider &other ) = delete;
~QgsEsriI3SDataProvider() final;
Qgis::DataProviderFlags flags() const override;
Qgis::TiledSceneProviderCapabilities capabilities() const final;
QgsEsriI3SDataProvider *clone() const final;
QgsCoordinateReferenceSystem crs() const final;
QgsRectangle extent() const final;
bool isValid() const final;
QString name() const final;
QString description() const final;
QString htmlMetadata() const final;
const QgsCoordinateReferenceSystem sceneCrs() const final;
const QgsTiledSceneBoundingVolume &boundingVolume() const final;
QgsTiledSceneIndex index() const final;
QgsDoubleRange zRange() const final;
private:
bool loadFromRestService( const QString &uri, json &layerJson, QString &i3sVersion );
bool loadFromSlpk( const QString &uri, json &layerJson, QString &i3sVersion );
bool checkI3SVersion( const QString &i3sVersion );
bool mIsValid = false;
std::shared_ptr<QgsEsriI3SDataProviderSharedData> mShared; //!< Mutable data shared between provider instances
};
/**
* \ingroup core
* Data provider metadata implementation for Esri I3S
* \since QGIS 4.0
*/
class QgsEsriI3SProviderMetadata : public QgsProviderMetadata
{
Q_OBJECT
public:
QgsEsriI3SProviderMetadata();
QIcon icon() const override;
QgsProviderMetadata::ProviderMetadataCapabilities capabilities() const override;
QgsEsriI3SDataProvider *createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, Qgis::DataProviderReadFlags flags = Qgis::DataProviderReadFlags() ) override;
QString filters( Qgis::FileFilterType type ) override;
ProviderCapabilities providerCapabilities() const override;
QList< Qgis::LayerType > supportedLayerTypes() const override;
};
///@endcond
#endif // QGSESRII3SDATAPROVIDER_H

View File

@ -19,6 +19,8 @@
#include "qgsmatrix4x4.h"
#include "qgsconfig.h"
#include "qgslogger.h"
#include "qgstiledscenetile.h"
#include "qgsziputils.h"
#include <QImage>
#include <QMatrix4x4>
@ -430,13 +432,32 @@ void dumpDracoModelInfo( draco::Mesh *dracoMesh )
bool QgsGltfUtils::loadDracoModel( const QByteArray &data, const I3SNodeContext &context, tinygltf::Model &model, QString *errors )
{
//
// SLPK and Extracted SLPK have the files gzipped
//
QByteArray dataExtracted;
if ( data.startsWith( QByteArray( "\x1f\x8b", 2 ) ) )
{
if ( !QgsZipUtils::decodeGzip( data, dataExtracted ) )
{
if ( errors )
*errors = "Failed to decode gzipped model";
return false;
}
}
else
{
dataExtracted = data;
}
//
// load the model in decoder and do basic sanity checks
//
draco::Decoder decoder;
draco::DecoderBuffer decoderBuffer;
decoderBuffer.Init( data.constData(), data.size() );
decoderBuffer.Init( dataExtracted.constData(), dataExtracted.size() );
draco::StatusOr<draco::EncodedGeometryType> geometryTypeStatus = decoder.GetEncodedGeometryType( &decoderBuffer );
if ( !geometryTypeStatus.ok() )
@ -790,4 +811,17 @@ bool QgsGltfUtils::writeGltfModel( const tinygltf::Model &model, const QString &
return res;
}
void QgsGltfUtils::I3SNodeContext::initFromTile( const QgsTiledSceneTile &tile, const QgsCoordinateReferenceSystem &layerCrs, const QgsCoordinateReferenceSystem &sceneCrs, const QgsCoordinateTransformContext &transformContext )
{
const QVariantMap tileMetadata = tile.metadata();
materialInfo = tileMetadata[QStringLiteral( "material" )].toMap();
isGlobalMode = sceneCrs.type() == Qgis::CrsType::Geocentric;
if ( isGlobalMode )
{
nodeCenterEcef = tile.boundingVolume().box().center();
datasetToSceneTransform = QgsCoordinateTransform( layerCrs, sceneCrs, transformContext );
}
}
///@endcond

View File

@ -41,6 +41,7 @@ class QMatrix4x4;
class QImage;
class QgsMatrix4x4;
class QgsTiledSceneTile;
class QgsVector3D;
namespace tinygltf
@ -173,8 +174,13 @@ class CORE_EXPORT QgsGltfUtils
* geometry of a I3S node.
* \since QGIS 4.0
*/
struct I3SNodeContext
struct CORE_EXPORT I3SNodeContext
{
//! Initialize the node content from tile's info
void initFromTile( const QgsTiledSceneTile &tile,
const QgsCoordinateReferenceSystem &layerCrs,
const QgsCoordinateReferenceSystem &sceneCrs,
const QgsCoordinateTransformContext &transformContext );
/**
* Material parsed from I3S material definition of the node. See

View File

@ -59,6 +59,7 @@ QgsTiledSceneLayerRenderer::QgsTiledSceneLayerRenderer( QgsTiledSceneLayer *laye
mRenderer.reset( layer->renderer()->clone() );
mSceneCrs = layer->dataProvider()->sceneCrs();
mLayerCrs = layer->dataProvider()->crs();
mClippingRegions = QgsMapClippingUtils::collectClippingRegionsForLayer( *renderContext(), layer );
mLayerBoundingVolume = layer->dataProvider()->boundingVolume();
@ -438,6 +439,21 @@ bool QgsTiledSceneLayerRenderer::renderTileContent( const QgsTiledSceneTile &til
}
if ( !res ) return false;
}
else if ( format == QLatin1String( "draco" ) )
{
QgsGltfUtils::I3SNodeContext i3sContext;
i3sContext.initFromTile( tile, mLayerCrs, mSceneCrs, context.renderContext().transformContext() );
QString errors;
if ( !QgsGltfUtils::loadDracoModel( tileContent, i3sContext, model, &errors ) )
{
if ( !mErrors.contains( errors ) )
mErrors.append( errors );
QgsDebugError( QStringLiteral( "Error raised reading %1: %2" )
.arg( contentUri, errors ) );
return false;
}
}
else
return false;

View File

@ -117,6 +117,7 @@ class CORE_EXPORT QgsTiledSceneLayerRenderer: public QgsMapLayerRenderer
QList< QgsMapClippingRegion > mClippingRegions;
QgsCoordinateReferenceSystem mSceneCrs;
QgsCoordinateReferenceSystem mLayerCrs;
QgsTiledSceneBoundingVolume mLayerBoundingVolume;
QgsTiledSceneIndex mIndex;

View File

@ -75,6 +75,7 @@ ADD_PYTHON_TEST(PyQgsElevationProfileManagerModel test_qgselevationprofilemanage
ADD_PYTHON_TEST(PyQgsElevationUtils test_qgselevationutils.py)
ADD_PYTHON_TEST(PyQgsEllipsoidUtils test_qgsellipsoidutils.py)
ADD_PYTHON_TEST(PyQgsEmbeddedSymbolRenderer test_qgsembeddedsymbolrenderer.py)
ADD_PYTHON_TEST(PyQgsEsriI3sLayer test_qgsesrii3slayer.py)
ADD_PYTHON_TEST(PyQgsExifTools test_qgsexiftools.py)
ADD_PYTHON_TEST(PyQgsExpression test_qgsexpression.py)
ADD_PYTHON_TEST(PyQgsExpressionPreviewWidget test_qgsexpressionpreviewwidget.py)

View File

@ -0,0 +1,756 @@
"""QGIS Unit tests for ESRI I3S tiled scene layer
.. 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__ = "Martin Dobias"
__date__ = "03/09/2025"
__copyright__ = "Copyright 2025, The QGIS Project"
import os
import tempfile
import gzip
from qgis.PyQt.QtCore import QUrl
from qgis.core import (
Qgis,
QgsTiledSceneLayer,
QgsCoordinateReferenceSystem,
QgsMatrix4x4,
QgsOrientedBox3D,
QgsTiledSceneRequest,
)
from qgis.testing import start_app, unittest
start_app()
def _make_tmp_eslpk_dataset(
temp_dir, layer_json_str, nodepage_json_str, i3s_version="1.8"
):
"""Creates files needed for a basic "Extracted SLPK" dataset"""
metadata_file = os.path.join(temp_dir, "metadata.json")
with open(metadata_file, "w", encoding="utf-8") as f:
f.write('{ "I3SVersion": "' + i3s_version + '" }')
layer_file = os.path.join(temp_dir, "3dSceneLayer.json.gz")
with gzip.open(layer_file, "wt", encoding="utf-8") as f:
f.write(layer_json_str)
nodepages_dir = os.path.join(temp_dir, "nodepages")
os.mkdir(nodepages_dir)
nodepage_0_file = os.path.join(nodepages_dir, "0.json.gz")
with gzip.open(nodepage_0_file, "wt", encoding="utf-8") as f:
f.write(nodepage_json_str)
class TestQgsEsriI3sLayer(unittest.TestCase):
def test_invalid_source(self):
layer = QgsTiledSceneLayer("file:///nope", "my layer", "esrii3s")
self.assertFalse(layer.dataProvider().isValid())
def test_invalid_json(self):
with tempfile.TemporaryDirectory() as temp_dir:
layer_json = """
{
"featurecollection": {}
}
"""
_make_tmp_eslpk_dataset(temp_dir, layer_json, "")
layer = QgsTiledSceneLayer("file://" + temp_dir, "my layer", "esrii3s")
self.assertFalse(layer.dataProvider().isValid())
self.assertEqual(
layer.error().summary(),
"Invalid I3S source: missing layer type.",
)
def test_old_i3s_version(self):
with tempfile.TemporaryDirectory() as temp_dir:
_make_tmp_eslpk_dataset(temp_dir, "", "", "1.6")
layer = QgsTiledSceneLayer("file://" + temp_dir, "my layer", "esrii3s")
self.assertFalse(layer.dataProvider().isValid())
self.assertEqual(
layer.error().summary(),
"Unsupported I3S version: 1.6",
)
def test_valid_global(self):
"""Test using a "global" dataset - i.e. using EPSG:4326"""
with tempfile.TemporaryDirectory(delete=False) as temp_dir:
layer_json = """
{
"id": 0,
"layerType": "IntegratedMesh",
"version": "1111-2222-3333",
"capabilities": ["View", "Query"],
"spatialReference": {
"wkid": 4326,
"latestWkid": 4326,
"vcsWkid": 3855,
"latestVcsWkid": 3855
},
"nodePages": {
"nodesPerPage": 64,
"lodSelectionMetricType": "maxScreenThresholdSQ"
}
}
"""
nodepage_json = """
{
"nodes": [
{
"index": 0,
"obb": {
"center": [-117.53759594675871, 34.12419117052764, 411.5930244093761],
"halfSize": [67.92003, 86.17007, 14.765519],
"quaternion": [0.206154981448711, 0.8528643536373323, 0.4652900171822784, 0.11673781661986028]
}
}
]
}
"""
_make_tmp_eslpk_dataset(temp_dir, layer_json, nodepage_json)
layer = QgsTiledSceneLayer("file://" + temp_dir, "my layer", "esrii3s")
self.assertTrue(layer.dataProvider().isValid())
self.assertEqual(layer.crs(), QgsCoordinateReferenceSystem("EPSG:4979"))
self.assertEqual(layer.dataProvider().sceneCrs().authid(), "EPSG:4978")
self.assertAlmostEqual(layer.extent().xMinimum(), -117.538339, 3)
self.assertAlmostEqual(layer.extent().xMaximum(), -117.536852, 3)
self.assertAlmostEqual(layer.extent().yMinimum(), 34.12340679, 3)
self.assertAlmostEqual(layer.extent().yMaximum(), 34.12497554, 3)
self.assertAlmostEqual(
layer.dataProvider().boundingVolume().box().centerX(),
-2443825.362862,
3,
)
self.assertAlmostEqual(
layer.dataProvider().boundingVolume().box().centerY(),
-4687033.280016,
3,
)
self.assertAlmostEqual(
layer.dataProvider().boundingVolume().box().centerZ(), 3558089.694926, 3
)
self.assertAlmostEqual(layer.dataProvider().zRange().lower(), 394.495, 3)
self.assertAlmostEqual(layer.dataProvider().zRange().upper(), 428.693, 3)
# check that version, tileset version, and z range are in html metadata
self.assertIn("1.8", layer.dataProvider().htmlMetadata())
self.assertIn("1111-2222-3333", layer.dataProvider().htmlMetadata())
self.assertIn("394.495 - 428.693", layer.dataProvider().htmlMetadata())
def test_valid_local(self):
"""Test using a "local" dataset - with projected CRS"""
with tempfile.TemporaryDirectory(delete=False) as temp_dir:
layer_json = """
{
"id": 0,
"layerType": "3DObject",
"version": "9FC7A46A-C550-4E1D-9001-DDCF825B5501",
"capabilities": ["View", "Query"],
"spatialReference": {
"wkid": 102067,
"latestWkid": 5514
},
"nodePages": {
"nodesPerPage": 64,
"lodSelectionMetricType": "maxScreenThresholdSQ"
},
"fullExtent": {
"xmin": -503064.4241950555,
"xmax": -490815.7221285819,
"ymin": -1209277.75240015844,
"ymax": -1199847.185454726,
"spatialReference": {
"wkid": 102067,
"latestWkid": 5514
},
"zmin": 197.438445862085445,
"zmax": 475.336097820065049
}
}
"""
nodepage_json = """
{
"nodes": [
{
"index": 0,
"obb" : {
"center": [-497670.6852373213, -1204678.5884475496, 295.70377745447672],
"halfSize": [6618.03271484375, 227.39961242675781, 4249.75927734375],
"quaternion": [0.12047340803502149, 0.70138504165507976, 0.6935764321960135, 0.11178959701679769]
}
}
]
}
"""
_make_tmp_eslpk_dataset(temp_dir, layer_json, nodepage_json)
layer = QgsTiledSceneLayer("file://" + temp_dir, "my layer", "esrii3s")
self.assertTrue(layer.dataProvider().isValid())
self.assertEqual(layer.crs(), QgsCoordinateReferenceSystem("EPSG:5514"))
self.assertEqual(layer.dataProvider().sceneCrs().authid(), "EPSG:5514")
self.assertAlmostEqual(layer.extent().xMinimum(), -503064.424195, 3)
self.assertAlmostEqual(layer.extent().xMaximum(), -490815.722128, 3)
self.assertAlmostEqual(layer.extent().yMinimum(), -1209277.752400, 3)
self.assertAlmostEqual(layer.extent().yMaximum(), -1199847.185454, 3)
self.assertAlmostEqual(
layer.dataProvider().boundingVolume().box().centerX(),
-497670.685237,
3,
)
self.assertAlmostEqual(
layer.dataProvider().boundingVolume().box().centerY(),
-1204678.588447,
3,
)
self.assertAlmostEqual(
layer.dataProvider().boundingVolume().box().centerZ(), 295.703777, 3
)
self.assertAlmostEqual(layer.dataProvider().zRange().lower(), 197.438445, 3)
self.assertAlmostEqual(layer.dataProvider().zRange().upper(), 475.336097, 3)
# check that version, tileset version, and z range are in html metadata
self.assertIn("1.8", layer.dataProvider().htmlMetadata())
self.assertIn(
"9FC7A46A-C550-4E1D-9001-DDCF825B5501",
layer.dataProvider().htmlMetadata(),
)
self.assertIn("197.438 - 475.336", layer.dataProvider().htmlMetadata())
def compare_boxes(self, box1: QgsOrientedBox3D, box2: QgsOrientedBox3D) -> bool:
"""
Compares two QgsOrientedBox3D objects within 4 decimal places
"""
fail_message = (
f"QgsOrientedBox3D([{box1.centerX():.4f}, {box1.centerY():.4f}, {box1.centerZ():.4f}], [{box1.halfAxes()[0]:.4f}, {box1.halfAxes()[1]:.4f},{box1.halfAxes()[2]:.4f},{box1.halfAxes()[3]:.4f},{box1.halfAxes()[4]:.4f},{box1.halfAxes()[5]:.4f},{box1.halfAxes()[6]:.4f},{box1.halfAxes()[7]:.4f},{box1.halfAxes()[8]:.4f}])"
"!="
f"QgsOrientedBox3D([{box2.centerX():.4f}, {box2.centerY():.4f}, {box2.centerZ():.4f}], [{box2.halfAxes()[0]:.4f}, {box2.halfAxes()[1]:.4f},{box2.halfAxes()[2]:.4f},{box2.halfAxes()[3]:.4f},{box2.halfAxes()[4]:.4f},{box2.halfAxes()[5]:.4f},{box2.halfAxes()[6]:.4f},{box2.halfAxes()[7]:.4f},{box2.halfAxes()[8]:.4f}])"
)
self.assertAlmostEqual(box1.centerX(), box2.centerX(), 4, fail_message)
self.assertAlmostEqual(box1.centerY(), box2.centerY(), 4, fail_message)
self.assertAlmostEqual(box1.centerZ(), box2.centerZ(), 4, fail_message)
self.assertAlmostEqual(box1.halfAxes()[0], box2.halfAxes()[0], 4, fail_message)
self.assertAlmostEqual(box1.halfAxes()[1], box2.halfAxes()[1], 4, fail_message)
self.assertAlmostEqual(box1.halfAxes()[2], box2.halfAxes()[2], 4, fail_message)
self.assertAlmostEqual(box1.halfAxes()[3], box2.halfAxes()[3], 4, fail_message)
self.assertAlmostEqual(box1.halfAxes()[4], box2.halfAxes()[4], 4, fail_message)
self.assertAlmostEqual(box1.halfAxes()[5], box2.halfAxes()[5], 4, fail_message)
self.assertAlmostEqual(box1.halfAxes()[6], box2.halfAxes()[6], 4, fail_message)
self.assertAlmostEqual(box1.halfAxes()[7], box2.halfAxes()[7], 4, fail_message)
self.assertAlmostEqual(box1.halfAxes()[8], box2.halfAxes()[8], 4, fail_message)
def compare_transforms(
self, transform1: QgsMatrix4x4, transform2: QgsMatrix4x4
) -> bool:
"""
Compares two QgsMatrix4x4 objects within 4 decimal places
"""
data1 = transform1.data()
data2 = transform2.data()
fail_message = (
f"QgsMatrix4x4({data1[0]:.4f}, {data1[4]:.4f}, {data1[8]:.4f}, {data1[12]:.4f}, "
f"{data1[1]:.4f}, {data1[5]:.4f}, {data1[9]:.4f}, {data1[13]:.4f}, "
f"{data1[2]:.4f}, {data1[6]:.4f}, {data1[10]:.4f}, {data1[14]:.4f}, "
f"{data1[3]:.4f}, {data1[7]:.4f}, {data1[11]:.4f}, {data1[15]:.4f})"
"!="
f"QgsMatrix4x4({data2[0]:.4f}, {data2[4]:.4f}, {data2[8]:.4f}, {data2[12]:.4f}, "
f"{data2[1]:.4f}, {data2[5]:.4f}, {data2[9]:.4f}, {data2[13]:.4f}, "
f"{data2[2]:.4f}, {data2[6]:.4f}, {data2[10]:.4f}, {data2[14]:.4f}, "
f"{data2[3]:.4f}, {data2[7]:.4f}, {data2[11]:.4f}, {data2[15]:.4f})"
)
for i in range(16):
self.assertAlmostEqual(data1[i], data2[i], 4, fail_message)
def test_index(self):
# TODO: use Rancho
with tempfile.TemporaryDirectory(delete=False) as temp_dir:
layer_json = """
{
"id": 0,
"layerType": "IntegratedMesh",
"version": "1111-2222-3333",
"capabilities": ["View", "Query"],
"spatialReference": {
"wkid": 4326,
"latestWkid": 4326,
"vcsWkid": 3855,
"latestVcsWkid": 3855
},
"nodePages": {
"nodesPerPage": 64,
"lodSelectionMetricType": "maxScreenThresholdSQ"
},
"materialDefinitions" : [{
"doubleSided" : true,
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"textureSetDefinitionId" : 0
},
"metallicFactor" : 0
}
}],
"textureSetDefinitions" : [{
"formats" : [{
"name" : "0",
"format" : "jpg"
},
{
"name" : "0_0_1",
"format" : "dds"
}]
}],
"geometryDefinitions" : [{
"geometryBuffers" : [{
"offset" : 8,
"position" : {
"type" : "Float32",
"component" : 3
},
"normal" : {
"type" : "Float32",
"component" : 3
},
"uv0" : {
"type" : "Float32",
"component" : 2
},
"color" : {
"type" : "UInt8",
"component" : 4
},
"featureId" : {
"type" : "UInt64",
"component" : 1,
"binding" : "per-feature"
},
"faceRange" : {
"type" : "UInt32",
"component" : 2,
"binding" : "per-feature"
}
},
{
"compressedAttributes" : {
"encoding" : "draco",
"attributes" : ["position", "uv0", "feature-index"]
}
}]
}]
}
"""
nodepage_json = """
{
"nodes" : [
{
"index" : 0,
"lodThreshold" : 196349.54374999998,
"obb" : {
"center" : [-117.53759594675871, 34.12419117052764, 411.5930244093761],
"halfSize" : [67.92003, 86.17007, 14.765519],
"quaternion" : [0.206154981448711, 0.8528643536373323, 0.4652900171822784, 0.11673781661986028]
},
"children" : [1, 2, 3, 4]
},
{
"index" : 1,
"parentIndex" : 0,
"lodThreshold" : 785398.1749999999,
"obb" : {
"center" : [-117.53793932189886, 34.123819641151094, 410.4816717179492],
"halfSize" : [34.66071, 43.807545, 8.943618],
"quaternion" : [-0.16656774214665032, -0.4782915919434134, 0.8335042596514344, 0.22082343511347818]
},
"mesh" : {
"material" : {
"definition" : 0,
"resource" : 16,
"texelCountHint" : 16777216
},
"geometry" : {
"definition" : 0,
"resource" : 16,
"vertexCount" : 60000,
"featureCount" : 1
}
},
"children" : [5, 6, 7, 8]
},
{
"index" : 2,
"parentIndex" : 0,
"lodThreshold" : 785398.1749999999,
"obb" : {
"center" : [-117.53724992475546, 34.123821545486685, 410.2505478467792],
"halfSize" : [9.228004, 34.19266, 44.047585],
"quaternion" : [0.8140355254912096, 0.5257760547630878, -0.216103082756028, 0.11918540640260444]
},
"mesh" : {
"material" : {
"definition" : 0,
"resource" : 37,
"texelCountHint" : 16777216
},
"geometry" : {
"definition" : 0,
"resource" : 37,
"vertexCount" : 59997,
"featureCount" : 1
}
}
},
{
"index" : 3,
"parentIndex" : 0,
"lodThreshold" : 785398.1749999999,
"obb" : {
"center" : [-117.53795948214312, 34.124566053867625, 412.3858846835792],
"halfSize" : [43.66336, 34.043926, 10.792544],
"quaternion" : [-0.397611945430886, -0.21999843379435893, 0.7659425991790682, -0.4547937606668636]
},
"mesh" : {
"material" : {
"definition" : 0,
"resource" : 58,
"texelCountHint" : 16777216
},
"geometry" : {
"definition" : 0,
"resource" : 58,
"vertexCount" : 59997,
"featureCount" : 1
}
}
},
{
"index" : 4,
"parentIndex" : 0,
"lodThreshold" : 785398.1749999999,
"obb" : {
"center" : [-117.53724435770368, 34.124570367218126, 414.2313824603334],
"halfSize" : [34.85811, 43.642643, 10.348813],
"quaternion" : [0.44530053235754224, -0.11174393593578129, -0.20527452317737305, 0.8643396894728205]
},
"mesh" : {
"material" : {
"definition" : 0,
"resource" : 79,
"texelCountHint" : 16777216
},
"geometry" : {
"definition" : 0,
"resource" : 79,
"vertexCount" : 60000,
"featureCount" : 1
}
}
},
{
"index" : 5,
"parentIndex" : 1,
"lodThreshold" : 1767145.8937499998,
"obb" : {
"center" : [-117.53812892736529, 34.12366471837075, 410.5171263786033],
"halfSize" : [16.485355, 16.677244, 1.7911408],
"quaternion" : [0.19655535620659972, 0.8626227061906642, 0.45742106616689565, 0.08952109772300766]
},
"mesh" : {
"material" : {
"definition" : 0,
"resource" : 0,
"texelCountHint" : 16777216
},
"geometry" : {
"definition" : 0,
"resource" : 0,
"vertexCount" : 13791,
"featureCount" : 1
}
}
},
{
"index" : 6,
"parentIndex" : 1,
"lodThreshold" : 1767145.8937499998,
"obb" : {
"center" : [-117.53777576262333, 34.12362478044946, 410.1134819108993],
"halfSize" : [21.521395, 3.2239213, 17.154518],
"quaternion" : [-0.33920143557919363, -0.6148235345765982, 0.04024529502917849, 0.7108549244816181]
},
"mesh" : {
"material" : {
"definition" : 0,
"resource" : 5,
"texelCountHint" : 16777216
},
"geometry" : {
"definition" : 0,
"resource" : 5,
"vertexCount" : 20673,
"featureCount" : 1
}
}
},
{
"index" : 7,
"parentIndex" : 1,
"lodThreshold" : 1767145.8937499998,
"obb" : {
"center" : [-117.53813350608226, 34.1240024065191, 411.1757797691971],
"halfSize" : [21.572693, 2.083206, 16.931368],
"quaternion" : [0.7021424175592919, 0.03536027529687852, 0.6126886538878836, 0.36105164421724356]
},
"mesh" : {
"material" : {
"definition" : 0,
"resource" : 10,
"texelCountHint" : 16777216
},
"geometry" : {
"definition" : 0,
"resource" : 10,
"vertexCount" : 40731,
"featureCount" : 1
}
}
},
{
"index" : 8,
"parentIndex" : 1,
"lodThreshold" : 1767145.8937499998,
"obb" : {
"center" : [-117.53777285367366, 34.12400549129491, 412.516752644442],
"halfSize" : [18.18257, 6.2166224, 22.5802],
"quaternion" : [0.9312857555826259, -0.2857478245548806, -0.04241912677533244, 0.22193611669728053]
},
"mesh" : {
"material" : {
"definition" : 0,
"resource" : 15,
"texelCountHint" : 16777216
},
"geometry" : {
"definition" : 0,
"resource" : 15,
"vertexCount" : 38001,
"featureCount" : 1
}
}
}
]}
"""
_make_tmp_eslpk_dataset(temp_dir, layer_json, nodepage_json)
layer = QgsTiledSceneLayer("file://" + temp_dir, "my layer", "esrii3s")
self.assertTrue(layer.dataProvider().isValid())
index = layer.dataProvider().index()
self.assertTrue(index.isValid())
root_tile = index.rootTile()
self.assertEqual(root_tile.id(), 0)
self.assertEqual(
root_tile.refinementProcess(), Qgis.TileRefinementProcess.Replacement
)
self.assertAlmostEqual(root_tile.geometricError(), 5.51488, 3)
self.assertEqual(root_tile.metadata(), {})
self.assertEqual(root_tile.resources(), {})
self.compare_boxes(
root_tile.boundingVolume().box(),
QgsOrientedBox3D(
[-2443825.3629, -4687033.2800, 3558089.6949],
[
-60.2956,
31.2621,
-0.4944,
20.9402,
41.5349,
72.5372,
5.7728,
11.0081,
-7.9698,
],
),
)
self.compare_transforms(
root_tile.transform(),
QgsMatrix4x4(
1.0,
0.0,
0.0,
-2443825.3629,
0.0,
1.0,
0.0,
-4687033.2800,
0.0,
0.0,
1.0,
3558089.6949,
0.0,
0.0,
0.0,
1.0,
),
)
self.assertEqual(index.parentTileId(root_tile.id()), -1)
self.assertEqual(index.childTileIds(root_tile.id()), [1, 2, 3, 4])
self.assertEqual(index.parentTileId(1), 0)
self.assertEqual(index.parentTileId(2), 0)
self.assertEqual(index.parentTileId(3), 0)
self.assertEqual(index.parentTileId(4), 0)
child_tile0 = index.getTile(1)
self.assertEqual(
child_tile0.resources(),
{"content": "file://" + temp_dir + "/nodes/16/geometries/1.bin.gz"},
)
self.assertEqual(
child_tile0.metadata(),
{
"contentFormat": "draco",
"gltfUpAxis": int(Qgis.Axis.Z),
"material": {
"doubleSided": True,
"pbrBaseColorFactor": [1.0, 1.0, 1.0, 1.0],
"pbrBaseColorTexture": "file://"
+ temp_dir
+ "/nodes/16/textures/0.jpg",
},
},
)
self.assertAlmostEqual(child_tile0.geometricError(), 1.40184, 3)
self.assertEqual(
child_tile0.refinementProcess(), Qgis.TileRefinementProcess.Replacement
)
self.compare_boxes(
child_tile0.boundingVolume().box(),
QgsOrientedBox3D(
[-2443863.7165, -4687038.3195, 3558054.9531],
[
-29.3571,
18.2818,
-2.3026,
-9.1461,
-19.4921,
-38.1511,
-4.3726,
-6.4730,
4.3554,
],
),
)
self.assertEqual(index.childTileIds(child_tile0.id()), [5, 6, 7, 8])
self.assertEqual(index.parentTileId(5), 1)
self.assertEqual(index.parentTileId(6), 1)
self.assertEqual(index.parentTileId(7), 1)
self.assertEqual(index.parentTileId(8), 1)
child_tile00 = index.getTile(5)
self.assertEqual(
child_tile00.resources(),
{"content": "file://" + temp_dir + "/nodes/0/geometries/1.bin.gz"},
)
self.assertAlmostEqual(child_tile00.geometricError(), 0.35578, 3)
self.assertEqual(
child_tile00.refinementProcess(), Qgis.TileRefinementProcess.Replacement
)
self.compare_boxes(
child_tile00.boundingVolume().box(),
QgsOrientedBox3D(
[-2443883.6980, -4687038.8068, 3558040.7461],
[
-14.9473,
6.9404,
0.4183,
4.2895,
8.4097,
13.7480,
0.5987,
1.3505,
-1.0129,
],
),
)
self.assertEqual(index.childTileIds(5), [])
child_tile1 = index.getTile(2)
self.assertEqual(
child_tile1.resources(),
{"content": "file://" + temp_dir + "/nodes/37/geometries/1.bin.gz"},
)
self.assertAlmostEqual(child_tile1.geometricError(), 1.40952, 3)
self.assertEqual(
child_tile1.refinementProcess(), Qgis.TileRefinementProcess.Replacement
)
self.compare_boxes(
child_tile1.boundingVolume().box(),
QgsOrientedBox3D(
[-2443807.1775, -4687067.4496, 3558054.9984],
[
3.2641,
7.4238,
-4.4032,
31.0303,
-14.3168,
-1.1352,
-9.9768,
-18.5566,
-38.6821,
],
),
)
self.assertEqual(index.childTileIds(2), [])
# getTiles() tests
# request to get tiles at max. resolution
# (nodes 0 and 1 are not present as they are replaced by children)
self.assertEqual(
index.getTiles(QgsTiledSceneRequest()), [5, 6, 7, 8, 2, 3, 4]
)
# request with coarse geometric error set
request = QgsTiledSceneRequest()
request.setRequiredGeometricError(10)
self.assertEqual(index.getTiles(request), [0])
# request with more detailed geometric error set
request = QgsTiledSceneRequest()
request.setRequiredGeometricError(5)
self.assertEqual(index.getTiles(request), [1, 2, 3, 4])
# restrict request to one parent tile
request = QgsTiledSceneRequest()
request.setParentTileId(1)
self.assertEqual(index.getTiles(request), [5, 6, 7, 8])
if __name__ == "__main__":
unittest.main()

View File

@ -51,7 +51,7 @@ class TestProviderTiledSceneMetadata(QgsProviderMetadata):
def filters(self, _type: Qgis.FileFilterType):
if _type == Qgis.FileFilterType.TiledScene:
return "Scene Layer Packages (*.slpk *.SLPK)"
return "Test Tiled Scene Filter (*.ttsf)"
class TestQgsProviderRegistry(QgisTestCase):
@ -299,18 +299,20 @@ class TestQgsProviderRegistry(QgisTestCase):
registry = QgsProviderRegistry.instance()
self.assertEqual(
registry.fileTiledSceneFilters(),
"All Supported Files (tileset.json TILESET.JSON);;"
"All Supported Files (tileset.json TILESET.JSON *.slpk *.SLPK);;"
"All Files (*.*);;"
"Cesium 3D Tiles (tileset.json TILESET.JSON)",
"Cesium 3D Tiles (tileset.json TILESET.JSON);;"
"ESRI Scene layer package (*.slpk *.SLPK)",
)
registry.registerProvider(TestProviderTiledSceneMetadata("slpk"))
self.assertEqual(
registry.fileTiledSceneFilters(),
"All Supported Files (tileset.json TILESET.JSON *.slpk *.SLPK);;"
"All Supported Files (tileset.json TILESET.JSON *.slpk *.SLPK *.ttsf);;"
"All Files (*.*);;"
"Cesium 3D Tiles (tileset.json TILESET.JSON);;"
"Scene Layer Packages (*.slpk *.SLPK)",
"ESRI Scene layer package (*.slpk *.SLPK);;"
"Test Tiled Scene Filter (*.ttsf)",
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB