Expression variable for the currently rendered part

During rendering, two new variables will be available:

  * `geometry_part_count`
  * `geometry_part_num` (1-based index)

Useful to apply different styles to different parts of multipart
features
This commit is contained in:
Matthias Kuhn 2016-01-08 13:42:53 +01:00
parent 3da051b989
commit 4e9afcec4c
12 changed files with 228 additions and 9 deletions

View File

@ -344,6 +344,11 @@ class QgsExpressionContext
*/
void appendScope( QgsExpressionContextScope* scope /Transfer/ );
/**
* Remove the last scope from the expression context.
*/
void popScope();
/** Appends a scope to the end of the context. This scope will override
* any matching variables or functions provided by existing scopes within the
* context. Ownership of the scope is transferred to the stack.

View File

@ -191,6 +191,13 @@ class QgsSymbolV2
*/
void renderFeature( const QgsFeature& feature, QgsRenderContext& context, int layer = -1, bool selected = false, bool drawVertexMarker = false, int currentVertexMarkerType = 0, int currentVertexMarkerSize = 0 );
/**
* Returns the symbol render context. Only valid between startRender and stopRender calls.
*
* @return The symbol render context
*/
QgsSymbolV2RenderContext* symbolRenderContext();
protected:
QgsSymbolV2( SymbolType type, const QgsSymbolLayerV2List& layers /Transfer/ ); // can't be instantiated
@ -283,6 +290,21 @@ class QgsSymbolV2RenderContext
// workaround for sip 4.7. Don't use assignment - will fail with assertion error
// QgsSymbolV2RenderContext& operator=( const QgsSymbolV2RenderContext& );
/**
* This scope is always available when a symbol of this type is being rendered.
*
* @return An expression scope for details about this symbol
*/
QgsExpressionContextScope* expressionContextScope();
/**
* Set an expression scope for this symbol.
*
* Will take ownership.
*
* @param contextScope An expression scope for details about this symbol
*/
void setExpressionContextScope( QgsExpressionContextScope* contextScope /Transfer/);
};

View File

