24#include <QMutexLocker>
25#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 = QObject::tr(
"Unable to open %1 for reading" ).arg( urlString );
81 if ( mAccessType == Remote )
85 mIsValid = mLazInfo->isValid();
88 mIsValid = loadSchema( *mLazInfo.get() );
96 mError = QObject::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 = QObject::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;
130 mRootBounds =
QgsBox3D( xmin, ymin, zmin, xmax, ymax, zmax );
133 mSpan = mRootBounds.width() / mCopcInfoVlr.spacing;
136 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
137 QgsDebugMsgLevel( QStringLiteral(
"lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 );
138 QgsDebugMsgLevel( QStringLiteral(
"res at lvl0 %1" ).arg( dx / mSpan ), 2 );
139 QgsDebugMsgLevel( QStringLiteral(
"res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
140 QgsDebugMsgLevel( QStringLiteral(
"res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
150 return std::unique_ptr<QgsPointCloudBlock>( cached );
153 std::unique_ptr<QgsPointCloudBlock> block;
154 if ( mAccessType == Local )
156 const bool found = fetchNodeHierarchy( n );
159 mHierarchyMutex.lock();
160 int pointCount = mHierarchy.value( n );
161 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
162 mHierarchyMutex.unlock();
167 QgsPointCloudExpression filterExpression = mFilterExpression;
169 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
171 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
172 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
173 file.seekg( blockOffset );
174 file.read( rawBlockData.data(), blockSize );
177 QgsDebugError( QStringLiteral(
"Could not read file %1" ).arg( mUri ) );
182 block = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
187 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
195 block = blockRequest->takeBlock();
198 QgsDebugError( QStringLiteral(
"Error downloading node %1 data, error : %2 " ).arg( n.
toString(), blockRequest->errorStr() ) );
201 storeNodeDataToCache( block.get(), n, request );
207 if ( mAccessType == Local )
212 scale(), offset(), mFilterExpression, request.
filterRect() );
215 if ( !fetchNodeHierarchy( n ) )
217 QMutexLocker locker( &mHierarchyMutex );
222 QgsPointCloudExpression filterExpression = mFilterExpression;
224 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
225 auto [ blockOffset, blockSize ] = mHierarchyNodePos.value( n );
226 int pointCount = mHierarchy.value( n );
229 scale(), offset(), filterExpression, request.
filterRect(),
230 blockOffset, blockSize, pointCount, *mLazInfo.get() );
235 return mLazInfo->crs();
238qint64 QgsCopcPointCloudIndex::pointCount()
const
240 return mLazInfo->pointCount();
243bool QgsCopcPointCloudIndex::loadHierarchy()
const
245 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
251 if ( mAccessType == Remote )
257 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
264 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
265 if ( !statisticsEvlrData.isEmpty() )
267 QgsMessageLog::logMessage( QObject::tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
271 lazperf::evlr_header statsEvlrHeader;
272 statsEvlrHeader.user_id =
"qgis";
273 statsEvlrHeader.record_id = 0;
274 statsEvlrHeader.description =
"Contains calculated statistics";
276 statsEvlrHeader.data_length = statsJson.size();
280 std::fstream copcFile;
281 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
282 if ( copcFile.is_open() && copcFile.good() )
285 lazperf::header14 header = mLazInfo->header();
286 header.evlr_count = header.evlr_count + 1;
288 header.write( copcFile );
291 copcFile.seekg( 0, std::ios::end );
293 statsEvlrHeader.write( copcFile );
294 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
302 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
310 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
311 if ( statisticsEvlrData.isEmpty() )
320bool QgsCopcPointCloudIndex::isValid()
const
327 QMutexLocker locker( &mHierarchyMutex );
329 QVector<QgsPointCloudNodeId> ancestors;
331 while ( !mHierarchy.contains( foundRoot ) )
333 ancestors.push_front( foundRoot );
336 ancestors.push_front( foundRoot );
339 auto hierarchyIt = mHierarchy.constFind( n );
340 if ( hierarchyIt == mHierarchy.constEnd() )
342 int nodesCount = *hierarchyIt;
343 if ( nodesCount < 0 )
345 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
346 mHierarchyMutex.unlock();
347 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
348 mHierarchyMutex.lock();
351 return mHierarchy.contains( n );
354void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
356 Q_ASSERT( byteSize > 0 );
358 switch ( mAccessType )
362 mCopcFile.seekg( offset );
363 std::unique_ptr<char []> data(
new char[ byteSize ] );
364 mCopcFile.read( data.get(), byteSize );
366 populateHierarchy( data.get(), byteSize );
371 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
373 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
374 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
375 QByteArray queryRange = QStringLiteral(
"bytes=%1-%2" ).arg( offset ).arg( offset + byteSize - 1 ).toLocal8Bit();
376 nr.setRawHeader(
"Range", queryRange );
384 if ( reply->error() != QNetworkReply::NoError )
386 QgsDebugError( QStringLiteral(
"Request failed: " ) + mUri );
390 QByteArray data = reply->data();
392 populateHierarchy( data.constData(), byteSize );
398void QgsCopcPointCloudIndex::populateHierarchy(
const char *hierarchyPageData, uint64_t byteSize )
const
416 QMutexLocker locker( &mHierarchyMutex );
418 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
420 const CopcEntry *entry =
reinterpret_cast<const CopcEntry *
>( hierarchyPageData + i );
421 const QgsPointCloudNodeId nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
422 mHierarchy[nodeId] = entry->pointCount;
423 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
429 return fetchNodeHierarchy( n );
434 bool nodeFound = fetchNodeHierarchy(
id );
435 Q_ASSERT( nodeFound );
439 QMutexLocker locker( &mHierarchyMutex );
440 pointCount = mHierarchy.value(
id, -1 );
443 QList<QgsPointCloudNodeId> children;
444 children.reserve( 8 );
445 const int d =
id.d() + 1;
446 const int x =
id.x() * 2;
447 const int y =
id.y() * 2;
448 const int z =
id.z() * 2;
450 for (
int i = 0; i < 8; ++i )
452 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
454 bool found = fetchNodeHierarchy( n2 );
456 QMutexLocker locker( &mHierarchyMutex );
457 if ( found && mHierarchy[
id] >= 0 )
458 children.append( n2 );
466void QgsCopcPointCloudIndex::copyCommonProperties( QgsCopcPointCloudIndex *destination )
const
471 destination->mIsValid = mIsValid;
472 destination->mAccessType = mAccessType;
473 destination->mUri = mUri;
474 if ( mAccessType == Local )
475 destination->mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
476 destination->mCopcInfoVlr = mCopcInfoVlr;
477 destination->mHierarchyNodePos = mHierarchyNodePos;
478 destination->mOriginalMetadata = mOriginalMetadata;
479 destination->mLazInfo.reset(
new QgsLazInfo( *mLazInfo ) );
482QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
const
484 Q_ASSERT( mAccessType == Local );
485 uint64_t offset = mLazInfo->firstEvlrOffset();
486 uint32_t evlrCount = mLazInfo->evlrCount();
488 QByteArray statisticsEvlrData;
490 for ( uint32_t i = 0; i < evlrCount; ++i )
492 lazperf::evlr_header header;
493 mCopcFile.seekg( offset );
495 mCopcFile.read( buffer, 60 );
496 header.fill( buffer, 60 );
499 if ( header.user_id ==
"qgis" && header.record_id == 0 )
501 statisticsEvlrData = QByteArray( header.data_length, Qt::Initialization::Uninitialized );
502 mCopcFile.read( statisticsEvlrData.data(), header.data_length );
506 offset += 60 + header.data_length;
509 return statisticsEvlrData;
512bool QgsCopcPointCloudIndex::gpsTimeFlag()
const
514 return mLazInfo.get()->header().global_encoding & 1;
static QgsTileDownloadManager * tileDownloadManager()
Returns the application's tile download manager, used for download of map tiles when rendering.
A 3-dimensional box composed of x, y, z coordinates.
double width() const
Returns the width of the box.
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.
void copyCommonProperties(QgsPointCloudIndex *destination) const
Copies common properties to the destination index.
virtual QgsPointCloudStatistics metadataStatistics() const
Returns the object containing the statistics metadata extracted from the dataset.
Represents a 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 indexed point cloud node.
QgsBox3D bounds() const
Returns node's bounding cube in CRS coords.
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)