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