@ -381,6 +381,12 @@ void QgsExpressionContext::appendScope( QgsExpressionContextScope* scope )
mStack.append( scope );
}
void QgsExpressionContext::popScope()
{
if ( !mStack.isEmpty() )
mStack.pop_back();
}
QgsExpressionContext& QgsExpressionContext::operator<<( QgsExpressionContextScope* scope )
{
mStack.append( scope );

View File

@ -378,6 +378,11 @@ class CORE_EXPORT QgsExpressionContext
*/
void appendScope( QgsExpressionContextScope* scope );
/**
* Remove the last scope from the expression context.
*/
void popScope();
/** Appends a scope to the end of the context. This scope will override
* any matching variables or functions provided by existing scopes within the
* context. Ownership of the scope is transferred to the stack.

View File

@ -186,17 +186,20 @@ bool QgsGeometryGeneratorSymbolLayerV2::isCompatibleWithSymbol( QgsSymbolV2* sym
Q_UNUSED( symbol )
return true;
}
void QgsGeometryGeneratorSymbolLayerV2::render( QgsSymbolV2RenderContext& context )
{
QgsGeometry geom = mExpression->evaluate( &context.renderContext().expressionContext() ).value<QgsGeometry>();
if ( context.feature() )
{
QgsFeature f = *context.feature();
QgsExpressionContext& expressionContext = context.renderContext().expressionContext();
QgsFeature f = expressionContext.feature();
QgsGeometry geom = mExpression->evaluate( &expressionContext ).value<QgsGeometry>();
f.setGeometry( geom );
QgsExpressionContextScope* subSymbolExpressionContextScope = mSymbol->symbolRenderContext()->expressionContextScope();
subSymbolExpressionContextScope->setFeature( f );
mSymbol->renderFeature( f, context.renderContext() );
}
}

View File

@ -88,6 +88,7 @@ QgsSymbolV2::QgsSymbolV2( SymbolType type, const QgsSymbolLayerV2List& layers )
, mRenderHints( 0 )
, mClipFeaturesToExtent( true )
, mLayer( nullptr )
, mSymbolRenderContext( nullptr )
{
// check they're all correct symbol layers
@ -235,6 +236,7 @@ const unsigned char* QgsSymbolV2::_getPolygon( QPolygonF& pts, QList<QPolygonF>&
QgsSymbolV2::~QgsSymbolV2()
{
delete mSymbolRenderContext;
// delete all symbol layers (we own them, so it's okay)
qDeleteAll( mLayers );
}
@ -434,8 +436,14 @@ bool QgsSymbolV2::changeSymbolLayer( int index, QgsSymbolLayerV2* layer )
void QgsSymbolV2::startRender( QgsRenderContext& context, const QgsFields* fields )
{
delete mSymbolRenderContext;
mSymbolRenderContext = new QgsSymbolV2RenderContext( context, outputUnit(), mAlpha, false, mRenderHints, nullptr, fields, mapUnitScale() );
QgsSymbolV2RenderContext symbolContext( context, outputUnit(), mAlpha, false, mRenderHints, nullptr, fields, mapUnitScale() );
QgsExpressionContextScope* scope = new QgsExpressionContextScope( QApplication::translate( "QgsSymbolV2", "Symbol Scope" ) );
mSymbolRenderContext->setExpressionContextScope( scope );
Q_FOREACH ( QgsSymbolLayerV2* layer, mLayers )
layer->startRender( symbolContext );
@ -443,10 +451,12 @@ void QgsSymbolV2::startRender( QgsRenderContext& context, const QgsFields* field
void QgsSymbolV2::stopRender( QgsRenderContext& context )
{
QgsSymbolV2RenderContext symbolContext( context, outputUnit(), mAlpha, false, mRenderHints, nullptr, nullptr, mapUnitScale() );
Q_UNUSED( context )
Q_FOREACH ( QgsSymbolLayerV2* layer, mLayers )
layer->stopRender( symbolContext );
layer->stopRender( *mSymbolRenderContext );
delete mSymbolRenderContext;
mSymbolRenderContext = nullptr;
mLayer = nullptr;
}
@ -701,6 +711,10 @@ void QgsSymbolV2::renderFeature( const QgsFeature& feature, QgsRenderContext& co
deleteSegmentizedGeometry = true;
}
context.expressionContext().appendScope( mSymbolRenderContext->expressionContextScope() );
mSymbolRenderContext->expressionContextScope()->setVariable( "geometry_part_count", segmentizedGeometry->geometry()->partCount() );
mSymbolRenderContext->expressionContextScope()->setVariable( "geometry_part_num", 1 );
switch ( QgsWKBTypes::flatType( segmentizedGeometry->geometry()->wkbType() ) )
{
case QgsWKBTypes::Point:
@ -766,6 +780,8 @@ void QgsSymbolV2::renderFeature( const QgsFeature& feature, QgsRenderContext& co
for ( int i = 0; i < mp->numGeometries(); ++i )
{
mSymbolRenderContext->expressionContextScope()->setVariable( "geometry_part_num", i + 1 );
const QgsPointV2* point = static_cast< const QgsPointV2* >( mp->geometryN( i ) );
_getPoint( pt, context, point );
static_cast<QgsMarkerSymbolV2*>( this )->renderPoint( pt, &feature, context, layer, selected );
@ -793,6 +809,8 @@ void QgsSymbolV2::renderFeature( const QgsFeature& feature, QgsRenderContext& co
for ( unsigned int i = 0; i < num; ++i )
{
mSymbolRenderContext->expressionContextScope()->setVariable( "geometry_part_num", i + 1 );
if ( geomCollection )
{
context.setGeometry( geomCollection->geometryN( i ) );
@ -824,6 +842,8 @@ void QgsSymbolV2::renderFeature( const QgsFeature& feature, QgsRenderContext& co
for ( unsigned int i = 0; i < num; ++i )
{
mSymbolRenderContext->expressionContextScope()->setVariable( "geometry_part_num", i + 1 );
if ( geomCollection )
{
context.setGeometry( geomCollection->geometryN( i ) );
@ -869,6 +889,13 @@ void QgsSymbolV2::renderFeature( const QgsFeature& feature, QgsRenderContext& co
{
delete segmentizedGeometry;
}
context.expressionContext().popScope();
}
QgsSymbolV2RenderContext* QgsSymbolV2::symbolRenderContext()
{
return mSymbolRenderContext;
}
////////////////////
@ -876,6 +903,7 @@ void QgsSymbolV2::renderFeature( const QgsFeature& feature, QgsRenderContext& co
QgsSymbolV2RenderContext::QgsSymbolV2RenderContext( QgsRenderContext& c, QgsSymbolV2::OutputUnit u, qreal alpha, bool selected, int renderHints, const QgsFeature* f, const QgsFields* fields, const QgsMapUnitScale& mapUnitScale )
: mRenderContext( c ),
mExpressionContextScope( nullptr ),
mOutputUnit( u ),
mMapUnitScale( mapUnitScale ),
mAlpha( alpha ),
@ -888,7 +916,7 @@ QgsSymbolV2RenderContext::QgsSymbolV2RenderContext( QgsRenderContext& c, QgsSymb
QgsSymbolV2RenderContext::~QgsSymbolV2RenderContext()
{
delete mExpressionContextScope;
}
void QgsSymbolV2RenderContext::setOriginalValueVariable( const QVariant& value )
@ -916,6 +944,16 @@ QgsSymbolV2RenderContext& QgsSymbolV2RenderContext::operator=( const QgsSymbolV2
return *this;
}
QgsExpressionContextScope* QgsSymbolV2RenderContext::expressionContextScope()
{
return mExpressionContextScope;
}
void QgsSymbolV2RenderContext::setExpressionContextScope( QgsExpressionContextScope* contextScope )
{
mExpressionContextScope = contextScope;
}
///////////////////
QgsMarkerSymbolV2* QgsMarkerSymbolV2::createSimple( const QgsStringMap& properties )

View File

@ -241,6 +241,13 @@ class CORE_EXPORT QgsSymbolV2
*/
void renderFeature( const QgsFeature& feature, QgsRenderContext& context, int layer = -1, bool selected = false, bool drawVertexMarker = false, int currentVertexMarkerType = 0, int currentVertexMarkerSize = 0 );
/**
* Returns the symbol render context. Only valid between startRender and stopRender calls.
*
* @return The symbol render context
*/
QgsSymbolV2RenderContext* symbolRenderContext();
protected:
QgsSymbolV2( SymbolType type, const QgsSymbolLayerV2List& layers ); // can't be instantiated
@ -310,6 +317,9 @@ class CORE_EXPORT QgsSymbolV2
const QgsVectorLayer* mLayer; //current vectorlayer
private:
//! Initialized in startRender, destroyed in stopRender
QgsSymbolV2RenderContext* mSymbolRenderContext;
Q_DISABLE_COPY( QgsSymbolV2 )
};
@ -365,8 +375,24 @@ class CORE_EXPORT QgsSymbolV2RenderContext
// workaround for sip 4.7. Don't use assignment - will fail with assertion error
QgsSymbolV2RenderContext& operator=( const QgsSymbolV2RenderContext& );
/**
* This scope is always available when a symbol of this type is being rendered.
*
* @return An expression scope for details about this symbol
*/
QgsExpressionContextScope* expressionContextScope();
/**
* Set an expression scope for this symbol.
*
* Will take ownership.
*
* @param contextScope An expression scope for details about this symbol
*/
void setExpressionContextScope( QgsExpressionContextScope* contextScope );
private:
QgsRenderContext& mRenderContext;
QgsExpressionContextScope* mExpressionContextScope;
QgsSymbolV2::OutputUnit mOutputUnit;
QgsMapUnitScale mMapUnitScale;
qreal mAlpha;

