/*************************************************************************** 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 #include #include #include #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 childNodeIndexes; QgsTiledSceneTile tile; }; QVector mTextureSetFormats; QVector mMaterialDefinitions; mutable QRecursiveMutex mLock; QUrl mRootUrl; QgsCoordinateTransformContext mTransformContext; long long mRootNodeIndex; int mNodesPerPage; bool mGlobalMode = false; QMap< long long, NodeDetails > mNodeMap; QSet 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() == 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() == "0" ) { formatType = QString::fromStdString( formatJson["format"].get() ); 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(); mRootNodeIndex = nodePagesJson.contains( "rootIndex" ) ? nodePagesJson["rootIndex"].get() : 0; int rootNodePage = static_cast( 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(), pbrBaseColorFactorJson[1].get(), pbrBaseColorFactorJson[2].get(), pbrBaseColorFactorJson[3].get() }; } 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(); 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(); } // 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 nodePagesToFetch; for ( long long childId : it.value().childNodeIndexes ) { int nodePageIndex = static_cast( 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(), center[1].get(), center[2].get() ), QgsVector3D( halfSize[0].get(), halfSize[1].get(), halfSize[2].get() ), QQuaternion( static_cast( quaternion[3].get() ), static_cast( quaternion[0].get() ), static_cast( quaternion[1].get() ), static_cast( quaternion[2].get() ) ) ); } 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(); 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(); 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(); 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 parentNodeIndex = nodeJson.contains( "parentIndex" ) ? nodeJson["parentIndex"].get() : -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(); // 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 childNodeIds; if ( nodeJson.contains( "children" ) ) { for ( const json &childJson : nodeJson["children"] ) { childNodeIds << childJson.get(); } } 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(); } 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(), fullExtentJson["ymin"].get(), fullExtentJson["xmax"].get(), fullExtentJson["ymax"].get() ); mZRange = QgsDoubleRange( fullExtentJson["zmin"].get(), fullExtentJson["zmax"].get() ); } 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() ); 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() ); 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() ); 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( "I3S Version" ) % QStringLiteral( "%1" ).arg( mShared->mI3sVersion ) % QStringLiteral( "\n" ); QString layerType = QString::fromStdString( mShared->mLayerJson["layerType"].get() ); metadata += QStringLiteral( "" ) % tr( "Layer Type" ) % QStringLiteral( "%1" ).arg( layerType ) % QStringLiteral( "\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() ); metadata += QStringLiteral( "" ) % tr( "Version" ) % QStringLiteral( "%1" ).arg( version ) % QStringLiteral( "\n" ); } // [optional] "The name of this layer." if ( mShared->mLayerJson.contains( "name" ) ) { QString name = QString::fromStdString( mShared->mLayerJson["name"].get() ); metadata += QStringLiteral( "" ) % tr( "Name" ) % QStringLiteral( "%1" ).arg( name ) % QStringLiteral( "\n" ); } // [optional] "The display alias to be used for this layer." if ( mShared->mLayerJson.contains( "alias" ) ) { QString alias = QString::fromStdString( mShared->mLayerJson["alias"].get() ); metadata += QStringLiteral( "" ) % tr( "Alias" ) % QStringLiteral( "%1" ).arg( alias ) % QStringLiteral( "\n" ); } // [optional] "Description string for this layer." if ( mShared->mLayerJson.contains( "description" ) ) { QString description = QString::fromStdString( mShared->mLayerJson["description"].get() ); metadata += QStringLiteral( "" ) % tr( "Description" ) % QStringLiteral( "%1" ).arg( description ) % QStringLiteral( "\n" ); } if ( !mShared->mZRange.isInfinite() ) { metadata += QStringLiteral( "" ) % tr( "Z Range" ) % QStringLiteral( "%1 - %2" ).arg( QLocale().toString( mShared->mZRange.lower() ), QLocale().toString( mShared->mZRange.upper() ) ) % QStringLiteral( "\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 QgsEsriI3SProviderMetadata::supportedLayerTypes() const { return { Qgis::LayerType::TiledScene }; } QgsProviderMetadata::ProviderMetadataCapabilities QgsEsriI3SProviderMetadata::capabilities() const { return QgsProviderMetadata::ProviderMetadataCapabilities(); } ///@endcond