diff --git a/python/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in b/python/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in index a144e401b90..cd41203b37a 100644 --- a/python/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in +++ b/python/core/auto_generated/pointcloud/qgspointcloudlayerelevationproperties.sip.in @@ -46,6 +46,10 @@ the layer. This can be used to correct or manually adjust for incorrect elevation values in a point cloud layer. +.. note:: + + Any scaling specified via :py:func:`~QgsPointCloudLayerElevationProperties.zScale` is applied before any offset value specified via :py:func:`~QgsPointCloudLayerElevationProperties.zOffset` + .. seealso:: :py:func:`setZOffset` %End @@ -56,7 +60,41 @@ the layer. This can be used to correct or manually adjust for incorrect elevation values in a point cloud layer. +.. note:: + + Any scaling specified via :py:func:`~QgsPointCloudLayerElevationProperties.zScale` is applied before any offset value specified via :py:func:`~QgsPointCloudLayerElevationProperties.zOffset` + .. seealso:: :py:func:`zOffset` +%End + + double zScale() const; +%Docstring +Returns the z scale, which is a scaling factor which should be applied to z values from +the layer. + +This can be used to correct or manually adjust for incorrect elevation values in a point cloud layer, such +as conversion of elevation values in feet to meters. + +.. note:: + + Any scaling specified via :py:func:`~QgsPointCloudLayerElevationProperties.zScale` is applied before any offset value specified via :py:func:`~QgsPointCloudLayerElevationProperties.zOffset` + +.. seealso:: :py:func:`setZScale` +%End + + void setZScale( double scale ); +%Docstring +Sets the z ``scale``, which is a scaling factor which will be applied to z values from +the layer. + +This can be used to correct or manually adjust for incorrect elevation values in a point cloud layer, such +as conversion of elevation values in feet to meters. + +.. note:: + + Any scaling specified via :py:func:`~QgsPointCloudLayerElevationProperties.zScale` is applied before any offset value specified via :py:func:`~QgsPointCloudLayerElevationProperties.zOffset` + +.. seealso:: :py:func:`zScale` %End }; diff --git a/python/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in b/python/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in index 504062793d3..c9bbc166dad 100644 --- a/python/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in +++ b/python/core/auto_generated/pointcloud/qgspointcloudrenderer.sip.in @@ -26,13 +26,16 @@ Encapsulates the render context for a 2D point cloud rendering operation. public: QgsPointCloudRenderContext( QgsRenderContext &context, const QgsVector3D &scale, const QgsVector3D &offset, - double zValueFixedOffset ); + double zValueScale, double zValueFixedOffset ); %Docstring Constructor for QgsPointCloudRenderContext. The ``scale`` and ``offset`` arguments specify the scale and offset of the layer's int32 coordinates compared to CRS coordinates respectively. +The ``zValueScale`` argument specifies any constant scaling factor which must be applied to z values +taken from the point cloud index. + The ``zValueFixedOffset`` argument specifies any constant offset value which must be added to z values taken from the point cloud index. %End @@ -114,9 +117,22 @@ Returns the offset for the y value in a point record. .. seealso:: :py:func:`yOffset` %End + double zValueScale() const; +%Docstring +Returns any constant scaling factor which must be applied to z values taken from the point cloud index. + +.. note:: + + Scaling of z values should be applied before the :py:func:`~QgsPointCloudRenderContext.zValueFixedOffset`. +%End + double zValueFixedOffset() const; %Docstring Returns any constant offset which must be applied to z values taken from the point cloud index. + +.. note:: + + Scaling of z values via :py:func:`~QgsPointCloudRenderContext.zValueScale` should be applied before the :py:func:`~QgsPointCloudRenderContext.zValueFixedOffset`. %End diff --git a/src/3d/qgspointcloudlayer3drenderer.cpp b/src/3d/qgspointcloudlayer3drenderer.cpp index 63d014a6f09..a99e3209123 100644 --- a/src/3d/qgspointcloudlayer3drenderer.cpp +++ b/src/3d/qgspointcloudlayer3drenderer.cpp @@ -29,9 +29,10 @@ #include "qgis.h" -QgsPointCloud3DRenderContext::QgsPointCloud3DRenderContext( const Qgs3DMapSettings &map, std::unique_ptr symbol, double zValueFixedOffset ) +QgsPointCloud3DRenderContext::QgsPointCloud3DRenderContext( const Qgs3DMapSettings &map, std::unique_ptr symbol, double zValueScale, double zValueFixedOffset ) : Qgs3DRenderContext( map ) , mSymbol( std::move( symbol ) ) + , mZValueScale( zValueScale ) , mZValueFixedOffset( zValueFixedOffset ) { @@ -115,6 +116,7 @@ Qt3DCore::QEntity *QgsPointCloudLayer3DRenderer::createEntity( const Qgs3DMapSet return nullptr; return new QgsPointCloudLayerChunkedEntity( pcl->dataProvider()->index(), map, dynamic_cast( mSymbol->clone() ), maximumScreenError(), showBoundingBoxes(), + static_cast< const QgsPointCloudLayerElevationProperties * >( pcl->elevationProperties() )->zScale(), static_cast< const QgsPointCloudLayerElevationProperties * >( pcl->elevationProperties() )->zOffset() ); } diff --git a/src/3d/qgspointcloudlayer3drenderer.h b/src/3d/qgspointcloudlayer3drenderer.h index 35029e0aa54..4822c4dcabc 100644 --- a/src/3d/qgspointcloudlayer3drenderer.h +++ b/src/3d/qgspointcloudlayer3drenderer.h @@ -46,11 +46,14 @@ class _3D_NO_EXPORT QgsPointCloud3DRenderContext : public Qgs3DRenderContext /** * Constructor for QgsPointCloud3DRenderContext. * + * The \a zValueScale argument specifies any constant scaling factor which must be applied to z values + * taken from the point cloud index. + * * The \a zValueFixedOffset argument specifies any constant offset value which must be added to z values * taken from the point cloud index. */ QgsPointCloud3DRenderContext( const Qgs3DMapSettings &map, std::unique_ptr< QgsPointCloud3DSymbol > symbol, - double zValueFixedOffset ); + double zValueScale, double zValueFixedOffset ); //! QgsPointCloudRenderContext cannot be copied. QgsPointCloud3DRenderContext( const QgsPointCloud3DRenderContext &rh ) = delete; @@ -133,8 +136,17 @@ class _3D_NO_EXPORT QgsPointCloud3DRenderContext : public Qgs3DRenderContext } } + /** + * Returns any constant scaling factor which must be applied to z values taken from the point cloud index. + * + * \note Scaling of z values should be applied before the zValueFixedOffset(). + */ + double zValueScale() const { return mZValueScale; } + /** * Returns any constant offset which must be applied to z values taken from the point cloud index. + * + * \note Scaling of z values via zValueScale() should be applied before the zValueFixedOffset(). */ double zValueFixedOffset() const { return mZValueFixedOffset; } @@ -145,6 +157,7 @@ class _3D_NO_EXPORT QgsPointCloud3DRenderContext : public Qgs3DRenderContext QgsPointCloudAttributeCollection mAttributes; std::unique_ptr mSymbol; QgsPointCloudCategoryList mFilteredOutCategories; + double mZValueScale = 1.0; double mZValueFixedOffset = 0; }; diff --git a/src/3d/qgspointcloudlayerchunkloader_p.cpp b/src/3d/qgspointcloudlayerchunkloader_p.cpp index 5d87a2b28d9..57183739625 100644 --- a/src/3d/qgspointcloudlayerchunkloader_p.cpp +++ b/src/3d/qgspointcloudlayerchunkloader_p.cpp @@ -46,10 +46,11 @@ /////////////// -QgsPointCloudLayerChunkLoader::QgsPointCloudLayerChunkLoader( const QgsPointCloudLayerChunkLoaderFactory *factory, QgsChunkNode *node, std::unique_ptr< QgsPointCloud3DSymbol > symbol, double zValueOffset ) +QgsPointCloudLayerChunkLoader::QgsPointCloudLayerChunkLoader( const QgsPointCloudLayerChunkLoaderFactory *factory, QgsChunkNode *node, std::unique_ptr< QgsPointCloud3DSymbol > symbol, + double zValueScale, double zValueOffset ) : QgsChunkLoader( node ) , mFactory( factory ) - , mContext( factory->mMap, std::move( symbol ), zValueOffset ) + , mContext( factory->mMap, std::move( symbol ), zValueScale, zValueOffset ) { QgsPointCloudIndex *pc = mFactory->mPointCloudIndex; mContext.setAttributes( pc->attributes() ); @@ -126,9 +127,11 @@ Qt3DCore::QEntity *QgsPointCloudLayerChunkLoader::createEntity( Qt3DCore::QEntit /////////////// -QgsPointCloudLayerChunkLoaderFactory::QgsPointCloudLayerChunkLoaderFactory( const Qgs3DMapSettings &map, QgsPointCloudIndex *pc, QgsPointCloud3DSymbol *symbol, double zValueOffset ) +QgsPointCloudLayerChunkLoaderFactory::QgsPointCloudLayerChunkLoaderFactory( const Qgs3DMapSettings &map, QgsPointCloudIndex *pc, QgsPointCloud3DSymbol *symbol, + double zValueScale, double zValueOffset ) : mMap( map ) , mPointCloudIndex( pc ) + , mZValueScale( zValueScale ) , mZValueOffset( zValueOffset ) { mSymbol.reset( symbol ); @@ -138,7 +141,7 @@ QgsChunkLoader *QgsPointCloudLayerChunkLoaderFactory::createChunkLoader( QgsChun { QgsChunkNodeId id = node->tileId(); Q_ASSERT( mPointCloudIndex->hasNode( IndexedPointCloudNode( id.d, id.x, id.y, id.z ) ) ); - return new QgsPointCloudLayerChunkLoader( this, node, std::unique_ptr< QgsPointCloud3DSymbol >( static_cast< QgsPointCloud3DSymbol * >( mSymbol->clone() ) ), mZValueOffset ); + return new QgsPointCloudLayerChunkLoader( this, node, std::unique_ptr< QgsPointCloud3DSymbol >( static_cast< QgsPointCloud3DSymbol * >( mSymbol->clone() ) ), mZValueScale, mZValueOffset ); } QgsAABB nodeBoundsToAABB( QgsPointCloudDataBounds nodeBounds, QgsVector3D offset, QgsVector3D scale, const Qgs3DMapSettings &map, double zValueOffset ); @@ -197,9 +200,9 @@ QgsAABB nodeBoundsToAABB( QgsPointCloudDataBounds nodeBounds, QgsVector3D offset } -QgsPointCloudLayerChunkedEntity::QgsPointCloudLayerChunkedEntity( QgsPointCloudIndex *pc, const Qgs3DMapSettings &map, QgsPointCloud3DSymbol *symbol, float maxScreenError, bool showBoundingBoxes, double zValueOffset ) +QgsPointCloudLayerChunkedEntity::QgsPointCloudLayerChunkedEntity( QgsPointCloudIndex *pc, const Qgs3DMapSettings &map, QgsPointCloud3DSymbol *symbol, float maxScreenError, bool showBoundingBoxes, double zValueScale, double zValueOffset ) : QgsChunkedEntity( maxScreenError, - new QgsPointCloudLayerChunkLoaderFactory( map, pc, symbol, zValueOffset ), true ) + new QgsPointCloudLayerChunkLoaderFactory( map, pc, symbol, zValueScale, zValueOffset ), true ) { setUsingAdditiveStrategy( true ); setShowBoundingBoxes( showBoundingBoxes ); diff --git a/src/3d/qgspointcloudlayerchunkloader_p.h b/src/3d/qgspointcloudlayerchunkloader_p.h index 4edeb726db1..0c14d964889 100644 --- a/src/3d/qgspointcloudlayerchunkloader_p.h +++ b/src/3d/qgspointcloudlayerchunkloader_p.h @@ -54,11 +54,13 @@ class QgsPointCloudLayerChunkLoaderFactory : public QgsChunkLoaderFactory { public: - /* + + /** * Constructs the factory * The factory takes ownership over the passed \a symbol */ - QgsPointCloudLayerChunkLoaderFactory( const Qgs3DMapSettings &map, QgsPointCloudIndex *pc, QgsPointCloud3DSymbol *symbol, double zValueOffset ); + QgsPointCloudLayerChunkLoaderFactory( const Qgs3DMapSettings &map, QgsPointCloudIndex *pc, QgsPointCloud3DSymbol *symbol, + double zValueScale, double zValueOffset ); //! Creates loader for the given chunk node. Ownership of the returned is passed to the caller. virtual QgsChunkLoader *createChunkLoader( QgsChunkNode *node ) const override; @@ -67,6 +69,7 @@ class QgsPointCloudLayerChunkLoaderFactory : public QgsChunkLoaderFactory const Qgs3DMapSettings &mMap; QgsPointCloudIndex *mPointCloudIndex; std::unique_ptr< QgsPointCloud3DSymbol > mSymbol; + double mZValueScale = 1.0; double mZValueOffset = 0; }; @@ -87,7 +90,7 @@ class QgsPointCloudLayerChunkLoader : public QgsChunkLoader * Constructs the loader * QgsPointCloudLayerChunkLoader takes ownership over symbol */ - QgsPointCloudLayerChunkLoader( const QgsPointCloudLayerChunkLoaderFactory *factory, QgsChunkNode *node, std::unique_ptr< QgsPointCloud3DSymbol > symbol, double zValueOffset ); + QgsPointCloudLayerChunkLoader( const QgsPointCloudLayerChunkLoaderFactory *factory, QgsChunkNode *node, std::unique_ptr< QgsPointCloud3DSymbol > symbol, double zValueScale, double zValueOffset ); ~QgsPointCloudLayerChunkLoader() override; virtual void cancel() override; @@ -117,7 +120,7 @@ class QgsPointCloudLayerChunkedEntity : public QgsChunkedEntity Q_OBJECT public: explicit QgsPointCloudLayerChunkedEntity( QgsPointCloudIndex *pc, const Qgs3DMapSettings &map, QgsPointCloud3DSymbol *symbol, float maxScreenError, bool showBoundingBoxes, - double zValueOffset ); + double zValueScale, double zValueOffset ); ~QgsPointCloudLayerChunkedEntity(); }; diff --git a/src/3d/symbols/qgspointcloud3dsymbol_p.cpp b/src/3d/symbols/qgspointcloud3dsymbol_p.cpp index b2f5d25470b..3b7918dbbc6 100644 --- a/src/3d/symbols/qgspointcloud3dsymbol_p.cpp +++ b/src/3d/symbols/qgspointcloud3dsymbol_p.cpp @@ -260,6 +260,7 @@ void QgsSingleColorPointCloud3DSymbolHandler::processNode( QgsPointCloudIndex *p const QgsVector3D scale = pc->scale(); const QgsVector3D offset = pc->offset(); + const double zValueScale = context.zValueScale(); const double zValueOffset = context.zValueFixedOffset(); for ( int i = 0; i < count; ++i ) @@ -270,7 +271,7 @@ void QgsSingleColorPointCloud3DSymbolHandler::processNode( QgsPointCloudIndex *p double x = offset.x() + scale.x() * ix; double y = offset.y() + scale.y() * iy; - double z = offset.z() + scale.z() * iz + zValueOffset; + double z = ( offset.z() + scale.z() * iz ) * zValueScale + zValueOffset; QgsVector3D point( x, y, z ); QgsVector3D p = context.map().mapToWorldCoordinates( point ); outNormal.positions.push_back( QVector3D( p.x(), p.y(), p.z() ) ); @@ -315,6 +316,7 @@ void QgsColorRampPointCloud3DSymbolHandler::processNode( QgsPointCloudIndex *pc, bool attrIsZ = false; QgsPointCloudAttribute::DataType attributeType = QgsPointCloudAttribute::Float; int attributeOffset = 0; + const double zValueScale = context.zValueScale(); const double zValueOffset = context.zValueFixedOffset(); QgsColorRampPointCloud3DSymbol *symbol = dynamic_cast( context.symbol() ); if ( symbol ) @@ -371,7 +373,7 @@ void QgsColorRampPointCloud3DSymbolHandler::processNode( QgsPointCloudIndex *pc, double x = offset.x() + scale.x() * ix; double y = offset.y() + scale.y() * iy; - double z = offset.z() + scale.z() * iz + zValueOffset; + double z = ( offset.z() + scale.z() * iz ) * zValueScale + zValueOffset; QgsVector3D point( x, y, z ); QgsVector3D p = context.map().mapToWorldCoordinates( point ); @@ -453,6 +455,7 @@ void QgsRGBPointCloud3DSymbolHandler::processNode( QgsPointCloudIndex *pc, const const QgsVector3D scale = pc->scale(); const QgsVector3D offset = pc->offset(); + const double zValueScale = context.zValueScale(); const double zValueOffset = context.zValueFixedOffset(); QgsContrastEnhancement *redContrastEnhancement = symbol->redContrastEnhancement(); @@ -473,7 +476,7 @@ void QgsRGBPointCloud3DSymbolHandler::processNode( QgsPointCloudIndex *pc, const qint32 iz = *( qint32 * )( ptr + i * recordSize + 8 ); double x = offset.x() + scale.x() * ix; double y = offset.y() + scale.y() * iy; - double z = offset.z() + scale.z() * iz + zValueOffset; + double z = ( offset.z() + scale.z() * iz ) * zValueScale + zValueOffset; QgsVector3D point( x, y, z ); QgsVector3D p = context.map().mapToWorldCoordinates( point ); @@ -598,6 +601,7 @@ void QgsClassificationPointCloud3DSymbolHandler::processNode( QgsPointCloudIndex const QgsVector3D scale = pc->scale(); const QgsVector3D offset = pc->offset(); + const double zValueScale = context.zValueScale(); const double zValueOffset = context.zValueFixedOffset(); QSet filteredOutValues = context.getFilteredOutValues(); @@ -609,7 +613,7 @@ void QgsClassificationPointCloud3DSymbolHandler::processNode( QgsPointCloudIndex double x = offset.x() + scale.x() * ix; double y = offset.y() + scale.y() * iy; - double z = offset.z() + scale.z() * iz + zValueOffset; + double z = ( offset.z() + scale.z() * iz ) * zValueScale + zValueOffset; QgsVector3D point( x, y, z ); QgsVector3D p = context.map().mapToWorldCoordinates( point ); diff --git a/src/app/pointcloud/qgspointcloudelevationpropertieswidget.cpp b/src/app/pointcloud/qgspointcloudelevationpropertieswidget.cpp index 38a11218107..1123e6654d0 100644 --- a/src/app/pointcloud/qgspointcloudelevationpropertieswidget.cpp +++ b/src/app/pointcloud/qgspointcloudelevationpropertieswidget.cpp @@ -27,10 +27,12 @@ QgsPointCloudElevationPropertiesWidget::QgsPointCloudElevationPropertiesWidget( setupUi( this ); mOffsetZSpinBox->setClearValue( 0 ); + mScaleZSpinBox->setClearValue( 1 ); syncToLayer( layer ); connect( mOffsetZSpinBox, qgis::overload::of( &QDoubleSpinBox::valueChanged ), this, &QgsPointCloudElevationPropertiesWidget::onChanged ); + connect( mScaleZSpinBox, qgis::overload::of( &QDoubleSpinBox::valueChanged ), this, &QgsPointCloudElevationPropertiesWidget::onChanged ); } void QgsPointCloudElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) @@ -41,6 +43,7 @@ void QgsPointCloudElevationPropertiesWidget::syncToLayer( QgsMapLayer *layer ) mBlockUpdates = true; mOffsetZSpinBox->setValue( static_cast< const QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() )->zOffset() ); + mScaleZSpinBox->setValue( static_cast< const QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() )->zScale() ); mBlockUpdates = false; } @@ -50,6 +53,7 @@ void QgsPointCloudElevationPropertiesWidget::apply() return; static_cast< QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() )->setZOffset( mOffsetZSpinBox->value() ); + static_cast< QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() )->setZScale( mScaleZSpinBox->value() ); mLayer->trigger3DUpdate(); } diff --git a/src/core/pointcloud/qgspointcloudattributebyramprenderer.cpp b/src/core/pointcloud/qgspointcloudattributebyramprenderer.cpp index 2b686300d6f..9a4aa97f179 100644 --- a/src/core/pointcloud/qgspointcloudattributebyramprenderer.cpp +++ b/src/core/pointcloud/qgspointcloudattributebyramprenderer.cpp @@ -111,7 +111,7 @@ void QgsPointCloudAttributeByRampRenderer::renderBlock( const QgsPointCloudBlock if ( applyYOffset ) attributeValue = context.offset().y() + context.scale().y() * attributeValue; if ( applyZOffset ) - attributeValue = context.offset().z() + context.scale().z() * attributeValue + context.zValueFixedOffset(); + attributeValue = ( context.offset().z() + context.scale().z() * attributeValue ) * context.zValueScale() + context.zValueFixedOffset(); mColorRampShader.shade( attributeValue, &red, &green, &blue, &alpha ); drawPoint( x, y, QColor( red, green, blue, alpha ), context ); diff --git a/src/core/pointcloud/qgspointcloudlayerelevationproperties.cpp b/src/core/pointcloud/qgspointcloudlayerelevationproperties.cpp index 6d9fb7f1e3f..dfc114c1059 100644 --- a/src/core/pointcloud/qgspointcloudlayerelevationproperties.cpp +++ b/src/core/pointcloud/qgspointcloudlayerelevationproperties.cpp @@ -32,6 +32,7 @@ QDomElement QgsPointCloudLayerElevationProperties::writeXml( QDomElement &parent { QDomElement element = document.createElement( QStringLiteral( "elevation" ) ); element.setAttribute( QStringLiteral( "zoffset" ), qgsDoubleToString( mZOffset ) ); + element.setAttribute( QStringLiteral( "zscale" ), qgsDoubleToString( mZScale ) ); parentElement.appendChild( element ); return element; } @@ -40,6 +41,7 @@ bool QgsPointCloudLayerElevationProperties::readXml( const QDomElement &element, { QDomElement elevationElement = element.firstChildElement( QStringLiteral( "elevation" ) ).toElement(); mZOffset = elevationElement.attribute( QStringLiteral( "zoffset" ), QStringLiteral( "0" ) ).toDouble(); + mZScale = elevationElement.attribute( QStringLiteral( "zscale" ), QStringLiteral( "1" ) ).toDouble(); return true; } @@ -60,7 +62,7 @@ QgsDoubleRange QgsPointCloudLayerElevationProperties::calculateZRange( QgsMapLay const QVariant zMax = pcLayer->dataProvider()->metadataStatistic( QStringLiteral( "Z" ), QgsStatisticalSummary::Max ); if ( zMin.isValid() && zMax.isValid() ) { - return QgsDoubleRange( zMin.toDouble() + mZOffset, zMax.toDouble() + mZOffset ); + return QgsDoubleRange( zMin.toDouble() * mZScale + mZOffset, zMax.toDouble() * mZScale + mZOffset ); } } } diff --git a/src/core/pointcloud/qgspointcloudlayerelevationproperties.h b/src/core/pointcloud/qgspointcloudlayerelevationproperties.h index 4361f91da34..28a88904c10 100644 --- a/src/core/pointcloud/qgspointcloudlayerelevationproperties.h +++ b/src/core/pointcloud/qgspointcloudlayerelevationproperties.h @@ -54,6 +54,8 @@ class CORE_EXPORT QgsPointCloudLayerElevationProperties : public QgsMapLayerElev * * This can be used to correct or manually adjust for incorrect elevation values in a point cloud layer. * + * \note Any scaling specified via zScale() is applied before any offset value specified via zOffset() + * * \see setZOffset() */ double zOffset() const { return mZOffset; } @@ -64,12 +66,41 @@ class CORE_EXPORT QgsPointCloudLayerElevationProperties : public QgsMapLayerElev * * This can be used to correct or manually adjust for incorrect elevation values in a point cloud layer. * + * \note Any scaling specified via zScale() is applied before any offset value specified via zOffset() + * * \see zOffset() */ void setZOffset( double offset ) { mZOffset = offset; } + /** + * Returns the z scale, which is a scaling factor which should be applied to z values from + * the layer. + * + * This can be used to correct or manually adjust for incorrect elevation values in a point cloud layer, such + * as conversion of elevation values in feet to meters. + * + * \note Any scaling specified via zScale() is applied before any offset value specified via zOffset() + * + * \see setZScale() + */ + double zScale() const { return mZScale; } + + /** + * Sets the z \a scale, which is a scaling factor which will be applied to z values from + * the layer. + * + * This can be used to correct or manually adjust for incorrect elevation values in a point cloud layer, such + * as conversion of elevation values in feet to meters. + * + * \note Any scaling specified via zScale() is applied before any offset value specified via zOffset() + * + * \see zScale() + */ + void setZScale( double scale ) { mZScale = scale; } + private: + double mZScale = 1.0; double mZOffset = 0.0; }; diff --git a/src/core/pointcloud/qgspointcloudlayerrenderer.cpp b/src/core/pointcloud/qgspointcloudlayerrenderer.cpp index 7bd6fe5c201..06ab5897c68 100644 --- a/src/core/pointcloud/qgspointcloudlayerrenderer.cpp +++ b/src/core/pointcloud/qgspointcloudlayerrenderer.cpp @@ -34,6 +34,7 @@ QgsPointCloudLayerRenderer::QgsPointCloudLayerRenderer( QgsPointCloudLayer *layer, QgsRenderContext &context ) : QgsMapLayerRenderer( layer->id(), &context ) , mLayer( layer ) + , mLayerAttributes( layer->attributes() ) { // TODO: we must not keep pointer to mLayer (it's dangerous) - we must copy anything we need for rendering // or use some locking to prevent read/write from multiple threads @@ -48,14 +49,18 @@ QgsPointCloudLayerRenderer::QgsPointCloudLayerRenderer( QgsPointCloudLayer *laye mOffset = mLayer->dataProvider()->index()->offset(); } - mZOffset = static_cast< const QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() )->zOffset(); + if ( const QgsPointCloudLayerElevationProperties *elevationProps = dynamic_cast< const QgsPointCloudLayerElevationProperties * >( mLayer->elevationProperties() ) ) + { + mZOffset = elevationProps->zOffset(); + mZScale = elevationProps->zScale(); + } mCloudExtent = mLayer->dataProvider()->polygonBounds(); } bool QgsPointCloudLayerRenderer::render() { - QgsPointCloudRenderContext context( *renderContext(), mScale, mOffset, mZOffset ); + QgsPointCloudRenderContext context( *renderContext(), mScale, mOffset, mZScale, mZOffset ); // Set up the render configuration options QPainter *painter = context.renderContext().painter(); @@ -93,14 +98,14 @@ bool QgsPointCloudLayerRenderer::render() if ( mAttributes.indexOf( attribute ) >= 0 ) continue; // don't re-add attributes we are already going to fetch - const int layerIndex = mLayer->attributes().indexOf( attribute ); + const int layerIndex = mLayerAttributes.indexOf( attribute ); if ( layerIndex < 0 ) { QgsMessageLog::logMessage( QObject::tr( "Required attribute %1 not found in layer" ).arg( attribute ), QObject::tr( "Point Cloud" ) ); continue; } - mAttributes.push_back( mLayer->attributes().at( layerIndex ) ); + mAttributes.push_back( mLayerAttributes.at( layerIndex ) ); } QgsPointCloudDataBounds db; diff --git a/src/core/pointcloud/qgspointcloudlayerrenderer.h b/src/core/pointcloud/qgspointcloudlayerrenderer.h index c483a84f4c7..87426b8472c 100644 --- a/src/core/pointcloud/qgspointcloudlayerrenderer.h +++ b/src/core/pointcloud/qgspointcloudlayerrenderer.h @@ -69,7 +69,9 @@ class CORE_EXPORT QgsPointCloudLayerRenderer: public QgsMapLayerRenderer QgsVector3D mScale; QgsVector3D mOffset; double mZOffset = 0; + double mZScale = 1.0; + QgsPointCloudAttributeCollection mLayerAttributes; QgsPointCloudAttributeCollection mAttributes; QgsGeometry mCloudExtent; diff --git a/src/core/pointcloud/qgspointcloudrenderer.cpp b/src/core/pointcloud/qgspointcloudrenderer.cpp index 477af685c90..eaaacd96c06 100644 --- a/src/core/pointcloud/qgspointcloudrenderer.cpp +++ b/src/core/pointcloud/qgspointcloudrenderer.cpp @@ -20,10 +20,11 @@ #include "qgsapplication.h" #include "qgssymbollayerutils.h" -QgsPointCloudRenderContext::QgsPointCloudRenderContext( QgsRenderContext &context, const QgsVector3D &scale, const QgsVector3D &offset, double zValueFixedOffset ) +QgsPointCloudRenderContext::QgsPointCloudRenderContext( QgsRenderContext &context, const QgsVector3D &scale, const QgsVector3D &offset, double zValueScale, double zValueFixedOffset ) : mRenderContext( context ) , mScale( scale ) , mOffset( offset ) + , mZValueScale( zValueScale ) , mZValueFixedOffset( zValueFixedOffset ) { diff --git a/src/core/pointcloud/qgspointcloudrenderer.h b/src/core/pointcloud/qgspointcloudrenderer.h index a91f3e75289..0a03af8fc6f 100644 --- a/src/core/pointcloud/qgspointcloudrenderer.h +++ b/src/core/pointcloud/qgspointcloudrenderer.h @@ -47,11 +47,14 @@ class CORE_EXPORT QgsPointCloudRenderContext * The \a scale and \a offset arguments specify the scale and offset of the layer's int32 coordinates * compared to CRS coordinates respectively. * + * The \a zValueScale argument specifies any constant scaling factor which must be applied to z values + * taken from the point cloud index. + * * The \a zValueFixedOffset argument specifies any constant offset value which must be added to z values * taken from the point cloud index. */ QgsPointCloudRenderContext( QgsRenderContext &context, const QgsVector3D &scale, const QgsVector3D &offset, - double zValueFixedOffset ); + double zValueScale, double zValueFixedOffset ); //! QgsPointCloudRenderContext cannot be copied. QgsPointCloudRenderContext( const QgsPointCloudRenderContext &rh ) = delete; @@ -136,8 +139,17 @@ class CORE_EXPORT QgsPointCloudRenderContext */ int zOffset() const { return mZOffset; } + /** + * Returns any constant scaling factor which must be applied to z values taken from the point cloud index. + * + * \note Scaling of z values should be applied before the zValueFixedOffset(). + */ + double zValueScale() const { return mZValueScale; } + /** * Returns any constant offset which must be applied to z values taken from the point cloud index. + * + * \note Scaling of z values via zValueScale() should be applied before the zValueFixedOffset(). */ double zValueFixedOffset() const { return mZValueFixedOffset; } @@ -193,6 +205,7 @@ class CORE_EXPORT QgsPointCloudRenderContext int mXOffset = 0; int mYOffset = 0; int mZOffset = 0; + double mZValueScale = 1.0; double mZValueFixedOffset = 0; }; @@ -463,7 +476,7 @@ class CORE_EXPORT QgsPointCloudRenderer static double pointZ( QgsPointCloudRenderContext &context, const char *ptr, int i ) { const qint32 iz = *reinterpret_cast( ptr + i * context.pointRecordSize() + context.zOffset() ); - return context.offset().z() + context.scale().z() * iz + context.zValueFixedOffset(); + return ( context.offset().z() + context.scale().z() * iz ) * context.zValueScale() + context.zValueFixedOffset(); } /** diff --git a/src/ui/pointcloud/qgspointcloudelevationpropertieswidgetbase.ui b/src/ui/pointcloud/qgspointcloudelevationpropertieswidgetbase.ui index 92668edfc60..bef6865de6b 100644 --- a/src/ui/pointcloud/qgspointcloudelevationpropertieswidgetbase.ui +++ b/src/ui/pointcloud/qgspointcloudelevationpropertieswidgetbase.ui @@ -7,7 +7,7 @@ 0 0 400 - 300 + 270 @@ -41,21 +41,37 @@ vectorgeneral - - - - Qt::Vertical + + + + 6 + + + 0.000000000000000 + + + 99999999999.000000000000000 + + + 1.000000000000000 - + - Offset elevation + Scale - + + + + Offset + + + + 6 @@ -68,6 +84,23 @@ + + + + Qt::Vertical + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Elevation scaling and offset can be used to manually correct elevation values in the point cloud at render time.</span></p><p>The scale is applied to the point cloud elevation values before adding the offset.</p></body></html> + + + true + + + diff --git a/tests/src/providers/testqgseptprovider.cpp b/tests/src/providers/testqgseptprovider.cpp index d8721b61eb6..d6014f3d1e2 100644 --- a/tests/src/providers/testqgseptprovider.cpp +++ b/tests/src/providers/testqgseptprovider.cpp @@ -244,8 +244,15 @@ void TestQgsEptProvider::calculateZRange() QVERIFY( layer->isValid() ); QgsDoubleRange range = layer->elevationProperties()->calculateZRange( layer.get() ); - QGSCOMPARENEAR( range.lower(), 1, 74.34 ); - QGSCOMPARENEAR( range.upper(), 2, 80.02 ); + 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 ); } diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 2474b33c212..244f1f8090e 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -213,6 +213,7 @@ ADD_PYTHON_TEST(PyQgsPointCloudAttributeComboBox test_qgspointcloudattributecomb ADD_PYTHON_TEST(PyQgsPointCloudAttributeModel test_qgspointcloudattributemodel.py) ADD_PYTHON_TEST(PyQgsPointCloudClassifiedRenderer test_qgspointcloudclassifiedrenderer.py) ADD_PYTHON_TEST(PyQgsPointCloudDataProvider test_qgspointcloudprovider.py) +ADD_PYTHON_TEST(PyQgsPointCloudElevationProperties test_qgspointcloudelevationproperties.py) ADD_PYTHON_TEST(PyQgsPointCloudExtentRenderer test_qgspointcloudextentrenderer.py) ADD_PYTHON_TEST(PyQgsPointCloudRgbRenderer test_qgspointcloudrgbrenderer.py) ADD_PYTHON_TEST(PyQgsPointClusterRenderer test_qgspointclusterrenderer.py) diff --git a/tests/src/python/test_qgspointcloudattributebyramprenderer.py b/tests/src/python/test_qgspointcloudattributebyramprenderer.py index fba6995418f..a4c23b4715e 100644 --- a/tests/src/python/test_qgspointcloudattributebyramprenderer.py +++ b/tests/src/python/test_qgspointcloudattributebyramprenderer.py @@ -129,7 +129,7 @@ class TestQgsPointCloudAttributeByRampRenderer(unittest.TestCase): renderer.setAttribute('attr') rc = QgsRenderContext() - prc = QgsPointCloudRenderContext(rc, QgsVector3D(), QgsVector3D()) + prc = QgsPointCloudRenderContext(rc, QgsVector3D(), QgsVector3D(), 1, 0) self.assertEqual(renderer.usedAttributes(prc), {'attr'}) diff --git a/tests/src/python/test_qgspointcloudclassifiedrenderer.py b/tests/src/python/test_qgspointcloudclassifiedrenderer.py index 0f911f8a3e3..3ada35f1ab4 100644 --- a/tests/src/python/test_qgspointcloudclassifiedrenderer.py +++ b/tests/src/python/test_qgspointcloudclassifiedrenderer.py @@ -107,7 +107,7 @@ class TestQgsPointCloudClassifiedRenderer(unittest.TestCase): renderer.setAttribute('attr') rc = QgsRenderContext() - prc = QgsPointCloudRenderContext(rc, QgsVector3D(), QgsVector3D()) + prc = QgsPointCloudRenderContext(rc, QgsVector3D(), QgsVector3D(), 1, 0) self.assertEqual(renderer.usedAttributes(prc), {'attr'}) diff --git a/tests/src/python/test_qgspointcloudelevationproperties.py b/tests/src/python/test_qgspointcloudelevationproperties.py new file mode 100644 index 00000000000..c7e2b830556 --- /dev/null +++ b/tests/src/python/test_qgspointcloudelevationproperties.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsPointCloudLayerElevationProperties + +.. note:: 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. +""" +__author__ = 'Nyall Dawson' +__date__ = '09/11/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' + +import qgis # NOQA + +from qgis.core import ( + QgsPointCloudLayerElevationProperties, + QgsReadWriteContext, +) + +from qgis.PyQt.QtXml import QDomDocument + +from qgis.testing import start_app, unittest + +start_app() + + +class TestQgsPointCloudElevationProperties(unittest.TestCase): + + def testBasic(self): + props = QgsPointCloudLayerElevationProperties(None) + self.assertEqual(props.zScale(), 1) + self.assertEqual(props.zOffset(), 0) + + props.setZOffset(0.5) + props.setZScale(2) + self.assertEqual(props.zScale(), 2) + self.assertEqual(props.zOffset(), 0.5) + + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + props.writeXml(elem, doc, QgsReadWriteContext()) + + props2 = QgsPointCloudLayerElevationProperties(None) + props2.readXml(elem, QgsReadWriteContext()) + self.assertEqual(props2.zScale(), 2) + self.assertEqual(props2.zOffset(), 0.5) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgspointcloudrgbrenderer.py b/tests/src/python/test_qgspointcloudrgbrenderer.py index b51988705d9..6024d1f6f5b 100644 --- a/tests/src/python/test_qgspointcloudrgbrenderer.py +++ b/tests/src/python/test_qgspointcloudrgbrenderer.py @@ -170,7 +170,7 @@ class TestQgsPointCloudRgbRenderer(unittest.TestCase): renderer.setRedAttribute('r') rc = QgsRenderContext() - prc = QgsPointCloudRenderContext(rc, QgsVector3D(), QgsVector3D()) + prc = QgsPointCloudRenderContext(rc, QgsVector3D(), QgsVector3D(), 1, 0) self.assertEqual(renderer.usedAttributes(prc), {'r', 'g', 'b'})