mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-15 00:04:00 -04:00
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:
parent
3da051b989
commit
4e9afcec4c
@ -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.
|
||||
|
@ -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/);
|
||||
};
|
||||
|
||||
|
||||
|
@ -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 );
|
||||
|
@ -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.
|
||||
|
@ -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() );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 )
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
105
tests/src/python/test_qgssymbolexpressionvariables.py
Normal file
105
tests/src/python/test_qgssymbolexpressionvariables.py
Normal 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()
|
BIN
tests/testdata/control_images/expected_geometry_part_count/expected_geometry_part_count.png
vendored
Normal file
BIN
tests/testdata/control_images/expected_geometry_part_count/expected_geometry_part_count.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 MiB |
BIN
tests/testdata/control_images/expected_geometry_part_num/expected_geometry_part_num.png
vendored
Normal file
BIN
tests/testdata/control_images/expected_geometry_part_num/expected_geometry_part_num.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 MiB |
Loading…
x
Reference in New Issue
Block a user