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