QGIS/src/core/tiledscene/qgsesrii3sdataprovider.cpp
Martin Dobias cb744707bd fixes
2025-10-02 13:24:05 +02:00

1073 lines
34 KiB
C++

/***************************************************************************
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 "moc_qgsesrii3sdataprovider.cpp"
#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 I3S_PROVIDER_KEY QStringLiteral( "esrii3s" )
#define I3S_PROVIDER_DESCRIPTION QStringLiteral( "ESRI I3S data provider" )
///@cond PRIVATE
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 );
void parseMesh( QgsTiledSceneTile &t, const json &meshJson );
QVariantMap parseMaterialDefinition( const json &materialDefinitionJson );
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 )
{
try
{
mGlobalMode = layerJson["spatialReference"]["latestWkid"].get<int>() == 4326;
}
catch ( json::exception &error )
{
// "spatialReference" is not required, yet the spec does not say what should
// be the default - assuming global mode is the best we can do...
mGlobalMode = true;
}
if ( layerJson.contains( "textureSetDefinitions" ) )
{
for ( auto textureSetDefinitionJson : layerJson["textureSetDefinitions"] )
{
QString formatType;
for ( const json &formatJson : textureSetDefinitionJson["formats"] )
{
if ( formatJson["name"].get<std::string>() == "0" )
{
formatType = QString::fromStdString( formatJson["format"].get<std::string>() );
break;
}
}
mTextureSetFormats.append( formatType );
}
}
if ( layerJson.contains( "materialDefinitions" ) )
{
for ( const json &materialDefinitionJson : layerJson["materialDefinitions"] )
{
QVariantMap materialDef = parseMaterialDefinition( materialDefinitionJson );
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 );
}
QVariantMap QgsEsriI3STiledSceneIndex::parseMaterialDefinition( const json &materialDefinitionJson )
{
QVariantMap materialDef;
if ( materialDefinitionJson.contains( "pbrMetallicRoughness" ) )
{
const json pbrJson = materialDefinitionJson["pbrMetallicRoughness"];
if ( pbrJson.contains( "baseColorFactor" ) )
{
const json pbrBaseColorFactorJson = pbrJson["baseColorFactor"];
materialDef[QStringLiteral( "pbrBaseColorFactor" )] = QVariantList
{
pbrBaseColorFactorJson[0].get<double>(),
pbrBaseColorFactorJson[1].get<double>(),
pbrBaseColorFactorJson[2].get<double>(),
pbrBaseColorFactorJson[3].get<double>()
};
}
else
{
materialDef[QStringLiteral( "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
const int textureSetDefinitionId = pbrJson["baseColorTexture"]["textureSetDefinitionId"].get<int>();
if ( textureSetDefinitionId < mTextureSetFormats.count() )
{
materialDef[QStringLiteral( "pbrBaseColorTextureName" )] = QStringLiteral( "0" );
materialDef[QStringLiteral( "pbrBaseColorTextureFormat" )] = mTextureSetFormats[textureSetDefinitionId];
}
else
{
QgsDebugError( QString( "referencing textureSetDefinition that does not exist! %1 " ).arg( textureSetDefinitionId ) );
}
}
}
else
{
materialDef[QStringLiteral( "pbrBaseColorFactor" )] = QVariantList{ 1.0, 1.0, 1.0, 1.0 };
}
if ( materialDefinitionJson.contains( "doubleSided" ) )
{
materialDef[QStringLiteral( "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.
return materialDef;
}
QgsTiledSceneTile QgsEsriI3STiledSceneIndex::rootTile() const
{
QMutexLocker locker( &mLock );
if ( !mNodeMap.contains( mRootNodeIndex ) )
{
QgsDebugError( QStringLiteral( "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( url );
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() )
{
const QString uri = QStringLiteral( "%1/layers/0/nodepages/%2" ).arg( mRootUrl.toString() ).arg( nodePage );
nodePageContent = retrieveContent( uri, feedback );
}
else
{
const QString uri = QStringLiteral( "%1/nodepages/%2.json.gz" ).arg( mRootUrl.toString() ).arg( nodePage );
const QByteArray nodePageContentGzipped = retrieveContent( uri, feedback );
if ( !QgsZipUtils::decodeGzip( nodePageContentGzipped, nodePageContent ) )
{
QgsDebugError( QStringLiteral( "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::parseMesh( QgsTiledSceneTile &t, const json &meshJson )
{
if ( !meshJson.contains( "geometry" ) || !meshJson.contains( "material" ) )
return;
int geometryResource = meshJson["geometry"]["resource"].get<int>();
QString geometryUri;
if ( mRootUrl.isLocalFile() )
geometryUri = QStringLiteral( "%1/nodes/%2/geometries/1.bin.gz" ).arg( mRootUrl.toString() ).arg( geometryResource );
else
geometryUri = QStringLiteral( "%1/layers/0/nodes/%2/geometries/1" ).arg( mRootUrl.toString() ).arg( geometryResource );
// parse material and related textures
const json materialJson = meshJson["material"];
int materialIndex = materialJson["definition"].get<int>();
QVariantMap materialInfo;
if ( materialIndex >= 0 && materialIndex < mMaterialDefinitions.count() )
{
materialInfo = mMaterialDefinitions[materialIndex];
if ( materialInfo.contains( QStringLiteral( "pbrBaseColorTextureName" ) ) )
{
const QString textureName = materialInfo[QStringLiteral( "pbrBaseColorTextureName" )].toString();
const QString textureFormat = materialInfo[QStringLiteral( "pbrBaseColorTextureFormat" )].toString();
materialInfo.remove( QStringLiteral( "pbrBaseColorTextureName" ) );
materialInfo.remove( QStringLiteral( "pbrBaseColorTextureFormat" ) );
const int textureResource = materialJson["resource"].get<int>();
QString textureUri;
if ( mRootUrl.isLocalFile() )
textureUri = QStringLiteral( "%1/nodes/%2/textures/%3.%4" ).arg( mRootUrl.toString() ).arg( textureResource ).arg( textureName, textureFormat );
else
textureUri = QStringLiteral( "%1/layers/0/nodes/%2/textures/%3" ).arg( mRootUrl.toString() ).arg( textureResource ).arg( textureName );
materialInfo[QStringLiteral( "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 );
}
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( QStringLiteral( "EPSG:4979" ) ),
QgsCoordinateReferenceSystem( QStringLiteral( "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 and material
parseMesh( t, nodeJson["mesh"] );
}
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;
int epsgCode = 0;
try
{
// "spatialReference" is not required in the spec, but it is unclear
// what would be the default value. Given that it is crucial to distinguish
// between "global" and "local" mode, we require its presence (haven't seen
// a dataset without spatial reference yet)
const json spatialReferenceJson = layerJson["spatialReference"];
epsgCode = spatialReferenceJson["latestWkid"].get<int>();
}
catch ( const json::exception & )
{
mError = QObject::tr( "Missing 'spatialReference' attribute in metadata!" );
return;
}
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( QLatin1String( "http" ) ) || uri.startsWith( QLatin1String( "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( QLatin1String( "http" ) ) )
{
if ( !loadFromRestService( uri, layerJson, i3sVersion ) )
return;
}
else
{
if ( !loadFromSlpk( uri, layerJson, i3sVersion ) )
return;
}
if ( !layerJson.contains( "layerType" ) )
{
appendError( QgsErrorMessage( tr( "Invalid I3S source: missing layer type." ), QStringLiteral( "I3S" ) ) );
return;
}
if ( !layerJson.contains( "nodePages" ) )
{
appendError( QgsErrorMessage( tr( "Missing 'nodePages' attribute (should be available in I3S >= 1.7)" ), QStringLiteral( "I3S" ) ) );
return;
}
QString layerType = QString::fromStdString( layerJson["layerType"].get<std::string>() );
if ( layerType != QLatin1String( "3DObject" ) && layerType != QLatin1String( "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 I3S_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" );
if ( mShared->mLayerJson.contains( "version" ) )
{
// [required] "The ID of the last update session in which any resource belonging to this layer has been updated."
// (even though marked as required, not all datasets provide it)
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" );
}
if ( !mShared->mZRange.isInfinite() )
{
metadata += QStringLiteral( "<tr><td class=\"highlight\">" ) % tr( "Z Range" ) % QStringLiteral( "</td><td>%1 - %2</a>" ).arg( QLocale().toString( mShared->mZRange.lower() ), QLocale().toString( mShared->mZRange.upper() ) ) % 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( I3S_PROVIDER_KEY, I3S_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