4

I'm building a Python algorithm for QGIS 3, to be run from the Processing Toolbox. I've finished debugging the algorithm body, which seems to work, but QGIS is unable to load its output.

To summarize, the algorithm generates a vector layer in memory, sets up a QgsHeatmapRenderer object with some desired parameters, and sets this as the layer's renderer. It works correctly, but QGIS is somehow unable to deal with the result.

Here's the algorithm code (also available on GitHub):

# -*- coding: utf-8 -*-

# Create a heatmap of a single route, based on data from the OpenRouteService
# API. This is a proof-of-concept script only.

from PyQt5.QtCore import QCoreApplication
from PyQt5.QtGui import QColor
from qgis.core import *
import processing
import openrouteservice

# Define variables as necessary
# Here
failures = 0

# Create necessary functions
# Here

class MyProcessingAlgorithm(QgsProcessingAlgorithm):
    """
    This algorithm creates a heatmap of a single route, based on data from the
    OpenRouteService API. This is a proof-of-concept script only.
    """

    # Constants used to refer to parameters and outputs. They will be
    # used when calling the algorithm from another algorithm, or when
    # calling from the QGIS console.

    INPUT = 'INPUT'
    OUTPUT = 'OUTPUT'

    def tr(self, string):
        """
        Returns a translatable string with the self.tr() function.
        """
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return MyProcessingAlgorithm()

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm. This
        string should be fixed for the algorithm, and must not be localised.
        The name should be unique within each provider. Names should contain
        lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return 'singlepathheatmap'

    def displayName(self):
        """
        Returns the translated algorithm name, which should be used for any
        user-visible display of the algorithm name.
        """
        return self.tr('Single Path Heatmap')

    def group(self):
        """
        Returns the name of the group this algorithm belongs to. This string
        should be localised.
        """
        return self.tr('Science Fair 2019')

    def groupId(self):
        """
        Returns the unique ID of the group this algorithm belongs to. This
        string should be fixed for the algorithm, and must not be localised.
        The group id should be unique within each provider. Group id should
        contain lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return 'sciencefairtwentynineteen'

    def shortHelpString(self):
        """
        Returns a localised short helper string for the algorithm. This string
        should provide a basic description about what the algorithm does and the
        parameters and outputs associated with it.
        """
        return self.tr("This script creates a heatmap of a single route, \
                        based on data from the OpenRouteService API. This is \
                        a proof-of-concept script only.")

    def initAlgorithm(self, config=None):
        """
        Define the inputs and output of the algorithm, along with some other
        properties.
        """

        # Add a text input for the user's OpenRouteService API key.
        self.addParameter(
            QgsProcessingParameterString(
                self.INPUT,
                self.tr('Your OpenRouteService API Key')
            )
        )

        # Add a feature sink in which to store our processed features (this
        # usually takes the form of a newly created vector layer when the
        # algorithm is run in QGIS).
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Heatmap Layer')
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        """
        Here is where the processing itself takes place.
        This algorithm imports data from 
        """

        # Retrieve the API key and feature sink.
        APIKey = self.parameterAsString(
            parameters,
            self.INPUT,
            context
        )
        # And the description of the output layer (most likely 'memory:').
        outName = self.parameterAsOutputLayer(
            parameters,
            self.OUTPUT,
            context
        )

        feedback.setProgressText('Loading routes...')
        # Begin processing
        start = (-114.31506,48.20218)
        end = (-114.63478,48.19400)
        client = openrouteservice.Client(key=APIKey)
        routes = client.directions((start,end))
        encoded = routes['routes'][0]['geometry']
        decoded = openrouteservice.convert.decode_polyline(encoded)
        routeLayer = QgsVectorLayer("LineString", "Kalispell to Ashley Lake",
                               "memory")
        provider = routeLayer.dataProvider()
        feat = QgsFeature()
        feat.setGeometry(QgsGeometry.fromPolyline(
            [QgsPoint(pt[0],pt[1]) for pt in decoded['coordinates']]))
        provider.addFeature(feat)
        result = processing.run('qgis:densifygeometriesgivenaninterval', {
                'INPUT':routeLayer,
                'INTERVAL':0.001,
                'OUTPUT':'memory:'
                })
        result2 = processing.run('native:extractvertices', {
                'INPUT':result['OUTPUT'],
                'OUTPUT':outName
                })
        # This is the output layer:
        outLayer = result2['OUTPUT']
        # Set up the desired heatmap renderer:
        rndrr = QgsHeatmapRenderer()
        rndrr.setColorRamp(QgsGradientColorRamp(
            QColor('transparent'),QColor(227,26,28)))
        rndrr.setRadiusUnit(1)
        rndrr.setRadius(500)
        # Set the output layer's renderer to the heatmap renderer just defined
        outLayer.setRenderer(rndrr)
        # Done with processing
        feedback.pushInfo("Done with processing.")

        # Return the output layer.
        return {self.OUTPUT: outLayer.id()}

And the output from running the algorithm: Algorithm Output (The algorithm requires me to input my OpenRouteService API key, which I've blacked out in this screenshot.)

As you can see, the algorithm appears to have been successful, except for the final loading of results.

As instructed, I checked the Log Messages Panel's Processing tab, but it contains no indication of the error, only this: Processing Tab, Log Messages Panel Each entry here corresponds to a layer generated by the native QGIS Densify by Interval and Extract Vertices algorithms, which mine calls.

However, the Python Warning log tab does contain something interesting, albeit unhelpful: Python Warning Tab, Log Messages Panel

I am relatively new to QGIS, so I'm unsure of what might be happening here. Can anyone offer some insight into this problem?

  • I don't know you problem. I would try removing "QgsProject.instance().addMapLayer(routeLayer)", and also the heatmap renderer. Can you comment these lines? – etrimaille Feb 24 at 11:20
  • @etrimaille Ah, good point. Since I'm leaving the "Load layers on completion" box checked, adding the layer "manually" in that way isn't necessary. – Luke R Feb 24 at 18:14
  • @etrimaille Update: commenting out those sections didn't resolve the issue, and I'm still seeing the same results, both in the algorithm output window and in the Python Warning log panel. I'm thinking it might be something to do with returning the wrong thing in the results dictionary at the end of the script, but I don't know. – Luke R Feb 24 at 18:21
  • I think something is wrong in defining the output. In this example also a processing tool is run internaly and its output is explicitly "copied" to the sink. – Andreas Müller Mar 25 at 10:32
  • @Andreas Müller That is entirely possible. I'll look into copying features over into my output layer. Although there's still an issue that isn't addressed with that - I'd like to be able to set the renderer for the output layer (whichever layer that ends up being). – Luke R Mar 27 at 2:08
4
+200

I have examined some of the Processing scripts included with QGIS, found here:

<Install Dir> \apps\qgis\python\plugins\processing\algs\qgis

Specifically I looked at: Heatmap.py

The heatmap script is similar to yours in the sense that the output is generated by a call to a 'tool', passing the whole source dataset, rather than iterating over the features within the source data and writing them out one by one. The relevant lines are:

Line 186:

outputFile = self.parameterAsOutputLayer(parameters, self.OUTPUT, context)

Line 210:

kde = QgsKernelDensityEstimation(kde_params, outputFile, output_format)

Line 233:

return {self.OUTPUT: outputFile}

From this I would suggest that you modify the last line of your code to read:

return {self.OUTPUT: outName}

Hopefully the output will load after this change! I'm not convinced that it will be styled though. You may glean some further useful hints from the SetVectorStyle.py script.

  • Ah, so that's where the native QGIS algorithms are saved. Thank you for the suggestion; I'll try it out soon. Also, even if your idea doesn't fix the issue, I'll be able to examine other algorithm scripts in that folder to figure out how everything is supposed to work. – Luke R Mar 21 at 21:01
  • All right. I tried your suggestion (using return {self.OUTPUT: outName}), and it seemed to help a little. The red error message in the algorithm execution window now reads, The following layers were not correctly generated.<ul><li>memory:Heatmap Layer</li></ul>You can check ... (truncated for brevity). As you can see, QGIS now appears to know the correct name for the output layer, so that's something. Otherwise, there are no discernible changes. – Luke R Mar 31 at 1:14
  • Another thing: I did look at SetVectorStyle.py, and it appears that processing scripts should return the native Python layer object. SetVectorStyle.py, for instance, does layer = parameterAsVectorLayer(...), and later, return {self.INPUT: layer}. – Luke R Mar 31 at 1:38
  • The difference that I see between your script and SetVectorStyle.py is that the SetVectorStyle.py input is a pre-existing layer in QGIS that doesn't need to be loaded – Andy Harfoot Apr 1 at 10:18

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.