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