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