diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index c55fd59435e3..6402003184f9 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -11130,6 +11130,21 @@ """ # -- Qgis.PointCloudAccessType.baseClass = Qgis +# monkey patching scoped based enum +Qgis.PointCloudZoomOutRenderBehavior.RenderExtents.__doc__ = "Render only point cloud extents when zoomed out" +Qgis.PointCloudZoomOutRenderBehavior.RenderOverview.__doc__ = "Render overview point cloud when zoomed out" +Qgis.PointCloudZoomOutRenderBehavior.RenderOverviewAndExtents.__doc__ = "Render point cloud extents over overview point cloud" +Qgis.PointCloudZoomOutRenderBehavior.__doc__ = """Point cloud zoom out options + +.. versionadded:: 3.42 + +* ``RenderExtents``: Render only point cloud extents when zoomed out +* ``RenderOverview``: Render overview point cloud when zoomed out +* ``RenderOverviewAndExtents``: Render point cloud extents over overview point cloud + +""" +# -- +Qgis.PointCloudZoomOutRenderBehavior.baseClass = Qgis try: Qgis.__attribute_docs__ = {'QGIS_DEV_VERSION': 'The development version', 'DEFAULT_SEARCH_RADIUS_MM': 'Identify search radius in mm', 'DEFAULT_MAPTOPIXEL_THRESHOLD': 'Default threshold between map coordinates and device coordinates for map2pixel simplification', 'DEFAULT_HIGHLIGHT_COLOR': 'Default highlight color. The transparency is expected to only be applied to polygon\nfill. Lines and outlines are rendered opaque.', 'DEFAULT_HIGHLIGHT_BUFFER_MM': 'Default highlight buffer in mm.', 'DEFAULT_HIGHLIGHT_MIN_WIDTH_MM': 'Default highlight line/stroke minimum width in mm.', 'SCALE_PRECISION': 'Fudge factor used to compare two scales. The code is often going from scale to scale\ndenominator. So it looses precision and, when a limit is inclusive, can lead to errors.\nTo avoid that, use this factor instead of using <= or >=.\n\n.. deprecated:: 3.40\n\n No longer used by QGIS and will be removed in QGIS 4.0.', 'DEFAULT_Z_COORDINATE': 'Default Z coordinate value.\nThis value have to be assigned to the Z coordinate for the vertex.', 'DEFAULT_M_COORDINATE': 'Default M coordinate value.\nThis value have to be assigned to the M coordinate for the vertex.\n\n.. versionadded:: 3.20', 'UI_SCALE_FACTOR': 'UI scaling factor. This should be applied to all widget sizes obtained from font metrics,\nto account for differences in the default font sizes across different platforms.', 'DEFAULT_SNAP_TOLERANCE': 'Default snapping distance tolerance.', 'DEFAULT_SNAP_UNITS': 'Default snapping distance units.'} Qgis.version = staticmethod(Qgis.version) diff --git a/python/PyQt6/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in b/python/PyQt6/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in index 8960f45ba28e..7b4e7f2a7b5e 100644 --- a/python/PyQt6/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in +++ b/python/PyQt6/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in @@ -575,6 +575,20 @@ Sets the text format renderers should use for rendering labels %Docstring Returns the text format renderer is using for rendering labels +.. versionadded:: 3.42 +%End + + void setZoomOutBehavior( const Qgis::PointCloudZoomOutRenderBehavior behavior ); +%Docstring +Sets the renderer behavior when zoomed out + +.. versionadded:: 3.42 +%End + + Qgis::PointCloudZoomOutRenderBehavior zoomOutBehavior() const; +%Docstring +Returns the renderer behavior when zoomed out + .. versionadded:: 3.42 %End diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 51de848c4c18..4d30981e6609 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -3255,6 +3255,13 @@ The development version Remote }; + enum class PointCloudZoomOutRenderBehavior /BaseType=IntEnum/ + { + RenderExtents, + RenderOverview, + RenderOverviewAndExtents + }; + static const double DEFAULT_SEARCH_RADIUS_MM; static const float DEFAULT_MAPTOPIXEL_THRESHOLD; diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 50ecf2eb4c17..0f9f1168ba09 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -11040,6 +11040,21 @@ """ # -- Qgis.PointCloudAccessType.baseClass = Qgis +# monkey patching scoped based enum +Qgis.PointCloudZoomOutRenderBehavior.RenderExtents.__doc__ = "Render only point cloud extents when zoomed out" +Qgis.PointCloudZoomOutRenderBehavior.RenderOverview.__doc__ = "Render overview point cloud when zoomed out" +Qgis.PointCloudZoomOutRenderBehavior.RenderOverviewAndExtents.__doc__ = "Render point cloud extents over overview point cloud" +Qgis.PointCloudZoomOutRenderBehavior.__doc__ = """Point cloud zoom out options + +.. versionadded:: 3.42 + +* ``RenderExtents``: Render only point cloud extents when zoomed out +* ``RenderOverview``: Render overview point cloud when zoomed out +* ``RenderOverviewAndExtents``: Render point cloud extents over overview point cloud + +""" +# -- +Qgis.PointCloudZoomOutRenderBehavior.baseClass = Qgis from enum import Enum diff --git a/python/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in b/python/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in index 8960f45ba28e..7b4e7f2a7b5e 100644 --- a/python/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in +++ b/python/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in @@ -575,6 +575,20 @@ Sets the text format renderers should use for rendering labels %Docstring Returns the text format renderer is using for rendering labels +.. versionadded:: 3.42 +%End + + void setZoomOutBehavior( const Qgis::PointCloudZoomOutRenderBehavior behavior ); +%Docstring +Sets the renderer behavior when zoomed out + +.. versionadded:: 3.42 +%End + + Qgis::PointCloudZoomOutRenderBehavior zoomOutBehavior() const; +%Docstring +Returns the renderer behavior when zoomed out + .. versionadded:: 3.42 %End diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index a978d6ff2b8a..a2b885ed6ddb 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -3255,6 +3255,13 @@ The development version Remote }; + enum class PointCloudZoomOutRenderBehavior + { + RenderExtents, + RenderOverview, + RenderOverviewAndExtents + }; + static const double DEFAULT_SEARCH_RADIUS_MM; static const float DEFAULT_MAPTOPIXEL_THRESHOLD; diff --git a/src/core/pointcloud/qgspointcloudlayer.cpp b/src/core/pointcloud/qgspointcloudlayer.cpp index 2cae35482674..1bf95bdabf95 100644 --- a/src/core/pointcloud/qgspointcloudlayer.cpp +++ b/src/core/pointcloud/qgspointcloudlayer.cpp @@ -45,6 +45,9 @@ #include "qgscopcpointcloudindex.h" #endif +#include "qgsvirtualpointcloudprovider.h" + + #include QgsPointCloudLayer::QgsPointCloudLayer( const QString &uri, @@ -956,16 +959,20 @@ void QgsPointCloudLayer::loadIndexesForRenderContext( QgsRenderContext &renderer } const QVector subIndex = mDataProvider->subIndexes(); - for ( int i = 0; i < subIndex.size(); ++i ) + if ( const QgsVirtualPointCloudProvider *vpcProvider = dynamic_cast( mDataProvider.get() ) ) { - // no need to load as it's there - if ( subIndex.at( i ).index() ) - continue; - - if ( subIndex.at( i ).extent().intersects( renderExtent ) && - renderExtent.width() < subIndex.at( i ).extent().width() ) + for ( int i = 0; i < subIndex.size(); ++i ) { - mDataProvider->loadSubIndex( i ); + // no need to load as it's there + if ( subIndex.at( i ).index() ) + continue; + + if ( subIndex.at( i ).extent().intersects( renderExtent ) && + ( renderExtent.width() < vpcProvider->averageSubIndexWidth() || + renderExtent.height() < vpcProvider->averageSubIndexHeight() ) ) + { + mDataProvider->loadSubIndex( i ); + } } } } diff --git a/src/core/pointcloud/qgspointcloudlayer.h b/src/core/pointcloud/qgspointcloudlayer.h index 706e3ebbdc7a..06b9b127353b 100644 --- a/src/core/pointcloud/qgspointcloudlayer.h +++ b/src/core/pointcloud/qgspointcloudlayer.h @@ -300,4 +300,4 @@ class CORE_EXPORT QgsPointCloudLayer : public QgsMapLayer, public QgsAbstractPro }; -#endif // QGSPOINTCLOUDPLAYER_H +#endif // QGSPOINTCLOUDLAYER_H diff --git a/src/core/pointcloud/qgspointcloudlayerrenderer.cpp b/src/core/pointcloud/qgspointcloudlayerrenderer.cpp index 2528366b2e0f..7194fe818487 100644 --- a/src/core/pointcloud/qgspointcloudlayerrenderer.cpp +++ b/src/core/pointcloud/qgspointcloudlayerrenderer.cpp @@ -36,6 +36,7 @@ #include "qgspointcloudrequest.h" #include "qgsrendercontext.h" #include "qgsruntimeprofiler.h" +#include "qgsvirtualpointcloudprovider.h" #include @@ -196,39 +197,62 @@ bool QgsPointCloudLayerRenderer::render() { canceled = !renderIndex( pc ); } - else + else if ( const QgsVirtualPointCloudProvider *vpcProvider = dynamic_cast( mLayer->dataProvider() ) ) { - mSubIndexExtentRenderer->startRender( context ); - for ( const auto &si : mSubIndexes ) + QVector< QgsPointCloudSubIndex > visibleIndexes; + for ( const QgsPointCloudSubIndex &si : mSubIndexes ) { - if ( canceled ) - break; - - QgsPointCloudIndex pc = si.index(); - - if ( !renderExtent.intersects( si.extent() ) ) - continue; - - if ( !pc || !pc.isValid() || renderExtent.width() > si.extent().width() ) + if ( renderExtent.intersects( si.extent() ) ) { - // when dealing with virtual point clouds, we want to render the individual extents when zoomed out - // and only use the selected renderer when zoomed in - mSubIndexExtentRenderer->renderExtent( si.polygonBounds(), context ); - // render the label of point cloud tile - if ( mSubIndexExtentRenderer->showLabels() ) - { - mSubIndexExtentRenderer->renderLabel( - context.renderContext().mapToPixel().transformBounds( si.extent().toRectF() ), - si.uri().section( "/", -1 ).section( ".", 0, 0 ), - context ); - } + visibleIndexes.append( si ); + } + } + const bool zoomedOut = renderExtent.width() > vpcProvider->averageSubIndexWidth() || + renderExtent.height() > vpcProvider->averageSubIndexHeight(); + QgsPointCloudIndex overviewIndex = vpcProvider->overview(); + // if the overview of virtual point cloud exists, and we are zoomed out, we render just overview + if ( vpcProvider->overview() && zoomedOut && + mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderOverview ) + { + renderIndex( overviewIndex ); + } + else + { + // if the overview of virtual point cloud exists, and we are zoomed out, but we want both overview and extents, + // we render overview + if ( vpcProvider->overview() && zoomedOut && + mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderOverviewAndExtents ) + { + renderIndex( overviewIndex ); } - else + mSubIndexExtentRenderer->startRender( context ); + for ( const QgsPointCloudSubIndex &si : visibleIndexes ) { - canceled = !renderIndex( pc ); + if ( canceled ) + break; + + QgsPointCloudIndex pc = si.index(); + // if the index of point cloud is invalid, or we are zoomed out and want extents, we render the point cloud extent + if ( !pc || !pc.isValid() || ( ( mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderExtents || mRenderer->zoomOutBehavior() == Qgis::PointCloudZoomOutRenderBehavior::RenderOverviewAndExtents ) && + zoomedOut ) ) + { + mSubIndexExtentRenderer->renderExtent( si.polygonBounds(), context ); + if ( mSubIndexExtentRenderer->showLabels() ) + { + mSubIndexExtentRenderer->renderLabel( + context.renderContext().mapToPixel().transformBounds( si.extent().toRectF() ), + si.uri().section( "/", -1 ).section( ".", 0, 0 ), + context ); + } + } + // else we just render the visible point cloud + else + { + canceled = !renderIndex( pc ); + } } + mSubIndexExtentRenderer->stopRender( context ); } - mSubIndexExtentRenderer->stopRender( context ); } mRenderer->stopRender( context ); diff --git a/src/core/pointcloud/qgspointcloudrenderer.cpp b/src/core/pointcloud/qgspointcloudrenderer.cpp index 6dadab581821..1ef20cf09d00 100644 --- a/src/core/pointcloud/qgspointcloudrenderer.cpp +++ b/src/core/pointcloud/qgspointcloudrenderer.cpp @@ -25,6 +25,7 @@ #include "qgslogger.h" #include "qgscircle.h" #include "qgsunittypes.h" +#include "qgsvirtualpointcloudprovider.h" #include #include @@ -68,7 +69,7 @@ QgsPointCloudRenderer::QgsPointCloudRenderer() settings.setEnabled( true ); settings.setSize( 1 ); textFormat.setBuffer( settings ); - mLabelTextFormat = ( textFormat ); + mLabelTextFormat = textFormat; } QgsPointCloudRenderer *QgsPointCloudRenderer::load( QDomElement &element, const QgsReadWriteContext &context ) @@ -218,6 +219,7 @@ void QgsPointCloudRenderer::copyCommonProperties( QgsPointCloudRenderer *destina destination->setShowLabels( mShowLabels ); destination->setLabelTextFormat( mLabelTextFormat ); + destination->setZoomOutBehavior( mZoomOutBehavior ); } void QgsPointCloudRenderer::restoreCommonProperties( const QDomElement &element, const QgsReadWriteContext &context ) @@ -228,8 +230,8 @@ void QgsPointCloudRenderer::restoreCommonProperties( const QDomElement &element, mMaximumScreenError = element.attribute( QStringLiteral( "maximumScreenError" ), QStringLiteral( "0.3" ) ).toDouble(); mMaximumScreenErrorUnit = QgsUnitTypes::decodeRenderUnit( element.attribute( QStringLiteral( "maximumScreenErrorUnit" ), QStringLiteral( "MM" ) ) ); - mPointSymbol = static_cast< Qgis::PointCloudSymbol >( element.attribute( QStringLiteral( "pointSymbol" ), QStringLiteral( "0" ) ).toInt() ); - mDrawOrder2d = static_cast< Qgis::PointCloudDrawOrder >( element.attribute( QStringLiteral( "drawOrder2d" ), QStringLiteral( "0" ) ).toInt() ); + mPointSymbol = qgsEnumKeyToValue( element.attribute( QStringLiteral( "pointSymbol" ) ), Qgis::PointCloudSymbol::Square ); + mDrawOrder2d = qgsEnumKeyToValue( element.attribute( QStringLiteral( "drawOrder2d" ) ), Qgis::PointCloudDrawOrder::Default ); mRenderAsTriangles = element.attribute( QStringLiteral( "renderAsTriangles" ), QStringLiteral( "0" ) ).toInt(); mHorizontalTriangleFilter = element.attribute( QStringLiteral( "horizontalTriangleFilter" ), QStringLiteral( "0" ) ).toInt(); @@ -242,6 +244,7 @@ void QgsPointCloudRenderer::restoreCommonProperties( const QDomElement &element, mLabelTextFormat = QgsTextFormat(); mLabelTextFormat.readXml( element.firstChildElement( QStringLiteral( "text-style" ) ), context ); } + mZoomOutBehavior = qgsEnumKeyToValue( element.attribute( QStringLiteral( "zoomOutBehavior" ) ), Qgis::PointCloudZoomOutRenderBehavior::RenderExtents ); } void QgsPointCloudRenderer::saveCommonProperties( QDomElement &element, const QgsReadWriteContext &context ) const @@ -252,8 +255,8 @@ void QgsPointCloudRenderer::saveCommonProperties( QDomElement &element, const Qg element.setAttribute( QStringLiteral( "maximumScreenError" ), qgsDoubleToString( mMaximumScreenError ) ); element.setAttribute( QStringLiteral( "maximumScreenErrorUnit" ), QgsUnitTypes::encodeUnit( mMaximumScreenErrorUnit ) ); - element.setAttribute( QStringLiteral( "pointSymbol" ), QString::number( static_cast< int >( mPointSymbol ) ) ); - element.setAttribute( QStringLiteral( "drawOrder2d" ), QString::number( static_cast< int >( mDrawOrder2d ) ) ); + element.setAttribute( QStringLiteral( "pointSymbol" ), qgsEnumValueToKey( mPointSymbol ) ); + element.setAttribute( QStringLiteral( "drawOrder2d" ), qgsEnumValueToKey( mDrawOrder2d ) ); element.setAttribute( QStringLiteral( "renderAsTriangles" ), QString::number( static_cast< int >( mRenderAsTriangles ) ) ); element.setAttribute( QStringLiteral( "horizontalTriangleFilter" ), QString::number( static_cast< int >( mHorizontalTriangleFilter ) ) ); @@ -267,6 +270,7 @@ void QgsPointCloudRenderer::saveCommonProperties( QDomElement &element, const Qg QDomDocument doc = element.ownerDocument(); element.appendChild( mLabelTextFormat.writeXml( doc, context ) ); } + element.setAttribute( QStringLiteral( "zoomOutBehavior" ), qgsEnumValueToKey( mZoomOutBehavior ) ); } Qgis::PointCloudSymbol QgsPointCloudRenderer::pointSymbol() const diff --git a/src/core/pointcloud/qgspointcloudrenderer.h b/src/core/pointcloud/qgspointcloudrenderer.h index f58db6489962..6689e0ac6a85 100644 --- a/src/core/pointcloud/qgspointcloudrenderer.h +++ b/src/core/pointcloud/qgspointcloudrenderer.h @@ -701,6 +701,18 @@ class CORE_EXPORT QgsPointCloudRenderer */ QgsTextFormat labelTextFormat() const { return mLabelTextFormat; } + /** + * Sets the renderer behavior when zoomed out + * \since QGIS 3.42 + */ + void setZoomOutBehavior( const Qgis::PointCloudZoomOutRenderBehavior behavior ) { mZoomOutBehavior = behavior; } + + /** + * Returns the renderer behavior when zoomed out + * \since QGIS 3.42 + */ + Qgis::PointCloudZoomOutRenderBehavior zoomOutBehavior() const { return mZoomOutBehavior; } + protected: /** @@ -842,6 +854,8 @@ class CORE_EXPORT QgsPointCloudRenderer bool mShowLabels = false; QgsTextFormat mLabelTextFormat; + + Qgis::PointCloudZoomOutRenderBehavior mZoomOutBehavior = Qgis::PointCloudZoomOutRenderBehavior::RenderExtents; }; #endif // QGSPOINTCLOUDRENDERER_H diff --git a/src/core/providers/vpc/qgsvirtualpointcloudprovider.cpp b/src/core/providers/vpc/qgsvirtualpointcloudprovider.cpp index c606dfd929d0..5d9d2cc19c9f 100644 --- a/src/core/providers/vpc/qgsvirtualpointcloudprovider.cpp +++ b/src/core/providers/vpc/qgsvirtualpointcloudprovider.cpp @@ -177,6 +177,8 @@ void QgsVirtualPointCloudProvider::parseFile() } QSet attributeNames; + double subIndexesWidth = 0.0; + double subIndexesHeight = 0.0; for ( const auto &f : data["features"] ) { @@ -202,12 +204,29 @@ void QgsVirtualPointCloudProvider::parseFile() QgsGeometry geometry; QgsDoubleRange zRange; - for ( const auto &asset : f["assets"] ) + // look directly for link to data file + if ( f["assets"].contains( "data" ) && f["assets"]["data"].contains( "href" ) ) { - if ( asset.contains( "href" ) ) + uri = QString::fromStdString( f["assets"]["data"]["href"] ); + } + + // look for vpc overview reference + if ( !mOverview && f["assets"].contains( "overview" ) && f["assets"]["overview"].contains( "href" ) ) + { + mOverview = QgsPointCloudIndex( new QgsCopcPointCloudIndex() ); + mOverview.load( fInfo.absoluteDir().absoluteFilePath( QString::fromStdString( f["assets"]["overview"]["href"] ) ) ); + } + // if it doesn't exist look for overview file in the directory + else if ( !mOverview ) + { + QDir vpcDir = fInfo.absoluteDir(); + QStringList nameFilter = { QString( fInfo.baseName() + "-overview.copc.laz" ) }; + vpcDir.setNameFilters( nameFilter ); + vpcDir.setFilter( QDir::Files ); + if ( !vpcDir.entryList().empty() ) { - uri = QString::fromStdString( asset["href"] ); - break; + mOverview = QgsPointCloudIndex( new QgsCopcPointCloudIndex() );; + mOverview.load( vpcDir.absoluteFilePath( vpcDir.entryList().first() ) ); } } @@ -364,12 +383,16 @@ void QgsVirtualPointCloudProvider::parseFile() } } + subIndexesWidth += extent.width(); + subIndexesHeight += extent.height(); mPolygonBounds->addPart( geometry ); mPointCount += count; QgsPointCloudSubIndex si( uri, geometry, extent, zRange, count ); mSubLayers.push_back( si ); } mExtent = mPolygonBounds->boundingBox(); + mAverageSubIndexWidth = subIndexesWidth / mSubLayers.size(); + mAverageSubIndexHeight = subIndexesHeight / mSubLayers.size(); populateAttributeCollection( attributeNames ); } @@ -509,7 +532,12 @@ QgsPointCloudRenderer *QgsVirtualPointCloudProvider::createRenderer( const QVari if ( mAttributes.indexOf( QLatin1String( "Classification" ) ) >= 0 ) { - return new QgsPointCloudClassifiedRenderer( QStringLiteral( "Classification" ), QgsPointCloudClassifiedRenderer::defaultCategories() ); + QgsPointCloudClassifiedRenderer *newRenderer = new QgsPointCloudClassifiedRenderer( QStringLiteral( "Classification" ), QgsPointCloudClassifiedRenderer::defaultCategories() ); + if ( mOverview ) + { + newRenderer->setZoomOutBehavior( Qgis::PointCloudZoomOutRenderBehavior::RenderOverview ); + } + return newRenderer; } return new QgsPointCloudExtentRenderer(); diff --git a/src/core/providers/vpc/qgsvirtualpointcloudprovider.h b/src/core/providers/vpc/qgsvirtualpointcloudprovider.h index d6be5dba8434..c9a49bb674f0 100644 --- a/src/core/providers/vpc/qgsvirtualpointcloudprovider.h +++ b/src/core/providers/vpc/qgsvirtualpointcloudprovider.h @@ -61,6 +61,26 @@ class CORE_EXPORT QgsVirtualPointCloudProvider: public QgsPointCloudDataProvider QgsPointCloudRenderer *createRenderer( const QVariantMap &configuration = QVariantMap() ) const override SIP_FACTORY; bool renderInPreview( const QgsDataProvider::PreviewContext & ) override { return false; } + /** + * Returns pointer to the overview index. May be NULLPTR if it doesn't exist. + * \since QGIS 3.42 + */ + QgsPointCloudIndex overview() const { return mOverview; } + + /** + * Returns the calculated average width of point clouds. + * \note We use this value to calculate when to switch between overview and point clouds + * \since QGIS 3.42 + */ + double averageSubIndexWidth() const { return mAverageSubIndexWidth; } + + /** + * Returns the calculated average height of point clouds. + * \note We use this value to calculate when to switch between overview and point clouds + * \since QGIS 3.42 + */ + double averageSubIndexHeight() const { return mAverageSubIndexHeight; } + signals: void subIndexLoaded( int i ); @@ -70,11 +90,14 @@ class CORE_EXPORT QgsVirtualPointCloudProvider: public QgsPointCloudDataProvider QVector mSubLayers; std::unique_ptr mPolygonBounds; QgsPointCloudAttributeCollection mAttributes; + QgsPointCloudIndex mOverview = QgsPointCloudIndex( nullptr ); QStringList mUriList; QgsRectangle mExtent; qint64 mPointCount = 0; QgsCoordinateReferenceSystem mCrs; + double mAverageSubIndexWidth = 0; + double mAverageSubIndexHeight = 0; }; class QgsVirtualPointCloudProviderMetadata : public QgsProviderMetadata diff --git a/src/core/qgis.h b/src/core/qgis.h index 1439ae053eb3..c7c19ce9436b 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -5734,6 +5734,18 @@ class CORE_EXPORT Qgis }; Q_ENUM( PointCloudAccessType ) + /** + * Point cloud zoom out options + * \since QGIS 3.42 + */ + enum class PointCloudZoomOutRenderBehavior : int + { + RenderExtents, //!< Render only point cloud extents when zoomed out + RenderOverview, //!< Render overview point cloud when zoomed out + RenderOverviewAndExtents //!< Render point cloud extents over overview point cloud + }; + Q_ENUM( PointCloudZoomOutRenderBehavior ) + /** * Identify search radius in mm */ diff --git a/src/gui/pointcloud/qgspointcloudrendererpropertieswidget.cpp b/src/gui/pointcloud/qgspointcloudrendererpropertieswidget.cpp index 10cff78b9836..00604ee8e276 100644 --- a/src/gui/pointcloud/qgspointcloudrendererpropertieswidget.cpp +++ b/src/gui/pointcloud/qgspointcloudrendererpropertieswidget.cpp @@ -33,6 +33,7 @@ #include "qgsstyle.h" #include "qgssymbolwidgetcontext.h" #include "qgstextformatwidget.h" +#include "qgsvirtualpointcloudprovider.h" static bool initPointCloudRenderer( const QString &name, QgsPointCloudRendererWidgetFunc f, const QString &iconName = QString() ) { @@ -90,8 +91,8 @@ QgsPointCloudRendererPropertiesWidget::QgsPointCloudRendererPropertiesWidget( Qg cboRenderers->setCurrentIndex( -1 ); // set no current renderer - mPointStyleComboBox->addItem( tr( "Square" ), static_cast( Qgis::PointCloudSymbol::Square ) ); - mPointStyleComboBox->addItem( tr( "Circle" ), static_cast( Qgis::PointCloudSymbol::Circle ) ); + mPointStyleComboBox->addItem( tr( "Square" ), QVariant::fromValue( Qgis::PointCloudSymbol::Square ) ); + mPointStyleComboBox->addItem( tr( "Circle" ), QVariant::fromValue( Qgis::PointCloudSymbol::Circle ) ); connect( cboRenderers, static_cast( &QComboBox::currentIndexChanged ), this, &QgsPointCloudRendererPropertiesWidget::rendererChanged ); @@ -103,9 +104,9 @@ QgsPointCloudRendererPropertiesWidget::QgsPointCloudRendererPropertiesWidget( Qg connect( mPointSizeSpinBox, qOverload( &QgsDoubleSpinBox::valueChanged ), this, &QgsPointCloudRendererPropertiesWidget::emitWidgetChanged ); connect( mPointSizeUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsPointCloudRendererPropertiesWidget::emitWidgetChanged ); - mDrawOrderComboBox->addItem( tr( "Default" ), static_cast( Qgis::PointCloudDrawOrder::Default ) ); - mDrawOrderComboBox->addItem( tr( "Bottom to Top" ), static_cast( Qgis::PointCloudDrawOrder::BottomToTop ) ); - mDrawOrderComboBox->addItem( tr( "Top to Bottom" ), static_cast( Qgis::PointCloudDrawOrder::TopToBottom ) ); + mDrawOrderComboBox->addItem( tr( "Default" ), QVariant::fromValue( Qgis::PointCloudDrawOrder::Default ) ); + mDrawOrderComboBox->addItem( tr( "Bottom to Top" ), QVariant::fromValue( Qgis::PointCloudDrawOrder::BottomToTop ) ); + mDrawOrderComboBox->addItem( tr( "Top to Bottom" ), QVariant::fromValue( Qgis::PointCloudDrawOrder::TopToBottom ) ); mMaxErrorUnitWidget->setUnits( { Qgis::RenderUnit::Millimeters, Qgis::RenderUnit::MetersInMapUnits, Qgis::RenderUnit::MapUnits, Qgis::RenderUnit::Pixels, Qgis::RenderUnit::Points, Qgis::RenderUnit::Inches } ); mMaxErrorSpinBox->setClearValue( 0.3 ); @@ -124,13 +125,47 @@ QgsPointCloudRendererPropertiesWidget::QgsPointCloudRendererPropertiesWidget( Qg connect( mHorizontalTriangleThresholdSpinBox, qOverload( &QgsDoubleSpinBox::valueChanged ), this, &QgsPointCloudRendererPropertiesWidget::emitWidgetChanged ); connect( mHorizontalTriangleUnitWidget, &QgsUnitSelectionWidget::changed, this, &QgsPointCloudRendererPropertiesWidget::emitWidgetChanged ); - // show label options only for virtual point clouds - bool showLabelOptions = !mLayer->dataProvider()->subIndexes().isEmpty(); - mLabels->setVisible( showLabelOptions ); - mLabelOptions->setVisible( showLabelOptions ); - mLabelOptions->setDialogTitle( tr( "Customize label text" ) ); - connect( mLabels, &QCheckBox::stateChanged, this, &QgsPointCloudRendererPropertiesWidget::emitWidgetChanged ); - connect( mLabelOptions, &QgsFontButton::changed, this, &QgsPointCloudRendererPropertiesWidget::emitWidgetChanged ); + // show virtual point cloud options only when vpc layer is selected + if ( !mLayer->dataProvider()->subIndexes().isEmpty() ) + { + mLabelOptions->setDialogTitle( tr( "Customize label text" ) ); + mLabelOptions->setText( tr( "Label format" ) ); + connect( mLabels, &QCheckBox::stateChanged, this, &QgsPointCloudRendererPropertiesWidget::emitWidgetChanged ); + connect( mLabelOptions, &QgsFontButton::changed, this, &QgsPointCloudRendererPropertiesWidget::emitWidgetChanged ); + mZoomOutOptions->addItem( tr( "Show Extents Only" ), QVariant::fromValue( Qgis::PointCloudZoomOutRenderBehavior::RenderExtents ) ); + + if ( const QgsVirtualPointCloudProvider *vpcProvider = dynamic_cast( mLayer->dataProvider() ) ) + { + if ( vpcProvider->overview() ) + { + mZoomOutOptions->addItem( tr( "Show Overview Only" ), QVariant::fromValue( Qgis::PointCloudZoomOutRenderBehavior::RenderOverview ) ); + mZoomOutOptions->addItem( tr( "Show Extents Over Overview" ), QVariant::fromValue( Qgis::PointCloudZoomOutRenderBehavior::RenderOverviewAndExtents ) ); + } + } + else + { + mZoomOutOptions->setEnabled( false ); + } + + connect( mZoomOutOptions, qOverload( &QComboBox::currentIndexChanged ), this, [this]( int ) { + switch ( mZoomOutOptions->currentData().value() ) + { + case Qgis::PointCloudZoomOutRenderBehavior::RenderOverview: + mLabels->setEnabled( false ); + mLabelOptions->setEnabled( false ); + break; + case Qgis::PointCloudZoomOutRenderBehavior::RenderExtents: + case Qgis::PointCloudZoomOutRenderBehavior::RenderOverviewAndExtents: + mLabels->setEnabled( true ); + mLabelOptions->setEnabled( true ); + } + emitWidgetChanged(); + } ); + } + else + { + mVpcGroupBox->setVisible( false ); + } syncToLayer( layer ); } @@ -176,8 +211,8 @@ void QgsPointCloudRendererPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mPointSizeUnitWidget->setUnit( mLayer->renderer()->pointSizeUnit() ); mPointSizeUnitWidget->setMapUnitScale( mLayer->renderer()->pointSizeMapUnitScale() ); - mPointStyleComboBox->setCurrentIndex( mPointStyleComboBox->findData( static_cast( mLayer->renderer()->pointSymbol() ) ) ); - mDrawOrderComboBox->setCurrentIndex( mDrawOrderComboBox->findData( static_cast( mLayer->renderer()->drawOrder2d() ) ) ); + mPointStyleComboBox->setCurrentIndex( mPointStyleComboBox->findData( QVariant::fromValue( mLayer->renderer()->pointSymbol() ) ) ); + mDrawOrderComboBox->setCurrentIndex( mDrawOrderComboBox->findData( QVariant::fromValue( mLayer->renderer()->drawOrder2d() ) ) ); mMaxErrorSpinBox->setValue( mLayer->renderer()->maximumScreenError() ); mMaxErrorUnitWidget->setUnit( mLayer->renderer()->maximumScreenErrorUnit() ); @@ -191,6 +226,17 @@ void QgsPointCloudRendererPropertiesWidget::syncToLayer( QgsMapLayer *layer ) { mLabels->setChecked( mLayer->renderer()->showLabels() ); mLabelOptions->setTextFormat( mLayer->renderer()->labelTextFormat() ); + mZoomOutOptions->setCurrentIndex( mZoomOutOptions->findData( QVariant::fromValue( mLayer->renderer()->zoomOutBehavior() ) ) ); + switch ( mLayer->renderer()->zoomOutBehavior() ) + { + case Qgis::PointCloudZoomOutRenderBehavior::RenderOverview: + mLabels->setEnabled( false ); + mLabelOptions->setEnabled( false ); + break; + default: + mLabels->setEnabled( true ); + mLabelOptions->setEnabled( true ); + } } } @@ -221,11 +267,11 @@ void QgsPointCloudRendererPropertiesWidget::apply() mLayer->renderer()->setPointSizeUnit( mPointSizeUnitWidget->unit() ); mLayer->renderer()->setPointSizeMapUnitScale( mPointSizeUnitWidget->getMapUnitScale() ); - mLayer->renderer()->setPointSymbol( static_cast( mPointStyleComboBox->currentData().toInt() ) ); + mLayer->renderer()->setPointSymbol( mPointStyleComboBox->currentData().value() ); mLayer->renderer()->setMaximumScreenError( mMaxErrorSpinBox->value() ); mLayer->renderer()->setMaximumScreenErrorUnit( mMaxErrorUnitWidget->unit() ); - mLayer->renderer()->setDrawOrder2d( static_cast( mDrawOrderComboBox->currentData().toInt() ) ); + mLayer->renderer()->setDrawOrder2d( mDrawOrderComboBox->currentData().value() ); mLayer->renderer()->setRenderAsTriangles( mTriangulateGroupBox->isChecked() ); mLayer->renderer()->setHorizontalTriangleFilter( mHorizontalTriangleCheckBox->isChecked() ); @@ -234,6 +280,7 @@ void QgsPointCloudRendererPropertiesWidget::apply() mLayer->renderer()->setShowLabels( mLabels->isChecked() ); mLayer->renderer()->setLabelTextFormat( mLabelOptions->textFormat() ); + mLayer->renderer()->setZoomOutBehavior( mZoomOutOptions->currentData().value() ); } void QgsPointCloudRendererPropertiesWidget::rendererChanged() diff --git a/src/ui/pointcloud/qgspointcloudrendererpropsdialogbase.ui b/src/ui/pointcloud/qgspointcloudrendererpropsdialogbase.ui index b7886305121e..6fd5eca3f68c 100644 --- a/src/ui/pointcloud/qgspointcloudrendererpropsdialogbase.ui +++ b/src/ui/pointcloud/qgspointcloudrendererpropsdialogbase.ui @@ -169,6 +169,58 @@ + + + + Virtual Point Cloud Options + + + false + + + false + + + + + + Qt::Horizontal + + + + 301 + 20 + + + + + + + + + + + + + + + + + + When zoomed out + + + + + + + Show tile labels + + + + + + @@ -279,33 +331,6 @@ 6 - - - - Show tile labels - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - diff --git a/tests/src/app/testqgsidentify.cpp b/tests/src/app/testqgsidentify.cpp index 856d5077c15c..3133da33b080 100644 --- a/tests/src/app/testqgsidentify.cpp +++ b/tests/src/app/testqgsidentify.cpp @@ -1301,7 +1301,7 @@ void TestQgsIdentify::identifyPointCloud() void TestQgsIdentify::identifyVirtualPointCloud() { #ifdef HAVE_COPC - std::unique_ptr pointCloud = std::make_unique( QStringLiteral( TEST_DATA_DIR ) + "/point_clouds/virtual/sunshine-coast/combined.vpc", QStringLiteral( "pointcloud" ), QStringLiteral( "vpc" ) ); + std::unique_ptr pointCloud = std::make_unique( QStringLiteral( TEST_DATA_DIR ) + "/point_clouds/virtual/sunshine-coast/combined-with-overview.vpc", QStringLiteral( "pointcloud" ), QStringLiteral( "vpc" ) ); QVERIFY( pointCloud->isValid() ); pointCloud->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:28356" ) ) ); QCOMPARE( pointCloud->crs3D().horizontalCrs().authid(), QStringLiteral( "EPSG:28356" ) ); diff --git a/tests/src/python/test_qgspointcloudclassifiedrenderer.py b/tests/src/python/test_qgspointcloudclassifiedrenderer.py index 3d6be8273afa..d1f882004b6f 100644 --- a/tests/src/python/test_qgspointcloudclassifiedrenderer.py +++ b/tests/src/python/test_qgspointcloudclassifiedrenderer.py @@ -468,6 +468,67 @@ def testRenderTriangles(self): ) ) + @unittest.skipIf( + "vpc" not in QgsProviderRegistry.instance().providerList(), + "VPC provider not available", + ) + def testOverviewRender(self): + layer = QgsPointCloudLayer( + unitTestDataPath() + + "/point_clouds/virtual/sunshine-coast/combined-with-overview.vpc", + "test", + "vpc", + ) + self.assertTrue(layer.isValid()) + + layer.setRenderer(layer.dataProvider().createRenderer()) + + layer.renderer().setPointSize(2) + layer.renderer().setPointSizeUnit(QgsUnitTypes.RenderUnit.RenderMillimeters) + + mapsettings = QgsMapSettings() + mapsettings.setOutputSize(QSize(400, 400)) + mapsettings.setOutputDpi(96) + mapsettings.setDestinationCrs(layer.crs()) + mapsettings.setExtent(QgsRectangle(498061, 7050991, 498069, 7050999)) + mapsettings.setLayers([layer]) + + self.assertTrue( + self.render_map_settings_check( + "classified_render_overview", "classified_render_overview", mapsettings + ) + ) + + @unittest.skipIf( + "vpc" not in QgsProviderRegistry.instance().providerList(), + "VPC provider not available", + ) + def testExtentsRender(self): + layer = QgsPointCloudLayer( + unitTestDataPath() + "/point_clouds/virtual/sunshine-coast/combined.vpc", + "test", + "vpc", + ) + self.assertTrue(layer.isValid()) + + layer.setRenderer(layer.dataProvider().createRenderer()) + + layer.renderer().setPointSize(2) + layer.renderer().setPointSizeUnit(QgsUnitTypes.RenderUnit.RenderMillimeters) + + mapsettings = QgsMapSettings() + mapsettings.setOutputSize(QSize(400, 400)) + mapsettings.setOutputDpi(96) + mapsettings.setDestinationCrs(layer.crs()) + mapsettings.setExtent(QgsRectangle(498061, 7050991, 498069, 7050999)) + mapsettings.setLayers([layer]) + + self.assertTrue( + self.render_map_settings_check( + "classified_render_extents", "classified_render_extents", mapsettings + ) + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/testdata/control_images/pointcloudrenderer/expected_classified_render_extents/expected_classified_render_extents.png b/tests/testdata/control_images/pointcloudrenderer/expected_classified_render_extents/expected_classified_render_extents.png new file mode 100644 index 000000000000..0fc041ce75d2 Binary files /dev/null and b/tests/testdata/control_images/pointcloudrenderer/expected_classified_render_extents/expected_classified_render_extents.png differ diff --git a/tests/testdata/control_images/pointcloudrenderer/expected_classified_render_overview/expected_classified_render_overview.png b/tests/testdata/control_images/pointcloudrenderer/expected_classified_render_overview/expected_classified_render_overview.png new file mode 100644 index 000000000000..95b6090cbd4c Binary files /dev/null and b/tests/testdata/control_images/pointcloudrenderer/expected_classified_render_overview/expected_classified_render_overview.png differ diff --git a/tests/testdata/point_clouds/virtual/sunshine-coast/combined-with-overview-overview.copc.laz b/tests/testdata/point_clouds/virtual/sunshine-coast/combined-with-overview-overview.copc.laz new file mode 100644 index 000000000000..ea5610819146 Binary files /dev/null and b/tests/testdata/point_clouds/virtual/sunshine-coast/combined-with-overview-overview.copc.laz differ diff --git a/tests/testdata/point_clouds/virtual/sunshine-coast/combined-with-overview.vpc b/tests/testdata/point_clouds/virtual/sunshine-coast/combined-with-overview.vpc new file mode 100644 index 000000000000..054337c51212 --- /dev/null +++ b/tests/testdata/point_clouds/virtual/sunshine-coast/combined-with-overview.vpc @@ -0,0 +1,877 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/pointcloud/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.1.0/schema.json" + ], + "id": "0-0.copc", + "geometry": { + "coordinates": [ + [ + [ + 498062.0, + 7050992.85, + 74.68 + ], + [ + 498062.0, + 7050995.82, + 74.68 + ], + [ + 498064.98, + 7050995.82, + 75.16 + ], + [ + 498064.98, + 7050992.85, + 75.16 + ], + [ + 498062.0, + 7050992.85, + 74.68 + ] + ] + ], + "type": "Polygon" + }, + "bbox": [ + 498062.0, + 7050992.85, + 74.68, + 498064.98, + 7050995.82, + 75.16 + ], + "properties": { + "datetime": "2024-10-18T00:00:00Z", + "pc:count": 91, + "pc:encoding": "?", + "pc:schemas": [ + { + "name": "X", + "size": 8, + "type": "floating" + }, + { + "name": "Y", + "size": 8, + "type": "floating" + }, + { + "name": "Z", + "size": 8, + "type": "floating" + }, + { + "name": "Intensity", + "size": 2, + "type": "unsigned" + }, + { + "name": "ReturnNumber", + "size": 1, + "type": "unsigned" + }, + { + "name": "NumberOfReturns", + "size": 1, + "type": "unsigned" + }, + { + "name": "ScanDirectionFlag", + "size": 1, + "type": "unsigned" + }, + { + "name": "EdgeOfFlightLine", + "size": 1, + "type": "unsigned" + }, + { + "name": "Classification", + "size": 1, + "type": "unsigned" + }, + { + "name": "Synthetic", + "size": 1, + "type": "unsigned" + }, + { + "name": "KeyPoint", + "size": 1, + "type": "unsigned" + }, + { + "name": "Withheld", + "size": 1, + "type": "unsigned" + }, + { + "name": "Overlap", + "size": 1, + "type": "unsigned" + }, + { + "name": "ScanAngleRank", + "size": 4, + "type": "floating" + }, + { + "name": "UserData", + "size": 1, + "type": "unsigned" + }, + { + "name": "PointSourceId", + "size": 2, + "type": "unsigned" + }, + { + "name": "GpsTime", + "size": 8, + "type": "floating" + }, + { + "name": "ScanChannel", + "size": 1, + "type": "unsigned" + }, + { + "name": "Red", + "size": 2, + "type": "unsigned" + }, + { + "name": "Green", + "size": 2, + "type": "unsigned" + }, + { + "name": "Blue", + "size": 2, + "type": "unsigned" + } + ], + "pc:type": "lidar", + "proj:bbox": [ + 498062.0, + 7050992.85, + 74.68, + 498064.98, + 7050995.82, + 75.16 + ], + "proj:geometry": { + "coordinates": [ + [ + [ + 498062.0, + 7050992.85, + 74.68 + ], + [ + 498062.0, + 7050995.82, + 74.68 + ], + [ + 498064.98, + 7050995.82, + 75.16 + ], + [ + 498064.98, + 7050992.85, + 75.16 + ], + [ + 498062.0, + 7050992.85, + 74.68 + ] + ] + ], + "type": "Polygon" + }, + "proj:wkt2": "" + }, + "links": [], + "assets": { + "data": { + "href": "./0-0.copc.laz", + "roles": [ + "data" + ] + }, + "overview": { + "href": "./combined-with-overview-overview.copc.laz", + "roles": [ + "overview" + ] + } + } + }, + { + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/pointcloud/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.1.0/schema.json" + ], + "id": "0-1.copc", + "geometry": { + "coordinates": [ + [ + [ + 498062.04, + 7050995.84, + 74.64 + ], + [ + 498062.04, + 7050997.03, + 74.64 + ], + [ + 498064.91, + 7050997.03, + 79.0 + ], + [ + 498064.91, + 7050995.84, + 79.0 + ], + [ + 498062.04, + 7050995.84, + 74.64 + ] + ] + ], + "type": "Polygon" + }, + "bbox": [ + 498062.04, + 7050995.84, + 74.64, + 498064.91, + 7050997.03, + 79.0 + ], + "properties": { + "datetime": "2024-10-18T00:00:00Z", + "pc:count": 47, + "pc:encoding": "?", + "pc:schemas": [ + { + "name": "X", + "size": 8, + "type": "floating" + }, + { + "name": "Y", + "size": 8, + "type": "floating" + }, + { + "name": "Z", + "size": 8, + "type": "floating" + }, + { + "name": "Intensity", + "size": 2, + "type": "unsigned" + }, + { + "name": "ReturnNumber", + "size": 1, + "type": "unsigned" + }, + { + "name": "NumberOfReturns", + "size": 1, + "type": "unsigned" + }, + { + "name": "ScanDirectionFlag", + "size": 1, + "type": "unsigned" + }, + { + "name": "EdgeOfFlightLine", + "size": 1, + "type": "unsigned" + }, + { + "name": "Classification", + "size": 1, + "type": "unsigned" + }, + { + "name": "Synthetic", + "size": 1, + "type": "unsigned" + }, + { + "name": "KeyPoint", + "size": 1, + "type": "unsigned" + }, + { + "name": "Withheld", + "size": 1, + "type": "unsigned" + }, + { + "name": "Overlap", + "size": 1, + "type": "unsigned" + }, + { + "name": "ScanAngleRank", + "size": 4, + "type": "floating" + }, + { + "name": "UserData", + "size": 1, + "type": "unsigned" + }, + { + "name": "PointSourceId", + "size": 2, + "type": "unsigned" + }, + { + "name": "GpsTime", + "size": 8, + "type": "floating" + }, + { + "name": "ScanChannel", + "size": 1, + "type": "unsigned" + }, + { + "name": "Red", + "size": 2, + "type": "unsigned" + }, + { + "name": "Green", + "size": 2, + "type": "unsigned" + }, + { + "name": "Blue", + "size": 2, + "type": "unsigned" + } + ], + "pc:type": "lidar", + "proj:bbox": [ + 498062.04, + 7050995.84, + 74.64, + 498064.91, + 7050997.03, + 79.0 + ], + "proj:geometry": { + "coordinates": [ + [ + [ + 498062.04, + 7050995.84, + 74.64 + ], + [ + 498062.04, + 7050997.03, + 74.64 + ], + [ + 498064.91, + 7050997.03, + 79.0 + ], + [ + 498064.91, + 7050995.84, + 79.0 + ], + [ + 498062.04, + 7050995.84, + 74.64 + ] + ] + ], + "type": "Polygon" + }, + "proj:wkt2": "" + }, + "links": [], + "assets": { + "data": { + "href": "./0-1.copc.laz", + "roles": [ + "data" + ] + }, + "overview": { + "href": "./combined-with-overview-overview.copc.laz", + "roles": [ + "overview" + ] + } + } + }, + { + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/pointcloud/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.1.0/schema.json" + ], + "id": "1-0.copc", + "geometry": { + "coordinates": [ + [ + [ + 498065.0, + 7050992.84, + 74.46 + ], + [ + 498065.0, + 7050995.83, + 74.46 + ], + [ + 498067.39, + 7050995.83, + 74.91 + ], + [ + 498067.39, + 7050992.84, + 74.91 + ], + [ + 498065.0, + 7050992.84, + 74.46 + ] + ] + ], + "type": "Polygon" + }, + "bbox": [ + 498065.0, + 7050992.84, + 74.46, + 498067.39, + 7050995.83, + 74.91 + ], + "properties": { + "datetime": "2024-10-18T00:00:00Z", + "pc:count": 77, + "pc:encoding": "?", + "pc:schemas": [ + { + "name": "X", + "size": 8, + "type": "floating" + }, + { + "name": "Y", + "size": 8, + "type": "floating" + }, + { + "name": "Z", + "size": 8, + "type": "floating" + }, + { + "name": "Intensity", + "size": 2, + "type": "unsigned" + }, + { + "name": "ReturnNumber", + "size": 1, + "type": "unsigned" + }, + { + "name": "NumberOfReturns", + "size": 1, + "type": "unsigned" + }, + { + "name": "ScanDirectionFlag", + "size": 1, + "type": "unsigned" + }, + { + "name": "EdgeOfFlightLine", + "size": 1, + "type": "unsigned" + }, + { + "name": "Classification", + "size": 1, + "type": "unsigned" + }, + { + "name": "Synthetic", + "size": 1, + "type": "unsigned" + }, + { + "name": "KeyPoint", + "size": 1, + "type": "unsigned" + }, + { + "name": "Withheld", + "size": 1, + "type": "unsigned" + }, + { + "name": "Overlap", + "size": 1, + "type": "unsigned" + }, + { + "name": "ScanAngleRank", + "size": 4, + "type": "floating" + }, + { + "name": "UserData", + "size": 1, + "type": "unsigned" + }, + { + "name": "PointSourceId", + "size": 2, + "type": "unsigned" + }, + { + "name": "GpsTime", + "size": 8, + "type": "floating" + }, + { + "name": "ScanChannel", + "size": 1, + "type": "unsigned" + }, + { + "name": "Red", + "size": 2, + "type": "unsigned" + }, + { + "name": "Green", + "size": 2, + "type": "unsigned" + }, + { + "name": "Blue", + "size": 2, + "type": "unsigned" + } + ], + "pc:type": "lidar", + "proj:bbox": [ + 498065.0, + 7050992.84, + 74.46, + 498067.39, + 7050995.83, + 74.91 + ], + "proj:geometry": { + "coordinates": [ + [ + [ + 498065.0, + 7050992.84, + 74.46 + ], + [ + 498065.0, + 7050995.83, + 74.46 + ], + [ + 498067.39, + 7050995.83, + 74.91 + ], + [ + 498067.39, + 7050992.84, + 74.91 + ], + [ + 498065.0, + 7050992.84, + 74.46 + ] + ] + ], + "type": "Polygon" + }, + "proj:wkt2": "" + }, + "links": [], + "assets": { + "data": { + "href": "./1-0.copc.laz", + "roles": [ + "data" + ] + }, + "overview": { + "href": "./combined-with-overview-overview.copc.laz", + "roles": [ + "overview" + ] + } + } + }, + { + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/pointcloud/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.1.0/schema.json" + ], + "id": "1-1.copc", + "geometry": { + "coordinates": [ + [ + [ + 498065.01, + 7050995.9, + 74.34 + ], + [ + 498065.01, + 7050997.04, + 74.34 + ], + [ + 498067.32, + 7050997.04, + 80.02 + ], + [ + 498067.32, + 7050995.9, + 80.02 + ], + [ + 498065.01, + 7050995.9, + 74.34 + ] + ] + ], + "type": "Polygon" + }, + "bbox": [ + 498065.01, + 7050995.9, + 74.34, + 498067.32, + 7050997.04, + 80.02 + ], + "properties": { + "datetime": "2024-10-18T00:00:00Z", + "pc:count": 38, + "pc:encoding": "?", + "pc:schemas": [ + { + "name": "X", + "size": 8, + "type": "floating" + }, + { + "name": "Y", + "size": 8, + "type": "floating" + }, + { + "name": "Z", + "size": 8, + "type": "floating" + }, + { + "name": "Intensity", + "size": 2, + "type": "unsigned" + }, + { + "name": "ReturnNumber", + "size": 1, + "type": "unsigned" + }, + { + "name": "NumberOfReturns", + "size": 1, + "type": "unsigned" + }, + { + "name": "ScanDirectionFlag", + "size": 1, + "type": "unsigned" + }, + { + "name": "EdgeOfFlightLine", + "size": 1, + "type": "unsigned" + }, + { + "name": "Classification", + "size": 1, + "type": "unsigned" + }, + { + "name": "Synthetic", + "size": 1, + "type": "unsigned" + }, + { + "name": "KeyPoint", + "size": 1, + "type": "unsigned" + }, + { + "name": "Withheld", + "size": 1, + "type": "unsigned" + }, + { + "name": "Overlap", + "size": 1, + "type": "unsigned" + }, + { + "name": "ScanAngleRank", + "size": 4, + "type": "floating" + }, + { + "name": "UserData", + "size": 1, + "type": "unsigned" + }, + { + "name": "PointSourceId", + "size": 2, + "type": "unsigned" + }, + { + "name": "GpsTime", + "size": 8, + "type": "floating" + }, + { + "name": "ScanChannel", + "size": 1, + "type": "unsigned" + }, + { + "name": "Red", + "size": 2, + "type": "unsigned" + }, + { + "name": "Green", + "size": 2, + "type": "unsigned" + }, + { + "name": "Blue", + "size": 2, + "type": "unsigned" + } + ], + "pc:type": "lidar", + "proj:bbox": [ + 498065.01, + 7050995.9, + 74.34, + 498067.32, + 7050997.04, + 80.02 + ], + "proj:geometry": { + "coordinates": [ + [ + [ + 498065.01, + 7050995.9, + 74.34 + ], + [ + 498065.01, + 7050997.04, + 74.34 + ], + [ + 498067.32, + 7050997.04, + 80.02 + ], + [ + 498067.32, + 7050995.9, + 80.02 + ], + [ + 498065.01, + 7050995.9, + 74.34 + ] + ] + ], + "type": "Polygon" + }, + "proj:wkt2": "" + }, + "links": [], + "assets": { + "data": { + "href": "./1-1.copc.laz", + "roles": [ + "data" + ] + }, + "overview": { + "href": "./combined-with-overview-overview.copc.laz", + "roles": [ + "overview" + ] + } + } + } + ] +}