[GIS] Programmatically selecting features by polygon

point-in-polygonpyqgispython-2.7qgis-2select

Expanding upon this post how could you use QGIS to programmatically draw a selection polygon and select features within the polygon? I have been using QgsMapToolEmitPoint(self.canvas), to find and select features by point but I am struggling to find a similar polygon solution.

Best Answer

Check this code for your answer:

https://github.com/dsgoficial/DsgTools/blob/master/ProductionTools/CopyPasteTool/multiLayerSelect.py

On DSGTools, there is a tool called Generic Selector which among other stuff, does exactly what you want. The selection using a polygon is achieved by using the shift modifier in the tool. The tool on the link above is a child of QgsMapTool and we have to reimplement some methods in order to get things done. Explaining the code in a general way, you have to reimplement 3 methods: the canvasPressEvent, canvasReleaseEvent and the canvasMoveEvent.

Let's break the problem into 2 smaller ones:

  1. Build the search polygon
  2. Get the features.

To build the polygon, you have to click, drag and release. The click event is the canvasPressEvent. In this method, you have to get the coordinate of the beginning of the rectangle, then you have to let the canvasMoveEvent that you have to build the rubberband (polygon that is shown on screen). The code snippet bellow shows you what you can do:

def canvasPressEvent(self, e):
    """
    Method used to build rectangle if shift is held, otherwise, feature select/deselect and identify is done.
    """
    if QtGui.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier:
        self.isEmittingPoint = True
        self.startPoint = self.toMapCoordinates(e.pos())
        self.endPoint = self.startPoint
        self.isEmittingPoint = True
        self.showRect(self.startPoint, self.endPoint)
    else:
        self.isEmittingPoint = False
        self.createContextMenu(e)

The variable self.isEmittingPoint is used to build the rubberband on the method onCanvasMoveEvent:

def canvasMoveEvent(self, e):
    """
    Used only on rectangle select.
    """
    if not self.isEmittingPoint:
        return
    self.endPoint = self.toMapCoordinates( e.pos() )
    self.showRect(self.startPoint, self.endPoint)

The method self.showRect resets and draws the rubberband:

def showRect(self, startPoint, endPoint):
    """
    Builds rubberband rect.
    """
    self.rubberBand.reset(QGis.Polygon)
    if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
        return
    point1 = QgsPoint(startPoint.x(), startPoint.y())
    point2 = QgsPoint(startPoint.x(), endPoint.y())
    point3 = QgsPoint(endPoint.x(), endPoint.y())
    point4 = QgsPoint(endPoint.x(), startPoint.y())

    self.rubberBand.addPoint(point1, False)
    self.rubberBand.addPoint(point2, False)
    self.rubberBand.addPoint(point3, False)
    self.rubberBand.addPoint(point4, True)    # true to update canvas
    self.rubberBand.show()

Then, when you release the click, the canvasReleaseEvent is emitted:

def canvasReleaseEvent(self, e):
    """
    After the rectangle is built, here features are selected.
    """
    if QtGui.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier:
        self.isEmittingPoint = False
        r = self.rectangle()
        layers = self.canvas.layers()
        for layer in layers:
            #ignore layers on black list and features that are not vector layers
            if layer.type() != QgsMapLayer.VectorLayer or (self.layerHasPartInBlackList(layer.name())):
                continue
            if r is not None:
                #builds bbRect and select from layer, adding selection
                bbRect = self.canvas.mapSettings().mapToLayerCoordinates(layer, r)
                layer.select(bbRect, True)
        self.rubberBand.hide()

On this method, we can achieve the second step, the selection. This tool on DSGTools selects from every layer loaded. To do so, first we have to get the search rectangle, transforming the rubberband into a QgsRectangle. We have implemented a method called rectangle to do so:

def rectangle(self):
    """
    Builds rectangle from self.startPoint and self.endPoint
    """
    if self.startPoint is None or self.endPoint is None:
        return None
    elif self.startPoint.x() == self.endPoint.x() or self.startPoint.y() == self.endPoint.y():
        return None
    return QgsRectangle(self.startPoint, self.endPoint)

After getting the rectangle, we iterate on every layer, get the bbRect and finally select the features using layer.select

I hope I was clear on my answer!

Related Question