diff --git a/CMakeLists.txt b/CMakeLists.txt index 202cc9d1edb..92d9c906823 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -411,21 +411,19 @@ if(WITH_CORE) message(STATUS "Qt WebKit support DISABLED.") endif() - if (WITH_EPT) # EPT provider - find_package(ZSTD REQUIRED) # for decompression of point clouds + if (WITH_EPT OR WITH_COPC) find_package(LazPerf) # for decompression of point clouds if (NOT LazPerf_FOUND) message(STATUS "Using embedded laz-perf") endif() + endif() + + if (WITH_EPT) # EPT provider + find_package(ZSTD REQUIRED) # for decompression of point clouds 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() diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index bdae2e38850..931b6c136bb 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1929,43 +1929,13 @@ if (WITH_EPT) ${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/ept/qgseptprovider.cpp - pointcloud/qgseptdecoder.cpp pointcloud/qgseptpointcloudindex.cpp pointcloud/qgsremoteeptpointcloudindex.cpp ) set(QGIS_CORE_HDRS ${QGIS_CORE_HDRS} providers/ept/qgseptprovider.h - pointcloud/qgseptdecoder.h pointcloud/qgseptpointcloudindex.h pointcloud/qgsremoteeptpointcloudindex.h ) @@ -1976,52 +1946,55 @@ 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 (WITH_EPT OR WITH_COPC) + 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} + pointcloud/qgseptdecoder.cpp + ) + set(QGIS_CORE_HDRS ${QGIS_CORE_HDRS} + pointcloud/qgseptdecoder.h + ) +endif() + if (APPLE) # Libtasn1 is for DER-encoded PKI ASN.1 parsing/extracting workarounds include_directories(SYSTEM @@ -2278,15 +2251,9 @@ if (WITH_EPT) target_link_libraries(qgis_core ${ZSTD_LIBRARY} ) - if (LazPerf_FOUND) - target_link_libraries(qgis_core ${LazPerf_LIBRARY}) - endif() endif() -if (WITH_COPC) - target_link_libraries(qgis_core - ${ZSTD_LIBRARY} - ) +if (WITH_EPT OR WITH_COPC) if (LazPerf_FOUND) target_link_libraries(qgis_core ${LazPerf_LIBRARY}) endif() diff --git a/src/core/pointcloud/qgscopcpointcloudindex.cpp b/src/core/pointcloud/qgscopcpointcloudindex.cpp index da68bf57038..aed1f419cf7 100644 --- a/src/core/pointcloud/qgscopcpointcloudindex.cpp +++ b/src/core/pointcloud/qgscopcpointcloudindex.cpp @@ -17,12 +17,6 @@ #include "qgscopcpointcloudindex.h" #include -#include -#include -#include -#include -#include -#include #include #include @@ -100,22 +94,20 @@ bool QgsCopcPointCloudIndex::loadSchema( const QString &filename ) // 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 ) ); + attributes.push_back( QgsPointCloudAttribute( "X", QgsPointCloudAttribute::Int32 ) ); + attributes.push_back( QgsPointCloudAttribute( "Y", QgsPointCloudAttribute::Int32 ) ); + attributes.push_back( QgsPointCloudAttribute( "Z", QgsPointCloudAttribute::Int32 ) ); + attributes.push_back( QgsPointCloudAttribute( "Intensity", QgsPointCloudAttribute::UShort ) ); + attributes.push_back( QgsPointCloudAttribute( "ReturnNumber", QgsPointCloudAttribute::Char ) ); + attributes.push_back( QgsPointCloudAttribute( "NumberOfReturns", QgsPointCloudAttribute::Char ) ); + attributes.push_back( QgsPointCloudAttribute( "ScanDirectionFlag", QgsPointCloudAttribute::Char ) ); + attributes.push_back( QgsPointCloudAttribute( "EdgeOfFlightLine", QgsPointCloudAttribute::Char ) ); + attributes.push_back( QgsPointCloudAttribute( "Classification", QgsPointCloudAttribute::Char ) ); + attributes.push_back( QgsPointCloudAttribute( "ScanAngleRank", QgsPointCloudAttribute::Short ) ); + attributes.push_back( QgsPointCloudAttribute( "UserData", QgsPointCloudAttribute::Char ) ); + attributes.push_back( QgsPointCloudAttribute( "PointSourceId", QgsPointCloudAttribute::UShort ) ); + attributes.push_back( QgsPointCloudAttribute( "GpsTime", QgsPointCloudAttribute::Double ) ); switch ( f.header().point_format_id ) { @@ -136,7 +128,11 @@ bool QgsCopcPointCloudIndex::loadSchema( const QString &filename ) return false; } - // TODO: add extrabyte attributes + QVector extrabyteAttributes = QgsEptDecoder::readExtraByteAttributes( file ); + for ( QgsEptDecoder::ExtraBytesAttributeDetails attr : extrabyteAttributes ) + { + attributes.push_back( QgsPointCloudAttribute( attr.attribute, attr.type ) ); + } setAttributes( attributes ); @@ -216,6 +212,21 @@ qint64 QgsCopcPointCloudIndex::pointCount() const QVariant QgsCopcPointCloudIndex::metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const { + if ( attribute == QStringLiteral( "X" ) && statistic == QgsStatisticalSummary::Min ) + return mExtent.xMinimum(); + if ( attribute == QStringLiteral( "X" ) && statistic == QgsStatisticalSummary::Max ) + return mExtent.xMaximum(); + + if ( attribute == QStringLiteral( "Y" ) && statistic == QgsStatisticalSummary::Min ) + return mExtent.yMinimum(); + if ( attribute == QStringLiteral( "Y" ) && statistic == QgsStatisticalSummary::Max ) + return mExtent.yMaximum(); + + if ( attribute == QStringLiteral( "Z" ) && statistic == QgsStatisticalSummary::Min ) + return mZMin; + if ( attribute == QStringLiteral( "Z" ) && statistic == QgsStatisticalSummary::Max ) + return mZMax; + if ( !mMetadataStats.contains( attribute ) ) return QVariant(); diff --git a/src/core/pointcloud/qgspointcloudindex.h b/src/core/pointcloud/qgspointcloudindex.h index ee0d30aa45d..1cb2f9b094c 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) - virtual float nodeError( const IndexedPointCloudNode &n ) const; + 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 index ed9800796e9..c4ec573b316 100644 --- a/src/core/providers/copc/qgscopcprovider.cpp +++ b/src/core/providers/copc/qgscopcprovider.cpp @@ -145,7 +145,7 @@ QgsCopcProvider *QgsCopcProviderMetadata::createProvider( const QString &uri, co QList QgsCopcProviderMetadata::querySublayers( const QString &uri, Qgis::SublayerQueryFlags, QgsFeedback * ) const { const QVariantMap parts = decodeUri( uri ); - if ( parts.value( QStringLiteral( "isCopc" ), false ).toBool() ) + if ( parts.value( QStringLiteral( "path" ) ).toString().endsWith( ".copc.laz", Qt::CaseSensitivity::CaseInsensitive ) ) { QgsProviderSublayerDetails details; details.setUri( uri ); @@ -164,8 +164,8 @@ 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; + if ( parts.value( QStringLiteral( "path" ) ).toString().endsWith( ".copc.laz", Qt::CaseSensitivity::CaseInsensitive ) ) + return 101; return 0; } @@ -174,7 +174,7 @@ QList QgsCopcProviderMetadata::validLayerTypesForUri( const QSt { const QVariantMap parts = decodeUri( uri ); const QFileInfo fi( parts.value( QStringLiteral( "path" ) ).toString() ); - if ( fi.exists() && parts.value( QStringLiteral( "isCopc" ), false ).toBool() ) + if ( parts.value( QStringLiteral( "path" ) ).toString().endsWith( ".copc.laz", Qt::CaseSensitivity::CaseInsensitive ) ) return QList< QgsMapLayerType>() << QgsMapLayerType::PointCloudLayer; return QList< QgsMapLayerType>(); @@ -189,7 +189,7 @@ bool QgsCopcProviderMetadata::uriIsBlocklisted( const QString &uri ) const const QFileInfo fi( parts.value( QStringLiteral( "path" ) ).toString() ); // internal details only - if ( fi.exists() && parts.value( QStringLiteral( "isCopc" ), false ).toBool() ) + if ( parts.value( QStringLiteral( "path" ) ).toString().endsWith( ".copc.laz", Qt::CaseSensitivity::CaseInsensitive ) ) return true; return false; @@ -200,7 +200,6 @@ 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; } @@ -215,7 +214,7 @@ QString QgsCopcProviderMetadata::filters( QgsProviderMetadata::FilterType type ) return QString(); case QgsProviderMetadata::FilterType::FilterPointCloud: - return QObject::tr( "COPC Point Clouds" ) + QStringLiteral( "COPC LAZ files (*.copc.laz *.COPC.LAZ)" ); + return QObject::tr( "COPC Point Clouds" ) + QStringLiteral( " (*.copc.laz *.COPC.LAZ)" ); } return QString(); } diff --git a/src/gui/providers/qgspointcloudsourceselect.cpp b/src/gui/providers/qgspointcloudsourceselect.cpp index d36135de40b..ed5f7521e32 100644 --- a/src/gui/providers/qgspointcloudsourceselect.cpp +++ b/src/gui/providers/qgspointcloudsourceselect.cpp @@ -78,10 +78,6 @@ 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/tests/src/providers/CMakeLists.txt b/tests/src/providers/CMakeLists.txt index 25ec018a46e..dd09edf34bd 100644 --- a/tests/src/providers/CMakeLists.txt +++ b/tests/src/providers/CMakeLists.txt @@ -58,10 +58,9 @@ 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_COPC) + add_qgis_test(testqgscopcprovider.cpp MODULE provider LINKEDLIBRARIES qgis_core) +endif() if (WITH_PDAL) include_directories( diff --git a/tests/src/providers/testqgscopcprovider.cpp b/tests/src/providers/testqgscopcprovider.cpp new file mode 100644 index 00000000000..1974703cc67 --- /dev/null +++ b/tests/src/providers/testqgscopcprovider.cpp @@ -0,0 +1,627 @@ +/*************************************************************************** + testqgseptprovider.cpp + -------------------------------------- + Date : November 2020 + Copyright : (C) 2020 by Nyall Dawson + Email : nyall dot dawson 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 + +#include "qgstest.h" +#include +#include +#include +#include +#include +#include +#include +#include + +//qgis includes... +#include "qgis.h" +#include "qgsapplication.h" +#include "qgsproviderregistry.h" +#include "qgscopcprovider.h" +#include "qgseptprovider.h" +#include "qgspointcloudlayer.h" +#include "qgspointcloudindex.h" +#include "qgspointcloudlayerelevationproperties.h" +#include "qgsprovidersublayerdetails.h" +#include "qgsgeometry.h" +#include "qgseptdecoder.h" + +/** + * \ingroup UnitTests + * This is a unit test for the COPC provider + */ +class TestQgsCopcProvider : public QObject +{ + Q_OBJECT + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() {}// will be called before each testfunction is executed. + void cleanup() {}// will be called after every testfunction. + + void filters(); + void encodeUri(); + void decodeUri(); + void preferredUri(); + void layerTypesForUri(); + void uriIsBlocklisted(); + void querySublayers(); + void brokenPath(); + void validLayer(); + void validLayerWithCopcHierarchy(); + void attributes(); + void calculateZRange(); + void testIdentify_data(); + void testIdentify(); +// void testExtraBytesAttributesExtraction(); +// void testExtraBytesAttributesValues(); + void testPointCloudIndex(); + + private: + QString mTestDataDir; + QString mReport; +}; + +//runs before all tests +void TestQgsCopcProvider::initTestCase() +{ + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + + mTestDataDir = QStringLiteral( TEST_DATA_DIR ) + '/'; //defined in CmakeLists.txt + mReport = QStringLiteral( "

