QGIS API Documentation 3.99.0-Master (357b655ed83)
Loading...
Searching...
No Matches
qgsesrii3sdataprovider.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsesrii3sdataprovider.cpp
3 --------------------------------------
4 Date : July 2025
5 Copyright : (C) 2025 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include <nlohmann/json.hpp>
19
20#include "qgsapplication.h"
21#include "qgslogger.h"
24#include "qgsproviderutils.h"
25#include "qgsreadwritelocker.h"
27#include "qgsthreadingutils.h"
29#include "qgstiledsceneindex.h"
31#include "qgstiledscenetile.h"
32#include "qgsziputils.h"
33
34#include <QDir>
35#include <QFile>
36#include <QFileInfo>
37#include <QIcon>
38#include <QQuaternion>
39#include <QString>
40
41#include "moc_qgsesrii3sdataprovider.cpp"
42
43using namespace Qt::StringLiterals;
44
45#define I3S_PROVIDER_KEY u"esrii3s"_s
46#define I3S_PROVIDER_DESCRIPTION u"ESRI I3S data provider"_s
47
48
50
56class QgsEsriI3STiledSceneIndex final : public QgsAbstractTiledSceneIndex
57{
58 public:
59
60 QgsEsriI3STiledSceneIndex(
61 const json &tileset,
62 const QUrl &rootUrl,
63 const QgsCoordinateTransformContext &transformContext );
64
65 QgsTiledSceneTile rootTile() const final;
66 QgsTiledSceneTile getTile( long long id ) final;
67 long long parentTileId( long long id ) const final;
68 QVector< long long > childTileIds( long long id ) const final;
69 QVector< long long > getTiles( const QgsTiledSceneRequest &request ) final;
70 Qgis::TileChildrenAvailability childAvailability( long long id ) const final;
71 bool fetchHierarchy( long long id, QgsFeedback *feedback = nullptr ) final;
72
73 protected:
74
75 QByteArray fetchContent( const QString &uri, QgsFeedback *feedback = nullptr ) final;
76
77 private:
78
79 bool fetchNodePage( int nodePage, QgsFeedback *feedback = nullptr );
80 void parseNodePage( const QByteArray &nodePageContent );
81 void parseMesh( QgsTiledSceneTile &t, const json &meshJson );
82 QVariantMap parseMaterialDefinition( const json &materialDefinitionJson );
83
84 struct NodeDetails
85 {
86 long long parentNodeIndex;
87 QVector<long long> childNodeIndexes;
89 };
90
91 QVector<QString> mTextureSetFormats;
92 QVector<QVariantMap> mMaterialDefinitions;
93
94 mutable QRecursiveMutex mLock;
95 QUrl mRootUrl;
96 QgsCoordinateTransformContext mTransformContext;
97 long long mRootNodeIndex;
98 int mNodesPerPage;
99 bool mGlobalMode = false;
100 QMap< long long, NodeDetails > mNodeMap;
101 QSet<int> mCachedNodePages;
102
103};
104
105
106
114class QgsEsriI3SDataProviderSharedData
115{
116 public:
117 QgsEsriI3SDataProviderSharedData();
118 void initialize( const QString &i3sVersion,
119 const json &layerJson,
120 const QUrl &rootUrl,
121 const QgsCoordinateTransformContext &transformContext );
122
123 QString mI3sVersion;
124 json mLayerJson;
125
126 QgsCoordinateReferenceSystem mLayerCrs;
127 QgsCoordinateReferenceSystem mSceneCrs;
128 QgsTiledSceneBoundingVolume mBoundingVolume;
129
130 QgsRectangle mExtent;
131 QgsDoubleRange mZRange;
132
133 QgsTiledSceneIndex mIndex;
134
135 QString mError;
136 QReadWriteLock mReadWriteLock;
137
138};
139
140//
141// QgsEsriI3STiledSceneIndex
142//
143
144QgsEsriI3STiledSceneIndex::QgsEsriI3STiledSceneIndex(
145 const json &layerJson,
146 const QUrl &rootUrl,
147 const QgsCoordinateTransformContext &transformContext )
148 : mRootUrl( rootUrl )
149 , mTransformContext( transformContext )
150{
151 // "spatialReference" is not required, yet the spec does not say what should
152 // be the default - assuming global mode is the best we can do...
153 mGlobalMode = true;
154 if ( layerJson.contains( "spatialReference" ) && layerJson["spatialReference"].is_object() )
155 {
156 const json spatialReferenceJson = layerJson["spatialReference"];
157 if ( spatialReferenceJson.contains( "latestWkid" ) && spatialReferenceJson["latestWkid"].is_number() )
158 {
159 int epsgCode = spatialReferenceJson["latestWkid"].get<int>();
160 mGlobalMode = epsgCode == 4326;
161 }
162 else if ( spatialReferenceJson.contains( "wkid" ) && spatialReferenceJson["wkid"].is_number() )
163 {
164 int epsgCode = spatialReferenceJson["wkid"].get<int>();
165 mGlobalMode = epsgCode == 4326;
166 }
167 }
168
169 if ( layerJson.contains( "textureSetDefinitions" ) )
170 {
171 for ( auto textureSetDefinitionJson : layerJson["textureSetDefinitions"] )
172 {
173 QString formatType;
174 for ( const json &formatJson : textureSetDefinitionJson["formats"] )
175 {
176 if ( formatJson["name"].get<std::string>() == "0" )
177 {
178 formatType = QString::fromStdString( formatJson["format"].get<std::string>() );
179 break;
180 }
181 }
182 mTextureSetFormats.append( formatType );
183 }
184 }
185
186 if ( layerJson.contains( "materialDefinitions" ) )
187 {
188 for ( const json &materialDefinitionJson : layerJson["materialDefinitions"] )
189 {
190 QVariantMap materialDef = parseMaterialDefinition( materialDefinitionJson );
191 mMaterialDefinitions.append( materialDef );
192 }
193 }
194
195 json nodePagesJson = layerJson["nodePages"];
196 mNodesPerPage = nodePagesJson["nodesPerPage"].get<int>();
197 mRootNodeIndex = nodePagesJson.contains( "rootIndex" ) ? nodePagesJson["rootIndex"].get<long long>() : 0;
198
199 int rootNodePage = static_cast<int>( mRootNodeIndex / mNodesPerPage );
200 fetchNodePage( rootNodePage );
201}
202
203QVariantMap QgsEsriI3STiledSceneIndex::parseMaterialDefinition( const json &materialDefinitionJson )
204{
205 QVariantMap materialDef;
206
207 if ( materialDefinitionJson.contains( "pbrMetallicRoughness" ) )
208 {
209 const json pbrJson = materialDefinitionJson["pbrMetallicRoughness"];
210 if ( pbrJson.contains( "baseColorFactor" ) )
211 {
212 const json pbrBaseColorFactorJson = pbrJson["baseColorFactor"];
213 materialDef[u"pbrBaseColorFactor"_s] = QVariantList
214 {
215 pbrBaseColorFactorJson[0].get<double>(),
216 pbrBaseColorFactorJson[1].get<double>(),
217 pbrBaseColorFactorJson[2].get<double>(),
218 pbrBaseColorFactorJson[3].get<double>()
219 };
220 }
221 else
222 {
223 materialDef[u"pbrBaseColorFactor"_s] = QVariantList{ 1.0, 1.0, 1.0, 1.0 };
224 }
225 if ( pbrJson.contains( "baseColorTexture" ) )
226 {
227 // but right now we only support png/jpg textures which have
228 // hardcoded name "0" by the spec, and we use texture set definitions
229 // only to figure out whether it is png or jpg
230 const int textureSetDefinitionId = pbrJson["baseColorTexture"]["textureSetDefinitionId"].get<int>();
231 if ( textureSetDefinitionId < mTextureSetFormats.count() )
232 {
233 materialDef[u"pbrBaseColorTextureName"_s] = u"0"_s;
234 materialDef[u"pbrBaseColorTextureFormat"_s] = mTextureSetFormats[textureSetDefinitionId];
235 }
236 else
237 {
238 QgsDebugError( QString( "referencing textureSetDefinition that does not exist! %1 " ).arg( textureSetDefinitionId ) );
239 }
240 }
241 }
242 else
243 {
244 materialDef[u"pbrBaseColorFactor"_s] = QVariantList{ 1.0, 1.0, 1.0, 1.0 };
245 }
246
247 if ( materialDefinitionJson.contains( "doubleSided" ) )
248 {
249 materialDef[u"doubleSided"_s] = materialDefinitionJson["doubleSided"].get<bool>();
250 }
251
252 // there are various other properties that can be defined in a material,
253 // but we do not support them: normal texture, occlusion texture, emissive texture,
254 // emissive factor, alpha mode, alpha cutoff, cull face.
255
256 return materialDef;
257}
258
259QgsTiledSceneTile QgsEsriI3STiledSceneIndex::rootTile() const
260{
261 QMutexLocker locker( &mLock );
262 if ( !mNodeMap.contains( mRootNodeIndex ) )
263 {
264 QgsDebugError( u"Unable to access the root tile!"_s );
265 return QgsTiledSceneTile();
266 }
267 return mNodeMap[mRootNodeIndex].tile;
268}
269
270QgsTiledSceneTile QgsEsriI3STiledSceneIndex::getTile( long long id )
271{
272 QMutexLocker locker( &mLock );
273 auto it = mNodeMap.constFind( id );
274 if ( it != mNodeMap.constEnd() )
275 {
276 return it.value().tile;
277 }
278
279 return QgsTiledSceneTile();
280}
281
282long long QgsEsriI3STiledSceneIndex::parentTileId( long long id ) const
283{
284 QMutexLocker locker( &mLock );
285 auto it = mNodeMap.constFind( id );
286 if ( it != mNodeMap.constEnd() )
287 {
288 return it.value().parentNodeIndex;
289 }
290
291 return -1;
292}
293
294QVector< long long > QgsEsriI3STiledSceneIndex::childTileIds( long long id ) const
295{
296 QMutexLocker locker( &mLock );
297 auto it = mNodeMap.constFind( id );
298 if ( it != mNodeMap.constEnd() )
299 {
300 return it.value().childNodeIndexes;
301 }
302
303 return {};
304}
305
306QVector< long long > QgsEsriI3STiledSceneIndex::getTiles( const QgsTiledSceneRequest &request )
307{
308 QVector< long long > results;
309
310 std::function< void( long long )> traverseNode;
311 traverseNode = [&request, &traverseNode, &results, this]( long long nodeId )
312 {
313 QgsTiledSceneTile t = getTile( nodeId );
314 if ( !request.filterBox().isNull() && !t.boundingVolume().intersects( request.filterBox() ) )
315 return;
316
317 if ( request.requiredGeometricError() <= 0 || t.geometricError() <= 0 || t.geometricError() > request.requiredGeometricError() )
318 {
319 // need to go deeper, this tile does not have enough details
320
321 if ( childAvailability( t.id() ) == Qgis::TileChildrenAvailability::NeedFetching &&
323 {
324 fetchHierarchy( t.id() );
325 }
326
327 // now we should have children available (if any)
328 auto it = mNodeMap.constFind( t.id() );
329 for ( long long childId : it.value().childNodeIndexes )
330 {
331 traverseNode( childId );
332 }
333
334 if ( it.value().childNodeIndexes.isEmpty() )
335 {
336 // there are no children available, so we use this tile even though we want more detail
337 results << t.id();
338 }
339 }
340 else
341 {
342 // this tile has error sufficiently low so that we do not need to traverse
343 // the node tree further down
344 results << t.id();
345 }
346 };
347
348 QMutexLocker locker( &mLock );
349 long long startNodeId = request.parentTileId() == -1 ? mRootNodeIndex : request.parentTileId();
350 traverseNode( startNodeId );
351
352 return results;
353}
354
355Qgis::TileChildrenAvailability QgsEsriI3STiledSceneIndex::childAvailability( long long id ) const
356{
357 QMutexLocker locker( &mLock );
358 auto it = mNodeMap.constFind( id );
359 if ( it == mNodeMap.constEnd() )
360 {
361 // we have no info about the node, so what we return is a bit arbitrary anyway
363 }
364
365 if ( it.value().childNodeIndexes.isEmpty() )
366 {
368 }
369
370 for ( long long childId : it.value().childNodeIndexes )
371 {
372 if ( !mNodeMap.contains( childId ) )
373 {
374 // at least one child is missing from the node map
376 }
377 }
379}
380
381bool QgsEsriI3STiledSceneIndex::fetchHierarchy( long long id, QgsFeedback *feedback )
382{
383 QMutexLocker locker( &mLock );
384 auto it = mNodeMap.constFind( id );
385 if ( it == mNodeMap.constEnd() )
386 return false;
387
388 // gather all the missing node pages to get information about child nodes
389 QSet<int> nodePagesToFetch;
390 for ( long long childId : it.value().childNodeIndexes )
391 {
392 int nodePageIndex = static_cast<int>( childId / mNodesPerPage );
393 if ( !mCachedNodePages.contains( nodePageIndex ) )
394 nodePagesToFetch.insert( nodePageIndex );
395 }
396
397 bool success = true;
398 for ( int nodePage : nodePagesToFetch )
399 {
400 if ( !fetchNodePage( nodePage, feedback ) )
401 {
402 success = false;
403 }
404 }
405
406 return success;
407}
408
409QByteArray QgsEsriI3STiledSceneIndex::fetchContent( const QString &uri, QgsFeedback *feedback )
410{
411 QUrl url( uri );
412 if ( url.isLocalFile() )
413 {
414 const QString slpkPath = mRootUrl.toLocalFile();
415 if ( QFileInfo( slpkPath ).suffix().compare( "slpk"_L1, Qt::CaseInsensitive ) == 0 )
416 {
417 const QString fileInSlpk = uri.mid( mRootUrl.toString().length() + 1 );
418
419 QByteArray data;
420 if ( QgsZipUtils::extractFileFromZip( slpkPath, fileInSlpk, data ) )
421 return data;
422
423 return QByteArray();
424 }
425 else
426 {
427 QFile file( url.toLocalFile() );
428 if ( file.open( QIODevice::ReadOnly ) )
429 {
430 return file.readAll();
431 }
432 }
433 }
434 else
435 {
436 QNetworkRequest networkRequest = QNetworkRequest( url );
437 QgsSetRequestInitiatorClass( networkRequest, u"QgsEsriI3STiledSceneIndex"_s );
438 networkRequest.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
439 networkRequest.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
440
442 networkRequest, QString(), false, feedback );
443 return reply.content();
444 }
445
446 return QByteArray();
447}
448
449bool QgsEsriI3STiledSceneIndex::fetchNodePage( int nodePage, QgsFeedback *feedback )
450{
451 QByteArray nodePageContent;
452 if ( !mRootUrl.isLocalFile() )
453 {
454 const QString uri = u"%1/layers/0/nodepages/%2"_s.arg( mRootUrl.toString() ).arg( nodePage );
455 nodePageContent = retrieveContent( uri, feedback );
456 }
457 else
458 {
459 const QString uri = u"%1/nodepages/%2.json.gz"_s.arg( mRootUrl.toString() ).arg( nodePage );
460 const QByteArray nodePageContentGzipped = retrieveContent( uri, feedback );
461
462 if ( !QgsZipUtils::decodeGzip( nodePageContentGzipped, nodePageContent ) )
463 {
464 QgsDebugError( u"Failed to decompress node page content: "_s + uri );
465 return false;
466 }
467
468 if ( nodePageContent.isEmpty() )
469 {
470 QgsDebugError( u"Failed to read node page content: "_s + uri );
471 return false;
472 }
473 }
474
475 try
476 {
477 parseNodePage( nodePageContent );
478 }
479 catch ( json::exception &error )
480 {
481 QgsDebugError( u"Error reading node page %1: %2"_s.arg( nodePage ).arg( error.what() ) );
482 return false;
483 }
484
485 mCachedNodePages.insert( nodePage );
486 return true;
487}
488
489static QgsOrientedBox3D parseBox( const json &box )
490{
491 try
492 {
493 json center = box["center"];
494 json halfSize = box["halfSize"];
495 json quaternion = box["quaternion"]; // order is x, y, z, w
496
497 return QgsOrientedBox3D(
498 QgsVector3D( center[0].get<double>(),
499 center[1].get<double>(),
500 center[2].get<double>() ),
501 QgsVector3D( halfSize[0].get<double>(),
502 halfSize[1].get<double>(),
503 halfSize[2].get<double>() ),
504 QQuaternion( static_cast<float>( quaternion[3].get<double>() ),
505 static_cast<float>( quaternion[0].get<double>() ),
506 static_cast<float>( quaternion[1].get<double>() ),
507 static_cast<float>( quaternion[2].get<double>() ) ) );
508 }
509 catch ( nlohmann::json::exception & )
510 {
511 return QgsOrientedBox3D();
512 }
513}
514
515void QgsEsriI3STiledSceneIndex::parseMesh( QgsTiledSceneTile &t, const json &meshJson )
516{
517 if ( !meshJson.contains( "geometry" ) || !meshJson.contains( "material" ) )
518 return;
519
520 int geometryResource = meshJson["geometry"]["resource"].get<int>();
521 QString geometryUri;
522 if ( mRootUrl.isLocalFile() )
523 geometryUri = u"%1/nodes/%2/geometries/1.bin.gz"_s.arg( mRootUrl.toString() ).arg( geometryResource );
524 else
525 geometryUri = u"%1/layers/0/nodes/%2/geometries/1"_s.arg( mRootUrl.toString() ).arg( geometryResource );
526
527 // parse material and related textures
528 const json materialJson = meshJson["material"];
529 int materialIndex = materialJson["definition"].get<int>();
530 QVariantMap materialInfo;
531 if ( materialIndex >= 0 && materialIndex < mMaterialDefinitions.count() )
532 {
533 materialInfo = mMaterialDefinitions[materialIndex];
534 if ( materialInfo.contains( u"pbrBaseColorTextureName"_s ) )
535 {
536 const QString textureName = materialInfo[u"pbrBaseColorTextureName"_s].toString();
537 const QString textureFormat = materialInfo[u"pbrBaseColorTextureFormat"_s].toString();
538 materialInfo.remove( u"pbrBaseColorTextureName"_s );
539 materialInfo.remove( u"pbrBaseColorTextureFormat"_s );
540
541 const int textureResource = materialJson["resource"].get<int>();
542 QString textureUri;
543 if ( mRootUrl.isLocalFile() )
544 textureUri = u"%1/nodes/%2/textures/%3.%4"_s.arg( mRootUrl.toString() ).arg( textureResource ).arg( textureName, textureFormat );
545 else
546 textureUri = u"%1/layers/0/nodes/%2/textures/%3"_s.arg( mRootUrl.toString() ).arg( textureResource ).arg( textureName );
547 materialInfo[u"pbrBaseColorTexture"_s] = textureUri;
548 }
549 }
550
551 t.setResources( { { u"content"_s, geometryUri } } );
552
553 QVariantMap metadata =
554 {
555 { u"gltfUpAxis"_s, static_cast< int >( Qgis::Axis::Z ) },
556 { u"contentFormat"_s, u"draco"_s },
557 { u"material"_s, materialInfo }
558 };
559 t.setMetadata( metadata );
560}
561
562void QgsEsriI3STiledSceneIndex::parseNodePage( const QByteArray &nodePageContent )
563{
564 const json nodePageJson = json::parse( nodePageContent.toStdString() );
565 for ( const json &nodeJson : nodePageJson["nodes"] )
566 {
567 long long nodeIndex = nodeJson["index"].get<long long>();
568 long long parentNodeIndex = nodeJson.contains( "parentIndex" ) ? nodeJson["parentIndex"].get<long long>() : -1;
569
570 QgsOrientedBox3D obb = parseBox( nodeJson["obb"] );
571
572 // OBB in global scene layers should be constructed in ECEF and its values are defined like this:
573 // - center - X,Y - lon/lat in degrees, Z - elevation in meters
574 // - half-size - in meters
575 // - quaternion - in reference to ECEF coordinate system
576
577 // OBB in local scene layers should be constructed in the CRS of the layer
578 // - center, half-size - in units of the CRS
579 // - quaternion - in reference to CRS of the layer
580
581 if ( mGlobalMode )
582 {
584 QgsCoordinateReferenceSystem( u"EPSG:4978"_s ),
585 mTransformContext );
586 QgsVector3D obbCenterEcef = ct.transform( obb.center() );
587 obb = QgsOrientedBox3D( { obbCenterEcef.x(), obbCenterEcef.y(), obbCenterEcef.z() }, obb.halfAxesList() );
588 }
589
590 double threshold = -1;
591 if ( nodeJson.contains( "lodThreshold" ) )
592 {
593 double maxScreenThresholdSquared = nodeJson["lodThreshold"].get<double>();
594
595 // This conversion from "maxScreenThresholdSQ" to geometry error is copied from CesiumJS
596 // implementation of I3S (the only difference is Cesium uses longest OBB axis length
597 threshold = obb.longestSide() / sqrt( maxScreenThresholdSquared / ( M_PI / 4 ) ) * 16;
598 }
599 QVector<long long> childNodeIds;
600 if ( nodeJson.contains( "children" ) )
601 {
602 for ( const json &childJson : nodeJson["children"] )
603 {
604 childNodeIds << childJson.get<long long>();
605 }
606 }
607
608 QgsTiledSceneTile t( nodeIndex );
609 t.setBoundingVolume( obb );
610 t.setGeometricError( threshold );
611
612 QgsMatrix4x4 transform;
613 transform.translate( obb.center() );
614 t.setTransform( transform );
615
616 if ( nodeJson.contains( "mesh" ) )
617 {
618 // parse geometry and material
619 parseMesh( t, nodeJson["mesh"] );
620 }
621
622 mNodeMap.insert( nodeIndex, { parentNodeIndex, childNodeIds, t } );
623 }
624}
625
626
627//
628// QgsEsriI3SDataProviderSharedData
629//
630
631QgsEsriI3SDataProviderSharedData::QgsEsriI3SDataProviderSharedData()
632 : mIndex( QgsTiledSceneIndex( nullptr ) )
633{
634}
635
636void QgsEsriI3SDataProviderSharedData::initialize(
637 const QString &i3sVersion,
638 const json &layerJson,
639 const QUrl &rootUrl,
640 const QgsCoordinateTransformContext &transformContext )
641{
642 mI3sVersion = i3sVersion;
643 mLayerJson = layerJson;
644
645 int epsgCode = 0;
646
647 // ESRI spatialReference https://developers.arcgis.com/web-map-specification/objects/spatialReference/
648 // defines latestWkid as optional, wkid as mandatory
649 // so we first check for latestWkid and use wkid as fallback
650 if ( layerJson.contains( "spatialReference" ) && layerJson["spatialReference"].is_object() )
651 {
652 const json spatialReferenceJson = layerJson["spatialReference"];
653 if ( spatialReferenceJson.contains( "latestWkid" ) && spatialReferenceJson["latestWkid"].is_number() )
654 {
655 epsgCode = spatialReferenceJson["latestWkid"].get<int>();
656 }
657 else if ( spatialReferenceJson.contains( "wkid" ) && spatialReferenceJson["wkid"].is_number() )
658 {
659 epsgCode = spatialReferenceJson["wkid"].get<int>();
660 }
661 }
662
663 if ( epsgCode == 4326 )
664 {
665 // "global" mode
666
667 // TODO: elevation can be ellipsoidal or gravity-based!
668 mLayerCrs = QgsCoordinateReferenceSystem( u"EPSG:4979"_s );
669 mSceneCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
670 }
671 else
672 {
673 // "local" mode - using a projected CRS
674 mLayerCrs = QgsCoordinateReferenceSystem( QString( "EPSG:%1" ).arg( epsgCode ) );
675 mSceneCrs = mLayerCrs;
676 }
677
678 mIndex = QgsTiledSceneIndex(
679 new QgsEsriI3STiledSceneIndex(
680 layerJson,
681 rootUrl,
682 transformContext
683 )
684 );
685
686 if ( layerJson.contains( "fullExtent" ) )
687 {
688 const json fullExtentJson = layerJson["fullExtent"];
689 mExtent = QgsRectangle(
690 fullExtentJson["xmin"].get<double>(),
691 fullExtentJson["ymin"].get<double>(),
692 fullExtentJson["xmax"].get<double>(),
693 fullExtentJson["ymax"].get<double>() );
694 mZRange = QgsDoubleRange(
695 fullExtentJson["zmin"].get<double>(),
696 fullExtentJson["zmax"].get<double>() );
697 }
698 else
699 {
700 QgsBox3D box = mIndex.rootTile().boundingVolume().bounds( QgsCoordinateTransform( mSceneCrs, mLayerCrs, transformContext ) );
701 mExtent = box.toRectangle();
702 mZRange = QgsDoubleRange( box.zMinimum(), box.zMaximum() );
703 }
704
705 mBoundingVolume = mIndex.rootTile().boundingVolume();
706}
707
708//
709// QgsEsriI3SDataProvider
710//
711
712
713QgsEsriI3SDataProvider::QgsEsriI3SDataProvider( const QString &uri,
714 const QgsDataProvider::ProviderOptions &providerOptions,
716 : QgsTiledSceneDataProvider( uri, providerOptions, flags )
717 , mShared( std::make_shared< QgsEsriI3SDataProviderSharedData >() )
718{
719 QgsDataSourceUri dataSource( dataSourceUri() );
720 const QString url = dataSource.param( u"url"_s );
721 QString sourcePath = QUrl::fromPercentEncoding( url.toUtf8() );
722
723 if ( sourcePath.isEmpty() )
724 {
725 sourcePath = uri;
726 }
727
728 QUrl rootUrl;
729 if ( sourcePath.startsWith( "http"_L1 ) || sourcePath.startsWith( "file"_L1 ) )
730 {
731 rootUrl = sourcePath;
732 }
733 else
734 {
735 // when saved in project as relative path, we then get just the path... (TODO?)
736 rootUrl = QUrl::fromLocalFile( sourcePath );
737 }
738
739 QString i3sVersion;
740 json layerJson;
741 if ( sourcePath.startsWith( "http"_L1 ) )
742 {
743 if ( !loadFromRestService( rootUrl.toString(), layerJson, i3sVersion ) )
744 return;
745 }
746 else
747 {
748 if ( !loadFromSlpk( rootUrl.toLocalFile(), layerJson, i3sVersion ) )
749 return;
750 }
751
752 if ( !layerJson.contains( "layerType" ) )
753 {
754 appendError( QgsErrorMessage( tr( "Invalid I3S source: missing layer type." ), u"I3S"_s ) );
755 return;
756 }
757
758 if ( !layerJson.contains( "nodePages" ) )
759 {
760 appendError( QgsErrorMessage( tr( "Missing 'nodePages' attribute (should be available in I3S >= 1.7)" ), u"I3S"_s ) );
761 return;
762 }
763
764 QString layerType = QString::fromStdString( layerJson["layerType"].get<std::string>() );
765 if ( layerType != "3DObject"_L1 && layerType != "IntegratedMesh"_L1 )
766 {
767 appendError( QgsErrorMessage( tr( "Unsupported layer type: " ) + layerType, u"I3S"_s ) );
768 return;
769 }
770
771 mShared->initialize( i3sVersion, layerJson, rootUrl, transformContext() );
772
773 if ( !mShared->mIndex.isValid() )
774 {
775 appendError( mShared->mError );
776 return;
777 }
778
779 mIsValid = true;
780}
781
782
783bool QgsEsriI3SDataProvider::loadFromRestService( const QString &uri, json &layerJson, QString &i3sVersion )
784{
785 QNetworkRequest networkRequest = QNetworkRequest( QUrl( uri ) );
786 QgsSetRequestInitiatorClass( networkRequest, u"QgsEsriI3SDataProvider"_s );
787 networkRequest.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
788 networkRequest.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
789
791 if ( reply.error() != QNetworkReply::NoError )
792 {
793 appendError( QgsErrorMessage( tr( "Failed to fetch layer metadata: " ) + networkRequest.url().toString(), u"I3S"_s ) );
794 return false;
795 }
796 QByteArray sceneLayerContent = reply.content();
797
798 json serviceJson;
799 try
800 {
801 serviceJson = json::parse( sceneLayerContent.toStdString() );
802 }
803 catch ( const json::parse_error & )
804 {
805 appendError( QgsErrorMessage( tr( "Unable to parse JSON: " ) + uri, u"I3S"_s ) );
806 return false;
807 }
808
809 if ( !serviceJson.contains( "serviceVersion" ) )
810 {
811 appendError( QgsErrorMessage( tr( "Missing I3S version: " ) + uri, u"I3S"_s ) );
812 return false;
813 }
814 i3sVersion = QString::fromStdString( serviceJson["serviceVersion"].get<std::string>() );
815 if ( !checkI3SVersion( i3sVersion ) )
816 return false;
817
818 if ( !serviceJson.contains( "layers" ) || !serviceJson["layers"].is_array() || serviceJson["layers"].size() < 1 )
819 {
820 appendError( QgsErrorMessage( tr( "Unable to get layer info: " ) + uri, u"I3S"_s ) );
821 return false;
822 }
823
824 layerJson = serviceJson["layers"][0];
825 return true;
826}
827
828bool QgsEsriI3SDataProvider::loadFromSlpk( const QString &uri, json &layerJson, QString &i3sVersion )
829{
830 bool isExtracted;
831 if ( QFileInfo( uri ).suffix().compare( "slpk"_L1, Qt::CaseInsensitive ) == 0 )
832 {
833 isExtracted = false;
834 }
835 else
836 {
837 isExtracted = true;
838 }
839
840 QByteArray metadataContent;
841 QString metadataFileName = u"metadata.json"_s;
842 if ( isExtracted ) // if a directory, read directly as Extracted SLPK
843 {
844 const QString metadataDirPath = QDir( uri ).filePath( metadataFileName );
845 QFile fMetadata( metadataDirPath );
846 if ( !fMetadata.open( QIODevice::ReadOnly ) )
847 {
848 appendError( QgsErrorMessage( tr( "Failed to read layer metadata: %1" ).arg( metadataDirPath ), u"I3S"_s ) );
849 return false;
850 }
851 metadataContent = fMetadata.readAll();
852 }
853 else // SLPK
854 {
855 if ( !QgsZipUtils::extractFileFromZip( uri, metadataFileName, metadataContent ) )
856 {
857 appendError( QgsErrorMessage( tr( "Failed to read %1 in file: %2" ).arg( metadataFileName ).arg( uri ), u"I3S"_s ) );
858 return false;
859 }
860 }
861
862 json metadataJson;
863 try
864 {
865 metadataJson = json::parse( metadataContent.toStdString() );
866 }
867 catch ( const json::parse_error & )
868 {
869 appendError( QgsErrorMessage( tr( "Unable to parse %1 in: %2" ).arg( metadataFileName ).arg( uri ), u"I3S"_s ) );
870 return false;
871 }
872
873 if ( !metadataJson.contains( "I3SVersion" ) )
874 {
875 appendError( QgsErrorMessage( tr( "Missing I3S version in %1 in: %2" ).arg( metadataFileName ).arg( uri ), u"I3S"_s ) );
876 return false;
877 }
878 i3sVersion = QString::fromStdString( metadataJson["I3SVersion"].get<std::string>() );
879 if ( !checkI3SVersion( i3sVersion ) )
880 return false;
881
882 QByteArray sceneLayerContentGzipped;
883 const QString sceneLayerContentFileName = u"3dSceneLayer.json.gz"_s;
884 if ( isExtracted ) // if a directory, read directly as Extracted SLPK
885 {
886 const QString sceneLayerContentDirPath = QDir( uri ).filePath( sceneLayerContentFileName );
887 QFile fSceneLayerContent( sceneLayerContentDirPath );
888 if ( !fSceneLayerContent.open( QIODevice::ReadOnly ) )
889 {
890 appendError( QgsErrorMessage( tr( "Failed to read layer metadata: %1" ).arg( sceneLayerContentDirPath ), u"I3S"_s ) );
891 return false;
892 }
893 sceneLayerContentGzipped = fSceneLayerContent.readAll();
894 }
895 else // SLPK
896 {
897 if ( !QgsZipUtils::extractFileFromZip( uri, sceneLayerContentFileName, sceneLayerContentGzipped ) )
898 {
899 appendError( QgsErrorMessage( tr( "Failed to read %1 in file: %2" ).arg( sceneLayerContentFileName ).arg( uri ), u"I3S"_s ) );
900 return false;
901 }
902 }
903
904 QByteArray sceneLayerContent;
905 if ( !QgsZipUtils::decodeGzip( sceneLayerContentGzipped, sceneLayerContent ) )
906 {
907 appendError( QgsErrorMessage( tr( "Failed to decompress %1 in: %2" ).arg( sceneLayerContentFileName ).arg( uri ), u"I3S"_s ) );
908 return false;
909 }
910
911 try
912 {
913 layerJson = json::parse( sceneLayerContent.toStdString() );
914 }
915 catch ( const json::parse_error & )
916 {
917 appendError( QgsErrorMessage( tr( "Unable to parse %1 in: %2" ).arg( sceneLayerContentFileName ).arg( uri ), u"I3S"_s ) );
918 return false;
919 }
920
921 return true;
922}
923
924bool QgsEsriI3SDataProvider::checkI3SVersion( const QString &i3sVersion )
925{
926 // We support I3S version >= 1.7 released in 2019. Earlier versions
927 // of the spec are much less efficient to work with (e.g. they do not
928 // support node pages, no Draco compression of geometries)
929
930 // Note: for more confusion, OGC has different versioning of I3S.
931 // ESRI I3S version 1.7 should be equivalent to OGC I3S version 1.3.
932 // Fortunately OGC versioning is not really used anywhere (apart from OGC docs)
933 // so we can ignore OGC I3S versions and use ESRI I3S version.
934
935 QStringList i3sVersionComponents = i3sVersion.split( '.' );
936 if ( i3sVersionComponents.size() != 2 )
937 {
938 appendError( QgsErrorMessage( tr( "Unexpected I3S version format: " ) + i3sVersion, u"I3S"_s ) );
939 return false;
940 }
941 int i3sVersionMajor = i3sVersionComponents[0].toInt();
942 int i3sVersionMinor = i3sVersionComponents[1].toInt();
943 if ( i3sVersionMajor != 1 || ( i3sVersionMajor == 1 && i3sVersionMinor < 7 ) )
944 {
945 appendError( QgsErrorMessage( tr( "Unsupported I3S version: " ) + i3sVersion, u"I3S"_s ) );
946 return false;
947 }
948 return true;
949}
950
951
952QgsEsriI3SDataProvider::QgsEsriI3SDataProvider( const QgsEsriI3SDataProvider &other )
954 , mIsValid( other.mIsValid )
955{
956 QgsReadWriteLocker locker( other.mShared->mReadWriteLock, QgsReadWriteLocker::Read );
957 mShared = other.mShared;
958}
959
960QgsEsriI3SDataProvider::~QgsEsriI3SDataProvider() = default;
961
962Qgis::DataProviderFlags QgsEsriI3SDataProvider::flags() const
963{
965}
966
967Qgis::TiledSceneProviderCapabilities QgsEsriI3SDataProvider::capabilities() const
968{
970}
971
972QgsEsriI3SDataProvider *QgsEsriI3SDataProvider::clone() const
973{
975 return new QgsEsriI3SDataProvider( *this );
976}
977
978QgsCoordinateReferenceSystem QgsEsriI3SDataProvider::crs() const
979{
981
982 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
983 return mShared->mLayerCrs;
984}
985
986QgsRectangle QgsEsriI3SDataProvider::extent() const
987{
989
990 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
991 return mShared->mExtent;
992}
993
994bool QgsEsriI3SDataProvider::isValid() const
995{
997
998 return mIsValid;
999}
1000
1001QString QgsEsriI3SDataProvider::name() const
1002{
1004
1005 return I3S_PROVIDER_KEY;
1006}
1007
1008QString QgsEsriI3SDataProvider::description() const
1009{
1011
1012 return QObject::tr( "ESRI I3S" );
1013}
1014
1015QString QgsEsriI3SDataProvider::htmlMetadata() const
1016{
1018
1019 QString metadata;
1020
1021 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1022
1023 metadata += u"<tr><td class=\"highlight\">"_s % tr( "I3S Version" ) % u"</td><td>%1</a>"_s.arg( mShared->mI3sVersion ) % u"</td></tr>\n"_s;
1024
1025 QString layerType = QString::fromStdString( mShared->mLayerJson["layerType"].get<std::string>() );
1026 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Layer Type" ) % u"</td><td>%1</a>"_s.arg( layerType ) % u"</td></tr>\n"_s;
1027
1028 if ( mShared->mLayerJson.contains( "version" ) )
1029 {
1030 // [required] "The ID of the last update session in which any resource belonging to this layer has been updated."
1031 // (even though marked as required, not all datasets provide it)
1032 QString version = QString::fromStdString( mShared->mLayerJson["version"].get<std::string>() );
1033 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Version" ) % u"</td><td>%1</a>"_s.arg( version ) % u"</td></tr>\n"_s;
1034 }
1035
1036 // [optional] "The name of this layer."
1037 if ( mShared->mLayerJson.contains( "name" ) )
1038 {
1039 QString name = QString::fromStdString( mShared->mLayerJson["name"].get<std::string>() );
1040 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Name" ) % u"</td><td>%1</a>"_s.arg( name ) % u"</td></tr>\n"_s;
1041 }
1042 // [optional] "The display alias to be used for this layer."
1043 if ( mShared->mLayerJson.contains( "alias" ) )
1044 {
1045 QString alias = QString::fromStdString( mShared->mLayerJson["alias"].get<std::string>() );
1046 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Alias" ) % u"</td><td>%1</a>"_s.arg( alias ) % u"</td></tr>\n"_s;
1047 }
1048 // [optional] "Description string for this layer."
1049 if ( mShared->mLayerJson.contains( "description" ) )
1050 {
1051 QString description = QString::fromStdString( mShared->mLayerJson["description"].get<std::string>() );
1052 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Description" ) % u"</td><td>%1</a>"_s.arg( description ) % u"</td></tr>\n"_s;
1053 }
1054
1055 if ( !mShared->mZRange.isInfinite() )
1056 {
1057 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Z Range" ) % u"</td><td>%1 - %2</a>"_s.arg( QLocale().toString( mShared->mZRange.lower() ), QLocale().toString( mShared->mZRange.upper() ) ) % u"</td></tr>\n"_s;
1058 }
1059
1060 return metadata;
1061}
1062
1063const QgsCoordinateReferenceSystem QgsEsriI3SDataProvider::sceneCrs() const
1064{
1066 if ( !mShared )
1068
1069 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1070 return mShared->mSceneCrs;
1071}
1072
1073const QgsTiledSceneBoundingVolume &QgsEsriI3SDataProvider::boundingVolume() const
1074{
1076 static QgsTiledSceneBoundingVolume nullVolume;
1077 if ( !mShared )
1078 return nullVolume;
1079
1080 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1081 return mShared ? mShared->mBoundingVolume : nullVolume;
1082}
1083
1084QgsTiledSceneIndex QgsEsriI3SDataProvider::index() const
1085{
1087 if ( !mShared )
1088 return QgsTiledSceneIndex( nullptr );
1089
1090 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1091 return mShared->mIndex;
1092}
1093
1094QgsDoubleRange QgsEsriI3SDataProvider::zRange() const
1095{
1097 if ( !mShared )
1098 return QgsDoubleRange();
1099
1100 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1101 return mShared->mZRange;
1102}
1103
1104
1105
1106//
1107// QgsEsriI3SProviderMetadata
1108//
1109
1110
1111QgsEsriI3SProviderMetadata::QgsEsriI3SProviderMetadata():
1113{
1114}
1115
1116QIcon QgsEsriI3SProviderMetadata::icon() const
1117{
1118 return QgsApplication::getThemeIcon( u"mIconEsriI3s.svg"_s );
1119}
1120
1121QgsEsriI3SDataProvider *QgsEsriI3SProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, Qgis::DataProviderReadFlags flags )
1122{
1123 return new QgsEsriI3SDataProvider( uri, options, flags );
1124}
1125
1126QList<QgsProviderSublayerDetails> QgsEsriI3SProviderMetadata::querySublayers( const QString &uri, Qgis::SublayerQueryFlags, QgsFeedback * ) const
1127{
1128 QString fileName;
1129 const QFileInfo fi( uri );
1130 if ( fi.isFile() )
1131 {
1132 fileName = uri;
1133 }
1134 else
1135 {
1136 const QVariantMap parts = decodeUri( uri );
1137 fileName = parts.value( u"path"_s ).toString();
1138 }
1139
1140 if ( fileName.isEmpty() )
1141 return {};
1142
1143 if ( QFileInfo( fileName ).suffix().compare( "slpk"_L1, Qt::CaseInsensitive ) == 0 )
1144 {
1145 QVariantMap parts;
1146 parts.insert( u"path"_s, fileName );
1147
1149 details.setUri( encodeUri( parts ) );
1150 details.setProviderKey( key() );
1153 return {details};
1154 }
1155 return {};
1156}
1157
1158QVariantMap QgsEsriI3SProviderMetadata::decodeUri( const QString &uri ) const
1159{
1160 QgsDataSourceUri dsUri( uri );
1161
1162 QVariantMap uriComponents;
1163 QString path = dsUri.param( u"url"_s );
1164 if ( path.isEmpty() && !uri.isEmpty() )
1165 {
1166 path = uri;
1167 }
1168 uriComponents.insert( u"path"_s, path );
1169
1170 return uriComponents;
1171}
1172
1173QList< Qgis::LayerType > QgsEsriI3SProviderMetadata::validLayerTypesForUri( const QString &uri ) const
1174{
1175 const QVariantMap parts = decodeUri( uri );
1176 QString filePath = parts.value( u"path"_s ).toString();
1177
1178 if ( filePath.endsWith( u".slpk"_s, Qt::CaseSensitivity::CaseInsensitive ) )
1179 return { Qgis::LayerType::TiledScene };
1180
1181 return {};
1182}
1183
1184QString QgsEsriI3SProviderMetadata::encodeUri( const QVariantMap &parts ) const
1185{
1186 QgsDataSourceUri dsUri;
1187 const QString partsKey = parts.contains( u"path"_s ) ? u"path"_s : u"url"_s;
1188 dsUri.setParam( u"url"_s, parts.value( partsKey ).toString() );
1189 return dsUri.encodedUri();
1190}
1191
1192QString QgsEsriI3SProviderMetadata::filters( Qgis::FileFilterType type )
1193{
1194 switch ( type )
1195 {
1202 return QString();
1203
1205 return QObject::tr( "ESRI Scene layer package" ) + u" (*.slpk *.SLPK)"_s;
1206 }
1207 return QString();
1208}
1209
1210QgsProviderMetadata::ProviderCapabilities QgsEsriI3SProviderMetadata::providerCapabilities() const
1211{
1212 return FileBasedUris;
1213}
1214
1215QList<Qgis::LayerType> QgsEsriI3SProviderMetadata::supportedLayerTypes() const
1216{
1217 return { Qgis::LayerType::TiledScene };
1218}
1219
1220QgsProviderMetadata::ProviderMetadataCapabilities QgsEsriI3SProviderMetadata::capabilities() const
1221{
1222 return ProviderMetadataCapability::PriorityForUri;
1223}
1224
1225int QgsEsriI3SProviderMetadata::priorityForUri( const QString &uri ) const
1226{
1227 const QVariantMap parts = decodeUri( uri );
1228 QString filePath = parts.value( u"path"_s ).toString();
1229
1230 if ( filePath.endsWith( u".slpk"_s, Qt::CaseSensitivity::CaseInsensitive ) )
1231 return 100;
1232
1233 return 0;
1234}
1235
QFlags< TiledSceneProviderCapability > TiledSceneProviderCapabilities
Tiled scene data provider capabilities.
Definition qgis.h:5932
QFlags< DataProviderFlag > DataProviderFlags
Data provider flags.
Definition qgis.h:2373
FileFilterType
Type of file filters.
Definition qgis.h:1412
@ TiledScene
Tiled scene layers.
Definition qgis.h:1419
@ Vector
Vector layers.
Definition qgis.h:1413
@ VectorTile
Vector tile layers.
Definition qgis.h:1418
@ Mesh
Mesh layers.
Definition qgis.h:1415
@ Raster
Raster layers.
Definition qgis.h:1414
@ MeshDataset
Mesh datasets.
Definition qgis.h:1416
@ PointCloud
Point clouds.
Definition qgis.h:1417
@ FastExtent2D
Provider's 2D extent retrieval via QgsDataProvider::extent() is always guaranteed to be trivial/fast ...
Definition qgis.h:2368
QFlags< DataProviderReadFlag > DataProviderReadFlags
Flags which control data provider construction.
Definition qgis.h:505
QFlags< SublayerQueryFlag > SublayerQueryFlags
Sublayer query flags.
Definition qgis.h:1457
TileChildrenAvailability
Possible availability states for a tile's children.
Definition qgis.h:5969
@ Available
Tile children are already available.
Definition qgis.h:5971
@ NeedFetching
Tile has children, but they are not yet available and must be fetched.
Definition qgis.h:5972
@ NoChildren
Tile is known to have no children.
Definition qgis.h:5970
@ TiledScene
Tiled scene layer. Added in QGIS 3.34.
Definition qgis.h:202
@ NoHierarchyFetch
Do not allow hierarchy fetching when hierarchy is not currently available. Avoids network requests,...
Definition qgis.h:5983
@ Z
Z-axis.
Definition qgis.h:2512
An abstract base class for tiled scene data provider indices.
virtual QVector< long long > childTileIds(long long id) const =0
Returns a list of the tile IDs of any children for the tile with matching id.
virtual long long parentTileId(long long id) const =0
Returns the tile ID of the parent tile of the tile with matching id, or -1 if the tile has no parent.
virtual QgsTiledSceneTile rootTile() const =0
Returns the root tile for the index.
virtual QByteArray fetchContent(const QString &uri, QgsFeedback *feedback=nullptr)=0
Fetches index content for the specified uri.
virtual bool fetchHierarchy(long long id, QgsFeedback *feedback=nullptr)=0
Populates the tile with the given id by fetching any sub datasets attached to the tile.
virtual QgsTiledSceneTile getTile(long long id)=0
Returns the tile with matching id, or an invalid tile if the matching tile is not available.
virtual Qgis::TileChildrenAvailability childAvailability(long long id) const =0
Returns the availability for a tile's children.
virtual QVector< long long > getTiles(const QgsTiledSceneRequest &request)=0
Returns the tile IDs which match the given request.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:45
double zMaximum() const
Returns the maximum z value.
Definition qgsbox3d.h:261
QgsRectangle toRectangle() const
Converts the box to a 2D rectangle.
Definition qgsbox3d.h:381
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:254
Represents a coordinate reference system (CRS).
Contains information about the context in which a coordinate transform is executed.
Handles coordinate transforms between two coordinate systems.
Stores the component parts of a data source URI (e.g.
QByteArray encodedUri() const
Returns the complete encoded URI as a byte array.
void setParam(const QString &key, const QString &value)
Sets a generic parameter value on the URI.
QgsRange which stores a range of double values.
Definition qgsrange.h:236
Represents a single error message.
Definition qgserror.h:35
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
A simple 4x4 matrix implementation useful for transformation in 3D space.
void translate(const QgsVector3D &vector)
Multiplies this matrix by another that translates coordinates by the components of a vector.
static QgsNetworkReplyContent blockingGet(QNetworkRequest &request, const QString &authCfg=QString(), bool forceRefresh=false, QgsFeedback *feedback=nullptr, Qgis::NetworkRequestFlags flags=Qgis::NetworkRequestFlags())
Posts a GET request to obtain the contents of the target request and returns a new QgsNetworkReplyCon...
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
QByteArray content() const
Returns the reply content.
QNetworkReply::NetworkError error() const
Returns the reply's error message, or QNetworkReply::NoError if no error was encountered.
Represents a oriented (rotated) box in 3 dimensions.
double longestSide() const
Returns size of the longest side of the box.
bool isNull() const
Returns true if the box is a null box.
QList< double > halfAxesList() const
Returns the half axes matrix;.
QgsVector3D center() const
Returns the vector to the center of the box.
Holds data provider key, description, and associated shared library file or function pointer informat...
QFlags< ProviderMetadataCapability > ProviderMetadataCapabilities
QFlags< ProviderCapability > ProviderCapabilities
Contains details about a sub layer available from a dataset.
void setUri(const QString &uri)
Sets the layer's uri.
void setType(Qgis::LayerType type)
Sets the layer type.
void setName(const QString &name)
Sets the layer's name.
void setProviderKey(const QString &key)
Sets the associated data provider key.
static QString suggestLayerNameFromFilePath(const QString &path)
Suggests a suitable layer name given only a file path.
A convenience class that simplifies locking and unlocking QReadWriteLocks.
A rectangle specified with double values.
Represents a bounding volume for a tiled scene.
bool intersects(const QgsOrientedBox3D &box) const
Returns true if this bounds intersects the specified box.
Base class for data providers for QgsTiledSceneLayer.
An index for tiled scene data providers.
Tiled scene data request.
QgsOrientedBox3D filterBox() const
Returns the box from which data will be taken.
long long parentTileId() const
Returns the parent tile ID, if filtering is limited to children of a specific tile.
double requiredGeometricError() const
Returns the required geometric error threshold for the returned tiles, in meters.
Qgis::TiledSceneRequestFlags flags() const
Returns the flags which affect how tiles are fetched.
Represents an individual tile from a tiled scene data source.
void setTransform(const QgsMatrix4x4 &transform)
Sets the tile's transform.
void setMetadata(const QVariantMap &metadata)
Sets additional metadata attached to the tile.
void setGeometricError(double error)
Sets the tile's geometric error, which is the error, in meters, of the tile's simplified representati...
const QgsTiledSceneBoundingVolume & boundingVolume() const
Returns the bounding volume for the tile.
void setBoundingVolume(const QgsTiledSceneBoundingVolume &volume)
Sets the bounding volume for the tile.
long long id() const
Returns the tile's unique ID.
void setResources(const QVariantMap &resources)
Sets the resources attached to the tile.
double geometricError() const
Returns the tile's geometric error, which is the error, in meters, of the tile's simplified represent...
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:33
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:52
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:54
double x() const
Returns X coordinate.
Definition qgsvector3d.h:50
static bool extractFileFromZip(const QString &zipFilename, const QString &filenameInZip, QByteArray &bytesOut)
Extracts a file from a zip archive, returns true on success.
static bool decodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Decodes gzip byte stream, returns true on success.
#define I3S_PROVIDER_DESCRIPTION
#define I3S_PROVIDER_KEY
#define QgsDebugError(str)
Definition qgslogger.h:59
#define QgsSetRequestInitiatorClass(request, _class)
#define QGIS_PROTECT_QOBJECT_THREAD_ACCESS
Setting options for creating vector data providers.