QGIS API Documentation 3.41.0-Master (3440c17df1d)
Loading...
Searching...
No Matches
qgseptpointcloudindex.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgspointcloudindex.cpp
3 --------------------
4 begin : October 2020
5 copyright : (C) 2020 by Peter Petrik
6 email : zilolv at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19#include "moc_qgseptpointcloudindex.cpp"
20#include <QFile>
21#include <QFileInfo>
22#include <QDir>
23#include <QJsonArray>
24#include <QJsonDocument>
25#include <QJsonObject>
26#include <QTime>
27#include <QtDebug>
28#include <QQueue>
29#include <QNetworkRequest>
30
31#include "qgsapplication.h"
34#include "qgseptdecoder.h"
36#include "qgslazdecoder.h"
41#include "qgslogger.h"
42#include "qgspointcloudexpression.h"
44
46
47#define PROVIDER_KEY QStringLiteral( "ept" )
48#define PROVIDER_DESCRIPTION QStringLiteral( "EPT point cloud provider" )
49
50QgsEptPointCloudIndex::QgsEptPointCloudIndex()
51{
52 mHierarchyNodes.insert( IndexedPointCloudNode( 0, 0, 0, 0 ) );
53}
54
55QgsEptPointCloudIndex::~QgsEptPointCloudIndex() = default;
56
57std::unique_ptr<QgsPointCloudIndex> QgsEptPointCloudIndex::clone() const
58{
59 QgsEptPointCloudIndex *clone = new QgsEptPointCloudIndex;
60 QMutexLocker locker( &mHierarchyMutex );
61 copyCommonProperties( clone );
62 return std::unique_ptr<QgsPointCloudIndex>( clone );
63}
64
65void QgsEptPointCloudIndex::load( const QString &urlString )
66{
67 QUrl url = urlString;
68 // Treat non-URLs as local files
69 if ( url.isValid() && ( url.scheme() == "http" || url.scheme() == "https" ) )
70 mAccessType = Remote;
71 else
72 mAccessType = Local;
73 mUri = urlString;
74
75 QStringList splitUrl = mUri.split( '/' );
76 splitUrl.pop_back();
77 mUrlDirectoryPart = splitUrl.join( '/' );
78
79 QByteArray content;
80 if ( mAccessType == Remote )
81 {
82 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
83 QgsSetRequestInitiatorClass( nr, QStringLiteral( "QgsEptPointCloudIndex" ) );
84
86 if ( req.get( nr ) != QgsBlockingNetworkRequest::NoError )
87 {
88 QgsDebugError( QStringLiteral( "Request failed: " ) + mUri );
89 mIsValid = false;
90 mError = req.errorMessage();
91 return;
92 }
93 content = req.reply().content();
94 }
95 else
96 {
97 QFile f( mUri );
98 if ( !f.open( QIODevice::ReadOnly ) )
99 {
100 mError = tr( "Unable to open %1 for reading" ).arg( mUri );
101 mIsValid = false;
102 return;
103 }
104 content = f.readAll();
105 }
106
107 bool success = loadSchema( content );
108 if ( success )
109 {
110 // try to import the metadata too!
111 const QString manifestPath = mUrlDirectoryPart + QStringLiteral( "/ept-sources/manifest.json" );
112 QByteArray manifestJson;
113 if ( mAccessType == Remote )
114 {
115 QUrl manifestUrl( manifestPath );
116
117 QNetworkRequest nr = QNetworkRequest( QUrl( manifestUrl ) );
118 QgsSetRequestInitiatorClass( nr, QStringLiteral( "QgsEptPointCloudIndex" ) );
120 if ( req.get( nr ) == QgsBlockingNetworkRequest::NoError )
121 manifestJson = req.reply().content();
122 }
123 else
124 {
125 QFile manifestFile( manifestPath );
126 if ( manifestFile.open( QIODevice::ReadOnly ) )
127 manifestJson = manifestFile.readAll();
128 }
129
130 if ( !manifestJson.isEmpty() )
131 loadManifest( manifestJson );
132 }
133
134 if ( !loadNodeHierarchy( IndexedPointCloudNode( 0, 0, 0, 0 ) ) )
135 {
136 QgsDebugError( QStringLiteral( "Failed to load root EPT node" ) );
137 success = false;
138 }
139
140 mIsValid = success;
141}
142
143void QgsEptPointCloudIndex::loadManifest( const QByteArray &manifestJson )
144{
145 QJsonParseError err;
146 // try to import the metadata too!
147 const QJsonDocument manifestDoc = QJsonDocument::fromJson( manifestJson, &err );
148 if ( err.error != QJsonParseError::NoError )
149 return;
150
151 const QJsonArray manifestArray = manifestDoc.array();
152 if ( manifestArray.empty() )
153 return;
154
155 // TODO how to handle multiple?
156 const QJsonObject sourceObject = manifestArray.at( 0 ).toObject();
157 const QString metadataPath = sourceObject.value( QStringLiteral( "metadataPath" ) ).toString();
158 const QString fullMetadataPath = mUrlDirectoryPart + QStringLiteral( "/ept-sources/" ) + metadataPath;
159
160 QByteArray metadataJson;
161 if ( mAccessType == Remote )
162 {
163 QUrl metadataUrl( fullMetadataPath );
164 QNetworkRequest nr = QNetworkRequest( QUrl( metadataUrl ) );
165 QgsSetRequestInitiatorClass( nr, QStringLiteral( "QgsEptPointCloudIndex" ) );
167 if ( req.get( nr ) != QgsBlockingNetworkRequest::NoError )
168 return;
169 metadataJson = req.reply().content();
170 }
171 else
172 {
173 QFile metadataFile( fullMetadataPath );
174 if ( ! metadataFile.open( QIODevice::ReadOnly ) )
175 return;
176 metadataJson = metadataFile.readAll();
177 }
178
179 const QJsonDocument metadataDoc = QJsonDocument::fromJson( metadataJson, &err );
180 if ( err.error != QJsonParseError::NoError )
181 return;
182
183 const QJsonObject metadataObject = metadataDoc.object().value( QStringLiteral( "metadata" ) ).toObject();
184 if ( metadataObject.empty() )
185 return;
186
187 const QJsonObject sourceMetadata = metadataObject.constBegin().value().toObject();
188 mOriginalMetadata = sourceMetadata.toVariantMap();
189}
190
191bool QgsEptPointCloudIndex::loadSchema( const QByteArray &dataJson )
192{
193 QJsonParseError err;
194 const QJsonDocument doc = QJsonDocument::fromJson( dataJson, &err );
195 if ( err.error != QJsonParseError::NoError )
196 return false;
197 const QJsonObject result = doc.object();
198 mDataType = result.value( QLatin1String( "dataType" ) ).toString(); // "binary" or "laszip"
199 if ( mDataType != QLatin1String( "laszip" ) && mDataType != QLatin1String( "binary" ) && mDataType != QLatin1String( "zstandard" ) )
200 return false;
201
202 const QString hierarchyType = result.value( QLatin1String( "hierarchyType" ) ).toString(); // "json" or "gzip"
203 if ( hierarchyType != QLatin1String( "json" ) )
204 return false;
205
206 mSpan = result.value( QLatin1String( "span" ) ).toInt();
207 mPointCount = result.value( QLatin1String( "points" ) ).toDouble();
208
209 // WKT
210 const QJsonObject srs = result.value( QLatin1String( "srs" ) ).toObject();
211 mWkt = srs.value( QLatin1String( "wkt" ) ).toString();
212
213 // rectangular
214 const QJsonArray bounds = result.value( QLatin1String( "bounds" ) ).toArray();
215 if ( bounds.size() != 6 )
216 return false;
217
218 const QJsonArray boundsConforming = result.value( QLatin1String( "boundsConforming" ) ).toArray();
219 if ( boundsConforming.size() != 6 )
220 return false;
221 mExtent.set( boundsConforming[0].toDouble(), boundsConforming[1].toDouble(),
222 boundsConforming[3].toDouble(), boundsConforming[4].toDouble() );
223 mZMin = boundsConforming[2].toDouble();
224 mZMax = boundsConforming[5].toDouble();
225
226 const QJsonArray schemaArray = result.value( QLatin1String( "schema" ) ).toArray();
228
229 for ( const QJsonValue &schemaItem : schemaArray )
230 {
231 const QJsonObject schemaObj = schemaItem.toObject();
232 const QString name = schemaObj.value( QLatin1String( "name" ) ).toString();
233 const QString type = schemaObj.value( QLatin1String( "type" ) ).toString();
234
235 const int size = schemaObj.value( QLatin1String( "size" ) ).toInt();
236
237 if ( name == QLatin1String( "ClassFlags" ) && size == 1 )
238 {
239 attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Synthetic" ), QgsPointCloudAttribute::UChar ) );
240 attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "KeyPoint" ), QgsPointCloudAttribute::UChar ) );
241 attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Withheld" ), QgsPointCloudAttribute::UChar ) );
242 attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Overlap" ), QgsPointCloudAttribute::UChar ) );
243 }
244 else if ( type == QLatin1String( "float" ) && ( size == 4 ) )
245 {
247 }
248 else if ( type == QLatin1String( "float" ) && ( size == 8 ) )
249 {
251 }
252 else if ( size == 1 )
253 {
255 }
256 else if ( type == QLatin1String( "unsigned" ) && size == 2 )
257 {
259 }
260 else if ( size == 2 )
261 {
263 }
264 else if ( size == 4 )
265 {
267 }
268 else
269 {
270 // unknown attribute type
271 return false;
272 }
273
274 double scale = 1.f;
275 if ( schemaObj.contains( QLatin1String( "scale" ) ) )
276 scale = schemaObj.value( QLatin1String( "scale" ) ).toDouble();
277
278 double offset = 0.f;
279 if ( schemaObj.contains( QLatin1String( "offset" ) ) )
280 offset = schemaObj.value( QLatin1String( "offset" ) ).toDouble();
281
282 if ( name == QLatin1String( "X" ) )
283 {
284 mOffset.set( offset, mOffset.y(), mOffset.z() );
285 mScale.set( scale, mScale.y(), mScale.z() );
286 }
287 else if ( name == QLatin1String( "Y" ) )
288 {
289 mOffset.set( mOffset.x(), offset, mOffset.z() );
290 mScale.set( mScale.x(), scale, mScale.z() );
291 }
292 else if ( name == QLatin1String( "Z" ) )
293 {
294 mOffset.set( mOffset.x(), mOffset.y(), offset );
295 mScale.set( mScale.x(), mScale.y(), scale );
296 }
297
298 // store any metadata stats which are present for the attribute
299 AttributeStatistics stats;
300 bool foundStats = false;
301 if ( schemaObj.contains( QLatin1String( "count" ) ) )
302 {
303 stats.count = schemaObj.value( QLatin1String( "count" ) ).toInt();
304 foundStats = true;
305 }
306 if ( schemaObj.contains( QLatin1String( "minimum" ) ) )
307 {
308 stats.minimum = schemaObj.value( QLatin1String( "minimum" ) ).toDouble();
309 foundStats = true;
310 }
311 if ( schemaObj.contains( QLatin1String( "maximum" ) ) )
312 {
313 stats.maximum = schemaObj.value( QLatin1String( "maximum" ) ).toDouble();
314 foundStats = true;
315 }
316 if ( schemaObj.contains( QLatin1String( "count" ) ) )
317 {
318 stats.mean = schemaObj.value( QLatin1String( "mean" ) ).toDouble();
319 foundStats = true;
320 }
321 if ( schemaObj.contains( QLatin1String( "stddev" ) ) )
322 {
323 stats.stDev = schemaObj.value( QLatin1String( "stddev" ) ).toDouble();
324 foundStats = true;
325 }
326 if ( schemaObj.contains( QLatin1String( "variance" ) ) )
327 {
328 stats.variance = schemaObj.value( QLatin1String( "variance" ) ).toDouble();
329 foundStats = true;
330 }
331 if ( foundStats )
332 mMetadataStats.insert( name, stats );
333
334 if ( schemaObj.contains( QLatin1String( "counts" ) ) )
335 {
336 QMap< int, int > classCounts;
337 const QJsonArray counts = schemaObj.value( QLatin1String( "counts" ) ).toArray();
338 for ( const QJsonValue &count : counts )
339 {
340 const QJsonObject countObj = count.toObject();
341 classCounts.insert( countObj.value( QLatin1String( "value" ) ).toInt(), countObj.value( QLatin1String( "count" ) ).toInt() );
342 }
343 mAttributeClasses.insert( name, classCounts );
344 }
345 }
346 setAttributes( attributes );
347
348 // save mRootBounds
349
350 // bounds (cube - octree volume)
351 const double xmin = bounds[0].toDouble();
352 const double ymin = bounds[1].toDouble();
353 const double zmin = bounds[2].toDouble();
354 const double xmax = bounds[3].toDouble();
355 const double ymax = bounds[4].toDouble();
356 const double zmax = bounds[5].toDouble();
357
358 mRootBounds = QgsPointCloudDataBounds(
359 ( xmin - mOffset.x() ) / mScale.x(),
360 ( ymin - mOffset.y() ) / mScale.y(),
361 ( zmin - mOffset.z() ) / mScale.z(),
362 ( xmax - mOffset.x() ) / mScale.x(),
363 ( ymax - mOffset.y() ) / mScale.y(),
364 ( zmax - mOffset.z() ) / mScale.z()
365 );
366
367
368#ifdef QGIS_DEBUG
369 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
370 QgsDebugMsgLevel( QStringLiteral( "lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 ); // all dims should be the same
371 QgsDebugMsgLevel( QStringLiteral( "res at lvl0 %1" ).arg( dx / mSpan ), 2 );
372 QgsDebugMsgLevel( QStringLiteral( "res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
373 QgsDebugMsgLevel( QStringLiteral( "res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
374#endif
375
376 return true;
377}
378
379std::unique_ptr<QgsPointCloudBlock> QgsEptPointCloudIndex::nodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request )
380{
381 if ( QgsPointCloudBlock *cached = getNodeDataFromCache( n, request ) )
382 {
383 return std::unique_ptr<QgsPointCloudBlock>( cached );
384 }
385
386 std::unique_ptr<QgsPointCloudBlock> block;
387 if ( mAccessType == Remote )
388 {
389 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
390 if ( !blockRequest )
391 return nullptr;
392
393 QEventLoop loop;
394 connect( blockRequest.get(), &QgsPointCloudBlockRequest::finished, &loop, &QEventLoop::quit );
395 loop.exec();
396
397 block = blockRequest->takeBlock();
398 if ( !block )
399 {
400 QgsDebugError( QStringLiteral( "Error downloading node %1 data, error : %2 " ).arg( n.toString(), blockRequest->errorStr() ) );
401 }
402 }
403 else
404 {
405 // we need to create a copy of the expression to pass to the decoder
406 // as the same QgsPointCloudExpression object mighgt be concurrently
407 // used on another thread, for example in a 3d view
408 QgsPointCloudExpression filterExpression = mFilterExpression;
409 QgsPointCloudAttributeCollection requestAttributes = request.attributes();
410 requestAttributes.extend( attributes(), filterExpression.referencedAttributes() );
411 QgsRectangle filterRect = request.filterRect();
412
413 if ( mDataType == QLatin1String( "binary" ) )
414 {
415 const QString filename = QStringLiteral( "%1/ept-data/%2.bin" ).arg( mUrlDirectoryPart, n.toString() );
416 block = QgsEptDecoder::decompressBinary( filename, attributes(), requestAttributes, scale(), offset(), filterExpression, filterRect );
417 }
418 else if ( mDataType == QLatin1String( "zstandard" ) )
419 {
420 const QString filename = QStringLiteral( "%1/ept-data/%2.zst" ).arg( mUrlDirectoryPart, n.toString() );
421 block = QgsEptDecoder::decompressZStandard( filename, attributes(), request.attributes(), scale(), offset(), filterExpression, filterRect );
422 }
423 else if ( mDataType == QLatin1String( "laszip" ) )
424 {
425 const QString filename = QStringLiteral( "%1/ept-data/%2.laz" ).arg( mUrlDirectoryPart, n.toString() );
426 block = QgsLazDecoder::decompressLaz( filename, requestAttributes, filterExpression, filterRect );
427 }
428 }
429
430 storeNodeDataToCache( block.get(), n, request );
431 return block;
432}
433
434QgsPointCloudBlockRequest *QgsEptPointCloudIndex::asyncNodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request )
435{
436 if ( QgsPointCloudBlock *cached = getNodeDataFromCache( n, request ) )
437 {
438 return new QgsCachedPointCloudBlockRequest( cached, n, mUri, attributes(), request.attributes(),
439 scale(), offset(), mFilterExpression, request.filterRect() );
440 }
441
442 if ( mAccessType != Remote )
443 return nullptr;
444
445 if ( !loadNodeHierarchy( n ) )
446 return nullptr;
447
448 QString fileUrl;
449 if ( mDataType == QLatin1String( "binary" ) )
450 {
451 fileUrl = QStringLiteral( "%1/ept-data/%2.bin" ).arg( mUrlDirectoryPart, n.toString() );
452 }
453 else if ( mDataType == QLatin1String( "zstandard" ) )
454 {
455 fileUrl = QStringLiteral( "%1/ept-data/%2.zst" ).arg( mUrlDirectoryPart, n.toString() );
456 }
457 else if ( mDataType == QLatin1String( "laszip" ) )
458 {
459 fileUrl = QStringLiteral( "%1/ept-data/%2.laz" ).arg( mUrlDirectoryPart, n.toString() );
460 }
461 else
462 {
463 return nullptr;
464 }
465
466 // we need to create a copy of the expression to pass to the decoder
467 // as the same QgsPointCloudExpression object might be concurrently
468 // used on another thread, for example in a 3d view
469 QgsPointCloudExpression filterExpression = mFilterExpression;
470 QgsPointCloudAttributeCollection requestAttributes = request.attributes();
471 requestAttributes.extend( attributes(), filterExpression.referencedAttributes() );
472 return new QgsEptPointCloudBlockRequest( n, fileUrl, mDataType, attributes(), requestAttributes, scale(), offset(), filterExpression, request.filterRect() );
473}
474
475bool QgsEptPointCloudIndex::hasNode( const IndexedPointCloudNode &n ) const
476{
477 return loadNodeHierarchy( n );
478}
479
480QgsCoordinateReferenceSystem QgsEptPointCloudIndex::crs() const
481{
483}
484
485qint64 QgsEptPointCloudIndex::pointCount() const
486{
487 return mPointCount;
488}
489
490qint64 QgsEptPointCloudIndex::nodePointCount( const IndexedPointCloudNode &nodeId ) const
491{
492 // First try loading our cached value
493 {
494 QMutexLocker locker( &mHierarchyMutex );
495 qint64 pointCount = mHierarchy.value( nodeId, -1 );
496 if ( pointCount != -1 )
497 return pointCount;
498 }
499
500 // Try loading all nodes' hierarchy files on the path from root and stop when
501 // one contains the point count for nodeId
502 QVector<IndexedPointCloudNode> pathToRoot = nodePathToRoot( nodeId );
503 for ( int i = pathToRoot.size() - 1; i >= 0; --i )
504 {
505 loadSingleNodeHierarchy( pathToRoot[i] );
506
507 QMutexLocker locker( &mHierarchyMutex );
508 qint64 pointCount = mHierarchy.value( nodeId, -1 );
509 if ( pointCount != -1 )
510 return pointCount;
511 }
512
513 return -1;
514}
515
516bool QgsEptPointCloudIndex::hasStatisticsMetadata() const
517{
518 return !mMetadataStats.isEmpty();
519}
520
521QVariant QgsEptPointCloudIndex::metadataStatistic( const QString &attribute, Qgis::Statistic statistic ) const
522{
523 if ( !mMetadataStats.contains( attribute ) )
524 return QVariant();
525
526 const AttributeStatistics &stats = mMetadataStats[ attribute ];
527 switch ( statistic )
528 {
530 return stats.count >= 0 ? QVariant( stats.count ) : QVariant();
531
533 return std::isnan( stats.mean ) ? QVariant() : QVariant( stats.mean );
534
536 return std::isnan( stats.stDev ) ? QVariant() : QVariant( stats.stDev );
537
539 return stats.minimum;
540
542 return stats.maximum;
543
545 return stats.minimum.isValid() && stats.maximum.isValid() ? QVariant( stats.maximum.toDouble() - stats.minimum.toDouble() ) : QVariant();
546
560 return QVariant();
561 }
562 return QVariant();
563}
564
565QVariantList QgsEptPointCloudIndex::metadataClasses( const QString &attribute ) const
566{
567 QVariantList classes;
568 const QMap< int, int > values = mAttributeClasses.value( attribute );
569 for ( auto it = values.constBegin(); it != values.constEnd(); ++it )
570 {
571 classes << it.key();
572 }
573 return classes;
574}
575
576QVariant QgsEptPointCloudIndex::metadataClassStatistic( const QString &attribute, const QVariant &value, Qgis::Statistic statistic ) const
577{
578 if ( statistic != Qgis::Statistic::Count )
579 return QVariant();
580
581 const QMap< int, int > values = mAttributeClasses.value( attribute );
582 if ( !values.contains( value.toInt() ) )
583 return QVariant();
584 return values.value( value.toInt() );
585}
586
587bool QgsEptPointCloudIndex::loadSingleNodeHierarchy( const IndexedPointCloudNode &nodeId ) const
588{
589 mHierarchyMutex.lock();
590 const bool foundInHierarchy = mHierarchy.contains( nodeId );
591 const bool foundInHierarchyNodes = mHierarchyNodes.contains( nodeId );
592 mHierarchyMutex.unlock();
593 // The hierarchy of the node is found => No need to load its file
594 if ( foundInHierarchy )
595 return true;
596 // We don't know that this node has a hierarchy file => We have nothing to load
597 if ( !foundInHierarchyNodes )
598 return true;
599
600 const QString filePath = QStringLiteral( "%1/ept-hierarchy/%2.json" ).arg( mUrlDirectoryPart, nodeId.toString() );
601
602 QByteArray dataJsonH;
603 if ( mAccessType == Remote )
604 {
605 QNetworkRequest nr( filePath );
606 QgsSetRequestInitiatorClass( nr, QStringLiteral( "QgsEptPointCloudIndex" ) );
607 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
608 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
609
610 std::unique_ptr<QgsTileDownloadManagerReply> reply( QgsApplication::tileDownloadManager()->get( nr ) );
611
612 QEventLoop loop;
613 connect( reply.get(), &QgsTileDownloadManagerReply::finished, &loop, &QEventLoop::quit );
614 loop.exec();
615
616 if ( reply->error() != QNetworkReply::NoError )
617 {
618 QgsDebugError( QStringLiteral( "Request failed: " ) + filePath );
619 return false;
620 }
621
622 dataJsonH = reply->data();
623 }
624 else
625 {
626 QFile file( filePath );
627 if ( ! file.open( QIODevice::ReadOnly ) )
628 {
629 QgsDebugError( QStringLiteral( "Loading file failed: " ) + filePath );
630 return false;
631 }
632 dataJsonH = file.readAll();
633 }
634
635 QJsonParseError errH;
636 const QJsonDocument docH = QJsonDocument::fromJson( dataJsonH, &errH );
637 if ( errH.error != QJsonParseError::NoError )
638 {
639 QgsDebugMsgLevel( QStringLiteral( "QJsonParseError when reading hierarchy from file %1" ).arg( filePath ), 2 );
640 return false;
641 }
642
643 QMutexLocker locker( &mHierarchyMutex );
644 const QJsonObject rootHObj = docH.object();
645 for ( auto it = rootHObj.constBegin(); it != rootHObj.constEnd(); ++it )
646 {
647 const QString nodeIdStr = it.key();
648 const int nodePointCount = it.value().toInt();
650 if ( nodePointCount >= 0 )
651 mHierarchy[nodeId] = nodePointCount;
652 else if ( nodePointCount == -1 )
653 mHierarchyNodes.insert( nodeId );
654 }
655
656 return true;
657}
658
659QVector<IndexedPointCloudNode> QgsEptPointCloudIndex::nodePathToRoot( const IndexedPointCloudNode &nodeId ) const
660{
661 QVector<IndexedPointCloudNode> path;
662 IndexedPointCloudNode currentNode = nodeId;
663 do
664 {
665 path.push_back( currentNode );
666 currentNode = currentNode.parentNode();
667 }
668 while ( currentNode.d() >= 0 );
669
670 return path;
671}
672
673bool QgsEptPointCloudIndex::loadNodeHierarchy( const IndexedPointCloudNode &nodeId ) const
674{
675 mHierarchyMutex.lock();
676 bool found = mHierarchy.contains( nodeId );
677 mHierarchyMutex.unlock();
678 if ( found )
679 return true;
680
681 QVector<IndexedPointCloudNode> pathToRoot = nodePathToRoot( nodeId );
682 for ( int i = pathToRoot.size() - 1; i >= 0 && !mHierarchy.contains( nodeId ); --i )
683 {
684 const IndexedPointCloudNode node = pathToRoot[i];
685 if ( !loadSingleNodeHierarchy( node ) )
686 return false;
687 }
688
689 mHierarchyMutex.lock();
690 found = mHierarchy.contains( nodeId );
691 mHierarchyMutex.unlock();
692
693 return found;
694}
695
696
697bool QgsEptPointCloudIndex::isValid() const
698{
699 return mIsValid;
700}
701
702QgsPointCloudIndex::AccessType QgsEptPointCloudIndex::accessType() const
703{
704 return mAccessType;
705}
706
707void QgsEptPointCloudIndex::copyCommonProperties( QgsEptPointCloudIndex *destination ) const
708{
710
711 // QgsEptPointCloudIndex specific fields
712 destination->mIsValid = mIsValid;
713 destination->mDataType = mDataType;
714 destination->mUrlDirectoryPart = mUrlDirectoryPart;
715 destination->mWkt = mWkt;
716 destination->mHierarchyNodes = mHierarchyNodes;
717 destination->mPointCount = mPointCount;
718 destination->mMetadataStats = mMetadataStats;
719 destination->mAttributeClasses = mAttributeClasses;
720 destination->mOriginalMetadata = mOriginalMetadata;
721}
722
Represents a indexed point cloud node in octree.
static IndexedPointCloudNode fromString(const QString &str)
Creates node from string.
QString toString() const
Encode node to string.
int d() const
Returns d.
IndexedPointCloudNode parentNode() const
Returns the parent of the node.
Statistic
Available generic statistics.
Definition qgis.h:5446
@ StDev
Standard deviation of values.
@ FirstQuartile
First quartile.
@ Mean
Mean of values.
@ Median
Median of values.
@ Max
Max of values.
@ Min
Min of values.
@ First
First value.
@ Range
Range of values (max - min)
@ Sum
Sum of values.
@ Minority
Minority of values.
@ CountMissing
Number of missing (null) values.
@ All
All statistics.
@ Majority
Majority of values.
@ Variety
Variety (count of distinct) values.
@ StDevSample
Sample standard deviation of values.
@ Last
Last value.
@ ThirdQuartile
Third quartile.
@ InterQuartileRange
Inter quartile range (IQR)
static QgsTileDownloadManager * tileDownloadManager()
Returns the application's tile download manager, used for download of map tiles when rendering.
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr, RequestFlags requestFlags=QgsBlockingNetworkRequest::RequestFlags())
Performs a "get" operation on the specified request.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
Class for handling a QgsPointCloudBlockRequest using existing cached QgsPointCloudBlock.
This class represents a coordinate reference system (CRS).
static QgsCoordinateReferenceSystem fromWkt(const QString &wkt)
Creates a CRS from a WKT spatial ref sys definition string.
Base class for handling loading QgsPointCloudBlock asynchronously from a remote EPT dataset.
QByteArray content() const
Returns the reply content.
Collection of point cloud attributes.
void push_back(const QgsPointCloudAttribute &attribute)
Adds extra attribute.
void extend(const QgsPointCloudAttributeCollection &otherCollection, const QSet< QString > &matchingNames)
Adds specific missing attributes from another QgsPointCloudAttributeCollection.
Attribute for point cloud data pair of name and size in bytes.
@ UShort
Unsigned short int 2 bytes.
@ UChar
Unsigned char 1 byte.
Base class for handling loading QgsPointCloudBlock asynchronously.
void finished()
Emitted when the request processing has finished.
Base class for storing raw data from point cloud nodes.
Represents packaged data bounds.
AccessType
The access type of the data, local is for local files and remote for remote files (over HTTP)
void copyCommonProperties(QgsPointCloudIndex *destination) const
Copies common properties to the destination index.
Point cloud data request.
QgsPointCloudAttributeCollection attributes() const
Returns attributes.
QgsRectangle filterRect() const
Returns the rectangle from which points will be taken, in point cloud's crs.
A rectangle specified with double values.
void finished()
Emitted when the reply has finished (either with a success or with a failure)
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:39
#define QgsDebugError(str)
Definition qgslogger.h:38
#define QgsSetRequestInitiatorClass(request, _class)