2

I'm very new to QGIS and PyQGIS. I've used ArcGIS Desktop and a little bit of ArcPy in the past.

Right now I am trying to build a processing plugin that takes a shapefile and data (csv), conducts a join, repaints the vector layer according to pre-made classification styles and outputs a collection of images to a folder. I want the user to be able to input the following parameters: shapefile (polygons) representing the vector layer, excel data (csv file), layout template (qpt file for creating the images) and a folder destination for where the images will go.

I have a Python script that accomplishes this process for one shapefile and now I'm trying to take that script and use it for my plugin. I took a course on building plugins but because I'm pretty inexperienced with QGIS I still have a lot of uncertainties. So far, I have been able to create the correct dialog box for the user inputs (and filter for the correct file extensions) but I'm stuck in my algorithm (in the plugin_algorithm.py). Right now I have this:

class ZoneMapperAlgorithm(QgsProcessingAlgorithm):
    """
    This algorithm takes polygons (shapefiles) representing agricultural, as well as the inputs of a 
    layout template and soil data (csv file) to
    produce classified maps to illustrate soil testing results
    """

    # 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.

    CHAMP = "CHAMP"
    RESULTS = "RESULTS"
    TEMPLATE = "TEMPLATE"
    IMAGE_COLLECTION = "IMAGE_COLLECTION"

def initAlgorithm(self, config):
    """
    Here we define the inputs and output of the algorithm, along
    with some other properties.
    """

    # We add the input vector features source. It can have any kind of
    # geometry.
    self.addParameter(
        QgsProcessingParameterFeatureSource(
            self.CHAMP,
            self.tr('Champ'),
            [QgsProcessing.TypeVectorPolygon]
        )
    )

    # We add our results.csv file as an input parameter
    self.addParameter(
        QgsProcessingParameterFile(
            self.RESULTS,
            self.tr('Results'),
            extension = "csv"
        )
    )
    #we add our template.qpt file as an input
    self.addParameter(
        QgsProcessingParameterFile(
            self.TEMPLATE,
            self.tr('Template'),
            extension = "qpt"
        )
    )


    # We create a folder to collect our images
    self.addParameter(
        QgsProcessingParameterFolderDestination(
            self.IMAGE_COLLECTION,
            self.tr('Collection Images')
        )
    )

def processAlgorithm(self, parameters, context, feedback):
    """
    Here is where the processing itself takes place.
    """
    lyrChamp = self.parameterAsVectorLayer(parameters, self.CHAMP, context)
    dataResults = self.parameterAsVectorLayer(parameters, self.RESULTS, context)
    lytTemplate = self.parameterAsFile(parameters, self.TEMPLATE, context)

    csvField = 'Zone'
    shpField = 'Zone'

    fieldSubset = ["pHeau", "pHtampon", "MatOrg", "P2O5", "K2O", "Ca", "Mg", "Alu", "CEC", "ISP", 
                  "Cu", "Fe", "Mn","Zn", "Bore", "Chaux", "Aire", "Type"]

    # here we set our join parameters
    joinObject = QgsVectorLayerJoinInfo()
    joinObject.setJoinFieldName(csvField)
    joinObject.setTargetFieldName(shpField)
    joinObject.setJoinLayerId(dataResults.id())
    joinObject.setUsingMemoryCache(True)
    joinObject.setJoinLayer(dataResults)
    joinObject.setUpsertOnEdit(True)
    joinObject.setDynamicFormEnabled(True)
    joinObject.setJoinFieldNamesSubset(fieldSubset)
    joinObject.setPrefix('')
    lyrChamp.addJoin(joinObject)

I'm not sure if I should use self.parameterAsVectorLayer for the data results but I know I have to convert it to a vector layer before I can conduct a join right? Also, if I try to run this as it is, I get an error like: "NoneType cannot be converted to a C/C++ QVariantMap in this context" and I know that's because I haven't returned anything but honestly I'm not sure what the best thing is to "return" in this context. Clearly I'm pretty lost.

