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