[api] Attach QgsPropertyCollection to QgsRasterPipe to allow

for data-defined raster pipeline properties
This commit is contained in:
Nyall Dawson 2021-06-24 13:10:05 +10:00
parent 3c3059c938
commit c013c78285
9 changed files with 283 additions and 1 deletions

View File

@ -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:

View File

@ -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 );

View File

@ -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() )
{

View File

@ -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++ )
@ -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;
}

View File

@ -21,6 +21,8 @@
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgis.h"
#include "qgspropertycollection.h"
#include <QImage>
#include <QMap>
#include <QObject>
@ -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<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

View File

@ -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)

View File

@ -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()

View File

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB