3

I have a QGIS python processing scripts that return multiple rasters. I want to return them in a group named 'Aligned'. There is nothing in the context.addLayerToLoadOnCompletion() function to specify group.

I tried doing something like this to achieve what I want but it didn't work. It did create a group at the end of all layers in the layers panel, but the outputs were still added outside the group.

root = context.project().instance().layerTreeRoot()
group = root.addGroup('Aligned')
context.addLayerToLoadOnCompletion(
    outputs["Runoff Local"]["OUTPUT"],
    QgsProcessingContext.LayerDetails(
        f"Runoff Local (L) ", context.project(), "Runoff Local"
    ),
)

Is there a way to cleanly group all layers from the script in one group?

PolyGeo
  • 65,136
  • 29
  • 109
  • 338
ar-siddiqui
  • 1,738
  • 7
  • 30
  • 1
    Just for reference, there is a Results group name option in QGIS --> Settings --> Options --> Processing, which you can use to get all results from processing algorithms to a group in the Layers panel. See https://docs.qgis.org/testing/en/docs/user_manual/processing/configuration.html – Germán Carrillo Nov 22 '21 at 01:48
  • It is also possible to use a QgsProcessingLayerPostProcessorInterface and move it to the group in its postProcessLayer() method. – bugmenot123 Apr 06 '23 at 22:40

2 Answers2

2

I was able to achieve this and this is my understanding:

If you do it through postProcessAlgorithm, QGIS will natively output layer as well, so you will have duplicate layers in your Canvas. So postProcessAlgorithm is not the best approach.

This is how I did it using the native QGIS output, rather than adding a layer to the canvas through my algorithm.

I first created a group through postProcessAlgorithm using Ben W's answer. This only creates and selects a group. Does not add anything.

    def postProcessAlgorithm(self, context, feedback):
    # following code make(if doesn't exist) and select a group so that the QGIS  spits layer at that location

    project = context.project()
    root = project.instance().layerTreeRoot()  # get base level node

    group = root.findGroup(self.run_name)  # find group in whole hierarchy
    if not group:  # if group does not already exists
        selected_nodes = (
            iface.layerTreeView().selectedNodes()
        )  # get all selected nodes
        if selected_nodes:  # if a node is selected
            # check the first node is group
            if isinstance(selected_nodes[0], QgsLayerTreeGroup):
                # if it is add a group inside
                group = selected_nodes[0].insertGroup(0, self.run_name)
            else:
                parent = selected_nodes[0].parent()
                # get current index so that new group can be inserted at that location
                index = parent.children()(selected_nodes[0])
                group = parent.insertGroup(index, self.run_name)
        else:
            group = root.insertGroup(0, self.run_name)

    # select the group
    select_group(self.run_name)

    return {}

Selection function made using the help of this Selecting subgroup in Layers panel using PyQGIS

def select_group(name: str) -> bool:
    """
    Select group item of a node tree
    """
view = iface.layerTreeView()
m = view.model()

listIndexes = m.match(
    m.index(0, 0),
    Qt.DisplayRole,
    name,
    1,
    Qt.MatchFixedString | Qt.MatchRecursive | Qt.MatchCaseSensitive | Qt.MatchWrap,
)

if listIndexes:
    i = listIndexes[0]
    view.selectionModel().setCurrentIndex(i, QItemSelectionModel.ClearAndSelect)
    return True

else:
    return False

ar-siddiqui
  • 1,738
  • 7
  • 30
0

Adding this here as a working example of doing this via the QgsProcessingLayerPostProcessorInterface class. As described here, to set post processors to multiple output layers it is necessary to store references to the created QgsProcessingLayerPostProcessorInterface instances with class-level scope e.g. in a dictionary declared as an instance attribute of the QgsProcessingAlgorithm sub-class.

