24#include <QMutexLocker>
25#include <QJsonDocument>
27#include <qnamespace.h>
42#include "qgspointcloudexpression.h"
44#include "lazperf/vlr.hpp"
49#define PROVIDER_KEY QStringLiteral( "copc" )
50#define PROVIDER_DESCRIPTION QStringLiteral( "COPC point cloud provider" )
52QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() =
default;
54QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() =
default;
56std::unique_ptr<QgsAbstractPointCloudIndex> QgsCopcPointCloudIndex::clone()
const
58 QgsCopcPointCloudIndex *clone =
new QgsCopcPointCloudIndex;
59 QMutexLocker locker( &mHierarchyMutex );
60 copyCommonProperties( clone );
61 return std::unique_ptr<QgsAbstractPointCloudIndex>( clone );
64void QgsCopcPointCloudIndex::load(
const QString &urlString )
68 if ( url.isValid() && ( url.scheme() ==
"http" || url.scheme() ==
"https" ) )
73 mCopcFile.open( QgsLazDecoder::toNativePath( urlString ), std::ios::binary );
74 if ( mCopcFile.fail() )
76 mError = QObject::tr(
"Unable to open %1 for reading" ).arg( urlString );
87 mIsValid = mLazInfo->isValid();
90 mIsValid = loadSchema( *mLazInfo.get() );
98 mError = QObject::tr(
"Unable to recognize %1 as a LAZ file: \"%2\"" ).arg( urlString, mLazInfo->error() );
102bool QgsCopcPointCloudIndex::loadSchema(
QgsLazInfo &lazInfo )
104 QByteArray copcInfoVlrData = lazInfo.
vlrData( QStringLiteral(
"copc" ), 1 );
105 if ( copcInfoVlrData.isEmpty() )
107 mError = QObject::tr(
"Invalid COPC file" );
110 mCopcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() );
112 mScale = lazInfo.
scale();
113 mOffset = lazInfo.
offset();
119 mExtent.
set( minCoords.
x(), minCoords.
y(), maxCoords.
x(), maxCoords.
y() );
120 mZMin = minCoords.
z();
121 mZMax = maxCoords.
z();
125 const double xmin = mCopcInfoVlr.center_x - mCopcInfoVlr.halfsize;
126 const double ymin = mCopcInfoVlr.center_y - mCopcInfoVlr.halfsize;
127 const double zmin = mCopcInfoVlr.center_z - mCopcInfoVlr.halfsize;
128 const double xmax = mCopcInfoVlr.center_x + mCopcInfoVlr.halfsize;
129 const double ymax = mCopcInfoVlr.center_y + mCopcInfoVlr.halfsize;
130 const double zmax = mCopcInfoVlr.center_z + mCopcInfoVlr.halfsize;
132 mRootBounds =
QgsBox3D( xmin, ymin, zmin, xmax, ymax, zmax );
135 mSpan = mRootBounds.width() / mCopcInfoVlr.spacing;
138 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
139 QgsDebugMsgLevel( QStringLiteral(
"lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 );
140 QgsDebugMsgLevel( QStringLiteral(
"res at lvl0 %1" ).arg( dx / mSpan ), 2 );
141 QgsDebugMsgLevel( QStringLiteral(
"res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
142 QgsDebugMsgLevel( QStringLiteral(
"res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
152 return std::unique_ptr<QgsPointCloudBlock>( cached );
155 std::unique_ptr<QgsPointCloudBlock> block;
158 const bool found = fetchNodeHierarchy( n );
161 mHierarchyMutex.lock();
162 int pointCount = mHierarchy.value( n );
163 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
164 mHierarchyMutex.unlock();
169 QgsPointCloudExpression filterExpression = mFilterExpression;
171 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
173 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
174 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
175 file.seekg( blockOffset );
176 file.read( rawBlockData.data(), blockSize );
179 QgsDebugError( QStringLiteral(
"Could not read file %1" ).arg( mUri ) );
184 block = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
189 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
197 block = blockRequest->takeBlock();
200 QgsDebugError( QStringLiteral(
"Error downloading node %1 data, error : %2 " ).arg( n.
toString(), blockRequest->errorStr() ) );
203 storeNodeDataToCache( block.get(), n, request );
214 scale(), offset(), mFilterExpression, request.
filterRect() );
217 if ( !fetchNodeHierarchy( n ) )
219 QMutexLocker locker( &mHierarchyMutex );
224 QgsPointCloudExpression filterExpression = mFilterExpression;
226 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
227 auto [ blockOffset, blockSize ] = mHierarchyNodePos.value( n );
228 int pointCount = mHierarchy.value( n );
231 scale(), offset(), filterExpression, request.
filterRect(),
232 blockOffset, blockSize, pointCount, *mLazInfo.get() );
237 return mLazInfo->crs();
240qint64 QgsCopcPointCloudIndex::pointCount()
const
242 return mLazInfo->pointCount();
245bool QgsCopcPointCloudIndex::loadHierarchy()
const
247 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
259 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
266 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
267 if ( !statisticsEvlrData.isEmpty() )
269 QgsMessageLog::logMessage( QObject::tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
273 lazperf::evlr_header statsEvlrHeader;
274 statsEvlrHeader.user_id =
"qgis";
275 statsEvlrHeader.record_id = 0;
276 statsEvlrHeader.description =
"Contains calculated statistics";
278 statsEvlrHeader.data_length = statsJson.size();
282 std::fstream copcFile;
283 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
284 if ( copcFile.is_open() && copcFile.good() )
287 lazperf::header14 header = mLazInfo->header();
288 header.evlr_count = header.evlr_count + 1;
290 header.write( copcFile );
293 copcFile.seekg( 0, std::ios::end );
295 statsEvlrHeader.write( copcFile );
296 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
304 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
312 const QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
313 if ( statisticsEvlrData.isEmpty() )
322bool QgsCopcPointCloudIndex::isValid()
const
329 QMutexLocker locker( &mHierarchyMutex );
331 QVector<QgsPointCloudNodeId> ancestors;
333 while ( !mHierarchy.contains( foundRoot ) )
335 ancestors.push_front( foundRoot );
338 ancestors.push_front( foundRoot );
341 auto hierarchyIt = mHierarchy.constFind( n );
342 if ( hierarchyIt == mHierarchy.constEnd() )
344 int nodesCount = *hierarchyIt;
345 if ( nodesCount < 0 )
347 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
348 mHierarchyMutex.unlock();
349 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
350 mHierarchyMutex.lock();
353 return mHierarchy.contains( n );
356void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
358 Q_ASSERT( byteSize > 0 );
360 QByteArray data = readRange( offset, byteSize );
361 if ( data.isEmpty() )
364 populateHierarchy( data.constData(), byteSize );
367void QgsCopcPointCloudIndex::populateHierarchy(
const char *hierarchyPageData, uint64_t byteSize )
const
385 QMutexLocker locker( &mHierarchyMutex );
387 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
389 const CopcEntry *entry =
reinterpret_cast<const CopcEntry *
>( hierarchyPageData + i );
390 const QgsPointCloudNodeId nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
391 mHierarchy[nodeId] = entry->pointCount;
392 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
398 return fetchNodeHierarchy( n );
403 bool nodeFound = fetchNodeHierarchy(
id );
404 Q_ASSERT( nodeFound );
408 QMutexLocker locker( &mHierarchyMutex );
409 pointCount = mHierarchy.value(
id, -1 );
412 QList<QgsPointCloudNodeId> children;
413 children.reserve( 8 );
414 const int d =
id.d() + 1;
415 const int x =
id.x() * 2;
416 const int y =
id.y() * 2;
417 const int z =
id.z() * 2;
419 for (
int i = 0; i < 8; ++i )
421 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
423 bool found = fetchNodeHierarchy( n2 );
425 QMutexLocker locker( &mHierarchyMutex );
426 if ( found && mHierarchy[
id] >= 0 )
427 children.append( n2 );
435void QgsCopcPointCloudIndex::copyCommonProperties( QgsCopcPointCloudIndex *destination )
const
440 destination->mIsValid = mIsValid;
441 destination->mAccessType = mAccessType;
442 destination->mUri = mUri;
444 destination->mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
445 destination->mCopcInfoVlr = mCopcInfoVlr;
446 destination->mHierarchyNodePos = mHierarchyNodePos;
447 destination->mOriginalMetadata = mOriginalMetadata;
448 destination->mLazInfo.reset(
new QgsLazInfo( *mLazInfo ) );
451QByteArray QgsCopcPointCloudIndex::readRange( uint64_t offset, uint64_t length )
const
455 QByteArray buffer( length, Qt::Initialization::Uninitialized );
456 mCopcFile.seekg( offset );
457 mCopcFile.read( buffer.data(), length );
458 if ( mCopcFile.eof() )
459 QgsDebugError( QStringLiteral(
"Read past end of file (path %1 offset %2 length %3)" ).arg( mUri ).arg( offset ).arg( length ) );
461 QgsDebugError( QStringLiteral(
"Error reading %1" ).arg( mUri ) );
466 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
468 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
469 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
470 QByteArray queryRange = QStringLiteral(
"bytes=%1-%2" ).arg( offset ).arg( offset + length - 1 ).toLocal8Bit();
471 nr.setRawHeader(
"Range", queryRange );
479 if ( reply->error() != QNetworkReply::NoError )
481 QgsDebugError( QStringLiteral(
"Request failed: %1 (offset %1 length %2)" ).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
543 { 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.
void copyCommonProperties(QgsAbstractPointCloudIndex *destination) const
Copies common properties to the destination index.
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.
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.
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(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)
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)