QGIS API Documentation 4.1.0-Master (d6fb7a379fb)
Loading...
Searching...
No Matches
qgscesiumtilesdataprovider.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscesiumtilesdataprovider.cpp
3 --------------------
4 begin : June 2023
5 copyright : (C) 2023 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19
20#include <nlohmann/json.hpp>
21
22#include "qgsapplication.h"
23#include "qgsauthmanager.h"
26#include "qgscesiumutils.h"
28#include "qgsellipsoidutils.h"
29#include "qgslogger.h"
31#include "qgsorientedbox3d.h"
33#include "qgsproviderutils.h"
34#include "qgsreadwritelocker.h"
36#include "qgssphere.h"
37#include "qgsthreadingutils.h"
40#include "qgstiledsceneindex.h"
41#include "qgstiledscenenode.h"
43#include "qgstiledscenetile.h"
44
45#include <QApplication>
46#include <QFileInfo>
47#include <QIcon>
48#include <QJsonDocument>
49#include <QJsonObject>
50#include <QNetworkRequest>
51#include <QRecursiveMutex>
52#include <QRegularExpression>
53#include <QString>
54#include <QUrl>
55#include <QUrlQuery>
56#include <qstringliteral.h>
57
58#include "moc_qgscesiumtilesdataprovider.cpp"
59
60using namespace Qt::StringLiterals;
61
63
64#define PROVIDER_KEY u"cesiumtiles"_s
65#define PROVIDER_DESCRIPTION u"Cesium 3D Tiles data provider"_s
66
67
68class QgsCesiumTiledSceneIndex final : public QgsAbstractTiledSceneIndex
69{
70 public:
71 QgsCesiumTiledSceneIndex( const json &tileset, const QUrl &rootUrl, const QString &authCfg, const QgsHttpHeaders &headers, const QgsCoordinateTransformContext &transformContext );
72
73 std::unique_ptr< QgsTiledSceneTile > tileFromJson( const json &node, const QUrl &baseUrl, const QgsTiledSceneTile *parent, Qgis::Axis gltfUpAxis );
74
79 std::unique_ptr< QgsTiledSceneNode > nodeFromJson( const json &node, const QUrl &baseUrl, QgsTiledSceneNode *parent, Qgis::Axis gltfUpAxis );
80 void refineNodeFromJson( QgsTiledSceneNode *node, const QUrl &baseUrl, const json &json );
81
82 QgsTiledSceneTile rootTile() const final;
83 QgsTiledSceneTile getTile( long long id ) final;
84 long long parentTileId( long long id ) const final;
85 QVector< long long > childTileIds( long long id ) const final;
86 QVector< long long > getTiles( const QgsTiledSceneRequest &request ) final;
87 Qgis::TileChildrenAvailability childAvailability( long long id ) const final;
88 bool fetchHierarchy( long long id, QgsFeedback *feedback = nullptr ) final;
89
90 protected:
91 QByteArray fetchContent( const QString &uri, QgsFeedback *feedback = nullptr ) final;
92
93 private:
94 enum class TileContentFormat
95 {
96 Json,
97 NotJson, // TODO: refine this to actual content types when/if needed!
98 };
99
100 void populateSubtreeRecursively(
101 QgsTiledSceneNode *node,
102 const QgsCesiumImplicitTiling::TileCoordinate &coord,
103 long long implicitRootTileId,
104 QgsCesiumImplicitTiling::Root &tilingData,
105 const QgsCesiumImplicitTiling::TileCoordinate &subtreeCoord
106 );
107
108 mutable QRecursiveMutex mLock;
109 QgsCoordinateTransformContext mTransformContext;
110 std::unique_ptr< QgsTiledSceneNode > mRootNode;
111 QMap< long long, QgsTiledSceneNode * > mNodeMap;
112 QMap< long long, TileContentFormat > mTileContentFormats;
113 QString mAuthCfg;
114 QgsHttpHeaders mHeaders;
115 long long mNextTileId = 0;
116
117 // Implicit tiling related
118
120 QMap<long long, QgsCesiumImplicitTiling::Root> mImplicitTilingRoots;
121
123 struct ImplicitTileInfo
124 {
126 long long implicitRootTileId = 0;
128 QgsCesiumImplicitTiling::TileCoordinate coord;
129 };
130
132 QMap<long long, ImplicitTileInfo> mImplicitTileIndex;
133};
134
135class QgsCesiumTilesDataProviderSharedData
136{
137 public:
138 QgsCesiumTilesDataProviderSharedData();
139 void initialize( const QString &tileset, const QUrl &rootUrl, const QgsCoordinateTransformContext &transformContext, const QString &authCfg, const QgsHttpHeaders &headers );
140
141 QgsCoordinateReferenceSystem mLayerCrs;
142 QgsCoordinateReferenceSystem mSceneCrs;
143 QgsTiledSceneBoundingVolume mBoundingVolume;
144
145 QgsRectangle mExtent;
146 nlohmann::json mTileset;
147 QgsDoubleRange mZRange;
148
149 QgsTiledSceneIndex mIndex;
150
151 QgsLayerMetadata mLayerMetadata;
152 QString mError;
153 QReadWriteLock mReadWriteLock;
154};
155
156
157//
158// QgsCesiumTiledSceneIndex
159//
160
161Qgis::Axis axisFromJson( const json &json )
162{
163 const std::string gltfUpAxisString = json.get<std::string>();
164 if ( gltfUpAxisString == "z" || gltfUpAxisString == "Z" )
165 {
166 return Qgis::Axis::Z;
167 }
168 else if ( gltfUpAxisString == "y" || gltfUpAxisString == "Y" )
169 {
170 return Qgis::Axis::Y;
171 }
172 else if ( gltfUpAxisString == "x" || gltfUpAxisString == "X" )
173 {
174 return Qgis::Axis::X;
175 }
176 QgsDebugError( u"Unsupported gltfUpAxis value: %1"_s.arg( QString::fromStdString( gltfUpAxisString ) ) );
177 return Qgis::Axis::Y;
178}
179
180QgsCesiumTiledSceneIndex::QgsCesiumTiledSceneIndex( const json &tileset, const QUrl &rootUrl, const QString &authCfg, const QgsHttpHeaders &headers, const QgsCoordinateTransformContext &transformContext )
181 : mTransformContext( transformContext )
182 , mAuthCfg( authCfg )
183 , mHeaders( headers )
184{
185 Qgis::Axis gltfUpAxis = Qgis::Axis::Y;
186 if ( tileset.contains( "asset" ) )
187 {
188 const auto &assetJson = tileset["asset"];
189 if ( assetJson.contains( "gltfUpAxis" ) )
190 {
191 gltfUpAxis = axisFromJson( assetJson["gltfUpAxis"] );
192 }
193 }
194
195 mRootNode = nodeFromJson( tileset["root"], rootUrl, nullptr, gltfUpAxis );
196}
197
198std::unique_ptr< QgsTiledSceneTile > QgsCesiumTiledSceneIndex::tileFromJson( const json &json, const QUrl &baseUrl, const QgsTiledSceneTile *parent, Qgis::Axis gltfUpAxis )
199{
200 auto tile = std::make_unique< QgsTiledSceneTile >( mNextTileId++ );
201
202 tile->setBaseUrl( baseUrl );
203 tile->setMetadata( {
204 { u"gltfUpAxis"_s, static_cast< int >( gltfUpAxis ) },
205 { u"contentFormat"_s, u"cesiumtiles"_s },
206 } );
207
208 QgsMatrix4x4 transform;
209 if ( json.contains( "transform" ) && !json["transform"].is_null() )
210 {
211 const auto &transformJson = json["transform"];
212 double *ptr = transform.data();
213 for ( int i = 0; i < 16; ++i )
214 ptr[i] = transformJson[i].get<double>();
215
216 if ( parent && parent->transform() )
217 {
218 transform = *parent->transform() * transform;
219 }
220 }
221 else if ( parent && parent->transform() )
222 {
223 transform = *parent->transform();
224 }
225 if ( !transform.isIdentity() )
226 tile->setTransform( transform );
227
228 const auto &boundingVolume = json["boundingVolume"];
230 if ( boundingVolume.contains( "region" ) )
231 {
232 const QgsBox3D region = QgsCesiumUtils::parseRegion( boundingVolume["region"] );
233 if ( !region.isNull() )
234 {
235 volume = QgsCesiumUtils::boundingVolumeFromRegion( region, mTransformContext );
236 }
237 }
238 else if ( boundingVolume.contains( "box" ) )
239 {
240 const QgsOrientedBox3D bbox = QgsCesiumUtils::parseBox( boundingVolume["box"] );
241 if ( !bbox.isNull() )
242 {
243 volume = QgsTiledSceneBoundingVolume( bbox );
244 if ( !transform.isIdentity() )
245 volume.transform( transform );
246 }
247 }
248 else if ( boundingVolume.contains( "sphere" ) )
249 {
250 QgsSphere sphere = QgsCesiumUtils::parseSphere( boundingVolume["sphere"] );
251 if ( !sphere.isNull() )
252 {
253 sphere = QgsCesiumUtils::transformSphere( sphere, transform );
255 }
256 }
257 else
258 {
259 QgsDebugError( u"unsupported boundingVolume format"_s );
260 }
261
262 tile->setBoundingVolume( volume );
263
264 if ( json.contains( "geometricError" ) )
265 tile->setGeometricError( json["geometricError"].get< double >() );
266 if ( json.contains( "refine" ) )
267 {
268 if ( json["refine"] == "ADD" )
269 tile->setRefinementProcess( Qgis::TileRefinementProcess::Additive );
270 else if ( json["refine"] == "REPLACE" )
271 tile->setRefinementProcess( Qgis::TileRefinementProcess::Replacement );
272 }
273 else if ( parent )
274 {
275 // children inherit the parent refinement if not explicitly set -- see https://github.com/CesiumGS/cesium-native/blob/172ac5ddcce602c8b268ad342639554dea2f6004/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp#L440C5-L440C40
276 tile->setRefinementProcess( parent->refinementProcess() );
277 }
278
279 if ( json.contains( "content" ) && !json["content"].is_null() )
280 {
281 const auto &contentJson = json["content"];
282
283 // sometimes URI, sometimes URL...
284 QString contentUri;
285 if ( contentJson.contains( "uri" ) && !contentJson["uri"].is_null() )
286 {
287 QString relativeUri = QString::fromStdString( contentJson["uri"].get<std::string>() );
288 contentUri = baseUrl.resolved( QUrl( relativeUri ) ).toString();
289
290 if ( baseUrl.hasQuery() && QUrl( relativeUri ).isRelative() )
291 contentUri = QgsCesiumUtils::appendQueryFromBaseUrl( contentUri, baseUrl );
292 }
293 else if ( contentJson.contains( "url" ) && !contentJson["url"].is_null() )
294 {
295 QString relativeUri = QString::fromStdString( contentJson["url"].get<std::string>() );
296 contentUri = baseUrl.resolved( QUrl( relativeUri ) ).toString();
297
298 if ( baseUrl.hasQuery() && QUrl( relativeUri ).isRelative() )
299 contentUri = QgsCesiumUtils::appendQueryFromBaseUrl( contentUri, baseUrl );
300 }
301 if ( !contentUri.isEmpty() )
302 {
303 tile->setResources( { { u"content"_s, contentUri } } );
304 }
305 }
306
307 return tile;
308}
309
310std::unique_ptr<QgsTiledSceneNode> QgsCesiumTiledSceneIndex::nodeFromJson( const json &json, const QUrl &baseUrl, QgsTiledSceneNode *parent, Qgis::Axis gltfUpAxis )
311{
312 std::unique_ptr< QgsTiledSceneTile > tile = tileFromJson( json, baseUrl, parent ? parent->tile() : nullptr, gltfUpAxis );
313 auto newNode = std::make_unique< QgsTiledSceneNode >( tile.release() );
314 mNodeMap.insert( newNode->tile()->id(), newNode.get() );
315
316 if ( json.contains( "implicitTiling" ) )
317 {
319 if ( QgsCesiumImplicitTiling::parseImplicitTiling( json, newNode.get(), baseUrl, gltfUpAxis, tilingData ) )
320 {
321 const long long rootTileId = newNode->tile()->id();
322 mImplicitTilingRoots.insert( rootTileId, tilingData );
323 mImplicitTileIndex.insert( rootTileId, { rootTileId, { 0, 0, 0 } } );
324 }
325 }
326 else if ( json.contains( "children" ) )
327 {
328 for ( const auto &childJson : json["children"] )
329 {
330 nodeFromJson( childJson, baseUrl, newNode.get(), gltfUpAxis );
331 }
332 }
333
334 if ( parent )
335 {
336 parent->addChild( newNode.release() );
337 return nullptr;
338 }
339 else
340 {
341 return newNode;
342 }
343}
344
345void QgsCesiumTiledSceneIndex::refineNodeFromJson( QgsTiledSceneNode *node, const QUrl &baseUrl, const json &json )
346{
347 const auto &rootTileJson = json["root"];
348
349 Qgis::Axis gltfUpAxis = Qgis::Axis::Y;
350 if ( json.contains( "asset" ) )
351 {
352 const auto &assetJson = json["asset"];
353 if ( assetJson.contains( "gltfUpAxis" ) )
354 {
355 gltfUpAxis = axisFromJson( assetJson["gltfUpAxis"] );
356 }
357 }
358
359 std::unique_ptr< QgsTiledSceneTile > newTile = tileFromJson( rootTileJson, baseUrl, node->tile(), gltfUpAxis );
360 // copy just the resources from the retrieved tileset to the refined node. We assume all the rest of the tile content
361 // should be the same between the node being refined and the root node of the fetched sub dataset!
362 // (Ie the bounding volume, geometric error, etc).
363 node->tile()->setResources( newTile->resources() );
364
365
366 // root tile of the sub dataset may have transform as well, we need to bring it back
367 // (actually even the referencing tile may have transform - if that's the case,
368 // that transform got combined with the root tile's transform in tileFromJson)
369 if ( newTile->transform() )
370 node->tile()->setTransform( *newTile->transform() );
371
372 if ( rootTileJson.contains( "children" ) )
373 {
374 for ( const auto &childJson : rootTileJson["children"] )
375 {
376 nodeFromJson( childJson, baseUrl, node, gltfUpAxis );
377 }
378 }
379}
380
381QgsTiledSceneTile QgsCesiumTiledSceneIndex::rootTile() const
382{
383 QMutexLocker locker( &mLock );
384 return mRootNode ? *mRootNode->tile() : QgsTiledSceneTile();
385}
386
387QgsTiledSceneTile QgsCesiumTiledSceneIndex::getTile( long long id )
388{
389 QMutexLocker locker( &mLock );
390 auto it = mNodeMap.constFind( id );
391 if ( it != mNodeMap.constEnd() )
392 {
393 return *( it.value()->tile() );
394 }
395
396 return QgsTiledSceneTile();
397}
398
399long long QgsCesiumTiledSceneIndex::parentTileId( long long id ) const
400{
401 QMutexLocker locker( &mLock );
402 auto it = mNodeMap.constFind( id );
403 if ( it != mNodeMap.constEnd() )
404 {
405 if ( QgsTiledSceneNode *parent = it.value()->parentNode() )
406 {
407 return parent->tile()->id();
408 }
409 }
410
411 return -1;
412}
413
414QVector< long long > QgsCesiumTiledSceneIndex::childTileIds( long long id ) const
415{
416 QMutexLocker locker( &mLock );
417 auto it = mNodeMap.constFind( id );
418 if ( it != mNodeMap.constEnd() )
419 {
420 QVector< long long > childIds;
421 const QList< QgsTiledSceneNode * > children = it.value()->children();
422 childIds.reserve( children.size() );
423 for ( QgsTiledSceneNode *child : children )
424 {
425 childIds << child->tile()->id();
426 }
427 return childIds;
428 }
429
430 return {};
431}
432
433QVector< long long > QgsCesiumTiledSceneIndex::getTiles( const QgsTiledSceneRequest &request )
434{
435 QVector< long long > results;
436
437 std::function< void( QgsTiledSceneNode * )> traverseNode;
438 traverseNode = [&request, &traverseNode, &results, this]( QgsTiledSceneNode *node ) {
439 QgsTiledSceneTile *tile = node->tile();
440
441 // check filter box first -- if the node doesn't intersect, then don't include the node and don't traverse
442 // to its children
443 if ( !request.filterBox().isNull() && !tile->boundingVolume().box().isNull() && !tile->boundingVolume().intersects( request.filterBox() ) )
444 return;
445
446 // TODO -- option to filter out nodes without content
447
448 if ( request.requiredGeometricError() <= 0 || tile->geometricError() <= 0 || tile->geometricError() > request.requiredGeometricError() )
449 {
450 // haven't traversed deep enough down this node, we need to explore children
451
452 // are children available?
453 QList< QgsTiledSceneNode * > children = node->children();
454 if ( children.empty() )
455 {
456 switch ( childAvailability( tile->id() ) )
457 {
460 break;
462 {
464 {
465 // do a blocking fetch of children
466 if ( fetchHierarchy( tile->id() ), request.feedback() )
467 {
468 children = node->children();
469 }
470 }
471 break;
472 }
473 }
474 }
475
476 for ( QgsTiledSceneNode *child : std::as_const( children ) )
477 {
478 if ( request.feedback() && request.feedback()->isCanceled() )
479 break;
480
481 traverseNode( child );
482 }
483
484 switch ( tile->refinementProcess() )
485 {
487 // child add to parent content, so we must also include the parent
488 results << tile->id();
489 break;
490
492 // children replace the parent, so we skip the parent if we found children
493 if ( children.empty() )
494 results << tile->id();
495 break;
496 }
497 }
498 else
499 {
500 results << tile->id();
501 }
502 };
503
504 QMutexLocker locker( &mLock );
505 if ( request.parentTileId() < 0 )
506 {
507 if ( mRootNode )
508 traverseNode( mRootNode.get() );
509 }
510 else
511 {
512 auto it = mNodeMap.constFind( request.parentTileId() );
513 if ( it != mNodeMap.constEnd() )
514 {
515 traverseNode( it.value() );
516 }
517 }
518
519 return results;
520}
521
522Qgis::TileChildrenAvailability QgsCesiumTiledSceneIndex::childAvailability( long long id ) const
523{
524 QString contentUri;
525 QMutexLocker locker( &mLock );
526 {
527 auto it = mNodeMap.constFind( id );
528 if ( it == mNodeMap.constEnd() )
530
531 // Check if this is an implicit tile
532 auto implicitIt = mImplicitTileIndex.constFind( id );
533 if ( implicitIt != mImplicitTileIndex.constEnd() )
534 {
535 const ImplicitTileInfo &info = implicitIt.value();
536 const auto tilingDataIt = mImplicitTilingRoots.constFind( info.implicitRootTileId );
537 if ( tilingDataIt == mImplicitTilingRoots.constEnd() )
539 const QgsCesiumImplicitTiling::Root &tilingData = tilingDataIt.value();
540
541 if ( !it.value()->children().isEmpty() )
542 return Qgis::TileChildrenAvailability::Available; // children have been populated already
543
544 return QgsCesiumImplicitTiling::childAvailability( tilingData, info.coord );
545 }
546
547 if ( !it.value()->children().isEmpty() )
549
550 contentUri = it.value()->tile()->resources().value( u"content"_s ).toString();
551 }
552 {
553 // maybe we already retrieved content for this node and know the answer:
554 auto it = mTileContentFormats.constFind( id );
555 if ( it != mTileContentFormats.constEnd() )
556 {
557 switch ( it.value() )
558 {
559 case TileContentFormat::NotJson:
561 case TileContentFormat::Json:
563 }
564 }
565 }
566 locker.unlock();
567
568 if ( contentUri.isEmpty() )
570
571 // https://github.com/CesiumGS/3d-tiles/tree/main/specification#tile-json says:
572 // "A file extension is not required for content.uri. A content’s tile format can
573 // be identified by the magic field in its header, or else as an external tileset if the content is JSON."
574 // This is rather annoying... it means we have to do a network request in order to determine whether
575 // a tile has children or geometry content!
576
577 // let's avoid this request if we can get away with it:
578 const thread_local QRegularExpression isJsonRx( u".*\\.json(?:\\?.*)?$"_s, QRegularExpression::PatternOption::CaseInsensitiveOption );
579 if ( isJsonRx.match( contentUri ).hasMatch() )
581
582 // things we know definitely CAN'T be a child tile map:
583 const thread_local QRegularExpression antiCandidateRx( u".*\\.(gltf|glb|b3dm|i3dm|pnts|cmpt|bin|glbin|glbuf|png|jpeg|jpg)(?:\\?.*)?$"_s, QRegularExpression::PatternOption::CaseInsensitiveOption );
584 if ( antiCandidateRx.match( contentUri ).hasMatch() )
586
587 // here we **could** do a fetch to verify what the content actually is. But we want this method to be non-blocking,
588 // so let's just report that there IS remote children available and then sort things out when we actually go to fetch those children...
590}
591
592void QgsCesiumTiledSceneIndex::populateSubtreeRecursively(
593 QgsTiledSceneNode *node, const QgsCesiumImplicitTiling::TileCoordinate &coord, long long implicitRootTileId, QgsCesiumImplicitTiling::Root &tilingData, const QgsCesiumImplicitTiling::TileCoordinate &subtreeCoord
594)
595{
596 if ( !node->children().isEmpty() )
597 return; // already populated
598
599 // Add immediate children
600 QMap<QgsCesiumImplicitTiling::TileCoordinate, QgsTiledSceneNode *> children = QgsCesiumImplicitTiling::createImplicitTilingChildren( node, coord, tilingData, subtreeCoord, mTransformContext, mNextTileId );
601 for ( auto it = children.constBegin(); it != children.constEnd(); ++it )
602 {
603 long long childId = it.value()->tile()->id();
604 mImplicitTileIndex.insert( childId, { implicitRootTileId, it.key() } );
605 mNodeMap.insert( childId, it.value() );
606 }
607
608 // Recurse into children that are still within the current subtree (not child subtree roots)
609 for ( QgsTiledSceneNode *child : node->children() )
610 {
611 const auto childIt = mImplicitTileIndex.constFind( child->tile()->id() );
612 if ( childIt == mImplicitTileIndex.constEnd() )
613 continue;
614
615 const QgsCesiumImplicitTiling::TileCoordinate &childCoord = childIt->coord;
616 const int childLocalLevel = childCoord.level - subtreeCoord.level;
617
618 // Only recurse for children within this subtree, not for child subtree roots
619 if ( childLocalLevel < tilingData.subtreeLevels )
620 populateSubtreeRecursively( child, childCoord, implicitRootTileId, tilingData, subtreeCoord );
621 }
622}
623
624
625bool QgsCesiumTiledSceneIndex::fetchHierarchy( long long id, QgsFeedback *feedback )
626{
627 QMutexLocker locker( &mLock );
628 auto it = mNodeMap.constFind( id );
629 if ( it == mNodeMap.constEnd() )
630 return false;
631
632 // Handle implicit tiles
633 auto implicitIt = mImplicitTileIndex.constFind( id );
634 if ( implicitIt != mImplicitTileIndex.constEnd() )
635 {
636 const ImplicitTileInfo &info = implicitIt.value();
637 QgsCesiumImplicitTiling::Root &tilingData = mImplicitTilingRoots[info.implicitRootTileId];
638
639 if ( !it.value()->children().isEmpty() )
640 return true;
641
643 Q_ASSERT( info.coord.level == subtreeCoord.level );
644
645 // Check whether we have subtree data - may need to fetch a subtree
646 if ( !tilingData.subtreeCache.contains( subtreeCoord ) )
647 {
648 const QString subtreeUri = QgsCesiumImplicitTiling::expandTemplateUri( tilingData.subtreeUriTemplate, tilingData.baseUrl, subtreeCoord );
649
650 locker.unlock();
651 const QByteArray data = retrieveContent( subtreeUri, feedback );
652 locker.relock();
653
655 tilingData.subtreeCache.insert( subtreeCoord, subtree );
656 }
657 const QgsCesiumImplicitTiling::Subtree &subtree = tilingData.subtreeCache[subtreeCoord];
658
659 // resolve this tile's own content
660 if ( it.value()->tile()->resources().isEmpty() && !tilingData.contentUriTemplate.isEmpty() )
661 {
662 const int rootBitIdx = QgsCesiumImplicitTiling::subtreeBitIndex( 0, 0, 0 );
663 if ( rootBitIdx < subtree.contentAvailability.size() && subtree.contentAvailability.testBit( rootBitIdx ) )
664 {
665 const QString rootContentUri = QgsCesiumImplicitTiling::expandTemplateUri( tilingData.contentUriTemplate, tilingData.baseUrl, info.coord );
666 it.value()->tile()->setResources( { { u"content"_s, rootContentUri } } );
667 }
668 }
669
670 populateSubtreeRecursively( it.value(), info.coord, info.implicitRootTileId, tilingData, subtreeCoord );
671
672 return !it.value()->children().isEmpty();
673 }
674
675 {
676 // maybe we already know what content type this tile has. If so, and it's not json, then
677 // don't try to fetch it as a hierarchy
678 auto it = mTileContentFormats.constFind( id );
679 if ( it != mTileContentFormats.constEnd() )
680 {
681 switch ( it.value() )
682 {
683 case TileContentFormat::NotJson:
684 return false;
685 case TileContentFormat::Json:
686 break;
687 }
688 }
689 }
690
691 const QString contentUri = it.value()->tile()->resources().value( u"content"_s ).toString();
692 locker.unlock();
693
694 if ( contentUri.isEmpty() )
695 return false;
696
697 // if node has content json, fetch it now and parse
698 const QByteArray subTile = retrieveContent( contentUri, feedback );
699 if ( !subTile.isEmpty() )
700 {
701 // we don't know for certain that the content IS json -- from https://github.com/CesiumGS/3d-tiles/tree/main/specification#tile-json says:
702 // "A file extension is not required for content.uri. A content’s tile format can
703 // be identified by the magic field in its header, or else as an external tileset if the content is JSON."
704 try
705 {
706 const auto subTileJson = json::parse( subTile.toStdString() );
707 QMutexLocker locker( &mLock );
708 refineNodeFromJson( it.value(), QUrl( contentUri ), subTileJson );
709 mTileContentFormats.insert( id, TileContentFormat::Json );
710 return true;
711 }
712 catch ( json::parse_error & )
713 {
714 QMutexLocker locker( &mLock );
715 mTileContentFormats.insert( id, TileContentFormat::NotJson );
716 return false;
717 }
718 }
719 else
720 {
721 // we got empty content, so the hierarchy content is probably missing,
722 // so let's mark it as not JSON so that we do not try to fetch it again
723 mTileContentFormats.insert( id, TileContentFormat::NotJson );
724 return false;
725 }
726}
727
728QByteArray QgsCesiumTiledSceneIndex::fetchContent( const QString &uri, QgsFeedback *feedback )
729{
730 QUrl url( uri );
731 // TODO -- error reporting?
732 if ( uri.startsWith( "http" ) )
733 {
734 QNetworkRequest networkRequest = QNetworkRequest( url );
735 QgsSetRequestInitiatorClass( networkRequest, u"QgsCesiumTiledSceneIndex"_s );
736 networkRequest.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
737 networkRequest.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
738
739 mHeaders.updateNetworkRequest( networkRequest );
740
741 if ( QThread::currentThread() == QApplication::instance()->thread() )
742 {
743 // running on main thread, use a blocking get to handle authcfg and SSL errors ok.
744 const QgsNetworkReplyContent reply = QgsNetworkAccessManager::instance()->blockingGet( networkRequest, mAuthCfg, false, feedback );
745 return reply.content();
746 }
747 else
748 {
749 // running on background thread, use tile download manager for efficient network handling
750 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( networkRequest, mAuthCfg ) )
751 {
752 // TODO -- report error
753 return QByteArray();
754 }
755 std::unique_ptr< QgsTileDownloadManagerReply > reply( QgsApplication::tileDownloadManager()->get( networkRequest ) );
756
757 QEventLoop loop;
758 if ( feedback )
759 QObject::connect( feedback, &QgsFeedback::canceled, &loop, &QEventLoop::quit );
760
761 QObject::connect( reply.get(), &QgsTileDownloadManagerReply::finished, &loop, &QEventLoop::quit );
762 loop.exec();
763
764 return reply->data();
765 }
766 }
767 else if ( url.isLocalFile() && QFile::exists( url.toLocalFile() ) )
768 {
769 QFile file( url.toLocalFile() );
770 if ( file.open( QIODevice::ReadOnly ) )
771 {
772 return file.readAll();
773 }
774 }
775 return QByteArray();
776}
777
778
779//
780// QgsCesiumTilesDataProviderSharedData
781//
782
783QgsCesiumTilesDataProviderSharedData::QgsCesiumTilesDataProviderSharedData()
784 : mIndex( QgsTiledSceneIndex( nullptr ) )
785{}
786
787void QgsCesiumTilesDataProviderSharedData::initialize( const QString &tileset, const QUrl &rootUrl, const QgsCoordinateTransformContext &transformContext, const QString &authCfg, const QgsHttpHeaders &headers )
788{
789 mTileset = json::parse( tileset.toStdString() );
790 if ( !mTileset.contains( "root" ) )
791 {
792 mError = QObject::tr( "JSON is not a valid Cesium 3D Tiles source (does not contain \"root\" value)" );
793 return;
794 }
795
796 mLayerMetadata.setType( u"dataset"_s );
797
798 if ( mTileset.contains( "asset" ) )
799 {
800 const auto &asset = mTileset["asset"];
801 if ( asset.contains( "tilesetVersion" ) )
802 {
803 try
804 {
805 const QString tilesetVersion = QString::fromStdString( asset["tilesetVersion"].get<std::string>() );
806 mLayerMetadata.setIdentifier( tilesetVersion );
807 }
808 catch ( json::type_error & )
809 {
810 QgsDebugError( u"Error when parsing tilesetVersion value"_s );
811 }
812 }
813 }
814
815 mIndex = QgsTiledSceneIndex( new QgsCesiumTiledSceneIndex( mTileset, rootUrl, authCfg, headers, transformContext ) );
816
817 // parse root
818 {
819 const auto &root = mTileset["root"];
820 // parse root bounding volume
821
822 // TODO -- read crs from metadata tags. Need to find real world examples of this. And can metadata crs override
823 // the EPSG:4979 requirement from a region bounding volume??
824
825 {
826 // TODO -- on some datasets there is a "boundingVolume" present on the tileset itself, i.e. not the root node.
827 // what does this mean? Should we use it instead of the root node bounding volume if it's present?
828
830
831 const auto &rootBoundingVolume = root["boundingVolume"];
832
833 QgsMatrix4x4 rootTransform;
834 if ( root.contains( "transform" ) && !root["transform"].is_null() )
835 {
836 const auto &transformJson = root["transform"];
837 double *ptr = rootTransform.data();
838 for ( int i = 0; i < 16; ++i )
839 ptr[i] = transformJson[i].get<double>();
840 }
841
842 if ( rootBoundingVolume.contains( "region" ) )
843 {
844 const QgsBox3D rootRegion = QgsCesiumUtils::parseRegion( rootBoundingVolume["region"] );
845 if ( !rootRegion.isNull() )
846 {
847 mBoundingVolume = QgsTiledSceneBoundingVolume( QgsOrientedBox3D::fromBox3D( rootRegion ) );
848
849 // only set z range for datasets which aren't too large (ie global datasets)
850 if ( !mIndex.rootTile().boundingVolume().box().isNull() )
851 {
852 mZRange = QgsDoubleRange( rootRegion.zMinimum(), rootRegion.zMaximum() );
853 }
854 mLayerCrs = QgsCoordinateReferenceSystem( u"EPSG:4979"_s );
855 mSceneCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
856
857 mLayerMetadata.setCrs( mSceneCrs );
858 mExtent = rootRegion.toRectangle();
859 spatialExtent.extentCrs = QgsCoordinateReferenceSystem( u"EPSG:4979"_s );
860 spatialExtent.bounds = rootRegion;
861 }
862 }
863 else if ( rootBoundingVolume.contains( "box" ) )
864 {
865 const QgsOrientedBox3D bbox = QgsCesiumUtils::parseBox( rootBoundingVolume["box"] );
866 if ( !bbox.isNull() )
867 {
868 // layer must advertise as EPSG:4979, as the various QgsMapLayer
869 // methods which utilize QgsMapLayer::crs() (such as layer extent transformation)
870 // are all purely 2D and can't handle the cesium data source z value
871 // range in EPSG:4978 !
872 mLayerCrs = QgsCoordinateReferenceSystem( u"EPSG:4979"_s );
873 mSceneCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
874 mLayerMetadata.setCrs( mSceneCrs );
875
876 mBoundingVolume = QgsTiledSceneBoundingVolume( bbox );
877 mBoundingVolume.transform( rootTransform );
878 try
879 {
880 QgsCoordinateTransform ct( mSceneCrs, mLayerCrs, transformContext );
881 ct.setBallparkTransformsAreAppropriate( true );
882 const QgsBox3D rootRegion = mBoundingVolume.bounds( ct );
883 // only set z range for datasets which aren't too large (ie global datasets)
884 if ( !mIndex.rootTile().boundingVolume().box().isNull() )
885 {
886 mZRange = QgsDoubleRange( rootRegion.zMinimum(), rootRegion.zMaximum() );
887 }
888
889 std::unique_ptr< QgsAbstractGeometry > extent2D( mBoundingVolume.as2DGeometry( ct ) );
890 mExtent = extent2D->boundingBox();
891 }
892 catch ( QgsCsException & )
893 {
894 QgsDebugError( u"Caught transform exception when transforming boundingVolume"_s );
895 }
896
897 spatialExtent.extentCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
898 spatialExtent.bounds = mBoundingVolume.bounds();
899 }
900 }
901 else if ( rootBoundingVolume.contains( "sphere" ) )
902 {
903 QgsSphere sphere = QgsCesiumUtils::parseSphere( rootBoundingVolume["sphere"] );
904 if ( !sphere.isNull() )
905 {
906 // layer must advertise as EPSG:4979, as the various QgsMapLayer
907 // methods which utilize QgsMapLayer::crs() (such as layer extent transformation)
908 // are all purely 2D and can't handle the cesium data source z value
909 // range in EPSG:4978 !
910 mLayerCrs = QgsCoordinateReferenceSystem( u"EPSG:4979"_s );
911 mSceneCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
912 mLayerMetadata.setCrs( mSceneCrs );
913
914 sphere = QgsCesiumUtils::transformSphere( sphere, rootTransform );
915
917 try
918 {
919 QgsCoordinateTransform ct( mSceneCrs, mLayerCrs, transformContext );
920 ct.setBallparkTransformsAreAppropriate( true );
921 const QgsBox3D rootRegion = mBoundingVolume.bounds( ct );
922 // only set z range for datasets which aren't too large (ie global datasets)
923 if ( !mIndex.rootTile().boundingVolume().box().isNull() )
924 {
925 mZRange = QgsDoubleRange( rootRegion.zMinimum(), rootRegion.zMaximum() );
926 }
927
928 std::unique_ptr< QgsAbstractGeometry > extent2D( mBoundingVolume.as2DGeometry( ct ) );
929 mExtent = extent2D->boundingBox();
930 }
931 catch ( QgsCsException & )
932 {
933 QgsDebugError( u"Caught transform exception when transforming boundingVolume"_s );
934 }
935
936 spatialExtent.extentCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
937 spatialExtent.bounds = mBoundingVolume.bounds();
938 }
939 }
940 else
941 {
942 mError = QObject::tr( "JSON is not a valid Cesium 3D Tiles source (unsupported boundingVolume format)" );
943 return;
944 }
945
946 QgsLayerMetadata::Extent layerExtent;
947 layerExtent.setSpatialExtents( { spatialExtent } );
948 mLayerMetadata.setExtent( layerExtent );
949 }
950 }
951}
952
953
954//
955// QgsCesiumTilesDataProvider
956//
957
958QgsCesiumTilesDataProvider::QgsCesiumTilesDataProvider( const QString &uri, const ProviderOptions &providerOptions, Qgis::DataProviderReadFlags flags )
959 : QgsTiledSceneDataProvider( uri, providerOptions, flags )
960 , mShared( std::make_shared< QgsCesiumTilesDataProviderSharedData >() )
961{
962 QgsDataSourceUri dsUri;
963 dsUri.setEncodedUri( uri );
964 mAuthCfg = dsUri.authConfigId();
965 mHeaders = dsUri.httpHeaders();
966
967 mIsValid = init();
968}
969
970QgsCesiumTilesDataProvider::QgsCesiumTilesDataProvider( const QgsCesiumTilesDataProvider &other )
972 , mIsValid( other.mIsValid )
973 , mAuthCfg( other.mAuthCfg )
974 , mHeaders( other.mHeaders )
975{
976 QgsReadWriteLocker locker( other.mShared->mReadWriteLock, QgsReadWriteLocker::Read );
977 mShared = other.mShared;
978}
979
980Qgis::DataProviderFlags QgsCesiumTilesDataProvider::flags() const
981{
982 return mProviderFlags;
983}
984
985Qgis::TiledSceneProviderCapabilities QgsCesiumTilesDataProvider::capabilities() const
986{
988}
989
990QgsCesiumTilesDataProvider::~QgsCesiumTilesDataProvider() = default;
991
992QgsCesiumTilesDataProvider *QgsCesiumTilesDataProvider::clone() const
993{
995 return new QgsCesiumTilesDataProvider( *this );
996}
997
998bool QgsCesiumTilesDataProvider::init()
999{
1001
1002 QString tileSetUri;
1003 const QString uri = dataSourceUri();
1004
1005 if ( uri.startsWith( "ion://"_L1 ) )
1006 {
1007 QUrl url( uri );
1008 const QString assetId = QUrlQuery( url ).queryItemValue( u"assetId"_s );
1009 const QString accessToken = QUrlQuery( url ).queryItemValue( u"accessToken"_s );
1010
1011 const QString CESIUM_ION_URL = u"https://api.cesium.com/"_s;
1012
1013 // get asset info
1014 {
1015 const QString assetInfoEndpoint = CESIUM_ION_URL + u"v1/assets/%1"_s.arg( assetId );
1016 QNetworkRequest request = QNetworkRequest( assetInfoEndpoint );
1017 QgsSetRequestInitiatorClass( request, u"QgsCesiumTilesDataProvider"_s ) mHeaders.updateNetworkRequest( request );
1018 if ( !accessToken.isEmpty() )
1019 request.setRawHeader( "Authorization", u"Bearer %1"_s.arg( accessToken ).toLocal8Bit() );
1020
1021 QgsBlockingNetworkRequest networkRequest;
1022 if ( accessToken.isEmpty() )
1023 networkRequest.setAuthCfg( mAuthCfg );
1024
1025 switch ( networkRequest.get( request ) )
1026 {
1028 break;
1029
1033 // TODO -- error reporting
1034 return false;
1035 }
1036
1037 const QgsNetworkReplyContent content = networkRequest.reply();
1038 const json assetInfoJson = json::parse( content.content().toStdString() );
1039 if ( assetInfoJson["type"] != "3DTILES" )
1040 {
1041 appendError( QgsErrorMessage( tr( "Only ion 3D Tiles content can be accessed, not %1" ).arg( QString::fromStdString( assetInfoJson["type"].get<std::string>() ) ) ) );
1042 return false;
1043 }
1044
1045 const QString name = QString::fromStdString( assetInfoJson["name"].get<std::string>() );
1046 if ( name.compare( "Google Photorealistic 3D Tiles"_L1, Qt::CaseInsensitive ) == 0 )
1047 {
1048 // consider Google Photorealistic 3D Tiles as a basemap source, as this completely covers
1049 // the globe and contains embedded terrain
1050 mProviderFlags.setFlag( Qgis::DataProviderFlag::IsBasemapSource, true );
1051 mProviderFlags.setFlag( Qgis::DataProviderFlag::Is3DBasemapSource, true );
1052 }
1053
1054 mShared->mLayerMetadata.setTitle( name );
1055 mShared->mLayerMetadata.setAbstract( QString::fromStdString( assetInfoJson["description"].get<std::string>() ) );
1056 const QString attribution = QString::fromStdString( assetInfoJson["attribution"].get<std::string>() );
1057 if ( !attribution.isEmpty() )
1058 mShared->mLayerMetadata.setRights( { attribution } );
1059
1060 mShared->mLayerMetadata.setDateTime( Qgis::MetadataDateType::Created, QDateTime::fromString( QString::fromStdString( assetInfoJson["dateAdded"].get<std::string>() ), Qt::DateFormat::ISODate ) );
1061 }
1062
1063 // get tileset access details
1064 {
1065 const QString tileAccessEndpoint = CESIUM_ION_URL + u"v1/assets/%1/endpoint"_s.arg( assetId );
1066 QNetworkRequest request = QNetworkRequest( tileAccessEndpoint );
1067 QgsSetRequestInitiatorClass( request, u"QgsCesiumTilesDataProvider"_s ) mHeaders.updateNetworkRequest( request );
1068 if ( !accessToken.isEmpty() )
1069 request.setRawHeader( "Authorization", u"Bearer %1"_s.arg( accessToken ).toLocal8Bit() );
1070
1071 QgsBlockingNetworkRequest networkRequest;
1072 if ( accessToken.isEmpty() )
1073 networkRequest.setAuthCfg( mAuthCfg );
1074
1075 switch ( networkRequest.get( request ) )
1076 {
1078 break;
1079
1083 // TODO -- error reporting
1084 return false;
1085 }
1086
1087 const QgsNetworkReplyContent content = networkRequest.reply();
1088 const json tileAccessJson = json::parse( content.content().toStdString() );
1089
1090 if ( tileAccessJson.contains( "url" ) )
1091 {
1092 tileSetUri = QString::fromStdString( tileAccessJson["url"].get<std::string>() );
1093 }
1094 else if ( tileAccessJson.contains( "options" ) )
1095 {
1096 const auto &optionsJson = tileAccessJson["options"];
1097 if ( optionsJson.contains( "url" ) )
1098 {
1099 tileSetUri = QString::fromStdString( optionsJson["url"].get<std::string>() );
1100 }
1101 }
1102
1103 if ( tileAccessJson.contains( "accessToken" ) )
1104 {
1105 // The tileset accessToken is NOT the same as the token we use to access the asset details -- ie we can't
1106 // use the same authentication as we got from the providers auth cfg!
1107 mHeaders.insert( u"Authorization"_s, u"Bearer %1"_s.arg( QString::fromStdString( tileAccessJson["accessToken"].get<std::string>() ) ) );
1108 }
1109 mAuthCfg.clear();
1110 }
1111 }
1112 else
1113 {
1114 QgsDataSourceUri dsUri;
1115 dsUri.setEncodedUri( uri );
1116 tileSetUri = dsUri.param( u"url"_s );
1117 }
1118
1119 if ( !tileSetUri.isEmpty() )
1120 {
1121 const QUrl url( tileSetUri );
1122
1123 QNetworkRequest request = QNetworkRequest( url );
1124 QgsSetRequestInitiatorClass( request, u"QgsCesiumTilesDataProvider"_s ) mHeaders.updateNetworkRequest( request );
1125
1126 QgsBlockingNetworkRequest networkRequest;
1127 networkRequest.setAuthCfg( mAuthCfg );
1128
1129 switch ( networkRequest.get( request ) )
1130 {
1132 break;
1133
1137 // TODO -- error reporting
1138 return false;
1139 }
1140
1141 const QgsNetworkReplyContent content = networkRequest.reply();
1142
1143 mShared->initialize( content.content(), tileSetUri, transformContext(), mAuthCfg, mHeaders );
1144
1145 mShared->mLayerMetadata.addLink( QgsAbstractMetadataBase::Link( tr( "Source" ), u"WWW:LINK"_s, tileSetUri ) );
1146 }
1147 else
1148 {
1149 // try uri as a local file
1150 const QFileInfo fi( dataSourceUri() );
1151 if ( fi.exists() )
1152 {
1153 QFile file( dataSourceUri() );
1154 if ( file.open( QIODevice::ReadOnly | QIODevice::Text ) )
1155 {
1156 const QByteArray raw = file.readAll();
1157 mShared->initialize( raw, QUrl::fromLocalFile( dataSourceUri() ), transformContext(), mAuthCfg, mHeaders );
1158 }
1159 else
1160 {
1161 return false;
1162 }
1163 }
1164 else
1165 {
1166 return false;
1167 }
1168 }
1169
1170 if ( !mShared->mIndex.isValid() )
1171 {
1172 appendError( mShared->mError );
1173 return false;
1174 }
1175 return true;
1176}
1177
1178QgsCoordinateReferenceSystem QgsCesiumTilesDataProvider::crs() const
1179{
1181
1182 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1183 return mShared->mLayerCrs;
1184}
1185
1186QgsRectangle QgsCesiumTilesDataProvider::extent() const
1187{
1189
1190 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1191 return mShared->mExtent;
1192}
1193
1194bool QgsCesiumTilesDataProvider::isValid() const
1195{
1197
1198 return mIsValid;
1199}
1200
1201QString QgsCesiumTilesDataProvider::name() const
1202{
1204
1205 return PROVIDER_KEY;
1206}
1207
1208QString QgsCesiumTilesDataProvider::description() const
1209{
1211
1212 return QObject::tr( "Cesium 3D Tiles" );
1213}
1214
1215QString QgsCesiumTilesDataProvider::htmlMetadata() const
1216{
1218
1219 QString metadata;
1220
1221 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1222 if ( mShared->mTileset.contains( "asset" ) )
1223 {
1224 const auto &asset = mShared->mTileset["asset"];
1225 if ( asset.contains( "version" ) )
1226 {
1227 const QString version = QString::fromStdString( asset["version"].get<std::string>() );
1228 metadata += u"<tr><td class=\"highlight\">"_s % tr( "3D Tiles Version" ) % u"</td><td>%1</a>"_s.arg( version ) % u"</td></tr>\n"_s;
1229 }
1230
1231 if ( asset.contains( "tilesetVersion" ) )
1232 {
1233 try
1234 {
1235 const QString tilesetVersion = QString::fromStdString( asset["tilesetVersion"].get<std::string>() );
1236 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Tileset Version" ) % u"</td><td>%1</a>"_s.arg( tilesetVersion ) % u"</td></tr>\n"_s;
1237 }
1238 catch ( json::type_error & )
1239 {
1240 QgsDebugError( u"Error when parsing tilesetVersion value"_s );
1241 }
1242 }
1243
1244 if ( asset.contains( "generator" ) )
1245 {
1246 const QString generator = QString::fromStdString( asset["generator"].get<std::string>() );
1247 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Tileset Generator" ) % u"</td><td>%1</a>"_s.arg( generator ) % u"</td></tr>\n"_s;
1248 }
1249 }
1250 if ( mShared->mTileset.contains( "extensionsRequired" ) )
1251 {
1252 QStringList extensions;
1253 for ( const auto &item : mShared->mTileset["extensionsRequired"] )
1254 {
1255 extensions << QString::fromStdString( item.get<std::string>() );
1256 }
1257 if ( !extensions.isEmpty() )
1258 {
1259 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Extensions Required" ) % u"</td><td><ul><li>%1</li></ul></a>"_s.arg( extensions.join( "</li><li>"_L1 ) ) % u"</td></tr>\n"_s;
1260 }
1261 }
1262 if ( mShared->mTileset.contains( "extensionsUsed" ) )
1263 {
1264 QStringList extensions;
1265 for ( const auto &item : mShared->mTileset["extensionsUsed"] )
1266 {
1267 extensions << QString::fromStdString( item.get<std::string>() );
1268 }
1269 if ( !extensions.isEmpty() )
1270 {
1271 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Extensions Used" ) % u"</td><td><ul><li>%1</li></ul></a>"_s.arg( extensions.join( "</li><li>"_L1 ) ) % u"</td></tr>\n"_s;
1272 }
1273 }
1274
1275 if ( !mShared->mZRange.isInfinite() )
1276 {
1277 metadata += u"<tr><td class=\"highlight\">"_s
1278 % tr( "Z Range" )
1279 % u"</td><td>%1 - %2</a>"_s.arg( QLocale().toString( mShared->mZRange.lower() ), QLocale().toString( mShared->mZRange.upper() ) )
1280 % u"</td></tr>\n"_s;
1281 }
1282
1283 return metadata;
1284}
1285
1286QgsLayerMetadata QgsCesiumTilesDataProvider::layerMetadata() const
1287{
1289 if ( !mShared )
1290 return QgsLayerMetadata();
1291
1292 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1293 return mShared->mLayerMetadata;
1294}
1295
1296const QgsCoordinateReferenceSystem QgsCesiumTilesDataProvider::sceneCrs() const
1297{
1299 if ( !mShared )
1301
1302 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1303 return mShared->mSceneCrs;
1304}
1305
1306const QgsTiledSceneBoundingVolume &QgsCesiumTilesDataProvider::boundingVolume() const
1307{
1309 static QgsTiledSceneBoundingVolume nullVolume;
1310 if ( !mShared )
1311 return nullVolume;
1312
1313 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1314 return mShared ? mShared->mBoundingVolume : nullVolume;
1315}
1316
1317QgsTiledSceneIndex QgsCesiumTilesDataProvider::index() const
1318{
1320 if ( !mShared )
1321 return QgsTiledSceneIndex( nullptr );
1322
1323 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1324 return mShared->mIndex;
1325}
1326
1327QgsDoubleRange QgsCesiumTilesDataProvider::zRange() const
1328{
1330 if ( !mShared )
1331 return QgsDoubleRange();
1332
1333 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1334 return mShared->mZRange;
1335}
1336
1337
1338//
1339// QgsCesiumTilesProviderMetadata
1340//
1341
1342QgsCesiumTilesProviderMetadata::QgsCesiumTilesProviderMetadata()
1343 : QgsProviderMetadata( PROVIDER_KEY, PROVIDER_DESCRIPTION )
1344{}
1345
1346QIcon QgsCesiumTilesProviderMetadata::icon() const
1347{
1348 return QgsApplication::getThemeIcon( u"mIconCesium3dTiles.svg"_s );
1349}
1350
1351QgsCesiumTilesDataProvider *QgsCesiumTilesProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, Qgis::DataProviderReadFlags flags )
1352{
1353 return new QgsCesiumTilesDataProvider( uri, options, flags );
1354}
1355
1356QList<QgsProviderSublayerDetails> QgsCesiumTilesProviderMetadata::querySublayers( const QString &uri, Qgis::SublayerQueryFlags, QgsFeedback * ) const
1357{
1358 const QVariantMap parts = decodeUri( uri );
1359 if ( parts.value( u"file-name"_s ).toString().compare( "tileset.json"_L1, Qt::CaseInsensitive ) == 0 )
1360 {
1362 details.setUri( uri );
1363 details.setProviderKey( PROVIDER_KEY );
1366 return { details };
1367 }
1368 else
1369 {
1370 return {};
1371 }
1372}
1373
1374int QgsCesiumTilesProviderMetadata::priorityForUri( const QString &uri ) const
1375{
1376 const QVariantMap parts = decodeUri( uri );
1377 if ( parts.value( u"file-name"_s ).toString().compare( "tileset.json"_L1, Qt::CaseInsensitive ) == 0 )
1378 return 100;
1379
1380 return 0;
1381}
1382
1383QList<Qgis::LayerType> QgsCesiumTilesProviderMetadata::validLayerTypesForUri( const QString &uri ) const
1384{
1385 const QVariantMap parts = decodeUri( uri );
1386 if ( parts.value( u"file-name"_s ).toString().compare( "tileset.json"_L1, Qt::CaseInsensitive ) == 0 )
1387 return QList< Qgis::LayerType>() << Qgis::LayerType::TiledScene;
1388
1389 return QList< Qgis::LayerType>();
1390}
1391
1392QVariantMap QgsCesiumTilesProviderMetadata::decodeUri( const QString &uri ) const
1393{
1394 QVariantMap uriComponents;
1395 QUrl url = QUrl::fromUserInput( uri );
1396 uriComponents.insert( u"file-name"_s, url.fileName() );
1397 uriComponents.insert( u"path"_s, uri );
1398 return uriComponents;
1399}
1400
1401QString QgsCesiumTilesProviderMetadata::filters( Qgis::FileFilterType type )
1402{
1403 switch ( type )
1404 {
1411 return QString();
1412
1414 return QObject::tr( "Cesium 3D Tiles" ) + u" (tileset.json TILESET.JSON)"_s;
1415 }
1416 return QString();
1417}
1418
1419QgsProviderMetadata::ProviderCapabilities QgsCesiumTilesProviderMetadata::providerCapabilities() const
1420{
1421 return FileBasedUris;
1422}
1423
1424QList<Qgis::LayerType> QgsCesiumTilesProviderMetadata::supportedLayerTypes() const
1425{
1426 return { Qgis::LayerType::TiledScene };
1427}
1428
1429QString QgsCesiumTilesProviderMetadata::encodeUri( const QVariantMap &parts ) const
1430{
1431 const QString path = parts.value( u"path"_s ).toString();
1432 return path;
1433}
1434
1435QgsProviderMetadata::ProviderMetadataCapabilities QgsCesiumTilesProviderMetadata::capabilities() const
1436{
1437 return ProviderMetadataCapability::LayerTypesForUri | ProviderMetadataCapability::PriorityForUri | ProviderMetadataCapability::QuerySublayers;
1438}
1439
QFlags< TiledSceneProviderCapability > TiledSceneProviderCapabilities
Tiled scene data provider capabilities.
Definition qgis.h:6199
QFlags< DataProviderFlag > DataProviderFlags
Data provider flags.
Definition qgis.h:2450
FileFilterType
Type of file filters.
Definition qgis.h:1460
@ TiledScene
Tiled scene layers.
Definition qgis.h:1467
@ Vector
Vector layers.
Definition qgis.h:1461
@ VectorTile
Vector tile layers.
Definition qgis.h:1466
@ Mesh
Mesh layers.
Definition qgis.h:1463
@ Raster
Raster layers.
Definition qgis.h:1462
@ MeshDataset
Mesh datasets.
Definition qgis.h:1464
@ PointCloud
Point clouds.
Definition qgis.h:1465
@ Is3DBasemapSource
Associated source should be considered a '3D basemap' layer. See Qgis::MapLayerProperty::Is3DBasemapL...
Definition qgis.h:2447
@ IsBasemapSource
Associated source should be considered a 'basemap' layer. See Qgis::MapLayerProperty::IsBasemapLayer.
Definition qgis.h:2444
QFlags< DataProviderReadFlag > DataProviderReadFlags
Flags which control data provider construction.
Definition qgis.h:512
@ ReadLayerMetadata
Provider can read layer metadata from data store. See QgsDataProvider::layerMetadata().
Definition qgis.h:6190
QFlags< SublayerQueryFlag > SublayerQueryFlags
Sublayer query flags.
Definition qgis.h:1518
TileChildrenAvailability
Possible availability states for a tile's children.
Definition qgis.h:6236
@ Available
Tile children are already available.
Definition qgis.h:6238
@ NeedFetching
Tile has children, but they are not yet available and must be fetched.
Definition qgis.h:6239
@ NoChildren
Tile is known to have no children.
Definition qgis.h:6237
@ TiledScene
Tiled scene layer. Added in QGIS 3.34.
Definition qgis.h:215
@ NoHierarchyFetch
Do not allow hierarchy fetching when hierarchy is not currently available. Avoids network requests,...
Definition qgis.h:6250
Axis
Cartesian axes.
Definition qgis.h:2607
@ X
X-axis.
Definition qgis.h:2608
@ Z
Z-axis.
Definition qgis.h:2610
@ Y
Y-axis.
Definition qgis.h:2609
@ Created
Date created.
Definition qgis.h:5063
@ Additive
When tile is refined its content should be used alongside its children simultaneously.
Definition qgis.h:6226
@ Replacement
When tile is refined then its children should be used in place of itself.
Definition qgis.h:6225
An abstract base class for tiled scene data provider indices.
virtual QgsTiledSceneTile rootTile() const =0
Returns the root tile for the index.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
static QgsTileDownloadManager * tileDownloadManager()
Returns the application's tile download manager, used for download of map tiles when rendering.
static QgsAuthManager * authManager()
Returns the application's authentication manager instance.
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
void setAuthCfg(const QString &authCfg)
Sets the authentication config id which should be used during the request.
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr, RequestFlags requestFlags=QgsBlockingNetworkRequest::RequestFlags())
Performs a "get" operation on the specified request.
@ NetworkError
A network error occurred.
@ ServerExceptionError
An exception was raised by the server.
@ NoError
No error was encountered.
@ TimeoutError
Timeout was reached before a reply was received.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
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:268
QgsRectangle toRectangle() const
Converts the box to a 2D rectangle.
Definition qgsbox3d.h:388
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:261
bool isNull() const
Test if the box is null (holding no spatial information).
Definition qgsbox3d.cpp:311
static QMap< TileCoordinate, QgsTiledSceneNode * > createImplicitTilingChildren(QgsTiledSceneNode *node, const TileCoordinate &coord, Root &tilingData, const TileCoordinate &subtreeCoord, QgsCoordinateTransformContext &transformContext, long long &nextTileId)
Creates immediate children of a node according to implicit tiling.
static TileCoordinate tileCoordinateToParentSubtree(TileCoordinate coord, int subtreeLevels)
Returns parent subtree's tile coordinates of the given tile.
static QString expandTemplateUri(const QString &templateUri, const QUrl &baseUrl, const TileCoordinate &coord)
Expands template URI (using {level}, {x}, {y} markers) to the final URI.
static Qgis::TileChildrenAvailability childAvailability(const Root &tilingData, const TileCoordinate &coord)
Returns tile availability of a child based on cached subtree data.
static bool parseImplicitTiling(const json &tileJson, QgsTiledSceneNode *newNode, const QUrl &baseUrl, Qgis::Axis gltfUpAxis, Root &tilingData)
Parses JSON definition of implicit tiling into tilingData argument and returns true on success.
static Subtree parseSubtree(const Root &tilingData, const QByteArray &data)
Parses subtree definition and returns it.
static int subtreeBitIndex(int localLevel, int localX, int localY)
Returns the bit index within a subtree's availability bitstream for a given local tile position.
static QgsSphere parseSphere(const json &sphere)
Parses a sphere object from a Cesium JSON document.
static QString appendQueryFromBaseUrl(const QString &contentUri, const QUrl &baseUrl)
Copies any query items from the base URL to the content URI - to replicate undocumented Cesium JS beh...
static QgsOrientedBox3D parseBox(const json &box)
Parses a box object from a Cesium JSON document to an oriented bounding box.
static QgsTiledSceneBoundingVolume boundingVolumeFromRegion(const QgsBox3D &region, const QgsCoordinateTransformContext &transformContext)
Calculates oriented bounding box in EPSG:4978 from "region" defined with min/max lat/lon coordinates ...
static QgsBox3D parseRegion(const json &region)
Parses a region object from a Cesium JSON object to a 3D box.
static QgsSphere transformSphere(const QgsSphere &sphere, const QgsMatrix4x4 &transform)
Applies a transform to a sphere.
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.
Custom exception class for Coordinate Reference System related exceptions.
Stores the component parts of a data source URI (e.g.
void setEncodedUri(const QByteArray &uri)
Sets the complete encoded uri.
QgsHttpHeaders httpHeaders() const
Returns http headers.
QString param(const QString &key) const
Returns a generic parameter value corresponding to the specified key.
QString authConfigId() const
Returns any associated authentication configuration ID stored in the URI.
QgsRange which stores a range of double values.
Definition qgsrange.h:217
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
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:56
void canceled()
Internal routines can connect to this signal if they use event loop.
Implements simple HTTP header management.
A structured metadata store for a map layer.
A simple 4x4 matrix implementation useful for transformation in 3D space.
bool isIdentity() const
Returns whether this matrix is an identity matrix.
double * data()
Returns pointer to the matrix data (stored in column-major order).
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.
Represents a oriented (rotated) box in 3 dimensions.
bool isNull() const
Returns true if the box is a null box.
static QgsOrientedBox3D fromBox3D(const QgsBox3D &box)
Constructs an oriented box from an axis-aligned bounding 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.
A spherical geometry object.
Definition qgssphere.h:46
bool isNull() const
Returns true if the sphere is a null (default constructed) sphere.
Definition qgssphere.cpp:32
QgsBox3D boundingBox() const
Returns the 3-dimensional bounding box containing the sphere.
Definition qgssphere.cpp:77
void finished()
Emitted when the reply has finished (either with a success or with a failure).
Represents a bounding volume for a tiled scene.
QgsOrientedBox3D box() const
Returns the volume's oriented box.
bool intersects(const QgsOrientedBox3D &box) const
Returns true if this bounds intersects the specified box.
void transform(const QgsMatrix4x4 &transform)
Applies a transform to the bounding volume.
Base class for data providers for QgsTiledSceneLayer.
An index for tiled scene data providers.
Allows representing QgsTiledSceneTiles in a hierarchical tree.
void addChild(QgsTiledSceneNode *child)
Adds a child to this node.
QgsTiledSceneNode * parentNode() const
Returns the parent of this node.
QList< QgsTiledSceneNode * > children() const
Returns this node's children.
QgsTiledSceneTile * tile()
Returns the tile associated with the node.
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.
QgsFeedback * feedback() const
Returns the feedback object that can be queried regularly by the request to check if it should be can...
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.
Qgis::TileRefinementProcess refinementProcess() const
Returns the tile's refinement process.
const QgsTiledSceneBoundingVolume & boundingVolume() const
Returns the bounding volume for the tile.
long long id() const
Returns the tile's unique ID.
const QgsMatrix4x4 * transform() const
Returns the tile's transform.
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...
#define QgsDebugError(str)
Definition qgslogger.h:59
#define QgsSetRequestInitiatorClass(request, _class)
#define QGIS_PROTECT_QOBJECT_THREAD_ACCESS
Definition of root implicit tiling node (typically root node of the whole tileset,...
QMap< TileCoordinate, Subtree > subtreeCache
int subtreeLevels
how many levels are stored in a single subtree
Data about subtree of a node - there should be subtree at least for root implicit tiling node,...
QBitArray contentAvailability
Bit array whether a tile has some content.
Contains ZXY coordinates of a node within implicit tiling.
Setting options for creating vector data providers.
Metadata extent structure.
void setSpatialExtents(const QList< QgsLayerMetadata::SpatialExtent > &extents)
Sets the spatial extents of the resource.
Metadata spatial extent structure.
QgsCoordinateReferenceSystem extentCrs
Coordinate reference system for spatial extent.
QgsBox3D bounds
Geospatial extent of the resource.