Skip to content

Commit

Permalink
Apply filters to feature request for categorized renderer
Browse files Browse the repository at this point in the history
Makes rendering much faster when only certain categories are checked,
as only the matching records for the displayed features are fetched
from the provider.
  • Loading branch information
nyalldawson committed Dec 3, 2015
1 parent 97e5d31 commit 9eee121
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 7 deletions.
2 changes: 2 additions & 0 deletions python/core/symbology-ng/qgscategorizedsymbolrendererv2.sip
Expand Up @@ -66,6 +66,8 @@ class QgsCategorizedSymbolRendererV2 : QgsFeatureRendererV2
//! returns bitwise OR-ed capabilities of the renderer //! returns bitwise OR-ed capabilities of the renderer
virtual int capabilities(); virtual int capabilities();


virtual QString filter( const QgsFields& fields = QgsFields() );

//! @note available in python as symbols2 //! @note available in python as symbols2
virtual QgsSymbolV2List symbols( QgsRenderContext& context ) /PyName=symbols2/; virtual QgsSymbolV2List symbols( QgsRenderContext& context ) /PyName=symbols2/;
void updateSymbols( QgsSymbolV2 * sym ); void updateSymbols( QgsSymbolV2 * sym );
Expand Down
13 changes: 12 additions & 1 deletion python/core/symbology-ng/qgsrendererv2.sip
Expand Up @@ -103,7 +103,18 @@ class QgsFeatureRendererV2


virtual void stopRender( QgsRenderContext& context ) = 0; virtual void stopRender( QgsRenderContext& context ) = 0;


virtual QString filter(); /**
* If a renderer does not require all the features this method may be overridden
* and return an expression used as where clause.
* This will be called once after {@link startRender()} and before the first call
* to {@link renderFeature()}.
* By default this returns a null string and all features will be requested.
* You do not need to specify the extent in here, this is taken care of separately and
* will be combined with a filter returned from this method.
*
* @return An expression used as where clause
*/
virtual QString filter( const QgsFields& fields = QgsFields() );


virtual QList<QString> usedAttributes() = 0; virtual QList<QString> usedAttributes() = 0;


Expand Down
2 changes: 1 addition & 1 deletion python/core/symbology-ng/qgsrulebasedrendererv2.sip
Expand Up @@ -326,7 +326,7 @@ class QgsRuleBasedRendererV2 : QgsFeatureRendererV2


virtual void stopRender( QgsRenderContext& context ); virtual void stopRender( QgsRenderContext& context );


virtual QString filter(); virtual QString filter( const QgsFields& fields = QgsFields() );


virtual QList<QString> usedAttributes(); virtual QList<QString> usedAttributes();


Expand Down
2 changes: 1 addition & 1 deletion src/core/qgsvectorlayerrenderer.cpp
Expand Up @@ -145,7 +145,7 @@ bool QgsVectorLayerRenderer::render()


mRendererV2->startRender( mContext, mFields ); mRendererV2->startRender( mContext, mFields );


QString rendererFilter = mRendererV2->filter(); QString rendererFilter = mRendererV2->filter( mFields );


QgsRectangle requestExtent = mContext.extent(); QgsRectangle requestExtent = mContext.extent();
mRendererV2->modifyRequestExtent( requestExtent, mContext ); mRendererV2->modifyRequestExtent( requestExtent, mContext );
Expand Down
69 changes: 69 additions & 0 deletions src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp
Expand Up @@ -519,6 +519,75 @@ void QgsCategorizedSymbolRendererV2::toSld( QDomDocument &doc, QDomElement &elem
} }
} }


QString QgsCategorizedSymbolRendererV2::filter( const QgsFields& fields )
{
int attrNum = fields.fieldNameIndex( mAttrName );
bool isExpression = ( attrNum == -1 );

bool hasDefault = false;
bool defaultActive = false;
bool allActive = true;
bool noneActive = true;

//we need to build lists of both inactive and active values, as either list may be required
//depending on whether the default category is active or not
QString activeValues;
QString inactiveValues;

Q_FOREACH ( const QgsRendererCategoryV2& cat, mCategories )
{
if ( cat.value() == "" )
{
hasDefault = true;
defaultActive = cat.renderState();
}

noneActive = noneActive && !cat.renderState();
allActive = allActive && cat.renderState();

QVariant::Type valType = isExpression ? cat.value().type() : fields.at( attrNum ).type();
QString value = QgsExpression::quotedValue( cat.value(), valType );

if ( !cat.renderState() )
{
if ( cat.value() != "" )
{
if ( !inactiveValues.isEmpty() )
inactiveValues.append( ',' );

inactiveValues.append( value );
}
}
else
{
if ( cat.value() != "" )
{
if ( !activeValues.isEmpty() )
activeValues.append( ',' );

activeValues.append( value );
}
}
}

if ( allActive && hasDefault )
{
return QString();
}
else if ( noneActive )
{
return "FALSE";
}
else if ( defaultActive )
{
return QString( "(%1) NOT IN (%2)" ).arg( mAttrName, inactiveValues );
}
else
{
return QString( "(%1) IN (%2)" ).arg( mAttrName, activeValues );
}
}

