PyQGIS – Removing Intermediate Layers in QGIS Processing Scripts

pyqgisqgis-3qgis-processing

I am creating a processing plugin in QGIS that has two steps.

  1. Write features to a FeatureSink.
  2. Convert the FeatureSink to a layer and perform native:joinattributesbylocation.

I am using the QGIS script template as a starting point.

I have two output parameters: one for each of the outputs of the second step but the script produces 3 temporary layers. It is writing the output of the FeatureSink to a scratch layer as well. How can I prevent this appearing? Ideally I would like to prevent the intermediate FeatureSink appearing at all, but I could also delete it at the end of the process.

Code snippet below:

    def initAlgorithm(self, config=None):
        mastermap_parameter = QgsProcessingParameterFeatureSource(
            self.INPUT,
            self.tr('\'Area\' data from OS Mastermap'),
            [QgsProcessing.TypeVectorAnyGeometry]
        )
        mastermap_parameter.setDefaultValue('OSMM_AREA')
        self.addParameter(mastermap_parameter)

        string1_parameter = QgsProcessingParameterString(
            self.STRING_1,
            self.tr('Select features with the following value...'),
            '',
            multiLine = False,
            optional = False
        )
        string1_parameter.setDefaultValue('Buildings')
        self.addParameter(string1_parameter)

        field1_parameter = QgsProcessingParameterField(
            self.FIELD_1,
            self.tr('...in the following attribute field.'),
            '',
            self.INPUT
        )
        field1_parameter.setDefaultValue('theme')
        self.addParameter(field1_parameter)
        
        string2_parameter = QgsProcessingParameterString(
            self.STRING_2,
            self.tr('Additionally, select features with the following value...'),
            '',
            multiLine = False,
            optional = True
        )
        string2_parameter.setDefaultValue('Building')
        self.addParameter(string2_parameter)
        
        field2_parameter = QgsProcessingParameterField(
            self.FIELD_2,
            self.tr('...in the following attribute field.'),
            '',
            self.INPUT,
            QgsProcessingParameterField.DataType.Any,
            allowMultiple = False,
            optional = True
        )
        field2_parameter.setDefaultValue('descriptivegroup')
        self.addParameter(field2_parameter)
        
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.NRD_INPUT,
                self.tr('Data from National Receptor Dataset'),
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Output data')
            )
        )
        
        self.addParameter(
            QgsProcessingParameterVectorDestination(
                self.OUTPUT_2,
                self.tr('OS Mastermap buildings without NRD')
            )
        )

    
    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(
            parameters,
            self.INPUT,
            context)
        
        field_1 = self.parameterAsString(
            parameters,
            self.FIELD_1,
            context)
        
        string_1 = self.parameterAsString(
            parameters,
            self.STRING_1,
            context)
        
        field_2 = self.parameterAsString(
            parameters,
            self.FIELD_2,
            context)
        
        string_2 = self.parameterAsString(
            parameters,
            self.STRING_2,
            context)       
        
        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            source.fields(),
            source.wkbType(),
            source.sourceCrs()
        )
        
        total = 100.0 / source.featureCount() if source.featureCount() else 0
        valid_features = []
        
        for current, feature in enumerate(source.getFeatures()):
            if feedback.isCanceled():
                break

            if (feature[field_1] == string_1 or feature[field_2] == string_2):
                sink.addFeature(feature, QgsFeatureSink.FastInsert)

            feedback.setProgress(int(current * total))
        
        nrd_input = self.parameterAsLayer(
            parameters,
            self.NRD_INPUT,
            context)
        
        mm_building_layer = QgsProcessingUtils.mapLayerFromString(
            dest_id,
            context)
        
        joined_layer = processing.run("native:joinattributesbylocation", {
                'DISCARD_NONMATCHING' : True,
                'INPUT' : mm_building_layer,
                'JOIN' : nrd_input,
                'JOIN_FIELDS' : [],
                'METHOD' : 0,
                'NON_MATCHING' : parameters['OUTPUT_2'],
                'OUTPUT' : parameters['OUTPUT'],
                'PREDICATE' : [0],
                'PREFIX' : ''
            },
            is_child_algorithm=True,
            context=context,
            feedback=feedback)
        
        if feedback.isCanceled():
            return {}
    
        return {} 

