24#include "lazperf/vlr.hpp"
37#include "qgspointcloudexpression.h"
43#include <QJsonDocument>
45#include <QMutexLocker>
49#include <qnamespace.h>
51using namespace Qt::StringLiterals;
55#define PROVIDER_KEY u"copc"_s
56#define PROVIDER_DESCRIPTION u"COPC point cloud provider"_s
58QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() =
default;
60QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() =
default;
62void QgsCopcPointCloudIndex::load(
const QString &urlString,
const QString &authcfg )
66 if ( url.isValid() && ( url.scheme() ==
"http" || url.scheme() ==
"https" ) )
72 mUri = url.toString();
78 mCopcFile.open( QgsLazDecoder::toNativePath( urlString ), std::ios::binary );
79 if ( mCopcFile.fail() )
81 mError = QObject::tr(
"Unable to open %1 for reading" ).arg( urlString );
88 mIsValid = mLazInfo->isValid() && loadSchema( *mLazInfo.get() ) && loadHierarchy();
91 mError = QObject::tr(
"Unable to recognize %1 as a LAZ file: \"%2\"" ).arg( urlString, mLazInfo->error() );
95bool QgsCopcPointCloudIndex::loadSchema(
QgsLazInfo &lazInfo )
97 QByteArray copcInfoVlrData = lazInfo.
vlrData( u
"copc"_s, 1 );
98 if ( copcInfoVlrData.isEmpty() )
100 mError = QObject::tr(
"Invalid COPC file" );
103 mCopcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() );
105 mScale = lazInfo.
scale();
106 mOffset = lazInfo.
offset();
112 mExtent.
set( minCoords.
x(), minCoords.
y(), maxCoords.
x(), maxCoords.
y() );
113 mZMin = minCoords.
z();
114 mZMax = maxCoords.
z();
118 const double xmin = mCopcInfoVlr.center_x - mCopcInfoVlr.halfsize;
119 const double ymin = mCopcInfoVlr.center_y - mCopcInfoVlr.halfsize;
120 const double zmin = mCopcInfoVlr.center_z - mCopcInfoVlr.halfsize;
121 const double xmax = mCopcInfoVlr.center_x + mCopcInfoVlr.halfsize;
122 const double ymax = mCopcInfoVlr.center_y + mCopcInfoVlr.halfsize;
123 const double zmax = mCopcInfoVlr.center_z + mCopcInfoVlr.halfsize;
125 mRootBounds =
QgsBox3D( xmin, ymin, zmin, xmax, ymax, zmax );
128 mSpan = mRootBounds.width() / mCopcInfoVlr.spacing;
131 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
132 QgsDebugMsgLevel( u
"lvl0 node size in CRS units: %1 %2 %3"_s.arg( dx ).arg( dy ).arg( dz ), 2 );
135 QgsDebugMsgLevel( u
"res at lvl2 %1 with node size %2"_s.arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
145 return std::unique_ptr<QgsPointCloudBlock>( cached );
148 std::unique_ptr<QgsPointCloudBlock> block;
151 QByteArray rawBlockData = rawNodeData( n );
152 if ( rawBlockData.isEmpty() )
155 mHierarchyMutex.lock();
156 auto pointCount = mHierarchy.value( n );
157 mHierarchyMutex.unlock();
162 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
164 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
168 block = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
172 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
180 block = blockRequest->takeBlock();
183 QgsDebugError( u
"Error downloading node %1 data, error : %2 "_s.arg( n.
toString(), blockRequest->errorStr() ) );
186 storeNodeDataToCache( block.get(), n, request );
199 if ( !fetchNodeHierarchy( n ) )
205 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
207 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
209 mHierarchyMutex.lock();
210 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
211 int pointCount = mHierarchy.value( n );
212 mHierarchyMutex.unlock();
214 return new QgsCopcPointCloudBlockRequest( n, mUri, attributes(), requestAttributes, scale(), offset(), filterExpression, request.
filterRect(), blockOffset, blockSize, pointCount, *mLazInfo.get(), mAuthCfg );
220 const bool found = fetchNodeHierarchy( n );
223 mHierarchyMutex.lock();
224 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
225 mHierarchyMutex.unlock();
230 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
231 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
232 file.seekg( blockOffset );
233 file.read( rawBlockData.data(), blockSize );
242 return readRange( blockOffset, blockSize );
247 return mLazInfo->crs();
250qint64 QgsCopcPointCloudIndex::pointCount()
const
252 return mLazInfo->pointCount();
255bool QgsCopcPointCloudIndex::loadHierarchy()
const
257 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
269 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
276 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
277 if ( !statisticsEvlrData.isEmpty() )
279 QgsMessageLog::logMessage( QObject::tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
283 lazperf::evlr_header statsEvlrHeader;
284 statsEvlrHeader.user_id =
"qgis";
285 statsEvlrHeader.reserved = 0;
286 statsEvlrHeader.record_id = 0;
287 statsEvlrHeader.description =
"Contains calculated statistics";
289 statsEvlrHeader.data_length = statsJson.size();
292 QMutexLocker locker( &mFileMutex );
294 std::fstream copcFile;
295 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
296 if ( copcFile.is_open() && copcFile.good() )
299 lazperf::header14 header = mLazInfo->header();
300 header.evlr_count = header.evlr_count + 1;
302 header.write( copcFile );
305 copcFile.seekg( 0, std::ios::end );
307 statsEvlrHeader.write( copcFile );
308 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
316 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
324 const QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
325 if ( statisticsEvlrData.isEmpty() )
334bool QgsCopcPointCloudIndex::isValid()
const
341 QMutexLocker locker( &mHierarchyMutex );
343 QVector<QgsPointCloudNodeId> ancestors;
345 while ( !mHierarchy.contains( foundRoot ) )
347 ancestors.push_front( foundRoot );
350 ancestors.push_front( foundRoot );
353 auto hierarchyIt = mHierarchy.constFind( n );
354 if ( hierarchyIt == mHierarchy.constEnd() )
356 int nodesCount = *hierarchyIt;
357 if ( nodesCount < 0 )
359 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
360 mHierarchyMutex.unlock();
361 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
362 mHierarchyMutex.lock();
365 return mHierarchy.contains( n );
368void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
370 Q_ASSERT( byteSize > 0 );
372 QByteArray data = readRange( offset, byteSize );
373 if ( data.isEmpty() )
376 populateHierarchy( data.constData(), byteSize );
379void QgsCopcPointCloudIndex::populateHierarchy(
const char *hierarchyPageData, uint64_t byteSize )
const
397 QMutexLocker locker( &mHierarchyMutex );
399 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
401 const CopcEntry *entry =
reinterpret_cast<const CopcEntry *
>( hierarchyPageData + i );
402 const QgsPointCloudNodeId nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
403 mHierarchy[nodeId] = entry->pointCount;
404 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
410 return fetchNodeHierarchy( n );
415 bool nodeFound = fetchNodeHierarchy(
id );
416 Q_ASSERT( nodeFound );
420 QMutexLocker locker( &mHierarchyMutex );
421 pointCount = mHierarchy.value(
id, -1 );
424 QList<QgsPointCloudNodeId> children =
id.childrenNodes();
430 const bool found = fetchNodeHierarchy( c );
431 QMutexLocker locker( &mHierarchyMutex );
432 const bool shouldRemove = !found || mHierarchy[c] < 0;
443QByteArray QgsCopcPointCloudIndex::readRange( uint64_t offset, uint64_t length )
const
447 QMutexLocker locker( &mFileMutex );
449 QByteArray buffer( length, Qt::Initialization::Uninitialized );
450 mCopcFile.seekg( offset );
451 mCopcFile.read( buffer.data(), length );
452 if ( mCopcFile.eof() )
453 QgsDebugError( u
"Read past end of file (path %1 offset %2 length %3)"_s.arg( mUri ).arg( offset ).arg( length ) );
460 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
462 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
463 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
464 QByteArray queryRange = u
"bytes=%1-%2"_s.arg( offset ).arg( offset + length - 1 ).toLocal8Bit();
465 nr.setRawHeader(
"Range", queryRange );
469 QgsDebugError( u
"Network request update failed for authcfg: %1"_s.arg( mAuthCfg ) );
479 if ( reply->error() != QNetworkReply::NoError )
481 QgsDebugError( u
"Request failed: %1 (offset %1 length %2)"_s.arg( mUri ).arg( offset ).arg( length ) );
485 return reply->data();
489QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
const
491 uint64_t offset = mLazInfo->firstEvlrOffset();
492 uint32_t evlrCount = mLazInfo->evlrCount();
494 QByteArray statisticsEvlrData;
496 for ( uint32_t i = 0; i < evlrCount; ++i )
498 lazperf::evlr_header header;
500 QByteArray buffer = readRange( offset, 60 );
501 header.fill( buffer.data(), buffer.size() );
503 if ( header.user_id ==
"qgis" && header.record_id == 0 )
505 statisticsEvlrData = readRange( offset + 60, header.data_length );
509 offset += 60 + header.data_length;
512 return statisticsEvlrData;
515void QgsCopcPointCloudIndex::reset()
533 mOriginalMetadata.clear();
536 mHierarchyNodePos.clear();
539QVariantMap QgsCopcPointCloudIndex::extraMetadata()
const
542 { u
"CopcGpsTimeFlag"_s, mLazInfo.get()->header().global_encoding & 1 },
548 QMutexLocker locker( &mHierarchyMutex );
550 auto hierarchyIt = mHierarchy.constFind( n );
551 if ( hierarchyIt == mHierarchy.constEnd() )
553 const int pointsCount = *hierarchyIt;
554 if ( pointsCount < 0 )
557 const QVector<QgsPointCloudNodeId> children = n.
childrenNodes();
560 auto hierarchyIt = mHierarchy.constFind( ch );
561 if ( hierarchyIt == mHierarchy.constEnd() )
563 const int pointsCount = *hierarchyIt;
564 if ( pointsCount < 0 )
570 const QVector<QgsPointCloudNodeId> grandChildren = ch.childrenNodes();
573 auto hierarchyIt = mHierarchy.constFind( gch );
574 if ( hierarchyIt == mHierarchy.constEnd() )
576 const int pointsCount = *hierarchyIt;
577 if ( pointsCount < 0 )
@ Local
Local means the source is a local file on the machine.
@ Remote
Remote means it's loaded through a protocol like HTTP.
virtual QgsPointCloudStatistics metadataStatistics() const
Returns the object containing the statistics metadata extracted from the dataset.
static QgsTileDownloadManager * tileDownloadManager()
Returns the application's tile download manager, used for download of map tiles when rendering.
static QgsAuthManager * authManager()
Returns the application's authentication manager instance.
A 3-dimensional box composed of x, y, z coordinates.
double width() const
Returns the width of the box.
Handles a QgsPointCloudBlockRequest using existing cached QgsPointCloudBlock.
Represents a coordinate reference system (CRS).
Base class for handling loading QgsPointCloudBlock asynchronously from a remote COPC dataset.
Extracts information contained in a LAZ file, such as the public header block and variable length rec...
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...
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 QgsLazInfo fromUrl(QUrl &url, const QString &authcfg=QString())
Static function to create a QgsLazInfo class from a file over network.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
Adds a message to the log instance (and creates it if necessary).
A 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 an indexed point cloud node's position in octree.
QString toString() const
Encode node to string.
QgsPointCloudNodeId parentNode() const
Returns the parent of the node.
QVector< QgsPointCloudNodeId > childrenNodes() const
Returns the node's 8 direct child nodes.
Keeps metadata for an indexed point cloud node.
QgsBox3D bounds() const
Returns node's bounding cube in CRS coords.
Point cloud data request.
bool ignoreIndexFilterEnabled() const
Returns whether the request will ignore the point cloud index's filter expression,...
QgsPointCloudAttributeCollection attributes() const
Returns attributes.
QgsRectangle filterRect() const
Returns the rectangle from which points will be taken, in point cloud's crs.
Used to store statistics of a point cloud dataset.
static QgsPointCloudStatistics fromStatisticsJson(const 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).
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
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.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
#define QgsDebugMsgLevel(str, level)
#define QgsDebugError(str)
#define QgsSetRequestInitiatorClass(request, _class)