from qgis.core import QgsProcessing
from qgis.core import QgsProcessingAlgorithm
from qgis.core import QgsProcessingParameterField
from qgis.core import QgsProcessingParameterVectorLayer
from qgis.core import QgsProcessingUtils
from qgis.core import QgsProcessingContext
from qgis.core import QgsProcessingLayerPostProcessorInterface
from qgis.core import QgsRasterLayer

import processing

class Group_output_layers(QgsProcessingAlgorithm):

post_processors = {}

def name(self):
    return 'group_output_layers'

def displayName(self):
    return 'group_output_layers'

def group(self):
    return 'Models'

def groupId(self):
    return 'Models'

def createInstance(self):
    return Group_output_layers()

def initAlgorithm(self, config=None):
    self.addParameter(QgsProcessingParameterVectorLayer('Inputpolygons', 'Input_polygons', types=[QgsProcessing.TypeVectorPolygon], defaultValue=None))
    self.addParameter(QgsProcessingParameterField('Burnfields', 'Burn_fields', type=0, parentLayerParameterName='Inputpolygons', allowMultiple=True, defaultToAllFields=True,))

def processAlgorithm(self, parameters, context, feedback):
    results = {}
    outputs = {}
    in_layer = self.parameterAsVectorLayer(parameters, 'Inputpolygons', context)
    fields = self.parameterAsFields(parameters, 'Burnfields', context)
    for index, field in enumerate(fields):
        if feedback.isCanceled():
            break
        # Rasterize
        alg_params = {
                'BURN': 0,
                'DATA_TYPE': 5,
                'EXTENT': in_layer.extent(),
                'EXTRA': '',
                'FIELD': field,
                'INIT': None,
                'INPUT': parameters['Inputpolygons'],
                'INVERT': False,
                'NODATA': -9999,
                'OPTIONS': '',
                'UNITS': 1,
                'HEIGHT': 90,
                'WIDTH': 90,
                'OUTPUT': 'TEMPORARY_OUTPUT',
                }

        outputs[f'Outlayer{field}'] = processing.run('gdal:rasterize',
                                                    alg_params,
                                                    is_child_algorithm=True,
                                                    context=context,
                                                    feedback=feedback)

        results[f'Rasterized_{field}'] = outputs[f'Outlayer{field}']['OUTPUT']

        for result_name, lyr_id in results.items():
            context.addLayerToLoadOnCompletion(lyr_id, QgsProcessingContext.LayerDetails(result_name, context.project(), result_name))
            self.post_processors[lyr_id] = GroupRasterPostProcessor.create()
            context.layerToLoadOnCompletionDetails(lyr_id).setPostProcessor(self.post_processors[lyr_id])

    return results


class GroupRasterPostProcessor(QgsProcessingLayerPostProcessorInterface):

instance = None
group_name = 'group1'

def postProcessLayer(self, layer, context, feedback):
    if not isinstance(layer, QgsRasterLayer):
        return
    project = context.project()
    root_group = project.layerTreeRoot()
    if not root_group.findGroup(self.group_name):
        root_group.insertGroup(0, self.group_name)
    group1 = root_group.findGroup(self.group_name)
    lyr_node = root_group.findLayer(layer.id())
    if lyr_node:
        node_clone = lyr_node.clone()
        group1.addChildNode(node_clone)
        lyr_node.parent().removeChildNode(lyr_node)

# Hack to work around sip bug!
@staticmethod
def create() -> 'GroupRasterPostProcessor':
    """
    Returns a new instance of the post processor, keeping a reference to the sip
    wrapper so that sip doesn't get confused with the Python subclass and call
    the base wrapper implementation instead...
    """
    GroupRasterPostProcessor.instance = GroupRasterPostProcessor()
    return GroupRasterPostProcessor.instance

References & Acknowledgements:

Nyall Dawson for the post processor example adapted here.

https://github.com/qgis/QGIS/issues/47533

Set display name of vector layer in processing script

Ben W
  • 21,426
  • 3
  • 15
  • 39