23#include <QJsonDocument>
37#include "qgspointcloudexpression.h"
41#define PROVIDER_KEY QStringLiteral( "ept" )
42#define PROVIDER_DESCRIPTION QStringLiteral( "EPT point cloud provider" )
44QgsEptPointCloudIndex::QgsEptPointCloudIndex() =
default;
46QgsEptPointCloudIndex::~QgsEptPointCloudIndex() =
default;
48std::unique_ptr<QgsPointCloudIndex> QgsEptPointCloudIndex::clone()
const
50 QgsEptPointCloudIndex *clone =
new QgsEptPointCloudIndex;
51 QMutexLocker locker( &mHierarchyMutex );
52 copyCommonProperties( clone );
53 return std::unique_ptr<QgsPointCloudIndex>( clone );
56void QgsEptPointCloudIndex::load(
const QString &fileName )
59 if ( !f.open( QIODevice::ReadOnly ) )
61 mError = tr(
"Unable to open %1 for reading" ).arg( fileName );
66 const QDir directory = QFileInfo( fileName ).absoluteDir();
67 mDirectory = directory.absolutePath();
69 const QByteArray dataJson = f.readAll();
70 bool success = loadSchema( dataJson );
75 QFile manifestFile( mDirectory + QStringLiteral(
"/ept-sources/manifest.json" ) );
76 if ( manifestFile.open( QIODevice::ReadOnly ) )
78 const QByteArray manifestJson = manifestFile.readAll();
79 loadManifest( manifestJson );
85 success = loadHierarchy();
91void QgsEptPointCloudIndex::loadManifest(
const QByteArray &manifestJson )
95 const QJsonDocument manifestDoc = QJsonDocument::fromJson( manifestJson, &err );
96 if ( err.error == QJsonParseError::NoError )
98 const QJsonArray manifestArray = manifestDoc.array();
100 if ( ! manifestArray.empty() )
102 const QJsonObject sourceObject = manifestArray.at( 0 ).toObject();
103 const QString metadataPath = sourceObject.value( QStringLiteral(
"metadataPath" ) ).toString();
104 QFile metadataFile( mDirectory + QStringLiteral(
"/ept-sources/" ) + metadataPath );
105 if ( metadataFile.open( QIODevice::ReadOnly ) )
107 const QByteArray metadataJson = metadataFile.readAll();
108 const QJsonDocument metadataDoc = QJsonDocument::fromJson( metadataJson, &err );
109 if ( err.error == QJsonParseError::NoError )
111 const QJsonObject metadataObject = metadataDoc.object().value( QStringLiteral(
"metadata" ) ).toObject();
112 if ( !metadataObject.empty() )
114 const QJsonObject sourceMetadata = metadataObject.constBegin().value().toObject();
115 mOriginalMetadata = sourceMetadata.toVariantMap();
123bool QgsEptPointCloudIndex::loadSchema(
const QByteArray &dataJson )
126 const QJsonDocument doc = QJsonDocument::fromJson( dataJson, &err );
127 if ( err.error != QJsonParseError::NoError )
129 const QJsonObject result = doc.object();
130 mDataType = result.value( QLatin1String(
"dataType" ) ).toString();
131 if ( mDataType != QLatin1String(
"laszip" ) && mDataType != QLatin1String(
"binary" ) && mDataType != QLatin1String(
"zstandard" ) )
134 const QString hierarchyType = result.value( QLatin1String(
"hierarchyType" ) ).toString();
135 if ( hierarchyType != QLatin1String(
"json" ) )
138 mSpan = result.value( QLatin1String(
"span" ) ).toInt();
139 mPointCount = result.value( QLatin1String(
"points" ) ).toDouble();
142 const QJsonObject srs = result.value( QLatin1String(
"srs" ) ).toObject();
143 mWkt = srs.value( QLatin1String(
"wkt" ) ).toString();
146 const QJsonArray bounds = result.value( QLatin1String(
"bounds" ) ).toArray();
147 if ( bounds.size() != 6 )
150 const QJsonArray boundsConforming = result.value( QLatin1String(
"boundsConforming" ) ).toArray();
151 if ( boundsConforming.size() != 6 )
153 mExtent.set( boundsConforming[0].toDouble(), boundsConforming[1].toDouble(),
154 boundsConforming[3].toDouble(), boundsConforming[4].toDouble() );
155 mZMin = boundsConforming[2].toDouble();
156 mZMax = boundsConforming[5].toDouble();
158 const QJsonArray schemaArray = result.value( QLatin1String(
"schema" ) ).toArray();
161 for (
const QJsonValue &schemaItem : schemaArray )
163 const QJsonObject schemaObj = schemaItem.toObject();
164 const QString name = schemaObj.value( QLatin1String(
"name" ) ).toString();
165 const QString type = schemaObj.value( QLatin1String(
"type" ) ).toString();
167 const int size = schemaObj.value( QLatin1String(
"size" ) ).toInt();
169 if ( type == QLatin1String(
"float" ) && ( size == 4 ) )
173 else if ( type == QLatin1String(
"float" ) && ( size == 8 ) )
177 else if ( size == 1 )
181 else if ( type == QLatin1String(
"unsigned" ) && size == 2 )
185 else if ( size == 2 )
189 else if ( size == 4 )
200 if ( schemaObj.contains( QLatin1String(
"scale" ) ) )
201 scale = schemaObj.value( QLatin1String(
"scale" ) ).toDouble();
204 if ( schemaObj.contains( QLatin1String(
"offset" ) ) )
205 offset = schemaObj.value( QLatin1String(
"offset" ) ).toDouble();
207 if ( name == QLatin1String(
"X" ) )
209 mOffset.set( offset, mOffset.y(), mOffset.z() );
210 mScale.set( scale, mScale.y(), mScale.z() );
212 else if ( name == QLatin1String(
"Y" ) )
214 mOffset.set( mOffset.x(), offset, mOffset.z() );
215 mScale.set( mScale.x(), scale, mScale.z() );
217 else if ( name == QLatin1String(
"Z" ) )
219 mOffset.set( mOffset.x(), mOffset.y(), offset );
220 mScale.set( mScale.x(), mScale.y(), scale );
224 AttributeStatistics stats;
225 bool foundStats =
false;
226 if ( schemaObj.contains( QLatin1String(
"count" ) ) )
228 stats.count = schemaObj.value( QLatin1String(
"count" ) ).toInt();
231 if ( schemaObj.contains( QLatin1String(
"minimum" ) ) )
233 stats.minimum = schemaObj.value( QLatin1String(
"minimum" ) ).toDouble();
236 if ( schemaObj.contains( QLatin1String(
"maximum" ) ) )
238 stats.maximum = schemaObj.value( QLatin1String(
"maximum" ) ).toDouble();
241 if ( schemaObj.contains( QLatin1String(
"count" ) ) )
243 stats.mean = schemaObj.value( QLatin1String(
"mean" ) ).toDouble();
246 if ( schemaObj.contains( QLatin1String(
"stddev" ) ) )
248 stats.stDev = schemaObj.value( QLatin1String(
"stddev" ) ).toDouble();
251 if ( schemaObj.contains( QLatin1String(
"variance" ) ) )
253 stats.variance = schemaObj.value( QLatin1String(
"variance" ) ).toDouble();
257 mMetadataStats.insert( name, stats );
259 if ( schemaObj.contains( QLatin1String(
"counts" ) ) )
261 QMap< int, int > classCounts;
262 const QJsonArray counts = schemaObj.value( QLatin1String(
"counts" ) ).toArray();
263 for (
const QJsonValue &count : counts )
265 const QJsonObject countObj = count.toObject();
266 classCounts.insert( countObj.value( QLatin1String(
"value" ) ).toInt(), countObj.value( QLatin1String(
"count" ) ).toInt() );
268 mAttributeClasses.insert( name, classCounts );
271 setAttributes( attributes );
276 const double xmin = bounds[0].toDouble();
277 const double ymin = bounds[1].toDouble();
278 const double zmin = bounds[2].toDouble();
279 const double xmax = bounds[3].toDouble();
280 const double ymax = bounds[4].toDouble();
281 const double zmax = bounds[5].toDouble();
284 ( xmin - mOffset.x() ) / mScale.x(),
285 ( ymin - mOffset.y() ) / mScale.y(),
286 ( zmin - mOffset.z() ) / mScale.z(),
287 ( xmax - mOffset.x() ) / mScale.x(),
288 ( ymax - mOffset.y() ) / mScale.y(),
289 ( zmax - mOffset.z() ) / mScale.z()
294 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
295 QgsDebugMsgLevel( QStringLiteral(
"lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 );
296 QgsDebugMsgLevel( QStringLiteral(
"res at lvl0 %1" ).arg( dx / mSpan ), 2 );
297 QgsDebugMsgLevel( QStringLiteral(
"res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
298 QgsDebugMsgLevel( QStringLiteral(
"res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
306 mHierarchyMutex.lock();
307 const bool found = mHierarchy.contains( n );
308 mHierarchyMutex.unlock();
315 QgsPointCloudExpression filterExpression = mFilterExpression;
317 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
320 if ( mDataType == QLatin1String(
"binary" ) )
322 const QString filename = QStringLiteral(
"%1/ept-data/%2.bin" ).arg( mDirectory, n.
toString() );
323 return QgsEptDecoder::decompressBinary( filename, attributes(), requestAttributes, scale(), offset(), filterExpression, filterRect );
325 else if ( mDataType == QLatin1String(
"zstandard" ) )
327 const QString filename = QStringLiteral(
"%1/ept-data/%2.zst" ).arg( mDirectory, n.
toString() );
328 return QgsEptDecoder::decompressZStandard( filename, attributes(), request.
attributes(), scale(), offset(), filterExpression, filterRect );
330 else if ( mDataType == QLatin1String(
"laszip" ) )
332 const QString filename = QStringLiteral(
"%1/ept-data/%2.laz" ).arg( mDirectory, n.
toString() );
333 return QgsLazDecoder::decompressLaz( filename, requestAttributes, filterExpression, filterRect );
354qint64 QgsEptPointCloudIndex::pointCount()
const
359bool QgsEptPointCloudIndex::hasStatisticsMetadata()
const
361 return !mMetadataStats.isEmpty();
366 if ( !mMetadataStats.contains( attribute ) )
369 const AttributeStatistics &stats = mMetadataStats[ attribute ];
373 return stats.count >= 0 ? QVariant( stats.count ) : QVariant();
376 return std::isnan( stats.mean ) ? QVariant() : QVariant( stats.mean );
379 return std::isnan( stats.stDev ) ? QVariant() : QVariant( stats.stDev );
382 return stats.minimum;
385 return stats.maximum;
388 return stats.minimum.isValid() && stats.maximum.isValid() ? QVariant( stats.maximum.toDouble() - stats.minimum.toDouble() ) : QVariant();
408QVariantList QgsEptPointCloudIndex::metadataClasses(
const QString &attribute )
const
410 QVariantList classes;
411 const QMap< int, int > values = mAttributeClasses.value( attribute );
412 for (
auto it = values.constBegin(); it != values.constEnd(); ++it )
424 const QMap< int, int > values = mAttributeClasses.value( attribute );
425 if ( !values.contains( value.toInt() ) )
427 return values.value( value.toInt() );
430bool QgsEptPointCloudIndex::loadHierarchy()
432 QQueue<QString> queue;
433 queue.enqueue( QStringLiteral(
"0-0-0-0" ) );
434 while ( !queue.isEmpty() )
436 const QString filename = QStringLiteral(
"%1/ept-hierarchy/%2.json" ).arg( mDirectory, queue.dequeue() );
437 QFile fH( filename );
438 if ( !fH.open( QIODevice::ReadOnly ) )
440 QgsDebugMsgLevel( QStringLiteral(
"unable to read hierarchy from file %1" ).arg( filename ), 2 );
441 mError = QStringLiteral(
"unable to read hierarchy from file %1" ).arg( filename );
445 const QByteArray dataJsonH = fH.readAll();
446 QJsonParseError errH;
447 const QJsonDocument docH = QJsonDocument::fromJson( dataJsonH, &errH );
448 if ( errH.error != QJsonParseError::NoError )
450 QgsDebugMsgLevel( QStringLiteral(
"QJsonParseError when reading hierarchy from file %1" ).arg( filename ), 2 );
451 mError = QStringLiteral(
"QJsonParseError when reading hierarchy from file %1" ).arg( filename );
455 const QJsonObject rootHObj = docH.object();
456 for (
auto it = rootHObj.constBegin(); it != rootHObj.constEnd(); ++it )
458 const QString nodeIdStr = it.key();
459 const int nodePointCount = it.value().toInt();
460 if ( nodePointCount < 0 )
462 queue.enqueue( nodeIdStr );
467 mHierarchyMutex.lock();
468 mHierarchy[nodeId] = nodePointCount;
469 mHierarchyMutex.unlock();
476bool QgsEptPointCloudIndex::isValid()
const
481void QgsEptPointCloudIndex::copyCommonProperties( QgsEptPointCloudIndex *destination )
const
486 destination->mIsValid = mIsValid;
487 destination->mDataType = mDataType;
488 destination->mDirectory = mDirectory;
489 destination->mWkt = mWkt;
490 destination->mPointCount = mPointCount;
491 destination->mMetadataStats = mMetadataStats;
492 destination->mAttributeClasses = mAttributeClasses;
493 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.
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.
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.
Base class for handling loading QgsPointCloudBlock asynchronously.
Represents packaged data bounds.
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.
Statistic
Enumeration of flags that specify statistics to be calculated.
@ InterQuartileRange
Inter quartile range (IQR)
@ Median
Median of values.
@ Variety
Variety (count of distinct) values.
@ First
First value (since QGIS 3.6)
@ Last
Last value (since QGIS 3.6)
@ Minority
Minority of values.
@ ThirdQuartile
Third quartile.
@ Majority
Majority of values.
@ CountMissing
Number of missing (null) values.
@ Range
Range of values (max - min)
@ StDevSample
Sample standard deviation of values.
@ FirstQuartile
First quartile.
@ StDev
Standard deviation of values.
#define QgsDebugMsgLevel(str, level)