Best Answer

You can create an intermediate layer with the filtered features, using native:extractbyexpression (and without a feature sink at any stage):

class myTestAlg(QgsProcessingAlgorithm):
    INPUT = 'INPUT'
    NRD_INPUT = 'NRD_INPUT'
    STRING_1 = 'STRING_1'
    STRING_2 = 'STRING_2'
    FIELD_1 = 'FIELD_1'
    FIELD_2 = 'FIELD_2'
    OUTPUT = 'OUTPUT'
    OUTPUT_2 = 'OUTPUT_2'

    def createInstance(self):
        return myTestAlg()

    def name(self):
        return 'test'

    def displayName(self):
        return 'Test'

    def initAlgorithm(self, config=None):
        # mastermap_parameter
        self.addParameter(QgsProcessingParameterFeatureSource(
            self.INPUT,
            self.tr('\'Area\' data from OS Mastermap'),
            types=[QgsProcessing.TypeVectorAnyGeometry],
            defaultValue='OSMM_AREA')
        )
        # string1_parameter
        self.addParameter(QgsProcessingParameterString(
            self.STRING_1,
            self.tr('Select features with the following value...'),
            'Buildings',
            multiLine = False,
            optional = False)
        )
        # field1_parameter
        self.addParameter(QgsProcessingParameterField(
            self.FIELD_1,
            self.tr('...in the following attribute field.'),
            'theme',
            self.INPUT)
        )
        # string2_parameter
        self.addParameter(QgsProcessingParameterString(
            self.STRING_2,
            self.tr('Additionally, select features with the following value...'),
            'Building',
            multiLine = False,
            optional = True)
        )
        #field2_parameter
        self.addParameter(QgsProcessingParameterField(
            self.FIELD_2,
            self.tr('...in the following attribute field.'),
            'descriptivegroup',
            self.INPUT,
            QgsProcessingParameterField.DataType.Any,
            allowMultiple = False,
            optional = True)
        )
        self.addParameter(QgsProcessingParameterFeatureSource(
            self.NRD_INPUT,
            self.tr('Data from National Receptor Dataset'),
            [QgsProcessing.TypeVectorAnyGeometry])
        )
        self.addParameter(QgsProcessingParameterFeatureSink(
            self.OUTPUT,
            self.tr('Output data'))
        )
        self.addParameter(QgsProcessingParameterFeatureSink(
            self.OUTPUT_2,
            self.tr('OS Mastermap buildings without NRD'))
        )
    
    def processAlgorithm(self, parameters, context, feedback):
        results = {}
        outputs = {}

        # Extract by expression
        field_1 = self.parameterAsString(parameters, 'FIELD_1', context)
        field_2 = self.parameterAsString(parameters, 'FIELD_2', context)
        
        alg_params = {
            'EXPRESSION': f'attribute( "{field_1}") =  \'{parameters["STRING_1"]}\' OR attribute( "{field_2}") =  \'{parameters["STRING_2"]}\' ',
            'INPUT': parameters[self.INPUT],
            'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT
        }
        mm_building_layer = processing.run('native:extractbyexpression', alg_params, context=context, feedback=feedback, is_child_algorithm=True)['OUTPUT']
        
        outputs['JoinAttributesByLocation'] = processing.run("native:joinattributesbylocation", {
                'DISCARD_NONMATCHING' : True,
                'INPUT' : mm_building_layer,
                'JOIN' : parameters['NRD_INPUT'],
                'JOIN_FIELDS' : [],
                'METHOD' : 0,
                'NON_MATCHING' : parameters['OUTPUT_2'],
                'OUTPUT' : parameters['OUTPUT'],
                'PREDICATE' : [0],
                'PREFIX' : ''
            },
            is_child_algorithm=True,
            context=context,
            feedback=feedback)
        results[self.OUTPUT] = outputs['JoinAttributesByLocation']['OUTPUT']
        results[self.OUTPUT_2] = outputs['JoinAttributesByLocation']['NON_MATCHING']
    
        return results

You could also filter features using getFeatures() (with an expression) or native:extractbyattribute (with a single attribute).

Related Question