4
\$\begingroup\$

I am developing a plugin for the GIS software, QGIS. The code below reads the number of layers in various groups (as shown in the image) and adds them to a QTableWidget:

Groups and layers

For each group, I want to count the number of layers, divide 1 by this number and then add the result of this to each of the layers in each group in the table.

Taking "Group1" as an example:

  1. Count the number of layers (in this case 3).
  2. Calculate 1 / 3 = 0.33....
  3. Insert 0.33... to the first three layers in the table.
  4. Repeat for the remaining groups.

So the table looks like this:

QTableWidget

However, there's a fair bit of list manipulation involved which seems unnecessary so was hoping to see if there's a way to reduce this.


Here is the code:

# Define QTableWidget
qTable = self.dockwidget.tableWidget
# Define group
root = QgsProject.instance().layerTreeRoot()
main_group = root.findGroup('Main group')

def refresh_table():
    # Define parameters for QTableWidget
    qTable.setRowCount(0)
    # Define list to contain all layer names
    layer_data = []  
    # Define list to contain the number of layers
    layer_count = []

    # Find all groups in main_group
    for group in main_group.children():
        # Define list to count number of layers in each group
        layer_list = []
        # Find all layers in each group 
        for child in group.children():
            node = root.findLayer(child.layer().id()) 
            try:
                # If layer is visible, add them to lists
                if node.isVisible() == Qt.Checked:  
                    layer_data.append(child.layerName())
                    layer_list.append(child.layerName())
            except AttributeError:
                pass
        # Insert the number of layers in each group to layer_count list
        layer_count.append(len(layer_list))

    # Get total number of layers 
    layer_data_count = len(layer_data)
    try:
        # Create new list for layer_count but ignore any zeros
        new_layer_count = [x for x in layer_count if x != 0]
    except ValueError:
        pass

    # List manipulation
    # Calculate the number of layers in each group and divide 1 by this number    
    value_list = [1 / float(x) for x in new_layer_count]
    # Format the list to one decimal place
    formatted_value_list = ['%.1f' % elem for elem in value_list]
    # Convert the values in list to float
    formatted_value_list_to_float = [float(x) for x in formatted_value_list]
    # Create final list containing each layer and the values of their group
    final_value_list = [x for n,x in zip(new_layer_count,formatted_value_list_to_float) for _ in range(n)]

    # Set number of rows/columns
    nb_row = len(layer_data)
    nb_col = 2
    qTable.setRowCount(nb_row)
    qTable.setColumnCount(nb_col)
    # Hide row index number
    qTable.verticalHeader().setVisible(False)

    # Insert layer names and values
    for row in range(layer_data_count):
        for col in [0]:
            item = QTableWidgetItem(str(layer_data[row]))
            qTable.setItem(row,col,item)
            # Make first column non-editable
            item.setFlags(QtCore.Qt.ItemIsEnabled)
        for col in [1]:
            item = QTableWidgetItem(str(final_value_list[row]))
            qTable.setItem(row,col,item)   
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

Your lists manipulations are indeed messy. It seems like you are trying to perform multiple things at once and thus you end up mixing variables that serve different purposes into the same loops.

Instead, you should try to extract logical steps to perform, such as:

  • extract informations you need from the internals of QGis to a similar but simpler representation;
  • compute the data you need out of this simplified representation;
  • format the data you computed for presentation;
  • use the formatted data to update your visual presentation.

Here the second and third step are simple enough they can be made at once.

But first, we will need an helper function to simplify the inner working of the first double for loop:

def is_visible_layer(layer, root):
    node = root.findLayer(layer.id())
    try:
        return node.isVisible() == Qt.Checked
    except AttributeError:
        return False

Now I’m not QGis expert, but I’m wondering if the root is really necessary to get the meta-information about the node. Isn't it possible to write it like:

def is_visible_layer(layer):
    try:
        return layer.isVisible() == Qt.Checked
    except AttributeError:
        return False

?

This let us write the for loop like:

for group in main_group.children():
    layer_list = []
    for child in group.children():
        if is_visible_layer(child.layer(), root):
            layer_data.append(child.layerName())
            layer_list.append(child.layerName())
    layer_count.append(len(layer_list))

Which can be simplified using list-comprehensions:

for group in main_group.children():
    layer_list = [child.layerName() for child in group.children() if is_visible_layer(child.layer(), root)]
    layer_data.extend(layer_list)
    layer_count.append(len(layer_list))

Now recall what I said about performing several things at once? Here you already try to count things while extracting the informations from the QGis internals. Let's get that for later and simplify the loop once again to build a list of list of layer names:

layers = [
    [
        child.layerName()
        for child in group.children()
        if is_visible_layer(child.layer(), root)
    ]
    for group in root.findGroup(main_group).children()
]

and then, only after you can start counting. And as it is much easier using this structure, you can start converting the list of list into a single list of couples that will mimic the table layout:

table = [
    (layer, len(group))
    for group in layers
    for layer in group
]

Turning this list of couples into an N × 2 table is then really easy. The whole code should look like:

def is_visible_layer(layer, root):
    node = root.findLayer(layer.id())
    try:
        return node.isVisible() == Qt.Checked
    except AttributeError:
        return False


def refresh_table(root, qTable, main_group='Main group'):
    # Get names of relevant layers, as a list of list of names
    layers = [
        [
            child.layerName()
            for child in group.children()
            if is_visible_layer(child.layer(), root)
        ]
        for group in root.findGroup(main_group).children()
    ]

    # Convert the layer list to the table layout i.e. a list of couples
    table = [
        (layer, len(group))
        for group in layers
        for layer in group
    ]

    # Configure output table
    qTable.setRowCount(len(table))
    qTable.setColumnCount(2)
    qTable.verticalHeader().setVisible(False)

    # Insert data into table
    for row, (layer, count) in enumerate(table):
        name_item = QTableWidgetItem(layer)
        name_item.setFlags(QtCore.Qt.ItemIsEnabled)
        count_item = QTableWidgetItem('{:.1f}'.format(1./count))
        qTable.setItem(row, 0, name_item)
        qTable.setItem(row, 1, count_item)

Call it like

refresh_table(
    QgsProject.instance().layerTreeRoot(),
    self.dockwidget.tableWidget)
\$\endgroup\$
2
  • \$\begingroup\$ Many thanks for your answer, I'll get this implemented at some point and report back :) \$\endgroup\$
    – Joseph
    Mar 16, 2017 at 14:32
  • \$\begingroup\$ Thanks, that fixed that error. Now received another one: for group in root.findGroup(main_group).children() AttributeError: 'QgsLayerTreeLayer' object has no attribute 'findGroup'. I will get back to this issue later as it's more a QGIS error than pure python. But in the meantime, I'll accept your answer. Many thanks :) \$\endgroup\$
    – Joseph
    Mar 17, 2017 at 14:24

Your Answer

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

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