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