Skip to content

Commit

Permalink
[api] Attach QgsPropertyCollection to QgsRasterPipe to allow
Browse files Browse the repository at this point in the history
for data-defined raster pipeline properties
  • Loading branch information
nyalldawson committed Jun 25, 2021
1 parent 3c3059c commit c013c78
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 1 deletion.
44 changes: 44 additions & 0 deletions python/core/auto_generated/raster/qgsrasterpipe.sip.in
Expand Up @@ -11,6 +11,7 @@




class QgsRasterPipe
{
%Docstring(signature="appended")
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions src/core/raster/qgsrasterlayer.cpp
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 );
Expand Down
2 changes: 2 additions & 0 deletions src/core/raster/qgsrasterlayerrenderer.cpp
Expand Up @@ -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() )
{
Expand Down
46 changes: 46 additions & 0 deletions src/core/raster/qgsrasterpipe.cpp
Expand Up @@ -29,6 +29,8 @@
#include "qgsrasterprojector.h"
#include "qgsrasternuller.h"

#include <mutex>

QgsRasterPipe::QgsRasterPipe( const QgsRasterPipe &pipe )
{
for ( int i = 0; i < pipe.size(); i++ )
Expand All @@ -49,6 +51,7 @@ QgsRasterPipe::QgsRasterPipe( const QgsRasterPipe &pipe )
}
}
setResamplingStage( pipe.resamplingStage() );
mDataDefinedProperties = pipe.mDataDefinedProperties;
}

QgsRasterPipe::~QgsRasterPipe()
Expand Down Expand Up @@ -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;
}
61 changes: 61 additions & 0 deletions src/core/raster/qgsrasterpipe.h
Expand Up @@ -21,6 +21,8 @@
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgis.h"
#include "qgspropertycollection.h"

#include <QImage>
#include <QMap>
#include <QObject>
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -245,6 +298,14 @@ class CORE_EXPORT QgsRasterPipe
bool connect( QVector<QgsRasterInterface *> 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
Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -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)
Expand Down
59 changes: 58 additions & 1 deletion tests/src/python/test_qgsrasterlayer.py
Expand Up @@ -55,7 +55,11 @@
QgsRasterHistogram,
QgsCubicRasterResampler,
QgsBilinearRasterResampler,
QgsLayerDefinition
QgsLayerDefinition,
QgsRasterPipe,
QgsProperty,
QgsExpressionContext,
QgsExpressionContextScope
)
from utilities import unitTestDataPath
from qgis.testing import start_app, unittest
Expand Down Expand Up @@ -1434,6 +1438,59 @@ def testTransformContextIsSyncedFromProject(self):
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()

0 comments on commit c013c78

Please sign in to comment.