Compare commits

...

21 Commits

Author SHA1 Message Date
Sandro Mani
b55d99a9e0
Merge ff94f3a2ce9978d88d9cfda379c3b2749e8e3acc into 8779a4ea7161a303bfd2ebb556c651f4f36cb807 2025-10-02 12:46:03 +00:00
Sandro Mani
ff94f3a2ce Pass encoded sanitized layer typeName in DescribeFeatureType link returned by QgsWfs3DescribeCollectionHandler 2025-10-02 14:45:41 +02:00
Sandro Mani
5230a7d73c Set typename instead of typenames in DescribeFeatureType URL returned by QgsWfs3DescribeCollectionHandler 2025-10-02 14:45:41 +02:00
Sandro Mani
246a898499 Fix extension of GEOJSON link in QgsWfs3CollectionsHandler and QgsWfs3DescribeCollectionHandler 2025-10-02 14:45:41 +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
Alessandro Pasotti
24862b27af [mssql] Fix curvepolygon hidden in browser
Fix #63365
2025-10-02 09:54:09 +10:00
dependabot[bot]
d2f68454cc Bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 09:19:34 +10:00
dependabot[bot]
a2c5d4bdf0 Bump actions/github-script from 7 to 8
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 09:19:05 +10:00
dependabot[bot]
b09b717a33 Bump actions/labeler from 5 to 6
Bumps [actions/labeler](https://github.com/actions/labeler) from 5 to 6.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 09:18:27 +10:00
dependabot[bot]
055401fde1 Bump actions/stale from 9 to 10
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v9...v10)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '10'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 09:17:39 +10:00
dependabot[bot]
4b8616b4f2 Bump tj-actions/changed-files from 46.0.5 to 47.0.0
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 46.0.5 to 47.0.0.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](ed68ef82c0...24d32ffd49)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 47.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 09:16:27 +10:00
31 changed files with 2218 additions and 60 deletions

View File

@ -54,7 +54,7 @@ jobs:
echo $(brew --prefix flex)/bin >> $GITHUB_PATH
echo $(brew --prefix libtool)/bin >> $GITHUB_PATH
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: '3.11'

View File

@ -27,7 +27,7 @@ jobs:
steps:
- name: 'Download artifact'
id: download_artifact
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
@ -71,7 +71,7 @@ jobs:
unzip data-*.zip
- name: 'Post artifact download link as comment on PR'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@ -18,7 +18,7 @@ jobs:
run: |
python ./scripts/get_latest_qgis_versions.py --release="stable" --github_token=${{ secrets.GITHUB_TOKEN }} >> $GITHUB_ENV
- name: Write comment
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const {ISSUE_BODY, QGIS_VERSION_LTR_PATCH, QGIS_VERSION_STABLE_PATCH} = process.env // Latest released version identified using get_latest_qgis_versions

View File

@ -25,7 +25,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install requirements
@ -162,7 +162,7 @@ jobs:
silversearcher-ag
- name: Retrieve changed files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c #v46
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 #v46
id: changed_files
with:
separator: " "
@ -177,7 +177,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install Requirements
@ -212,7 +212,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Run Check

View File

@ -15,6 +15,6 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
- uses: actions/labeler@v6
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -24,7 +24,7 @@ jobs:
fetch-depth: 200
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.13'
@ -66,7 +66,7 @@ jobs:
- name: Listen for `/fix-precommit` comment
if: failure() && github.event_name == 'issue_comment' && github.event.comment.body == '/fix-precommit' && github.event.issue.pull_request
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@ -88,7 +88,7 @@ jobs:
- name: Comment on PR if pre-commit failed
if: failure() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@ -12,7 +12,7 @@ jobs:
runs-on: [ubuntu-latest]
steps:
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: '3.13'

View File

@ -14,7 +14,7 @@ jobs:
if: github.repository_owner == 'qgis'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-pr-message: >

View File

@ -24,7 +24,7 @@ jobs:
steps:
- name: 'Download artifact'
id: download_artifact
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
@ -58,7 +58,7 @@ jobs:
- name: 'Post test report markdown summary as comment on PR'
if: fromJSON(steps.download_artifact.outputs.artifact_id) > 0
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

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

@ -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

