19#include "moc_qgseptpointcloudindex.cpp"
24#include <QJsonDocument>
29#include <QNetworkRequest>
42#include "qgspointcloudexpression.h"
47#define PROVIDER_KEY QStringLiteral( "ept" )
48#define PROVIDER_DESCRIPTION QStringLiteral( "EPT point cloud provider" )
50QgsEptPointCloudIndex::QgsEptPointCloudIndex()
55QgsEptPointCloudIndex::~QgsEptPointCloudIndex() =
default;
57std::unique_ptr<QgsPointCloudIndex> QgsEptPointCloudIndex::clone()
const
59 QgsEptPointCloudIndex *clone =
new QgsEptPointCloudIndex;
60 QMutexLocker locker( &mHierarchyMutex );
61 copyCommonProperties( clone );
62 return std::unique_ptr<QgsPointCloudIndex>( clone );
65void QgsEptPointCloudIndex::load(
const QString &urlString )
69 if ( url.isValid() && ( url.scheme() ==
"http" || url.scheme() ==
"https" ) )
75 QStringList splitUrl = mUri.split(
'/' );
77 mUrlDirectoryPart = splitUrl.join(
'/' );
80 if ( mAccessType == Remote )
82 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
98 if ( !f.open( QIODevice::ReadOnly ) )
100 mError = tr(
"Unable to open %1 for reading" ).arg( mUri );
104 content = f.readAll();
107 bool success = loadSchema( content );
111 const QString manifestPath = mUrlDirectoryPart + QStringLiteral(
"/ept-sources/manifest.json" );
112 QByteArray manifestJson;
113 if ( mAccessType == Remote )
115 QUrl manifestUrl( manifestPath );
117 QNetworkRequest nr = QNetworkRequest( QUrl( manifestUrl ) );
125 QFile manifestFile( manifestPath );
126 if ( manifestFile.open( QIODevice::ReadOnly ) )
127 manifestJson = manifestFile.readAll();
130 if ( !manifestJson.isEmpty() )
131 loadManifest( manifestJson );
136 QgsDebugError( QStringLiteral(
"Failed to load root EPT node" ) );
143void QgsEptPointCloudIndex::loadManifest(
const QByteArray &manifestJson )
147 const QJsonDocument manifestDoc = QJsonDocument::fromJson( manifestJson, &err );
148 if ( err.error != QJsonParseError::NoError )
151 const QJsonArray manifestArray = manifestDoc.array();
152 if ( manifestArray.empty() )
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;
160 QByteArray metadataJson;
161 if ( mAccessType == Remote )
163 QUrl metadataUrl( fullMetadataPath );
164 QNetworkRequest nr = QNetworkRequest( QUrl( metadataUrl ) );
173 QFile metadataFile( fullMetadataPath );
174 if ( ! metadataFile.open( QIODevice::ReadOnly ) )
176 metadataJson = metadataFile.readAll();
179 const QJsonDocument metadataDoc = QJsonDocument::fromJson( metadataJson, &err );
180 if ( err.error != QJsonParseError::NoError )
183 const QJsonObject metadataObject = metadataDoc.object().value( QStringLiteral(
"metadata" ) ).toObject();
184 if ( metadataObject.empty() )
187 const QJsonObject sourceMetadata = metadataObject.constBegin().value().toObject();
188 mOriginalMetadata = sourceMetadata.toVariantMap();
191bool QgsEptPointCloudIndex::loadSchema(
const QByteArray &dataJson )
194 const QJsonDocument doc = QJsonDocument::fromJson( dataJson, &err );
195 if ( err.error != QJsonParseError::NoError )
197 const QJsonObject result = doc.object();
198 mDataType = result.value( QLatin1String(
"dataType" ) ).toString();
199 if ( mDataType != QLatin1String(
"laszip" ) && mDataType != QLatin1String(
"binary" ) && mDataType != QLatin1String(
"zstandard" ) )
202 const QString hierarchyType = result.value( QLatin1String(
"hierarchyType" ) ).toString();
203 if ( hierarchyType != QLatin1String(
"json" ) )
206 mSpan = result.value( QLatin1String(
"span" ) ).toInt();
207 mPointCount = result.value( QLatin1String(
"points" ) ).toDouble();
210 const QJsonObject srs = result.value( QLatin1String(
"srs" ) ).toObject();
211 mWkt = srs.value( QLatin1String(
"wkt" ) ).toString();
214 const QJsonArray bounds = result.value( QLatin1String(
"bounds" ) ).toArray();
215 if ( bounds.size() != 6 )
218 const QJsonArray boundsConforming = result.value( QLatin1String(
"boundsConforming" ) ).toArray();
219 if ( boundsConforming.size() != 6 )
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();
226 const QJsonArray schemaArray = result.value( QLatin1String(
"schema" ) ).toArray();
229 for (
const QJsonValue &schemaItem : schemaArray )
231 const QJsonObject schemaObj = schemaItem.toObject();
232 const QString name = schemaObj.value( QLatin1String(
"name" ) ).toString();
233 const QString type = schemaObj.value( QLatin1String(
"type" ) ).toString();
235 const int size = schemaObj.value( QLatin1String(
"size" ) ).toInt();
237 if ( name == QLatin1String(
"ClassFlags" ) && size == 1 )
244 else if ( type == QLatin1String(
"float" ) && ( size == 4 ) )
248 else if ( type == QLatin1String(
"float" ) && ( size == 8 ) )
252 else if ( size == 1 )
256 else if ( type == QLatin1String(
"unsigned" ) && size == 2 )
260 else if ( size == 2 )
264 else if ( size == 4 )
275 if ( schemaObj.contains( QLatin1String(
"scale" ) ) )
276 scale = schemaObj.value( QLatin1String(
"scale" ) ).toDouble();
279 if ( schemaObj.contains( QLatin1String(
"offset" ) ) )
280 offset = schemaObj.value( QLatin1String(
"offset" ) ).toDouble();
282 if ( name == QLatin1String(
"X" ) )
284 mOffset.set( offset, mOffset.y(), mOffset.z() );
285 mScale.set( scale, mScale.y(), mScale.z() );
287 else if ( name == QLatin1String(
"Y" ) )
289 mOffset.set( mOffset.x(), offset, mOffset.z() );
290 mScale.set( mScale.x(), scale, mScale.z() );
292 else if ( name == QLatin1String(
"Z" ) )
294 mOffset.set( mOffset.x(), mOffset.y(), offset );
295 mScale.set( mScale.x(), mScale.y(), scale );
299 AttributeStatistics stats;
300 bool foundStats =
false;
301 if ( schemaObj.contains( QLatin1String(
"count" ) ) )
303 stats.count = schemaObj.value( QLatin1String(
"count" ) ).toInt();
306 if ( schemaObj.contains( QLatin1String(
"minimum" ) ) )
308 stats.minimum = schemaObj.value( QLatin1String(
"minimum" ) ).toDouble();
311 if ( schemaObj.contains( QLatin1String(
"maximum" ) ) )
313 stats.maximum = schemaObj.value( QLatin1String(
"maximum" ) ).toDouble();
316 if ( schemaObj.contains( QLatin1String(
"count" ) ) )
318 stats.mean = schemaObj.value( QLatin1String(
"mean" ) ).toDouble();
321 if ( schemaObj.contains( QLatin1String(
"stddev" ) ) )
323 stats.stDev = schemaObj.value( QLatin1String(
"stddev" ) ).toDouble();
326 if ( schemaObj.contains( QLatin1String(
"variance" ) ) )
328 stats.variance = schemaObj.value( QLatin1String(
"variance" ) ).toDouble();
332 mMetadataStats.insert( name, stats );
334 if ( schemaObj.contains( QLatin1String(
"counts" ) ) )
336 QMap< int, int > classCounts;
337 const QJsonArray counts = schemaObj.value( QLatin1String(
"counts" ) ).toArray();
338 for (
const QJsonValue &count : counts )
340 const QJsonObject countObj = count.toObject();
341 classCounts.insert( countObj.value( QLatin1String(
"value" ) ).toInt(), countObj.value( QLatin1String(
"count" ) ).toInt() );
343 mAttributeClasses.insert( name, classCounts );
346 setAttributes( attributes );
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();
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()
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 );
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 );
383 return std::unique_ptr<QgsPointCloudBlock>( cached );
386 std::unique_ptr<QgsPointCloudBlock> block;
387 if ( mAccessType == Remote )
389 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
397 block = blockRequest->takeBlock();
400 QgsDebugError( QStringLiteral(
"Error downloading node %1 data, error : %2 " ).arg( n.
toString(), blockRequest->errorStr() ) );
408 QgsPointCloudExpression filterExpression = mFilterExpression;
410 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
413 if ( mDataType == QLatin1String(
"binary" ) )
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 );
418 else if ( mDataType == QLatin1String(
"zstandard" ) )
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 );
423 else if ( mDataType == QLatin1String(
"laszip" ) )
425 const QString filename = QStringLiteral(
"%1/ept-data/%2.laz" ).arg( mUrlDirectoryPart, n.
toString() );
426 block = QgsLazDecoder::decompressLaz( filename, requestAttributes, filterExpression, filterRect );
430 storeNodeDataToCache( block.get(), n, request );
439 scale(), offset(), mFilterExpression, request.
filterRect() );
442 if ( mAccessType != Remote )
445 if ( !loadNodeHierarchy( n ) )
449 if ( mDataType == QLatin1String(
"binary" ) )
451 fileUrl = QStringLiteral(
"%1/ept-data/%2.bin" ).arg( mUrlDirectoryPart, n.
toString() );
453 else if ( mDataType == QLatin1String(
"zstandard" ) )
455 fileUrl = QStringLiteral(
"%1/ept-data/%2.zst" ).arg( mUrlDirectoryPart, n.
toString() );
457 else if ( mDataType == QLatin1String(
"laszip" ) )
459 fileUrl = QStringLiteral(
"%1/ept-data/%2.laz" ).arg( mUrlDirectoryPart, n.
toString() );
469 QgsPointCloudExpression filterExpression = mFilterExpression;
471 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
477 return loadNodeHierarchy( n );
485qint64 QgsEptPointCloudIndex::pointCount()
const
494 QMutexLocker locker( &mHierarchyMutex );
495 qint64 pointCount = mHierarchy.value( nodeId, -1 );
496 if ( pointCount != -1 )
502 QVector<IndexedPointCloudNode> pathToRoot = nodePathToRoot( nodeId );
503 for (
int i = pathToRoot.size() - 1; i >= 0; --i )
505 loadSingleNodeHierarchy( pathToRoot[i] );
507 QMutexLocker locker( &mHierarchyMutex );
508 qint64 pointCount = mHierarchy.value( nodeId, -1 );
509 if ( pointCount != -1 )
516bool QgsEptPointCloudIndex::hasStatisticsMetadata()
const
518 return !mMetadataStats.isEmpty();
521QVariant QgsEptPointCloudIndex::metadataStatistic(
const QString &attribute,
Qgis::Statistic statistic )
const
523 if ( !mMetadataStats.contains( attribute ) )
526 const AttributeStatistics &stats = mMetadataStats[ attribute ];
530 return stats.count >= 0 ? QVariant( stats.count ) : QVariant();
533 return std::isnan( stats.mean ) ? QVariant() : QVariant( stats.mean );
536 return std::isnan( stats.stDev ) ? QVariant() : QVariant( stats.stDev );
539 return stats.minimum;
542 return stats.maximum;
545 return stats.minimum.isValid() && stats.maximum.isValid() ? QVariant( stats.maximum.toDouble() - stats.minimum.toDouble() ) : QVariant();
565QVariantList QgsEptPointCloudIndex::metadataClasses(
const QString &attribute )
const
567 QVariantList classes;
568 const QMap< int, int > values = mAttributeClasses.value( attribute );
569 for (
auto it = values.constBegin(); it != values.constEnd(); ++it )
576QVariant QgsEptPointCloudIndex::metadataClassStatistic(
const QString &attribute,
const QVariant &value,
Qgis::Statistic statistic )
const
581 const QMap< int, int > values = mAttributeClasses.value( attribute );
582 if ( !values.contains( value.toInt() ) )
584 return values.value( value.toInt() );
589 mHierarchyMutex.lock();
590 const bool foundInHierarchy = mHierarchy.contains( nodeId );
591 const bool foundInHierarchyNodes = mHierarchyNodes.contains( nodeId );
592 mHierarchyMutex.unlock();
594 if ( foundInHierarchy )
597 if ( !foundInHierarchyNodes )
600 const QString filePath = QStringLiteral(
"%1/ept-hierarchy/%2.json" ).arg( mUrlDirectoryPart, nodeId.
toString() );
602 QByteArray dataJsonH;
603 if ( mAccessType == Remote )
605 QNetworkRequest nr( filePath );
607 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
608 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
616 if ( reply->error() != QNetworkReply::NoError )
618 QgsDebugError( QStringLiteral(
"Request failed: " ) + filePath );
622 dataJsonH = reply->data();
626 QFile file( filePath );
627 if ( ! file.open( QIODevice::ReadOnly ) )
629 QgsDebugError( QStringLiteral(
"Loading file failed: " ) + filePath );
632 dataJsonH = file.readAll();
635 QJsonParseError errH;
636 const QJsonDocument docH = QJsonDocument::fromJson( dataJsonH, &errH );
637 if ( errH.error != QJsonParseError::NoError )
639 QgsDebugMsgLevel( QStringLiteral(
"QJsonParseError when reading hierarchy from file %1" ).arg( filePath ), 2 );
643 QMutexLocker locker( &mHierarchyMutex );
644 const QJsonObject rootHObj = docH.object();
645 for (
auto it = rootHObj.constBegin(); it != rootHObj.constEnd(); ++it )
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 );
659QVector<IndexedPointCloudNode> QgsEptPointCloudIndex::nodePathToRoot(
const IndexedPointCloudNode &nodeId )
const
661 QVector<IndexedPointCloudNode> path;
665 path.push_back( currentNode );
668 while ( currentNode.
d() >= 0 );
675 mHierarchyMutex.lock();
676 bool found = mHierarchy.contains( nodeId );
677 mHierarchyMutex.unlock();
681 QVector<IndexedPointCloudNode> pathToRoot = nodePathToRoot( nodeId );
682 for (
int i = pathToRoot.size() - 1; i >= 0 && !mHierarchy.contains( nodeId ); --i )
685 if ( !loadSingleNodeHierarchy( node ) )
689 mHierarchyMutex.lock();
690 found = mHierarchy.contains( nodeId );
691 mHierarchyMutex.unlock();
697bool QgsEptPointCloudIndex::isValid()
const
707void QgsEptPointCloudIndex::copyCommonProperties( QgsEptPointCloudIndex *destination )
const
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;
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.
IndexedPointCloudNode parentNode() const
Returns the parent of the node.
Statistic
Available generic statistics.
@ StDev
Standard deviation of values.
@ FirstQuartile
First quartile.
@ Median
Median of values.
@ Range
Range of values (max - min)
@ Minority
Minority of values.
@ CountMissing
Number of missing (null) values.
@ Majority
Majority of values.
@ Variety
Variety (count of distinct) values.
@ StDevSample
Sample standard deviation of values.
@ 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.
@ Short
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)
#define QgsDebugError(str)
#define QgsSetRequestInitiatorClass(request, _class)