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 Answer 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:
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.
Explore related questions
See similar questions with these tags.