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