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() );
319 if ( mDataType == QLatin1String(
"binary" ) )
321 const QString filename = QStringLiteral(
"%1/ept-data/%2.bin" ).arg( mDirectory, n.
toString() );
322 return QgsEptDecoder::decompressBinary( filename, attributes(), requestAttributes, scale(), offset(), filterExpression );
324 else if ( mDataType == QLatin1String(
"zstandard" ) )
326 const QString filename = QStringLiteral(
"%1/ept-data/%2.zst" ).arg( mDirectory, n.
toString() );
327 return QgsEptDecoder::decompressZStandard( filename, attributes(), request.
attributes(), scale(), offset(), filterExpression );
329 else if ( mDataType == QLatin1String(
"laszip" ) )
331 const QString filename = QStringLiteral(
"%1/ept-data/%2.laz" ).arg( mDirectory, n.
toString() );
332 return QgsLazDecoder::decompressLaz( filename, requestAttributes, filterExpression );
353qint64 QgsEptPointCloudIndex::pointCount()
const
358bool QgsEptPointCloudIndex::hasStatisticsMetadata()
const
360 return !mMetadataStats.isEmpty();
365 if ( !mMetadataStats.contains( attribute ) )
368 const AttributeStatistics &stats = mMetadataStats[ attribute ];
372 return stats.count >= 0 ? QVariant( stats.count ) : QVariant();
375 return std::isnan( stats.mean ) ? QVariant() : QVariant( stats.mean );
378 return std::isnan( stats.stDev ) ? QVariant() : QVariant( stats.stDev );
381 return stats.minimum;
384 return stats.maximum;
387 return stats.minimum.isValid() && stats.maximum.isValid() ? QVariant( stats.maximum.toDouble() - stats.minimum.toDouble() ) : QVariant();
407QVariantList QgsEptPointCloudIndex::metadataClasses(
const QString &attribute )
const
409 QVariantList classes;
410 const QMap< int, int > values = mAttributeClasses.value( attribute );
411 for (
auto it = values.constBegin(); it != values.constEnd(); ++it )
423 const QMap< int, int > values = mAttributeClasses.value( attribute );
424 if ( !values.contains( value.toInt() ) )
426 return values.value( value.toInt() );
429bool QgsEptPointCloudIndex::loadHierarchy()
431 QQueue<QString> queue;
432 queue.enqueue( QStringLiteral(
"0-0-0-0" ) );
433 while ( !queue.isEmpty() )
435 const QString filename = QStringLiteral(
"%1/ept-hierarchy/%2.json" ).arg( mDirectory, queue.dequeue() );
436 QFile fH( filename );
437 if ( !fH.open( QIODevice::ReadOnly ) )
439 QgsDebugMsgLevel( QStringLiteral(
"unable to read hierarchy from file %1" ).arg( filename ), 2 );
440 mError = QStringLiteral(
"unable to read hierarchy from file %1" ).arg( filename );
444 const QByteArray dataJsonH = fH.readAll();
445 QJsonParseError errH;
446 const QJsonDocument docH = QJsonDocument::fromJson( dataJsonH, &errH );
447 if ( errH.error != QJsonParseError::NoError )
449 QgsDebugMsgLevel( QStringLiteral(
"QJsonParseError when reading hierarchy from file %1" ).arg( filename ), 2 );
450 mError = QStringLiteral(
"QJsonParseError when reading hierarchy from file %1" ).arg( filename );
454 const QJsonObject rootHObj = docH.object();
455 for (
auto it = rootHObj.constBegin(); it != rootHObj.constEnd(); ++it )
457 const QString nodeIdStr = it.key();
458 const int nodePointCount = it.value().toInt();
459 if ( nodePointCount < 0 )
461 queue.enqueue( nodeIdStr );
466 mHierarchyMutex.lock();
467 mHierarchy[nodeId] = nodePointCount;
468 mHierarchyMutex.unlock();
475bool QgsEptPointCloudIndex::isValid()
const
480void QgsEptPointCloudIndex::copyCommonProperties( QgsEptPointCloudIndex *destination )
const
485 destination->mIsValid = mIsValid;
486 destination->mDataType = mDataType;
487 destination->mDirectory = mDirectory;
488 destination->mWkt = mWkt;
489 destination->mPointCount = mPointCount;
490 destination->mMetadataStats = mMetadataStats;
491 destination->mAttributeClasses = mAttributeClasses;
492 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.
Base class for storing raw data from point cloud nodes.
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.
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)
const QgsCoordinateReferenceSystem & crs