QGIS API Documentation 3.41.0-Master (45a0abf3bec)
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"
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 = 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 = 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 = 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 = QgsPointCloudDataBounds(
131 ( xmin - mOffset.x() ) / mScale.x(),
132 ( ymin - mOffset.y() ) / mScale.y(),
133 ( zmin - mOffset.z() ) / mScale.z(),
134 ( xmax - mOffset.x() ) / mScale.x(),
135 ( ymax - mOffset.y() ) / mScale.y(),
136 ( zmax - mOffset.z() ) / mScale.z()
137 );
138
139 double calculatedSpan = nodeMapExtent( root() ).width() / mCopcInfoVlr.spacing;
140 mSpan = calculatedSpan;
141
142#ifdef QGIS_DEBUG
143 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
144 QgsDebugMsgLevel( QStringLiteral( "lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 ); // all dims should be the same
145 QgsDebugMsgLevel( QStringLiteral( "res at lvl0 %1" ).arg( dx / mSpan ), 2 );
146 QgsDebugMsgLevel( QStringLiteral( "res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
147 QgsDebugMsgLevel( QStringLiteral( "res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
148#endif
149
150 return true;
151}
152
153std::unique_ptr<QgsPointCloudBlock> QgsCopcPointCloudIndex::nodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request )
154{
155 if ( QgsPointCloudBlock *cached = getNodeDataFromCache( n, request ) )
156 {
157 return std::unique_ptr<QgsPointCloudBlock>( cached );
158 }
159
160 std::unique_ptr<QgsPointCloudBlock> block;
161 if ( mAccessType == Local )
162 {
163 const bool found = fetchNodeHierarchy( n );
164 if ( !found )
165 return nullptr;
166 mHierarchyMutex.lock();
167 int pointCount = mHierarchy.value( n );
168 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
169 mHierarchyMutex.unlock();
170
171 // we need to create a copy of the expression to pass to the decoder
172 // as the same QgsPointCloudExpression object mighgt be concurrently
173 // used on another thread, for example in a 3d view
174 QgsPointCloudExpression filterExpression = mFilterExpression;
175 QgsPointCloudAttributeCollection requestAttributes = request.attributes();
176 requestAttributes.extend( attributes(), filterExpression.referencedAttributes() );
177
178 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
179 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
180 file.seekg( blockOffset );
181 file.read( rawBlockData.data(), blockSize );
182 if ( !file )
183 {
184 QgsDebugError( QStringLiteral( "Could not read file %1" ).arg( mUri ) );
185 return nullptr;
186 }
187 QgsRectangle filterRect = request.filterRect();
188
189 block = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
190 }
191 else
192 {
193
194 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
195 if ( !blockRequest )
196 return nullptr;
197
198 QEventLoop loop;
199 connect( blockRequest.get(), &QgsPointCloudBlockRequest::finished, &loop, &QEventLoop::quit );
200 loop.exec();
201
202 block = blockRequest->takeBlock();
203
204 if ( !block )
205 QgsDebugError( QStringLiteral( "Error downloading node %1 data, error : %2 " ).arg( n.toString(), blockRequest->errorStr() ) );
206 }
207
208 storeNodeDataToCache( block.get(), n, request );
209 return block;
210}
211
212QgsPointCloudBlockRequest *QgsCopcPointCloudIndex::asyncNodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request )
213{
214 if ( mAccessType == Local )
215 return nullptr; // TODO
216 if ( QgsPointCloudBlock *cached = getNodeDataFromCache( n, request ) )
217 {
218 return new QgsCachedPointCloudBlockRequest( cached, n, mUri, attributes(), request.attributes(),
219 scale(), offset(), mFilterExpression, request.filterRect() );
220 }
221
222 if ( !fetchNodeHierarchy( n ) )
223 return nullptr;
224 QMutexLocker locker( &mHierarchyMutex );
225
226 // we need to create a copy of the expression to pass to the decoder
227 // as the same QgsPointCloudExpression object might be concurrently
228 // used on another thread, for example in a 3d view
229 QgsPointCloudExpression filterExpression = mFilterExpression;
230 QgsPointCloudAttributeCollection requestAttributes = request.attributes();
231 requestAttributes.extend( attributes(), filterExpression.referencedAttributes() );
232 auto [ blockOffset, blockSize ] = mHierarchyNodePos.value( n );
233 int pointCount = mHierarchy.value( n );
234
235 return new QgsCopcPointCloudBlockRequest( n, mUri, attributes(), requestAttributes,
236 scale(), offset(), filterExpression, request.filterRect(),
237 blockOffset, blockSize, pointCount, *mLazInfo.get() );
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()
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 == Remote )
259 {
260 QgsMessageLog::logMessage( 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( 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( 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 mCopcFile.close();
287 std::fstream copcFile;
288 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
289 if ( copcFile.is_open() && copcFile.good() )
290 {
291 // Write the new number of EVLRs
292 lazperf::header14 header = mLazInfo->header();
293 header.evlr_count = header.evlr_count + 1;
294 copcFile.seekp( 0 );
295 header.write( copcFile );
296
297 // Append EVLR data to the end
298 copcFile.seekg( 0, std::ios::end );
299
300 statsEvlrHeader.write( copcFile );
301 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
302 }
303 else
304 {
305 QgsMessageLog::logMessage( tr( "Couldn't open COPC file \"%1\" to write statistics" ).arg( mUri ) );
306 return false;
307 }
308 copcFile.close();
309 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
310 return true;
311}
312
313QgsPointCloudStatistics QgsCopcPointCloudIndex::readStatistics()
314{
315 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
316
317 if ( statisticsEvlrData.isEmpty() )
318 {
320 }
321
322 return QgsPointCloudStatistics::fromStatisticsJson( statisticsEvlrData );
323}
324
325bool QgsCopcPointCloudIndex::isValid() const
326{
327 return mIsValid;
328}
329
330bool QgsCopcPointCloudIndex::fetchNodeHierarchy( const IndexedPointCloudNode &n ) const
331{
332 QMutexLocker locker( &mHierarchyMutex );
333
334 QVector<IndexedPointCloudNode> ancestors;
335 IndexedPointCloudNode foundRoot = n;
336 while ( !mHierarchy.contains( foundRoot ) )
337 {
338 ancestors.push_front( foundRoot );
339 foundRoot = foundRoot.parentNode();
340 }
341 ancestors.push_front( foundRoot );
342 for ( IndexedPointCloudNode n : ancestors )
343 {
344 auto hierarchyIt = mHierarchy.constFind( n );
345 if ( hierarchyIt == mHierarchy.constEnd() )
346 return false;
347 int nodesCount = *hierarchyIt;
348 if ( nodesCount < 0 )
349 {
350 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
351 mHierarchyMutex.unlock();
352 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
353 mHierarchyMutex.lock();
354 }
355 }
356 return mHierarchy.contains( n );
357}
358
359void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize ) const
360{
361 Q_ASSERT( byteSize > 0 );
362
363 switch ( mAccessType )
364 {
365 case Local:
366 {
367 mCopcFile.seekg( offset );
368 std::unique_ptr<char []> data( new char[ byteSize ] );
369 mCopcFile.read( data.get(), byteSize );
370
371 populateHierarchy( data.get(), byteSize );
372 return;
373 }
374 case Remote:
375 {
376 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
377 QgsSetRequestInitiatorClass( nr, QStringLiteral( "QgsCopcPointCloudIndex" ) );
378 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
379 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
380 QByteArray queryRange = QStringLiteral( "bytes=%1-%2" ).arg( offset ).arg( offset + byteSize - 1 ).toLocal8Bit();
381 nr.setRawHeader( "Range", queryRange );
382
383 std::unique_ptr<QgsTileDownloadManagerReply> reply( QgsApplication::tileDownloadManager()->get( nr ) );
384
385 QEventLoop loop;
386 connect( reply.get(), &QgsTileDownloadManagerReply::finished, &loop, &QEventLoop::quit );
387 loop.exec();
388
389 if ( reply->error() != QNetworkReply::NoError )
390 {
391 QgsDebugError( QStringLiteral( "Request failed: " ) + mUri );
392 return;
393 }
394
395 QByteArray data = reply->data();
396
397 populateHierarchy( data.constData(), byteSize );
398 return;
399 }
400 }
401}
402
403void QgsCopcPointCloudIndex::populateHierarchy( const char *hierarchyPageData, uint64_t byteSize ) const
404{
405 struct CopcVoxelKey
406 {
407 int32_t level;
408 int32_t x;
409 int32_t y;
410 int32_t z;
411 };
412
413 struct CopcEntry
414 {
415 CopcVoxelKey key;
416 uint64_t offset;
417 int32_t byteSize;
418 int32_t pointCount;
419 };
420
421 QMutexLocker locker( &mHierarchyMutex );
422
423 for ( uint64_t i = 0; i < byteSize; i += sizeof( CopcEntry ) )
424 {
425 const CopcEntry *entry = reinterpret_cast<const CopcEntry *>( hierarchyPageData + i );
426 const IndexedPointCloudNode nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
427 mHierarchy[nodeId] = entry->pointCount;
428 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
429 }
430}
431
432bool QgsCopcPointCloudIndex::hasNode( const IndexedPointCloudNode &n ) const
433{
434 return fetchNodeHierarchy( n );
435}
436
437QList<IndexedPointCloudNode> QgsCopcPointCloudIndex::nodeChildren( const IndexedPointCloudNode &n ) const
438{
439 fetchNodeHierarchy( n );
440
441 mHierarchyMutex.lock();
442
443 auto hierarchyIt = mHierarchy.constFind( n );
444 Q_ASSERT( hierarchyIt != mHierarchy.constEnd() );
445 QList<IndexedPointCloudNode> lst;
446 lst.reserve( 8 );
447 const int d = n.d() + 1;
448 const int x = n.x() * 2;
449 const int y = n.y() * 2;
450 const int z = n.z() * 2;
451 mHierarchyMutex.unlock();
452
453 for ( int i = 0; i < 8; ++i )
454 {
455 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
456 const IndexedPointCloudNode n2( d, x + dx, y + dy, z + dz );
457 if ( fetchNodeHierarchy( n2 ) && mHierarchy[n] >= 0 )
458 lst.append( n2 );
459 }
460 return lst;
461}
462
463void QgsCopcPointCloudIndex::copyCommonProperties( QgsCopcPointCloudIndex *destination ) const
464{
466
467 // QgsCopcPointCloudIndex specific fields
468 destination->mIsValid = mIsValid;
469 destination->mAccessType = mAccessType;
470 destination->mUri = mUri;
471 if ( mAccessType == Local )
472 destination->mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
473 destination->mCopcInfoVlr = mCopcInfoVlr;
474 destination->mHierarchyNodePos = mHierarchyNodePos;
475 destination->mOriginalMetadata = mOriginalMetadata;
476 destination->mLazInfo.reset( new QgsLazInfo( *mLazInfo ) );
477}
478
479QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
480{
481 Q_ASSERT( mAccessType == Local ); // TODO: Remote
482 uint64_t offset = mLazInfo->firstEvlrOffset();
483 uint32_t evlrCount = mLazInfo->evlrCount();
484
485 QByteArray statisticsEvlrData;
486
487 for ( uint32_t i = 0; i < evlrCount; ++i )
488 {
489 lazperf::evlr_header header;
490 mCopcFile.seekg( offset );
491 char buffer[60];
492 mCopcFile.read( buffer, 60 );
493 header.fill( buffer, 60 );
494
495 // UserID: "qgis", record id: 0
496 if ( header.user_id == "qgis" && header.record_id == 0 )
497 {
498 statisticsEvlrData = QByteArray( header.data_length, Qt::Initialization::Uninitialized );
499 mCopcFile.read( statisticsEvlrData.data(), header.data_length );
500 break;
501 }
502
503 offset += 60 + header.data_length;
504 }
505
506 return statisticsEvlrData;
507}
508
509bool QgsCopcPointCloudIndex::gpsTimeFlag() const
510{
511 return mLazInfo.get()->header().global_encoding & 1;
512}
513
Represents a indexed point cloud node in octree.
int y() const
Returns y.
int x() const
Returns x.
QString toString() const
Encode node to string.
int d() const
Returns d.
IndexedPointCloudNode parentNode() const
Returns the parent of the node.
int z() const
Returns z.
static QgsTileDownloadManager * tileDownloadManager()
Returns the application's tile download manager, used for download of map tiles when rendering.
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 packaged data bounds.
void copyCommonProperties(QgsPointCloudIndex *destination) const
Copies common properties to the destination index.
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)