From c013c7828577d9d8daa9af35c4a3a3b9563f0ff4 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 24 Jun 2021 13:10:05 +1000 Subject: [PATCH] [api] Attach QgsPropertyCollection to QgsRasterPipe to allow for data-defined raster pipeline properties --- .../raster/qgsrasterpipe.sip.in | 44 +++++++++++++ src/core/raster/qgsrasterlayer.cpp | 9 +++ src/core/raster/qgsrasterlayerrenderer.cpp | 2 + src/core/raster/qgsrasterpipe.cpp | 46 +++++++++++++ src/core/raster/qgsrasterpipe.h | 61 +++++++++++++++++ tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgsrasterlayer.py | 59 ++++++++++++++++- tests/src/python/test_qgsrasterpipe.py | 62 ++++++++++++++++++ .../expected_raster_data_defined_opacity.png | Bin 0 -> 40242 bytes 9 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 tests/src/python/test_qgsrasterpipe.py create mode 100644 tests/testdata/control_images/expected_raster_data_defined_opacity/expected_raster_data_defined_opacity.png diff --git a/python/core/auto_generated/raster/qgsrasterpipe.sip.in b/python/core/auto_generated/raster/qgsrasterpipe.sip.in index 128aa7c0fd5..937524b2fde 100644 --- a/python/core/auto_generated/raster/qgsrasterpipe.sip.in +++ b/python/core/auto_generated/raster/qgsrasterpipe.sip.in @@ -11,6 +11,7 @@ + class QgsRasterPipe { %Docstring(signature="appended") @@ -22,6 +23,11 @@ Contains a pipeline of raster interfaces for sequential raster processing. %End public: + enum Property + { + RendererOpacity, + }; + QgsRasterPipe(); %Docstring Constructor for an empty QgsRasterPipe. @@ -182,6 +188,44 @@ Returns which stage of the pipe should apply resampling .. seealso:: :py:func:`setResamplingStage` .. versionadded:: 3.16 +%End + + QgsPropertyCollection &dataDefinedProperties(); +%Docstring +Returns a reference to the pipe's property collection, used for data defined overrides. + +.. seealso:: :py:func:`setDataDefinedProperties` + +.. versionadded:: 3.22 +%End + + + void setDataDefinedProperties( const QgsPropertyCollection &collection ); +%Docstring +Sets the pipe's property ``collection``, used for data defined overrides. + +Any existing properties will be discarded. + +.. seealso:: :py:func:`dataDefinedProperties` + +.. seealso:: Property + +.. versionadded:: 3.22 +%End + + void evaluateDataDefinedProperties( QgsExpressionContext &context ); +%Docstring +Evalutes any data defined properties set on the pipe, applying their results +to the corresponding interfaces in place. + +.. versionadded:: 3.22 +%End + + static QgsPropertiesDefinition propertyDefinitions(); +%Docstring +Returns the definitions for data defined properties available for use in raster pipes. + +.. versionadded:: 3.22 %End private: diff --git a/src/core/raster/qgsrasterlayer.cpp b/src/core/raster/qgsrasterlayer.cpp index 1b617c1dfa2..81dbdfdb734 100644 --- a/src/core/raster/qgsrasterlayer.cpp +++ b/src/core/raster/qgsrasterlayer.cpp @@ -168,6 +168,7 @@ QgsRasterLayer *QgsRasterLayer::clone() const if ( mPipe->at( i ) ) layer->pipe()->set( mPipe->at( i )->clone() ); } + layer->pipe()->setDataDefinedProperties( mPipe->dataDefinedProperties() ); return layer; } @@ -1979,6 +1980,10 @@ bool QgsRasterLayer::readSymbology( const QDomNode &layer_node, QString &errorMe setBlendMode( QgsPainting::getCompositionMode( static_cast< QgsPainting::BlendMode >( e.text().toInt() ) ) ); } + QDomElement elemDataDefinedProperties = layer_node.firstChildElement( QStringLiteral( "pipe-data-defined-properties" ) ); + if ( !elemDataDefinedProperties.isNull() ) + mPipe->dataDefinedProperties().readXml( elemDataDefinedProperties, QgsRasterPipe::propertyDefinitions() ); + readCustomProperties( layer_node ); return true; @@ -2181,6 +2186,10 @@ bool QgsRasterLayer::writeSymbology( QDomNode &layer_node, QDomDocument &documen interface->writeXml( document, pipeElement ); } + QDomElement elemDataDefinedProperties = document.createElement( QStringLiteral( "pipe-data-defined-properties" ) ); + mPipe->dataDefinedProperties().writeXml( elemDataDefinedProperties, QgsRasterPipe::propertyDefinitions() ); + layer_node.appendChild( elemDataDefinedProperties ); + QDomElement resamplingStageElement = document.createElement( QStringLiteral( "resamplingStage" ) ); QDomText resamplingStageText = document.createTextNode( resamplingStage() == Qgis::RasterResamplingStage::Provider ? QStringLiteral( "provider" ) : QStringLiteral( "resamplingFilter" ) ); resamplingStageElement.appendChild( resamplingStageText ); diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index 07ec3a32e83..7ce3581fc36 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -254,6 +254,8 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender layer->refreshRendererIfNeeded( rasterRenderer, rendererContext.extent() ); } + mPipe->evaluateDataDefinedProperties( rendererContext.expressionContext() ); + const QgsRasterLayerTemporalProperties *temporalProperties = qobject_cast< const QgsRasterLayerTemporalProperties * >( layer->temporalProperties() ); if ( temporalProperties->isActive() && renderContext()->isTemporal() ) { diff --git a/src/core/raster/qgsrasterpipe.cpp b/src/core/raster/qgsrasterpipe.cpp index 4199cc6bcf0..3a91f11a46a 100644 --- a/src/core/raster/qgsrasterpipe.cpp +++ b/src/core/raster/qgsrasterpipe.cpp @@ -29,6 +29,8 @@ #include "qgsrasterprojector.h" #include "qgsrasternuller.h" +#include + QgsRasterPipe::QgsRasterPipe( const QgsRasterPipe &pipe ) { for ( int i = 0; i < pipe.size(); i++ ) @@ -49,6 +51,7 @@ QgsRasterPipe::QgsRasterPipe( const QgsRasterPipe &pipe ) } } setResamplingStage( pipe.resamplingStage() ); + mDataDefinedProperties = pipe.mDataDefinedProperties; } QgsRasterPipe::~QgsRasterPipe() @@ -381,3 +384,46 @@ void QgsRasterPipe::setResamplingStage( Qgis::RasterResamplingStage stage ) l_provider->enableProviderResampling( stage == Qgis::RasterResamplingStage::Provider ); } } + +void QgsRasterPipe::evaluateDataDefinedProperties( QgsExpressionContext &context ) +{ + if ( !mDataDefinedProperties.hasActiveProperties() ) + return; + + if ( mDataDefinedProperties.isActive( RendererOpacity ) ) + { + if ( QgsRasterRenderer *r = renderer() ) + { + const double prevOpacity = r->opacity(); + context.setOriginalValueVariable( prevOpacity * 100 ); + bool ok = false; + const double opacity = mDataDefinedProperties.valueAsDouble( RendererOpacity, context, prevOpacity, &ok ) / 100; + if ( ok ) + { + r->setOpacity( opacity ); + } + } + } +} + +QgsPropertiesDefinition QgsRasterPipe::sPropertyDefinitions; + +void QgsRasterPipe::initPropertyDefinitions() +{ + const QString origin = QStringLiteral( "raster" ); + + sPropertyDefinitions = QgsPropertiesDefinition + { + { QgsRasterPipe::RendererOpacity, QgsPropertyDefinition( "RendererOpacity", QObject::tr( "Renderer opacity" ), QgsPropertyDefinition::Opacity, origin ) }, + }; +} + +QgsPropertiesDefinition QgsRasterPipe::propertyDefinitions() +{ + static std::once_flag initialized; + std::call_once( initialized, [ = ]( ) + { + initPropertyDefinitions(); + } ); + return sPropertyDefinitions; +} diff --git a/src/core/raster/qgsrasterpipe.h b/src/core/raster/qgsrasterpipe.h index 00e2599334f..3fce19ed317 100644 --- a/src/core/raster/qgsrasterpipe.h +++ b/src/core/raster/qgsrasterpipe.h @@ -21,6 +21,8 @@ #include "qgis_core.h" #include "qgis_sip.h" #include "qgis.h" +#include "qgspropertycollection.h" + #include #include #include @@ -48,6 +50,15 @@ class CORE_EXPORT QgsRasterPipe { public: + /** + * Data definable properties. + * \since QGIS 3.22 + */ + enum Property + { + RendererOpacity, //!< Raster renderer global opacity + }; + /** * Constructor for an empty QgsRasterPipe. */ @@ -213,6 +224,48 @@ class CORE_EXPORT QgsRasterPipe */ Qgis::RasterResamplingStage resamplingStage() const { return mResamplingStage; } + /** + * Returns a reference to the pipe's property collection, used for data defined overrides. + * \see setDataDefinedProperties() + * \since QGIS 3.22 + */ + QgsPropertyCollection &dataDefinedProperties() { return mDataDefinedProperties; } + + /** + * Returns a reference to the pipe's property collection, used for data defined overrides. + * \see setDataDefinedProperties() + * \see Property + * \note not available in Python bindings + * \since QGIS 3.22 + */ + const QgsPropertyCollection &dataDefinedProperties() const SIP_SKIP { return mDataDefinedProperties; } + + /** + * Sets the pipe's property \a collection, used for data defined overrides. + * + * Any existing properties will be discarded. + * + * \see dataDefinedProperties() + * \see Property + * \since QGIS 3.22 + */ + void setDataDefinedProperties( const QgsPropertyCollection &collection ) { mDataDefinedProperties = collection; } + + /** + * Evalutes any data defined properties set on the pipe, applying their results + * to the corresponding interfaces in place. + * + * \since QGIS 3.22 + */ + void evaluateDataDefinedProperties( QgsExpressionContext &context ); + + /** + * Returns the definitions for data defined properties available for use in raster pipes. + * + * \since QGIS 3.22 + */ + static QgsPropertiesDefinition propertyDefinitions(); + private: #ifdef SIP_RUN QgsRasterPipe( const QgsRasterPipe &pipe ); @@ -245,6 +298,14 @@ class CORE_EXPORT QgsRasterPipe bool connect( QVector interfaces ); Qgis::RasterResamplingStage mResamplingStage = Qgis::RasterResamplingStage::ResampleFilter; + + //! Property collection for data defined raster pipe settings + QgsPropertyCollection mDataDefinedProperties; + + //! Property definitions + static QgsPropertiesDefinition sPropertyDefinitions; + + static void initPropertyDefinitions(); }; #endif diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 698f90a0a5d..699e57e628b 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -256,6 +256,7 @@ ADD_PYTHON_TEST(PyQgsRasterFileWriterTask test_qgsrasterfilewritertask.py) ADD_PYTHON_TEST(PyQgsRasterLayer test_qgsrasterlayer.py) ADD_PYTHON_TEST(PyQgsRasterLayerRenderer test_qgsrasterlayerrenderer.py) ADD_PYTHON_TEST(PyQgsRasterColorRampShader test_qgsrastercolorrampshader.py) +ADD_PYTHON_TEST(PyQgsRasterPipe test_qgsrasterpipe.py) ADD_PYTHON_TEST(PyQgsRasterRange test_qgsrasterrange.py) ADD_PYTHON_TEST(PyQgsRasterRendererUtils test_qgsrasterrendererutils.py) ADD_PYTHON_TEST(PyQgsRasterResampler test_qgsrasterresampler.py) diff --git a/tests/src/python/test_qgsrasterlayer.py b/tests/src/python/test_qgsrasterlayer.py index 6f62b217d47..30fcbe4b9f5 100644 --- a/tests/src/python/test_qgsrasterlayer.py +++ b/tests/src/python/test_qgsrasterlayer.py @@ -55,7 +55,11 @@ from qgis.core import (Qgis, QgsRasterHistogram, QgsCubicRasterResampler, QgsBilinearRasterResampler, - QgsLayerDefinition + QgsLayerDefinition, + QgsRasterPipe, + QgsProperty, + QgsExpressionContext, + QgsExpressionContextScope ) from utilities import unitTestDataPath from qgis.testing import start_app, unittest @@ -1434,6 +1438,59 @@ class TestQgsRasterLayerTransformContext(unittest.TestCase): self.assertTrue( rl.transformContext().hasTransform(QgsCoordinateReferenceSystem('EPSG:4326'), QgsCoordinateReferenceSystem('EPSG:3857'))) + def test_save_restore_pipe_data_defined_settings(self): + """ + Test that raster pipe data defined settings are correctly saved/restored along with the layer + """ + rl = QgsRasterLayer(self.rpath, 'raster') + rl.pipe().dataDefinedProperties().setProperty(QgsRasterPipe.RendererOpacity, QgsProperty.fromExpression('100/2')) + + doc = QDomDocument() + layer_elem = doc.createElement("maplayer") + self.assertTrue(rl.writeLayerXml(layer_elem, doc, QgsReadWriteContext())) + + rl2 = QgsRasterLayer(self.rpath, 'raster') + self.assertEqual(rl2.pipe().dataDefinedProperties().property(QgsRasterPipe.RendererOpacity), + QgsProperty()) + + self.assertTrue(rl2.readXml(layer_elem, QgsReadWriteContext())) + self.assertEqual(rl2.pipe().dataDefinedProperties().property(QgsRasterPipe.RendererOpacity), + QgsProperty.fromExpression('100/2')) + + def test_render_data_defined_opacity(self): + path = os.path.join(unitTestDataPath('raster'), + 'band1_float32_noct_epsg4326.tif') + raster_layer = QgsRasterLayer(path, 'test') + self.assertTrue(raster_layer.isValid()) + + renderer = QgsSingleBandGrayRenderer(raster_layer.dataProvider(), 1) + raster_layer.setRenderer(renderer) + raster_layer.setContrastEnhancement( + QgsContrastEnhancement.StretchToMinimumMaximum, + QgsRasterMinMaxOrigin.MinMax) + + raster_layer.pipe().dataDefinedProperties().setProperty(QgsRasterPipe.RendererOpacity, QgsProperty.fromExpression('@layer_opacity')) + + ce = raster_layer.renderer().contrastEnhancement() + ce.setMinimumValue(-3.3319999287625854e+38) + ce.setMaximumValue(3.3999999521443642e+38) + + map_settings = QgsMapSettings() + map_settings.setLayers([raster_layer]) + map_settings.setExtent(raster_layer.extent()) + + context = QgsExpressionContext() + scope = QgsExpressionContextScope() + scope.setVariable('layer_opacity', 50) + context.appendScope(scope) + map_settings.setExpressionContext(context) + + checker = QgsRenderChecker() + checker.setControlName("expected_raster_data_defined_opacity") + checker.setMapSettings(map_settings) + + self.assertTrue(checker.runTest("raster_data_defined_opacity")) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsrasterpipe.py b/tests/src/python/test_qgsrasterpipe.py new file mode 100644 index 00000000000..540bb3bf722 --- /dev/null +++ b/tests/src/python/test_qgsrasterpipe.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + test_qgsrasterpipe.py + --------------------- + Date : June 2021 + Copyright : (C) 2021 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. * +* * +*************************************************************************** + +From build dir, run: ctest -R PyQgsRasterPipe\$ -V + +""" + +__author__ = 'Nyall Dawson' +__date__ = 'June 2021' +__copyright__ = '(C) 2021, Nyall Dawson' + +import qgis # NOQA +from qgis.core import (QgsRasterPipe, + QgsProperty, + QgsExpressionContext, + QgsSingleBandPseudoColorRenderer + ) +from qgis.testing import start_app, unittest + +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# not used in this test +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsRasterPipe(unittest.TestCase): + + def test_data_defined_properties(self): + pipe = QgsRasterPipe() + + pipe.dataDefinedProperties().setProperty(QgsRasterPipe.RendererOpacity, QgsProperty.fromExpression('100/2')) + self.assertEqual(pipe.dataDefinedProperties().property(QgsRasterPipe.RendererOpacity), + QgsProperty.fromExpression('100/2')) + + pipe.set(QgsSingleBandPseudoColorRenderer(None)) + self.assertTrue(pipe.renderer()) + self.assertEqual(pipe.renderer().opacity(), 1.0) + + # apply properties to pipe + pipe.evaluateDataDefinedProperties(QgsExpressionContext()) + self.assertEqual(pipe.renderer().opacity(), 0.5) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/control_images/expected_raster_data_defined_opacity/expected_raster_data_defined_opacity.png b/tests/testdata/control_images/expected_raster_data_defined_opacity/expected_raster_data_defined_opacity.png new file mode 100644 index 0000000000000000000000000000000000000000..6fec7077b220974afc06af5b3693adb4e3cb3262 GIT binary patch literal 40242 zcmeI5v1=4T9EE3(Lp0n5EJV;0B3ju6`~wnB&7lO5v#=AfQAC0^f&|4zEG#2}bsE8d zD2Sk)AcFn_3KphFZKsWTSJ)&O?hWj|8ImuM1U8R-zx}?QgJCb+qw|Yr>ZwChA%uFc z(7!PHeRcf3chBhO(vOwl=-0lbg-f?XnEo*Su2xP=9}eM281(0ccdBsf{^NVqxtq&> zN5P3-RaH&Qj00zH+?8@~o;;LtolZx}sq?A1Y`xOe60#A{Za>>N6d|OrdjKH_DU3j2 zl*>E*#= z*Z#j3VC!FZ?h#f?VQvwEkirNQHV!KUA%zhrY#deyLJA{L*f^{ZgcL@guyMLy-#(W{ zQlCp7KYdX;-}m=xQcmq{`Z>4StLD`Cc*&A(3ttE!KGsqbLL^MS5JG&cr6z<(n0z6G z_*hF#2$3-PLJ0A(mYNVEVe*B#>O0ca9jQ8c)4l3^>W*1G-{#!%#)f$ATq)a)4m_VyF2vJFyatJYjR%;NVk}~DkwMRg2-i=3M zq3IkeBt|;ZafD<7AFD-3j7+BE2+0IKR*R4rnM}tKk_mjQ79lY*nT{hQ6ZlvyLSkex z9dCT6{ztr!o%(z|cKPa#-nl|PpM3Rh(t817*Rv~+JLT+3z^NCFkW^wV=MmB};MCJ2 zB$Zgpd4#kKIQ8@hNhQ{D9w99QPCY$BQi-*kZ+zGOQye(gu3g?e=lTRWksmP=0ba69 zpw$|LsH99egqT39H3(5jnQ{m*fmUk}qLMP@5MlzY)*wVBWy)pSsW<%za_SCT&8h!m zuAWcLu|l@L^Iy!*?hj$+^`JjD+&*DATFuUUS;vrpbV>z6QfL{n2r)oTsX#~yEkhO| z2FNKD2uY!3$Rfl5Ii&(2DYOjP=0Ak}mo~NfkUiHHTHb0m{4kysqTN5X6hdsMwIYOQ zoNOtC*idUl2+=s%QV6l3)`}3Kak8ZlVneMJAw=V3OKluIu=;8J`jv6v;M8J&ZT|9u Ee@>`62mk;8 literal 0 HcmV?d00001