31#include "qgspointcloudexpression.h"
41#include <QJsonDocument>
43#include <QNetworkRequest>
50#define PROVIDER_KEY QStringLiteral( "ept" )
51#define PROVIDER_DESCRIPTION QStringLiteral( "EPT point cloud provider" )
53QgsEptPointCloudIndex::QgsEptPointCloudIndex()
58QgsEptPointCloudIndex::~QgsEptPointCloudIndex() =
default;
60void QgsEptPointCloudIndex::load(
const QString &urlString,
const QString &authcfg )
64 if ( url.isValid() && ( url.scheme() ==
"http" || url.scheme() ==
"https" ) )
70 QStringList splitUrl = mUri.split(
'/' );
72 mUrlDirectoryPart = splitUrl.join(
'/' );
78 QNetworkRequest nr = QNetworkRequest( QUrl( mUri ) );
95 if ( !f.open( QIODevice::ReadOnly ) )
97 mError = QObject::tr(
"Unable to open %1 for reading" ).arg( mUri );
101 content = f.readAll();
104 bool success = loadSchema( content );
108 const QString manifestPath = mUrlDirectoryPart + QStringLiteral(
"/ept-sources/manifest.json" );
109 QByteArray manifestJson;
112 QUrl manifestUrl( manifestPath );
114 QNetworkRequest nr = QNetworkRequest( QUrl( manifestUrl ) );
123 QFile manifestFile( manifestPath );
124 if ( manifestFile.open( QIODevice::ReadOnly ) )
125 manifestJson = manifestFile.readAll();
128 if ( !manifestJson.isEmpty() )
129 loadManifest( manifestJson );
134 QgsDebugError( QStringLiteral(
"Failed to load root EPT node" ) );
141void QgsEptPointCloudIndex::loadManifest(
const QByteArray &manifestJson )
145 const QJsonDocument manifestDoc = QJsonDocument::fromJson( manifestJson, &err );
146 if ( err.error != QJsonParseError::NoError )
149 const QJsonArray manifestArray = manifestDoc.array();
150 if ( manifestArray.empty() )
154 const QJsonObject sourceObject = manifestArray.at( 0 ).toObject();
155 const QString metadataPath = sourceObject.value( QStringLiteral(
"metadataPath" ) ).toString();
156 const QString fullMetadataPath = mUrlDirectoryPart + QStringLiteral(
"/ept-sources/" ) + metadataPath;
158 QByteArray metadataJson;
161 QUrl metadataUrl( fullMetadataPath );
162 QNetworkRequest nr = QNetworkRequest( QUrl( metadataUrl ) );
172 QFile metadataFile( fullMetadataPath );
173 if ( ! metadataFile.open( QIODevice::ReadOnly ) )
175 metadataJson = metadataFile.readAll();
178 const QJsonDocument metadataDoc = QJsonDocument::fromJson( metadataJson, &err );
179 if ( err.error != QJsonParseError::NoError )
182 const QJsonObject metadataObject = metadataDoc.object().value( QStringLiteral(
"metadata" ) ).toObject();
183 if ( metadataObject.empty() )
186 const QJsonObject sourceMetadata = metadataObject.constBegin().value().toObject();
187 mOriginalMetadata = sourceMetadata.toVariantMap();
190bool QgsEptPointCloudIndex::loadSchema(
const QByteArray &dataJson )
193 const QJsonDocument doc = QJsonDocument::fromJson( dataJson, &err );
194 if ( err.error != QJsonParseError::NoError )
196 const QJsonObject result = doc.object();
197 mDataType = result.value( QLatin1String(
"dataType" ) ).toString();
198 if ( mDataType != QLatin1String(
"laszip" ) && mDataType != QLatin1String(
"binary" ) && mDataType != QLatin1String(
"zstandard" ) )
201 const QString hierarchyType = result.value( QLatin1String(
"hierarchyType" ) ).toString();
202 if ( hierarchyType != QLatin1String(
"json" ) )
205 mSpan = result.value( QLatin1String(
"span" ) ).toInt();
206 mPointCount = result.value( QLatin1String(
"points" ) ).toDouble();
209 const QJsonObject srs = result.value( QLatin1String(
"srs" ) ).toObject();
210 mWkt = srs.value( QLatin1String(
"wkt" ) ).toString();
213 const QJsonArray bounds = result.value( QLatin1String(
"bounds" ) ).toArray();
214 if ( bounds.size() != 6 )
217 const QJsonArray boundsConforming = result.value( QLatin1String(
"boundsConforming" ) ).toArray();
218 if ( boundsConforming.size() != 6 )
220 mExtent.set( boundsConforming[0].toDouble(), boundsConforming[1].toDouble(),
221 boundsConforming[3].toDouble(), boundsConforming[4].toDouble() );
222 mZMin = boundsConforming[2].toDouble();
223 mZMax = boundsConforming[5].toDouble();
225 const QJsonArray schemaArray = result.value( QLatin1String(
"schema" ) ).toArray();
228 for (
const QJsonValue &schemaItem : schemaArray )
230 const QJsonObject schemaObj = schemaItem.toObject();
231 const QString name = schemaObj.value( QLatin1String(
"name" ) ).toString();
232 const QString type = schemaObj.value( QLatin1String(
"type" ) ).toString();
234 const int size = schemaObj.value( QLatin1String(
"size" ) ).toInt();
236 if ( name == QLatin1String(
"ClassFlags" ) && size == 1 )
243 else if ( type == QLatin1String(
"float" ) && ( size == 4 ) )
247 else if ( type == QLatin1String(
"float" ) && ( size == 8 ) )
251 else if ( size == 1 )
255 else if ( type == QLatin1String(
"unsigned" ) && size == 2 )
259 else if ( size == 2 )
263 else if ( size == 4 )
274 if ( schemaObj.contains( QLatin1String(
"scale" ) ) )
275 scale = schemaObj.value( QLatin1String(
"scale" ) ).toDouble();
278 if ( schemaObj.contains( QLatin1String(
"offset" ) ) )
279 offset = schemaObj.value( QLatin1String(
"offset" ) ).toDouble();
281 if ( name == QLatin1String(
"X" ) )
283 mOffset.set( offset, mOffset.y(), mOffset.z() );
284 mScale.set( scale, mScale.y(), mScale.z() );
286 else if ( name == QLatin1String(
"Y" ) )
288 mOffset.set( mOffset.x(), offset, mOffset.z() );
289 mScale.set( mScale.x(), scale, mScale.z() );
291 else if ( name == QLatin1String(
"Z" ) )
293 mOffset.set( mOffset.x(), mOffset.y(), offset );
294 mScale.set( mScale.x(), mScale.y(), scale );
298 AttributeStatistics stats;
299 bool foundStats =
false;
300 if ( schemaObj.contains( QLatin1String(
"count" ) ) )
302 stats.count = schemaObj.value( QLatin1String(
"count" ) ).toInt();
305 if ( schemaObj.contains( QLatin1String(
"minimum" ) ) )
307 stats.minimum = schemaObj.value( QLatin1String(
"minimum" ) ).toDouble();
310 if ( schemaObj.contains( QLatin1String(
"maximum" ) ) )
312 stats.maximum = schemaObj.value( QLatin1String(
"maximum" ) ).toDouble();
315 if ( schemaObj.contains( QLatin1String(
"count" ) ) )
317 stats.mean = schemaObj.value( QLatin1String(
"mean" ) ).toDouble();
320 if ( schemaObj.contains( QLatin1String(
"stddev" ) ) )
322 stats.stDev = schemaObj.value( QLatin1String(
"stddev" ) ).toDouble();
325 if ( schemaObj.contains( QLatin1String(
"variance" ) ) )
327 stats.variance = schemaObj.value( QLatin1String(
"variance" ) ).toDouble();
331 mMetadataStats.insert( name, stats );
333 if ( schemaObj.contains( QLatin1String(
"counts" ) ) )
335 QMap< int, int > classCounts;
336 const QJsonArray counts = schemaObj.value( QLatin1String(
"counts" ) ).toArray();
337 for (
const QJsonValue &count : counts )
339 const QJsonObject countObj = count.toObject();
340 classCounts.insert( countObj.value( QLatin1String(
"value" ) ).toInt(), countObj.value( QLatin1String(
"count" ) ).toInt() );
342 mAttributeClasses.insert( name, classCounts );
345 setAttributes( attributes );
350 const double xmin = bounds[0].toDouble();
351 const double ymin = bounds[1].toDouble();
352 const double zmin = bounds[2].toDouble();
353 const double xmax = bounds[3].toDouble();
354 const double ymax = bounds[4].toDouble();
355 const double zmax = bounds[5].toDouble();
357 mRootBounds =
QgsBox3D( xmin, ymin, zmin, xmax, ymax, zmax );
360 double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
361 QgsDebugMsgLevel( QStringLiteral(
"lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 );
362 QgsDebugMsgLevel( QStringLiteral(
"res at lvl0 %1" ).arg( dx / mSpan ), 2 );
363 QgsDebugMsgLevel( QStringLiteral(
"res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
364 QgsDebugMsgLevel( QStringLiteral(
"res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
374 return std::unique_ptr<QgsPointCloudBlock>( cached );
377 std::unique_ptr<QgsPointCloudBlock> block;
380 std::unique_ptr<QgsPointCloudBlockRequest> blockRequest( asyncNodeData( n, request ) );
388 block = blockRequest->takeBlock();
391 QgsDebugError( QStringLiteral(
"Error downloading node %1 data, error : %2 " ).arg( n.
toString(), blockRequest->errorStr() ) );
399 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
401 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
404 if ( mDataType == QLatin1String(
"binary" ) )
406 const QString filename = QStringLiteral(
"%1/ept-data/%2.bin" ).arg( mUrlDirectoryPart, n.
toString() );
407 block = QgsEptDecoder::decompressBinary( filename, attributes(), requestAttributes, scale(), offset(), filterExpression, filterRect );
409 else if ( mDataType == QLatin1String(
"zstandard" ) )
411 const QString filename = QStringLiteral(
"%1/ept-data/%2.zst" ).arg( mUrlDirectoryPart, n.
toString() );
412 block = QgsEptDecoder::decompressZStandard( filename, attributes(), request.
attributes(), scale(), offset(), filterExpression, filterRect );
414 else if ( mDataType == QLatin1String(
"laszip" ) )
416 const QString filename = QStringLiteral(
"%1/ept-data/%2.laz" ).arg( mUrlDirectoryPart, n.
toString() );
417 block = QgsLazDecoder::decompressLaz( filename, requestAttributes, filterExpression, filterRect );
421 storeNodeDataToCache( block.get(), n, request );
430 scale(), offset(), mFilterExpression, request.
filterRect() );
436 if ( !loadNodeHierarchy( n ) )
440 if ( mDataType == QLatin1String(
"binary" ) )
442 fileUrl = QStringLiteral(
"%1/ept-data/%2.bin" ).arg( mUrlDirectoryPart, n.
toString() );
444 else if ( mDataType == QLatin1String(
"zstandard" ) )
446 fileUrl = QStringLiteral(
"%1/ept-data/%2.zst" ).arg( mUrlDirectoryPart, n.
toString() );
448 else if ( mDataType == QLatin1String(
"laszip" ) )
450 fileUrl = QStringLiteral(
"%1/ept-data/%2.laz" ).arg( mUrlDirectoryPart, n.
toString() );
460 QgsPointCloudExpression filterExpression = request.
ignoreIndexFilterEnabled() ? QgsPointCloudExpression() : mFilterExpression;
462 requestAttributes.
extend( attributes(), filterExpression.referencedAttributes() );
468 return loadNodeHierarchy( n );
476qint64 QgsEptPointCloudIndex::pointCount()
const
491 QVector<QgsPointCloudNodeId> pathToRoot = nodePathToRoot(
id );
492 for (
int i = pathToRoot.size() - 1; i >= 0; --i )
494 loadSingleNodeHierarchy( pathToRoot[i] );
496 QMutexLocker locker( &mHierarchyMutex );
497 qint64 pointCount = mHierarchy.value(
id, -1 );
498 if ( pointCount != -1 )
508 QMap<QString, QgsPointCloudAttributeStatistics> statsMap;
511 QString name = attribute.name();
512 const AttributeStatistics &stats = mMetadataStats[ name ];
513 if ( !stats.minimum.isValid() )
516 s.
minimum = stats.minimum.toDouble();
517 s.
maximum = stats.maximum.toDouble();
519 s.
stDev = stats.stDev;
520 s.
count = stats.count;
524 statsMap[ name ] = std::move( s );
529bool QgsEptPointCloudIndex::loadSingleNodeHierarchy(
const QgsPointCloudNodeId &nodeId )
const
531 mHierarchyMutex.lock();
532 const bool foundInHierarchy = mHierarchy.contains( nodeId );
533 const bool foundInHierarchyNodes = mHierarchyNodes.contains( nodeId );
534 mHierarchyMutex.unlock();
536 if ( foundInHierarchy )
539 if ( !foundInHierarchyNodes )
542 const QString filePath = QStringLiteral(
"%1/ept-hierarchy/%2.json" ).arg( mUrlDirectoryPart, nodeId.
toString() );
544 QByteArray dataJsonH;
547 QNetworkRequest nr( filePath );
549 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
550 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute,
true );
554 QgsDebugError( QStringLiteral(
"Network request update failed for authcfg: %1" ).arg( mAuthCfg ) );
564 if ( reply->error() != QNetworkReply::NoError )
566 QgsDebugError( QStringLiteral(
"Request failed: " ) + filePath );
570 dataJsonH = reply->data();
574 QFile file( filePath );
575 if ( ! file.open( QIODevice::ReadOnly ) )
577 QgsDebugError( QStringLiteral(
"Loading file failed: " ) + filePath );
580 dataJsonH = file.readAll();
583 QJsonParseError errH;
584 const QJsonDocument docH = QJsonDocument::fromJson( dataJsonH, &errH );
585 if ( errH.error != QJsonParseError::NoError )
587 QgsDebugMsgLevel( QStringLiteral(
"QJsonParseError when reading hierarchy from file %1" ).arg( filePath ), 2 );
591 QMutexLocker locker( &mHierarchyMutex );
592 const QJsonObject rootHObj = docH.object();
593 for (
auto it = rootHObj.constBegin(); it != rootHObj.constEnd(); ++it )
595 const QString nodeIdStr = it.key();
596 const int nodePointCount = it.value().toInt();
598 if ( nodePointCount >= 0 )
599 mHierarchy[nodeId] = nodePointCount;
600 else if ( nodePointCount == -1 )
601 mHierarchyNodes.insert( nodeId );
607QVector<QgsPointCloudNodeId> QgsEptPointCloudIndex::nodePathToRoot(
const QgsPointCloudNodeId &nodeId )
const
609 QVector<QgsPointCloudNodeId> path;
613 path.push_back( currentNode );
616 while ( currentNode.
d() >= 0 );
625 QMutexLocker lock( &mHierarchyMutex );
626 found = mHierarchy.contains( nodeId );
631 QVector<QgsPointCloudNodeId> pathToRoot = nodePathToRoot( nodeId );
632 for (
int i = pathToRoot.size() - 1; i >= 0 && !mHierarchy.contains( nodeId ); --i )
635 if ( !loadSingleNodeHierarchy( node ) )
640 QMutexLocker lock( &mHierarchyMutex );
641 found = mHierarchy.contains( nodeId );
648bool QgsEptPointCloudIndex::isValid()
const
659#undef PROVIDER_DESCRIPTION
PointCloudAccessType
The access type of the data, local is for local files and remote for remote files (over HTTP).
@ Local
Local means the source is a local file on the machine.
@ Remote
Remote means it's loaded through a protocol like HTTP.
virtual QgsPointCloudNode getNode(const QgsPointCloudNodeId &id) const
Returns object for a given node.
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 thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
void setAuthCfg(const QString &authCfg)
Sets the authentication config id which should be used during the request.
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr, RequestFlags requestFlags=QgsBlockingNetworkRequest::RequestFlags())
Performs a "get" operation on the specified request.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
A 3-dimensional box composed of x, y, z coordinates.
Handles a QgsPointCloudBlockRequest using existing cached QgsPointCloudBlock.
Represents a coordinate reference system (CRS).
static QgsCoordinateReferenceSystem fromWkt(const QString &wkt)
Creates a CRS from a WKT spatial ref sys definition string.
Base class for handling loading QgsPointCloudBlock asynchronously from a remote EPT dataset.
QByteArray content() const
Returns the reply content.
A collection of point cloud attributes.
void push_back(const QgsPointCloudAttribute &attribute)
Adds extra attribute.
void extend(const QgsPointCloudAttributeCollection &otherCollection, const QSet< QString > &matchingNames)
Adds specific missing attributes from another QgsPointCloudAttributeCollection.
Attribute for point cloud data pair of name and size in bytes.
@ UShort
Unsigned short int 2 bytes.
@ Short
Short int 2 bytes.
@ UChar
Unsigned char 1 byte.
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.
static QgsPointCloudNodeId fromString(const QString &str)
Creates node from string.
QString toString() const
Encode node to string.
QgsPointCloudNodeId parentNode() const
Returns the parent of the node.
Keeps metadata for an indexed point cloud node.
QList< QgsPointCloudNodeId > children() const
Returns IDs of child nodes.
qint64 pointCount() const
Returns number of points contained in node data.
float error() const
Returns node's error in map units (used to determine in whether the node has enough detail for the cu...
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.
A rectangle specified with double values.
void finished()
Emitted when the reply has finished (either with a success or with a failure).
#define QgsDebugMsgLevel(str, level)
#define QgsDebugError(str)
#define QgsSetRequestInitiatorClass(request, _class)
Stores statistics of one attribute of a point cloud dataset.
QMap< int, int > classCount