@ -149,7 +149,7 @@ QVector<QgsDataItem *> QgsMssqlConnectionItem::createChildren()
}
// build sql statement
QString query = QgsMssqlConnection::buildQueryForTables( mName );
const QString query = QgsMssqlConnection::buildQueryForTables( mName );
const bool disableInvalidGeometryHandling = QgsMssqlConnection::isInvalidGeometryHandlingDisabled( mName );
@ -477,19 +477,16 @@ QgsMssqlLayerItem *QgsMssqlSchemaItem::addLayer( const QgsMssqlLayerProperty &la
QString tip = tr( "%1 as %2 in %3" ).arg( layerProperty.geometryColName, QgsWkbTypes::displayString( wkbType ), layerProperty.srid );
Qgis::BrowserLayerType layerType;
Qgis::WkbType flatType = QgsWkbTypes::flatType( wkbType );
switch ( flatType )
const Qgis::GeometryType geomType = QgsWkbTypes::geometryType( wkbType );
switch ( geomType )
{
case Qgis::WkbType::Point:
case Qgis::WkbType::MultiPoint:
case Qgis::GeometryType::Point:
layerType = Qgis::BrowserLayerType::Point;
break;
case Qgis::WkbType::LineString:
case Qgis::WkbType::MultiLineString:
case Qgis::GeometryType::Line:
layerType = Qgis::BrowserLayerType::Line;
break;
case Qgis::WkbType::Polygon:
case Qgis::WkbType::MultiPolygon:
case Qgis::GeometryType::Polygon:
layerType = Qgis::BrowserLayerType::Polygon;
break;
default:

View File

@ -371,7 +371,7 @@ void QgsWfs3CollectionsHandler::handleRequest( const QgsServerApiContext &contex
} } }
},
{ "links", {
{ { "href", href( context, QStringLiteral( "/%1/items" ).arg( shortName ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::JSON ) ) }, { "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::items ) }, { "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::GEOJSON ) }, { "title", title + " as GeoJSON" } }, { { "href", href( context, QStringLiteral( "/%1/items" ).arg( shortName ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::HTML ) ) }, { "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::items ) }, { "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::HTML ) }, { "title", title + " as HTML" } } /* TODO: not sure what these "concepts" are about, neither if they are mandatory
{ { "href", href( context, QStringLiteral( "/%1/items" ).arg( shortName ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::GEOJSON ) ) }, { "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::items ) }, { "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::GEOJSON ) }, { "title", title + " as GeoJSON" } }, { { "href", href( context, QStringLiteral( "/%1/items" ).arg( shortName ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::HTML ) ) }, { "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::items ) }, { "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::HTML ) }, { "title", title + " as HTML" } } /* TODO: not sure what these "concepts" are about, neither if they are mandatory
{
{ "href", href( api, context.request(), QStringLiteral( "/%1/concepts" ).arg( shortName ) ) },
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::item ) },
@ -442,9 +442,10 @@ void QgsWfs3DescribeCollectionHandler::handleRequest( const QgsServerApiContext
const std::string title { mapLayer->serverProperties()->wfsTitle().isEmpty() ? mapLayer->name().toStdString() : mapLayer->serverProperties()->wfsTitle().toStdString() };
const std::string itemsTitle { title + " items" };
const QString shortName { mapLayer->serverProperties()->shortName().isEmpty() ? mapLayer->name() : mapLayer->serverProperties()->shortName() };
const QString typeName = QString( shortName ).replace( ' ', '_' ).replace( ':', '-' ).replace( QChar( 0x2014 ) /* em-dash */, '-' );
json linksList = links( context );
linksList.push_back(
{ { "href", href( context, QStringLiteral( "/items" ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::JSON ) ) },
{ { "href", href( context, QStringLiteral( "/items" ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::GEOJSON ) ) },
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::items ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::GEOJSON ) },
{ "title", itemsTitle + " as " + QgsServerOgcApi::contentTypeToStdString( QgsServerOgcApi::ContentType::GEOJSON ) }
@ -460,7 +461,7 @@ void QgsWfs3DescribeCollectionHandler::handleRequest( const QgsServerApiContext
);
linksList.push_back(
{ { "href", parentLink( context.request()->url(), 3 ).toStdString() + "?request=DescribeFeatureType&typenames=" + QUrlQuery( shortName ).toString( QUrl::EncodeSpaces ).toStdString() + "&service=WFS&version=2.0"
{ { "href", parentLink( context.request()->url(), 3 ).toStdString() + "?request=DescribeFeatureType&typename=" + QUrlQuery( typeName ).toString( QUrl::EncodeSpaces ).toStdString() + "&service=WFS&version=2.0"
},
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::describedBy ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::XML ) },

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)",
)

View File

@ -38,7 +38,7 @@ Content-Type: application/json
"type": "text/html"
},
{
"href": "http://server.qgis.org/wfs3/collections/layer1_with_short_name/items.json",
"href": "http://server.qgis.org/wfs3/collections/layer1_with_short_name/items.geojson",
"rel": "items",
"title": "A Layer1 with a short name items as GEOJSON",
"type": "application/geo+json"
@ -50,7 +50,7 @@ Content-Type: application/json
"type": "text/html"
},
{
"href": "http://server.qgis.org/?request=DescribeFeatureType&typenames=layer1_with_short_name&service=WFS&version=2.0",
"href": "http://server.qgis.org/?request=DescribeFeatureType&typename=layer1_with_short_name&service=WFS&version=2.0",
"rel": "describedBy",
"title": "Schema for A Layer1 with a short name",
"type": "application/xml"
@ -58,4 +58,4 @@ Content-Type: application/json
],
"timeStamp": "2019-07-05T12:27:07Z",
"title": "A Layer1 with a short name"
}
}

