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