3

I am writing a process that will take parameters as input, and then process part of a raster layer. One (well, four) of the parameters should specify a rectangle on the canvas. Ideally the user should be able to draw a rectangle - left click and drag - and the ulx, uly and lrx, lry coordinates be passed to the process. Now, I have found some code which allows the drawing of a rectangle on the canvas but I do not know how to capture the ulx, uly, lrx and lry for input to the process. I am using QGIS 3.16.

Can anyone point me at some existing PyQGIS that does this, or give some pointers as to how to do this?

2
  • Have a look at this, extracting vertices of a polygon: gis.stackexchange.com/questions/381187/…
    – Babel
    Commented Dec 20, 2020 at 17:15
  • babel - many thanks, but that is not what I am looking for. I want to be able to draw a rectangle on a raster layer and get the coords for that. Commented Dec 21, 2020 at 6:53

1 Answer 1

4

You need to override the canvasReleaseEvent() method inside the QgsMapTool class which you use to draw the rectangle on the canvas so that, as soon as the left mouse button is released after dragging the rectangle, you can retrieve the coordinates of the upper left and lower right corners and pass them to some processing method. The code example below should give you an idea of how this might work. You can paste this into an editor in the Python console and click run.

map_canvas = iface.mapCanvas()

class dragRectangle(QgsMapToolEmitPoint):
    def __init__(self, canvas):
        self.canvas = canvas
        QgsMapToolEmitPoint.__init__(self, self.canvas)
        self.start_point = None
        self.end_point = None
        self.rubber_band = QgsRubberBand(self.canvas, True)
        self.rubber_band.setStrokeColor(Qt.red)
        self.rubber_band.setWidth(1)
        self.msg = QMessageBox()
        
    def canvasPressEvent(self, e):
        self.start_point = self.toMapCoordinates(e.pos())
        self.end_point = self.start_point
    
    def canvasMoveEvent(self, e):
        if self.start_point:
            end_point = self.toMapCoordinates(e.pos())
            self.rubber_band.reset()
            p = self.get_rect_points(self.start_point, end_point)
            self.rubber_band.setToGeometry(QgsGeometry.fromPolygonXY(p), None)
                
    def canvasReleaseEvent(self, e):
        self.end_point = self.toMapCoordinates(e.pos())
        r = self.get_rectangle()
        self.rubber_band.reset()
        self.start_point = None
        self.end_point = None
        ulx = r.xMinimum()
        uly = r.yMaximum()
        lrx = r.xMaximum()
        lry = r.yMinimum()
        self.msg.setText("Upper left x: {} \nUpper left y: {}\nLower right x: {}\nLower right y: {}".format(ulx, uly, lrx, lry))
        self.msg.show()
        # instead of showing a mesage box you can call a method which does your
        # processing and pass ulx, uly, lrx & lry parameters...
        

    def get_rect_points(self, startPoint, endPoint):
        points = [[QgsPointXY(startPoint.x(), startPoint.y()), QgsPointXY(endPoint.x(), startPoint.y()), QgsPointXY(endPoint.x(), endPoint.y()), QgsPointXY(startPoint.x(), endPoint.y())]]
        return points
        
    def get_rectangle(self): 
        rect = QgsRectangle(self.start_point, self.end_point)
        return rect
    
T = dragRectangle(map_canvas)
map_canvas.setMapTool(T)
#T.deactivated.connect(lambda: print('Tool deactivated'))

Updated answer:

If you are creating a processing script which extends the QgsProcessingAlgorithm class, there is a much simpler solution. You can use the QgsProcessingParameterExtent class inside the initAlgorithm() method which handles taking a user defined extent entered directly, calculated from a layer, from the current canvas extent or from a rectangle drawn on the canvas (you do not even need to create your own map tool class!). The extent can then be accessed inside the processAlgorithm() method by calling self.parameterAsExtent(). You should be able to get the idea from the example script below. You can save it as a .py file and add it to the processing toolbox to test how it works.

This is a modified version of a template script from underdark available here:

https://anitagraser.com/2018/03/25/processing-script-template-for-qgis3/

from qgis.PyQt.QtCore import QCoreApplication, QVariant
from qgis.core import (QgsField, QgsFeature, QgsFeatureSink, QgsFeatureRequest, QgsProcessing,
        QgsProcessingAlgorithm, QgsProcessingParameterFeatureSource,
        QgsProcessingParameterExtent, QgsProcessingParameterFeatureSink)
                       
class ExAlgo(QgsProcessingAlgorithm):
    INPUT = 'INPUT'
    OUTPUT = 'OUTPUT'
    EXTENT = 'EXTENT'
 
    def __init__(self):
        super().__init__()
 
    def name(self):
        return "exalgo"
     
    def tr(self, text):
        return QCoreApplication.translate("exalgo", text)
         
    def displayName(self):
        return self.tr("Example script")
 
    def group(self):
        return self.tr("Examples")
 
    def groupId(self):
        return "examples"
 
    def shortHelpString(self):
        return self.tr("Example script which demonstrates getting user defined\
                        extent with the QgsProcessingParameterExtent class")
 
    def helpUrl(self):
        return "https://qgis.org"
         
    def createInstance(self):
        return type(self)()
   
    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterFeatureSource(
            self.INPUT,
            self.tr("Input layer"),
            [QgsProcessing.TypeVectorAnyGeometry]))
        
        # The QgsProcessingParameterExtent class takes care of getting the
        # user defined extent for the processing algorithm
        self.addParameter(QgsProcessingParameterExtent(
            self.EXTENT,
            self.tr("Processing extent")))
            
        self.addParameter(QgsProcessingParameterFeatureSink(
            self.OUTPUT,
            self.tr("Output layer"),
            QgsProcessing.TypeVectorAnyGeometry))
 
    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)
        # extent is a QgsRectangle object which you can use in your processing
        extent = self.parameterAsExtent(parameters, self.EXTENT, context)
        (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
                                               source.fields(), source.wkbType(),
                                               source.sourceCrs())
 
        features = source.getFeatures(QgsFeatureRequest())
        for feat in features:
            if feat.geometry().intersects(extent):
                out_feat = QgsFeature()
                out_feat.setGeometry(feat.geometry())
                out_feat.setAttributes(feat.attributes())
                sink.addFeature(out_feat, QgsFeatureSink.FastInsert)
 
        return {self.OUTPUT: dest_id}
8
  • 1
    Spot on Ben, perfect. Thanks. Works perfectly through console too.Thanks so much. Commented Dec 21, 2020 at 6:54
  • Great! You're welcome.
    – Ben W
    Commented Dec 21, 2020 at 7:02
  • I am having trouble getting this code to activate in my script. I have a standard processing template code, with InitAlgorithm, and processAlgorithm methods in the main Algorithm class. I call this dragRectangle from the initAlgorithm method, and it executes, but it doesn't stay active - that is to say it is not available to define a rectangle on the canvas. Commented Dec 21, 2020 at 14:57
  • If I call dragRectangle (using your statements) from the end of the code, outside both classes, dragRectangle executes and stays active when I reload the plugin, until I change cursor/function. But if I then I execute the plugin, dragRectangle does not execute. It appears to be a matter of focus changing as execution proceeds,so where should I place the executing statements - any ideas, please? Commented Dec 21, 2020 at 14:59
  • @WestCorkPalaeo, please see my updated answer. I didn't properly interpret from your question that you are creating a QgsProcessingAlgorithm. In that case there is a much simpler solution which does not even require you to create your own map tool class! Just add a QgsProcessingParameterExtent in your initAlgorithm() method!
    – Ben W
    Commented Dec 22, 2020 at 2:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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