19#include "moc_qgscopcpointcloudindex.cpp"
25#include <QMutexLocker>
26#include <QJsonDocument>
40#include "qgspointcloudexpression.h"
42#include "lazperf/vlr.hpp"
47#define PROVIDER_KEY QStringLiteral( "copc" )
48#define PROVIDER_DESCRIPTION QStringLiteral( "COPC point cloud provider" )
50QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() =
default;
52QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() =
default;
54std::unique_ptr<QgsPointCloudIndex> QgsCopcPointCloudIndex::clone()
const
56 QgsCopcPointCloudIndex *clone =
new QgsCopcPointCloudIndex;
57 QMutexLocker locker( &mHierarchyMutex );
58 copyCommonProperties( clone );
59 return std::unique_ptr<QgsPointCloudIndex>( clone );
62void QgsCopcPointCloudIndex::load(
const QString &urlString )
66 if ( url.isValid() && ( url.scheme() ==
"http" || url.scheme() ==
"https" ) )
71 mCopcFile.open( QgsLazDecoder::toNativePath( urlString ), std::ios::binary );
72 if ( mCopcFile.fail() )
74 mError = tr(
"Unable to open %1 for reading" ).arg( urlString );
81 if ( mAccessType == Remote )
85 mIsValid = mLazInfo->isValid();
88 mIsValid = loadSchema( *mLazInfo.get() );
96 mError = tr(
"Unable to recognize %1 as a LAZ file: \"%2\"" ).arg( urlString, mLazInfo->error() );
100bool QgsCopcPointCloudIndex::loadSchema(
QgsLazInfo &lazInfo )
102 QByteArray copcInfoVlrData = lazInfo.
vlrData( QStringLiteral(
"copc" ), 1 );
103 if ( copcInfoVlrData.isEmpty() )
105 mError = tr(
"Invalid COPC file" );
108 mCopcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() );
110 mScale = lazInfo.
scale();
111 mOffset = lazInfo.
offset();
117 mExtent.
set( minCoords.
x(), minCoords.
y(), maxCoords.
x(), maxCoords.
y() );
118 mZMin = minCoords.
z();
119 mZMax = maxCoords.
z();
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;
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()
139 double calculatedSpan = nodeMapExtent( root() ).width() / mCopcInfoVlr.spacing;
140 mSpan = calculatedSpan;
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 );
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 );
157 return std::unique_ptr<QgsPointCloudBlock>( cached );
160 std::unique_ptr<QgsPointCloudBlock> block;
161 if ( mAccessType == Local )
163 const bool found = fetchNodeHierarchy( n );
166 mHierarchyMutex.lock();
167 int pointCount = mHierarchy.value( n );
168 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
169 mHierarchyMutex.unlock();
174 QgsPointCloudExpression filterExpression = mFilterExpression;
176 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
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 );
184 QgsDebugError( QStringLiteral(
"Could not read file %1" ).arg( mUri ) );
189 block = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
194 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
202 block = blockRequest->takeBlock();
205 QgsDebugError( QStringLiteral(
"Error downloading node %1 data, error : %2 " ).arg( n.
toString(), blockRequest->errorStr() ) );
208 storeNodeDataToCache( block.get(), n, request );
214 if ( mAccessType == Local )
219 scale(), offset(), mFilterExpression, request.
filterRect() );
222 if ( !fetchNodeHierarchy( n ) )
224 QMutexLocker locker( &mHierarchyMutex );
229 QgsPointCloudExpression filterExpression = mFilterExpression;
231 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
232 auto [ blockOffset, blockSize ] = mHierarchyNodePos.value( n );
233 int pointCount = mHierarchy.value( n );
236 scale(), offset(), filterExpression, request.
filterRect(),
237 blockOffset, blockSize, pointCount, *mLazInfo.get() );
242 return mLazInfo->crs();
245qint64 QgsCopcPointCloudIndex::pointCount()
const
247 return mLazInfo->pointCount();
250bool QgsCopcPointCloudIndex::loadHierarchy()
252 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
258 if ( mAccessType == Remote )
264 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
271 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
272 if ( !statisticsEvlrData.isEmpty() )
274 QgsMessageLog::logMessage( tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
278 lazperf::evlr_header statsEvlrHeader;
279 statsEvlrHeader.user_id =
"qgis";
280 statsEvlrHeader.record_id = 0;
281 statsEvlrHeader.description =
"Contains calculated statistics";
283 statsEvlrHeader.data_length = statsJson.size();
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() )
292 lazperf::header14 header = mLazInfo->header();
293 header.evlr_count = header.evlr_count + 1;
295 header.write( copcFile );
298 copcFile.seekg( 0, std::ios::end );
300 statsEvlrHeader.write( copcFile );
301 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
309 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
315 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
317 if ( statisticsEvlrData.isEmpty() )
325bool QgsCopcPointCloudIndex::isValid()
const
332 QMutexLocker locker( &mHierarchyMutex );
334 QVector<IndexedPointCloudNode> ancestors;
336 while ( !mHierarchy.contains( foundRoot ) )
338 ancestors.push_front( foundRoot );
341 ancestors.push_front( foundRoot );
344 auto hierarchyIt = mHierarchy.constFind( n );
345 if ( hierarchyIt == mHierarchy.constEnd() )
347 int nodesCount = *hierarchyIt;
348 if ( nodesCount < 0 )
350 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
351 mHierarchyMutex.unlock();
352 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
353 mHierarchyMutex.lock();
356 return mHierarchy.contains( n );
359void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
361 Q_ASSERT( byteSize > 0 );
363 switch ( mAccessType )
367 mCopcFile.seekg( offset );
368 std::unique_ptr<char []> data(
new char[ byteSize ] );
369 mCopcFile.read( data.get(), byteSize );
371 populateHierarchy( data.get(), byteSize );
376 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
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 );
389 if ( reply->error() != QNetworkReply::NoError )
391 QgsDebugError( QStringLiteral(
"Request failed: " ) + mUri );
395 QByteArray data = reply->data();
397 populateHierarchy( data.constData(), byteSize );
403void QgsCopcPointCloudIndex::populateHierarchy(
const char *hierarchyPageData, uint64_t byteSize )
const
421 QMutexLocker locker( &mHierarchyMutex );
423 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
425 const CopcEntry *entry =
reinterpret_cast<const CopcEntry *
>( hierarchyPageData + i );
427 mHierarchy[nodeId] = entry->pointCount;
428 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
434 return fetchNodeHierarchy( n );
437QList<IndexedPointCloudNode> QgsCopcPointCloudIndex::nodeChildren(
const IndexedPointCloudNode &n )
const
439 fetchNodeHierarchy( n );
441 mHierarchyMutex.lock();
443 auto hierarchyIt = mHierarchy.constFind( n );
444 Q_ASSERT( hierarchyIt != mHierarchy.constEnd() );
445 QList<IndexedPointCloudNode> lst;
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();
453 for (
int i = 0; i < 8; ++i )
455 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
457 if ( fetchNodeHierarchy( n2 ) && mHierarchy[n] >= 0 )
463void QgsCopcPointCloudIndex::copyCommonProperties( QgsCopcPointCloudIndex *destination )
const
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 ) );
479QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
481 Q_ASSERT( mAccessType == Local );
482 uint64_t offset = mLazInfo->firstEvlrOffset();
483 uint32_t evlrCount = mLazInfo->evlrCount();
485 QByteArray statisticsEvlrData;
487 for ( uint32_t i = 0; i < evlrCount; ++i )
489 lazperf::evlr_header header;
490 mCopcFile.seekg( offset );
492 mCopcFile.read( buffer, 60 );
493 header.fill( buffer, 60 );
496 if ( header.user_id ==
"qgis" && header.record_id == 0 )
498 statisticsEvlrData = QByteArray( header.data_length, Qt::Initialization::Uninitialized );
499 mCopcFile.read( statisticsEvlrData.data(), header.data_length );
503 offset += 60 + header.data_length;
506 return statisticsEvlrData;
509bool QgsCopcPointCloudIndex::gpsTimeFlag()
const
511 return mLazInfo.get()->header().global_encoding & 1;
Represents a indexed point cloud node in octree.
QString toString() const
Encode node to string.
IndexedPointCloudNode parentNode() const
Returns the parent of the node.
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...
QgsVector3D maxCoords() const
Returns the maximum coordinate across X, Y and Z axis.
QgsPointCloudAttributeCollection attributes() const
Returns the list of attributes contained in the LAZ file.
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.
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.
QgsVector3D offset() const
Returns the offset of the points coordinates.
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...
double y() const
Returns Y coordinate.
double z() const
Returns Z coordinate.
double x() const
Returns X coordinate.
void set(double x, double y, double z)
Sets vector coordinates.
#define QgsDebugMsgLevel(str, level)
#define QgsDebugError(str)
#define QgsSetRequestInitiatorClass(request, _class)