Apply filters to feature request for categorized renderer

Makes rendering much faster when only certain categories are checked,
as only the matching records for the displayed features are fetched
from the provider.
This commit is contained in:
Nyall Dawson 2015-12-04 07:28:31 +11:00
parent 97e5d31706
commit 9eee121115
11 changed files with 208 additions and 7 deletions

View File

@ -66,6 +66,8 @@ class QgsCategorizedSymbolRendererV2 : QgsFeatureRendererV2
//! returns bitwise OR-ed capabilities of the renderer
virtual int capabilities();
virtual QString filter( const QgsFields& fields = QgsFields() );
//! @note available in python as symbols2
virtual QgsSymbolV2List symbols( QgsRenderContext& context ) /PyName=symbols2/;
void updateSymbols( QgsSymbolV2 * sym );

View File

@ -103,7 +103,18 @@ class QgsFeatureRendererV2
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;

View File

@ -326,7 +326,7 @@ class QgsRuleBasedRendererV2 : QgsFeatureRendererV2
virtual void stopRender( QgsRenderContext& context );
virtual QString filter();
virtual QString filter( const QgsFields& fields = QgsFields() );
virtual QList<QString> usedAttributes();

View File

@ -145,7 +145,7 @@ bool QgsVectorLayerRenderer::render()
mRendererV2->startRender( mContext, mFields );
QString rendererFilter = mRendererV2->filter();
QString rendererFilter = mRendererV2->filter( mFields );
QgsRectangle requestExtent = mContext.extent();
mRendererV2->modifyRequestExtent( requestExtent, mContext );

View File

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

View File

@ -97,6 +97,8 @@ class CORE_EXPORT QgsCategorizedSymbolRendererV2 : public QgsFeatureRendererV2
//! returns bitwise OR-ed capabilities of the renderer
virtual int capabilities() override { return SymbolLevels | RotationField | Filter; }
virtual QString filter( const QgsFields& fields = QgsFields() ) override;
//! @note available in python as symbols2
virtual QgsSymbolV2List symbols( QgsRenderContext& context ) override;
void updateSymbols( QgsSymbolV2 * sym );

View File

@ -20,6 +20,7 @@
#include "qgsrectangle.h"
#include "qgsrendercontext.h"
#include "qgssymbolv2.h"
#include "qgsfield.h"
#include <QList>
#include <QString>
@ -30,7 +31,6 @@
#include <QDomElement>
class QgsFeature;
class QgsFields;
class QgsVectorLayer;
class QgsPaintEffect;
@ -141,7 +141,7 @@ class CORE_EXPORT QgsFeatureRendererV2
*
* @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;

View File

@ -895,7 +895,7 @@ void QgsRuleBasedRendererV2::stopRender( QgsRenderContext& context )
mRootRule->stopRender( context );
}
QString QgsRuleBasedRendererV2::filter()
QString QgsRuleBasedRendererV2::filter( const QgsFields& )
{
return mFilter;
}

View File

@ -385,7 +385,7 @@ class CORE_EXPORT QgsRuleBasedRendererV2 : public QgsFeatureRendererV2
virtual void stopRender( QgsRenderContext& context ) override;
virtual QString filter() override;
virtual QString filter( const QgsFields& fields = QgsFields() ) override;
virtual QList<QString> usedAttributes() override;

View File

@ -13,6 +13,7 @@ ADD_PYTHON_TEST(PyQgsAtlasComposition test_qgsatlascomposition.py)
ADD_PYTHON_TEST(PyQgsAttributeTableModel test_qgsattributetablemodel.py)
#ADD_PYTHON_TEST(PyQgsAuthenticationSystem test_qgsauthsystem.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(PyQgsColorSchemeRegistry test_qgscolorschemeregistry.py)
ADD_PYTHON_TEST(PyQgsComposerEffects test_qgscomposereffects.py)

View File

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