23#include "lazperf/vlr.hpp"
36#include "qgspointcloudexpression.h"
42#include <QJsonDocument>
44#include <QMutexLocker>
47#include <qnamespace.h>
51#define PROVIDER_KEY QStringLiteral( "copc" )
52#define PROVIDER_DESCRIPTION QStringLiteral( "COPC point cloud provider" )
54QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() =
default;
56QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() =
default;
58void QgsCopcPointCloudIndex::load(
const QString &urlString,
const QString &authcfg )
62 if ( url.isValid() && ( url.scheme() ==
"http" || url.scheme() ==
"https" ) )
68 mUri = url.toString();
74 mCopcFile.open( QgsLazDecoder::toNativePath( urlString ), std::ios::binary );
75 if ( mCopcFile.fail() )
77 mError = QObject::tr(
"Unable to open %1 for reading" ).arg( urlString );
84 mIsValid = mLazInfo->isValid() && loadSchema( *mLazInfo.get() ) && loadHierarchy();
87 mError = QObject::tr(
"Unable to recognize %1 as a LAZ file: \"%2\"" ).arg( urlString, mLazInfo->error() );
91bool QgsCopcPointCloudIndex::loadSchema(
QgsLazInfo &lazInfo )
93 QByteArray copcInfoVlrData = lazInfo.
vlrData( QStringLiteral(
"copc" ), 1 );
94 if ( copcInfoVlrData.isEmpty() )
96 mError = QObject::tr(
"Invalid COPC file" );
99 mCopcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() );
101 mScale = lazInfo.
scale();
102 mOffset = lazInfo.
offset();
108 mExtent.
set( minCoords.
x(), minCoords.
y(), maxCoords.
x(), maxCoords.
y() );
109 mZMin = minCoords.
z();
110 mZMax = maxCoords.
z();
114 const double xmin = mCopcInfoVlr.center_x - mCopcInfoVlr.halfsize;
115 const double ymin = mCopcInfoVlr.center_y - mCopcInfoVlr.halfsize;
116 const double zmin = mCopcInfoVlr.center_z - mCopcInfoVlr.halfsize;
117 const double xmax = mCopcInfoVlr.center_x + mCopcInfoVlr.halfsize;
118 const double ymax = mCopcInfoVlr.center_y + mCopcInfoVlr.halfsize;
119 const double zmax = mCopcInfoVlr.center_z + mCopcInfoVlr.halfsize;
121 mRootBounds =
QgsBox3D( xmin, ymin, zmin, xmax, ymax, zmax );
124 mSpan = mRootBounds.width() / mCopcInfoVlr.spacing;
127 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
128 QgsDebugMsgLevel( QStringLiteral(
"lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 );
129 QgsDebugMsgLevel( QStringLiteral(
"res at lvl0 %1" ).arg( dx / mSpan ), 2 );
130 QgsDebugMsgLevel( QStringLiteral(
"res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
131 QgsDebugMsgLevel( QStringLiteral(
"res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
141 return std::unique_ptr<QgsPointCloudBlock>( cached );
144 std::unique_ptr<QgsPointCloudBlock> block;
147 QByteArray rawBlockData = rawNodeData( n );
148 if ( rawBlockData.isEmpty() )
151 mHierarchyMutex.lock();
152 auto pointCount = mHierarchy.value( n );
153 mHierarchyMutex.unlock();
158 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
160 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
164 block = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
169 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
177 block = blockRequest->takeBlock();
180 QgsDebugError( QStringLiteral(
"Error downloading node %1 data, error : %2 " ).arg( n.
toString(), blockRequest->errorStr() ) );
183 storeNodeDataToCache( block.get(), n, request );
194 scale(), offset(), mFilterExpression, request.
filterRect() );
197 if ( !fetchNodeHierarchy( n ) )
199 QMutexLocker locker( &mHierarchyMutex );
204 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
206 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
207 auto [ blockOffset, blockSize ] = mHierarchyNodePos.value( n );
208 int pointCount = mHierarchy.value( n );
211 scale(), offset(), filterExpression, request.
filterRect(),
212 blockOffset, blockSize, pointCount, *mLazInfo.get(), mAuthCfg );
218 const bool found = fetchNodeHierarchy( n );
221 mHierarchyMutex.lock();
222 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
223 mHierarchyMutex.unlock();
228 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
229 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
230 file.seekg( blockOffset );
231 file.read( rawBlockData.data(), blockSize );
234 QgsDebugError( QStringLiteral(
"Could not read file %1" ).arg( mUri ) );
240 return readRange( blockOffset, blockSize );
245 return mLazInfo->crs();
248qint64 QgsCopcPointCloudIndex::pointCount()
const
250 return mLazInfo->pointCount();
253bool QgsCopcPointCloudIndex::loadHierarchy()
const
255 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
267 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
274 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
275 if ( !statisticsEvlrData.isEmpty() )
277 QgsMessageLog::logMessage( QObject::tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
281 lazperf::evlr_header statsEvlrHeader;
282 statsEvlrHeader.user_id =
"qgis";
283 statsEvlrHeader.reserved = 0;
284 statsEvlrHeader.record_id = 0;
285 statsEvlrHeader.description =
"Contains calculated statistics";
287 statsEvlrHeader.data_length = statsJson.size();
290 QMutexLocker locker( &mFileMutex );
292 std::fstream copcFile;
293 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
294 if ( copcFile.is_open() && copcFile.good() )
297 lazperf::header14 header = mLazInfo->header();
298 header.evlr_count = header.evlr_count + 1;
300 header.write( copcFile );
303 copcFile.seekg( 0, std::ios::end );
305 statsEvlrHeader.write( copcFile );
306 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
314 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
322 const QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
323 if ( statisticsEvlrData.isEmpty() )
332bool QgsCopcPointCloudIndex::isValid()
const
339 QMutexLocker locker( &mHierarchyMutex );
341 QVector<QgsPointCloudNodeId> ancestors;
343 while ( !mHierarchy.contains( foundRoot ) )
345 ancestors.push_front( foundRoot );
348 ancestors.push_front( foundRoot );
351 auto hierarchyIt = mHierarchy.constFind( n );
352 if ( hierarchyIt == mHierarchy.constEnd() )
354 int nodesCount = *hierarchyIt;
355 if ( nodesCount < 0 )
357 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
358 mHierarchyMutex.unlock();
359 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
360 mHierarchyMutex.lock();
363 return mHierarchy.contains( n );
366void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
368 Q_ASSERT( byteSize > 0 );
370 QByteArray data = readRange( offset, byteSize );
371 if ( data.isEmpty() )
374 populateHierarchy( data.constData(), byteSize );
377void QgsCopcPointCloudIndex::populateHierarchy(
const char *hierarchyPageData, uint64_t byteSize )
const
395 QMutexLocker locker( &mHierarchyMutex );
397 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
399 const CopcEntry *entry =
reinterpret_cast<const CopcEntry *
>( hierarchyPageData + i );
400 const QgsPointCloudNodeId nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
401 mHierarchy[nodeId] = entry->pointCount;
402 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
408 return fetchNodeHierarchy( n );
413 bool nodeFound = fetchNodeHierarchy(
id );
414 Q_ASSERT( nodeFound );
418 QMutexLocker locker( &mHierarchyMutex );
419 pointCount = mHierarchy.value(
id, -1 );
422 QList<QgsPointCloudNodeId> children;
423 children.reserve( 8 );
424 const int d =
id.d() + 1;
425 const int x =
id.x() * 2;
426 const int y =
id.y() * 2;
427 const int z =
id.z() * 2;
429 for (
int i = 0; i < 8; ++i )
431 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
433 bool found = fetchNodeHierarchy( n2 );
435 QMutexLocker locker( &mHierarchyMutex );
436 if ( found && mHierarchy[
id] >= 0 )
437 children.append( n2 );
445QByteArray QgsCopcPointCloudIndex::readRange( uint64_t offset, uint64_t length )
const
449 QMutexLocker locker( &mFileMutex );
451 QByteArray buffer( length, Qt::Initialization::Uninitialized );
452 mCopcFile.seekg( offset );
453 mCopcFile.read( buffer.data(), length );
454 if ( mCopcFile.eof() )
455 QgsDebugError( QStringLiteral(
"Read past end of file (path %1 offset %2 length %3)" ).arg( mUri ).arg( offset ).arg( length ) );
457 QgsDebugError( QStringLiteral(
"Error reading %1" ).arg( mUri ) );
462 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
464 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
465 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
466 QByteArray queryRange = QStringLiteral(
"bytes=%1-%2" ).arg( offset ).arg( offset + length - 1 ).toLocal8Bit();
467 nr.setRawHeader(
"Range", queryRange );
471 QgsDebugError( QStringLiteral(
"Network request update failed for authcfg: %1" ).arg( mAuthCfg ) );
481 if ( reply->error() != QNetworkReply::NoError )
483 QgsDebugError( QStringLiteral(
"Request failed: %1 (offset %1 length %2)" ).arg( mUri ).arg( offset ).arg( length ) );
487 return reply->data();
491QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
const
493 uint64_t offset = mLazInfo->firstEvlrOffset();
494 uint32_t evlrCount = mLazInfo->evlrCount();
496 QByteArray statisticsEvlrData;
498 for ( uint32_t i = 0; i < evlrCount; ++i )
500 lazperf::evlr_header header;
502 QByteArray buffer = readRange( offset, 60 );
503 header.fill( buffer.data(), buffer.size() );
505 if ( header.user_id ==
"qgis" && header.record_id == 0 )
507 statisticsEvlrData = readRange( offset + 60, header.data_length );
511 offset += 60 + header.data_length;
514 return statisticsEvlrData;
517void QgsCopcPointCloudIndex::reset()
535 mOriginalMetadata.clear();
538 mHierarchyNodePos.clear();
541QVariantMap QgsCopcPointCloudIndex::extraMetadata()
const
545 { QStringLiteral(
"CopcGpsTimeFlag" ), mLazInfo.get()->header().global_encoding & 1 },
@ 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())
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.
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.
#define QgsDebugMsgLevel(str, level)
#define QgsDebugError(str)
#define QgsSetRequestInitiatorClass(request, _class)