mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-03 00:04:47 -04:00
Add ESRI I3S data provider + 2D/3D rendering of it
This commit is contained in:
parent
24862b27af
commit
cbe1a74224
120
images/themes/default/mIconEsriI3s.svg
Normal file
120
images/themes/default/mIconEsriI3s.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
@ -22,11 +22,13 @@
|
||||
#include "qgscesiumutils.h"
|
||||
#include "qgscoordinatetransform.h"
|
||||
#include "qgsgeotransform.h"
|
||||
#include "qgsgltfutils.h"
|
||||
#include "qgsgltf3dutils.h"
|
||||
#include "qgsquantizedmeshtiles.h"
|
||||
#include "qgsraycastingutils_p.h"
|
||||
#include "qgstiledsceneboundingvolume.h"
|
||||
#include "qgstiledscenetile.h"
|
||||
#include "qgsziputils.h"
|
||||
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
@ -132,6 +134,42 @@ void QgsTiledSceneChunkLoader::start()
|
||||
entityTransform.tileTransform.translate( tileContent.rtcCenter );
|
||||
mEntity = QgsGltf3DUtils::gltfToEntity( tileContent.gltf, entityTransform, uri, &errors );
|
||||
}
|
||||
else if ( format == "draco" )
|
||||
{
|
||||
// SLPK and Extracted SLPK have the files gzipped
|
||||
QByteArray contentExtracted;
|
||||
if ( content.startsWith( QByteArray( "\x1f\x8b", 2 ) ) )
|
||||
{
|
||||
if ( !QgsZipUtils::decodeGzip( content, contentExtracted ) )
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
contentExtracted = content;
|
||||
}
|
||||
|
||||
const QVariantMap tileMetadata = tile.metadata();
|
||||
QgsCoordinateReferenceSystem sceneCrs = mFactory.mBoundsTransform.sourceCrs();
|
||||
|
||||
QgsGltfUtils::I3SNodeContext i3sContext;
|
||||
i3sContext.materialInfo = tileMetadata["material"].toMap();
|
||||
i3sContext.isGlobalMode = sceneCrs.type() == Qgis::CrsType::Geocentric;
|
||||
if ( i3sContext.isGlobalMode )
|
||||
{
|
||||
i3sContext.nodeCenterEcef = tile.boundingVolume().box().center();
|
||||
i3sContext.datasetToSceneTransform = QgsCoordinateTransform( mFactory.mLayerCrs, sceneCrs, mFactory.mRenderContext.transformContext() );
|
||||
}
|
||||
|
||||
QString dracoLoadError;
|
||||
tinygltf::Model model;
|
||||
if ( !QgsGltfUtils::loadDracoModel( contentExtracted, i3sContext, model, &dracoLoadError ) )
|
||||
{
|
||||
errors.append( dracoLoadError );
|
||||
return;
|
||||
}
|
||||
|
||||
mEntity = QgsGltf3DUtils::parsedGltfToEntity( model, entityTransform, QString(), &errors );
|
||||
}
|
||||
else
|
||||
return; // unsupported tile content type
|
||||
|
||||
@ -173,13 +211,20 @@ 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 )
|
||||
{
|
||||
mBoundsTransform = QgsCoordinateTransform( tileCrs, context.crs(), context.transformContext() );
|
||||
mLayerCrs = layerCrs;
|
||||
}
|
||||
|
||||
QgsChunkLoader *QgsTiledSceneChunkLoaderFactory::createChunkLoader( QgsChunkNode *node ) const
|
||||
@ -346,8 +391,16 @@ 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 );
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
995
src/core/tiledscene/qgsesrii3sdataprovider.cpp
Normal file
995
src/core/tiledscene/qgsesrii3sdataprovider.cpp
Normal file
@ -0,0 +1,995 @@
|
||||
/***************************************************************************
|
||||
qgsesrii3sdataprovider.cpp
|
||||
--------------------------------------
|
||||
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. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "qgsesrii3sdataprovider.h"
|
||||
|
||||
#include "qgsapplication.h"
|
||||
#include "qgslogger.h"
|
||||
#include "qgsnetworkaccessmanager.h"
|
||||
#include "qgsreadwritelocker.h"
|
||||
#include "qgsthreadingutils.h"
|
||||
#include "qgssetrequestinitiator_p.h"
|
||||
#include "qgsziputils.h"
|
||||
|
||||
#include "qgstiledsceneboundingvolume.h"
|
||||
#include "qgstiledsceneindex.h"
|
||||
#include "qgstiledscenerequest.h"
|
||||
#include "qgstiledscenetile.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QIcon>
|
||||
#include <QQuaternion>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
|
||||
#define PROVIDER_KEY QStringLiteral( "esrii3s" )
|
||||
#define PROVIDER_DESCRIPTION QStringLiteral( "ESRI I3S data provider" )
|
||||
|
||||
|
||||
|
||||
class QgsEsriI3STiledSceneIndex final : public QgsAbstractTiledSceneIndex
|
||||
{
|
||||
public:
|
||||
|
||||
QgsEsriI3STiledSceneIndex(
|
||||
const json &tileset,
|
||||
const QUrl &rootUrl,
|
||||
const QgsCoordinateTransformContext &transformContext );
|
||||
|
||||
QgsTiledSceneTile rootTile() const final;
|
||||
QgsTiledSceneTile getTile( long long id ) final;
|
||||
long long parentTileId( long long id ) const final;
|
||||
QVector< long long > childTileIds( long long id ) const final;
|
||||
QVector< long long > getTiles( const QgsTiledSceneRequest &request ) final;
|
||||
Qgis::TileChildrenAvailability childAvailability( long long id ) const final;
|
||||
bool fetchHierarchy( long long id, QgsFeedback *feedback = nullptr ) final;
|
||||
|
||||
protected:
|
||||
|
||||
QByteArray fetchContent( const QString &uri, QgsFeedback *feedback = nullptr ) final;
|
||||
|
||||
private:
|
||||
|
||||
bool fetchNodePage( int nodePage, QgsFeedback *feedback = nullptr );
|
||||
void parseNodePage( const QByteArray &nodePageContent );
|
||||
|
||||
struct NodeDetails
|
||||
{
|
||||
long long parentNodeIndex;
|
||||
QVector<long long> childNodeIndexes;
|
||||
QgsTiledSceneTile tile;
|
||||
};
|
||||
|
||||
QVector<QString> mTextureSetFormats;
|
||||
QVector<QVariantMap> mMaterialDefinitions;
|
||||
|
||||
mutable QRecursiveMutex mLock;
|
||||
QUrl mRootUrl;
|
||||
QgsCoordinateTransformContext mTransformContext;
|
||||
long long mRootNodeIndex;
|
||||
int mNodesPerPage;
|
||||
bool mGlobalMode = false;
|
||||
QMap< long long, NodeDetails > mNodeMap;
|
||||
QSet<int> mCachedNodePages;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
class QgsEsriI3SDataProviderSharedData
|
||||
{
|
||||
public:
|
||||
QgsEsriI3SDataProviderSharedData();
|
||||
void initialize( const QString &i3sVersion,
|
||||
const json &layerJson,
|
||||
const QUrl &rootUrl,
|
||||
const QgsCoordinateTransformContext &transformContext );
|
||||
|
||||
QString mI3sVersion;
|
||||
json mLayerJson;
|
||||
|
||||
QgsCoordinateReferenceSystem mLayerCrs;
|
||||
QgsCoordinateReferenceSystem mSceneCrs;
|
||||
QgsTiledSceneBoundingVolume mBoundingVolume;
|
||||
|
||||
QgsRectangle mExtent;
|
||||
QgsDoubleRange mZRange;
|
||||
|
||||
QgsTiledSceneIndex mIndex;
|
||||
|
||||
QString mError;
|
||||
QReadWriteLock mReadWriteLock;
|
||||
|
||||
};
|
||||
|
||||
//
|
||||
// QgsEsriI3STiledSceneIndex
|
||||
//
|
||||
|
||||
QgsEsriI3STiledSceneIndex::QgsEsriI3STiledSceneIndex(
|
||||
const json &layerJson,
|
||||
const QUrl &rootUrl,
|
||||
const QgsCoordinateTransformContext &transformContext )
|
||||
: mRootUrl( rootUrl )
|
||||
, mTransformContext( transformContext )
|
||||
{
|
||||
mGlobalMode = layerJson["spatialReference"]["latestWkid"].get<int>() == 4326;
|
||||
|
||||
if ( layerJson.contains( "textureSetDefinitions" ) )
|
||||
{
|
||||
for ( auto textureSetDefinitionJson : layerJson["textureSetDefinitions"] )
|
||||
{
|
||||
QString formatType;
|
||||
for ( auto formatJson : textureSetDefinitionJson["formats"] )
|
||||
{
|
||||
QString formatName = QString::fromStdString( formatJson["name"].get<std::string>() );
|
||||
if ( formatName == "0" )
|
||||
{
|
||||
formatType = QString::fromStdString( formatJson["format"].get<std::string>() );
|
||||
break;
|
||||
}
|
||||
}
|
||||
mTextureSetFormats.append( formatType );
|
||||
}
|
||||
}
|
||||
|
||||
for ( auto materialDefinitionJson : layerJson["materialDefinitions"] )
|
||||
{
|
||||
QVariantMap materialDef;
|
||||
json pbrJson = materialDefinitionJson["pbrMetallicRoughness"];
|
||||
if ( pbrJson.contains( "baseColorFactor" ) )
|
||||
{
|
||||
json pbrBaseColorFactorJson = pbrJson["baseColorFactor"];
|
||||
materialDef["pbrBaseColorFactor"] = QVariantList
|
||||
{
|
||||
pbrBaseColorFactorJson[0].get<double>(),
|
||||
pbrBaseColorFactorJson[1].get<double>(),
|
||||
pbrBaseColorFactorJson[2].get<double>(),
|
||||
pbrBaseColorFactorJson[3].get<double>()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
materialDef["pbrBaseColorFactor"] = QVariantList{ 1.0, 1.0, 1.0, 1.0 };
|
||||
}
|
||||
if ( pbrJson.contains( "baseColorTexture" ) )
|
||||
{
|
||||
// but right now we only support png/jpg textures which have
|
||||
// hardcoded name "0" by the spec, and we use texture set definitions
|
||||
// only to figure out whether it is png or jpg
|
||||
int textureSetDefinitionId = pbrJson["baseColorTexture"]["textureSetDefinitionId"].get<int>();
|
||||
if ( textureSetDefinitionId < mTextureSetFormats.count() )
|
||||
{
|
||||
materialDef["pbrBaseColorTextureName"] = QString( "0" );
|
||||
materialDef["pbrBaseColorTextureFormat"] = mTextureSetFormats[textureSetDefinitionId];
|
||||
}
|
||||
else
|
||||
{
|
||||
QgsDebugError( QString( "referencing textureSetDefinition that does not exist! %1 ").arg( textureSetDefinitionId ) );
|
||||
}
|
||||
}
|
||||
if ( pbrJson.contains( "doubleSided" ) )
|
||||
{
|
||||
materialDef["doubleSided"] = materialDefinitionJson["doubleSided"].get<bool>();
|
||||
}
|
||||
|
||||
// there are various other properties that can be defined in a material,
|
||||
// but we do not support them: normal texture, occlusion texture, emissive texture,
|
||||
// emissive factor, alpha mode, alpha cutoff, cull face.
|
||||
|
||||
mMaterialDefinitions.append( materialDef );
|
||||
}
|
||||
|
||||
json nodePagesJson = layerJson["nodePages"];
|
||||
mNodesPerPage = nodePagesJson["nodesPerPage"].get<int>();
|
||||
mRootNodeIndex = nodePagesJson.contains( "rootIndex" ) ? nodePagesJson["rootIndex"].get<long long>() : 0;
|
||||
|
||||
int rootNodePage = static_cast<int>( mRootNodeIndex / mNodesPerPage );
|
||||
fetchNodePage( rootNodePage );
|
||||
}
|
||||
|
||||
QgsTiledSceneTile QgsEsriI3STiledSceneIndex::rootTile() const
|
||||
{
|
||||
QMutexLocker locker( &mLock );
|
||||
if ( !mNodeMap.contains( mRootNodeIndex ) )
|
||||
{
|
||||
QgsDebugError( "Unable to access the root tile!" );
|
||||
return QgsTiledSceneTile();
|
||||
}
|
||||
return mNodeMap[mRootNodeIndex].tile;
|
||||
}
|
||||
|
||||
QgsTiledSceneTile QgsEsriI3STiledSceneIndex::getTile( long long id )
|
||||
{
|
||||
QMutexLocker locker( &mLock );
|
||||
auto it = mNodeMap.constFind( id );
|
||||
if ( it != mNodeMap.constEnd() )
|
||||
{
|
||||
return it.value().tile;
|
||||
}
|
||||
|
||||
return QgsTiledSceneTile();
|
||||
}
|
||||
|
||||
long long QgsEsriI3STiledSceneIndex::parentTileId( long long id ) const
|
||||
{
|
||||
QMutexLocker locker( &mLock );
|
||||
auto it = mNodeMap.constFind( id );
|
||||
if ( it != mNodeMap.constEnd() )
|
||||
{
|
||||
return it.value().parentNodeIndex;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
QVector< long long > QgsEsriI3STiledSceneIndex::childTileIds( long long id ) const
|
||||
{
|
||||
QMutexLocker locker( &mLock );
|
||||
auto it = mNodeMap.constFind( id );
|
||||
if ( it != mNodeMap.constEnd() )
|
||||
{
|
||||
return it.value().childNodeIndexes;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QVector< long long > QgsEsriI3STiledSceneIndex::getTiles( const QgsTiledSceneRequest &request )
|
||||
{
|
||||
QVector< long long > results;
|
||||
|
||||
std::function< void( long long )> traverseNode;
|
||||
traverseNode = [&request, &traverseNode, &results, this]( long long nodeId )
|
||||
{
|
||||
QgsTiledSceneTile t = getTile( nodeId );
|
||||
if ( !request.filterBox().isNull() && !t.boundingVolume().intersects( request.filterBox() ) )
|
||||
return;
|
||||
|
||||
if ( request.requiredGeometricError() <= 0 || t.geometricError() <= 0 || t.geometricError() > request.requiredGeometricError() )
|
||||
{
|
||||
// need to go deeper, this tile does not have enough details
|
||||
|
||||
if ( childAvailability( t.id() ) == Qgis::TileChildrenAvailability::NeedFetching &&
|
||||
!( request.flags() & Qgis::TiledSceneRequestFlag::NoHierarchyFetch ) )
|
||||
{
|
||||
fetchHierarchy( t.id() );
|
||||
}
|
||||
|
||||
// now we should have children available (if any)
|
||||
auto it = mNodeMap.constFind( t.id() );
|
||||
for ( long long childId : it.value().childNodeIndexes )
|
||||
{
|
||||
traverseNode( childId );
|
||||
}
|
||||
|
||||
if ( it.value().childNodeIndexes.isEmpty() )
|
||||
{
|
||||
// there are no children available, so we use this tile even though we want more detail
|
||||
results << t.id();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// this tile has error sufficiently low so that we do not need to traverse
|
||||
// the node tree further down
|
||||
results << t.id();
|
||||
}
|
||||
};
|
||||
|
||||
QMutexLocker locker( &mLock );
|
||||
long long startNodeId = request.parentTileId() == -1 ? mRootNodeIndex : request.parentTileId();
|
||||
traverseNode( startNodeId );
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Qgis::TileChildrenAvailability QgsEsriI3STiledSceneIndex::childAvailability( long long id ) const
|
||||
{
|
||||
QMutexLocker locker( &mLock );
|
||||
auto it = mNodeMap.constFind( id );
|
||||
if ( it == mNodeMap.constEnd() )
|
||||
{
|
||||
// we have no info about the node, so what we return is a bit arbitrary anyway
|
||||
return Qgis::TileChildrenAvailability::NoChildren;
|
||||
}
|
||||
|
||||
if ( it.value().childNodeIndexes.isEmpty() )
|
||||
{
|
||||
return Qgis::TileChildrenAvailability::NoChildren;
|
||||
}
|
||||
|
||||
for ( long long childId : it.value().childNodeIndexes )
|
||||
{
|
||||
if ( !mNodeMap.contains( childId ) )
|
||||
{
|
||||
// at least one child is missing from the node map
|
||||
return Qgis::TileChildrenAvailability::NeedFetching;
|
||||
}
|
||||
}
|
||||
return Qgis::TileChildrenAvailability::Available;
|
||||
}
|
||||
|
||||
bool QgsEsriI3STiledSceneIndex::fetchHierarchy( long long id, QgsFeedback *feedback )
|
||||
{
|
||||
QMutexLocker locker( &mLock );
|
||||
auto it = mNodeMap.constFind( id );
|
||||
if ( it == mNodeMap.constEnd() )
|
||||
return false;
|
||||
|
||||
// gather all the missing node pages to get information about child nodes
|
||||
QSet<int> nodePagesToFetch;
|
||||
for ( long long childId : it.value().childNodeIndexes )
|
||||
{
|
||||
int nodePageIndex = static_cast<int>( childId / mNodesPerPage );
|
||||
if ( !mCachedNodePages.contains( nodePageIndex ) )
|
||||
nodePagesToFetch.insert( nodePageIndex );
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
for ( int nodePage : nodePagesToFetch )
|
||||
{
|
||||
if ( !fetchNodePage( nodePage, feedback ) )
|
||||
{
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
QByteArray QgsEsriI3STiledSceneIndex::fetchContent( const QString &uri, QgsFeedback *feedback )
|
||||
{
|
||||
QUrl url( uri );
|
||||
if ( url.isLocalFile() && QFile::exists( url.toLocalFile() ) )
|
||||
{
|
||||
QFile file( url.toLocalFile() );
|
||||
if ( file.open( QIODevice::ReadOnly ) )
|
||||
{
|
||||
return file.readAll();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QNetworkRequest networkRequest = QNetworkRequest( QUrl( uri ) );
|
||||
QgsSetRequestInitiatorClass( networkRequest, QStringLiteral( "QgsEsriI3STiledSceneIndex" ) );
|
||||
networkRequest.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
|
||||
networkRequest.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
|
||||
|
||||
const QgsNetworkReplyContent reply = QgsNetworkAccessManager::instance()->blockingGet(
|
||||
networkRequest, QString(), false, feedback );
|
||||
return reply.content();
|
||||
}
|
||||
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
bool QgsEsriI3STiledSceneIndex::fetchNodePage( int nodePage, QgsFeedback *feedback )
|
||||
{
|
||||
QByteArray nodePageContent;
|
||||
if ( !mRootUrl.isLocalFile() )
|
||||
{
|
||||
QString uri = mRootUrl.toString() + QString( "/layers/0/nodepages/%1" ).arg( nodePage );
|
||||
nodePageContent = retrieveContent( uri, feedback );
|
||||
}
|
||||
else
|
||||
{
|
||||
QString uri = mRootUrl.toString() + QString( "/nodepages/%1.json.gz" ).arg( nodePage );
|
||||
QByteArray nodePageContentGzipped = retrieveContent( uri, feedback );
|
||||
|
||||
if ( !QgsZipUtils::decodeGzip( nodePageContentGzipped, nodePageContent ) )
|
||||
{
|
||||
QgsDebugError( "Failed to decompress node page content: " + uri );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
parseNodePage( nodePageContent );
|
||||
}
|
||||
catch ( json::exception &error )
|
||||
{
|
||||
QgsDebugError( QStringLiteral( "Error reading node page %1: %2" ).arg( nodePage ).arg( error.what() ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
mCachedNodePages.insert( nodePage );
|
||||
return true;
|
||||
}
|
||||
|
||||
static QgsOrientedBox3D parseBox( const json &box )
|
||||
{
|
||||
try
|
||||
{
|
||||
json center = box["center"];
|
||||
json halfSize = box["halfSize"];
|
||||
json quaternion = box["quaternion"]; // order is x, y, z, w
|
||||
|
||||
return QgsOrientedBox3D(
|
||||
QgsVector3D( center[0].get<double>(),
|
||||
center[1].get<double>(),
|
||||
center[2].get<double>() ),
|
||||
QgsVector3D( halfSize[0].get<double>(),
|
||||
halfSize[1].get<double>(),
|
||||
halfSize[2].get<double>() ),
|
||||
QQuaternion( static_cast<float>( quaternion[3].get<double>() ),
|
||||
static_cast<float>( quaternion[0].get<double>() ),
|
||||
static_cast<float>( quaternion[1].get<double>() ),
|
||||
static_cast<float>( quaternion[2].get<double>() ) ) );
|
||||
}
|
||||
catch ( nlohmann::json::exception & )
|
||||
{
|
||||
return QgsOrientedBox3D();
|
||||
}
|
||||
}
|
||||
|
||||
void QgsEsriI3STiledSceneIndex::parseNodePage( const QByteArray &nodePageContent )
|
||||
{
|
||||
const json nodePageJson = json::parse( nodePageContent.toStdString() );
|
||||
for ( const json &nodeJson : nodePageJson["nodes"] )
|
||||
{
|
||||
long long nodeIndex = nodeJson["index"].get<long long>();
|
||||
long long parentNodeIndex = nodeJson.contains( "parentIndex" ) ? nodeJson["parentIndex"].get<long long>() : -1;
|
||||
|
||||
QgsOrientedBox3D obb = parseBox( nodeJson["obb"] );
|
||||
|
||||
// OBB in global scene layers should be constructed in ECEF and its values are defined like this:
|
||||
// - center - X,Y - lon/lat in degrees, Z - elevation in meters
|
||||
// - half-size - in meters
|
||||
// - quaternion - in reference to ECEF coordinate system
|
||||
|
||||
// OBB in local scene layers should be constructed in the CRS of the layer
|
||||
// - center, half-size - in units of the CRS
|
||||
// - quaternion - in reference to CRS of the layer
|
||||
|
||||
if ( mGlobalMode )
|
||||
{
|
||||
QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( "EPSG:4979" ), QgsCoordinateReferenceSystem( "EPSG:4978" ), mTransformContext );
|
||||
QgsVector3D obbCenterEcef = ct.transform( obb.center() );
|
||||
obb = QgsOrientedBox3D( { obbCenterEcef.x(), obbCenterEcef.y(), obbCenterEcef.z() }, obb.halfAxesList() );
|
||||
}
|
||||
|
||||
double threshold = -1;
|
||||
if ( nodeJson.contains( "lodThreshold" ) )
|
||||
{
|
||||
double maxScreenThresholdSquared = nodeJson["lodThreshold"].get<double>();
|
||||
|
||||
// This conversion from "maxScreenThresholdSQ" to geometry error is copied from CesiumJS
|
||||
// implementation of I3S (the only difference is Cesium uses longest OBB axis length
|
||||
threshold = obb.longestSide() / sqrt( maxScreenThresholdSquared / ( M_PI / 4 ) ) * 16;
|
||||
}
|
||||
QVector<long long> childNodeIds;
|
||||
if ( nodeJson.contains( "children" ) )
|
||||
{
|
||||
for ( const json &childJson : nodeJson["children"] )
|
||||
{
|
||||
childNodeIds << childJson.get<long long>();
|
||||
}
|
||||
}
|
||||
|
||||
QgsTiledSceneTile t( nodeIndex );
|
||||
t.setBoundingVolume( obb );
|
||||
t.setGeometricError( threshold );
|
||||
|
||||
QgsMatrix4x4 transform;
|
||||
transform.translate( obb.center() );
|
||||
t.setTransform( transform );
|
||||
|
||||
if ( nodeJson.contains( "mesh" ) )
|
||||
{
|
||||
// parse geometry
|
||||
const json meshJson = nodeJson["mesh"];
|
||||
int geometryResource = meshJson["geometry"]["resource"].get<int>();
|
||||
QString geometryUri;
|
||||
if ( mRootUrl.isLocalFile() )
|
||||
geometryUri = mRootUrl.toString() + QString( "/nodes/%1/geometries/1.bin.gz" ).arg( geometryResource );
|
||||
else
|
||||
geometryUri = mRootUrl.toString() + QString( "/layers/0/nodes/%1/geometries/1" ).arg( geometryResource );
|
||||
|
||||
// parse material and related textures
|
||||
const json materialJson = meshJson["material"];
|
||||
int materialIndex = materialJson["definition"].get<int>();
|
||||
QVariantMap materialInfo = mMaterialDefinitions[materialIndex];
|
||||
if ( materialInfo.contains( "pbrBaseColorTextureName" ) )
|
||||
{
|
||||
QString textureName = materialInfo["pbrBaseColorTextureName"].toString();
|
||||
QString textureFormat = materialInfo["pbrBaseColorTextureFormat"].toString();
|
||||
materialInfo.remove( "pbrBaseColorTextureName" );
|
||||
materialInfo.remove( "pbrBaseColorTextureFormat" );
|
||||
|
||||
int textureResource = materialJson["resource"].get<int>();
|
||||
QString textureUri;
|
||||
if ( mRootUrl.isLocalFile() )
|
||||
textureUri = mRootUrl.toString() + QString( "/nodes/%1/textures/%2.%3" ).arg( textureResource ).arg( textureName, textureFormat );
|
||||
else
|
||||
textureUri = mRootUrl.toString() + QString( "/layers/0/nodes/%1/textures/%2" ).arg( textureResource ).arg( textureName );
|
||||
materialInfo["pbrBaseColorTexture"] = textureUri;
|
||||
}
|
||||
|
||||
t.setResources( { { QStringLiteral( "content" ), geometryUri } } );
|
||||
|
||||
QVariantMap metadata =
|
||||
{
|
||||
{ QStringLiteral( "gltfUpAxis" ), static_cast< int >( Qgis::Axis::Z ) },
|
||||
{ QStringLiteral( "contentFormat" ), QStringLiteral( "draco" ) },
|
||||
{ QStringLiteral( "material" ), materialInfo }
|
||||
};
|
||||
t.setMetadata( metadata );
|
||||
}
|
||||
|
||||
mNodeMap.insert( nodeIndex, { parentNodeIndex, childNodeIds, t } );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// QgsEsriI3SDataProviderSharedData
|
||||
//
|
||||
|
||||
QgsEsriI3SDataProviderSharedData::QgsEsriI3SDataProviderSharedData()
|
||||
: mIndex( QgsTiledSceneIndex( nullptr ) )
|
||||
{
|
||||
}
|
||||
|
||||
void QgsEsriI3SDataProviderSharedData::initialize(
|
||||
const QString &i3sVersion,
|
||||
const json &layerJson,
|
||||
const QUrl &rootUrl,
|
||||
const QgsCoordinateTransformContext &transformContext )
|
||||
{
|
||||
mI3sVersion = i3sVersion;
|
||||
mLayerJson = layerJson;
|
||||
|
||||
const json spatialReferenceJson = layerJson["spatialReference"];
|
||||
int epsgCode = spatialReferenceJson["latestWkid"].get<int>();
|
||||
|
||||
if ( epsgCode == 4326 )
|
||||
{
|
||||
// "global" mode
|
||||
|
||||
// TODO: elevation can be ellipsoidal or gravity-based!
|
||||
mLayerCrs = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4979" ) );
|
||||
mSceneCrs = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4978" ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
// "local" mode - using a projected CRS
|
||||
mLayerCrs = QgsCoordinateReferenceSystem( QString( "EPSG:%1" ).arg( epsgCode ) );
|
||||
mSceneCrs = mLayerCrs;
|
||||
}
|
||||
|
||||
mIndex = QgsTiledSceneIndex(
|
||||
new QgsEsriI3STiledSceneIndex(
|
||||
layerJson,
|
||||
rootUrl,
|
||||
transformContext
|
||||
)
|
||||
);
|
||||
|
||||
if ( layerJson.contains( "fullExtent" ) )
|
||||
{
|
||||
const json fullExtentJson = layerJson["fullExtent"];
|
||||
mExtent = QgsRectangle(
|
||||
fullExtentJson["xmin"].get<double>(),
|
||||
fullExtentJson["ymin"].get<double>(),
|
||||
fullExtentJson["xmax"].get<double>(),
|
||||
fullExtentJson["ymax"].get<double>() );
|
||||
mZRange = QgsDoubleRange(
|
||||
fullExtentJson["zmin"].get<double>(),
|
||||
fullExtentJson["zmax"].get<double>() );
|
||||
}
|
||||
else
|
||||
{
|
||||
QgsBox3D box = mIndex.rootTile().boundingVolume().bounds( QgsCoordinateTransform( mSceneCrs, mLayerCrs, transformContext ) );
|
||||
mExtent = box.toRectangle();
|
||||
mZRange = QgsDoubleRange( box.zMinimum(), box.zMaximum() );
|
||||
}
|
||||
|
||||
mBoundingVolume = mIndex.rootTile().boundingVolume();
|
||||
}
|
||||
|
||||
//
|
||||
// QgsEsriI3SDataProvider
|
||||
//
|
||||
|
||||
|
||||
QgsEsriI3SDataProvider::QgsEsriI3SDataProvider( const QString &uri,
|
||||
const QgsDataProvider::ProviderOptions &providerOptions,
|
||||
Qgis::DataProviderReadFlags flags )
|
||||
: QgsTiledSceneDataProvider( uri, providerOptions, flags )
|
||||
, mShared( std::make_shared< QgsEsriI3SDataProviderSharedData >() )
|
||||
{
|
||||
QUrl rootUrl;
|
||||
if ( uri.startsWith( "http" ) || uri.startsWith( "file" ) )
|
||||
{
|
||||
rootUrl = uri;
|
||||
}
|
||||
else
|
||||
{
|
||||
// when saved in project as relative path, we then get just the path... (TODO?)
|
||||
rootUrl = QUrl::fromLocalFile( uri );
|
||||
}
|
||||
|
||||
QString i3sVersion;
|
||||
json layerJson;
|
||||
if ( uri.startsWith( "http" ) )
|
||||
{
|
||||
if ( !loadFromRestService( uri, layerJson, i3sVersion ) )
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( !loadFromSlpk( uri, layerJson, i3sVersion ) )
|
||||
return;
|
||||
}
|
||||
|
||||
QString layerType = QString::fromStdString( layerJson["layerType"].get<std::string>() );
|
||||
if ( layerType != "3DObject" && layerType != "IntegratedMesh" )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Unsupported layer type: " ) + layerType, QStringLiteral( "I3S" ) ) );
|
||||
return;
|
||||
}
|
||||
|
||||
mShared->initialize( i3sVersion, layerJson, rootUrl, transformContext() );
|
||||
|
||||
if ( !mShared->mIndex.isValid() )
|
||||
{
|
||||
appendError( mShared->mError );
|
||||
return;
|
||||
}
|
||||
|
||||
mIsValid = true;
|
||||
}
|
||||
|
||||
|
||||
bool QgsEsriI3SDataProvider::loadFromRestService( const QString &uri, json &layerJson, QString &i3sVersion )
|
||||
{
|
||||
QNetworkRequest networkRequest = QNetworkRequest( QUrl( uri ) );
|
||||
QgsSetRequestInitiatorClass( networkRequest, QStringLiteral( "QgsEsriI3SDataProvider" ) );
|
||||
networkRequest.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
|
||||
networkRequest.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
|
||||
|
||||
const QgsNetworkReplyContent reply = QgsNetworkAccessManager::instance()->blockingGet( networkRequest );
|
||||
if ( reply.error() != QNetworkReply::NoError )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Failed to fetch layer metadata: " ) + networkRequest.url().toString(), QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
QByteArray sceneLayerContent = reply.content();
|
||||
|
||||
json serviceJson;
|
||||
try
|
||||
{
|
||||
serviceJson = json::parse( sceneLayerContent.toStdString() );
|
||||
}
|
||||
catch ( const json::parse_error & )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Unable to parse JSON: " ) + uri, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !serviceJson.contains( "serviceVersion" ) )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Missing I3S version: " ) + uri, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
i3sVersion = QString::fromStdString( serviceJson["serviceVersion"].get<std::string>() );
|
||||
if ( !checkI3SVersion( i3sVersion ) )
|
||||
return false;
|
||||
|
||||
if ( !serviceJson.contains( "layers" ) || !serviceJson["layers"].is_array() || serviceJson["layers"].size() < 1 )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Unable to get layer info: " ) + uri, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
layerJson = serviceJson["layers"][0];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QgsEsriI3SDataProvider::loadFromSlpk( const QString &uri, json &layerJson, QString &i3sVersion )
|
||||
{
|
||||
// Extracted SLPK = SLPK content extracted to a single directory
|
||||
|
||||
// TODO: add true SLPK support (all data packaged in a single ZIP file)
|
||||
|
||||
QUrl rootUrl( uri );
|
||||
|
||||
QString metadataFile = rootUrl.toLocalFile() + "/metadata.json";
|
||||
QFile fMetadata( metadataFile );
|
||||
if ( !fMetadata.open( QIODevice::ReadOnly ) )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Failed to read layer metadata: " ) + metadataFile, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
QByteArray metadataContent = fMetadata.readAll();
|
||||
|
||||
json metadataJson;
|
||||
try
|
||||
{
|
||||
metadataJson = json::parse( metadataContent.toStdString() );
|
||||
}
|
||||
catch ( const json::parse_error & )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Unable to parse metadata JSON: " ) + uri, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !metadataJson.contains( "I3SVersion" ) )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Missing I3S version: " ) + metadataFile, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
i3sVersion = QString::fromStdString( metadataJson["I3SVersion"].get<std::string>() );
|
||||
if ( !checkI3SVersion( i3sVersion ) )
|
||||
return false;
|
||||
|
||||
QString sceneLayerFile = rootUrl.toLocalFile() + "/3dSceneLayer.json.gz";
|
||||
QFile f( sceneLayerFile );
|
||||
if ( !f.open( QIODevice::ReadOnly ) )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Failed to read layer metadata: " ) + sceneLayerFile, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
QByteArray sceneLayerGzipped = f.readAll();
|
||||
QByteArray sceneLayerContent;
|
||||
if ( !QgsZipUtils::decodeGzip( sceneLayerGzipped, sceneLayerContent ) )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Failed to decode layer metadata: " ) + sceneLayerFile, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
layerJson = json::parse( sceneLayerContent.toStdString() );
|
||||
}
|
||||
catch ( const json::parse_error & )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Unable to parse JSON: " ) + uri, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QgsEsriI3SDataProvider::checkI3SVersion( const QString &i3sVersion )
|
||||
{
|
||||
// We support I3S version >= 1.7 released in 2019. Earlier versions
|
||||
// of the spec are much less efficient to work with (e.g. they do not
|
||||
// support node pages, no Draco compression of geometries)
|
||||
|
||||
// Note: for more confusion, OGC has different versioning of I3S.
|
||||
// ESRI I3S version 1.7 should be equivalent to OGC I3S version 1.3.
|
||||
// Fortunately OGC versioning is not really used anywhere (apart from OGC docs)
|
||||
// so we can ignore OGC I3S versions and use ESRI I3S version.
|
||||
|
||||
QStringList i3sVersionComponents = i3sVersion.split( '.' );
|
||||
if ( i3sVersionComponents.size() != 2 )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Unexpected I3S version format: " ) + i3sVersion, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
int i3sVersionMajor = i3sVersionComponents[0].toInt();
|
||||
int i3sVersionMinor = i3sVersionComponents[1].toInt();
|
||||
if ( i3sVersionMajor != 1 || ( i3sVersionMajor == 1 && i3sVersionMinor < 7 ) )
|
||||
{
|
||||
appendError( QgsErrorMessage( tr( "Unsupported I3S version: " ) + i3sVersion, QStringLiteral( "I3S" ) ) );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
QgsEsriI3SDataProvider::QgsEsriI3SDataProvider( const QgsEsriI3SDataProvider &other )
|
||||
: QgsTiledSceneDataProvider( other )
|
||||
, mIsValid( other.mIsValid )
|
||||
{
|
||||
QgsReadWriteLocker locker( other.mShared->mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
mShared = other.mShared;
|
||||
}
|
||||
|
||||
QgsEsriI3SDataProvider::~QgsEsriI3SDataProvider() = default;
|
||||
|
||||
Qgis::DataProviderFlags QgsEsriI3SDataProvider::flags() const
|
||||
{
|
||||
return Qgis::DataProviderFlag::FastExtent2D;
|
||||
}
|
||||
|
||||
Qgis::TiledSceneProviderCapabilities QgsEsriI3SDataProvider::capabilities() const
|
||||
{
|
||||
return Qgis::TiledSceneProviderCapabilities();
|
||||
}
|
||||
|
||||
QgsEsriI3SDataProvider *QgsEsriI3SDataProvider::clone() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
return new QgsEsriI3SDataProvider( *this );
|
||||
}
|
||||
|
||||
QgsCoordinateReferenceSystem QgsEsriI3SDataProvider::crs() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
|
||||
QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
return mShared->mLayerCrs;
|
||||
}
|
||||
|
||||
QgsRectangle QgsEsriI3SDataProvider::extent() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
|
||||
QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
return mShared->mExtent;
|
||||
}
|
||||
|
||||
bool QgsEsriI3SDataProvider::isValid() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
|
||||
return mIsValid;
|
||||
}
|
||||
|
||||
QString QgsEsriI3SDataProvider::name() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
|
||||
return PROVIDER_KEY;
|
||||
}
|
||||
|
||||
QString QgsEsriI3SDataProvider::description() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
|
||||
return QObject::tr( "ESRI I3S" );
|
||||
}
|
||||
|
||||
QString QgsEsriI3SDataProvider::htmlMetadata() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
|
||||
QString metadata;
|
||||
|
||||
QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
|
||||
metadata += QStringLiteral( "<tr><td class=\"highlight\">" ) % tr( "I3S Version" ) % QStringLiteral( "</td><td>%1</a>" ).arg( mShared->mI3sVersion ) % QStringLiteral( "</td></tr>\n" );
|
||||
|
||||
QString layerType = QString::fromStdString( mShared->mLayerJson["layerType"].get<std::string>() );
|
||||
metadata += QStringLiteral( "<tr><td class=\"highlight\">" ) % tr( "Layer Type" ) % QStringLiteral( "</td><td>%1</a>" ).arg( layerType ) % QStringLiteral( "</td></tr>\n" );
|
||||
|
||||
// [required] "The ID of the last update session in which any resource belonging to this layer has been updated."
|
||||
QString version = QString::fromStdString( mShared->mLayerJson["version"].get<std::string>() );
|
||||
metadata += QStringLiteral( "<tr><td class=\"highlight\">" ) % tr( "Version" ) % QStringLiteral( "</td><td>%1</a>" ).arg( version ) % QStringLiteral( "</td></tr>\n" );
|
||||
|
||||
// [optional] "The name of this layer."
|
||||
if ( mShared->mLayerJson.contains( "name" ) )
|
||||
{
|
||||
QString name = QString::fromStdString( mShared->mLayerJson["name"].get<std::string>() );
|
||||
metadata += QStringLiteral( "<tr><td class=\"highlight\">" ) % tr( "Name" ) % QStringLiteral( "</td><td>%1</a>" ).arg( name ) % QStringLiteral( "</td></tr>\n" );
|
||||
}
|
||||
// [optional] "The display alias to be used for this layer."
|
||||
if ( mShared->mLayerJson.contains( "alias" ) )
|
||||
{
|
||||
QString alias = QString::fromStdString( mShared->mLayerJson["alias"].get<std::string>() );
|
||||
metadata += QStringLiteral( "<tr><td class=\"highlight\">" ) % tr( "Alias" ) % QStringLiteral( "</td><td>%1</a>" ).arg( alias ) % QStringLiteral( "</td></tr>\n" );
|
||||
}
|
||||
// [optional] "Description string for this layer."
|
||||
if ( mShared->mLayerJson.contains( "description" ) )
|
||||
{
|
||||
QString description = QString::fromStdString( mShared->mLayerJson["description"].get<std::string>() );
|
||||
metadata += QStringLiteral( "<tr><td class=\"highlight\">" ) % tr( "Description" ) % QStringLiteral( "</td><td>%1</a>" ).arg( description ) % QStringLiteral( "</td></tr>\n" );
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
const QgsCoordinateReferenceSystem QgsEsriI3SDataProvider::sceneCrs() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
if ( !mShared )
|
||||
return QgsCoordinateReferenceSystem();
|
||||
|
||||
QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
return mShared->mSceneCrs;
|
||||
}
|
||||
|
||||
const QgsTiledSceneBoundingVolume &QgsEsriI3SDataProvider::boundingVolume() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
static QgsTiledSceneBoundingVolume nullVolume;
|
||||
if ( !mShared )
|
||||
return nullVolume;
|
||||
|
||||
QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
return mShared ? mShared->mBoundingVolume : nullVolume;
|
||||
}
|
||||
|
||||
QgsTiledSceneIndex QgsEsriI3SDataProvider::index() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
if ( !mShared )
|
||||
return QgsTiledSceneIndex( nullptr );
|
||||
|
||||
QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
return mShared->mIndex;
|
||||
}
|
||||
|
||||
QgsDoubleRange QgsEsriI3SDataProvider::zRange() const
|
||||
{
|
||||
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
||||
if ( !mShared )
|
||||
return QgsDoubleRange();
|
||||
|
||||
QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
|
||||
return mShared->mZRange;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// QgsEsriI3SProviderMetadata
|
||||
//
|
||||
|
||||
|
||||
QgsEsriI3SProviderMetadata::QgsEsriI3SProviderMetadata():
|
||||
QgsProviderMetadata( PROVIDER_KEY, PROVIDER_DESCRIPTION )
|
||||
{
|
||||
}
|
||||
|
||||
QIcon QgsEsriI3SProviderMetadata::icon() const
|
||||
{
|
||||
return QgsApplication::getThemeIcon( QStringLiteral( "mIconEsriI3s.svg" ) );
|
||||
}
|
||||
|
||||
QgsEsriI3SDataProvider *QgsEsriI3SProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, Qgis::DataProviderReadFlags flags )
|
||||
{
|
||||
return new QgsEsriI3SDataProvider( uri, options, flags );
|
||||
}
|
||||
|
||||
QString QgsEsriI3SProviderMetadata::filters( Qgis::FileFilterType type )
|
||||
{
|
||||
switch ( type )
|
||||
{
|
||||
case Qgis::FileFilterType::Vector:
|
||||
case Qgis::FileFilterType::Raster:
|
||||
case Qgis::FileFilterType::Mesh:
|
||||
case Qgis::FileFilterType::MeshDataset:
|
||||
case Qgis::FileFilterType::VectorTile:
|
||||
case Qgis::FileFilterType::PointCloud:
|
||||
return QString();
|
||||
|
||||
case Qgis::FileFilterType::TiledScene:
|
||||
return QObject::tr( "ESRI Scene layer package" ) + QStringLiteral( " (*.slpk *.SLPK)" );
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QgsProviderMetadata::ProviderCapabilities QgsEsriI3SProviderMetadata::providerCapabilities() const
|
||||
{
|
||||
return FileBasedUris;
|
||||
}
|
||||
|
||||
QList<Qgis::LayerType> QgsEsriI3SProviderMetadata::supportedLayerTypes() const
|
||||
{
|
||||
return { Qgis::LayerType::TiledScene };
|
||||
}
|
||||
|
||||
QgsProviderMetadata::ProviderMetadataCapabilities QgsEsriI3SProviderMetadata::capabilities() const
|
||||
{
|
||||
return QgsProviderMetadata::ProviderMetadataCapabilities();
|
||||
}
|
||||
|
||||
///@endcond
|
84
src/core/tiledscene/qgsesrii3sdataprovider.h
Normal file
84
src/core/tiledscene/qgsesrii3sdataprovider.h
Normal file
@ -0,0 +1,84 @@
|
||||
/***************************************************************************
|
||||
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"
|
||||
|
||||
class QgsEsriI3SDataProviderSharedData;
|
||||
|
||||
///@cond PRIVATE
|
||||
|
||||
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
|
||||
|
||||
};
|
||||
|
||||
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
|
@ -34,6 +34,7 @@
|
||||
#include "qgstextrenderer.h"
|
||||
#include "qgsruntimeprofiler.h"
|
||||
#include "qgsapplication.h"
|
||||
#include "qgsziputils.h"
|
||||
|
||||
#include <QMatrix4x4>
|
||||
#include <qglobal.h>
|
||||
@ -59,6 +60,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 +440,44 @@ bool QgsTiledSceneLayerRenderer::renderTileContent( const QgsTiledSceneTile &til
|
||||
}
|
||||
if ( !res ) return false;
|
||||
}
|
||||
else if ( format == QLatin1String( "draco" ) )
|
||||
{
|
||||
// SLPK and Extracted SLPK have the files gzipped
|
||||
QByteArray tileContentExtracted;
|
||||
if ( tileContent.startsWith( QByteArray( "\x1f\x8b", 2 ) ) )
|
||||
{
|
||||
if ( !QgsZipUtils::decodeGzip( tileContent, tileContentExtracted ) )
|
||||
{
|
||||
QgsDebugError( QStringLiteral( "Unable to decode gzipped data: %1" ).arg( contentUri ) );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tileContentExtracted = tileContent;
|
||||
}
|
||||
|
||||
const QVariantMap tileMetadata = tile.metadata();
|
||||
|
||||
QgsGltfUtils::I3SNodeContext i3sContext;
|
||||
i3sContext.materialInfo = tileMetadata["material"].toMap();
|
||||
i3sContext.isGlobalMode = mSceneCrs.type() == Qgis::CrsType::Geocentric;
|
||||
if ( i3sContext.isGlobalMode )
|
||||
{
|
||||
i3sContext.nodeCenterEcef = tile.boundingVolume().box().center();
|
||||
i3sContext.datasetToSceneTransform = QgsCoordinateTransform( mLayerCrs, mSceneCrs, context.renderContext().transformContext() );
|
||||
}
|
||||
|
||||
QString errors;
|
||||
if ( !QgsGltfUtils::loadDracoModel( tileContentExtracted, 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;
|
||||
|
||||
|
@ -117,6 +117,7 @@ class CORE_EXPORT QgsTiledSceneLayerRenderer: public QgsMapLayerRenderer
|
||||
QList< QgsMapClippingRegion > mClippingRegions;
|
||||
|
||||
QgsCoordinateReferenceSystem mSceneCrs;
|
||||
QgsCoordinateReferenceSystem mLayerCrs;
|
||||
QgsTiledSceneBoundingVolume mLayerBoundingVolume;
|
||||
QgsTiledSceneIndex mIndex;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user