Here is my original Python script that runs in QGIS and accomplishes what I want for a single specific shapefile (it also imports a sattelite image basemap but I'm not as concerned about that step right now):

#First we must import our satellite base layer 

import requests
service_url = "mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}" 
service_uri = "crs=EPSG:3857&format&type=xyz&url=https://mt1.google.com/vt/lyrs%3Ds%26x%3D%7Bx%7D%26y%3D%7By%7D%26z%3D%7Bz%7D&zmax=19&zmin=0"

#We add our sattelite map layer
tms_layer = QgsRasterLayer(service_uri, "Google Sat", "wms")
tms_layer.isValid()
QgsProject.instance().addMapLayer(tms_layer)

#Now we define our desired shape files (fields)
vectorLyr=QgsVectorLayer("O:/User_Code/PlanZone_Auto/Projects/Shapefiles/ZonesPractice2.shp","ZonesPractice2","ogr")
vectorLyr.isValid()

#Next we define our csv results file
uri='file:///O:/User_Code/PlanZone_Auto/Projects/ResultsFiles/ResultSol.csv?delimiter=,'
infoLyr=QgsVectorLayer(uri,'ResultSol','delimitedtext')
infoLyr.isValid()

#Now we add these layers (shape file and results file, respectively)
QgsProject.instance().addMapLayer(vectorLyr)
QgsProject.instance().addMapLayer(infoLyr)

#Now we can conduct our join
csvField='Zone'
shpField='Zone'

#here we make a subset of the fields we want to be used from the joined layer
fieldSubset = ["pHeau","pHtampon","MatOrg","P2O5","K2O","Ca","Mg","Alu","CEC","ISP","Cu","Fe","Mn","Zn","Bore","Chaux","Aire","Type"]

#here we set our join parameters 
joinObject=QgsVectorLayerJoinInfo()
joinObject.setJoinFieldName(csvField)
joinObject.setTargetFieldName(shpField)
joinObject.setJoinLayerId(infoLyr.id())
joinObject.setUsingMemoryCache(True)
joinObject.setJoinLayer(infoLyr)
joinObject.setUpsertOnEdit (True)
joinObject.setDynamicFormEnabled (True)
joinObject.setJoinFieldNamesSubset(fieldSubset)
joinObject.setPrefix('')
vectorLyr.addJoin(joinObject)

#Now we want to change the symbology and upload our style

styles = ["0-Champ","1-MatOrg","2-CEC","3-pHeau","4-Phosphore","5-Potassium","6-Calcium","7-Magnesium","8-Aluminium","9-ISP","10-Zinc","11-Cuivre","12-Bore","13-Chaux"]

for s in styles:
    vectorLyr.loadNamedStyle("O:/Client dossier propre/AgPrec/RapportZones/StyleQGIS/{}.qml".format(s))
    vectorLyr.triggerRepaint()

    #Now we want to create our layout for image export using a template we have previously made

    # Load template from file
    project = QgsProject.instance()
    manager = project.layoutManager()
    layout = QgsPrintLayout(project)
    layoutName = "Champs"

    layouts_list = manager.printLayouts()
    for layout in layouts_list:
        if layout.name() == layoutName:
            manager.removeLayout(layout)

    layout = QgsPrintLayout(project)

    tmpfile = 'O:/Client dossier/CLIENT/Zone/PlanZone/Graphiques/template/template.qpt'
    with open(tmpfile) as f:
        template_content = f.read()
    doc = QDomDocument()
    doc.setContent(template_content)

    # adding to existing items
    layout.loadFromTemplate(doc, QgsReadWriteContext(), True)

    layout.setName(layoutName)
    manager.addLayout(layout)

    #Now we want to export the layout to an image

    manager = QgsProject.instance().layoutManager()

    #this accesses a specific layout, by name (which is a string)
    layout = manager.layoutByName(layoutName)
    
    #this creates a QgsLayoutExporter object
    exporter = QgsLayoutExporter(layout)

    #this exports an image of the layout object
    exporter.exportToImage('O:/User_Code/PlanZone_Auto/Projects/Graphiques/{}.jpg'.format(s), QgsLayoutExporter.ImageExportSettings()) 

1

I solved my own problem...kind of. Instead of making a Plugin, I built a model using the graphical modeler. I wrote my original working python script into a processing script and made the input variables parameters in model builder. The working script looks like this:


from PyQt5.QtCore import QCoreApplication
from qgis.core import QgsProject, QgsLayout, QgsLayoutExporter, QgsReadWriteContext, QgsMapSettings, QgsProcessingAlgorithm, QgsProcessingParameterString, QgsProcessingParameterFile, QgsPrintLayout, QgsProcessingParameterVectorLayer, QgsVectorLayer, QgsVectorLayerJoinInfo
from PyQt5.QtXml import QDomDocument

class SoilMapper(QgsProcessingAlgorithm):

    FOLDER = 'FOLDER'
    TEMPLATE = 'TEMPLATE'
    LAYER = 'LAYER'
    RESULTS = 'RESULTS'

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFile(
                self.TEMPLATE,
                self.tr('Template File'),
            )
        )
        
        self.addParameter(
            QgsProcessingParameterFile(
                self.RESULTS,
                self.tr('Results File'),
            )
        )

        self.addParameter(
            QgsProcessingParameterString(
                self.FOLDER,
                self.tr('Output Folder'),
            )
        )
        self.addParameter(
            QgsProcessingParameterVectorLayer(
                self.LAYER,
                self.tr('Layer'),
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        output_folder = self.parameterAsString(parameters, 'FOLDER', context)
        template_file = self.parameterAsFile(parameters, 'TEMPLATE', context)
        vector_layer = self.parameterAsVectorLayer (parameters, 'LAYER', context)
        results_file = self.parameterAsFile(parameters, 'RESULTS', context)

        #Next we define our csv results file
        uri= results_file 
        infoLyr=QgsVectorLayer(uri)
        infoLyr.isValid()

        QgsProject.instance().addMapLayer(infoLyr)

        #Now we can conduct our join
        csvField='Zone'
        shpField='Zone'

        #here we make a subset of the fields we want to be used from the joined layer
        fieldSubset = ["pHeau","pHtampon","MatOrg","P2O5","K2O","Ca","Mg","Alu","CEC","ISP","Cu","Fe","Mn","Zn","Bore","Chaux","Aire","Type"]

        #here we set our join parameters 
        joinObject=QgsVectorLayerJoinInfo()
        joinObject.setJoinFieldName(csvField)
        joinObject.setTargetFieldName(shpField)
        joinObject.setJoinLayerId(infoLyr.id())
        joinObject.setUsingMemoryCache(True)
        joinObject.setJoinLayer(infoLyr)
        joinObject.setUpsertOnEdit (True)
        joinObject.setDynamicFormEnabled (True)
        joinObject.setJoinFieldNamesSubset(fieldSubset)
        joinObject.setPrefix('')
        vector_layer.addJoin(joinObject)

        #Now we want to change the symbology and upload our style

        styles = ["0-Champ","1-MatOrg","2-CEC","3-pHeau","4-Phosphore","5-Potassium","6-Calcium","7-Magnesium","8-Aluminium","9-ISP","10-Zinc","11-Cuivre","12-Bore","13-Chaux"]

        for s in styles:
            vector_layer.loadNamedStyle("O:/dossier propre/StyleQGIS/{}.qml".format(s))
            vector_layer.triggerRepaint()

            #Now we want to create our layout for image export using a template we have previously made

            # Load template from file
            project = QgsProject.instance()
            manager = project.layoutManager()
            layout = QgsPrintLayout(project)
            layoutName = "Champs"

            layouts_list = manager.printLayouts()
            for layout in layouts_list:
                if layout.name() == layoutName:
                    manager.removeLayout(layout)

            layout = QgsPrintLayout(project)

            tmpfile = template_file
            with open(tmpfile) as f:
                template_content = f.read()
            doc = QDomDocument()
            doc.setContent(template_content)

            # adding to existing items
            layout.loadFromTemplate(doc, QgsReadWriteContext(), True)

            layout.setName(layoutName)
            manager.addLayout(layout)

            #Now we want to export the layout to an image

            manager = QgsProject.instance().layoutManager()

            #this accesses a specific layout, by name (which is a string)
            layout = manager.layoutByName(layoutName)
            
            #this creates a QgsLayoutExporter object
            exporter = QgsLayoutExporter(layout)

            #this exports an image of the layout object
            exporter.exportToImage('{}/{}.jpg'.format(str(output_folder),s), QgsLayoutExporter.ImageExportSettings()) 
            
        return {}

    def name(self):
        return 'Soil_Mapper'

    def displayName(self):
        return self.tr('Maps Soil Results')

    def group(self):
        return self.tr(self.groupId())

    def groupId(self):
        return ''

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return SoilMapper()

The model in graphical modeler looks like this, where the "Maps Soil Results" is the algorithm I wrote:

enter image description here

I'm sure that now that I have a working model I could make a plugin using plugin builder but this does everything I want so I'm happy.

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.