23#include "lazperf/vlr.hpp"
36#include "qgspointcloudexpression.h"
42#include <QJsonDocument>
44#include <QMutexLocker>
48#include <qnamespace.h>
50using namespace Qt::StringLiterals;
54#define PROVIDER_KEY u"copc"_s
55#define PROVIDER_DESCRIPTION u"COPC point cloud provider"_s
57QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() =
default;
59QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() =
default;
61void QgsCopcPointCloudIndex::load(
const QString &urlString,
const QString &authcfg )
65 if ( url.isValid() && ( url.scheme() ==
"http" || url.scheme() ==
"https" ) )
71 mUri = url.toString();
77 mCopcFile.open( QgsLazDecoder::toNativePath( urlString ), std::ios::binary );
78 if ( mCopcFile.fail() )
80 mError = QObject::tr(
"Unable to open %1 for reading" ).arg( urlString );
87 mIsValid = mLazInfo->isValid() && loadSchema( *mLazInfo.get() ) && loadHierarchy();
90 mError = QObject::tr(
"Unable to recognize %1 as a LAZ file: \"%2\"" ).arg( urlString, mLazInfo->error() );
94bool QgsCopcPointCloudIndex::loadSchema(
QgsLazInfo &lazInfo )
96 QByteArray copcInfoVlrData = lazInfo.
vlrData( u
"copc"_s, 1 );
97 if ( copcInfoVlrData.isEmpty() )
99 mError = QObject::tr(
"Invalid COPC file" );
102 mCopcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() );
104 mScale = lazInfo.
scale();
105 mOffset = lazInfo.
offset();
111 mExtent.
set( minCoords.
x(), minCoords.
y(), maxCoords.
x(), maxCoords.
y() );
112 mZMin = minCoords.
z();
113 mZMax = maxCoords.
z();
117 const double xmin = mCopcInfoVlr.center_x - mCopcInfoVlr.halfsize;
118 const double ymin = mCopcInfoVlr.center_y - mCopcInfoVlr.halfsize;
119 const double zmin = mCopcInfoVlr.center_z - mCopcInfoVlr.halfsize;
120 const double xmax = mCopcInfoVlr.center_x + mCopcInfoVlr.halfsize;
121 const double ymax = mCopcInfoVlr.center_y + mCopcInfoVlr.halfsize;
122 const double zmax = mCopcInfoVlr.center_z + mCopcInfoVlr.halfsize;
124 mRootBounds =
QgsBox3D( xmin, ymin, zmin, xmax, ymax, zmax );
127 mSpan = mRootBounds.width() / mCopcInfoVlr.spacing;
130 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
131 QgsDebugMsgLevel( u
"lvl0 node size in CRS units: %1 %2 %3"_s.arg( dx ).arg( dy ).arg( dz ), 2 );
134 QgsDebugMsgLevel( u
"res at lvl2 %1 with node size %2"_s.arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
144 return std::unique_ptr<QgsPointCloudBlock>( cached );
147 std::unique_ptr<QgsPointCloudBlock> block;
150 QByteArray rawBlockData = rawNodeData( n );
151 if ( rawBlockData.isEmpty() )
154 mHierarchyMutex.lock();
155 auto pointCount = mHierarchy.value( n );
156 mHierarchyMutex.unlock();
161 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
163 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
167 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 );
197 scale(), offset(), mFilterExpression, request.
filterRect() );
200 if ( !fetchNodeHierarchy( n ) )
202 QMutexLocker locker( &mHierarchyMutex );
207 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
209 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
210 auto [ blockOffset, blockSize ] = mHierarchyNodePos.value( n );
211 int pointCount = mHierarchy.value( n );
214 scale(), offset(), filterExpression, request.
filterRect(),
215 blockOffset, blockSize, pointCount, *mLazInfo.get(), mAuthCfg );
221 const bool found = fetchNodeHierarchy( n );
224 mHierarchyMutex.lock();
225 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
226 mHierarchyMutex.unlock();
231 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
232 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
233 file.seekg( blockOffset );
234 file.read( rawBlockData.data(), blockSize );
243 return readRange( blockOffset, blockSize );
248 return mLazInfo->crs();
251qint64 QgsCopcPointCloudIndex::pointCount()
const
253 return mLazInfo->pointCount();
256bool QgsCopcPointCloudIndex::loadHierarchy()
const
258 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
270 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
277 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
278 if ( !statisticsEvlrData.isEmpty() )
280 QgsMessageLog::logMessage( QObject::tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
284 lazperf::evlr_header statsEvlrHeader;
285 statsEvlrHeader.user_id =
"qgis";
286 statsEvlrHeader.reserved = 0;
287 statsEvlrHeader.record_id = 0;
288 statsEvlrHeader.description =
"Contains calculated statistics";
290 statsEvlrHeader.data_length = statsJson.size();
293 QMutexLocker locker( &mFileMutex );
295 std::fstream copcFile;
296 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
297 if ( copcFile.is_open() && copcFile.good() )
300 lazperf::header14 header = mLazInfo->header();
301 header.evlr_count = header.evlr_count + 1;
303 header.write( copcFile );
306 copcFile.seekg( 0, std::ios::end );
308 statsEvlrHeader.write( copcFile );
309 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
317 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
325 const QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
326 if ( statisticsEvlrData.isEmpty() )
335bool QgsCopcPointCloudIndex::isValid()
const
342 QMutexLocker locker( &mHierarchyMutex );
344 QVector<QgsPointCloudNodeId> ancestors;
346 while ( !mHierarchy.contains( foundRoot ) )
348 ancestors.push_front( foundRoot );
351 ancestors.push_front( foundRoot );
354 auto hierarchyIt = mHierarchy.constFind( n );
355 if ( hierarchyIt == mHierarchy.constEnd() )
357 int nodesCount = *hierarchyIt;
358 if ( nodesCount < 0 )
360 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
361 mHierarchyMutex.unlock();
362 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
363 mHierarchyMutex.lock();
366 return mHierarchy.contains( n );
369void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
371 Q_ASSERT( byteSize > 0 );
373 QByteArray data = readRange( offset, byteSize );
374 if ( data.isEmpty() )
377 populateHierarchy( data.constData(), byteSize );
380void QgsCopcPointCloudIndex::populateHierarchy(
const char *hierarchyPageData, uint64_t byteSize )
const
398 QMutexLocker locker( &mHierarchyMutex );
400 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
402 const CopcEntry *entry =
reinterpret_cast<const CopcEntry *
>( hierarchyPageData + i );
403 const QgsPointCloudNodeId nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
404 mHierarchy[nodeId] = entry->pointCount;
405 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
411 return fetchNodeHierarchy( n );
416 bool nodeFound = fetchNodeHierarchy(
id );
417 Q_ASSERT( nodeFound );
421 QMutexLocker locker( &mHierarchyMutex );
422 pointCount = mHierarchy.value(
id, -1 );
425 QList<QgsPointCloudNodeId> children;
426 children.reserve( 8 );
427 const int d =
id.d() + 1;
428 const int x =
id.x() * 2;
429 const int y =
id.y() * 2;
430 const int z =
id.z() * 2;
432 for (
int i = 0; i < 8; ++i )
434 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
436 bool found = fetchNodeHierarchy( n2 );
438 QMutexLocker locker( &mHierarchyMutex );
439 if ( found && mHierarchy[
id] >= 0 )
440 children.append( n2 );
448QByteArray QgsCopcPointCloudIndex::readRange( uint64_t offset, uint64_t length )
const
452 QMutexLocker locker( &mFileMutex );
454 QByteArray buffer( length, Qt::Initialization::Uninitialized );
455 mCopcFile.seekg( offset );
456 mCopcFile.read( buffer.data(), length );
457 if ( mCopcFile.eof() )
458 QgsDebugError( u
"Read past end of file (path %1 offset %2 length %3)"_s.arg( mUri ).arg( offset ).arg( length ) );
465 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
467 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
468 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
469 QByteArray queryRange = u
"bytes=%1-%2"_s.arg( offset ).arg( offset + length - 1 ).toLocal8Bit();
470 nr.setRawHeader(
"Range", queryRange );
474 QgsDebugError( u
"Network request update failed for authcfg: %1"_s.arg( mAuthCfg ) );
484 if ( reply->error() != QNetworkReply::NoError )
486 QgsDebugError( u
"Request failed: %1 (offset %1 length %2)"_s.arg( mUri ).arg( offset ).arg( length ) );
490 return reply->data();
494QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
const
496 uint64_t offset = mLazInfo->firstEvlrOffset();
497 uint32_t evlrCount = mLazInfo->evlrCount();
499 QByteArray statisticsEvlrData;
501 for ( uint32_t i = 0; i < evlrCount; ++i )
503 lazperf::evlr_header header;
505 QByteArray buffer = readRange( offset, 60 );
506 header.fill( buffer.data(), buffer.size() );
508 if ( header.user_id ==
"qgis" && header.record_id == 0 )
510 statisticsEvlrData = readRange( offset + 60, header.data_length );
514 offset += 60 + header.data_length;
517 return statisticsEvlrData;
520void QgsCopcPointCloudIndex::reset()
538 mOriginalMetadata.clear();
541 mHierarchyNodePos.clear();
544QVariantMap QgsCopcPointCloudIndex::extraMetadata()
const
548 { u
"CopcGpsTimeFlag"_s, 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)