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 );
171 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
179 block = blockRequest->takeBlock();
182 QgsDebugError( u
"Error downloading node %1 data, error : %2 "_s.arg( n.
toString(), blockRequest->errorStr() ) );
185 storeNodeDataToCache( block.get(), n, request );
198 if ( !fetchNodeHierarchy( n ) )
200 QMutexLocker locker( &mHierarchyMutex );
205 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
207 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
208 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
209 int pointCount = mHierarchy.value( n );
211 return new QgsCopcPointCloudBlockRequest( n, mUri, attributes(), requestAttributes, scale(), offset(), filterExpression, request.
filterRect(), blockOffset, blockSize, pointCount, *mLazInfo.get(), mAuthCfg );
217 const bool found = fetchNodeHierarchy( n );
220 mHierarchyMutex.lock();
221 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
222 mHierarchyMutex.unlock();
227 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
228 std::ifstream file( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
229 file.seekg( blockOffset );
230 file.read( rawBlockData.data(), blockSize );
239 return readRange( blockOffset, blockSize );
244 return mLazInfo->crs();
247qint64 QgsCopcPointCloudIndex::pointCount()
const
249 return mLazInfo->pointCount();
252bool QgsCopcPointCloudIndex::loadHierarchy()
const
254 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
266 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
273 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
274 if ( !statisticsEvlrData.isEmpty() )
276 QgsMessageLog::logMessage( QObject::tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mUri ) );
280 lazperf::evlr_header statsEvlrHeader;
281 statsEvlrHeader.user_id =
"qgis";
282 statsEvlrHeader.reserved = 0;
283 statsEvlrHeader.record_id = 0;
284 statsEvlrHeader.description =
"Contains calculated statistics";
286 statsEvlrHeader.data_length = statsJson.size();
289 QMutexLocker locker( &mFileMutex );
291 std::fstream copcFile;
292 copcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios_base::binary | std::iostream::in | std::iostream::out );
293 if ( copcFile.is_open() && copcFile.good() )
296 lazperf::header14 header = mLazInfo->header();
297 header.evlr_count = header.evlr_count + 1;
299 header.write( copcFile );
302 copcFile.seekg( 0, std::ios::end );
304 statsEvlrHeader.write( copcFile );
305 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
313 mCopcFile.open( QgsLazDecoder::toNativePath( mUri ), std::ios::binary );
321 const QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
322 if ( statisticsEvlrData.isEmpty() )
331bool QgsCopcPointCloudIndex::isValid()
const
338 QMutexLocker locker( &mHierarchyMutex );
340 QVector<QgsPointCloudNodeId> ancestors;
342 while ( !mHierarchy.contains( foundRoot ) )
344 ancestors.push_front( foundRoot );
347 ancestors.push_front( foundRoot );
350 auto hierarchyIt = mHierarchy.constFind( n );
351 if ( hierarchyIt == mHierarchy.constEnd() )
353 int nodesCount = *hierarchyIt;
354 if ( nodesCount < 0 )
356 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
357 mHierarchyMutex.unlock();
358 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
359 mHierarchyMutex.lock();
362 return mHierarchy.contains( n );
365void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
367 Q_ASSERT( byteSize > 0 );
369 QByteArray data = readRange( offset, byteSize );
370 if ( data.isEmpty() )
373 populateHierarchy( data.constData(), byteSize );
376void QgsCopcPointCloudIndex::populateHierarchy(
const char *hierarchyPageData, uint64_t byteSize )
const
394 QMutexLocker locker( &mHierarchyMutex );
396 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
398 const CopcEntry *entry =
reinterpret_cast<const CopcEntry *
>( hierarchyPageData + i );
399 const QgsPointCloudNodeId nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z );
400 mHierarchy[nodeId] = entry->pointCount;
401 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
407 return fetchNodeHierarchy( n );
412 bool nodeFound = fetchNodeHierarchy(
id );
413 Q_ASSERT( nodeFound );
417 QMutexLocker locker( &mHierarchyMutex );
418 pointCount = mHierarchy.value(
id, -1 );
421 QList<QgsPointCloudNodeId> children;
422 children.reserve( 8 );
423 const int d =
id.d() + 1;
424 const int x =
id.x() * 2;
425 const int y =
id.y() * 2;
426 const int z =
id.z() * 2;
428 for (
int i = 0; i < 8; ++i )
430 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
432 bool found = fetchNodeHierarchy( n2 );
434 QMutexLocker locker( &mHierarchyMutex );
435 if ( found && mHierarchy[
id] >= 0 )
436 children.append( n2 );
444QByteArray QgsCopcPointCloudIndex::readRange( uint64_t offset, uint64_t length )
const
448 QMutexLocker locker( &mFileMutex );
450 QByteArray buffer( length, Qt::Initialization::Uninitialized );
451 mCopcFile.seekg( offset );
452 mCopcFile.read( buffer.data(), length );
453 if ( mCopcFile.eof() )
454 QgsDebugError( u
"Read past end of file (path %1 offset %2 length %3)"_s.arg( mUri ).arg( offset ).arg( length ) );
461 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
463 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
464 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
465 QByteArray queryRange = u
"bytes=%1-%2"_s.arg( offset ).arg( offset + length - 1 ).toLocal8Bit();
466 nr.setRawHeader(
"Range", queryRange );
470 QgsDebugError( u
"Network request update failed for authcfg: %1"_s.arg( mAuthCfg ) );
480 if ( reply->error() != QNetworkReply::NoError )
482 QgsDebugError( u
"Request failed: %1 (offset %1 length %2)"_s.arg( mUri ).arg( offset ).arg( length ) );
486 return reply->data();
490QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
const
492 uint64_t offset = mLazInfo->firstEvlrOffset();
493 uint32_t evlrCount = mLazInfo->evlrCount();
495 QByteArray statisticsEvlrData;
497 for ( uint32_t i = 0; i < evlrCount; ++i )
499 lazperf::evlr_header header;
501 QByteArray buffer = readRange( offset, 60 );
502 header.fill( buffer.data(), buffer.size() );
504 if ( header.user_id ==
"qgis" && header.record_id == 0 )
506 statisticsEvlrData = readRange( offset + 60, header.data_length );
510 offset += 60 + header.data_length;
513 return statisticsEvlrData;
516void QgsCopcPointCloudIndex::reset()
534 mOriginalMetadata.clear();
537 mHierarchyNodePos.clear();
540QVariantMap QgsCopcPointCloudIndex::extraMetadata()
const
543 { 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(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
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)