QGIS API Documentation 3.41.0-Master (d2aaa9c6e02)
Loading...
Searching...
No Matches
qgscopcpointcloudindex.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscopcpointcloudindex.cpp
3 --------------------
4 begin : March 2022
5 copyright : (C) 2022 by Belgacem Nedjima
6 email : belgacem dot nedjima 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
20#include <fstream>
21#include <QFile>
22#include <QtDebug>
23#include <QQueue>
24#include <QMutexLocker>
25#include <QJsonDocument>
26#include <QJsonObject>
27#include <qnamespace.h>
28
29#include "qgsapplication.h"
30#include "qgsbox3d.h"
33#include "qgseptdecoder.h"
34#include "qgslazdecoder.h"
37#include "qgspointcloudindex.h"
40#include "qgslogger.h"
41#include "qgsmessagelog.h"
42#include "qgspointcloudexpression.h"
43
44#include "lazperf/vlr.hpp"
46
48
49#define PROVIDER_KEY QStringLiteral( "copc" )
50#define PROVIDER_DESCRIPTION QStringLiteral( "COPC point cloud provider" )
51
52QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() = default;
53
54QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() = default;
55
56std::unique_ptr<QgsAbstractPointCloudIndex> QgsCopcPointCloudIndex::clone() const
57{
58 QgsCopcPointCloudIndex *clone = new QgsCopcPointCloudIndex;
59 QMutexLocker locker( &mHierarchyMutex );
60 copyCommonProperties( clone );
61 return std::unique_ptr<QgsAbstractPointCloudIndex>( clone );
62}
63
64void QgsCopcPointCloudIndex::load( const QString &urlString )
65{
66 QUrl url = urlString;
67 // Treat non-URLs as local files
68 if ( url.isValid() && ( url.scheme() == "http" || url.scheme() == "https" ) )
70 else
71 {
73 mCopcFile.open( QgsLazDecoder::toNativePath( urlString ), std::ios::binary );
74 if ( mCopcFile.fail() )
75 {
76 mError = QObject::tr( "Unable to open %1 for reading" ).arg( urlString );
77 mIsValid = false;
78 return;
79 }
80 }
81 mUri = urlString;
82
83 if ( mAccessType == Qgis::PointCloudAccessType::Remote )
84 mLazInfo.reset( new QgsLazInfo( QgsLazInfo::fromUrl( url ) ) );
85 else
86 mLazInfo.reset( new QgsLazInfo( QgsLazInfo::fromFile( mCopcFile ) ) );
87 mIsValid = mLazInfo->isValid();
88 if ( mIsValid )
89 {
90 mIsValid = loadSchema( *mLazInfo.get() );
91 if ( mIsValid )
92 {
93 loadHierarchy();
94 }
95 }
96 if ( !mIsValid )
97 {
98 mError = QObject::tr( "Unable to recognize %1 as a LAZ file: \"%2\"" ).arg( urlString, mLazInfo->error() );
99 }
100}
101
102bool QgsCopcPointCloudIndex::loadSchema( QgsLazInfo &lazInfo )
103{
104 QByteArray copcInfoVlrData = lazInfo.vlrData( QStringLiteral( "copc" ), 1 );
105 if ( copcInfoVlrData.isEmpty() )
106 {
107 mError = QObject::tr( "Invalid COPC file" );
108 return false;
109 }
110 mCopcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() );
111
112 mScale = lazInfo.scale();
113 mOffset = lazInfo.offset();
114
115 mOriginalMetadata = lazInfo.toMetadata();
116
117 QgsVector3D minCoords = lazInfo.minCoords();
118 QgsVector3D maxCoords = lazInfo.maxCoords();
119 mExtent.set( minCoords.x(), minCoords.y(), maxCoords.x(), maxCoords.y() );
120 mZMin = minCoords.z();
121 mZMax = maxCoords.z();
122
123 setAttributes( lazInfo.attributes() );
124
125 const double xmin = mCopcInfoVlr.center_x - mCopcInfoVlr.halfsize;
126 const double ymin = mCopcInfoVlr.center_y - mCopcInfoVlr.halfsize;
127 const double zmin = mCopcInfoVlr.center_z - mCopcInfoVlr.halfsize;
128 const double xmax = mCopcInfoVlr.center_x + mCopcInfoVlr.halfsize;
129 const double ymax = mCopcInfoVlr.center_y + mCopcInfoVlr.halfsize;
130 const double zmax = mCopcInfoVlr.center_z + mCopcInfoVlr.halfsize;
131
132 mRootBounds = QgsBox3D( xmin, ymin, zmin, xmax, ymax, zmax );
133
134 // TODO: Rounding?
135 mSpan = mRootBounds.width() / mCopcInfoVlr.spacing;
136
137#ifdef QGIS_DEBUG
138 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
139 QgsDebugMsgLevel( QStringLiteral( "lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 ); // all dims should be the same
140 QgsDebugMsgLevel( QStringLiteral( "res at lvl0 %1" ).arg( dx / mSpan ), 2 );
141 QgsDebugMsgLevel( QStringLiteral( "res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
142 QgsDebugMsgLevel( QStringLiteral( "res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
143#endif
144
145 return true;
146}
147
148std::unique_ptr<QgsPointCloudBlock> QgsCopcPointCloudIndex::nodeData( const QgsPointCloudNodeId &n, const QgsPointCloudRequest &request )
149{
150 if ( QgsPointCloudBlock *cached = getNodeDataFromCache( n, request ) )
151 {
152 return std::unique_ptr<QgsPointCloudBlock>( cached );
153 }
154
155 std::unique_ptr<QgsPointCloudBlock> block;
156 if ( mAccessType == Qgis::PointCloudAccessType::Local )
157 {
158 const bool found = fetchNodeHierarchy( n );
159 if ( !found )
160 return nullptr;
161 mHierarchyMutex.lock();
162 int pointCount = mHierarchy.value( n );
163 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
164 mHierarchyMutex.unlock();
165
166 // we need to create a copy of the expression to pass to the decoder
167 // as the same QgsPointCloudExpression object mighgt be concurrently
168 // used on another thread, for example in a 3d view
169 QgsPointCloudExpression filterExpression = mFilterExpression;
170 QgsPointCloudAttributeCollection requestAttributes = request.attributes();
171 requestAttributes.extend( attributes(), filterExpression.referencedAttributes() );
172
173 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
174 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
175 file.seekg( blockOffset );
176 file.read( rawBlockData.data(), blockSize );
177 if ( !file )
178 {
179 QgsDebugError( QStringLiteral( "Could not read file %1" ).arg( mUri ) );
180 return nullptr;
181 }
182 QgsRectangle filterRect = request.filterRect();
183
184 block = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
185 }
186 else
187 {
188
189 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
190 if ( !blockRequest )
191 return nullptr;
192
193 QEventLoop loop;
194 QObject::connect( blockRequest.get(), &QgsPointCloudBlockRequest::finished, &loop, &QEventLoop::quit );
195 loop.exec();
196
197 block = blockRequest->takeBlock();
198
199 if ( !block )
200 QgsDebugError( QStringLiteral( "Error downloading node %1 data, error : %2 " ).arg( n.toString(), blockRequest->errorStr() ) );
201 }
202
203 storeNodeDataToCache( block.get(), n, request );
204 return block;
205}
206
207QgsPointCloudBlockRequest *QgsCopcPointCloudIndex::asyncNodeData( const QgsPointCloudNodeId &n, const QgsPointCloudRequest &request )
208{
209 if ( mAccessType == Qgis::PointCloudAccessType::Local )
210 return nullptr; // TODO
211 if ( QgsPointCloudBlock *cached = getNodeDataFromCache( n, request ) )
212 {
213 return new QgsCachedPointCloudBlockRequest( cached, n, mUri, attributes(), request.attributes(),
214 scale(), offset(), mFilterExpression, request.filterRect() );
215 }
216
217 if ( !fetchNodeHierarchy( n ) )
218 return nullptr;
219 QMutexLocker locker( &mHierarchyMutex );
220
221 // we need to create a copy of the expression to pass to the decoder
222 // as the same QgsPointCloudExpression object might be concurrently
223 // used on another thread, for example in a 3d view
224 QgsPointCloudExpression filterExpression = mFilterExpression;
225 QgsPointCloudAttributeCollection requestAttributes = request.attributes();
226 requestAttributes.extend( attributes(), filterExpression.referencedAttributes() );
227 auto [ blockOffset, blockSize ] = mHierarchyNodePos.value( n );
228 int pointCount = mHierarchy.value( n );
229
230 return new QgsCopcPointCloudBlockRequest( n, mUri, attributes(), requestAttributes,
231 scale(), offset(), filterExpression, request.filterRect(),
232 blockOffset, blockSize, pointCount, *mLazInfo.get() );
233}
234
235QgsCoordinateReferenceSystem QgsCopcPointCloudIndex::crs() const
236{
237 return mLazInfo->crs();
238}
239
240qint64 QgsCopcPointCloudIndex::pointCount() const
241{
242 return mLazInfo->pointCount();
243}
244
245bool QgsCopcPointCloudIndex::loadHierarchy() const
246{
247 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
248 return true;
249}
250
251bool QgsCopcPointCloudIndex::writeStatistics( QgsPointCloudStatistics &stats )
252{
253 if ( mAccessType == Qgis::PointCloudAccessType::Remote )
254 {
255 QgsMessageLog::logMessage( QObject::tr( "Can't write statistics to remote file \"%1\"" ).arg( mUri ) );
256 return false;
257 }
258
259 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
260 {
261 // EVLR isn't supported in the first place
262 QgsMessageLog::logMessage( QObject::tr( "Can't write statistics to \"%1\": laz version != 1.4" ).arg( mUri ) );
263 return false;
264 }
265
266 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
267 if ( !statisticsEvlrData.isEmpty() )
268 {
269 QgsMessageLog::logMessage( QObject::tr( "Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
270 return false;
271 }
272
273 lazperf::evlr_header statsEvlrHeader;
274 statsEvlrHeader.user_id = "qgis";
275 statsEvlrHeader.record_id = 0;
276 statsEvlrHeader.description = "Contains calculated statistics";
277 QByteArray statsJson = stats.toStatisticsJson();
278 statsEvlrHeader.data_length = statsJson.size();
279
280 // Save the EVLRs to the end of the original file (while erasing the existing EVLRs in the file)
281 mCopcFile.close();
282 std::fstream copcFile;
283 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
284 if ( copcFile.is_open() && copcFile.good() )
285 {
286 // Write the new number of EVLRs
287 lazperf::header14 header = mLazInfo->header();
288 header.evlr_count = header.evlr_count + 1;
289 copcFile.seekp( 0 );
290 header.write( copcFile );
291
292 // Append EVLR data to the end
293 copcFile.seekg( 0, std::ios::end );
294
295 statsEvlrHeader.write( copcFile );
296 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
297 }
298 else
299 {
300 QgsMessageLog::logMessage( QObject::tr( "Couldn't open COPC file \"%1\" to write statistics" ).arg( mUri ) );
301 return false;
302 }
303 copcFile.close();
304 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
305 return true;
306}
307
308QgsPointCloudStatistics QgsCopcPointCloudIndex::metadataStatistics() const
309{
310 if ( ! mStatistics )
311 {
312 const QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
313 if ( statisticsEvlrData.isEmpty() )
315 else
316 mStatistics = QgsPointCloudStatistics::fromStatisticsJson( statisticsEvlrData );
317 }
318
319 return *mStatistics;
320}
321
322bool QgsCopcPointCloudIndex::isValid() const
323{
324 return mIsValid;
325}
326
327bool QgsCopcPointCloudIndex::fetchNodeHierarchy( const QgsPointCloudNodeId &n ) const
328{
329 QMutexLocker locker( &mHierarchyMutex );
330
331 QVector<QgsPointCloudNodeId> ancestors;
332 QgsPointCloudNodeId foundRoot = n;
333 while ( !mHierarchy.contains( foundRoot ) )
334 {
335 ancestors.push_front( foundRoot );
336 foundRoot = foundRoot.parentNode();
337 }
338 ancestors.push_front( foundRoot );
339 for ( QgsPointCloudNodeId n : ancestors )
340 {
341 auto hierarchyIt = mHierarchy.constFind( n );
342 if ( hierarchyIt == mHierarchy.constEnd() )
343 return false;
344 int nodesCount = *hierarchyIt;
345 if ( nodesCount < 0 )
346 {
347 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
348 mHierarchyMutex.unlock();
349 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
350 mHierarchyMutex.lock();
351 }
352 }
353 return mHierarchy.contains( n );
354}
355
356void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize ) const
357{
358 Q_ASSERT( byteSize > 0 );
359
360 QByteArray data = readRange( offset, byteSize );
361 if ( data.isEmpty() )
362 return;
363
364 populateHierarchy( data.constData(), byteSize );
365}
366
367void QgsCopcPointCloudIndex::populateHierarchy( const char *hierarchyPageData, uint64_t byteSize ) const
368{
369 struct CopcVoxelKey
370 {
371 int32_t level;
372 int32_t x;
373 int32_t y;
374 int32_t z;
375 };
376
377 struct CopcEntry
378 {
379 CopcVoxelKey key;
380 uint64_t offset;
381 int32_t byteSize;
382 int32_t pointCount;
383 };
384
385 QMutexLocker locker( &mHierarchyMutex );
386
387 for ( uint64_t i = 0; i < byteSize; i += sizeof( CopcEntry ) )
388 {
389 const CopcEntry *entry = reinterpret_cast<const CopcEntry *>( hierarchyPageData + i );
390 const QgsPointCloudNodeId nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
391 mHierarchy[nodeId] = entry->pointCount;
392 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
393 }
394}
395
396bool QgsCopcPointCloudIndex::hasNode( const QgsPointCloudNodeId &n ) const
397{
398 return fetchNodeHierarchy( n );
399}
400
401QgsPointCloudNode QgsCopcPointCloudIndex::getNode( const QgsPointCloudNodeId &id ) const
402{
403 bool nodeFound = fetchNodeHierarchy( id );
404 Q_ASSERT( nodeFound );
405
406 qint64 pointCount;
407 {
408 QMutexLocker locker( &mHierarchyMutex );
409 pointCount = mHierarchy.value( id, -1 );
410 }
411
412 QList<QgsPointCloudNodeId> children;
413 children.reserve( 8 );
414 const int d = id.d() + 1;
415 const int x = id.x() * 2;
416 const int y = id.y() * 2;
417 const int z = id.z() * 2;
418
419 for ( int i = 0; i < 8; ++i )
420 {
421 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
422 const QgsPointCloudNodeId n2( d, x + dx, y + dy, z + dz );
423 bool found = fetchNodeHierarchy( n2 );
424 {
425 QMutexLocker locker( &mHierarchyMutex );
426 if ( found && mHierarchy[id] >= 0 )
427 children.append( n2 );
428 }
429 }
430
431 QgsBox3D bounds = QgsPointCloudNode::bounds( mRootBounds, id );
432 return QgsPointCloudNode( id, pointCount, children, bounds.width() / mSpan, bounds );
433}
434
435void QgsCopcPointCloudIndex::copyCommonProperties( QgsCopcPointCloudIndex *destination ) const
436{
438
439 // QgsCopcPointCloudIndex specific fields
440 destination->mIsValid = mIsValid;
441 destination->mAccessType = mAccessType;
442 destination->mUri = mUri;
443 if ( mAccessType == Qgis::PointCloudAccessType::Local )
444 destination->mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
445 destination->mCopcInfoVlr = mCopcInfoVlr;
446 destination->mHierarchyNodePos = mHierarchyNodePos;
447 destination->mOriginalMetadata = mOriginalMetadata;
448 destination->mLazInfo.reset( new QgsLazInfo( *mLazInfo ) );
449}
450
451QByteArray QgsCopcPointCloudIndex::readRange( uint64_t offset, uint64_t length ) const
452{
453 if ( mAccessType == Qgis::PointCloudAccessType::Local )
454 {
455 QByteArray buffer( length, Qt::Initialization::Uninitialized );
456 mCopcFile.seekg( offset );
457 mCopcFile.read( buffer.data(), length );
458 if ( mCopcFile.eof() )
459 QgsDebugError( QStringLiteral( "Read past end of file (path %1 offset %2 length %3)" ).arg( mUri ).arg( offset ).arg( length ) );
460 if ( !mCopcFile )
461 QgsDebugError( QStringLiteral( "Error reading %1" ).arg( mUri ) );
462 return buffer;
463 }
464 else
465 {
466 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
467 QgsSetRequestInitiatorClass( nr, QStringLiteral( "QgsCopcPointCloudIndex" ) );
468 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
469 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
470 QByteArray queryRange = QStringLiteral( "bytes=%1-%2" ).arg( offset ).arg( offset + length - 1 ).toLocal8Bit();
471 nr.setRawHeader( "Range", queryRange );
472
473 std::unique_ptr<QgsTileDownloadManagerReply> reply( QgsApplication::tileDownloadManager()->get( nr ) );
474
475 QEventLoop loop;
476 QObject::connect( reply.get(), &QgsTileDownloadManagerReply::finished, &loop, &QEventLoop::quit );
477 loop.exec();
478
479 if ( reply->error() != QNetworkReply::NoError )
480 {
481 QgsDebugError( QStringLiteral( "Request failed: %1 (offset %1 length %2)" ).arg( mUri ).arg( offset ).arg( length ) );
482 return {};
483 }
484
485 return reply->data();
486 }
487}
488
489QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData() const
490{
491 uint64_t offset = mLazInfo->firstEvlrOffset();
492 uint32_t evlrCount = mLazInfo->evlrCount();
493
494 QByteArray statisticsEvlrData;
495
496 for ( uint32_t i = 0; i < evlrCount; ++i )
497 {
498 lazperf::evlr_header header;
499
500 QByteArray buffer = readRange( offset, 60 );
501 header.fill( buffer.data(), buffer.size() );
502
503 if ( header.user_id == "qgis" && header.record_id == 0 )
504 {
505 statisticsEvlrData = readRange( offset + 60, header.data_length );
506 break;
507 }
508
509 offset += 60 + header.data_length;
510 }
511
512 return statisticsEvlrData;
513}
514
515void QgsCopcPointCloudIndex::reset()
516{
517 // QgsAbstractPointCloudIndex
518 mExtent = QgsRectangle();
519 mZMin = 0;
520 mZMax = 0;
521 mHierarchy.clear();
522 mScale = QgsVector3D();
523 mOffset = QgsVector3D();
524 mRootBounds = QgsBox3D();
525 mAttributes = QgsPointCloudAttributeCollection();
526 mSpan = 0;
527 mError.clear();
528
529 // QgsCopcPointCloudIndex
530 mIsValid = false;
532 mCopcFile.close();
533 mOriginalMetadata.clear();
534 mStatistics.reset();
535 mLazInfo.reset();
536 mHierarchyNodePos.clear();
537}
538
539QVariantMap QgsCopcPointCloudIndex::extraMetadata() const
540{
541 return
542 {
543 { QStringLiteral( "CopcGpsTimeFlag" ), mLazInfo.get()->header().global_encoding & 1 },
544 };
545}
546
@ Local
Local means the source is a local file on the machine.
@ Remote
Remote means it's loaded through a protocol like HTTP.
void copyCommonProperties(QgsAbstractPointCloudIndex *destination) const
Copies common properties to the destination index.
virtual QgsPointCloudStatistics metadataStatistics() const
Returns the object containing the statistics metadata extracted from the dataset.
static QgsTileDownloadManager * tileDownloadManager()
Returns the application's tile download manager, used for download of map tiles when rendering.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:43
double width() const
Returns the width of the box.
Definition qgsbox3d.h:293
Class for handling a QgsPointCloudBlockRequest using existing cached QgsPointCloudBlock.
This class represents a coordinate reference system (CRS).
Base class for handling loading QgsPointCloudBlock asynchronously from a remote COPC dataset.
Class for extracting information contained in LAZ file such as the public header block and variable l...
Definition qgslazinfo.h:39
QgsVector3D maxCoords() const
Returns the maximum coordinate across X, Y and Z axis.
Definition qgslazinfo.h:95
QgsPointCloudAttributeCollection attributes() const
Returns the list of attributes contained in the LAZ file.
Definition qgslazinfo.h:120
QByteArray vlrData(QString userId, int recordId)
Returns the binary data of the variable length record with the user identifier userId and record iden...
static QgsLazInfo fromUrl(QUrl &url)
Static function to create a QgsLazInfo class from a file over network.
QVariantMap toMetadata() const
Returns a map containing various metadata extracted from the LAZ file.
QgsVector3D scale() const
Returns the scale of the points coordinates.
Definition qgslazinfo.h:77
static QgsLazInfo fromFile(std::ifstream &file)
Static function to create a QgsLazInfo class from a file.
QgsVector3D minCoords() const
Returns the minimum coordinate across X, Y and Z axis.
Definition qgslazinfo.h:93
QgsVector3D offset() const
Returns the offset of the points coordinates.
Definition qgslazinfo.h:79
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Collection of point cloud attributes.
void extend(const QgsPointCloudAttributeCollection &otherCollection, const QSet< QString > &matchingNames)
Adds specific missing attributes from another QgsPointCloudAttributeCollection.
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 a indexed point cloud node's position in octree.
QString toString() const
Encode node to string.
QgsPointCloudNodeId parentNode() const
Returns the parent of the node.
Keeps metadata for indexed point cloud node.
QgsBox3D bounds() const
Returns node's bounding cube in CRS coords.
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.
Class used to store statistics of a point cloud dataset.
static QgsPointCloudStatistics fromStatisticsJson(const QByteArray &stats)
Creates a statistics object from the JSON object stats.
QByteArray toStatisticsJson() const
Converts the current statistics object into JSON object.
A rectangle specified with double values.
void finished()
Emitted when the reply has finished (either with a success or with a failure)
Class for storage of 3D vectors similar to QVector3D, with the difference that it uses double precisi...
Definition qgsvector3d.h:31
double y() const
Returns Y coordinate.
Definition qgsvector3d.h:50
double z() const
Returns Z coordinate.
Definition qgsvector3d.h:52
double x() const
Returns X coordinate.
Definition qgsvector3d.h:48
void set(double x, double y, double z)
Sets vector coordinates.
Definition qgsvector3d.h:73
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:39
#define QgsDebugError(str)
Definition qgslogger.h:38
#define QgsSetRequestInitiatorClass(request, _class)