From 9eee12111567a84f4d4de7e020392b3c01c28598 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 4 Dec 2015 07:28:31 +1100 Subject: [PATCH] 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. --- .../qgscategorizedsymbolrendererv2.sip | 2 + python/core/symbology-ng/qgsrendererv2.sip | 13 +- .../symbology-ng/qgsrulebasedrendererv2.sip | 2 +- src/core/qgsvectorlayerrenderer.cpp | 2 +- .../qgscategorizedsymbolrendererv2.cpp | 69 +++++++++++ .../qgscategorizedsymbolrendererv2.h | 2 + src/core/symbology-ng/qgsrendererv2.h | 4 +- .../symbology-ng/qgsrulebasedrendererv2.cpp | 2 +- .../symbology-ng/qgsrulebasedrendererv2.h | 2 +- tests/src/python/CMakeLists.txt | 1 + .../test_qgscategorizedsymbolrendererv2.py | 116 ++++++++++++++++++ 11 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 tests/src/python/test_qgscategorizedsymbolrendererv2.py diff --git a/python/core/symbology-ng/qgscategorizedsymbolrendererv2.sip b/python/core/symbology-ng/qgscategorizedsymbolrendererv2.sip index b3a685b88ec..1cf60ce3f3d 100644 --- a/python/core/symbology-ng/qgscategorizedsymbolrendererv2.sip +++ b/python/core/symbology-ng/qgscategorizedsymbolrendererv2.sip @@ -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 ); diff --git a/python/core/symbology-ng/qgsrendererv2.sip b/python/core/symbology-ng/qgsrendererv2.sip index b406522f91f..73b00e5025e 100644 --- a/python/core/symbology-ng/qgsrendererv2.sip +++ b/python/core/symbology-ng/qgsrendererv2.sip @@ -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 usedAttributes() = 0; diff --git a/python/core/symbology-ng/qgsrulebasedrendererv2.sip b/python/core/symbology-ng/qgsrulebasedrendererv2.sip index 8b0721d4df4..c9c49461f5f 100644 --- a/python/core/symbology-ng/qgsrulebasedrendererv2.sip +++ b/python/core/symbology-ng/qgsrulebasedrendererv2.sip @@ -326,7 +326,7 @@ class QgsRuleBasedRendererV2 : QgsFeatureRendererV2 virtual void stopRender( QgsRenderContext& context ); - virtual QString filter(); + virtual QString filter( const QgsFields& fields = QgsFields() ); virtual QList usedAttributes(); diff --git a/src/core/qgsvectorlayerrenderer.cpp b/src/core/qgsvectorlayerrenderer.cpp index 98a18c8f53b..e22ebfd0816 100644 --- a/src/core/qgsvectorlayerrenderer.cpp +++ b/src/core/qgsvectorlayerrenderer.cpp @@ -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 ); diff --git a/src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp b/src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp index 5c62f26dc43..00a81037e33 100644 --- a/src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp +++ b/src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp @@ -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 ); diff --git a/src/core/symbology-ng/qgscategorizedsymbolrendererv2.h b/src/core/symbology-ng/qgscategorizedsymbolrendererv2.h index a96197c0fdf..c5ea2698f9c 100644 --- a/src/core/symbology-ng/qgscategorizedsymbolrendererv2.h +++ b/src/core/symbology-ng/qgscategorizedsymbolrendererv2.h @@ -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 ); diff --git a/src/core/symbology-ng/qgsrendererv2.h b/src/core/symbology-ng/qgsrendererv2.h index 55dc255cd9a..91005ecb054 100644 --- a/src/core/symbology-ng/qgsrendererv2.h +++ b/src/core/symbology-ng/qgsrendererv2.h @@ -20,6 +20,7 @@ #include "qgsrectangle.h" #include "qgsrendercontext.h" #include "qgssymbolv2.h" +#include "qgsfield.h" #include #include @@ -30,7 +31,6 @@ #include 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 usedAttributes() = 0; diff --git a/src/core/symbology-ng/qgsrulebasedrendererv2.cpp b/src/core/symbology-ng/qgsrulebasedrendererv2.cpp index b0e049b3a94..def5473a980 100644 --- a/src/core/symbology-ng/qgsrulebasedrendererv2.cpp +++ b/src/core/symbology-ng/qgsrulebasedrendererv2.cpp @@ -895,7 +895,7 @@ void QgsRuleBasedRendererV2::stopRender( QgsRenderContext& context ) mRootRule->stopRender( context ); } -QString QgsRuleBasedRendererV2::filter() +QString QgsRuleBasedRendererV2::filter( const QgsFields& ) { return mFilter; } diff --git a/src/core/symbology-ng/qgsrulebasedrendererv2.h b/src/core/symbology-ng/qgsrulebasedrendererv2.h index d85c358df57..c3e3e58fe49 100644 --- a/src/core/symbology-ng/qgsrulebasedrendererv2.h +++ b/src/core/symbology-ng/qgsrulebasedrendererv2.h @@ -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 usedAttributes() override; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 74777b7145a..c8dcd47add6 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -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) diff --git a/tests/src/python/test_qgscategorizedsymbolrendererv2.py b/tests/src/python/test_qgscategorizedsymbolrendererv2.py new file mode 100644 index 00000000000..86a697931e7 --- /dev/null +++ b/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()