QGIS API Documentation 3.41.0-Master (fda2aa46e9a)
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 "qgseptdecoder.h"
30#include "qgslazdecoder.h"
34#include "qgslogger.h"
35#include "qgsfeedback.h"
36#include "qgsmessagelog.h"
37#include "qgspointcloudexpression.h"
38
39#include "lazperf/lazperf.hpp"
40#include "lazperf/readers.hpp"
41#include "lazperf/vlr.hpp"
42
44
45#define PROVIDER_KEY QStringLiteral( "copc" )
46#define PROVIDER_DESCRIPTION QStringLiteral( "COPC point cloud provider" )
47
48QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() = default;
49
50QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() = default;
51
52std::unique_ptr<QgsPointCloudIndex> QgsCopcPointCloudIndex::clone() const
53{
54 QgsCopcPointCloudIndex *clone = new QgsCopcPointCloudIndex;
55 QMutexLocker locker( &mHierarchyMutex );
56 copyCommonProperties( clone );
57 return std::unique_ptr<QgsPointCloudIndex>( clone );
58}
59
60void QgsCopcPointCloudIndex::load( const QString &fileName )
61{
62 mUri = fileName;
63 mCopcFile.open( QgsLazDecoder::toNativePath( fileName ), std::ios::binary );
64
65 if ( !mCopcFile.is_open() || !mCopcFile.good() )
66 {
67 mError = tr( "Unable to open %1 for reading" ).arg( fileName );
68 mIsValid = false;
69 return;
70 }
71
72 mLazInfo.reset( new QgsLazInfo( QgsLazInfo::fromFile( mCopcFile ) ) );
73 mIsValid = mLazInfo->isValid();
74 if ( mIsValid )
75 {
76 mIsValid = loadSchema( *mLazInfo.get() );
77 }
78 if ( !mIsValid )
79 {
80 mError = tr( "Unable to recognize %1 as a LAZ file: \"%2\"" ).arg( fileName, mLazInfo->error() );
81 return;
82 }
83
84 loadHierarchy();
85}
86
87bool QgsCopcPointCloudIndex::loadSchema( QgsLazInfo &lazInfo )
88{
89 QByteArray copcInfoVlrData = lazInfo.vlrData( QStringLiteral( "copc" ), 1 );
90 if ( copcInfoVlrData.isEmpty() )
91 {
92 mError = tr( "Invalid COPC file" );
93 return false;
94 }
95 mCopcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() );
96
97 mScale = lazInfo.scale();
98 mOffset = lazInfo.offset();
99
100 mOriginalMetadata = lazInfo.toMetadata();
101
102 QgsVector3D minCoords = lazInfo.minCoords();
103 QgsVector3D maxCoords = lazInfo.maxCoords();
104 mExtent.set( minCoords.x(), minCoords.y(), maxCoords.x(), maxCoords.y() );
105 mZMin = minCoords.z();
106 mZMax = maxCoords.z();
107
108 setAttributes( lazInfo.attributes() );
109
110 const double xmin = mCopcInfoVlr.center_x - mCopcInfoVlr.halfsize;
111 const double ymin = mCopcInfoVlr.center_y - mCopcInfoVlr.halfsize;
112 const double zmin = mCopcInfoVlr.center_z - mCopcInfoVlr.halfsize;
113 const double xmax = mCopcInfoVlr.center_x + mCopcInfoVlr.halfsize;
114 const double ymax = mCopcInfoVlr.center_y + mCopcInfoVlr.halfsize;
115 const double zmax = mCopcInfoVlr.center_z + mCopcInfoVlr.halfsize;
116
117 mRootBounds = QgsPointCloudDataBounds(
118 ( xmin - mOffset.x() ) / mScale.x(),
119 ( ymin - mOffset.y() ) / mScale.y(),
120 ( zmin - mOffset.z() ) / mScale.z(),
121 ( xmax - mOffset.x() ) / mScale.x(),
122 ( ymax - mOffset.y() ) / mScale.y(),
123 ( zmax - mOffset.z() ) / mScale.z()
124 );
125
126 double calculatedSpan = nodeMapExtent( root() ).width() / mCopcInfoVlr.spacing;
127 mSpan = calculatedSpan;
128
129#ifdef QGIS_DEBUG
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 IndexedPointCloudNode &n, const QgsPointCloudRequest &request )
141{
142 if ( QgsPointCloudBlock *cached = getNodeDataFromCache( n, request ) )
143 {
144 return std::unique_ptr<QgsPointCloudBlock>( cached );
145 }
146
147 const bool found = fetchNodeHierarchy( n );
148 if ( !found )
149 return nullptr;
150 mHierarchyMutex.lock();
151 int pointCount = mHierarchy.value( n );
152 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
153 mHierarchyMutex.unlock();
154
155 // we need to create a copy of the expression to pass to the decoder
156 // as the same QgsPointCloudExpression object mighgt be concurrently
157 // used on another thread, for example in a 3d view
158 QgsPointCloudExpression filterExpression = mFilterExpression;
159 QgsPointCloudAttributeCollection requestAttributes = request.attributes();
160 requestAttributes.extend( attributes(), filterExpression.referencedAttributes() );
161
162 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
163 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
164 file.seekg( blockOffset );
165 file.read( rawBlockData.data(), blockSize );
166 if ( !file )
167 {
168 QgsDebugError( QStringLiteral( "Could not read file %1" ).arg( mUri ) );
169 return nullptr;
170 }
171 QgsRectangle filterRect = request.filterRect();
172
173 std::unique_ptr<QgsPointCloudBlock> decoded = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
174 storeNodeDataToCache( decoded.get(), n, request );
175 return decoded;
176}
177
178QgsPointCloudBlockRequest *QgsCopcPointCloudIndex::asyncNodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request )
179{
180 Q_UNUSED( n )
181 Q_UNUSED( request )
182 Q_ASSERT( false );
183 return nullptr; // unsupported
184}
185
186QgsCoordinateReferenceSystem QgsCopcPointCloudIndex::crs() const
187{
188 return mLazInfo->crs();
189}
190
191qint64 QgsCopcPointCloudIndex::pointCount() const
192{
193 return mLazInfo->pointCount();
194}
195
196bool QgsCopcPointCloudIndex::loadHierarchy()
197{
198 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
199 return true;
200}
201
202bool QgsCopcPointCloudIndex::writeStatistics( QgsPointCloudStatistics &stats )
203{
204 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
205 {
206 // EVLR isn't supported in the first place
207 QgsMessageLog::logMessage( tr( "Can't write statistics to \"%1\": laz version != 1.4" ).arg( mUri ) );
208 return false;
209 }
210
211 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
212 if ( !statisticsEvlrData.isEmpty() )
213 {
214 QgsMessageLog::logMessage( tr( "Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
215 return false;
216 }
217
218 lazperf::evlr_header statsEvlrHeader;
219 statsEvlrHeader.user_id = "qgis";
220 statsEvlrHeader.record_id = 0;
221 statsEvlrHeader.description = "Contains calculated statistics";
222 QByteArray statsJson = stats.toStatisticsJson();
223 statsEvlrHeader.data_length = statsJson.size();
224
225 // Save the EVLRs to the end of the original file (while erasing the existing EVLRs in the file)
226 mCopcFile.close();
227 std::fstream copcFile;
228 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
229 if ( copcFile.is_open() && copcFile.good() )
230 {
231 // Write the new number of EVLRs
232 lazperf::header14 header = mLazInfo->header();
233 header.evlr_count = header.evlr_count + 1;
234 copcFile.seekp( 0 );
235 header.write( copcFile );
236
237 // Append EVLR data to the end
238 copcFile.seekg( 0, std::ios::end );
239
240 statsEvlrHeader.write( copcFile );
241 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
242 }
243 else
244 {
245 QgsMessageLog::logMessage( tr( "Couldn't open COPC file \"%1\" to write statistics" ).arg( mUri ) );
246 return false;
247 }
248 copcFile.close();
249 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
250 return true;
251}
252
253QgsPointCloudStatistics QgsCopcPointCloudIndex::readStatistics()
254{
255 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
256
257 if ( statisticsEvlrData.isEmpty() )
258 {
260 }
261
262 return QgsPointCloudStatistics::fromStatisticsJson( statisticsEvlrData );
263}
264
265bool QgsCopcPointCloudIndex::isValid() const
266{
267 return mIsValid;
268}
269
270bool QgsCopcPointCloudIndex::fetchNodeHierarchy( const IndexedPointCloudNode &n ) const
271{
272 QMutexLocker locker( &mHierarchyMutex );
273
274 QVector<IndexedPointCloudNode> ancestors;
275 IndexedPointCloudNode foundRoot = n;
276 while ( !mHierarchy.contains( foundRoot ) )
277 {
278 ancestors.push_front( foundRoot );
279 foundRoot = foundRoot.parentNode();
280 }
281 ancestors.push_front( foundRoot );
282 for ( IndexedPointCloudNode n : ancestors )
283 {
284 auto hierarchyIt = mHierarchy.constFind( n );
285 if ( hierarchyIt == mHierarchy.constEnd() )
286 return false;
287 int nodesCount = *hierarchyIt;
288 if ( nodesCount < 0 )
289 {
290 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
291 mHierarchyMutex.unlock();
292 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
293 mHierarchyMutex.lock();
294 }
295 }
296 return mHierarchy.contains( n );
297}
298
299void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize ) const
300{
301 mCopcFile.seekg( offset );
302 std::unique_ptr<char []> data( new char[ byteSize ] );
303 mCopcFile.read( data.get(), byteSize );
304
305 populateHierarchy( data.get(), byteSize );
306}
307
308void QgsCopcPointCloudIndex::populateHierarchy( const char *hierarchyPageData, uint64_t byteSize ) const
309{
310 struct CopcVoxelKey
311 {
312 int32_t level;
313 int32_t x;
314 int32_t y;
315 int32_t z;
316 };
317
318 struct CopcEntry
319 {
320 CopcVoxelKey key;
321 uint64_t offset;
322 int32_t byteSize;
323 int32_t pointCount;
324 };
325
326 QMutexLocker locker( &mHierarchyMutex );
327
328 for ( uint64_t i = 0; i < byteSize; i += sizeof( CopcEntry ) )
329 {
330 const CopcEntry *entry = reinterpret_cast<const CopcEntry *>( hierarchyPageData + i );
331 const IndexedPointCloudNode nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
332 mHierarchy[nodeId] = entry->pointCount;
333 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
334 }
335}
336
337bool QgsCopcPointCloudIndex::hasNode( const IndexedPointCloudNode &n ) const
338{
339 return fetchNodeHierarchy( n );
340}
341
342QList<IndexedPointCloudNode> QgsCopcPointCloudIndex::nodeChildren( const IndexedPointCloudNode &n ) const
343{
344 fetchNodeHierarchy( n );
345
346 mHierarchyMutex.lock();
347
348 auto hierarchyIt = mHierarchy.constFind( n );
349 Q_ASSERT( hierarchyIt != mHierarchy.constEnd() );
350 QList<IndexedPointCloudNode> lst;
351 lst.reserve( 8 );
352 const int d = n.d() + 1;
353 const int x = n.x() * 2;
354 const int y = n.y() * 2;
355 const int z = n.z() * 2;
356 mHierarchyMutex.unlock();
357
358 for ( int i = 0; i < 8; ++i )
359 {
360 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
361 const IndexedPointCloudNode n2( d, x + dx, y + dy, z + dz );
362 if ( fetchNodeHierarchy( n2 ) && mHierarchy[n] >= 0 )
363 lst.append( n2 );
364 }
365 return lst;
366}
367
368void QgsCopcPointCloudIndex::copyCommonProperties( QgsCopcPointCloudIndex *destination ) const
369{
371
372 // QgsCopcPointCloudIndex specific fields
373 destination->mIsValid = mIsValid;
374 destination->mUri = mUri;
375 destination->mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
376 destination->mCopcInfoVlr = mCopcInfoVlr;
377 destination->mHierarchyNodePos = mHierarchyNodePos;
378 destination->mOriginalMetadata = mOriginalMetadata;
379 destination->mLazInfo.reset( new QgsLazInfo( *mLazInfo ) );
380}
381
382QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
383{
384 uint64_t offset = mLazInfo->firstEvlrOffset();
385 uint32_t evlrCount = mLazInfo->evlrCount();
386
387 QByteArray statisticsEvlrData;
388
389 for ( uint32_t i = 0; i < evlrCount; ++i )
390 {
391 lazperf::evlr_header header;
392 mCopcFile.seekg( offset );
393 char buffer[60];
394 mCopcFile.read( buffer, 60 );
395 header.fill( buffer, 60 );
396
397 // UserID: "qgis", record id: 0
398 if ( header.user_id == "qgis" && header.record_id == 0 )
399 {
400 statisticsEvlrData = QByteArray( header.data_length, Qt::Initialization::Uninitialized );
401 mCopcFile.read( statisticsEvlrData.data(), header.data_length );
402 break;
403 }
404
405 offset += 60 + header.data_length;
406 }
407
408 return statisticsEvlrData;
409}
410
Represents a indexed point cloud node in octree.
int y() const
Returns y.
int x() const
Returns x.
int d() const
Returns d.
IndexedPointCloudNode parentNode() const
Returns the parent of the node.
int z() const
Returns z.
This class represents a coordinate reference system (CRS).
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...
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.
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.
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