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