View File

@ -219,6 +219,9 @@ void TestQgsExpressionContext::contextScopeFunctions()
void TestQgsExpressionContext::contextStack()
{
QgsExpressionContext context;
context.popScope();
//test retrieving from empty context
QVERIFY( !context.hasVariable( "test" ) );
QVERIFY( !context.variable( "test" ).isValid() );
@ -290,6 +293,11 @@ void TestQgsExpressionContext::contextStack()
scope2->addVariable( QgsExpressionContextScope::StaticVariable( "readonly", 5, true ) );
QVERIFY( context.isReadOnly( "readonly" ) );
QVERIFY( !context.isReadOnly( "test" ) );
// Check scopes can be popped
context.popScope();
QCOMPARE( scopes.length(), 2 );
QCOMPARE( scopes.at( 0 ), scope1 );
}
void TestQgsExpressionContext::contextCopy()

View File

@ -59,6 +59,7 @@ ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py)
ADD_PYTHON_TEST(PyQgsSpatialIndex test_qgsspatialindex.py)
ADD_PYTHON_TEST(PyQgsSpatialiteProvider test_provider_spatialite.py)
ADD_PYTHON_TEST(PyQgsSymbolLayerV2 test_qgssymbollayerv2.py)
ADD_PYTHON_TEST(PyQgsSymbolExpressionVariables test_qgssymbolexpressionvariables.py)
ADD_PYTHON_TEST(PyQgsSyntacticSugar test_syntactic_sugar.py)
ADD_PYTHON_TEST(PyQgsVectorColorRamp test_qgsvectorcolorramp.py)
ADD_PYTHON_TEST(PyQgsVectorFileWriter test_qgsvectorfilewriter.py)

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
"""
***************************************************************************
test_qgssymbolexpressionvariables.py
---------------------
Date : January 2016
Copyright : (C) 2016 by Matthias Kuhn
Email : matthias at opengis dot ch
***************************************************************************
* *
* 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__ = 'Matthias Kuhn'
__date__ = 'January 2016'
__copyright__ = '(C) 2016, Matthiasd Kuhn'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import qgis
import os
from PyQt4.QtCore import QSize
from qgis.core import (QgsVectorLayer,
QgsMapLayerRegistry,
QgsRectangle,
QgsMultiRenderChecker,
QgsSingleSymbolRendererV2,
QgsFillSymbolV2,
QgsMarkerSymbolV2,
QgsRendererCategoryV2,
QgsCategorizedSymbolRendererV2,
QgsGraduatedSymbolRendererV2,
QgsRendererRangeV2,
QgsFeatureRequest
)
from utilities import (unitTestDataPath,
getQgisTestApp,
TestCase,
unittest
)
# Convenience instances in case you may need them
# not used in this test
QGISAPP, CANVAS, IFACE, PARENT = getQgisTestApp()
TEST_DATA_DIR = unitTestDataPath()
class TestQgsSymbolExpressionVariables(TestCase):
def setUp(self):
myShpFile = os.path.join(TEST_DATA_DIR, 'polys.shp')
self.layer = QgsVectorLayer(myShpFile, 'Polys', 'ogr')
QgsMapLayerRegistry.instance().addMapLayer(self.layer)
rendered_layers = [self.layer.id()]
self.mapsettings = CANVAS.mapSettings()
self.mapsettings.setOutputSize(QSize(400, 400))
self.mapsettings.setOutputDpi(96)
self.mapsettings.setExtent(QgsRectangle(-163, 22, -70, 52))
self.mapsettings.setLayers(rendered_layers)
def tearDown(self):
QgsMapLayerRegistry.instance().removeAllMapLayers()
def testPartNum(self):
# Create rulebased style
sym1 = QgsFillSymbolV2.createSimple({'color': '#fdbf6f'})
renderer = QgsSingleSymbolRendererV2(sym1)
renderer.symbols()[0].symbolLayers()[0].setDataDefinedProperty('color', 'color_rgb( (@geometry_part_num - 1) * 200, 0, 0 )')
self.layer.setRendererV2(renderer)
# Setup rendering check
renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(self.mapsettings)
renderchecker.setControlName('expected_geometry_part_num')
result = renderchecker.runTest('part_geometry_part_num')
self.assertTrue(result)
def testPartCount(self):
# Create rulebased style
sym1 = QgsFillSymbolV2.createSimple({'color': '#fdbf6f'})
renderer = QgsSingleSymbolRendererV2(sym1)
renderer.symbols()[0].symbolLayers()[0].setDataDefinedProperty('color', 'color_rgb( (@geometry_part_count - 1) * 200, 0, 0 )')
self.layer.setRendererV2(renderer)
# Setup rendering check
renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(self.mapsettings)
renderchecker.setControlName('expected_geometry_part_count')
result = renderchecker.runTest('part_geometry_part_count')
self.assertTrue(result)
if __name__ == '__main__':
unittest.main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB