diff --git a/CMakeLists.txt b/CMakeLists.txt index 2505a58a746..202cc9d1edb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -286,6 +286,8 @@ if(WITH_CORE) set (WITH_EPT TRUE CACHE BOOL "Determines whether Entwine Point Cloud (EPT) support should be built") + set (WITH_COPC TRUE CACHE BOOL "Determines whether Cloud Optimized Point Cloud (COPC) support should be built") + set (WITH_THREAD_LOCAL TRUE CACHE BOOL "Determines whether std::thread_local should be used") mark_as_advanced(WITH_THREAD_LOCAL) @@ -418,6 +420,15 @@ if(WITH_CORE) set(HAVE_EPT TRUE) # used in qgsconfig.h endif() + if (WITH_COPC) # COPC provider + find_package(ZSTD REQUIRED) # for decompression of point clouds + find_package(LazPerf) # for decompression of point clouds + if (NOT LazPerf_FOUND) + message(STATUS "Using embedded laz-perf") + endif() + set(HAVE_COPC TRUE) # used in qgsconfig.h + endif() + if (WITH_PDAL) if (NOT WITH_EPT) message(FATAL_ERROR "PDAL provider cannot be built with EPT disabled") diff --git a/cmake_templates/qgsconfig.h.in b/cmake_templates/qgsconfig.h.in index 32a90653db7..b6002acd035 100644 --- a/cmake_templates/qgsconfig.h.in +++ b/cmake_templates/qgsconfig.h.in @@ -98,6 +98,8 @@ #cmakedefine HAVE_EPT +#cmakedefine HAVE_COPC + #cmakedefine HAVE_PDAL_QGIS #define PDAL_VERSION "${PDAL_VERSION}" #define PDAL_VERSION_MAJOR_INT ${PDAL_VERSION_MAJOR} diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9d20f7ebee3..bdae2e38850 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1973,6 +1973,55 @@ if (WITH_EPT) add_definitions( -DWITH_EPT ) endif() +if (WITH_COPC) + include_directories(providers/copc) + + include_directories(SYSTEM + ${ZSTD_INCLUDE_DIR} + ) + + if (LazPerf_FOUND) + # Use system laz-perf + include_directories(SYSTEM + ${LazPerf_INCLUDE_DIR} + ) + else() + # Use embedded laz-perf from external/laz-perf + include_directories(SYSTEM + ) + + set(QGIS_CORE_SRCS ${QGIS_CORE_SRCS} + ${CMAKE_SOURCE_DIR}/external/lazperf/charbuf.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/filestream.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/header.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/lazperf.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/readers.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/vlr.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/detail/field_byte10.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/detail/field_byte14.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/detail/field_gpstime10.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/detail/field_nir14.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/detail/field_point10.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/detail/field_point14.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/detail/field_rgb10.cpp + ${CMAKE_SOURCE_DIR}/external/lazperf/detail/field_rgb14.cpp + ) + endif() + + set(QGIS_CORE_SRCS ${QGIS_CORE_SRCS} + providers/copc/qgscopcprovider.cpp + pointcloud/qgseptdecoder.cpp + pointcloud/qgscopcpointcloudindex.cpp + ) + set(QGIS_CORE_HDRS ${QGIS_CORE_HDRS} + providers/copc/qgscopcprovider.h + pointcloud/qgseptdecoder.h + pointcloud/qgscopcpointcloudindex.h + ) + + add_definitions( -DWITH_COPC ) +endif() + if (APPLE) # Libtasn1 is for DER-encoded PKI ASN.1 parsing/extracting workarounds include_directories(SYSTEM @@ -2109,6 +2158,11 @@ if (WITH_EPT) ${CMAKE_SOURCE_DIR}/src/core/providers/ept) endif() +if (WITH_COPC) + target_include_directories(qgis_core PUBLIC + ${CMAKE_SOURCE_DIR}/src/core/providers/copc) +endif() + GENERATE_EXPORT_HEADER( qgis_core BASE_NAME CORE @@ -2229,6 +2283,15 @@ if (WITH_EPT) endif() endif() +if (WITH_COPC) + target_link_libraries(qgis_core + ${ZSTD_LIBRARY} + ) + if (LazPerf_FOUND) + target_link_libraries(qgis_core ${LazPerf_LIBRARY}) + endif() +endif() + if (WITH_PDAL) target_link_libraries(qgis_core ${PDAL_LIBRARIES} diff --git a/src/core/pointcloud/qgscopcpointcloudindex.cpp b/src/core/pointcloud/qgscopcpointcloudindex.cpp new file mode 100644 index 00000000000..da68bf57038 --- /dev/null +++ b/src/core/pointcloud/qgscopcpointcloudindex.cpp @@ -0,0 +1,346 @@ +/*************************************************************************** + qgscopcpointcloudindex.cpp + -------------------- + begin : March 2022 + copyright : (C) 2022 by Belgacem Nedjima + email : belgacem dot nedjima at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgscopcpointcloudindex.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "qgseptdecoder.h" +#include "qgscoordinatereferencesystem.h" +#include "qgspointcloudrequest.h" +#include "qgspointcloudattribute.h" +#include "qgslogger.h" +#include "qgsfeedback.h" +#include "qgsmessagelog.h" +#include "qgspointcloudexpression.h" + +#include "lazperf/lazperf.hpp" +#include "lazperf/readers.hpp" + +///@cond PRIVATE + +#define PROVIDER_KEY QStringLiteral( "copc" ) +#define PROVIDER_DESCRIPTION QStringLiteral( "COPC point cloud provider" ) + +QgsCopcPointCloudIndex::QgsCopcPointCloudIndex() = default; + +QgsCopcPointCloudIndex::~QgsCopcPointCloudIndex() = default; + +void QgsCopcPointCloudIndex::load( const QString &fileName ) +{ + QFile f( fileName ); + if ( !f.open( QIODevice::ReadOnly ) ) + { + QgsMessageLog::logMessage( tr( "Unable to open %1 for reading" ).arg( fileName ) ); + mIsValid = false; + return; + } + + mFileName = fileName; + + bool success = loadSchema( fileName ); + + if ( success ) + { + success = loadHierarchy( fileName ); + } + + mIsValid = success; +} + +bool QgsCopcPointCloudIndex::loadSchema( const QString &filename ) +{ + std::ifstream file( filename.toStdString() ); + lazperf::reader::generic_file f( file ); + + mDataType = QStringLiteral( "copc" ); + + mPointCount = f.header().point_count; + + mScale = QgsVector3D( f.header().scale.x, f.header().scale.y, f.header().scale.z ); + mOffset = QgsVector3D( f.header().offset.x, f.header().offset.y, f.header().offset.z ); + + // The COPC format only uses PDRF 6, 7 or 8. So there should be a OGC Coordinate System WKT record + mWkt = QString(); + std::vector wktRecordData = f.vlrData( "LASF_Projection", 2112 ); + if ( !wktRecordData.empty() ) + { + lazperf::wkt_vlr wktVlr; + wktVlr.fill( wktRecordData.data(), wktRecordData.size() ); + mWkt = QString::fromStdString( wktVlr.wkt ); + } + + mExtent.set( f.header().minx, f.header().miny, f.header().maxx, f.header().maxy ); + + mZMin = f.header().minz; + mZMax = f.header().maxz; + + + // Attributes for COPC format + // COPC supports only PDRF 6, 7 and 8 + + // TODO: How to handle bitfields in LAZ + + QgsPointCloudAttributeCollection attributes; + attributes.push_back( QgsPointCloudAttribute( "X", ( QgsPointCloudAttribute::DataType ) 9 ) ); + attributes.push_back( QgsPointCloudAttribute( "Y", ( QgsPointCloudAttribute::DataType ) 9 ) ); + attributes.push_back( QgsPointCloudAttribute( "Z", ( QgsPointCloudAttribute::DataType ) 9 ) ); + attributes.push_back( QgsPointCloudAttribute( "Classification", ( QgsPointCloudAttribute::DataType ) 0 ) ); + attributes.push_back( QgsPointCloudAttribute( "Intensity", ( QgsPointCloudAttribute::DataType ) 3 ) ); + attributes.push_back( QgsPointCloudAttribute( "ReturnNumber", ( QgsPointCloudAttribute::DataType ) 0 ) ); + attributes.push_back( QgsPointCloudAttribute( "NumberOfReturns", ( QgsPointCloudAttribute::DataType ) 0 ) ); + attributes.push_back( QgsPointCloudAttribute( "ScanDirectionFlag", ( QgsPointCloudAttribute::DataType ) 0 ) ); + attributes.push_back( QgsPointCloudAttribute( "EdgeOfFlightLine", ( QgsPointCloudAttribute::DataType ) 0 ) ); + attributes.push_back( QgsPointCloudAttribute( "ScanAngleRank", ( QgsPointCloudAttribute::DataType ) 8 ) ); + attributes.push_back( QgsPointCloudAttribute( "UserData", ( QgsPointCloudAttribute::DataType ) 0 ) ); + attributes.push_back( QgsPointCloudAttribute( "PointSourceId", ( QgsPointCloudAttribute::DataType ) 3 ) ); + attributes.push_back( QgsPointCloudAttribute( "GpsTime", ( QgsPointCloudAttribute::DataType ) 9 ) ); + + switch ( f.header().point_format_id ) + { + case 6: + break; + case 7: + attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Red" ), QgsPointCloudAttribute::UShort ) ); + attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Green" ), QgsPointCloudAttribute::UShort ) ); + attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Blue" ), QgsPointCloudAttribute::UShort ) ); + break; + case 8: + attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Red" ), QgsPointCloudAttribute::UShort ) ); + attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Green" ), QgsPointCloudAttribute::UShort ) ); + attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Blue" ), QgsPointCloudAttribute::UShort ) ); + attributes.push_back( QgsPointCloudAttribute( QStringLiteral( "NIR" ), QgsPointCloudAttribute::UShort ) ); + break; + default: + return false; + } + + // TODO: add extrabyte attributes + + setAttributes( attributes ); + + std::vector copcInfoVlrData = f.vlrData( "copc", 1 ); + + lazperf::copc_info_vlr copcInfoVlr; + copcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() ); + + const double xmin = copcInfoVlr.center_x - copcInfoVlr.halfsize; + const double ymin = copcInfoVlr.center_y - copcInfoVlr.halfsize; + const double zmin = copcInfoVlr.center_z - copcInfoVlr.halfsize; + const double xmax = copcInfoVlr.center_x + copcInfoVlr.halfsize; + const double ymax = copcInfoVlr.center_y + copcInfoVlr.halfsize; + const double zmax = copcInfoVlr.center_z + copcInfoVlr.halfsize; + + mRootBounds = QgsPointCloudDataBounds( + ( xmin - mOffset.x() ) / mScale.x(), + ( ymin - mOffset.y() ) / mScale.y(), + ( zmin - mOffset.z() ) / mScale.z(), + ( xmax - mOffset.x() ) / mScale.x(), + ( ymax - mOffset.y() ) / mScale.y(), + ( zmax - mOffset.z() ) / mScale.z() + ); + + double calculatedSpan = nodeMapExtent( root() ).width() / copcInfoVlr.spacing; + mSpan = calculatedSpan; + +#ifdef QGIS_DEBUG + double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin; + QgsDebugMsgLevel( QStringLiteral( "lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 ); // all dims should be the same + QgsDebugMsgLevel( QStringLiteral( "res at lvl0 %1" ).arg( dx / mSpan ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 ); +#endif + + return true; +} + +QgsPointCloudBlock *QgsCopcPointCloudIndex::nodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request ) +{ + mHierarchyMutex.lock(); + const bool found = mHierarchy.contains( n ); + int pointCount = mHierarchy[n]; + uint64_t blockOffset = mHierarchyNodeOffset[n]; + int32_t blockSize = mHierarchyNodeByteSize[n]; + mHierarchyMutex.unlock(); + if ( !found ) + return nullptr; + + // we need to create a copy of the expression to pass to the decoder + // as the same QgsPointCloudExpression object mighgt be concurrently + // used on another thread, for example in a 3d view + QgsPointCloudExpression filterExpression = mFilterExpression; + QgsPointCloudAttributeCollection requestAttributes = request.attributes(); + requestAttributes.extend( attributes(), filterExpression.referencedAttributes() ); + + return QgsEptDecoder::decompressCopc( mFileName, blockOffset, blockSize, pointCount, attributes(), requestAttributes, scale(), offset(), filterExpression ); +} + +QgsPointCloudBlockRequest *QgsCopcPointCloudIndex::asyncNodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request ) +{ + Q_UNUSED( n ); + Q_UNUSED( request ); + Q_ASSERT( false ); + return nullptr; // unsupported +} + +QgsCoordinateReferenceSystem QgsCopcPointCloudIndex::crs() const +{ + return QgsCoordinateReferenceSystem::fromWkt( mWkt ); +} + +qint64 QgsCopcPointCloudIndex::pointCount() const +{ + return mPointCount; +} + +QVariant QgsCopcPointCloudIndex::metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const +{ + if ( !mMetadataStats.contains( attribute ) ) + return QVariant(); + + const AttributeStatistics &stats = mMetadataStats[ attribute ]; + switch ( statistic ) + { + case QgsStatisticalSummary::Count: + return stats.count >= 0 ? QVariant( stats.count ) : QVariant(); + + case QgsStatisticalSummary::Mean: + return std::isnan( stats.mean ) ? QVariant() : QVariant( stats.mean ); + + case QgsStatisticalSummary::StDev: + return std::isnan( stats.stDev ) ? QVariant() : QVariant( stats.stDev ); + + case QgsStatisticalSummary::Min: + return stats.minimum; + + case QgsStatisticalSummary::Max: + return stats.maximum; + + case QgsStatisticalSummary::Range: + return stats.minimum.isValid() && stats.maximum.isValid() ? QVariant( stats.maximum.toDouble() - stats.minimum.toDouble() ) : QVariant(); + + case QgsStatisticalSummary::CountMissing: + case QgsStatisticalSummary::Sum: + case QgsStatisticalSummary::Median: + case QgsStatisticalSummary::StDevSample: + case QgsStatisticalSummary::Minority: + case QgsStatisticalSummary::Majority: + case QgsStatisticalSummary::Variety: + case QgsStatisticalSummary::FirstQuartile: + case QgsStatisticalSummary::ThirdQuartile: + case QgsStatisticalSummary::InterQuartileRange: + case QgsStatisticalSummary::First: + case QgsStatisticalSummary::Last: + case QgsStatisticalSummary::All: + return QVariant(); + } + return QVariant(); +} + +QVariantList QgsCopcPointCloudIndex::metadataClasses( const QString &attribute ) const +{ + QVariantList classes; + const QMap< int, int > values = mAttributeClasses.value( attribute ); + for ( auto it = values.constBegin(); it != values.constEnd(); ++it ) + { + classes << it.key(); + } + return classes; +} + +QVariant QgsCopcPointCloudIndex::metadataClassStatistic( const QString &attribute, const QVariant &value, QgsStatisticalSummary::Statistic statistic ) const +{ + if ( statistic != QgsStatisticalSummary::Count ) + return QVariant(); + + const QMap< int, int > values = mAttributeClasses.value( attribute ); + if ( !values.contains( value.toInt() ) ) + return QVariant(); + return values.value( value.toInt() ); +} + +bool QgsCopcPointCloudIndex::loadHierarchy( const QString &filename ) +{ + std::ifstream file( filename.toStdString() ); + lazperf::reader::generic_file f( file ); + + std::vector copcInfoVlrData = f.vlrData( "copc", 1 ); + + lazperf::copc_info_vlr copcInfoVlr; + copcInfoVlr.fill( copcInfoVlrData.data(), copcInfoVlrData.size() ); + + QQueue> queue; + queue.push_back( std::make_pair( copcInfoVlr.root_hier_offset, copcInfoVlr.root_hier_size ) ); + + struct CopcVoxelKey + { + int32_t level; + int32_t x; + int32_t y; + int32_t z; + }; + + struct CopcEntry + { + CopcVoxelKey key; + uint64_t offset; + int32_t byteSize; + int32_t pointCount; + }; + + while ( !queue.isEmpty() ) + { + auto [offset, size] = queue.dequeue(); + + file.seekg( offset ); + std::unique_ptr data( new char[ offset ] ); + file.read( data.get(), size ); + + for ( uint64_t i = 0; i < size; i += sizeof( CopcEntry ) ) + { + CopcEntry *entry = reinterpret_cast( data.get() + i ); + if ( entry->pointCount < 0 ) + { + queue.enqueue( std::make_pair( entry->offset, entry->byteSize ) ); + } + else if ( entry->pointCount > 0 ) + { + const IndexedPointCloudNode nodeId( entry->key.level, entry->key.x, entry->key.y, entry->key.z ); + mHierarchyMutex.lock(); + mHierarchy[nodeId] = entry->pointCount; + mHierarchyNodeOffset[nodeId] = entry->offset; + mHierarchyNodeByteSize[nodeId] = entry->byteSize; + mHierarchyMutex.unlock(); + } + } + } + return true; +} + +bool QgsCopcPointCloudIndex::isValid() const +{ + return mIsValid; +} + +///@endcond diff --git a/src/core/pointcloud/qgscopcpointcloudindex.h b/src/core/pointcloud/qgscopcpointcloudindex.h new file mode 100644 index 00000000000..5bc18c4d383 --- /dev/null +++ b/src/core/pointcloud/qgscopcpointcloudindex.h @@ -0,0 +1,93 @@ +/*************************************************************************** + qgscopcpointcloudindex.h + -------------------- + begin : March 2022 + copyright : (C) 2022 by Belgacem Nedjima + email : belgacem dot nedjima at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSCOPCPOINTCLOUDINDEX_H +#define QGSCOPCPOINTCLOUDINDEX_H + +#include +#include +#include +#include +#include +#include +#include + +#include "qgspointcloudindex.h" +#include "qgspointcloudattribute.h" +#include "qgsstatisticalsummary.h" +#include "qgis_sip.h" + +///@cond PRIVATE +#define SIP_NO_FILE + +class QgsCoordinateReferenceSystem; + +class CORE_EXPORT QgsCopcPointCloudIndex: public QgsPointCloudIndex +{ + Q_OBJECT + public: + + explicit QgsCopcPointCloudIndex(); + ~QgsCopcPointCloudIndex(); + + void load( const QString &fileName ) override; + + QgsPointCloudBlock *nodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request ) override; + QgsPointCloudBlockRequest *asyncNodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request ) override; + + QgsCoordinateReferenceSystem crs() const override; + qint64 pointCount() const override; + QVariant metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const override; + QVariantList metadataClasses( const QString &attribute ) const override; + QVariant metadataClassStatistic( const QString &attribute, const QVariant &value, QgsStatisticalSummary::Statistic statistic ) const override; + QVariantMap originalMetadata() const override { return mOriginalMetadata; } + + bool isValid() const override; + QgsPointCloudIndex::AccessType accessType() const override { return QgsPointCloudIndex::Local; }; + + protected: + bool loadSchema( const QString &filename ); + bool loadHierarchy( const QString &filename ); + + bool mIsValid = false; + QString mDataType; + QString mFileName; + QString mWkt; + + qint64 mPointCount = 0; + + mutable QHash mHierarchyNodeOffset; //!< Additional data hierarchy for COPC + mutable QHash mHierarchyNodeByteSize; //!< Additional data hierarchy for COPC + + struct AttributeStatistics + { + int count = -1; + QVariant minimum; + QVariant maximum; + double mean = std::numeric_limits< double >::quiet_NaN(); + double stDev = std::numeric_limits< double >::quiet_NaN(); + double variance = std::numeric_limits< double >::quiet_NaN(); + }; + + QMap< QString, AttributeStatistics > mMetadataStats; + + QMap< QString, QMap< int, int > > mAttributeClasses; + QVariantMap mOriginalMetadata; +}; + +///@endcond +#endif // QGSCOPCPOINTCLOUDINDEX_H diff --git a/src/core/pointcloud/qgseptdecoder.cpp b/src/core/pointcloud/qgseptdecoder.cpp index 4c44924a551..b0a659fed77 100644 --- a/src/core/pointcloud/qgseptdecoder.cpp +++ b/src/core/pointcloud/qgseptdecoder.cpp @@ -720,4 +720,303 @@ QgsPointCloudBlock *QgsEptDecoder::decompressLaz( const QByteArray &byteArrayDat return __decompressLaz( file, attributes, requestedAttributes, scale, offset, filterExpression ); } + +QgsPointCloudBlock *QgsEptDecoder::decompressCopc( const QString &filename, uint64_t blockOffset, uint64_t blockSize, int32_t pointCount, const QgsPointCloudAttributeCollection &attributes, const QgsPointCloudAttributeCollection &requestedAttributes, const QgsVector3D &_scale, const QgsVector3D &_offset, QgsPointCloudExpression &filterExpression ) +{ + Q_UNUSED( attributes ); + Q_UNUSED( _scale ); + Q_UNUSED( _offset ); + std::ifstream file( filename.toStdString(), std::ios::binary ); + lazperf::reader::generic_file f( file ); + std::unique_ptr data( new char[ blockSize ] ); + file.seekg( blockOffset ); + file.read( data.get(), blockSize ); + std::unique_ptr decodedData( new char[ f.header().point_record_length ] ); + + lazperf::reader::chunk_decompressor decompressor( f.header().pointFormat(), f.header().ebCount(), data.get() ); + + const QgsVector3D hScale( f.header().scale.x, f.header().scale.y, f.header().scale.z ); + const QgsVector3D hOffset( f.header().offset.x, f.header().offset.y, f.header().offset.z ); + + const size_t requestedPointRecordSize = requestedAttributes.pointRecordSize(); + QByteArray blockData; + blockData.resize( requestedPointRecordSize * pointCount ); + char *dataBuffer = blockData.data(); + + const QVector requestedAttributesVector = requestedAttributes.attributes(); + + std::size_t outputOffset = 0; + + enum class LazAttribute + { + X, + Y, + Z, + Classification, + Intensity, + ReturnNumber, + NumberOfReturns, + ScanDirectionFlag, + EdgeOfFlightLine, + ScanAngleRank, + UserData, + PointSourceId, + GpsTime, + Red, + Green, + Blue, + ExtraBytes, + MissingOrUnknown + }; + + struct RequestedAttributeDetails + { + RequestedAttributeDetails( LazAttribute attribute, QgsPointCloudAttribute::DataType type, int size, int offset = -1 ) + : attribute( attribute ) + , type( type ) + , size( size ) + , offset( offset ) + {} + + LazAttribute attribute; + QgsPointCloudAttribute::DataType type; + int size; + int offset; // Used in case the attribute is an extra byte attribute + }; + + QVector extrabytesAttr = QgsEptDecoder::readExtraByteAttributes( file ); + + std::vector< RequestedAttributeDetails > requestedAttributeDetails; + requestedAttributeDetails.reserve( requestedAttributesVector.size() ); + for ( const QgsPointCloudAttribute &requestedAttribute : requestedAttributesVector ) + { + if ( requestedAttribute.name().compare( QLatin1String( "X" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::X, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "Y" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::Y, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "Z" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::Z, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "Classification" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::Classification, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "Intensity" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::Intensity, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "ReturnNumber" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::ReturnNumber, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "NumberOfReturns" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::NumberOfReturns, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "ScanDirectionFlag" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::ScanDirectionFlag, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "EdgeOfFlightLine" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::EdgeOfFlightLine, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "ScanAngleRank" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::ScanAngleRank, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "UserData" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::UserData, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "PointSourceId" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::PointSourceId, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "GpsTime" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::GpsTime, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "Red" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::Red, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "Green" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::Green, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else if ( requestedAttribute.name().compare( QLatin1String( "Blue" ), Qt::CaseInsensitive ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::Blue, requestedAttribute.type(), requestedAttribute.size() ) ); + } + else + { + bool foundAttr = false; + for ( QgsEptDecoder::ExtraBytesAttributeDetails &eba : extrabytesAttr ) + { + if ( requestedAttribute.name().compare( eba.attribute.trimmed() ) == 0 ) + { + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::ExtraBytes, eba.type, eba.size, eba.offset ) ); + foundAttr = true; + break; + } + } + if ( !foundAttr ) + { + // this can possibly happen -- e.g. if a style built using a different point cloud format references an attribute which isn't available from the laz file + requestedAttributeDetails.emplace_back( RequestedAttributeDetails( LazAttribute::MissingOrUnknown, requestedAttribute.type(), requestedAttribute.size() ) ); + } + } + } + + std::unique_ptr< QgsPointCloudBlock > block = std::make_unique< QgsPointCloudBlock >( + pointCount, + requestedAttributes, + blockData, hScale, hOffset + ); + + int skippedPoints = 0; + const bool filterIsValid = filterExpression.isValid(); + if ( !filterExpression.prepare( block.get() ) && filterIsValid ) + { + // skip processing if the expression cannot be prepared + block->setPointCount( 0 ); + return block.release(); + } + + lazperf::las::point14 p; + lazperf::las::gpstime gps; + lazperf::las::rgb rgb; + + for ( int i = 0 ; i < pointCount; ++i ) + { + decompressor.decompress( decodedData.get() ); + char *buf = decodedData.get(); + p.unpack( buf ); + gps.unpack( buf + sizeof( lazperf::las::point14 ) ); + rgb.unpack( buf + sizeof( lazperf::las::point14 ) + sizeof( lazperf::las::gpstime ) ); + + for ( const RequestedAttributeDetails &requestedAttribute : requestedAttributeDetails ) + { + switch ( requestedAttribute.attribute ) + { + case LazAttribute::X: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.x() ); + break; + case LazAttribute::Y: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.y() ); + break; + case LazAttribute::Z: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.z() ); + break; + case LazAttribute::Classification: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.classification() ); + break; + case LazAttribute::Intensity: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.intensity() ); + break; + case LazAttribute::ReturnNumber: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.returnNum() ); + break; + case LazAttribute::NumberOfReturns: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.numReturns() ); + break; + case LazAttribute::ScanDirectionFlag: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.scanDirFlag() ); + break; + case LazAttribute::EdgeOfFlightLine: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.eofFlag() ); + break; + case LazAttribute::ScanAngleRank: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.scanAngle() ); + break; + case LazAttribute::UserData: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.userData() ); + break; + case LazAttribute::PointSourceId: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, p.pointSourceID() ); + break; + case LazAttribute::GpsTime: + // lazperf internally stores gps value as int64 field, but in fact it is a double value + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, + *reinterpret_cast( reinterpret_cast( &gps.value ) ) ); + break; + case LazAttribute::Red: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, rgb.r ); + break; + case LazAttribute::Green: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, rgb.g ); + break; + case LazAttribute::Blue: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, rgb.b ); + break; + case LazAttribute::ExtraBytes: + { + switch ( requestedAttribute.type ) + { + case QgsPointCloudAttribute::Char: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + case QgsPointCloudAttribute::UChar: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + case QgsPointCloudAttribute::Short: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + case QgsPointCloudAttribute::UShort: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + case QgsPointCloudAttribute::Int32: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + case QgsPointCloudAttribute::UInt32: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + case QgsPointCloudAttribute::Int64: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + case QgsPointCloudAttribute::UInt64: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + case QgsPointCloudAttribute::Float: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + case QgsPointCloudAttribute::Double: + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, *reinterpret_cast( &buf[requestedAttribute.offset] ) ); + break; + } + } + break; + case LazAttribute::MissingOrUnknown: + // just store 0 for unknown/missing attributes + _storeToStream( dataBuffer, outputOffset, requestedAttribute.type, 0 ); + break; + } + + outputOffset += requestedAttribute.size; + } + + // check if point needs to be filtered out + if ( filterIsValid ) + { + // we're always evaluating the last written point in the buffer + double eval = filterExpression.evaluate( i - skippedPoints ); + if ( !eval || std::isnan( eval ) ) + { + // if the point is filtered out, rewind the offset so the next point is written over it + outputOffset -= requestedPointRecordSize; + ++skippedPoints; + } + } + } + + block->setPointCount( pointCount - skippedPoints ); + return block.release(); +} + ///@endcond diff --git a/src/core/pointcloud/qgseptdecoder.h b/src/core/pointcloud/qgseptdecoder.h index 0d253be67eb..564cc771a0b 100644 --- a/src/core/pointcloud/qgseptdecoder.h +++ b/src/core/pointcloud/qgseptdecoder.h @@ -50,6 +50,7 @@ namespace QgsEptDecoder QgsPointCloudBlock *decompressZStandard( const QByteArray &data, const QgsPointCloudAttributeCollection &attributes, const QgsPointCloudAttributeCollection &requestedAttributes, const QgsVector3D &scale, const QgsVector3D &offset, QgsPointCloudExpression &filterExpression ); QgsPointCloudBlock *decompressLaz( const QString &filename, const QgsPointCloudAttributeCollection &attributes, const QgsPointCloudAttributeCollection &requestedAttributes, const QgsVector3D &scale, const QgsVector3D &offset, QgsPointCloudExpression &filterExpression ); QgsPointCloudBlock *decompressLaz( const QByteArray &data, const QgsPointCloudAttributeCollection &attributes, const QgsPointCloudAttributeCollection &requestedAttributes, const QgsVector3D &scale, const QgsVector3D &offset, QgsPointCloudExpression &filterExpression ); + QgsPointCloudBlock *decompressCopc( const QString &filename, uint64_t blockOffset, uint64_t blockSize, int32_t pointCount, const QgsPointCloudAttributeCollection &attributes, const QgsPointCloudAttributeCollection &requestedAttributes, const QgsVector3D &_scale, const QgsVector3D &_offset, QgsPointCloudExpression &filterExpression ); //! Returns the list of extrabytes attributes with their type, size and offsets represented in the LAS file template diff --git a/src/core/pointcloud/qgspointcloudindex.h b/src/core/pointcloud/qgspointcloudindex.h index 1cb2f9b094c..ee0d30aa45d 100644 --- a/src/core/pointcloud/qgspointcloudindex.h +++ b/src/core/pointcloud/qgspointcloudindex.h @@ -262,7 +262,7 @@ class CORE_EXPORT QgsPointCloudIndex: public QObject QgsDoubleRange nodeZRange( const IndexedPointCloudNode &node ) const; //! Returns node's error in map units (used to determine in whether the node has enough detail for the current view) - float nodeError( const IndexedPointCloudNode &n ) const; + virtual float nodeError( const IndexedPointCloudNode &n ) const; //! Returns scale QgsVector3D scale() const; diff --git a/src/core/providers/copc/qgscopcprovider.cpp b/src/core/providers/copc/qgscopcprovider.cpp new file mode 100644 index 00000000000..ed9800796e9 --- /dev/null +++ b/src/core/providers/copc/qgscopcprovider.cpp @@ -0,0 +1,241 @@ +/*************************************************************************** + qgscopcdataprovider.cpp + ----------------------- + begin : March 2022 + copyright : (C) 2022 by Belgacem Nedjima + email : belgacem dot nedjima at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgis.h" +#include "qgslogger.h" +#include "qgsproviderregistry.h" +#include "qgscopcprovider.h" +#include "qgscopcpointcloudindex.h" +#include "qgsruntimeprofiler.h" +#include "qgsapplication.h" +#include "qgsprovidersublayerdetails.h" +#include "qgsproviderutils.h" + +#include + +///@cond PRIVATE + +#define PROVIDER_KEY QStringLiteral( "copc" ) +#define PROVIDER_DESCRIPTION QStringLiteral( "COPC point cloud data provider" ) + +QgsCopcProvider::QgsCopcProvider( + const QString &uri, + const QgsDataProvider::ProviderOptions &options, + QgsDataProvider::ReadFlags flags ) + : QgsPointCloudDataProvider( uri, options, flags ) +{ + if ( uri.startsWith( QStringLiteral( "http" ), Qt::CaseSensitivity::CaseInsensitive ) ) + mIndex.reset( nullptr ); + else + mIndex.reset( new QgsCopcPointCloudIndex ); + + std::unique_ptr< QgsScopedRuntimeProfile > profile; + if ( QgsApplication::profiler()->groupIsActive( QStringLiteral( "projectload" ) ) ) + profile = std::make_unique< QgsScopedRuntimeProfile >( tr( "Open data source" ), QStringLiteral( "projectload" ) ); + + loadIndex( ); +} + +QgsCopcProvider::~QgsCopcProvider() = default; + +QgsCoordinateReferenceSystem QgsCopcProvider::crs() const +{ + return mIndex->crs(); +} + +QgsRectangle QgsCopcProvider::extent() const +{ + return mIndex->extent(); +} + +QgsPointCloudAttributeCollection QgsCopcProvider::attributes() const +{ + return mIndex->attributes(); +} + +bool QgsCopcProvider::isValid() const +{ + if ( !mIndex.get() ) + { + return false; + } + return mIndex->isValid(); +} + +QString QgsCopcProvider::name() const +{ + return QStringLiteral( "copc" ); +} + +QString QgsCopcProvider::description() const +{ + return QStringLiteral( "Point Clouds COPC" ); +} + +QgsPointCloudIndex *QgsCopcProvider::index() const +{ + return mIndex.get(); +} + +qint64 QgsCopcProvider::pointCount() const +{ + return mIndex->pointCount(); +} + +QVariantList QgsCopcProvider::metadataClasses( const QString &attribute ) const +{ + return mIndex->metadataClasses( attribute ); +} + +QVariant QgsCopcProvider::metadataClassStatistic( const QString &attribute, const QVariant &value, QgsStatisticalSummary::Statistic statistic ) const +{ + return mIndex->metadataClassStatistic( attribute, value, statistic ); +} + +void QgsCopcProvider::loadIndex( ) +{ + if ( mIndex->isValid() ) + return; + + mIndex->load( dataSourceUri() ); + + // TODO: Read metadata from LAZ into mOriginalMetadata + // NOTE: This is not done in EPT provider either +} + +QVariantMap QgsCopcProvider::originalMetadata() const +{ + return mOriginalMetadata; +} + +void QgsCopcProvider::generateIndex() +{ + //no-op, index is always generated +} + +QVariant QgsCopcProvider::metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const +{ + return mIndex->metadataStatistic( attribute, statistic ); +} + +QgsCopcProviderMetadata::QgsCopcProviderMetadata(): + QgsProviderMetadata( PROVIDER_KEY, PROVIDER_DESCRIPTION ) +{ +} + +QgsCopcProvider *QgsCopcProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags ) +{ + return new QgsCopcProvider( uri, options, flags ); +} + +QList QgsCopcProviderMetadata::querySublayers( const QString &uri, Qgis::SublayerQueryFlags, QgsFeedback * ) const +{ + const QVariantMap parts = decodeUri( uri ); + if ( parts.value( QStringLiteral( "isCopc" ), false ).toBool() ) + { + QgsProviderSublayerDetails details; + details.setUri( uri ); + details.setProviderKey( QStringLiteral( "copc" ) ); + details.setType( QgsMapLayerType::PointCloudLayer ); + details.setName( QgsProviderUtils::suggestLayerNameFromFilePath( uri ) ); + return {details}; + } + else + { + return {}; + } +} + +int QgsCopcProviderMetadata::priorityForUri( const QString &uri ) const +{ + const QVariantMap parts = decodeUri( uri ); + const QFileInfo fi( parts.value( QStringLiteral( "path" ) ).toString() ); + if ( fi.exists() && parts.value( QStringLiteral( "isCopc" ), false ).toBool() ) + return 100; + + return 0; +} + +QList QgsCopcProviderMetadata::validLayerTypesForUri( const QString &uri ) const +{ + const QVariantMap parts = decodeUri( uri ); + const QFileInfo fi( parts.value( QStringLiteral( "path" ) ).toString() ); + if ( fi.exists() && parts.value( QStringLiteral( "isCopc" ), false ).toBool() ) + return QList< QgsMapLayerType>() << QgsMapLayerType::PointCloudLayer; + + return QList< QgsMapLayerType>(); +} + +bool QgsCopcProviderMetadata::uriIsBlocklisted( const QString &uri ) const +{ + const QVariantMap parts = decodeUri( uri ); + if ( !parts.contains( QStringLiteral( "path" ) ) ) + return false; + + const QFileInfo fi( parts.value( QStringLiteral( "path" ) ).toString() ); + + // internal details only + if ( fi.exists() && parts.value( QStringLiteral( "isCopc" ), false ).toBool() ) + return true; + + return false; +} + +QVariantMap QgsCopcProviderMetadata::decodeUri( const QString &uri ) const +{ + const QString path = uri; + QVariantMap uriComponents; + uriComponents.insert( QStringLiteral( "path" ), path ); + uriComponents.insert( QStringLiteral( "isCopc" ), uri.endsWith( ".copc.laz" ) ); + return uriComponents; +} + +QString QgsCopcProviderMetadata::filters( QgsProviderMetadata::FilterType type ) +{ + switch ( type ) + { + case QgsProviderMetadata::FilterType::FilterVector: + case QgsProviderMetadata::FilterType::FilterRaster: + case QgsProviderMetadata::FilterType::FilterMesh: + case QgsProviderMetadata::FilterType::FilterMeshDataset: + return QString(); + + case QgsProviderMetadata::FilterType::FilterPointCloud: + return QObject::tr( "COPC Point Clouds" ) + QStringLiteral( "COPC LAZ files (*.copc.laz *.COPC.LAZ)" ); + } + return QString(); +} + +QgsProviderMetadata::ProviderCapabilities QgsCopcProviderMetadata::providerCapabilities() const +{ + return FileBasedUris; +} + +QString QgsCopcProviderMetadata::encodeUri( const QVariantMap &parts ) const +{ + const QString path = parts.value( QStringLiteral( "path" ) ).toString(); + return path; +} + +QgsProviderMetadata::ProviderMetadataCapabilities QgsCopcProviderMetadata::capabilities() const +{ + return ProviderMetadataCapability::LayerTypesForUri + | ProviderMetadataCapability::PriorityForUri + | ProviderMetadataCapability::QuerySublayers; +} +///@endcond + diff --git a/src/core/providers/copc/qgscopcprovider.h b/src/core/providers/copc/qgscopcprovider.h new file mode 100644 index 00000000000..04a4676e198 --- /dev/null +++ b/src/core/providers/copc/qgscopcprovider.h @@ -0,0 +1,88 @@ +/*************************************************************************** + qgscopcdataprovider.h + --------------------- + begin : March 2022 + copyright : (C) 2022 by Belgacem Nedjima + email : belgacem dot nedjima at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSCOPCPROVIDER_H +#define QGSCOPCPROVIDER_H + +#include "qgis_core.h" +#include "qgspointclouddataprovider.h" +#include "qgsprovidermetadata.h" + +#include + +#include "qgis_sip.h" + +///@cond PRIVATE +#define SIP_NO_FILE + +class QgsCopcPointCloudIndex; +class QgsRemoteCopcPointCloudIndex; + +class QgsCopcProvider: public QgsPointCloudDataProvider +{ + Q_OBJECT + public: + QgsCopcProvider( const QString &uri, + const QgsDataProvider::ProviderOptions &providerOptions, + QgsDataProvider::ReadFlags flags = QgsDataProvider::ReadFlags() ); + + ~QgsCopcProvider(); + + QgsCoordinateReferenceSystem crs() const override; + + QgsRectangle extent() const override; + QgsPointCloudAttributeCollection attributes() const override; + bool isValid() const override; + QString name() const override; + QString description() const override; + QgsPointCloudIndex *index() const override; + qint64 pointCount() const override; + QVariant metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const override; + QVariantList metadataClasses( const QString &attribute ) const override; + QVariant metadataClassStatistic( const QString &attribute, const QVariant &value, QgsStatisticalSummary::Statistic statistic ) const override; + QVariantMap originalMetadata() const override; + void loadIndex( ) override; + void generateIndex( ) override; + PointCloudIndexGenerationState indexingState( ) override { return PointCloudIndexGenerationState::Indexed; } + + private: + QVariantMap mOriginalMetadata; + std::unique_ptr mIndex; + + QgsRectangle mExtent; + uint64_t mPointCount; + QgsCoordinateReferenceSystem mCrs; +}; + +class QgsCopcProviderMetadata : public QgsProviderMetadata +{ + public: + QgsCopcProviderMetadata(); + QgsProviderMetadata::ProviderMetadataCapabilities capabilities() const override; + QgsCopcProvider *createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags = QgsDataProvider::ReadFlags() ) override; + QList< QgsProviderSublayerDetails > querySublayers( const QString &uri, Qgis::SublayerQueryFlags flags = Qgis::SublayerQueryFlags(), QgsFeedback *feedback = nullptr ) const override; + int priorityForUri( const QString &uri ) const override; + QList< QgsMapLayerType > validLayerTypesForUri( const QString &uri ) const override; + bool uriIsBlocklisted( const QString &uri ) const override; + QString encodeUri( const QVariantMap &parts ) const override; + QVariantMap decodeUri( const QString &uri ) const override; + QString filters( FilterType type ) override; + ProviderCapabilities providerCapabilities() const override; +}; + +///@endcond +#endif // QGSCOPCPROVIDER_H diff --git a/src/core/providers/qgsproviderregistry.cpp b/src/core/providers/qgsproviderregistry.cpp index 6c9745d672f..271f227aff8 100644 --- a/src/core/providers/qgsproviderregistry.cpp +++ b/src/core/providers/qgsproviderregistry.cpp @@ -39,6 +39,10 @@ #include "providers/ept/qgseptprovider.h" #endif +#ifdef HAVE_COPC +#include "providers/copc/qgscopcprovider.h" +#endif + #include "qgsruntimeprofiler.h" #include "qgsfileutils.h" @@ -197,7 +201,13 @@ void QgsProviderRegistry::init() mProviders[ pc->key() ] = pc; } #endif - +#ifdef HAVE_COPC + { + const QgsScopedRuntimeProfile profile( QObject::tr( "Create COPC point cloud provider" ) ); + QgsProviderMetadata *pc = new QgsCopcProviderMetadata(); + mProviders[ pc->key() ] = pc; + } +#endif registerUnusableUriHandler( new PdalUnusableUriHandlerInterface() ); #ifdef HAVE_STATIC_PROVIDERS diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 172a2b3d513..895105d85ac 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1427,6 +1427,15 @@ if (WITH_EPT) ) endif() +if (WITH_COPC) + set(QGIS_GUI_SRCS ${QGIS_GUI_SRCS} + providers/copc/qgscopcproviderguimetadata.cpp + ) + set(QGIS_GUI_HDRS ${QGIS_GUI_HDRS} + providers/copc/qgscopcproviderguimetadata.h + ) +endif() + # disable deprecation warnings for classes re-exporting deprecated methods if(MSVC) @@ -1512,6 +1521,12 @@ if (WITH_EPT) ) endif() +if (WITH_COPC) + target_include_directories(qgis_gui PUBLIC + ${CMAKE_SOURCE_DIR}/src/gui/providers/copc + ) +endif() + GENERATE_EXPORT_HEADER( qgis_gui diff --git a/src/gui/providers/copc/qgscopcproviderguimetadata.cpp b/src/gui/providers/copc/qgscopcproviderguimetadata.cpp new file mode 100644 index 00000000000..fcc45361c3c --- /dev/null +++ b/src/gui/providers/copc/qgscopcproviderguimetadata.cpp @@ -0,0 +1,28 @@ +/*************************************************************************** + qgscopcproviderguimetadata.cpp + -------------------- + begin : March 2022 + copyright : (C) 2022 by Belgacem Nedjima + email : belgacem dot nedjima at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsapplication.h" +#include "qgscopcproviderguimetadata.h" + +///@cond PRIVATE + +QgsCopcProviderGuiMetadata::QgsCopcProviderGuiMetadata() + : QgsProviderGuiMetadata( QStringLiteral( "copc" ) ) +{ +} + +///@endcond diff --git a/src/gui/providers/copc/qgscopcproviderguimetadata.h b/src/gui/providers/copc/qgscopcproviderguimetadata.h new file mode 100644 index 00000000000..beb29f2cd33 --- /dev/null +++ b/src/gui/providers/copc/qgscopcproviderguimetadata.h @@ -0,0 +1,37 @@ +/*************************************************************************** + qgscopcproviderguimetadata.h + -------------------- + begin : March 2022 + copyright : (C) 2020 by Belgacem Nedjima + email : belgacem dot nedjima at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSCOPCPROVIDERGUIMETADATA_H +#define QGSCOPCPROVIDERGUIMETADATA_H + +///@cond PRIVATE +#define SIP_NO_FILE + +#include +#include + +#include "qgsproviderguimetadata.h" + +class QgsCopcProviderGuiMetadata: public QgsProviderGuiMetadata +{ + public: + QgsCopcProviderGuiMetadata(); +}; + +///@endcond + +#endif // QGSCopcPROVIDERGUIMETADATA_H diff --git a/src/gui/providers/qgspointcloudsourceselect.cpp b/src/gui/providers/qgspointcloudsourceselect.cpp index ed5f7521e32..d36135de40b 100644 --- a/src/gui/providers/qgspointcloudsourceselect.cpp +++ b/src/gui/providers/qgspointcloudsourceselect.cpp @@ -78,6 +78,10 @@ void QgsPointCloudSourceSelect::addButtonClicked() // auto determine preferred provider for each path const QList< QgsProviderRegistry::ProviderCandidateDetails > preferredProviders = QgsProviderRegistry::instance()->preferredProvidersForUri( mPath ); + for ( QgsProviderRegistry::ProviderCandidateDetails p : preferredProviders ) + { + qDebug() << p.metadata()->key(); + } // maybe we should raise an assert if preferredProviders size is 0 or >1? Play it safe for now... if ( preferredProviders.empty() ) continue; diff --git a/src/gui/qgsproviderguiregistry.cpp b/src/gui/qgsproviderguiregistry.cpp index d332d4b33bb..dfc59f95f20 100644 --- a/src/gui/qgsproviderguiregistry.cpp +++ b/src/gui/qgsproviderguiregistry.cpp @@ -33,6 +33,10 @@ #include "qgseptproviderguimetadata.h" #endif +#ifdef HAVE_COPC +#include "qgscopcproviderguimetadata.h" +#endif + #ifdef HAVE_STATIC_PROVIDERS #include "qgswmsprovidergui.h" #include "qgswcsprovidergui.h" @@ -92,6 +96,11 @@ void QgsProviderGuiRegistry::loadStaticProviders( ) mProviders[ ept->key() ] = ept; #endif +#ifdef HAVE_COPC + QgsProviderGuiMetadata *copc = new QgsCopcProviderGuiMetadata(); + mProviders[ ept->key() ] = copc; +#endif + // only show point cloud option if we have at least one point cloud provider available! if ( !QgsProviderRegistry::instance()->filePointCloudFilters().isEmpty() ) { diff --git a/tests/src/providers/CMakeLists.txt b/tests/src/providers/CMakeLists.txt index 2b475c8f517..25ec018a46e 100644 --- a/tests/src/providers/CMakeLists.txt +++ b/tests/src/providers/CMakeLists.txt @@ -58,6 +58,11 @@ if (WITH_EPT) add_qgis_test(testqgseptprovider.cpp MODULE provider LINKEDLIBRARIES qgis_core) endif() +# TODO: test COPC +#if (WITH_COPC) +# add_qgis_test(testqgscopcprovider.cpp MODULE provider LINKEDLIBRARIES qgis_core) +#endif() + if (WITH_PDAL) include_directories( ${CMAKE_SOURCE_DIR}/src/providers/pdal