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;
56void QgsCopcPointCloudIndex::load(
const QString &urlString )
60 if ( url.isValid() && ( url.scheme() ==
"http" || url.scheme() ==
"https" ) )
65 mCopcFile.open( QgsLazDecoder::toNativePath( urlString ), std::ios::binary );
66 if ( mCopcFile.fail() )
68 mError = QObject::tr(
"Unable to open %1 for reading" ).arg( urlString );
79 mIsValid = mLazInfo->isValid();
82 mIsValid = loadSchema( *mLazInfo.get() );
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( QStringLiteral(
"copc" ), 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( QStringLiteral(
"lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 );
132 QgsDebugMsgLevel( QStringLiteral(
"res at lvl0 %1" ).arg( dx / mSpan ), 2 );
133 QgsDebugMsgLevel( QStringLiteral(
"res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
134 QgsDebugMsgLevel( QStringLiteral(
"res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
144 return std::unique_ptr<QgsPointCloudBlock>( cached );
147 std::unique_ptr<QgsPointCloudBlock> block;
150 const bool found = fetchNodeHierarchy( n );
153 mHierarchyMutex.lock();
154 int pointCount = mHierarchy.value( n );
155 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
156 mHierarchyMutex.unlock();
161 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
163 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
165 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
166 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
167 file.seekg( blockOffset );
168 file.read( rawBlockData.data(), blockSize );
171 QgsDebugError( QStringLiteral(
"Could not read file %1" ).arg( mUri ) );
176 block = QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression, filterRect );
181 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
189 block = blockRequest->takeBlock();
192 QgsDebugError( QStringLiteral(
"Error downloading node %1 data, error : %2 " ).arg( n.
toString(), blockRequest->errorStr() ) );
195 storeNodeDataToCache( block.get(), n, request );
206 scale(), offset(), mFilterExpression, request.
filterRect() );
209 if ( !fetchNodeHierarchy( n ) )
211 QMutexLocker locker( &mHierarchyMutex );
216 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
218 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
219 auto [ blockOffset, blockSize ] = mHierarchyNodePos.value( n );
220 int pointCount = mHierarchy.value( n );
223 scale(), offset(), filterExpression, request.
filterRect(),
224 blockOffset, blockSize, pointCount, *mLazInfo.get() );
229 return mLazInfo->crs();
232qint64 QgsCopcPointCloudIndex::pointCount()
const
234 return mLazInfo->pointCount();
237bool QgsCopcPointCloudIndex::loadHierarchy()
const
239 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
251 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
258 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
259 if ( !statisticsEvlrData.isEmpty() )
261 QgsMessageLog::logMessage( QObject::tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
265 lazperf::evlr_header statsEvlrHeader;
266 statsEvlrHeader.user_id =
"qgis";
267 statsEvlrHeader.record_id = 0;
268 statsEvlrHeader.description =
"Contains calculated statistics";
270 statsEvlrHeader.data_length = statsJson.size();
274 std::fstream copcFile;
275 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
276 if ( copcFile.is_open() && copcFile.good() )
279 lazperf::header14 header = mLazInfo->header();
280 header.evlr_count = header.evlr_count + 1;
282 header.write( copcFile );
285 copcFile.seekg( 0, std::ios::end );
287 statsEvlrHeader.write( copcFile );
288 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
296 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
304 const QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
305 if ( statisticsEvlrData.isEmpty() )
314bool QgsCopcPointCloudIndex::isValid()
const
321 QMutexLocker locker( &mHierarchyMutex );
323 QVector<QgsPointCloudNodeId> ancestors;
325 while ( !mHierarchy.contains( foundRoot ) )
327 ancestors.push_front( foundRoot );
330 ancestors.push_front( foundRoot );
333 auto hierarchyIt = mHierarchy.constFind( n );
334 if ( hierarchyIt == mHierarchy.constEnd() )
336 int nodesCount = *hierarchyIt;
337 if ( nodesCount < 0 )
339 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
340 mHierarchyMutex.unlock();
341 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
342 mHierarchyMutex.lock();
345 return mHierarchy.contains( n );
348void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
350 Q_ASSERT( byteSize > 0 );
352 QByteArray data = readRange( offset, byteSize );
353 if ( data.isEmpty() )
356 populateHierarchy( data.constData(), byteSize );
359void QgsCopcPointCloudIndex::populateHierarchy(
const char *hierarchyPageData, uint64_t byteSize )
const
377 QMutexLocker locker( &mHierarchyMutex );
379 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
381 const CopcEntry *entry =
reinterpret_cast<const CopcEntry *
>( hierarchyPageData + i );
382 const QgsPointCloudNodeId nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
383 mHierarchy[nodeId] = entry->pointCount;
384 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
390 return fetchNodeHierarchy( n );
395 bool nodeFound = fetchNodeHierarchy(
id );
396 Q_ASSERT( nodeFound );
400 QMutexLocker locker( &mHierarchyMutex );
401 pointCount = mHierarchy.value(
id, -1 );
404 QList<QgsPointCloudNodeId> children;
405 children.reserve( 8 );
406 const int d =
id.d() + 1;
407 const int x =
id.x() * 2;
408 const int y =
id.y() * 2;
409 const int z =
id.z() * 2;
411 for (
int i = 0; i < 8; ++i )
413 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
415 bool found = fetchNodeHierarchy( n2 );
417 QMutexLocker locker( &mHierarchyMutex );
418 if ( found && mHierarchy[
id] >= 0 )
419 children.append( n2 );
427QByteArray QgsCopcPointCloudIndex::readRange( uint64_t offset, uint64_t length )
const
431 QByteArray buffer( length, Qt::Initialization::Uninitialized );
432 mCopcFile.seekg( offset );
433 mCopcFile.read( buffer.data(), length );
434 if ( mCopcFile.eof() )
435 QgsDebugError( QStringLiteral(
"Read past end of file (path %1 offset %2 length %3)" ).arg( mUri ).arg( offset ).arg( length ) );
437 QgsDebugError( QStringLiteral(
"Error reading %1" ).arg( mUri ) );
442 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
444 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
445 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
446 QByteArray queryRange = QStringLiteral(
"bytes=%1-%2" ).arg( offset ).arg( offset + length - 1 ).toLocal8Bit();
447 nr.setRawHeader(
"Range", queryRange );
455 if ( reply->error() != QNetworkReply::NoError )
457 QgsDebugError( QStringLiteral(
"Request failed: %1 (offset %1 length %2)" ).arg( mUri ).arg( offset ).arg( length ) );
461 return reply->data();
465QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
const
467 uint64_t offset = mLazInfo->firstEvlrOffset();
468 uint32_t evlrCount = mLazInfo->evlrCount();
470 QByteArray statisticsEvlrData;
472 for ( uint32_t i = 0; i < evlrCount; ++i )
474 lazperf::evlr_header header;
476 QByteArray buffer = readRange( offset, 60 );
477 header.fill( buffer.data(), buffer.size() );
479 if ( header.user_id ==
"qgis" && header.record_id == 0 )
481 statisticsEvlrData = readRange( offset + 60, header.data_length );
485 offset += 60 + header.data_length;
488 return statisticsEvlrData;
491void QgsCopcPointCloudIndex::reset()
509 mOriginalMetadata.clear();
512 mHierarchyNodePos.clear();
515QVariantMap QgsCopcPointCloudIndex::extraMetadata()
const
519 { 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.
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, 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).
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.
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.
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)