2

I have written a simple QgsProcessingAlgorithm that populates a QgsFeatureSink.
Before outputting the result, I need to call another algorithm to clip my features.
Everything seems to work just fine, the clip algorithm is called and its output shows in the log, except my actual output is not clipped.
It seems the sink is returned no matter what I specify to be actually returned by the function.

What I am doing wrong ?

from qgis.PyQt.QtCore import QCoreApplication, QVariant
from qgis.processing import alg
from qgis.core import (
    QgsField,
    QgsFields,
    QgsFeature,
    QgsFeatureSink,
    QgsGeometry,
    QgsPointXY,
    QgsProcessing,
    QgsProcessingAlgorithm,
    QgsWkbTypes
)
import processing
import random

@alg(name='test_alg', label=alg.tr('Test'), group='test', group_label=alg.tr('Test'))
@alg.input(type=alg.SOURCE, name='INPUT', label='Input')
@alg.input(type=int, name='NB_POINTS', label='Number of points to create', default=1000, minValue=10, maxValue=100000)
@alg.input(type=alg.SINK, name='OUTPUT', label='Output')
def testAlg(instance, parameters, context, feedback, inputs):
    """
    Test script to randomly generate n points unniformly distributed within a polygon extent
    and clip the result with regards to the polygon itself.
    """
    # Define some attributes
    fields = QgsFields()
    fields.append(QgsField('Point_No', QVariant.Double))
    # Initialise variables
    source = instance.parameterAsSource(parameters, 'INPUT', context)
    nb_points = instance.parameterAsInt(parameters, 'NB_POINTS', context)
    # Setup the sink
    (sink, dest_id) = instance.parameterAsSink(parameters, 'OUTPUT', context, fields, QgsWkbTypes.Point, source.sourceCrs())
    # Define the Points generator
    def genPoints(n, extent, attributes):
        """
        Generator to produce the Points yielded as Features with attribute
        """
        feat = QgsFeature()
        xmin, xmax, ymin, ymax = extent.xMinimum(), extent.xMaximum(), extent.yMinimum(), extent.yMaximum()
        total = 100 / n
        for i in range(n):
            feedback.setProgress(i * total)
            attributes[0] = i
            x, y = random.uniform(xmin, xmax), random.uniform(ymin, ymax)
            feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
            feat.setAttributes(attributes)
            yield feat

    feedback.pushInfo(f'Generating {nb_points} Points')
    sink.addFeatures(genPoints(nb_points, source.sourceExtent(), [0]),
                      QgsFeatureSink.FastInsert)    
    feedback.pushInfo('Clipping to Input Polygon...')
    clipped_points = processing.run('native:clip', {
            'INPUT': dest_id,
            'OVERLAY': parameters['INPUT'],
            'OUTPUT': 'memory:ClippedPoints',
        }, context=context, feedback=feedback)['OUTPUT']

    return {'OUTPUT': clipped_points.id()}
  • Change the workflow and create your point layer as a temporary intermediate layer (don't add it to the sink), clip it and then add features from the clipped layer to the sink. See qgistutorials.com/en/docs/3/processing_python_scripts.html. – spatialthoughts Jun 29 at 11:31
  • Also doesn't the processing algorithm for generating random point within a polygon doesn't work for you? – spatialthoughts Jun 29 at 11:35
  • @spatialthoughts Thanks, that is pretty much what I've done in my answer. As for the random points, this is not really what I'm after, this was just a simple example to get the workflow working. – YeO Jun 29 at 13:04
0

My feeling is that the QgsProcessingAlgorithm will return the sink no matter what because it is setup in the parameters. Incidentally, the way to define a sink output is as an input, but that's another story.

I resorted to create a temporary QgsVectorLayer to populate with my features, then clip it and only then populate my sink. It works but does feel a bit stupid to populate the sink at this stage.

It turns out the temporary QgsVectorLayer is the way to go, but in order to be clipped properly, it is critical to set the Crs otherwise there won't be anything left after the clipping.

The trick here (or at least this is my understanding) is only to direct the output of the clipping algorithm to the sink and it will be returned just fine (note the is_child_algorithm=True, this seems to be needed as well, as I've experienced a few crashes without it).

from qgis.PyQt.QtCore import QCoreApplication, QVariant
from qgis.processing import alg
from qgis.core import (
    QgsField,
    QgsFields,
    QgsFeature,
    QgsGeometry,
    QgsPointXY,
    QgsProcessing,
    QgsProcessingAlgorithm,
    QgsProcessingUtils,
    QgsVectorLayer
)
import processing
import random

@alg(name='test_alg3', label=alg.tr('Test3'), group='test', group_label=alg.tr('Test'))
@alg.input(type=alg.SOURCE, name='INPUT', label='Input')
@alg.input(type=int, name='NB_POINTS', label='Number of points to create', default=1000, minValue=10, maxValue=100000)
@alg.input(type=alg.SINK, name='OUTPUT', label='Output')
def testAlg(instance, parameters, context, feedback, inputs):
    """
    Test script to randomly generate n points unniformly distributed within a polygon extent
    and clip the result with regards to the polygon itself.
    """
    # Define some attributes
    fields = QgsFields()
    fields.append(QgsField('Point_No', QVariant.Double))
    # Initialise variables
    source = instance.parameterAsSource(parameters, 'INPUT', context)
    nb_points = instance.parameterAsInt(parameters, 'NB_POINTS', context)

    # Define the Points generator
    def genPoints(n, extent, attributes):
        """
        Generator to produce the Points yielded as Features with attribute
        """
        feat = QgsFeature()
        xmin, xmax, ymin, ymax = extent.xMinimum(), extent.xMaximum(), extent.yMinimum(), extent.yMaximum()
        total = 100 / n
        for i in range(n):
            feedback.setProgress(i * total)
            attributes[0] = i
            x, y = random.uniform(xmin, xmax), random.uniform(ymin, ymax)
            feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
            feat.setAttributes(attributes)
            yield feat

    feedback.pushInfo(f'Generating {nb_points} Points...')
    temp = QgsVectorLayer('Point', 'temp', 'memory')
    temp.setCrs(source.sourceCrs())
    temp.dataProvider().addFeatures(genPoints(nb_points, source.sourceExtent(), [0]))

    feedback.pushInfo('Clipping to Input Polygon...')
    results = processing.run('native:clip', {
            'INPUT': temp,
            'OVERLAY': parameters['INPUT'],
            'OUTPUT': parameters['OUTPUT']
        }, context=context, feedback=feedback, is_child_algorithm=True)

    return results

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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