QGIS API Documentation 3.99.0-Master (09f76ad7019)
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"
25#include "qgscesiumutils.h"
27#include "qgsellipsoidutils.h"
28#include "qgslogger.h"
30#include "qgsorientedbox3d.h"
32#include "qgsproviderutils.h"
33#include "qgsreadwritelocker.h"
35#include "qgssphere.h"
36#include "qgsthreadingutils.h"
39#include "qgstiledsceneindex.h"
40#include "qgstiledscenenode.h"
42#include "qgstiledscenetile.h"
43
44#include <QApplication>
45#include <QFileInfo>
46#include <QIcon>
47#include <QJsonDocument>
48#include <QJsonObject>
49#include <QNetworkRequest>
50#include <QRecursiveMutex>
51#include <QRegularExpression>
52#include <QString>
53#include <QUrl>
54#include <QUrlQuery>
55#include <qstringliteral.h>
56
57#include "moc_qgscesiumtilesdataprovider.cpp"
58
59using namespace Qt::StringLiterals;
60
62
63#define PROVIDER_KEY u"cesiumtiles"_s
64#define PROVIDER_DESCRIPTION u"Cesium 3D Tiles data provider"_s
65
66
67// This is to support a case seen with Google's tiles. Root URL is something like this:
68// https://tile.googleapis.com/.../root.json?key=123
69// The returned JSON contains relative links with "session" (e.g. "/.../abc.json?session=456")
70// When fetching such abc.json, we have to include also "key" from the original URL!
71// Then the content of abc.json contains relative links (e.g. "/.../xyz.glb") and we
72// need to add both "key" and "session" (otherwise requests fail).
73//
74// This function simply copies any query items from the base URL to the content URI.
75static QString appendQueryFromBaseUrl( const QString &contentUri, const QUrl &baseUrl )
76{
77 QUrlQuery contentQuery( QUrl( contentUri ).query() );
78 const QList<QPair<QString, QString>> baseUrlQueryItems = QUrlQuery( baseUrl.query() ).queryItems();
79 for ( const QPair<QString, QString> &kv : baseUrlQueryItems )
80 {
81 contentQuery.addQueryItem( kv.first, kv.second );
82 }
83 QUrl newContentUrl( contentUri );
84 newContentUrl.setQuery( contentQuery );
85 return newContentUrl.toString();
86}
87
88
89class QgsCesiumTiledSceneIndex final : public QgsAbstractTiledSceneIndex
90{
91 public:
92
93 QgsCesiumTiledSceneIndex(
94 const json &tileset,
95 const QUrl &rootUrl,
96 const QString &authCfg,
97 const QgsHttpHeaders &headers,
98 const QgsCoordinateTransformContext &transformContext );
99
100 std::unique_ptr< QgsTiledSceneTile > tileFromJson( const json &node, const QUrl &baseUrl, const QgsTiledSceneTile *parent, Qgis::Axis gltfUpAxis );
101
106 std::unique_ptr< QgsTiledSceneNode > nodeFromJson( const json &node, const QUrl &baseUrl, QgsTiledSceneNode *parent, Qgis::Axis gltfUpAxis );
107 void refineNodeFromJson( QgsTiledSceneNode *node, const QUrl &baseUrl, const json &json );
108
109 QgsTiledSceneTile rootTile() const final;
110 QgsTiledSceneTile getTile( long long id ) final;
111 long long parentTileId( long long id ) const final;
112 QVector< long long > childTileIds( long long id ) const final;
113 QVector< long long > getTiles( const QgsTiledSceneRequest &request ) final;
114 Qgis::TileChildrenAvailability childAvailability( long long id ) const final;
115 bool fetchHierarchy( long long id, QgsFeedback *feedback = nullptr ) final;
116
117 protected:
118
119 QByteArray fetchContent( const QString &uri, QgsFeedback *feedback = nullptr ) final;
120
121 private:
122
123 enum class TileContentFormat
124 {
125 Json,
126 NotJson, // TODO: refine this to actual content types when/if needed!
127 };
128
129 mutable QRecursiveMutex mLock;
130 QgsCoordinateTransformContext mTransformContext;
131 std::unique_ptr< QgsTiledSceneNode > mRootNode;
132 QMap< long long, QgsTiledSceneNode * > mNodeMap;
133 QMap< long long, TileContentFormat > mTileContentFormats;
134 QString mAuthCfg;
135 QgsHttpHeaders mHeaders;
136 long long mNextTileId = 0;
137
138};
139
140class QgsCesiumTilesDataProviderSharedData
141{
142 public:
143 QgsCesiumTilesDataProviderSharedData();
144 void initialize( const QString &tileset,
145 const QUrl &rootUrl,
146 const QgsCoordinateTransformContext &transformContext,
147 const QString &authCfg,
148 const QgsHttpHeaders &headers );
149
150 QgsCoordinateReferenceSystem mLayerCrs;
151 QgsCoordinateReferenceSystem mSceneCrs;
152 QgsTiledSceneBoundingVolume mBoundingVolume;
153
154 QgsRectangle mExtent;
155 nlohmann::json mTileset;
156 QgsDoubleRange mZRange;
157
158 QgsTiledSceneIndex mIndex;
159
160 QgsLayerMetadata mLayerMetadata;
161 QString mError;
162 QReadWriteLock mReadWriteLock;
163
164};
165
166
167//
168// QgsCesiumTiledSceneIndex
169//
170
171Qgis::Axis axisFromJson( const json &json )
172{
173 const std::string gltfUpAxisString = json.get<std::string>();
174 if ( gltfUpAxisString == "z" || gltfUpAxisString == "Z" )
175 {
176 return Qgis::Axis::Z;
177 }
178 else if ( gltfUpAxisString == "y" || gltfUpAxisString == "Y" )
179 {
180 return Qgis::Axis::Y;
181 }
182 else if ( gltfUpAxisString == "x" || gltfUpAxisString == "X" )
183 {
184 return Qgis::Axis::X;
185 }
186 QgsDebugError( u"Unsupported gltfUpAxis value: %1"_s.arg( QString::fromStdString( gltfUpAxisString ) ) );
187 return Qgis::Axis::Y;
188}
189
190QgsCesiumTiledSceneIndex::QgsCesiumTiledSceneIndex( const json &tileset, const QUrl &rootUrl, const QString &authCfg, const QgsHttpHeaders &headers, const QgsCoordinateTransformContext &transformContext )
191 : mTransformContext( transformContext )
192 , mAuthCfg( authCfg )
193 , mHeaders( headers )
194{
195 Qgis::Axis gltfUpAxis = Qgis::Axis::Y;
196 if ( tileset.contains( "asset" ) )
197 {
198 const auto &assetJson = tileset["asset"];
199 if ( assetJson.contains( "gltfUpAxis" ) )
200 {
201 gltfUpAxis = axisFromJson( assetJson["gltfUpAxis"] );
202 }
203 }
204
205 mRootNode = nodeFromJson( tileset[ "root" ], rootUrl, nullptr, gltfUpAxis );
206}
207
208std::unique_ptr< QgsTiledSceneTile > QgsCesiumTiledSceneIndex::tileFromJson( const json &json, const QUrl &baseUrl, const QgsTiledSceneTile *parent, Qgis::Axis gltfUpAxis )
209{
210 auto tile = std::make_unique< QgsTiledSceneTile >( mNextTileId++ );
211
212 tile->setBaseUrl( baseUrl );
213 tile->setMetadata(
214 {
215 { u"gltfUpAxis"_s, static_cast< int >( gltfUpAxis ) },
216 { u"contentFormat"_s, u"cesiumtiles"_s },
217 } );
218
219 QgsMatrix4x4 transform;
220 if ( json.contains( "transform" ) && !json["transform"].is_null() )
221 {
222 const auto &transformJson = json["transform"];
223 double *ptr = transform.data();
224 for ( int i = 0; i < 16; ++i )
225 ptr[i] = transformJson[i].get<double>();
226
227 if ( parent && parent->transform() )
228 {
229 transform = *parent->transform() * transform;
230 }
231 }
232 else if ( parent && parent->transform() )
233 {
234 transform = *parent->transform();
235 }
236 if ( !transform.isIdentity() )
237 tile->setTransform( transform );
238
239 const auto &boundingVolume = json[ "boundingVolume" ];
241 if ( boundingVolume.contains( "region" ) )
242 {
243 QgsBox3D rootRegion = QgsCesiumUtils::parseRegion( boundingVolume[ "region" ] );
244 if ( !rootRegion.isNull() )
245 {
246 if ( rootRegion.width() > 20 || rootRegion.height() > 20 )
247 {
248 // treat very large regions as global -- these will not transform to EPSG:4978
249 }
250 else
251 {
252 // we need to transform regions from EPSG:4979 to EPSG:4978
253 QVector< QgsVector3D > corners = rootRegion.corners();
254
255 QVector< double > x;
256 x.reserve( 8 );
257 QVector< double > y;
258 y.reserve( 8 );
259 QVector< double > z;
260 z.reserve( 8 );
261 for ( int i = 0; i < 8; ++i )
262 {
263 const QgsVector3D &corner = corners[i];
264 x.append( corner.x() );
265 y.append( corner.y() );
266 z.append( corner.z() );
267 }
268 QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( u"EPSG:4979"_s ), QgsCoordinateReferenceSystem( u"EPSG:4978"_s ), mTransformContext );
269 ct.setBallparkTransformsAreAppropriate( true );
270 try
271 {
272 ct.transformInPlace( x, y, z );
273 }
274 catch ( QgsCsException & )
275 {
276 QgsDebugError( u"Cannot transform region bounding volume"_s );
277 }
278
279 const auto minMaxX = std::minmax_element( x.constBegin(), x.constEnd() );
280 const auto minMaxY = std::minmax_element( y.constBegin(), y.constEnd() );
281 const auto minMaxZ = std::minmax_element( z.constBegin(), z.constEnd() );
282 volume = QgsTiledSceneBoundingVolume( QgsOrientedBox3D::fromBox3D( QgsBox3D( *minMaxX.first, *minMaxY.first, *minMaxZ.first, *minMaxX.second, *minMaxY.second, *minMaxZ.second ) ) );
283
284 // note that matrix transforms are NOT applied to region bounding volumes!
285 }
286 }
287 }
288 else if ( boundingVolume.contains( "box" ) )
289 {
290 const QgsOrientedBox3D bbox = QgsCesiumUtils::parseBox( boundingVolume["box"] );
291 if ( !bbox.isNull() )
292 {
293 volume = QgsTiledSceneBoundingVolume( bbox );
294 if ( !transform.isIdentity() )
295 volume.transform( transform );
296 }
297 }
298 else if ( boundingVolume.contains( "sphere" ) )
299 {
300 QgsSphere sphere = QgsCesiumUtils::parseSphere( boundingVolume["sphere"] );
301 if ( !sphere.isNull() )
302 {
303 sphere = QgsCesiumUtils::transformSphere( sphere, transform );
305 }
306 }
307 else
308 {
309 QgsDebugError( u"unsupported boundingVolume format"_s );
310 }
311
312 tile->setBoundingVolume( volume );
313
314 if ( json.contains( "geometricError" ) )
315 tile->setGeometricError( json["geometricError"].get< double >() );
316 if ( json.contains( "refine" ) )
317 {
318 if ( json["refine"] == "ADD" )
319 tile->setRefinementProcess( Qgis::TileRefinementProcess::Additive );
320 else if ( json["refine"] == "REPLACE" )
321 tile->setRefinementProcess( Qgis::TileRefinementProcess::Replacement );
322 }
323 else if ( parent )
324 {
325 // children inherit the parent refinement if not explicitly set -- see https://github.com/CesiumGS/cesium-native/blob/172ac5ddcce602c8b268ad342639554dea2f6004/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp#L440C5-L440C40
326 tile->setRefinementProcess( parent->refinementProcess() );
327 }
328
329 if ( json.contains( "content" ) && !json["content"].is_null() )
330 {
331 const auto &contentJson = json["content"];
332
333 // sometimes URI, sometimes URL...
334 QString contentUri;
335 if ( contentJson.contains( "uri" ) && !contentJson["uri"].is_null() )
336 {
337 QString relativeUri = QString::fromStdString( contentJson["uri"].get<std::string>() );
338 contentUri = baseUrl.resolved( QUrl( relativeUri ) ).toString();
339
340 if ( baseUrl.hasQuery() && QUrl( relativeUri ).isRelative() )
341 contentUri = appendQueryFromBaseUrl( contentUri, baseUrl );
342 }
343 else if ( contentJson.contains( "url" ) && !contentJson["url"].is_null() )
344 {
345 QString relativeUri = QString::fromStdString( contentJson["url"].get<std::string>() );
346 contentUri = baseUrl.resolved( QUrl( relativeUri ) ).toString();
347
348 if ( baseUrl.hasQuery() && QUrl( relativeUri ).isRelative() )
349 contentUri = appendQueryFromBaseUrl( contentUri, baseUrl );
350 }
351 if ( !contentUri.isEmpty() )
352 {
353 tile->setResources( {{ u"content"_s, contentUri } } );
354 }
355 }
356
357 return tile;
358}
359
360std::unique_ptr<QgsTiledSceneNode> QgsCesiumTiledSceneIndex::nodeFromJson( const json &json, const QUrl &baseUrl, QgsTiledSceneNode *parent, Qgis::Axis gltfUpAxis )
361{
362 std::unique_ptr< QgsTiledSceneTile > tile = tileFromJson( json, baseUrl, parent ? parent->tile() : nullptr, gltfUpAxis );
363 auto newNode = std::make_unique< QgsTiledSceneNode >( tile.release() );
364 mNodeMap.insert( newNode->tile()->id(), newNode.get() );
365
366 if ( json.contains( "children" ) )
367 {
368 for ( const auto &childJson : json["children"] )
369 {
370 nodeFromJson( childJson, baseUrl, newNode.get(), gltfUpAxis );
371 }
372 }
373
374 if ( parent )
375 {
376 parent->addChild( newNode.release() );
377 return nullptr;
378 }
379 else
380 {
381 return newNode;
382 }
383}
384
385void QgsCesiumTiledSceneIndex::refineNodeFromJson( QgsTiledSceneNode *node, const QUrl &baseUrl, const json &json )
386{
387 const auto &rootTileJson = json["root"];
388
389 Qgis::Axis gltfUpAxis = Qgis::Axis::Y;
390 if ( json.contains( "asset" ) )
391 {
392 const auto &assetJson = json["asset"];
393 if ( assetJson.contains( "gltfUpAxis" ) )
394 {
395 gltfUpAxis = axisFromJson( assetJson["gltfUpAxis"] );
396 }
397 }
398
399 std::unique_ptr< QgsTiledSceneTile > newTile = tileFromJson( rootTileJson, baseUrl, node->tile(), gltfUpAxis );
400 // copy just the resources from the retrieved tileset to the refined node. We assume all the rest of the tile content
401 // should be the same between the node being refined and the root node of the fetched sub dataset!
402 // (Ie the bounding volume, geometric error, etc).
403 node->tile()->setResources( newTile->resources() );
404
405
406 // root tile of the sub dataset may have transform as well, we need to bring it back
407 // (actually even the referencing tile may have transform - if that's the case,
408 // that transform got combined with the root tile's transform in tileFromJson)
409 if ( newTile->transform() )
410 node->tile()->setTransform( *newTile->transform() );
411
412 if ( rootTileJson.contains( "children" ) )
413 {
414 for ( const auto &childJson : rootTileJson["children"] )
415 {
416 nodeFromJson( childJson, baseUrl, node, gltfUpAxis );
417 }
418 }
419}
420
421QgsTiledSceneTile QgsCesiumTiledSceneIndex::rootTile() const
422{
423 QMutexLocker locker( &mLock );
424 return mRootNode ? *mRootNode->tile() : QgsTiledSceneTile();
425}
426
427QgsTiledSceneTile QgsCesiumTiledSceneIndex::getTile( long long id )
428{
429 QMutexLocker locker( &mLock );
430 auto it = mNodeMap.constFind( id );
431 if ( it != mNodeMap.constEnd() )
432 {
433 return *( it.value()->tile() );
434 }
435
436 return QgsTiledSceneTile();
437}
438
439long long QgsCesiumTiledSceneIndex::parentTileId( long long id ) const
440{
441 QMutexLocker locker( &mLock );
442 auto it = mNodeMap.constFind( id );
443 if ( it != mNodeMap.constEnd() )
444 {
445 if ( QgsTiledSceneNode *parent = it.value()->parentNode() )
446 {
447 return parent->tile()->id();
448 }
449 }
450
451 return -1;
452}
453
454QVector< long long > QgsCesiumTiledSceneIndex::childTileIds( long long id ) const
455{
456 QMutexLocker locker( &mLock );
457 auto it = mNodeMap.constFind( id );
458 if ( it != mNodeMap.constEnd() )
459 {
460 QVector< long long > childIds;
461 const QList< QgsTiledSceneNode * > children = it.value()->children();
462 childIds.reserve( children.size() );
463 for ( QgsTiledSceneNode *child : children )
464 {
465 childIds << child->tile()->id();
466 }
467 return childIds;
468 }
469
470 return {};
471}
472
473QVector< long long > QgsCesiumTiledSceneIndex::getTiles( const QgsTiledSceneRequest &request )
474{
475 QVector< long long > results;
476
477 std::function< void( QgsTiledSceneNode * )> traverseNode;
478 traverseNode = [&request, &traverseNode, &results, this]( QgsTiledSceneNode * node )
479 {
480 QgsTiledSceneTile *tile = node->tile();
481
482 // check filter box first -- if the node doesn't intersect, then don't include the node and don't traverse
483 // to its children
484 if ( !request.filterBox().isNull() && !tile->boundingVolume().box().isNull() && !tile->boundingVolume().intersects( request.filterBox() ) )
485 return;
486
487 // TODO -- option to filter out nodes without content
488
489 if ( request.requiredGeometricError() <= 0 || tile->geometricError() <= 0 || tile->geometricError() > request.requiredGeometricError() )
490 {
491 // haven't traversed deep enough down this node, we need to explore children
492
493 // are children available?
494 QList< QgsTiledSceneNode * > children = node->children();
495 if ( children.empty() )
496 {
497 switch ( childAvailability( tile->id() ) )
498 {
501 break;
503 {
505 {
506 // do a blocking fetch of children
507 if ( fetchHierarchy( tile->id() ), request.feedback() )
508 {
509 children = node->children();
510 }
511 }
512 break;
513 }
514 }
515 }
516
517 for ( QgsTiledSceneNode *child : std::as_const( children ) )
518 {
519 if ( request.feedback() && request.feedback()->isCanceled() )
520 break;
521
522 traverseNode( child );
523 }
524
525 switch ( tile->refinementProcess() )
526 {
528 // child add to parent content, so we must also include the parent
529 results << tile->id();
530 break;
531
533 // children replace the parent, so we skip the parent if we found children
534 if ( children.empty() )
535 results << tile->id();
536 break;
537 }
538 }
539 else
540 {
541 results << tile->id();
542 }
543
544 };
545
546 QMutexLocker locker( &mLock );
547 if ( request.parentTileId() < 0 )
548 {
549 if ( mRootNode )
550 traverseNode( mRootNode.get() );
551 }
552 else
553 {
554 auto it = mNodeMap.constFind( request.parentTileId() );
555 if ( it != mNodeMap.constEnd() )
556 {
557 traverseNode( it.value() );
558 }
559 }
560
561 return results;
562}
563
564Qgis::TileChildrenAvailability QgsCesiumTiledSceneIndex::childAvailability( long long id ) const
565{
566 QString contentUri;
567 QMutexLocker locker( &mLock );
568 {
569 auto it = mNodeMap.constFind( id );
570 if ( it == mNodeMap.constEnd() )
572
573 if ( !it.value()->children().isEmpty() )
575
576 contentUri = it.value()->tile()->resources().value( u"content"_s ).toString();
577 }
578 {
579 // maybe we already retrieved content for this node and know the answer:
580 auto it = mTileContentFormats.constFind( id );
581 if ( it != mTileContentFormats.constEnd() )
582 {
583 switch ( it.value() )
584 {
585 case TileContentFormat::NotJson:
587 case TileContentFormat::Json:
589 }
590 }
591 }
592 locker.unlock();
593
594 if ( contentUri.isEmpty() )
596
597 // https://github.com/CesiumGS/3d-tiles/tree/main/specification#tile-json says:
598 // "A file extension is not required for content.uri. A content’s tile format can
599 // be identified by the magic field in its header, or else as an external tileset if the content is JSON."
600 // This is rather annoying... it means we have to do a network request in order to determine whether
601 // a tile has children or geometry content!
602
603 // let's avoid this request if we can get away with it:
604 const thread_local QRegularExpression isJsonRx( u".*\\.json(?:\\?.*)?$"_s, QRegularExpression::PatternOption::CaseInsensitiveOption );
605 if ( isJsonRx.match( contentUri ).hasMatch() )
607
608 // things we know definitely CAN'T be a child tile map:
609 const thread_local QRegularExpression antiCandidateRx( u".*\\.(gltf|glb|b3dm|i3dm|pnts|cmpt|bin|glbin|glbuf|png|jpeg|jpg)(?:\\?.*)?$"_s, QRegularExpression::PatternOption::CaseInsensitiveOption );
610 if ( antiCandidateRx.match( contentUri ).hasMatch() )
612
613 // here we **could** do a fetch to verify what the content actually is. But we want this method to be non-blocking,
614 // so let's just report that there IS remote children available and then sort things out when we actually go to fetch those children...
616}
617
618bool QgsCesiumTiledSceneIndex::fetchHierarchy( long long id, QgsFeedback *feedback )
619{
620 QMutexLocker locker( &mLock );
621 auto it = mNodeMap.constFind( id );
622 if ( it == mNodeMap.constEnd() )
623 return false;
624
625 {
626 // maybe we already know what content type this tile has. If so, and it's not json, then
627 // don't try to fetch it as a hierarchy
628 auto it = mTileContentFormats.constFind( id );
629 if ( it != mTileContentFormats.constEnd() )
630 {
631 switch ( it.value() )
632 {
633 case TileContentFormat::NotJson:
634 return false;
635 case TileContentFormat::Json:
636 break;
637 }
638 }
639 }
640
641 const QString contentUri = it.value()->tile()->resources().value( u"content"_s ).toString();
642 locker.unlock();
643
644 if ( contentUri.isEmpty() )
645 return false;
646
647 // if node has content json, fetch it now and parse
648 const QByteArray subTile = retrieveContent( contentUri, feedback );
649 if ( !subTile.isEmpty() )
650 {
651 // we don't know for certain that the content IS json -- from https://github.com/CesiumGS/3d-tiles/tree/main/specification#tile-json says:
652 // "A file extension is not required for content.uri. A content’s tile format can
653 // be identified by the magic field in its header, or else as an external tileset if the content is JSON."
654 try
655 {
656 const auto subTileJson = json::parse( subTile.toStdString() );
657 QMutexLocker locker( &mLock );
658 refineNodeFromJson( it.value(), QUrl( contentUri ), subTileJson );
659 mTileContentFormats.insert( id, TileContentFormat::Json );
660 return true;
661 }
662 catch ( json::parse_error & )
663 {
664 QMutexLocker locker( &mLock );
665 mTileContentFormats.insert( id, TileContentFormat::NotJson );
666 return false;
667 }
668 }
669 else
670 {
671 // we got empty content, so the hierarchy content is probably missing,
672 // so let's mark it as not JSON so that we do not try to fetch it again
673 mTileContentFormats.insert( id, TileContentFormat::NotJson );
674 return false;
675 }
676}
677
678QByteArray QgsCesiumTiledSceneIndex::fetchContent( const QString &uri, QgsFeedback *feedback )
679{
680 QUrl url( uri );
681 // TODO -- error reporting?
682 if ( uri.startsWith( "http" ) )
683 {
684 QNetworkRequest networkRequest = QNetworkRequest( url );
685 QgsSetRequestInitiatorClass( networkRequest, u"QgsCesiumTiledSceneIndex"_s );
686 networkRequest.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
687 networkRequest.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
688
689 mHeaders.updateNetworkRequest( networkRequest );
690
691 if ( QThread::currentThread() == QApplication::instance()->thread() )
692 {
693 // running on main thread, use a blocking get to handle authcfg and SSL errors ok.
695 networkRequest, mAuthCfg, false, feedback );
696 return reply.content();
697 }
698 else
699 {
700 // running on background thread, use tile download manager for efficient network handling
701 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( networkRequest, mAuthCfg ) )
702 {
703 // TODO -- report error
704 return QByteArray();
705 }
706 std::unique_ptr< QgsTileDownloadManagerReply > reply( QgsApplication::tileDownloadManager()->get( networkRequest ) );
707
708 QEventLoop loop;
709 if ( feedback )
710 QObject::connect( feedback, &QgsFeedback::canceled, &loop, &QEventLoop::quit );
711
712 QObject::connect( reply.get(), &QgsTileDownloadManagerReply::finished, &loop, &QEventLoop::quit );
713 loop.exec();
714
715 return reply->data();
716 }
717 }
718 else if ( url.isLocalFile() && QFile::exists( url.toLocalFile() ) )
719 {
720 QFile file( url.toLocalFile() );
721 if ( file.open( QIODevice::ReadOnly ) )
722 {
723 return file.readAll();
724 }
725 }
726 return QByteArray();
727}
728
729
730//
731// QgsCesiumTilesDataProviderSharedData
732//
733
734QgsCesiumTilesDataProviderSharedData::QgsCesiumTilesDataProviderSharedData()
735 : mIndex( QgsTiledSceneIndex( nullptr ) )
736{
737
738}
739
740void QgsCesiumTilesDataProviderSharedData::initialize( const QString &tileset, const QUrl &rootUrl, const QgsCoordinateTransformContext &transformContext, const QString &authCfg, const QgsHttpHeaders &headers )
741{
742 mTileset = json::parse( tileset.toStdString() );
743 if ( !mTileset.contains( "root" ) )
744 {
745 mError = QObject::tr( "JSON is not a valid Cesium 3D Tiles source (does not contain \"root\" value)" );
746 return;
747 }
748
749 mLayerMetadata.setType( u"dataset"_s );
750
751 if ( mTileset.contains( "asset" ) )
752 {
753 const auto &asset = mTileset[ "asset" ];
754 if ( asset.contains( "tilesetVersion" ) )
755 {
756 try
757 {
758 const QString tilesetVersion = QString::fromStdString( asset["tilesetVersion"].get<std::string>() );
759 mLayerMetadata.setIdentifier( tilesetVersion );
760 }
761 catch ( json::type_error & )
762 {
763 QgsDebugError( u"Error when parsing tilesetVersion value"_s );
764 }
765 }
766 }
767
768 mIndex = QgsTiledSceneIndex(
769 new QgsCesiumTiledSceneIndex(
770 mTileset,
771 rootUrl,
772 authCfg,
773 headers,
774 transformContext
775 )
776 );
777
778 // parse root
779 {
780 const auto &root = mTileset[ "root" ];
781 // parse root bounding volume
782
783 // TODO -- read crs from metadata tags. Need to find real world examples of this. And can metadata crs override
784 // the EPSG:4979 requirement from a region bounding volume??
785
786 {
787 // TODO -- on some datasets there is a "boundingVolume" present on the tileset itself, i.e. not the root node.
788 // what does this mean? Should we use it instead of the root node bounding volume if it's present?
789
791
792 const auto &rootBoundingVolume = root[ "boundingVolume" ];
793
794 QgsMatrix4x4 rootTransform;
795 if ( root.contains( "transform" ) && !root["transform"].is_null() )
796 {
797 const auto &transformJson = root["transform"];
798 double *ptr = rootTransform.data();
799 for ( int i = 0; i < 16; ++i )
800 ptr[i] = transformJson[i].get<double>();
801 }
802
803 if ( rootBoundingVolume.contains( "region" ) )
804 {
805 const QgsBox3D rootRegion = QgsCesiumUtils::parseRegion( rootBoundingVolume[ "region" ] );
806 if ( !rootRegion.isNull() )
807 {
808 mBoundingVolume = QgsTiledSceneBoundingVolume( QgsOrientedBox3D::fromBox3D( rootRegion ) );
809
810 // only set z range for datasets which aren't too large (ie global datasets)
811 if ( !mIndex.rootTile().boundingVolume().box().isNull() )
812 {
813 mZRange = QgsDoubleRange( rootRegion.zMinimum(), rootRegion.zMaximum() );
814 }
815 mLayerCrs = QgsCoordinateReferenceSystem( u"EPSG:4979"_s );
816 mSceneCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
817
818 mLayerMetadata.setCrs( mSceneCrs );
819 mExtent = rootRegion.toRectangle();
820 spatialExtent.extentCrs = QgsCoordinateReferenceSystem( u"EPSG:4979"_s );
821 spatialExtent.bounds = rootRegion;
822 }
823 }
824 else if ( rootBoundingVolume.contains( "box" ) )
825 {
826 const QgsOrientedBox3D bbox = QgsCesiumUtils::parseBox( rootBoundingVolume["box"] );
827 if ( !bbox.isNull() )
828 {
829 // layer must advertise as EPSG:4979, as the various QgsMapLayer
830 // methods which utilize QgsMapLayer::crs() (such as layer extent transformation)
831 // are all purely 2D and can't handle the cesium data source z value
832 // range in EPSG:4978 !
833 mLayerCrs = QgsCoordinateReferenceSystem( u"EPSG:4979"_s );
834 mSceneCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
835 mLayerMetadata.setCrs( mSceneCrs );
836
837 mBoundingVolume = QgsTiledSceneBoundingVolume( bbox );
838 mBoundingVolume.transform( rootTransform );
839 try
840 {
841 QgsCoordinateTransform ct( mSceneCrs, mLayerCrs, transformContext );
842 ct.setBallparkTransformsAreAppropriate( true );
843 const QgsBox3D rootRegion = mBoundingVolume.bounds( ct );
844 // only set z range for datasets which aren't too large (ie global datasets)
845 if ( !mIndex.rootTile().boundingVolume().box().isNull() )
846 {
847 mZRange = QgsDoubleRange( rootRegion.zMinimum(), rootRegion.zMaximum() );
848 }
849
850 std::unique_ptr< QgsAbstractGeometry > extent2D( mBoundingVolume.as2DGeometry( ct ) );
851 mExtent = extent2D->boundingBox();
852 }
853 catch ( QgsCsException & )
854 {
855 QgsDebugError( u"Caught transform exception when transforming boundingVolume"_s );
856 }
857
858 spatialExtent.extentCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
859 spatialExtent.bounds = mBoundingVolume.bounds();
860 }
861 }
862 else if ( rootBoundingVolume.contains( "sphere" ) )
863 {
864 QgsSphere sphere = QgsCesiumUtils::parseSphere( rootBoundingVolume["sphere"] );
865 if ( !sphere.isNull() )
866 {
867 // layer must advertise as EPSG:4979, as the various QgsMapLayer
868 // methods which utilize QgsMapLayer::crs() (such as layer extent transformation)
869 // are all purely 2D and can't handle the cesium data source z value
870 // range in EPSG:4978 !
871 mLayerCrs = QgsCoordinateReferenceSystem( u"EPSG:4979"_s );
872 mSceneCrs = QgsCoordinateReferenceSystem( u"EPSG:4978"_s );
873 mLayerMetadata.setCrs( mSceneCrs );
874
875 sphere = QgsCesiumUtils::transformSphere( sphere, rootTransform );
876
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
902 {
903 mError = QObject::tr( "JSON is not a valid Cesium 3D Tiles source (unsupported boundingVolume format)" );
904 return;
905 }
906
907 QgsLayerMetadata::Extent layerExtent;
908 layerExtent.setSpatialExtents( {spatialExtent } );
909 mLayerMetadata.setExtent( layerExtent );
910 }
911 }
912}
913
914
915//
916// QgsCesiumTilesDataProvider
917//
918
919QgsCesiumTilesDataProvider::QgsCesiumTilesDataProvider( const QString &uri, const ProviderOptions &providerOptions, Qgis::DataProviderReadFlags flags )
920 : QgsTiledSceneDataProvider( uri, providerOptions, flags )
921 , mShared( std::make_shared< QgsCesiumTilesDataProviderSharedData >() )
922{
923 QgsDataSourceUri dsUri;
924 dsUri.setEncodedUri( uri );
925 mAuthCfg = dsUri.authConfigId();
926 mHeaders = dsUri.httpHeaders();
927
928 mIsValid = init();
929}
930
931QgsCesiumTilesDataProvider::QgsCesiumTilesDataProvider( const QgsCesiumTilesDataProvider &other )
933 , mIsValid( other.mIsValid )
934 , mAuthCfg( other.mAuthCfg )
935 , mHeaders( other.mHeaders )
936{
937 QgsReadWriteLocker locker( other.mShared->mReadWriteLock, QgsReadWriteLocker::Read );
938 mShared = other.mShared;
939}
940
941Qgis::DataProviderFlags QgsCesiumTilesDataProvider::flags() const
942{
943 return mProviderFlags;
944}
945
946Qgis::TiledSceneProviderCapabilities QgsCesiumTilesDataProvider::capabilities() const
947{
949}
950
951QgsCesiumTilesDataProvider::~QgsCesiumTilesDataProvider() = default;
952
953QgsCesiumTilesDataProvider *QgsCesiumTilesDataProvider::clone() const
954{
956 return new QgsCesiumTilesDataProvider( *this );
957}
958
959bool QgsCesiumTilesDataProvider::init()
960{
962
963 QString tileSetUri;
964 const QString uri = dataSourceUri();
965
966 if ( uri.startsWith( "ion://"_L1 ) )
967 {
968 QUrl url( uri );
969 const QString assetId = QUrlQuery( url ).queryItemValue( u"assetId"_s );
970 const QString accessToken = QUrlQuery( url ).queryItemValue( u"accessToken"_s );
971
972 const QString CESIUM_ION_URL = u"https://api.cesium.com/"_s;
973
974 // get asset info
975 {
976 const QString assetInfoEndpoint = CESIUM_ION_URL + u"v1/assets/%1"_s.arg( assetId );
977 QNetworkRequest request = QNetworkRequest( assetInfoEndpoint );
978 QgsSetRequestInitiatorClass( request, u"QgsCesiumTilesDataProvider"_s )
979 mHeaders.updateNetworkRequest( request );
980 if ( !accessToken.isEmpty() )
981 request.setRawHeader( "Authorization", u"Bearer %1"_s.arg( accessToken ).toLocal8Bit() );
982
983 QgsBlockingNetworkRequest networkRequest;
984 if ( accessToken.isEmpty() )
985 networkRequest.setAuthCfg( mAuthCfg );
986
987 switch ( networkRequest.get( request ) )
988 {
990 break;
991
995 // TODO -- error reporting
996 return false;
997 }
998
999 const QgsNetworkReplyContent content = networkRequest.reply();
1000 const json assetInfoJson = json::parse( content.content().toStdString() );
1001 if ( assetInfoJson["type"] != "3DTILES" )
1002 {
1003 appendError( QgsErrorMessage( tr( "Only ion 3D Tiles content can be accessed, not %1" ).arg( QString::fromStdString( assetInfoJson["type"].get<std::string>() ) ) ) );
1004 return false;
1005 }
1006
1007 const QString name = QString::fromStdString( assetInfoJson["name"].get<std::string>() );
1008 if ( name.compare( "Google Photorealistic 3D Tiles"_L1, Qt::CaseInsensitive ) == 0 )
1009 {
1010 // consider Google Photorealistic 3D Tiles as a basemap source, as this completely covers
1011 // the globe and contains embedded terrain
1012 mProviderFlags.setFlag( Qgis::DataProviderFlag::IsBasemapSource, true );
1013 mProviderFlags.setFlag( Qgis::DataProviderFlag::Is3DBasemapSource, true );
1014 }
1015
1016 mShared->mLayerMetadata.setTitle( name );
1017 mShared->mLayerMetadata.setAbstract( QString::fromStdString( assetInfoJson["description"].get<std::string>() ) );
1018 const QString attribution = QString::fromStdString( assetInfoJson["attribution"].get<std::string>() );
1019 if ( !attribution.isEmpty() )
1020 mShared->mLayerMetadata.setRights( { attribution } );
1021
1022 mShared->mLayerMetadata.setDateTime( Qgis::MetadataDateType::Created, QDateTime::fromString( QString::fromStdString( assetInfoJson["dateAdded"].get<std::string>() ), Qt::DateFormat::ISODate ) );
1023 }
1024
1025 // get tileset access details
1026 {
1027 const QString tileAccessEndpoint = CESIUM_ION_URL + u"v1/assets/%1/endpoint"_s.arg( assetId );
1028 QNetworkRequest request = QNetworkRequest( tileAccessEndpoint );
1029 QgsSetRequestInitiatorClass( request, u"QgsCesiumTilesDataProvider"_s )
1030 mHeaders.updateNetworkRequest( request );
1031 if ( !accessToken.isEmpty() )
1032 request.setRawHeader( "Authorization", u"Bearer %1"_s.arg( accessToken ).toLocal8Bit() );
1033
1034 QgsBlockingNetworkRequest networkRequest;
1035 if ( accessToken.isEmpty() )
1036 networkRequest.setAuthCfg( mAuthCfg );
1037
1038 switch ( networkRequest.get( request ) )
1039 {
1041 break;
1042
1046 // TODO -- error reporting
1047 return false;
1048 }
1049
1050 const QgsNetworkReplyContent content = networkRequest.reply();
1051 const json tileAccessJson = json::parse( content.content().toStdString() );
1052
1053 if ( tileAccessJson.contains( "url" ) )
1054 {
1055 tileSetUri = QString::fromStdString( tileAccessJson["url"].get<std::string>() );
1056 }
1057 else if ( tileAccessJson.contains( "options" ) )
1058 {
1059 const auto &optionsJson = tileAccessJson["options"];
1060 if ( optionsJson.contains( "url" ) )
1061 {
1062 tileSetUri = QString::fromStdString( optionsJson["url"].get<std::string>() );
1063 }
1064 }
1065
1066 if ( tileAccessJson.contains( "accessToken" ) )
1067 {
1068 // The tileset accessToken is NOT the same as the token we use to access the asset details -- ie we can't
1069 // use the same authentication as we got from the providers auth cfg!
1070 mHeaders.insert( u"Authorization"_s,
1071 u"Bearer %1"_s.arg( QString::fromStdString( tileAccessJson["accessToken"].get<std::string>() ) ) );
1072 }
1073 mAuthCfg.clear();
1074 }
1075 }
1076 else
1077 {
1078 QgsDataSourceUri dsUri;
1079 dsUri.setEncodedUri( uri );
1080 tileSetUri = dsUri.param( u"url"_s );
1081 }
1082
1083 if ( !tileSetUri.isEmpty() )
1084 {
1085 const QUrl url( tileSetUri );
1086
1087 QNetworkRequest request = QNetworkRequest( url );
1088 QgsSetRequestInitiatorClass( request, u"QgsCesiumTilesDataProvider"_s )
1089 mHeaders.updateNetworkRequest( request );
1090
1091 QgsBlockingNetworkRequest networkRequest;
1092 networkRequest.setAuthCfg( mAuthCfg );
1093
1094 switch ( networkRequest.get( request ) )
1095 {
1097 break;
1098
1102 // TODO -- error reporting
1103 return false;
1104 }
1105
1106 const QgsNetworkReplyContent content = networkRequest.reply();
1107
1108 mShared->initialize( content.content(), tileSetUri, transformContext(), mAuthCfg, mHeaders );
1109
1110 mShared->mLayerMetadata.addLink( QgsAbstractMetadataBase::Link( tr( "Source" ), u"WWW:LINK"_s, tileSetUri ) );
1111 }
1112 else
1113 {
1114 // try uri as a local file
1115 const QFileInfo fi( dataSourceUri() );
1116 if ( fi.exists() )
1117 {
1118 QFile file( dataSourceUri( ) );
1119 if ( file.open( QIODevice::ReadOnly | QIODevice::Text ) )
1120 {
1121 const QByteArray raw = file.readAll();
1122 mShared->initialize( raw, QUrl::fromLocalFile( dataSourceUri() ), transformContext(), mAuthCfg, mHeaders );
1123 }
1124 else
1125 {
1126 return false;
1127 }
1128 }
1129 else
1130 {
1131 return false;
1132 }
1133 }
1134
1135 if ( !mShared->mIndex.isValid() )
1136 {
1137 appendError( mShared->mError );
1138 return false;
1139 }
1140 return true;
1141}
1142
1143QgsCoordinateReferenceSystem QgsCesiumTilesDataProvider::crs() const
1144{
1146
1147 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1148 return mShared->mLayerCrs;
1149}
1150
1151QgsRectangle QgsCesiumTilesDataProvider::extent() const
1152{
1154
1155 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1156 return mShared->mExtent;
1157}
1158
1159bool QgsCesiumTilesDataProvider::isValid() const
1160{
1162
1163 return mIsValid;
1164}
1165
1166QString QgsCesiumTilesDataProvider::name() const
1167{
1169
1170 return PROVIDER_KEY;
1171}
1172
1173QString QgsCesiumTilesDataProvider::description() const
1174{
1176
1177 return QObject::tr( "Cesium 3D Tiles" );
1178}
1179
1180QString QgsCesiumTilesDataProvider::htmlMetadata() const
1181{
1183
1184 QString metadata;
1185
1186 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1187 if ( mShared->mTileset.contains( "asset" ) )
1188 {
1189 const auto &asset = mShared->mTileset[ "asset" ];
1190 if ( asset.contains( "version" ) )
1191 {
1192 const QString version = QString::fromStdString( asset["version"].get<std::string>() );
1193 metadata += u"<tr><td class=\"highlight\">"_s % tr( "3D Tiles Version" ) % u"</td><td>%1</a>"_s.arg( version ) % u"</td></tr>\n"_s;
1194 }
1195
1196 if ( asset.contains( "tilesetVersion" ) )
1197 {
1198 try
1199 {
1200 const QString tilesetVersion = QString::fromStdString( asset["tilesetVersion"].get<std::string>() );
1201 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Tileset Version" ) % u"</td><td>%1</a>"_s.arg( tilesetVersion ) % u"</td></tr>\n"_s;
1202 }
1203 catch ( json::type_error & )
1204 {
1205 QgsDebugError( u"Error when parsing tilesetVersion value"_s );
1206 }
1207 }
1208
1209 if ( asset.contains( "generator" ) )
1210 {
1211 const QString generator = QString::fromStdString( asset["generator"].get<std::string>() );
1212 metadata += u"<tr><td class=\"highlight\">"_s % tr( "Tileset Generator" ) % u"</td><td>%1</a>"_s.arg( generator ) % u"</td></tr>\n"_s;
1213 }
1214 }
1215 if ( mShared->mTileset.contains( "extensionsRequired" ) )
1216 {
1217 QStringList extensions;
1218 for ( const auto &item : mShared->mTileset["extensionsRequired"] )
1219 {
1220 extensions << QString::fromStdString( item.get<std::string>() );
1221 }
1222 if ( !extensions.isEmpty() )
1223 {
1224 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;
1225 }
1226 }
1227 if ( mShared->mTileset.contains( "extensionsUsed" ) )
1228 {
1229 QStringList extensions;
1230 for ( const auto &item : mShared->mTileset["extensionsUsed"] )
1231 {
1232 extensions << QString::fromStdString( item.get<std::string>() );
1233 }
1234 if ( !extensions.isEmpty() )
1235 {
1236 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;
1237 }
1238 }
1239
1240 if ( !mShared->mZRange.isInfinite() )
1241 {
1242 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;
1243 }
1244
1245 return metadata;
1246}
1247
1248QgsLayerMetadata QgsCesiumTilesDataProvider::layerMetadata() const
1249{
1251 if ( !mShared )
1252 return QgsLayerMetadata();
1253
1254 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1255 return mShared->mLayerMetadata;
1256}
1257
1258const QgsCoordinateReferenceSystem QgsCesiumTilesDataProvider::sceneCrs() const
1259{
1261 if ( !mShared )
1263
1264 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1265 return mShared->mSceneCrs ;
1266}
1267
1268const QgsTiledSceneBoundingVolume &QgsCesiumTilesDataProvider::boundingVolume() const
1269{
1271 static QgsTiledSceneBoundingVolume nullVolume;
1272 if ( !mShared )
1273 return nullVolume;
1274
1275 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1276 return mShared ? mShared->mBoundingVolume : nullVolume;
1277}
1278
1279QgsTiledSceneIndex QgsCesiumTilesDataProvider::index() const
1280{
1282 if ( !mShared )
1283 return QgsTiledSceneIndex( nullptr );
1284
1285 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1286 return mShared->mIndex;
1287}
1288
1289QgsDoubleRange QgsCesiumTilesDataProvider::zRange() const
1290{
1292 if ( !mShared )
1293 return QgsDoubleRange();
1294
1295 QgsReadWriteLocker locker( mShared->mReadWriteLock, QgsReadWriteLocker::Read );
1296 return mShared->mZRange;
1297}
1298
1299
1300//
1301// QgsCesiumTilesProviderMetadata
1302//
1303
1304QgsCesiumTilesProviderMetadata::QgsCesiumTilesProviderMetadata():
1305 QgsProviderMetadata( PROVIDER_KEY, PROVIDER_DESCRIPTION )
1306{
1307}
1308
1309QIcon QgsCesiumTilesProviderMetadata::icon() const
1310{
1311 return QgsApplication::getThemeIcon( u"mIconCesium3dTiles.svg"_s );
1312}
1313
1314QgsCesiumTilesDataProvider *QgsCesiumTilesProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, Qgis::DataProviderReadFlags flags )
1315{
1316 return new QgsCesiumTilesDataProvider( uri, options, flags );
1317}
1318
1319QList<QgsProviderSublayerDetails> QgsCesiumTilesProviderMetadata::querySublayers( const QString &uri, Qgis::SublayerQueryFlags, QgsFeedback * ) const
1320{
1321 const QVariantMap parts = decodeUri( uri );
1322 if ( parts.value( u"file-name"_s ).toString().compare( "tileset.json"_L1, Qt::CaseInsensitive ) == 0 )
1323 {
1325 details.setUri( uri );
1326 details.setProviderKey( PROVIDER_KEY );
1329 return {details};
1330 }
1331 else
1332 {
1333 return {};
1334 }
1335}
1336
1337int QgsCesiumTilesProviderMetadata::priorityForUri( const QString &uri ) const
1338{
1339 const QVariantMap parts = decodeUri( uri );
1340 if ( parts.value( u"file-name"_s ).toString().compare( "tileset.json"_L1, Qt::CaseInsensitive ) == 0 )
1341 return 100;
1342
1343 return 0;
1344}
1345
1346QList<Qgis::LayerType> QgsCesiumTilesProviderMetadata::validLayerTypesForUri( const QString &uri ) const
1347{
1348 const QVariantMap parts = decodeUri( uri );
1349 if ( parts.value( u"file-name"_s ).toString().compare( "tileset.json"_L1, Qt::CaseInsensitive ) == 0 )
1350 return QList< Qgis::LayerType>() << Qgis::LayerType::TiledScene;
1351
1352 return QList< Qgis::LayerType>();
1353}
1354
1355QVariantMap QgsCesiumTilesProviderMetadata::decodeUri( const QString &uri ) const
1356{
1357 QVariantMap uriComponents;
1358 QUrl url = QUrl::fromUserInput( uri );
1359 uriComponents.insert( u"file-name"_s, url.fileName() );
1360 uriComponents.insert( u"path"_s, uri );
1361 return uriComponents;
1362}
1363
1364QString QgsCesiumTilesProviderMetadata::filters( Qgis::FileFilterType type )
1365{
1366 switch ( type )
1367 {
1374 return QString();
1375
1377 return QObject::tr( "Cesium 3D Tiles" ) + u" (tileset.json TILESET.JSON)"_s;
1378 }
1379 return QString();
1380}
1381
1382QgsProviderMetadata::ProviderCapabilities QgsCesiumTilesProviderMetadata::providerCapabilities() const
1383{
1384 return FileBasedUris;
1385}
1386
1387QList<Qgis::LayerType> QgsCesiumTilesProviderMetadata::supportedLayerTypes() const
1388{
1389 return { Qgis::LayerType::TiledScene };
1390}
1391
1392QString QgsCesiumTilesProviderMetadata::encodeUri( const QVariantMap &parts ) const
1393{
1394 const QString path = parts.value( u"path"_s ).toString();
1395 return path;
1396}
1397
1398QgsProviderMetadata::ProviderMetadataCapabilities QgsCesiumTilesProviderMetadata::capabilities() const
1399{
1400 return ProviderMetadataCapability::LayerTypesForUri
1401 | ProviderMetadataCapability::PriorityForUri
1402 | ProviderMetadataCapability::QuerySublayers;
1403}
1404
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
@ Is3DBasemapSource
Associated source should be considered a '3D basemap' layer. See Qgis::MapLayerProperty::Is3DBasemapL...
Definition qgis.h:2370
@ IsBasemapSource
Associated source should be considered a 'basemap' layer. See Qgis::MapLayerProperty::IsBasemapLayer.
Definition qgis.h:2367
QFlags< DataProviderReadFlag > DataProviderReadFlags
Flags which control data provider construction.
Definition qgis.h:505
@ ReadLayerMetadata
Provider can read layer metadata from data store. See QgsDataProvider::layerMetadata().
Definition qgis.h:5923
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
Axis
Cartesian axes.
Definition qgis.h:2509
@ X
X-axis.
Definition qgis.h:2510
@ Z
Z-axis.
Definition qgis.h:2512
@ Y
Y-axis.
Definition qgis.h:2511
@ Created
Date created.
Definition qgis.h:4801
@ Additive
When tile is refined its content should be used alongside its children simultaneously.
Definition qgis.h:5959
@ Replacement
When tile is refined then its children should be used in place of itself.
Definition qgis.h:5958
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:261
QgsRectangle toRectangle() const
Converts the box to a 2D rectangle.
Definition qgsbox3d.h:381
QVector< QgsVector3D > corners() const
Returns an array of all box corners as 3D vectors.
Definition qgsbox3d.cpp:365
double width() const
Returns the width of the box.
Definition qgsbox3d.h:280
double zMinimum() const
Returns the minimum z value.
Definition qgsbox3d.h:254
double height() const
Returns the height of the box.
Definition qgsbox3d.h:287
bool isNull() const
Test if the box is null (holding no spatial information).
Definition qgsbox3d.cpp:316
static QgsSphere parseSphere(const json &sphere)
Parses a sphere object from a Cesium JSON document.
static QgsOrientedBox3D parseBox(const json &box)
Parses a box object from a Cesium JSON document to an oriented bounding box.
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: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
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:55
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:34
QgsBox3D boundingBox() const
Returns the 3-dimensional bounding box containing the sphere.
Definition qgssphere.cpp:79
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...
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
#define QgsDebugError(str)
Definition qgslogger.h:59
#define QgsSetRequestInitiatorClass(request, _class)
#define QGIS_PROTECT_QOBJECT_THREAD_ACCESS
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.