QgsSymbolV2List QgsCategorizedSymbolRendererV2::symbols( QgsRenderContext &context ) QgsSymbolV2List QgsCategorizedSymbolRendererV2::symbols( QgsRenderContext &context )
{ {
Q_UNUSED( context ); Q_UNUSED( context );
Expand Down
2 changes: 2 additions & 0 deletions src/core/symbology-ng/qgscategorizedsymbolrendererv2.h
Expand Up @@ -97,6 +97,8 @@ class CORE_EXPORT QgsCategorizedSymbolRendererV2 : public QgsFeatureRendererV2
//! returns bitwise OR-ed capabilities of the renderer //! returns bitwise OR-ed capabilities of the renderer
virtual int capabilities() override { return SymbolLevels | RotationField | Filter; } virtual int capabilities() override { return SymbolLevels | RotationField | Filter; }


virtual QString filter( const QgsFields& fields = QgsFields() ) override;

//! @note available in python as symbols2 //! @note available in python as symbols2
virtual QgsSymbolV2List symbols( QgsRenderContext& context ) override; virtual QgsSymbolV2List symbols( QgsRenderContext& context ) override;
void updateSymbols( QgsSymbolV2 * sym ); void updateSymbols( QgsSymbolV2 * sym );
Expand Down
4 changes: 2 additions & 2 deletions src/core/symbology-ng/qgsrendererv2.h
Expand Up @@ -20,6 +20,7 @@
#include "qgsrectangle.h" #include "qgsrectangle.h"
#include "qgsrendercontext.h" #include "qgsrendercontext.h"
#include "qgssymbolv2.h" #include "qgssymbolv2.h"
#include "qgsfield.h"


#include <QList> #include <QList>
#include <QString> #include <QString>
Expand All @@ -30,7 +31,6 @@
#include <QDomElement> #include <QDomElement>


class QgsFeature; class QgsFeature;
class QgsFields;
class QgsVectorLayer; class QgsVectorLayer;
class QgsPaintEffect; class QgsPaintEffect;


Expand Down Expand Up @@ -141,7 +141,7 @@ class CORE_EXPORT QgsFeatureRendererV2
* *
* @return An expression used as where clause * @return An expression used as where clause
*/ */
virtual QString filter() { return QString::null; } virtual QString filter( const QgsFields& fields = QgsFields() ) { Q_UNUSED( fields ); return QString::null; }


virtual QList<QString> usedAttributes() = 0; virtual QList<QString> usedAttributes() = 0;


Expand Down
2 changes: 1 addition & 1 deletion src/core/symbology-ng/qgsrulebasedrendererv2.cpp
Expand Up @@ -895,7 +895,7 @@ void QgsRuleBasedRendererV2::stopRender( QgsRenderContext& context )
mRootRule->stopRender( context ); mRootRule->stopRender( context );
} }


QString QgsRuleBasedRendererV2::filter() QString QgsRuleBasedRendererV2::filter( const QgsFields& )
{ {
return mFilter; return mFilter;
} }
Expand Down
2 changes: 1 addition & 1 deletion src/core/symbology-ng/qgsrulebasedrendererv2.h
Expand Up @@ -385,7 +385,7 @@ class CORE_EXPORT QgsRuleBasedRendererV2 : public QgsFeatureRendererV2


virtual void stopRender( QgsRenderContext& context ) override; virtual void stopRender( QgsRenderContext& context ) override;


virtual QString filter() override; virtual QString filter( const QgsFields& fields = QgsFields() ) override;


virtual QList<QString> usedAttributes() override; virtual QList<QString> usedAttributes() override;


Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -13,6 +13,7 @@ ADD_PYTHON_TEST(PyQgsAtlasComposition test_qgsatlascomposition.py)
ADD_PYTHON_TEST(PyQgsAttributeTableModel test_qgsattributetablemodel.py) ADD_PYTHON_TEST(PyQgsAttributeTableModel test_qgsattributetablemodel.py)
#ADD_PYTHON_TEST(PyQgsAuthenticationSystem test_qgsauthsystem.py) #ADD_PYTHON_TEST(PyQgsAuthenticationSystem test_qgsauthsystem.py)
ADD_PYTHON_TEST(PyQgsBlendModes test_qgsblendmodes.py) ADD_PYTHON_TEST(PyQgsBlendModes test_qgsblendmodes.py)
ADD_PYTHON_TEST(PyQgsCategorizedSymbolRendererV2 test_qgscategorizedsymbolrendererv2.py)
ADD_PYTHON_TEST(PyQgsColorScheme test_qgscolorscheme.py) ADD_PYTHON_TEST(PyQgsColorScheme test_qgscolorscheme.py)
ADD_PYTHON_TEST(PyQgsColorSchemeRegistry test_qgscolorschemeregistry.py) ADD_PYTHON_TEST(PyQgsColorSchemeRegistry test_qgscolorschemeregistry.py)
ADD_PYTHON_TEST(PyQgsComposerEffects test_qgscomposereffects.py) ADD_PYTHON_TEST(PyQgsComposerEffects test_qgscomposereffects.py)
Expand Down
116 changes: 116 additions & 0 deletions tests/src/python/test_qgscategorizedsymbolrendererv2.py
@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsCategorizedSymbolRendererV2
.. note:: 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__ = 'Nyall Dawson'
__date__ = '2/12/2015'
__copyright__ = 'Copyright 2015, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'