View File

@ -43,7 +43,7 @@ Content-Type: application/json
"type": "text/html"
},
{
"href": "http://server.qgis.org/wfs3/collections/points/items.json",
"href": "http://server.qgis.org/wfs3/collections/points/items.geojson",
"rel": "items",
"title": "points items as GEOJSON",
"type": "application/geo+json"
@ -55,7 +55,7 @@ Content-Type: application/json
"type": "text/html"
},
{
"href": "http://server.qgis.org/?request=DescribeFeatureType&typenames=points&service=WFS&version=2.0",
"href": "http://server.qgis.org/?request=DescribeFeatureType&typename=points&service=WFS&version=2.0",
"rel": "describedBy",
"title": "Schema for points",
"type": "application/xml"
@ -63,4 +63,4 @@ Content-Type: application/json
],
"timeStamp": "2019-07-05T12:27:07Z",
"title": "points"
}
}

View File

@ -27,11 +27,11 @@
<link rel="self" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9.html" title="Feature collection as HTML" type="text/html">
<link rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.json" title="A test wfs vector layer èé items as GEOJSON" type="application/geo+json">
<link rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.geojson" title="A test wfs vector layer èé items as GEOJSON" type="application/geo+json">
<link rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.html" title="A test wfs vector layer èé items as HTML" type="text/html">
<link rel="describedBy" href="http://server.qgis.org/?request=DescribeFeatureType&typenames=testlayer%20èé&service=WFS&version=2.0" title="Schema for A test wfs vector layer èé" type="application/xml">
<link rel="describedBy" href="http://server.qgis.org/?request=DescribeFeatureType&typename=testlayer_èé&service=WFS&version=2.0" title="Schema for A test wfs vector layer èé" type="application/xml">
@ -113,7 +113,7 @@
<li><a rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.json">A test wfs vector layer èé items as GEOJSON</a></li>
<li><a rel="items" href="http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.geojson">A test wfs vector layer èé items as GEOJSON</a></li>
@ -121,7 +121,7 @@
<li><a rel="describedBy" href="http://server.qgis.org/?request=DescribeFeatureType&typenames=testlayer%20èé&service=WFS&version=2.0">Schema for A test wfs vector layer èé</a></li>
<li><a rel="describedBy" href="http://server.qgis.org/?request=DescribeFeatureType&typenames=testlayer_èé&service=WFS&version=2.0">Schema for A test wfs vector layer èé</a></li>
</ul>

View File

@ -38,7 +38,7 @@ Content-Type: application/json
"type": "text/html"
},
{
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.json",
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.geojson",
"rel": "items",
"title": "A test wfs vector layer èé items as GEOJSON",
"type": "application/geo+json"
@ -50,7 +50,7 @@ Content-Type: application/json
"type": "text/html"
},
{
"href": "http://server.qgis.org/?request=DescribeFeatureType&typenames=testlayer%20èé&service=WFS&version=2.0",
"href": "http://server.qgis.org/?request=DescribeFeatureType&typename=testlayer_èé&service=WFS&version=2.0",
"rel": "describedBy",
"title": "Schema for A test wfs vector layer èé",
"type": "application/xml"

View File

@ -29,7 +29,7 @@ Content-Type: application/json
"id": "testlayer èé",
"links": [
{
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.json",
"href": "http://server.qgis.org/wfs3/collections/testlayer%20%C3%A8%C3%A9/items.geojson",
"rel": "items",
"title": "A test wfs vector layer èé as GeoJSON",
"type": "application/geo+json"
@ -70,7 +70,7 @@ Content-Type: application/json
"id": "layer1_with_short_name",
"links": [
{
"href": "http://server.qgis.org/wfs3/collections/layer1_with_short_name/items.json",
"href": "http://server.qgis.org/wfs3/collections/layer1_with_short_name/items.geojson",
"rel": "items",
"title": "A Layer1 with a short name as GeoJSON",
"type": "application/geo+json"
@ -111,7 +111,7 @@ Content-Type: application/json
"id": "exclude_attribute",
"links": [
{
"href": "http://server.qgis.org/wfs3/collections/exclude_attribute/items.json",
"href": "http://server.qgis.org/wfs3/collections/exclude_attribute/items.geojson",
"rel": "items",
"title": "A test vector layer exclude attrs as GeoJSON",
"type": "application/geo+json"
@ -152,7 +152,7 @@ Content-Type: application/json
"id": "fields_alias",
"links": [
{
"href": "http://server.qgis.org/wfs3/collections/fields_alias/items.json",
"href": "http://server.qgis.org/wfs3/collections/fields_alias/items.geojson",
"rel": "items",
"title": "A test vector layer with aliases as GeoJSON",
"type": "application/geo+json"
@ -187,4 +187,4 @@ Content-Type: application/json
}
],
"timeStamp": "2019-07-05T12:27:07Z"
}
}