24 #include <QMutexLocker>
25 #include <QJsonDocument>
26 #include <QJsonObject>
36 #include "qgspointcloudexpression.h"
38 #include "lazperf/lazperf.hpp"
39 #include "lazperf/readers.hpp"
40 #include "lazperf/vlr.hpp"
44 #define PROVIDER_KEY QStringLiteral( "copc" )
45 #define PROVIDER_DESCRIPTION QStringLiteral( "COPC point cloud provider" )
47 QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() =
default;
49 QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() =
default;
51 std::unique_ptr<QgsPointCloudIndex> QgsCopcPointCloudIndex::clone()
const
53 QgsCopcPointCloudIndex *clone =
new QgsCopcPointCloudIndex;
54 QMutexLocker locker( &mHierarchyMutex );
55 copyCommonProperties( clone );
56 return std::unique_ptr<QgsPointCloudIndex>( clone );
59 void QgsCopcPointCloudIndex::load(
const QString &fileName )
62 mCopcFile.open( QgsLazDecoder::toNativePath( fileName ), std::ios::binary );
64 if ( !mCopcFile.is_open() || !mCopcFile.good() )
66 mError = tr(
"Unable to open %1 for reading" ).arg( fileName );
72 mIsValid = mLazInfo->isValid();
75 mIsValid = loadSchema( *mLazInfo.get() );
79 mError = tr(
"Unable to recognize %1 as a LAZ file: \"%2\"" ).arg( fileName, mLazInfo->error() );
86 bool QgsCopcPointCloudIndex::loadSchema(
QgsLazInfo &lazInfo )
88 QByteArray copcInfoVlrData = lazInfo.
vlrData( QStringLiteral(
"copc" ), 1 );
89 if ( copcInfoVlrData.isEmpty() )
91 mError = tr(
"Invalid COPC file" );
94 mCopcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() );
96 mScale = lazInfo.
scale();
97 mOffset = lazInfo.
offset();
103 mExtent.
set( minCoords.
x(), minCoords.
y(), maxCoords.
x(), maxCoords.
y() );
104 mZMin = minCoords.
z();
105 mZMax = maxCoords.
z();
109 const double xmin = mCopcInfoVlr.center_x - mCopcInfoVlr.halfsize;
110 const double ymin = mCopcInfoVlr.center_y - mCopcInfoVlr.halfsize;
111 const double zmin = mCopcInfoVlr.center_z - mCopcInfoVlr.halfsize;
112 const double xmax = mCopcInfoVlr.center_x + mCopcInfoVlr.halfsize;
113 const double ymax = mCopcInfoVlr.center_y + mCopcInfoVlr.halfsize;
114 const double zmax = mCopcInfoVlr.center_z + mCopcInfoVlr.halfsize;
117 ( xmin - mOffset.x() ) / mScale.x(),
118 ( ymin - mOffset.y() ) / mScale.y(),
119 ( zmin - mOffset.z() ) / mScale.z(),
120 ( xmax - mOffset.x() ) / mScale.x(),
121 ( ymax - mOffset.y() ) / mScale.y(),
122 ( zmax - mOffset.z() ) / mScale.z()
125 double calculatedSpan = nodeMapExtent( root() ).width() / mCopcInfoVlr.spacing;
126 mSpan = calculatedSpan;
129 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
130 QgsDebugMsgLevel( QStringLiteral(
"lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 );
131 QgsDebugMsgLevel( QStringLiteral(
"res at lvl0 %1" ).arg( dx / mSpan ), 2 );
132 QgsDebugMsgLevel( QStringLiteral(
"res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
133 QgsDebugMsgLevel( QStringLiteral(
"res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
141 const bool found = fetchNodeHierarchy( n );
144 mHierarchyMutex.lock();
145 int pointCount = mHierarchy.value( n );
146 auto [blockOffset, blockSize] = mHierarchyNodePos.value( n );
147 mHierarchyMutex.unlock();
152 QgsPointCloudExpression filterExpression = mFilterExpression;
154 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
156 QByteArray rawBlockData( blockSize, Qt::Initialization::Uninitialized );
157 std::ifstream file( QgsLazDecoder::toNativePath( mFileName ), std::ios::binary );
158 file.seekg( blockOffset );
159 file.read( rawBlockData.data(), blockSize );
162 QgsDebugMsg( QStringLiteral(
"Could not read file %1" ).arg( mFileName ) );
166 return QgsLazDecoder::decompressCopc( rawBlockData, *mLazInfo.get(), pointCount, requestAttributes, filterExpression );
179 return mLazInfo->crs();
182 qint64 QgsCopcPointCloudIndex::pointCount()
const
184 return mLazInfo->pointCount();
187 bool QgsCopcPointCloudIndex::loadHierarchy()
189 QMutexLocker locker( &mHierarchyMutex );
190 fetchHierarchyPage( mCopcInfoVlr.root_hier_offset, mCopcInfoVlr.root_hier_size );
196 if ( mLazInfo->version() != qMakePair<uint8_t, uint8_t>( 1, 4 ) )
203 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
204 if ( !statisticsEvlrData.isEmpty() )
206 QgsMessageLog::logMessage( tr(
"Can't write statistics to \"%1\": file already contains COPC statistics!" ).arg( mFileName ) );
210 lazperf::evlr_header statsEvlrHeader;
211 statsEvlrHeader.user_id =
"qgis";
212 statsEvlrHeader.record_id = 0;
213 statsEvlrHeader.description =
"Contains calculated statistics";
215 statsEvlrHeader.data_length = statsJson.size();
219 std::fstream copcFile;
220 copcFile.open( QgsLazDecoder::toNativePath( mFileName ), std::ios_base::binary | std::iostream::in | std::iostream::out );
221 if ( copcFile.is_open() && copcFile.good() )
224 lazperf::header14 header = mLazInfo->header();
225 header.evlr_count = header.evlr_count + 1;
227 header.write( copcFile );
230 copcFile.seekg( 0, std::ios::end );
232 statsEvlrHeader.write( copcFile );
233 copcFile.write( statsJson.data(), statsEvlrHeader.data_length );
241 mCopcFile.open( QgsLazDecoder::toNativePath( mFileName ), std::ios::binary );
247 QByteArray statisticsEvlrData = fetchCopcStatisticsEvlrData();
249 if ( statisticsEvlrData.isEmpty() )
257 bool QgsCopcPointCloudIndex::isValid()
const
264 QMutexLocker locker( &mHierarchyMutex );
266 QVector<IndexedPointCloudNode> ancestors;
268 while ( !mHierarchy.contains( foundRoot ) )
270 ancestors.push_front( foundRoot );
273 ancestors.push_front( foundRoot );
276 auto hierarchyIt = mHierarchy.constFind( n );
277 if ( hierarchyIt == mHierarchy.constEnd() )
279 int nodesCount = *hierarchyIt;
280 if ( nodesCount < 0 )
282 auto hierarchyNodePos = mHierarchyNodePos.constFind( n );
283 fetchHierarchyPage( hierarchyNodePos->first, hierarchyNodePos->second );
289 void QgsCopcPointCloudIndex::fetchHierarchyPage( uint64_t offset, uint64_t byteSize )
const
291 mCopcFile.seekg( offset );
292 std::unique_ptr<char []> data(
new char[ byteSize ] );
293 mCopcFile.read( data.get(), byteSize );
311 for ( uint64_t i = 0; i < byteSize; i +=
sizeof( CopcEntry ) )
313 CopcEntry *entry =
reinterpret_cast<CopcEntry *
>( data.get() + i );
315 mHierarchy[nodeId] = entry->pointCount;
316 mHierarchyNodePos.insert( nodeId, QPair<uint64_t, int32_t>( entry->offset, entry->byteSize ) );
322 fetchNodeHierarchy( n );
323 mHierarchyMutex.lock();
325 auto it = mHierarchy.constFind( n );
326 const bool found = it != mHierarchy.constEnd() && *it > 0;
327 mHierarchyMutex.unlock();
331 QList<IndexedPointCloudNode> QgsCopcPointCloudIndex::nodeChildren(
const IndexedPointCloudNode &n )
const
333 fetchNodeHierarchy( n );
335 mHierarchyMutex.lock();
337 auto hierarchyIt = mHierarchy.constFind( n );
338 Q_ASSERT( hierarchyIt != mHierarchy.constEnd() );
339 QList<IndexedPointCloudNode> lst;
341 const int d = n.
d() + 1;
342 const int x = n.
x() * 2;
343 const int y = n.
y() * 2;
344 const int z = n.
z() * 2;
345 mHierarchyMutex.unlock();
347 for (
int i = 0; i < 8; ++i )
349 int dx = i & 1, dy = !!( i & 2 ), dz = !!( i & 4 );
351 if ( fetchNodeHierarchy( n2 ) && mHierarchy[n] > 0 )
357 void QgsCopcPointCloudIndex::copyCommonProperties( QgsCopcPointCloudIndex *destination )
const
362 destination->mIsValid = mIsValid;
363 destination->mFileName = mFileName;
364 destination->mCopcFile.open( QgsLazDecoder::toNativePath( mFileName ), std::ios::binary );
365 destination->mCopcInfoVlr = mCopcInfoVlr;
366 destination->mHierarchyNodePos = mHierarchyNodePos;
367 destination->mOriginalMetadata = mOriginalMetadata;
368 destination->mLazInfo.reset(
new QgsLazInfo( *mLazInfo ) );
371 QByteArray QgsCopcPointCloudIndex::fetchCopcStatisticsEvlrData()
373 uint64_t offset = mLazInfo->firstEvlrOffset();
374 uint32_t evlrCount = mLazInfo->evlrCount();
376 QByteArray statisticsEvlrData;
378 for ( uint32_t i = 0; i < evlrCount; ++i )
380 lazperf::evlr_header header;
381 mCopcFile.seekg( offset );
383 mCopcFile.read( buffer, 60 );
384 header.fill( buffer, 60 );
387 if ( header.user_id ==
"qgis" && header.record_id == 0 )
389 statisticsEvlrData = QByteArray( header.data_length, Qt::Initialization::Uninitialized );
390 mCopcFile.read( statisticsEvlrData.data(), header.data_length );
394 offset += 60 + header.data_length;
397 return statisticsEvlrData;