QGIS API Documentation 3.41.0-Master (fda2aa46e9a)
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
30#include "qgseptdecoder.h"
31#include "qgslazdecoder.h"
35#include "qgslogger.h"
36#include "qgspointcloudexpression.h"
37
39
40#define PROVIDER_KEY QStringLiteral( "ept" )
41#define PROVIDER_DESCRIPTION QStringLiteral( "EPT point cloud provider" )
42
43QgsEptPointCloudIndex::QgsEptPointCloudIndex() = default;
44
45QgsEptPointCloudIndex::~QgsEptPointCloudIndex() = default;
46
47std::unique_ptr<QgsPointCloudIndex> QgsEptPointCloudIndex::clone() const
48{
49 QgsEptPointCloudIndex *clone = new QgsEptPointCloudIndex;
50 QMutexLocker locker( &mHierarchyMutex );
51 copyCommonProperties( clone );
52 return std::unique_ptr<QgsPointCloudIndex>( clone );
53}
54
55void QgsEptPointCloudIndex::load( const QString &fileName )
56{
57 mUri = fileName;
58 QFile f( fileName );
59 if ( !f.open( QIODevice::ReadOnly ) )
60 {
61 mError = tr( "Unable to open %1 for reading" ).arg( fileName );
62 mIsValid = false;
63 return;
64 }
65
66 const QDir directory = QFileInfo( fileName ).absoluteDir();
67 mDirectory = directory.absolutePath();
68
69 const QByteArray dataJson = f.readAll();
70 bool success = loadSchema( dataJson );
71
72 if ( success )
73 {
74 // try to import the metadata too!
75 QFile manifestFile( mDirectory + QStringLiteral( "/ept-sources/manifest.json" ) );
76 if ( manifestFile.open( QIODevice::ReadOnly ) )
77 {
78 const QByteArray manifestJson = manifestFile.readAll();
79 loadManifest( manifestJson );
80 }
81 }
82
83 if ( success )
84 {
85 success = loadHierarchy();
86 }
87
88 mIsValid = success;
89}
90
91void QgsEptPointCloudIndex::loadManifest( const QByteArray &manifestJson )
92{
93 QJsonParseError err;
94 // try to import the metadata too!
95 const QJsonDocument manifestDoc = QJsonDocument::fromJson( manifestJson, &err );
96 if ( err.error == QJsonParseError::NoError )
97 {
98 const QJsonArray manifestArray = manifestDoc.array();
99 // TODO how to handle multiple?
100 if ( ! manifestArray.empty() )
101 {
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 ) )
106 {
107 const QByteArray metadataJson = metadataFile.readAll();
108 const QJsonDocument metadataDoc = QJsonDocument::fromJson( metadataJson, &err );
109 if ( err.error == QJsonParseError::NoError )
110 {
111 const QJsonObject metadataObject = metadataDoc.object().value( QStringLiteral( "metadata" ) ).toObject();
112 if ( !metadataObject.empty() )
113 {
114 const QJsonObject sourceMetadata = metadataObject.constBegin().value().toObject();
115 mOriginalMetadata = sourceMetadata.toVariantMap();
116 }
117 }
118 }
119 }
120 }
121}
122
123bool QgsEptPointCloudIndex::loadSchema( const QByteArray &dataJson )
124{
125 QJsonParseError err;
126 const QJsonDocument doc = QJsonDocument::fromJson( dataJson, &err );
127 if ( err.error != QJsonParseError::NoError )
128 return false;
129 const QJsonObject result = doc.object();
130 mDataType = result.value( QLatin1String( "dataType" ) ).toString(); // "binary" or "laszip"
131 if ( mDataType != QLatin1String( "laszip" ) && mDataType != QLatin1String( "binary" ) && mDataType != QLatin1String( "zstandard" ) )
132 return false;
133
134 const QString hierarchyType = result.value( QLatin1String( "hierarchyType" ) ).toString(); // "json" or "gzip"
135 if ( hierarchyType != QLatin1String( "json" ) )
136 return false;
137
138 mSpan = result.value( QLatin1String( "span" ) ).toInt();
139 mPointCount = result.value( QLatin1String( "points" ) ).toDouble();
140
141 // WKT
142 const QJsonObject srs = result.value( QLatin1String( "srs" ) ).toObject();
143 mWkt = srs.value( QLatin1String( "wkt" ) ).toString();
144
145 // rectangular
146 const QJsonArray bounds = result.value( QLatin1String( "bounds" ) ).toArray();
147 if ( bounds.size() != 6 )
148 return false;
149
150 const QJsonArray boundsConforming = result.value( QLatin1String( "boundsConforming" ) ).toArray();
151 if ( boundsConforming.size() != 6 )
152 return false;
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();
157
158 const QJsonArray schemaArray = result.value( QLatin1String( "schema" ) ).toArray();
160
161 for ( const QJsonValue &schemaItem : schemaArray )
162 {
163 const QJsonObject schemaObj = schemaItem.toObject();
164 const QString name = schemaObj.value( QLatin1String( "name" ) ).toString();
165 const QString type = schemaObj.value( QLatin1String( "type" ) ).toString();
166
167 const int size = schemaObj.value( QLatin1String( "size" ) ).toInt();
168
169 if ( name == QLatin1String( "ClassFlags" ) && size == 1 )
170 {
171 attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Synthetic" ), QgsPointCloudAttribute::UChar ) );
172 attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "KeyPoint" ), QgsPointCloudAttribute::UChar ) );
173 attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Withheld" ), QgsPointCloudAttribute::UChar ) );
174 attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Overlap" ), QgsPointCloudAttribute::UChar ) );
175 }
176 else if ( type == QLatin1String( "float" ) && ( size == 4 ) )
177 {
179 }
180 else if ( type == QLatin1String( "float" ) && ( size == 8 ) )
181 {
183 }
184 else if ( size == 1 )
185 {
187 }
188 else if ( type == QLatin1String( "unsigned" ) && size == 2 )
189 {
191 }
192 else if ( size == 2 )
193 {
195 }
196 else if ( size == 4 )
197 {
199 }
200 else
201 {
202 // unknown attribute type
203 return false;
204 }
205
206 double scale = 1.f;
207 if ( schemaObj.contains( QLatin1String( "scale" ) ) )
208 scale = schemaObj.value( QLatin1String( "scale" ) ).toDouble();
209
210 double offset = 0.f;
211 if ( schemaObj.contains( QLatin1String( "offset" ) ) )
212 offset = schemaObj.value( QLatin1String( "offset" ) ).toDouble();
213
214 if ( name == QLatin1String( "X" ) )
215 {
216 mOffset.set( offset, mOffset.y(), mOffset.z() );
217 mScale.set( scale, mScale.y(), mScale.z() );
218 }
219 else if ( name == QLatin1String( "Y" ) )
220 {
221 mOffset.set( mOffset.x(), offset, mOffset.z() );
222 mScale.set( mScale.x(), scale, mScale.z() );
223 }
224 else if ( name == QLatin1String( "Z" ) )
225 {
226 mOffset.set( mOffset.x(), mOffset.y(), offset );
227 mScale.set( mScale.x(), mScale.y(), scale );
228 }
229
230 // store any metadata stats which are present for the attribute
231 AttributeStatistics stats;
232 bool foundStats = false;
233 if ( schemaObj.contains( QLatin1String( "count" ) ) )
234 {
235 stats.count = schemaObj.value( QLatin1String( "count" ) ).toInt();
236 foundStats = true;
237 }
238 if ( schemaObj.contains( QLatin1String( "minimum" ) ) )
239 {
240 stats.minimum = schemaObj.value( QLatin1String( "minimum" ) ).toDouble();
241 foundStats = true;
242 }
243 if ( schemaObj.contains( QLatin1String( "maximum" ) ) )
244 {
245 stats.maximum = schemaObj.value( QLatin1String( "maximum" ) ).toDouble();
246 foundStats = true;
247 }
248 if ( schemaObj.contains( QLatin1String( "count" ) ) )
249 {
250 stats.mean = schemaObj.value( QLatin1String( "mean" ) ).toDouble();
251 foundStats = true;
252 }
253 if ( schemaObj.contains( QLatin1String( "stddev" ) ) )
254 {
255 stats.stDev = schemaObj.value( QLatin1String( "stddev" ) ).toDouble();
256 foundStats = true;
257 }
258 if ( schemaObj.contains( QLatin1String( "variance" ) ) )
259 {
260 stats.variance = schemaObj.value( QLatin1String( "variance" ) ).toDouble();
261 foundStats = true;
262 }
263 if ( foundStats )
264 mMetadataStats.insert( name, stats );
265
266 if ( schemaObj.contains( QLatin1String( "counts" ) ) )
267 {
268 QMap< int, int > classCounts;
269 const QJsonArray counts = schemaObj.value( QLatin1String( "counts" ) ).toArray();
270 for ( const QJsonValue &count : counts )
271 {
272 const QJsonObject countObj = count.toObject();
273 classCounts.insert( countObj.value( QLatin1String( "value" ) ).toInt(), countObj.value( QLatin1String( "count" ) ).toInt() );
274 }
275 mAttributeClasses.insert( name, classCounts );
276 }
277 }
278 setAttributes( attributes );
279
280 // save mRootBounds
281
282 // bounds (cube - octree volume)
283 const double xmin = bounds[0].toDouble();
284 const double ymin = bounds[1].toDouble();
285 const double zmin = bounds[2].toDouble();
286 const double xmax = bounds[3].toDouble();
287 const double ymax = bounds[4].toDouble();
288 const double zmax = bounds[5].toDouble();
289
290 mRootBounds = QgsPointCloudDataBounds(
291 ( xmin - mOffset.x() ) / mScale.x(),
292 ( ymin - mOffset.y() ) / mScale.y(),
293 ( zmin - mOffset.z() ) / mScale.z(),
294 ( xmax - mOffset.x() ) / mScale.x(),
295 ( ymax - mOffset.y() ) / mScale.y(),
296 ( zmax - mOffset.z() ) / mScale.z()
297 );
298
299
300#ifdef QGIS_DEBUG
301 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
302 QgsDebugMsgLevel( QStringLiteral( "lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 ); // all dims should be the same
303 QgsDebugMsgLevel( QStringLiteral( "res at lvl0 %1" ).arg( dx / mSpan ), 2 );
304 QgsDebugMsgLevel( QStringLiteral( "res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
305 QgsDebugMsgLevel( QStringLiteral( "res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
306#endif
307
308 return true;
309}
310
311std::unique_ptr<QgsPointCloudBlock> QgsEptPointCloudIndex::nodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request )
312{
313 if ( QgsPointCloudBlock *cached = getNodeDataFromCache( n, request ) )
314 {
315 return std::unique_ptr<QgsPointCloudBlock>( cached );
316 }
317
318 mHierarchyMutex.lock();
319 const bool found = mHierarchy.contains( n );
320 mHierarchyMutex.unlock();
321 if ( !found )
322 return nullptr;
323
324 // we need to create a copy of the expression to pass to the decoder
325 // as the same QgsPointCloudExpression object mighgt be concurrently
326 // used on another thread, for example in a 3d view
327 QgsPointCloudExpression filterExpression = mFilterExpression;
328 QgsPointCloudAttributeCollection requestAttributes = request.attributes();
329 requestAttributes.extend( attributes(), filterExpression.referencedAttributes() );
330 QgsRectangle filterRect = request.filterRect();
331
332 std::unique_ptr<QgsPointCloudBlock> decoded;
333 if ( mDataType == QLatin1String( "binary" ) )
334 {
335 const QString filename = QStringLiteral( "%1/ept-data/%2.bin" ).arg( mDirectory, n.toString() );
336 decoded = QgsEptDecoder::decompressBinary( filename, attributes(), requestAttributes, scale(), offset(), filterExpression, filterRect );
337 }
338 else if ( mDataType == QLatin1String( "zstandard" ) )
339 {
340 const QString filename = QStringLiteral( "%1/ept-data/%2.zst" ).arg( mDirectory, n.toString() );
341 decoded = QgsEptDecoder::decompressZStandard( filename, attributes(), request.attributes(), scale(), offset(), filterExpression, filterRect );
342 }
343 else if ( mDataType == QLatin1String( "laszip" ) )
344 {
345 const QString filename = QStringLiteral( "%1/ept-data/%2.laz" ).arg( mDirectory, n.toString() );
346 decoded = QgsLazDecoder::decompressLaz( filename, requestAttributes, filterExpression, filterRect );
347 }
348
349 storeNodeDataToCache( decoded.get(), n, request );
350 return decoded;
351}
352
353QgsPointCloudBlockRequest *QgsEptPointCloudIndex::asyncNodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request )
354{
355 Q_UNUSED( n );
356 Q_UNUSED( request );
357 Q_ASSERT( false );
358 return nullptr; // unsupported
359}
360
361QgsCoordinateReferenceSystem QgsEptPointCloudIndex::crs() const
362{
364}
365
366qint64 QgsEptPointCloudIndex::pointCount() const
367{
368 return mPointCount;
369}
370
371bool QgsEptPointCloudIndex::hasStatisticsMetadata() const
372{
373 return !mMetadataStats.isEmpty();
374}
375
376QVariant QgsEptPointCloudIndex::metadataStatistic( const QString &attribute, Qgis::Statistic statistic ) const
377{
378 if ( !mMetadataStats.contains( attribute ) )
379 return QVariant();
380
381 const AttributeStatistics &stats = mMetadataStats[ attribute ];
382 switch ( statistic )
383 {
385 return stats.count >= 0 ? QVariant( stats.count ) : QVariant();
386
388 return std::isnan( stats.mean ) ? QVariant() : QVariant( stats.mean );
389
391 return std::isnan( stats.stDev ) ? QVariant() : QVariant( stats.stDev );
392
394 return stats.minimum;
395
397 return stats.maximum;
398
400 return stats.minimum.isValid() && stats.maximum.isValid() ? QVariant( stats.maximum.toDouble() - stats.minimum.toDouble() ) : QVariant();
401
415 return QVariant();
416 }
417 return QVariant();
418}
419
420QVariantList QgsEptPointCloudIndex::metadataClasses( const QString &attribute ) const
421{
422 QVariantList classes;
423 const QMap< int, int > values = mAttributeClasses.value( attribute );
424 for ( auto it = values.constBegin(); it != values.constEnd(); ++it )
425 {
426 classes << it.key();
427 }
428 return classes;
429}
430
431QVariant QgsEptPointCloudIndex::metadataClassStatistic( const QString &attribute, const QVariant &value, Qgis::Statistic statistic ) const
432{
433 if ( statistic != Qgis::Statistic::Count )
434 return QVariant();
435
436 const QMap< int, int > values = mAttributeClasses.value( attribute );
437 if ( !values.contains( value.toInt() ) )
438 return QVariant();
439 return values.value( value.toInt() );
440}
441
442bool QgsEptPointCloudIndex::loadHierarchy()
443{
444 QQueue<QString> queue;
445 queue.enqueue( QStringLiteral( "0-0-0-0" ) );
446 while ( !queue.isEmpty() )
447 {
448 const QString filename = QStringLiteral( "%1/ept-hierarchy/%2.json" ).arg( mDirectory, queue.dequeue() );
449 QFile fH( filename );
450 if ( !fH.open( QIODevice::ReadOnly ) )
451 {
452 QgsDebugMsgLevel( QStringLiteral( "unable to read hierarchy from file %1" ).arg( filename ), 2 );
453 mError = QStringLiteral( "unable to read hierarchy from file %1" ).arg( filename );
454 return false;
455 }
456
457 const QByteArray dataJsonH = fH.readAll();
458 QJsonParseError errH;
459 const QJsonDocument docH = QJsonDocument::fromJson( dataJsonH, &errH );
460 if ( errH.error != QJsonParseError::NoError )
461 {
462 QgsDebugMsgLevel( QStringLiteral( "QJsonParseError when reading hierarchy from file %1" ).arg( filename ), 2 );
463 mError = QStringLiteral( "QJsonParseError when reading hierarchy from file %1" ).arg( filename );
464 return false;
465 }
466
467 const QJsonObject rootHObj = docH.object();
468 for ( auto it = rootHObj.constBegin(); it != rootHObj.constEnd(); ++it )
469 {
470 const QString nodeIdStr = it.key();
471 const int nodePointCount = it.value().toInt();
472 if ( nodePointCount < 0 )
473 {
474 queue.enqueue( nodeIdStr );
475 }
476 else
477 {
479 mHierarchyMutex.lock();
480 mHierarchy[nodeId] = nodePointCount;
481 mHierarchyMutex.unlock();
482 }
483 }
484 }
485 return true;
486}
487
488bool QgsEptPointCloudIndex::isValid() const
489{
490 return mIsValid;
491}
492
493void QgsEptPointCloudIndex::copyCommonProperties( QgsEptPointCloudIndex *destination ) const
494{
496
497 // QgsEptPointCloudIndex specific fields
498 destination->mIsValid = mIsValid;
499 destination->mDataType = mDataType;
500 destination->mDirectory = mDirectory;
501 destination->mWkt = mWkt;
502 destination->mPointCount = mPointCount;
503 destination->mMetadataStats = mMetadataStats;
504 destination->mAttributeClasses = mAttributeClasses;
505 destination->mOriginalMetadata = mOriginalMetadata;
506}
507
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.
Statistic
Available generic statistics.
Definition qgis.h:5432
@ 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)
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.
@ UChar
Unsigned char 1 byte.
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.
QgsRectangle filterRect() const
Returns the rectangle from which points will be taken, in point cloud's crs.
A rectangle specified with double values.
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:39