COPC Provider Tests

\n" ); +} + +//runs after all tests +void TestQgsCopcProvider::cleanupTestCase() +{ + QgsApplication::exitQgis(); + const QString myReportFile = QDir::tempPath() + "/qgistest.html"; + QFile myFile( myReportFile ); + if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) ) + { + QTextStream myQTextStream( &myFile ); + myQTextStream << mReport; + myFile.close(); + } +} + +void TestQgsCopcProvider::filters() +{ + QgsProviderMetadata *metadata = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "copc" ) ); + QVERIFY( metadata ); + + QCOMPARE( metadata->filters( QgsProviderMetadata::FilterType::FilterPointCloud ), QStringLiteral( "COPC Point Clouds (*.copc.laz *.COPC.LAZ)" ) ); + QCOMPARE( metadata->filters( QgsProviderMetadata::FilterType::FilterVector ), QString() ); + + const QString registryPointCloudFilters = QgsProviderRegistry::instance()->filePointCloudFilters(); + QVERIFY( registryPointCloudFilters.contains( "(*.copc.laz *.COPC.LAZ)" ) ); +} + +void TestQgsCopcProvider::encodeUri() +{ + QgsProviderMetadata *metadata = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "copc" ) ); + QVERIFY( metadata ); + + QVariantMap parts; + parts.insert( QStringLiteral( "path" ), QStringLiteral( "/home/point_clouds/dataset.copc.laz" ) ); + QCOMPARE( metadata->encodeUri( parts ), QStringLiteral( "/home/point_clouds/dataset.copc.laz" ) ); +} + +void TestQgsCopcProvider::decodeUri() +{ + QgsProviderMetadata *metadata = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "copc" ) ); + QVERIFY( metadata ); + + const QVariantMap parts = metadata->decodeUri( QStringLiteral( "/home/point_clouds/dataset.copc.laz" ) ); + QCOMPARE( parts.value( QStringLiteral( "path" ) ).toString(), QStringLiteral( "/home/point_clouds/dataset.copc.laz" ) ); +} + +void TestQgsCopcProvider::preferredUri() +{ + QgsProviderMetadata *copcMetadata = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "copc" ) ); + QVERIFY( copcMetadata->capabilities() & QgsProviderMetadata::PriorityForUri ); + + // test that COPC is the preferred provider for .copc.laz uris + QList candidates = QgsProviderRegistry::instance()->preferredProvidersForUri( QStringLiteral( "/home/test/dataset.copc.laz" ) ); + QCOMPARE( candidates.size(), 1 ); + QCOMPARE( candidates.at( 0 ).metadata()->key(), QStringLiteral( "copc" ) ); + QCOMPARE( candidates.at( 0 ).layerTypes(), QList< QgsMapLayerType >() << QgsMapLayerType::PointCloudLayer ); + + candidates = QgsProviderRegistry::instance()->preferredProvidersForUri( QStringLiteral( "/home/test/dataset.COPC.LAZ" ) ); + QCOMPARE( candidates.size(), 1 ); + QCOMPARE( candidates.at( 0 ).metadata()->key(), QStringLiteral( "copc" ) ); + QCOMPARE( candidates.at( 0 ).layerTypes(), QList< QgsMapLayerType >() << QgsMapLayerType::PointCloudLayer ); + + QVERIFY( !QgsProviderRegistry::instance()->shouldDeferUriForOtherProviders( QStringLiteral( "/home/test/dataset.copc.laz" ), QStringLiteral( "copc" ) ) ); + QVERIFY( QgsProviderRegistry::instance()->shouldDeferUriForOtherProviders( QStringLiteral( "/home/test/dataset.copc.laz" ), QStringLiteral( "ogr" ) ) ); +} + +void TestQgsCopcProvider::layerTypesForUri() +{ + QgsProviderMetadata *copcMetadata = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "copc" ) ); + QVERIFY( copcMetadata->capabilities() & QgsProviderMetadata::LayerTypesForUri ); + + QCOMPARE( copcMetadata->validLayerTypesForUri( QStringLiteral( "/home/test/cloud.copc.laz" ) ), QList< QgsMapLayerType >() << QgsMapLayerType::PointCloudLayer ); + QCOMPARE( copcMetadata->validLayerTypesForUri( QStringLiteral( "/home/test/ept.json" ) ), QList< QgsMapLayerType >() ); +} + +void TestQgsCopcProvider::uriIsBlocklisted() +{ + QVERIFY( !QgsProviderRegistry::instance()->uriIsBlocklisted( QStringLiteral( "/home/test/ept.json" ) ) ); + QVERIFY( QgsProviderRegistry::instance()->uriIsBlocklisted( QStringLiteral( "/home/test/dataset.copc.laz" ) ) ); +} + +void TestQgsCopcProvider::querySublayers() +{ + // test querying sub layers for a ept layer + QgsProviderMetadata *eptMetadata = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "copc" ) ); + + // invalid uri + QList< QgsProviderSublayerDetails >res = eptMetadata->querySublayers( QString() ); + QVERIFY( res.empty() ); + + // not a copc layer + res = eptMetadata->querySublayers( QString( TEST_DATA_DIR ) + "/lines.shp" ); + QVERIFY( res.empty() ); + + // valid copc layer + res = eptMetadata->querySublayers( mTestDataDir + "/point_clouds/copc/sunshine-coast.copc.laz" ); + QCOMPARE( res.count(), 1 ); + QCOMPARE( res.at( 0 ).name(), QStringLiteral( "sunshine-coast.copc" ) ); + QCOMPARE( res.at( 0 ).uri(), mTestDataDir + "/point_clouds/copc/sunshine-coast.copc.laz" ); + QCOMPARE( res.at( 0 ).providerKey(), QStringLiteral( "copc" ) ); + QCOMPARE( res.at( 0 ).type(), QgsMapLayerType::PointCloudLayer ); + + // make sure result is valid to load layer from + const QgsProviderSublayerDetails::LayerOptions options{ QgsCoordinateTransformContext() }; + std::unique_ptr< QgsPointCloudLayer > ml( qgis::down_cast< QgsPointCloudLayer * >( res.at( 0 ).toLayer( options ) ) ); + QVERIFY( ml->isValid() ); +} + +void TestQgsCopcProvider::brokenPath() +{ + // test loading a bad layer URI + std::unique_ptr< QgsPointCloudLayer > layer = std::make_unique< QgsPointCloudLayer >( QStringLiteral( "not valid" ), QStringLiteral( "layer" ), QStringLiteral( "copc" ) ); + QVERIFY( !layer->isValid() ); +} + +void TestQgsCopcProvider::validLayer() +{ + std::unique_ptr< QgsPointCloudLayer > layer = std::make_unique< QgsPointCloudLayer >( mTestDataDir + QStringLiteral( "point_clouds/copc/sunshine-coast.copc.laz" ), QStringLiteral( "layer" ), QStringLiteral( "copc" ) ); + QVERIFY( layer->isValid() ); + + QCOMPARE( layer->crs().authid(), QStringLiteral( "EPSG:28356" ) ); + QGSCOMPARENEAR( layer->extent().xMinimum(), 498062.0, 0.1 ); + QGSCOMPARENEAR( layer->extent().yMinimum(), 7050992.84, 0.1 ); + QGSCOMPARENEAR( layer->extent().xMaximum(), 498067.39, 0.1 ); + QGSCOMPARENEAR( layer->extent().yMaximum(), 7050997.04, 0.1 ); + QCOMPARE( layer->dataProvider()->polygonBounds().asWkt( 0 ), QStringLiteral( "Polygon ((498062 7050993, 498067 7050993, 498067 7050997, 498062 7050997, 498062 7050993))" ) ); + QCOMPARE( layer->dataProvider()->pointCount(), 253 ); + QCOMPARE( layer->pointCount(), 253 ); + + QVERIFY( layer->dataProvider()->index() ); + // all hierarchy is stored in a single node + QVERIFY( layer->dataProvider()->index()->hasNode( IndexedPointCloudNode::fromString( "0-0-0-0" ) ) ); + QVERIFY( !layer->dataProvider()->index()->hasNode( IndexedPointCloudNode::fromString( "1-0-0-0" ) ) ); +} + +#include "qgscopcpointcloudindex.h" + +void TestQgsCopcProvider::validLayerWithCopcHierarchy() +{ + std::unique_ptr< QgsPointCloudLayer > layer = std::make_unique< QgsPointCloudLayer >( mTestDataDir + QStringLiteral( "point_clouds/copc/lone-star.copc.laz" ), QStringLiteral( "layer" ), QStringLiteral( "copc" ) ); + QVERIFY( layer->isValid() ); + + QGSCOMPARENEAR( layer->extent().xMinimum(), 515368.6022, 0.1 ); + QGSCOMPARENEAR( layer->extent().yMinimum(), 4918340.364, 0.1 ); + QGSCOMPARENEAR( layer->extent().xMaximum(), 515401.043, 0.1 ); + QGSCOMPARENEAR( layer->extent().yMaximum(), 4918381.124, 0.1 ); + + QVERIFY( layer->dataProvider()->index() ); + // all hierarchy is stored in multiple nodes + QVERIFY( layer->dataProvider()->index()->hasNode( IndexedPointCloudNode::fromString( "1-1-1-0" ) ) ); + QVERIFY( layer->dataProvider()->index()->hasNode( IndexedPointCloudNode::fromString( "2-3-3-1" ) ) ); +} + +void TestQgsCopcProvider::attributes() +{ + std::unique_ptr< QgsPointCloudLayer > layer = std::make_unique< QgsPointCloudLayer >( mTestDataDir + QStringLiteral( "point_clouds/copc/sunshine-coast.copc.laz" ), QStringLiteral( "layer" ), QStringLiteral( "copc" ) ); + QVERIFY( layer->isValid() ); + + const QgsPointCloudAttributeCollection attributes = layer->attributes(); + QCOMPARE( attributes.count(), 16 ); + QCOMPARE( attributes.at( 0 ).name(), QStringLiteral( "X" ) ); + QCOMPARE( attributes.at( 0 ).type(), QgsPointCloudAttribute::Int32 ); + QCOMPARE( attributes.at( 1 ).name(), QStringLiteral( "Y" ) ); + QCOMPARE( attributes.at( 1 ).type(), QgsPointCloudAttribute::Int32 ); + QCOMPARE( attributes.at( 2 ).name(), QStringLiteral( "Z" ) ); + QCOMPARE( attributes.at( 2 ).type(), QgsPointCloudAttribute::Int32 ); + QCOMPARE( attributes.at( 3 ).name(), QStringLiteral( "Intensity" ) ); + QCOMPARE( attributes.at( 3 ).type(), QgsPointCloudAttribute::UShort ); + QCOMPARE( attributes.at( 4 ).name(), QStringLiteral( "ReturnNumber" ) ); + QCOMPARE( attributes.at( 4 ).type(), QgsPointCloudAttribute::Char ); + QCOMPARE( attributes.at( 5 ).name(), QStringLiteral( "NumberOfReturns" ) ); + QCOMPARE( attributes.at( 5 ).type(), QgsPointCloudAttribute::Char ); + QCOMPARE( attributes.at( 6 ).name(), QStringLiteral( "ScanDirectionFlag" ) ); + QCOMPARE( attributes.at( 6 ).type(), QgsPointCloudAttribute::Char ); + QCOMPARE( attributes.at( 7 ).name(), QStringLiteral( "EdgeOfFlightLine" ) ); + QCOMPARE( attributes.at( 7 ).type(), QgsPointCloudAttribute::Char ); + QCOMPARE( attributes.at( 8 ).name(), QStringLiteral( "Classification" ) ); + QCOMPARE( attributes.at( 8 ).type(), QgsPointCloudAttribute::Char ); + QCOMPARE( attributes.at( 9 ).name(), QStringLiteral( "ScanAngleRank" ) ); + QCOMPARE( attributes.at( 9 ).type(), QgsPointCloudAttribute::Short ); + QCOMPARE( attributes.at( 10 ).name(), QStringLiteral( "UserData" ) ); + QCOMPARE( attributes.at( 10 ).type(), QgsPointCloudAttribute::Char ); + QCOMPARE( attributes.at( 11 ).name(), QStringLiteral( "PointSourceId" ) ); + QCOMPARE( attributes.at( 11 ).type(), QgsPointCloudAttribute::UShort ); + QCOMPARE( attributes.at( 12 ).name(), QStringLiteral( "GpsTime" ) ); + QCOMPARE( attributes.at( 12 ).type(), QgsPointCloudAttribute::Double ); + QCOMPARE( attributes.at( 13 ).name(), QStringLiteral( "Red" ) ); + QCOMPARE( attributes.at( 13 ).type(), QgsPointCloudAttribute::UShort ); + QCOMPARE( attributes.at( 14 ).name(), QStringLiteral( "Green" ) ); + QCOMPARE( attributes.at( 14 ).type(), QgsPointCloudAttribute::UShort ); + QCOMPARE( attributes.at( 15 ).name(), QStringLiteral( "Blue" ) ); + QCOMPARE( attributes.at( 15 ).type(), QgsPointCloudAttribute::UShort ); +} + +void TestQgsCopcProvider::calculateZRange() +{ + std::unique_ptr< QgsPointCloudLayer > layer = std::make_unique< QgsPointCloudLayer >( mTestDataDir + QStringLiteral( "point_clouds/copc/sunshine-coast.copc.laz" ), QStringLiteral( "layer" ), QStringLiteral( "copc" ) ); + QVERIFY( layer->isValid() ); + + QgsDoubleRange range = layer->elevationProperties()->calculateZRange( layer.get() ); + QGSCOMPARENEAR( range.lower(), 74.34, 0.01 ); + QGSCOMPARENEAR( range.upper(), 80.02, 0.01 ); + + static_cast< QgsPointCloudLayerElevationProperties * >( layer->elevationProperties() )->setZScale( 2 ); + static_cast< QgsPointCloudLayerElevationProperties * >( layer->elevationProperties() )->setZOffset( 0.5 ); + + range = layer->elevationProperties()->calculateZRange( layer.get() ); + QGSCOMPARENEAR( range.lower(), 149.18, 0.01 ); + QGSCOMPARENEAR( range.upper(), 160.54, 0.01 ); +} + +void TestQgsCopcProvider::testIdentify_data() +{ + QTest::addColumn( "datasetPath" ); + + QTest::newRow( "copc" ) << mTestDataDir + QStringLiteral( "point_clouds/copc/sunshine-coast.copc.laz" ); +} + +void TestQgsCopcProvider::testIdentify() +{ + QFETCH( QString, datasetPath ); + + std::unique_ptr< QgsPointCloudLayer > layer = std::make_unique< QgsPointCloudLayer >( datasetPath, QStringLiteral( "layer" ), QStringLiteral( "copc" ) ); + + QVERIFY( layer->isValid() ); + + // identify 1 point click (rectangular point shape) + { + QgsPolygonXY polygon; + QVector ring; + ring.push_back( QgsPointXY( 498062.50018404237926, 7050996.5845294082537 ) ); + ring.push_back( QgsPointXY( 498062.5405028705718, 7050996.5845294082537 ) ); + ring.push_back( QgsPointXY( 498062.5405028705718, 7050996.6248482363299 ) ); + ring.push_back( QgsPointXY( 498062.50018404237926, 7050996.6248482363299 ) ); + ring.push_back( QgsPointXY( 498062.50018404237926, 7050996.5845294082537 ) ); + polygon.push_back( ring ); + const float maxErrorInMapCoords = 0.0022857920266687870026; + QVector> points = layer->dataProvider()->identify( maxErrorInMapCoords, QgsGeometry::fromPolygonXY( polygon ) ); + QCOMPARE( points.size(), 1 ); + const QMap identifiedPoint = points[0]; + QMap expected; + + expected[ QStringLiteral( "Blue" ) ] = 0; + expected[ QStringLiteral( "Classification" ) ] = 2; + expected[ QStringLiteral( "EdgeOfFlightLine" ) ] = 0; + expected[ QStringLiteral( "GpsTime" ) ] = 268793.37257748609409; + expected[ QStringLiteral( "Green" ) ] = 0; + expected[ QStringLiteral( "Intensity" ) ] = 1765; + expected[ QStringLiteral( "NumberOfReturns" ) ] = 1; + expected[ QStringLiteral( "PointSourceId" ) ] = 7041; + expected[ QStringLiteral( "Red" ) ] = 0; + expected[ QStringLiteral( "ReturnNumber" ) ] = 1; + expected[ QStringLiteral( "ScanAngleRank" ) ] = -59; + expected[ QStringLiteral( "ScanDirectionFlag" ) ] = 1; + expected[ QStringLiteral( "UserData" ) ] = 17; + expected[ QStringLiteral( "X" ) ] = 498062.52; + expected[ QStringLiteral( "Y" ) ] = 7050996.61; + expected[ QStringLiteral( "Z" ) ] = 75.0; + QVERIFY( identifiedPoint == expected ); + } + + // identify 1 point (circular point shape) + { + QPolygonF polygon; + polygon.push_back( QPointF( 498066.28873652569018, 7050994.9709538575262 ) ); + polygon.push_back( QPointF( 498066.21890226693358, 7050995.0112726856023 ) ); + polygon.push_back( QPointF( 498066.21890226693358, 7050995.0919103417546 ) ); + polygon.push_back( QPointF( 498066.28873652569018, 7050995.1322291698307 ) ); + polygon.push_back( QPointF( 498066.35857078444678, 7050995.0919103417546 ) ); + polygon.push_back( QPointF( 498066.35857078444678, 7050995.0112726856023 ) ); + polygon.push_back( QPointF( 498066.28873652569018, 7050994.9709538575262 ) ); + const float maxErrorInMapCoords = 0.0091431681066751480103; + const QVector> identifiedPoints = layer->dataProvider()->identify( maxErrorInMapCoords, QgsGeometry::fromQPolygonF( polygon ) ); + QVector> expected; + { + QMap point; + point[ QStringLiteral( "Blue" ) ] = "0" ; + point[ QStringLiteral( "Classification" ) ] = "2" ; + point[ QStringLiteral( "EdgeOfFlightLine" ) ] = "0" ; + point[ QStringLiteral( "GpsTime" ) ] = "268793.3373408913" ; + point[ QStringLiteral( "Green" ) ] = "0" ; + point[ QStringLiteral( "Intensity" ) ] = "278" ; + point[ QStringLiteral( "NumberOfReturns" ) ] = "1" ; + point[ QStringLiteral( "PointSourceId" ) ] = "7041" ; + point[ QStringLiteral( "Red" ) ] = "0" ; + point[ QStringLiteral( "ReturnNumber" ) ] = "1" ; + point[ QStringLiteral( "ScanAngleRank" ) ] = "-59" ; + point[ QStringLiteral( "ScanDirectionFlag" ) ] = "1" ; + point[ QStringLiteral( "UserData" ) ] = "17" ; + point[ QStringLiteral( "X" ) ] = "498066.27" ; + point[ QStringLiteral( "Y" ) ] = "7050995.06" ; + point[ QStringLiteral( "Z" ) ] = "74.60" ; + expected.push_back( point ); + } + + // compare values using toDouble() so that fuzzy comparison is used in case of + // tiny rounding errors (e.g. 74.6 vs 74.60000000000001) + QCOMPARE( identifiedPoints.count(), 1 ); + const QStringList keys = expected[0].keys(); + for ( const QString &k : keys ) + QCOMPARE( identifiedPoints[0][k].toDouble(), expected[0][k].toDouble() ); + } + + // test rectangle selection + { + QPolygonF polygon; + polygon.push_back( QPointF( 498063.24382022250211, 7050996.8638040581718 ) ); + polygon.push_back( QPointF( 498063.02206666755956, 7050996.8638040581718 ) ); + polygon.push_back( QPointF( 498063.02206666755956, 7050996.6360026793554 ) ); + polygon.push_back( QPointF( 498063.24382022250211, 7050996.6360026793554 ) ); + polygon.push_back( QPointF( 498063.24382022250211, 7050996.8638040581718 ) ); + + const float maxErrorInMapCoords = 0.0022857920266687870026; + const QVector> identifiedPoints = layer->dataProvider()->identify( maxErrorInMapCoords, QgsGeometry::fromQPolygonF( polygon ) ); + QVector> expected; + { + QMap point; + point[ QStringLiteral( "Blue" ) ] = "0" ; + point[ QStringLiteral( "Classification" ) ] = "2" ; + point[ QStringLiteral( "EdgeOfFlightLine" ) ] = "0" ; + point[ QStringLiteral( "GpsTime" ) ] = "268793.3813974548" ; + point[ QStringLiteral( "Green" ) ] = "0" ; + point[ QStringLiteral( "Intensity" ) ] = "1142" ; + point[ QStringLiteral( "NumberOfReturns" ) ] = "1" ; + point[ QStringLiteral( "PointSourceId" ) ] = "7041" ; + point[ QStringLiteral( "Red" ) ] = "0" ; + point[ QStringLiteral( "ReturnNumber" ) ] = "1" ; + point[ QStringLiteral( "ScanAngleRank" ) ] = "-59" ; + point[ QStringLiteral( "ScanDirectionFlag" ) ] = "1" ; + point[ QStringLiteral( "UserData" ) ] = "17" ; + point[ QStringLiteral( "X" ) ] = "498063.14" ; + point[ QStringLiteral( "Y" ) ] = "7050996.79" ; + point[ QStringLiteral( "Z" ) ] = "74.89" ; + expected.push_back( point ); + } + { + QMap point; + point[ QStringLiteral( "Blue" ) ] = "0" ; + point[ QStringLiteral( "Classification" ) ] = "3" ; + point[ QStringLiteral( "EdgeOfFlightLine" ) ] = "0" ; + point[ QStringLiteral( "GpsTime" ) ] = "269160.5176644815" ; + point[ QStringLiteral( "Green" ) ] = "0" ; + point[ QStringLiteral( "Intensity" ) ] = "1631" ; + point[ QStringLiteral( "NumberOfReturns" ) ] = "1" ; + point[ QStringLiteral( "PointSourceId" ) ] = "7042" ; + point[ QStringLiteral( "Red" ) ] = "0" ; + point[ QStringLiteral( "ReturnNumber" ) ] = "1" ; + point[ QStringLiteral( "ScanAngleRank" ) ] = "48" ; + point[ QStringLiteral( "ScanDirectionFlag" ) ] = "1" ; + point[ QStringLiteral( "UserData" ) ] = "17" ; + point[ QStringLiteral( "X" ) ] = "498063.11" ; + point[ QStringLiteral( "Y" ) ] = "7050996.75" ; + point[ QStringLiteral( "Z" ) ] = "74.90" ; + expected.push_back( point ); + } + + QVERIFY( expected.size() == identifiedPoints.size() ); + + const QStringList keys = expected[0].keys(); + for ( int i = 0; i < expected.size(); ++i ) + { + for ( const QString &k : keys ) + QCOMPARE( identifiedPoints[i][k].toDouble(), expected[i][k].toDouble() ); + } + } +} + +// TODO: fix extrabytes tests +//void TestQgsCopcProvider::testExtraBytesAttributesExtraction() +//{ +// { +// QString dataPath = mTestDataDir + QStringLiteral( "point_clouds/copc/extrabytes-dataset.copc.laz" ); +// std::ifstream file( dataPath.toStdString(), std::ios::binary ); +// QVector attributes = QgsEptDecoder::readExtraByteAttributes( file ); +// for ( QgsEptDecoder::ExtraBytesAttributeDetails attr : attributes ) +// { +// qDebug() << attr.attribute << " " << attr.type << " " << attr.size << " " << attr.offset; +// } +// QCOMPARE( attributes.size(), 4 ); + +// QCOMPARE( attributes[0].attribute, QStringLiteral( "Amplitude" ) ); +// QCOMPARE( attributes[1].attribute, QStringLiteral( "Reflectance" ) ); +// QCOMPARE( attributes[2].attribute, QStringLiteral( "ClassFlags" ) ); +// QCOMPARE( attributes[3].attribute, QStringLiteral( "Deviation" ) ); + +// QCOMPARE( attributes[0].type, QgsPointCloudAttribute::Float ); +// QCOMPARE( attributes[1].type, QgsPointCloudAttribute::Float ); +// QCOMPARE( attributes[2].type, QgsPointCloudAttribute::UChar ); +// QCOMPARE( attributes[3].type, QgsPointCloudAttribute::Float ); + +// QCOMPARE( attributes[0].size, 4 ); +// QCOMPARE( attributes[1].size, 4 ); +// QCOMPARE( attributes[2].size, 1 ); +// QCOMPARE( attributes[3].size, 4 ); + +// QCOMPARE( attributes[0].offset, 43 ); +// QCOMPARE( attributes[1].offset, 39 ); +// QCOMPARE( attributes[2].offset, 38 ); +// QCOMPARE( attributes[3].offset, 34 ); +// } + +// { +// QString dataPath = mTestDataDir + QStringLiteral( "point_clouds/copc/no-extrabytes-dataset.copc.laz" ); +// std::ifstream file( dataPath.toStdString(), std::ios::binary ); +// QVector attributes = QgsEptDecoder::readExtraByteAttributes( file ); +// QCOMPARE( attributes.size(), 0 ); +// } +//} + +//void TestQgsCopcProvider::testExtraBytesAttributesValues() +//{ +// QString dataPath = mTestDataDir + QStringLiteral( "point_clouds/copc/extrabytes-dataset.copc.laz" ); +// std::unique_ptr< QgsPointCloudLayer > layer = std::make_unique< QgsPointCloudLayer >( dataPath, QStringLiteral( "layer" ), QStringLiteral( "copc" ) ); +// QVERIFY( layer->isValid() ); +// { +// for ( QgsPointCloudAttribute attr : layer->attributes().attributes() ) +// { +// qDebug() << attr.name() << " " << attr.type(); +// } +// const float maxErrorInMapCoords = 0.0015207174f; +// QPolygonF polygon; +// polygon.push_back( QPointF( 527919.2459517354, 6210983.5918774214 ) ); +// polygon.push_back( QPointF( 527919.0742796324, 6210983.5918774214 ) ); +// polygon.push_back( QPointF( 527919.0742796324, 6210983.4383113598 ) ); +// polygon.push_back( QPointF( 527919.2459517354, 6210983.4383113598 ) ); +// polygon.push_back( QPointF( 527919.2459517354, 6210983.5918774214 ) ); + +// const QVector> identifiedPoints = layer->dataProvider()->identify( maxErrorInMapCoords, QgsGeometry::fromQPolygonF( polygon ) ); + +// QVector> expectedPoints; +// { +// QMap point; +// point[ QStringLiteral( "Amplitude" ) ] = "4.409999847412109" ; +// point[ QStringLiteral( "Blue" ) ] = "0" ; +// point[ QStringLiteral( "ClassFlags" ) ] = "0" ; +// point[ QStringLiteral( "Classification" ) ] = "5" ; +// point[ QStringLiteral( "Deviation" ) ] = "2" ; +// point[ QStringLiteral( "EdgeOfFlightLine" ) ] = "0" ; +// point[ QStringLiteral( "GpsTime" ) ] = "302522582.235838" ; +// point[ QStringLiteral( "Green" ) ] = "0" ; +// point[ QStringLiteral( "Intensity" ) ] = "441" ; +// point[ QStringLiteral( "NumberOfReturns" ) ] = "3" ; +// point[ QStringLiteral( "PointSourceId" ) ] = "15017" ; +// point[ QStringLiteral( "Red" ) ] = "0" ; +// point[ QStringLiteral( "Reflectance" ) ] = "-17.829999923706055" ; +// point[ QStringLiteral( "ReturnNumber" ) ] = "2" ; +// point[ QStringLiteral( "ScanAngleRank" ) ] = "-6" ; +// point[ QStringLiteral( "ScanDirectionFlag" ) ] = "0" ; +// point[ QStringLiteral( "UserData" ) ] = "0" ; +// point[ QStringLiteral( "X" ) ] = "527919.18" ; +// point[ QStringLiteral( "Y" ) ] = "6210983.47" ; +// point[ QStringLiteral( "Z" ) ] = "149.341" ; +// expectedPoints.push_back( point ); +// } +// { +// QMap point; +// point[ QStringLiteral( "Amplitude" ) ] = "14.170000076293945" ; +// point[ QStringLiteral( "Blue" ) ] = "0" ; +// point[ QStringLiteral( "ClassFlags" ) ] = "0" ; +// point[ QStringLiteral( "Classification" ) ] = "2" ; +// point[ QStringLiteral( "Deviation" ) ] = "0" ; +// point[ QStringLiteral( "EdgeOfFlightLine" ) ] = "0" ; +// point[ QStringLiteral( "GpsTime" ) ] = "302522582.235839" ; +// point[ QStringLiteral( "Green" ) ] = "0" ; +// point[ QStringLiteral( "Intensity" ) ] = "1417" ; +// point[ QStringLiteral( "NumberOfReturns" ) ] = "3" ; +// point[ QStringLiteral( "PointSourceId" ) ] = "15017" ; +// point[ QStringLiteral( "Red" ) ] = "0" ; +// point[ QStringLiteral( "Reflectance" ) ] = "-8.050000190734863" ; +// point[ QStringLiteral( "ReturnNumber" ) ] = "3" ; +// point[ QStringLiteral( "ScanAngleRank" ) ] = "-6" ; +// point[ QStringLiteral( "ScanDirectionFlag" ) ] = "0" ; +// point[ QStringLiteral( "UserData" ) ] = "0" ; +// point[ QStringLiteral( "X" ) ] = "527919.11" ; +// point[ QStringLiteral( "Y" ) ] = "6210983.55" ; +// point[ QStringLiteral( "Z" ) ] = "147.111" ; +// expectedPoints.push_back( point ); +// } + +// QCOMPARE( identifiedPoints, expectedPoints ); +// } +//} + +void TestQgsCopcProvider::testPointCloudIndex() +{ + std::unique_ptr< QgsPointCloudLayer > layer = std::make_unique< QgsPointCloudLayer >( mTestDataDir + QStringLiteral( "point_clouds/copc/lone-star.copc.laz" ), QStringLiteral( "layer" ), QStringLiteral( "copc" ) ); + QVERIFY( layer->isValid() ); + + QgsPointCloudIndex *index = layer->dataProvider()->index(); + QVERIFY( index->isValid() ); + + QCOMPARE( index->nodePointCount( IndexedPointCloudNode::fromString( QStringLiteral( "0-0-0-0" ) ) ), 56721 ); + QCOMPARE( index->nodePointCount( IndexedPointCloudNode::fromString( QStringLiteral( "1-1-1-1" ) ) ), -1 ); + QCOMPARE( index->nodePointCount( IndexedPointCloudNode::fromString( QStringLiteral( "2-3-3-1" ) ) ), 446 ); + QCOMPARE( index->nodePointCount( IndexedPointCloudNode::fromString( QStringLiteral( "9-9-9-9" ) ) ), -1 ); + + QCOMPARE( index->pointCount(), 518862 ); + QCOMPARE( index->zMin(), 2322.89625 ); + QCOMPARE( index->zMax(), 2338.5755 ); + QCOMPARE( index->scale().toVector3D(), QVector3D( 0.0001, 0.0001, 0.0001 ) ); + QCOMPARE( index->offset().toVector3D(), QVector3D( 515385, 4918361, 2330.5 ) ); + QCOMPARE( index->span(), 128 ); + + QCOMPARE( index->nodeError( IndexedPointCloudNode::fromString( QStringLiteral( "0-0-0-0" ) ) ), 0.328125 ); + QCOMPARE( index->nodeError( IndexedPointCloudNode::fromString( QStringLiteral( "1-1-1-1" ) ) ), 0.1640625 ); + QCOMPARE( index->nodeError( IndexedPointCloudNode::fromString( QStringLiteral( "2-3-3-1" ) ) ), 0.08203125 ); + + { + QgsPointCloudDataBounds bounds = index->nodeBounds( IndexedPointCloudNode::fromString( QStringLiteral( "0-0-0-0" ) ) ); + QCOMPARE( bounds.xMin(), -170000 ); + QCOMPARE( bounds.yMin(), -210000 ); + QCOMPARE( bounds.zMin(), -85000 ); + QCOMPARE( bounds.xMax(), 250000 ); + QCOMPARE( bounds.yMax(), 210000 ); + QCOMPARE( bounds.zMax(), 335000 ); + } + + { + QgsPointCloudDataBounds bounds = index->nodeBounds( IndexedPointCloudNode::fromString( QStringLiteral( "1-1-1-1" ) ) ); + QCOMPARE( bounds.xMin(), 40000 ); + QCOMPARE( bounds.yMin(), 0 ); + QCOMPARE( bounds.zMin(), 125000 ); + QCOMPARE( bounds.xMax(), 250000 ); + QCOMPARE( bounds.yMax(), 210000 ); + QCOMPARE( bounds.zMax(), 335000 ); + } + + { + QgsPointCloudDataBounds bounds = index->nodeBounds( IndexedPointCloudNode::fromString( QStringLiteral( "2-3-3-1" ) ) ); + QCOMPARE( bounds.xMin(), 145000 ); + QCOMPARE( bounds.yMin(), 105000 ); + QCOMPARE( bounds.zMin(), 20000 ); + QCOMPARE( bounds.xMax(), 250000 ); + QCOMPARE( bounds.yMax(), 210000 ); + QCOMPARE( bounds.zMax(), 125000 ); + } +} + +QGSTEST_MAIN( TestQgsCopcProvider ) +#include "testqgscopcprovider.moc" diff --git a/tests/testdata/point_clouds/copc/extrabytes-dataset.copc.laz b/tests/testdata/point_clouds/copc/extrabytes-dataset.copc.laz new file mode 100644 index 00000000000..e3348ffd3d7 Binary files /dev/null and b/tests/testdata/point_clouds/copc/extrabytes-dataset.copc.laz differ diff --git a/tests/testdata/point_clouds/copc/lone-star.copc.laz b/tests/testdata/point_clouds/copc/lone-star.copc.laz new file mode 100644 index 00000000000..1327e58fd87 Binary files /dev/null and b/tests/testdata/point_clouds/copc/lone-star.copc.laz differ diff --git a/tests/testdata/point_clouds/copc/no-extrabytes-dataset.copc.laz b/tests/testdata/point_clouds/copc/no-extrabytes-dataset.copc.laz new file mode 100644 index 00000000000..e45d65cf2ac Binary files /dev/null and b/tests/testdata/point_clouds/copc/no-extrabytes-dataset.copc.laz differ diff --git a/tests/testdata/point_clouds/copc/norgb.copc.laz b/tests/testdata/point_clouds/copc/norgb.copc.laz new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/testdata/point_clouds/copc/rgb.copc.laz b/tests/testdata/point_clouds/copc/rgb.copc.laz new file mode 100644 index 00000000000..b1422594cce Binary files /dev/null and b/tests/testdata/point_clouds/copc/rgb.copc.laz differ diff --git a/tests/testdata/point_clouds/copc/rgb16.copc.laz b/tests/testdata/point_clouds/copc/rgb16.copc.laz new file mode 100644 index 00000000000..248db7af4f1 Binary files /dev/null and b/tests/testdata/point_clouds/copc/rgb16.copc.laz differ diff --git a/tests/testdata/point_clouds/copc/sunshine-coast.copc.laz b/tests/testdata/point_clouds/copc/sunshine-coast.copc.laz new file mode 100644 index 00000000000..4dd2e0d8282 Binary files /dev/null and b/tests/testdata/point_clouds/copc/sunshine-coast.copc.laz differ