4

I want to delete categories in my vector-layer style if the feature-count for the category is zero.

enter image description here

The following code works so far if you run it in the python console but the feature count for the layer is wrong after the categories with zero objects have been deleted.

enter image description here

If i turn on the edit mode, create a feature in the layer and close the attribute-input-dialog the feature count is recalculated.

enter image description here

How can I trigger the refresh of the feature count programatically?

# encoding: utf-8

from PyQt4.QtGui import *
from PyQt4.QtCore import *
from qgis.core import QgsVectorLayer, QgsFeature, QgsFeatureRequest, QgsExpression, QgsCategorizedSymbolRendererV2
from qgis.gui import QgsMessageBar

layer =iface.activeLayer()
anzahl=0
deleted=0
if type(layer) == QgsVectorLayer and layer.hasGeometryType():
    r = layer.rendererV2()
    if r.type() == 'RuleRenderer':
        anzahl = len(r.rootRule().children())
        # code snippet from g.carillo: https://gis.stackexchange.com/questions/209674/qgis-test-rules-with-python
        for rule in r.rootRule().children(): # Iterate rules
            if rule.filter(): # Filter out empty rules
                request = QgsFeatureRequest( rule.filter() )
                count = len( [f for f in layer.getFeatures( request )] )

                if count <1:
                    r.rootRule().removeChild(rule)
                    deleted +=1
    elif r.type() == 'categorizedSymbol':
        anzahl = len(r.categories())
        counter=0
        del_categories=[]
        new_categories=[]
        for position, category in enumerate(r.categories()): # Iterate rules
            myVariable = category.value()
            expression_string = u" \""+unicode(r.legendClassificationAttribute())+"\" = '"+myVariable+"' "
            #print expression_string
            filter_string = QgsExpression( expression_string)
            request = QgsFeatureRequest( filter_string )
            count = len( [f for f in layer.getFeatures( request )] )

            if count <1:
                del_categories.append(category)
                deleted +=1
            else:
                new_categories.append(category)
                print layer.name() + ' : ' + category.label() + ' : ' + myVariable + ' : ' + str(count)

            counter+=1
        print del_categories

        r.deleteAllCategories()

        for i in new_categories:
            try:
                r.addCategory(i)
            except:
                print "oh no"

    # apply the renderer to the layer
    if r is not None:
        layer.setRendererV2(r)
    layer.dataProvider().forceReload()
    layer.triggerRepaint()

    iface.layerTreeView().refreshLayerSymbology(layer.id())

    print("Layer style filtered! "+str(deleted)+" of "+str(anzahl)+" rules filtered")

1 Answer 1

4

Here is some code that I got working through slighly different means:

# encoding: utf-8

from PyQt4.QtGui import *
from PyQt4.QtCore import *
from qgis.core import QgsVectorLayer, QgsFeature, QgsFeatureRequest, QgsExpression, QgsCategorizedSymbolRendererV2
from qgis.gui import QgsMessageBar

layer =iface.activeLayer()
anzahl=0
deleted=0
if type(layer) == QgsVectorLayer and layer.hasGeometryType():
    r = layer.rendererV2()
    if r.type() == 'RuleRenderer':
        anzahl = len(r.rootRule().children())
        # code snippet from g.carillo: https://gis.stackexchange.com/questions/209674/qgis-test-rules-with-python
        for rule in r.rootRule().children(): # Iterate rules
            if rule.filter(): # Filter out empty rules
                request = QgsFeatureRequest( rule.filter() )
                count = len( [f for f in layer.getFeatures( request )] )

                if count <1:
                    r.rootRule().removeChild(rule)
                    deleted +=1
    elif r.type() == 'categorizedSymbol':

        ### START NEW CODE ###

        layerCategories = r.categories()
        counter=0

        # Because list indicies shrink on element deletion, use a iterator we can push around.
        i = 0
        while i < len(layerCategories):
            # Cast myVariable to string to avoid coercion errors.
            myVariable = layerCategories[i].value()
            expression_string = u" \""+unicode(r.legendClassificationAttribute())+"\" = '"+ str(myVariable) +"' "
            filter_string = QgsExpression( expression_string)
            request = QgsFeatureRequest( filter_string )
            # Because we are only interested in categories with zero items,
            # we should only check to see if the iterator has a next item.
            # If the category has no features it will throw an error.
            try:
                layer.getFeatures(request).next()
            # Catch the error and finally delete the category.
            except StopIteration:
                print "Category Deleted, No Items Found:", layerCategories[i].label()
                r.deleteCategory(i) # Actually delete category from QGIS
                del layerCategories[i] # Delete the category from our tracking list.
                counter+=1
                if i == 0: # Avoid out-of-bounds errors
                    i=-1 # i has to be -1 as at the end of the new code block i will be increased by +1
                else:
                    i-=1 # Push iterator back because we deleted a category from our tracking list, otherwise we would skip one.

            i+=1

        ### END NEW CODE ###

    # apply the renderer to the layer
    if r is not None:
        layer.setRendererV2(r)
    layer.dataProvider().forceReload()
    layer.triggerRepaint()

    iface.layerTreeView().refreshLayerSymbology(layer.id())

    print("Layer style filtered! "+str(deleted)+" of "+str(anzahl)+" rules filtered")

Instead of deleteAllCategories, this just uses deleteCategory. This gives us, I think, a couple advantages:

  • No mismatched feature counts (opining on that in a minute).
  • We only have to look at the first feature found from an iterator of getFeatures. If I remember correctly, Python iterators are lazy and only evaluate when requested: where as before you counted N features, now you count N categories. This pontentially save a lot of time if you have hundreds of thousands of features.

But you don't have you feedback that you do with you previous version. Just a message telling you which categories were removed.


As to why the feature count is mismatched when using your orignal version:

I think deleting all categories and rebuilding from the scratch is causing the feature counts to point to garbage areas of memory, hence wrong numbers or zeros. This techinque is a misuse of the API or adding a category doesn't signal for a recount of features is a bug or the fact that deleting a category doesn't point to garbage memory is a happy accident.

Looking at the source it is implied that a listener for the feature counter listens for a particular message to recount features. But I am not that familar with the code didn't find the message it is listening for, and unfornately it seems like a lot of the signals cannot be emmited in the Python API.

Meanwhile, adding/removing categories from the UI and hitting 'OK' triggers the recount of features that using the API doesn't.

2
  • if the first category has a count of zero i has to be set to -1 as at the end of the new code block i will be increased by +1. otherwise you will skip the second category
    – Thomas B
    Sep 25, 2017 at 7:19
  • I've approved @markgraeflerland edit to your code. Please double check that it is correct.
    – jbalk
    Sep 25, 2017 at 7:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.