import qgis

from utilities import (unittest,
TestCase,
getQgisTestApp,
)
from qgis.core import (QgsCategorizedSymbolRendererV2,
QgsRendererCategoryV2,
QgsMarkerSymbolV2,
QgsVectorGradientColorRampV2,
QgsVectorLayer,
QgsFeature,
QgsGeometry,
QgsPoint,
QgsSymbolV2,
QgsSymbolLayerV2Utils,
QgsRenderContext
)
from PyQt4.QtCore import Qt, QVariant
from PyQt4.QtXml import QDomDocument
from PyQt4.QtGui import QColor

QGISAPP, CANVAS, IFACE, PARENT = getQgisTestApp()


def createMarkerSymbol():
symbol = QgsMarkerSymbolV2.createSimple({
"color": "100,150,50",
"name": "square",
"size": "3.0"
})
return symbol


class TestQgsCategorizedSymbolRendererV2(TestCase):

def testFilter(self):
"""Test filter creation"""
renderer = QgsCategorizedSymbolRendererV2()
renderer.setClassAttribute('field')

renderer.addCategory(QgsRendererCategoryV2('a', createMarkerSymbol(), 'a'))
renderer.addCategory(QgsRendererCategoryV2('b', createMarkerSymbol(), 'b'))
renderer.addCategory(QgsRendererCategoryV2('c', createMarkerSymbol(), 'c'))
# add default category
renderer.addCategory(QgsRendererCategoryV2('', createMarkerSymbol(), 'default'))

self.assertEqual(renderer.filter(), '')
#remove categories, leaving default
assert renderer.updateCategoryRenderState(0, False)
self.assertEqual(renderer.filter(), "(field) NOT IN ('a')")
assert renderer.updateCategoryRenderState(1, False)
self.assertEqual(renderer.filter(), "(field) NOT IN ('a','b')")
assert renderer.updateCategoryRenderState(2, False)
self.assertEqual(renderer.filter(), "(field) NOT IN ('a','b','c')")
#remove default category
assert renderer.updateCategoryRenderState(3, False)
self.assertEqual(renderer.filter(), "FALSE")
#add back other categories, leaving default disabled
assert renderer.updateCategoryRenderState(0, True)
self.assertEqual(renderer.filter(), "(field) IN ('a')")
assert renderer.updateCategoryRenderState(1, True)
self.assertEqual(renderer.filter(), "(field) IN ('a','b')")
assert renderer.updateCategoryRenderState(2, True)
self.assertEqual(renderer.filter(), "(field) IN ('a','b','c')")

renderer.deleteAllCategories()
# just default category
renderer.addCategory(QgsRendererCategoryV2('', createMarkerSymbol(), 'default'))
self.assertEqual(renderer.filter(), '')
assert renderer.updateCategoryRenderState(0, False)
self.assertEqual(renderer.filter(), 'FALSE')

renderer.deleteAllCategories()
# no default category
renderer.addCategory(QgsRendererCategoryV2('a', createMarkerSymbol(), 'a'))
renderer.addCategory(QgsRendererCategoryV2('b', createMarkerSymbol(), 'b'))
renderer.addCategory(QgsRendererCategoryV2('c', createMarkerSymbol(), 'c'))
self.assertEqual(renderer.filter(), "(field) IN ('a','b','c')")
assert renderer.updateCategoryRenderState(0, False)
self.assertEqual(renderer.filter(), "(field) IN ('b','c')")
assert renderer.updateCategoryRenderState(2, False)
self.assertEqual(renderer.filter(), "(field) IN ('b')")
assert renderer.updateCategoryRenderState(1, False)
self.assertEqual(renderer.filter(), "FALSE")

renderer.deleteAllCategories()
#numeric categories
renderer.addCategory(QgsRendererCategoryV2(1, createMarkerSymbol(), 'a'))
renderer.addCategory(QgsRendererCategoryV2(2, createMarkerSymbol(), 'b'))
renderer.addCategory(QgsRendererCategoryV2(3, createMarkerSymbol(), 'c'))
self.assertEqual(renderer.filter(), '(field) IN (1,2,3)')
assert renderer.updateCategoryRenderState(0, False)
self.assertEqual(renderer.filter(), "(field) IN (2,3)")
assert renderer.updateCategoryRenderState(2, False)
self.assertEqual(renderer.filter(), "(field) IN (2)")
assert renderer.updateCategoryRenderState(1, False)
self.assertEqual(renderer.filter(), "FALSE")


if __name__ == "__main__":
unittest.main()

0 comments on commit 9eee121

Please sign in to comment.