19#include "moc_qgscopcpointcloudindex.cpp"
25#include <QMutexLocker>
26#include <QJsonDocument>
41#include "qgspointcloudexpression.h"
43#include "lazperf/vlr.hpp"
48#define PROVIDER_KEY QStringLiteral( "copc" )
49#define PROVIDER_DESCRIPTION QStringLiteral( "COPC point cloud provider" )
51QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() =
default;
53QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() =
default;
55std::unique_ptr<QgsPointCloudIndex> QgsCopcPointCloudIndex::clone()
const
57 QgsCopcPointCloudIndex *clone =
new QgsCopcPointCloudIndex;
58 QMutexLocker locker( &mHierarchyMutex );
59 copyCommonProperties( clone );
60 return std::unique_ptr<QgsPointCloudIndex>( clone );
63void QgsCopcPointCloudIndex::load(
const QString &urlString )
67 if ( url.isValid() && ( url.scheme() ==
"http" || url.scheme() ==
"https" ) )
72 mCopcFile.open( QgsLazDecoder::toNativePath( urlString ), std::ios::binary );
73 if ( mCopcFile.fail() )
75 mError = QObject::tr(
"Unable to open %1 for reading" ).arg( urlString );
82 if ( mAccessType == Remote )
86 mIsValid = mLazInfo->isValid();
89 mIsValid = loadSchema( *mLazInfo.get() );
97 mError = QObject::tr(
"Unable to recognize %1 as a LAZ file: \"%2\"" ).arg( urlString, mLazInfo->error() );
101bool QgsCopcPointCloudIndex::loadSchema(
QgsLazInfo &lazInfo )
103 QByteArray copcInfoVlrData = lazInfo.
vlrData( QStringLiteral(
"copc" ), 1 );
104 if ( copcInfoVlrData.isEmpty() )
106 mError = QObject::tr(
"Invalid COPC file" );
109 mCopcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() );
111 mScale = lazInfo.
scale();
112 mOffset = lazInfo.
offset();
118 mExtent.
set( minCoords.
x(), minCoords.
y(), maxCoords.
x(), maxCoords.
y() );
119 mZMin = minCoords.
z();
120 mZMax = maxCoords.
z();
124 const double xmin = mCopcInfoVlr.center_x - mCopcInfoVlr.halfsize;
125 const double ymin = mCopcInfoVlr.center_y - mCopcInfoVlr.halfsize;
126 const double zmin = mCopcInfoVlr.center_z - mCopcInfoVlr.halfsize;
127 const double xmax = mCopcInfoVlr.center_x + mCopcInfoVlr.halfsize;
128 const double ymax = mCopcInfoVlr.center_y + mCopcInfoVlr.halfsize;
129 const double zmax = mCopcInfoVlr.center_z + mCopcInfoVlr.halfsize;
131 mRootBounds =
QgsBox3D( xmin, ymin, zmin, xmax, ymax, zmax );
134 mSpan = mRootBounds.width() / mCopcInfoVlr.spacing;
137 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
138 QgsDebugMsgLevel( QStringLiteral(
"lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 );
139 QgsDebugMsgLevel( QStringLiteral(
"res at lvl0 %1" ).arg( dx / mSpan ), 2 );
140 QgsDebugMsgLevel( QStringLiteral(
"res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
141 QgsDebugMsgLevel( QStringLiteral(
"res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
151 return std::unique_ptr<QgsPointCloudBlock>( cached );
154 std::unique_ptr<QgsPointCloudBlock> block;
155 if ( mAccessType == Local )
157 const bool found = fetchNodeHierarchy( n );
160 mHierarchyMutex.lock();
161 int pointCount = mHierarchy.value( n );
162 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
163 mHierarchyMutex.unlock();
168 QgsPointCloudExpression filterExpression = mFilterExpression;
170 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
172 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
173 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
174 file.seekg( blockOffset );
175 file.read( rawBlockData.data(), blockSize );
178 QgsDebugError( QStringLiteral(
"Could not read file %1" ).arg( mUri ) );
183 block = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
188 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
196 block = blockRequest->takeBlock();
199 QgsDebugError( QStringLiteral(
"Error downloading node %1 data, error : %2 " ).arg( n.
toString(), blockRequest->errorStr() ) );
202 storeNodeDataToCache( block.get(), n, request );
208 if ( mAccessType == Local )
213 scale(), offset(), mFilterExpression, request.
filterRect() );
216 if ( !fetchNodeHierarchy( n ) )
218 QMutexLocker locker( &mHierarchyMutex );
223 QgsPointCloudExpression filterExpression = mFilterExpression;
225 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
226 auto [ blockOffset, blockSize ] = mHierarchyNodePos.value( n );
227 int pointCount = mHierarchy.value( n );
230 scale(), offset(), filterExpression, request.
filterRect(),
231 blockOffset, blockSize, pointCount, *mLazInfo.get() );
236 return mLazInfo->crs();
239qint64 QgsCopcPointCloudIndex::pointCount()
const
241 return mLazInfo->pointCount();
244bool QgsCopcPointCloudIndex::loadHierarchy()
246 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
252 if ( mAccessType == Remote )
258 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
265 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
266 if ( !statisticsEvlrData.isEmpty() )
268 QgsMessageLog::logMessage( QObject::tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
272 lazperf::evlr_header statsEvlrHeader;
273 statsEvlrHeader.user_id =
"qgis";
274 statsEvlrHeader.record_id = 0;
275 statsEvlrHeader.description =
"Contains calculated statistics";
277 statsEvlrHeader.data_length = statsJson.size();
281 std::fstream copcFile;
282 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
283 if ( copcFile.is_open() && copcFile.good() )
286 lazperf::header14 header = mLazInfo->header();
287 header.evlr_count = header.evlr_count + 1;
289 header.write( copcFile );
292 copcFile.seekg( 0, std::ios::end );
294 statsEvlrHeader.write( copcFile );
295 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
303 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
311 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
312 if ( statisticsEvlrData.isEmpty() )
321bool QgsCopcPointCloudIndex::isValid()
const
328 QMutexLocker locker( &mHierarchyMutex );
330 QVector<QgsPointCloudNodeId> ancestors;
332 while ( !mHierarchy.contains( foundRoot ) )
334 ancestors.push_front( foundRoot );
337 ancestors.push_front( foundRoot );
340 auto hierarchyIt = mHierarchy.constFind( n );
341 if ( hierarchyIt == mHierarchy.constEnd() )
343 int nodesCount = *hierarchyIt;
344 if ( nodesCount < 0 )
346 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
347 mHierarchyMutex.unlock();
348 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
349 mHierarchyMutex.lock();
352 return mHierarchy.contains( n );
355void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
357 Q_ASSERT( byteSize > 0 );
359 switch ( mAccessType )
363 mCopcFile.seekg( offset );
364 std::unique_ptr<char []> data(
new char[ byteSize ] );
365 mCopcFile.read( data.get(), byteSize );
367 populateHierarchy( data.get(), byteSize );
372 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
374 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
375 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
376 QByteArray queryRange = QStringLiteral(
"bytes=%1-%2" ).arg( offset ).arg( offset + byteSize - 1 ).toLocal8Bit();
377 nr.setRawHeader(
"Range", queryRange );
385 if ( reply->error() != QNetworkReply::NoError )
387 QgsDebugError( QStringLiteral(
"Request failed: " ) + mUri );
391 QByteArray data = reply->data();
393 populateHierarchy( data.constData(), byteSize );
399void QgsCopcPointCloudIndex::populateHierarchy(
const char *hierarchyPageData, uint64_t byteSize )
const
417 QMutexLocker locker( &mHierarchyMutex );
419 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
421 const CopcEntry *entry =
reinterpret_cast<const CopcEntry *
>( hierarchyPageData + i );
422 const QgsPointCloudNodeId nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
423 mHierarchy[nodeId] = entry->pointCount;
424 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
430 return fetchNodeHierarchy( n );
435 bool nodeFound = fetchNodeHierarchy(
id );
436 Q_ASSERT( nodeFound );
440 QMutexLocker locker( &mHierarchyMutex );
441 pointCount = mHierarchy.value(
id, -1 );
444 QList<QgsPointCloudNodeId> children;
445 children.reserve( 8 );
446 const int d =
id.d() + 1;
447 const int x =
id.x() * 2;
448 const int y =
id.y() * 2;
449 const int z =
id.z() * 2;
451 for (
int i = 0; i < 8; ++i )
453 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
455 bool found = fetchNodeHierarchy( n2 );
457 QMutexLocker locker( &mHierarchyMutex );
458 if ( found && mHierarchy[
id] >= 0 )
459 children.append( n2 );
467void QgsCopcPointCloudIndex::copyCommonProperties( QgsCopcPointCloudIndex *destination )
const
472 destination->mIsValid = mIsValid;
473 destination->mAccessType = mAccessType;
474 destination->mUri = mUri;
475 if ( mAccessType == Local )
476 destination->mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
477 destination->mCopcInfoVlr = mCopcInfoVlr;
478 destination->mHierarchyNodePos = mHierarchyNodePos;
479 destination->mOriginalMetadata = mOriginalMetadata;
480 destination->mLazInfo.reset(
new QgsLazInfo( *mLazInfo ) );
483QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
const
485 Q_ASSERT( mAccessType == Local );
486 uint64_t offset = mLazInfo->firstEvlrOffset();
487 uint32_t evlrCount = mLazInfo->evlrCount();
489 QByteArray statisticsEvlrData;
491 for ( uint32_t i = 0; i < evlrCount; ++i )
493 lazperf::evlr_header header;
494 mCopcFile.seekg( offset );
496 mCopcFile.read( buffer, 60 );
497 header.fill( buffer, 60 );
500 if ( header.user_id ==
"qgis" && header.record_id == 0 )
502 statisticsEvlrData = QByteArray( header.data_length, Qt::Initialization::Uninitialized );
503 mCopcFile.read( statisticsEvlrData.data(), header.data_length );
507 offset += 60 + header.data_length;
510 return statisticsEvlrData;
513bool QgsCopcPointCloudIndex::gpsTimeFlag()
const
515 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)