Getting Started Writing QGIS Python Plugins

OCTOBER 17, 2014 pyqgis qgis

This blog post is a QGIS plugin tutorial for beginners. It was written to support a workshop we ran for the Scottish QGIS user group here in the UK and aims to be a simple step-by-step guide.

In this tutorial you will develop your first QGIS plugin - a Map Tool for selecting the closest feature within multiple loaded vector layers. Knowledge of Python is recommended but not required.

The Goal

Before we get started let’s look at where we’re going.

We will develop a plugin that implements a new QGIS Map Tool. The Identify Features QGIS Identify Tool and Pan Map QGIS Pan Tool tools are both examples of QGIS Map Tools. A Map Tool is a tool which performs an action when used with the map canvas.

We will create a new Select Nearest Feature Map Tool Nearest Feature Tool which will sit in the plugins toolbar.

Our Select Nearest Feature Map Tool will allow the user to select the feature nearest a mouse click on the canvas. For example, clicking here:

Example Canvas Click

would select the following polygon:

Example Polygon Selection

The Starting Point

Before getting started:

The QGIS Plugin Builder plugin was used to create a base plugin which we’ll modify to fit our requirements.

This base plugin can be found in the zip file mentioned above under code/01__Empty Plugin/NearestFeature

code/01__Empty Plugin contains a batch file install.bat that can be used to copy the plugin into your QGIS plugins folder, making it available to QGIS.

Let’s now load and run this simple base plugin in QGIS.

  • Run install.bat
  • Restart QGIS if already open
  • Open the Plugin Manager: Plugins > Manage and Install Plugins
  • Enable the Nearest Feature plugin

A new action Nearest Feature Tool should now be visible in the plugins toolbar which opens the following dialog:

Empty Dialog

Creating a Basic Map Tool

When activated, our plugin currently shows a simple dialog (functionality provided by the Plugin Builder plugin. We’re going to adapt it to instead activate a Map Tool.

A basic Map Tool is included within the zip file mentioned above. It can be found in nearest_feature_map_tool.py in the Additional Files folder.

  • Copy nearest_feature_map_tool.py into the NearestFeature folder and open it in an editor.
  • Note that many of the code segments (highlighted in gray) below link to relevant parts of the API docs. Those links will open in a dedicated browser tab.

nearest_feature_tool.py defines a new NearestFeatureMapTool class (line 28) which inherits (is based on) QgsMapTool, the QGIS Map Tool class. Its __init__() method expects to be passed a reference to a QgsMapCanvas. This canvas reference is passed to the constructor of the underlying QgsMapTool class on line 32 and then stored on line 33 for later use. The QGIS API documentation describes the functionality made available by QgsMapTool.

On line 34 we define a simple, different-looking cursor (a QCursor based on Qt.CrossCursor) later used to indicate that the Map Tool is active.

Our class definition features a method called activate(). Notice the API documentation for QgsMapTool already defines a method with the same name. Any methods defined as virtual methods in the API documentation can be overwritten or redefined as they have been within this file. Here we have overwritten the default implementation of activate().

The activate() method is called when the tool is activated. The new cursor based on Qt.CrossCursor defined above is set with a call to QgsMapCanvas.setCursor().

For the moment, when activated, our Map Tool would simply change the cursor style - that’s all.

Great - next we’ll get our plugin to use the new Map Tool.

Connecting the Basic Map Tool

In this section we will modify the plugin to make use of our new Map Tool.

  • Open nearest_feature.py in a text editor.

We need to first import the NearestFeatureMapTool class before we can use it.

  • Add the following code towards the top of the file just before os.path is imported:
from nearest_feature_map_tool import NearestFeatureMapTool

Next we will create a new instance of the NearestFeatureMapTool class and store a reference to it in self.nearestFeatureMapTool.

  • Add the following code to the initGui() method just before the call to self.add_action() taking care to ensure the indentation is correct:
# Create a new NearestFeatureMapTool and keep reference
self.nearestFeatureMapTool = NearestFeatureMapTool(self.iface.mapCanvas())

Notice that a reference to the map canvas has been passed when creating the new NearestFeatureMapTool instance.

The run() method is called when our plugin is called by the user. It’s currently used to show the dialog we saw previously. Let’s overwrite its current implementation with the following:

# Simply activate our tool
self.iface.mapCanvas().setMapTool(self.nearestFeatureMapTool)

The QGIS map canvas (QgsMapCanvas class) provides the setMapTool() method for setting map tools. This method takes a reference to the new map tool, in this case a reference to a NearestFeatureMapTool.

To ensure that we leave things in a clean state when the plugin is unloaded (or reloaded) we should also ensure the Map Tool is unset when the plugin’s unload() method is called.

  • Add the following code to the end of the unload() method:
# Unset the map tool in case it's set
self.iface.mapCanvas().unsetMapTool(self.nearestFeatureMapTool)

Now let’s see the new map tool in action.

  • Save your files
  • Run install.bat to copy the updated files to the QGIS plugin folder
  • Configure the Plugin Reloader plugin to reload the NearestFeature plugin using its configure button, Configure Plugin Reloader Tool

Configure Plugin Reloader Dialog

  • Reload the Nearest Feature plugin using the Reload Plugin Tool button.
  • Click the Nearest Feature Tool button

When passing the mouse over the map canvas the cursor should now be shown as a simple cursor resembling a plus sign. Congratulations - the Map Tool is being activated.

Map Tool State

When you use the Identify Features Map Tool you’ll notice that its button remains depressed when the tool is in use. The button for our map tool does not yet act in this way. Let’s fix that.

The action (QAction) associated with our plugin is defined in the initGui() method with a call to self.add_action().

self.add_action() actually returns a reference to the new action that’s been added. We’ll make use of this behaviour to make the action / button associated with our Map Tool toggleable (checkable).

  • Modify the call to add_action() as follows:
action = self.add_action(
            icon_path,
            text=self.tr(u'Select nearest feature.'),
            callback=self.run,
            parent=self.iface.mainWindow())

action.setCheckable(True)

We now use the reference to the new action to make it checkable.

The QgsMapTool class has a setAction() method which can be used to associate a QAction with the Map Tool. This allows the Map Tool to handle making the associated button look pressed.

  • Add the following line to the end of initGui():
self.nearestFeatureMapTool.setAction(action)
  • Save your files and run install.bat
  • Reload the Nearest Feature plugin using the Reload Plugin button
  • Click the Nearest Feature Tool button

The button should now remain pressed, indicating that the tool is in use.

  • Activate the Identify Features Identify Features Tool tool

The Nearest Feature button should now appear unpressed.

Handling Mouse Clicks

The QgsMapTool class has a number of methods for handling user events such as mouse clicks and movement. We will override the canvasReleaseEvent() method to implement the search for the closest feature. canvasReleaseEvent() is called whenever the user clicks on the map canvas and is passed a QMouseEvent as an argument.

We will now write some functionality which:

  1. Loops through all visible vector layers and for each:
    • Deselects all features
    • Loops through all features and for each:
      • Determines their distance from the mouse click
      • Keeps track of the closest feature and its distance
  2. Determines the closest feature from all layers
  3. Selects that feature

 

  • Add the following method to the NearestFeatureMapTool class:
def canvasReleaseEvent(self, mouseEvent):
    """
    Each time the mouse is clicked on the map canvas, perform
    the following tasks:
        Loop through all visible vector layers and for each:
            Ensure no features are selected
            Determine the distance of the closes feature in the layer to the mouse click
            Keep track of the layer id and id of the closest feature
        Select the id of the closes feature
    """

    layerData = []

    for layer in self.canvas.layers():

        if layer.type() != QgsMapLayer.VectorLayer:
            # Ignore this layer as it's not a vector
            continue

        if layer.featureCount() == 0:
            # There are no features - skip
            continue

        layer.removeSelection()

The layers() method of QgsMapCanvas (stored earlier in self.canvas) returns a list of QgsMapLayer. These are references to all visible layers and could represent vector layers, raster layers or even plugin layers.

We use the type() and featureCount() methods to skip non-vector layers and empty vector layers.

Finally we use the layer’s removeSelection() method to clear any existing selection. layerData is a list that we’ll use in a moment.

Our plugin now clears the selection in all visible vector layers.

  • Open the Shapefiles included in the Data folder.
  • Make a selection of one or more layers.
  • Reload the plugin and ensure it is working as expected (removing any selection).

Accessing Features and Geometry

We now need access to each feature and its geometry to determine its distance from the mouse click.

  • Add the following code to canvasReleaseEvent() within the loop over layers:
# Determine the location of the click in real-world coords
layerPoint = self.toLayerCoordinates( layer, mouseEvent.pos() )

shortestDistance = float("inf")
closestFeatureId = -1

# Loop through all features in the layer
for f in layer.getFeatures():
    dist = f.geometry().distance( QgsGeometry.fromPoint( layerPoint) )
    if dist < shortestDistance:
        shortestDistance = dist
        closestFeatureId = f.id()

info = (layer, closestFeatureId, shortestDistance)
layerData.append(info)

The mouse click event (a QMouseEvent) is stored in mouseEvent. Its pos() method returns a QPoint describing the position of the mouse click relative to the map canvas (x and y pixel coordinates). To calculate its distance to each feature we’ll need to first convert the mouse click position into real world (layer) coordinates. This can be done using a call to QgsMapTool.toLayerCoordinates() which automatically deals with on-the-fly projection and returns a QPoint in layer coordinates.

The features of a vector layer can be accessed using the layer’s getFeatures() method which returns (by default) a list of all QgsFeature in the layer that we can iterate over using a simple loop.

With access to features we can easily gain access to geometry using QgsFeature.geometry(). The QgsGeometry class has a number of spatial relationship methods including distance() which returns the distance to a second QgsGeometry passed as an argument.

In the code above we loop over all features, keeping track of the feature id of the closest feature using QgsFeature.id(). The shortest distance and closest feature id are stored in shortestDistance and closestFeature. When we are finished iterating through all the features in this layer, we store a note of the layer, its closest feature id and associated distance into layerData.

Note that we convert layerPoint (a QgsPoint) into a QgsGeometry so we can use it directly in spatial relationship operations such as QgsGeometry.distance().

Completing canvasReleaseEvent

We’re almost done. At this point layerData is a list of tuples, one for each vector layer containing:

  1. A reference to the layer
  2. The id of the closest feature within that layer
  3. The distance of that closest feature from the mouse click

Now we can simply sort layerData by distance (its 3rd column) and make a selection based on the layer and feature in the first row of layerData.

  • Add the following code to canvasReleaseEvent() outside the outer for loop:
if not len(layerData) > 0:
    # Looks like no vector layers were found - do nothing
    return

# Sort the layer information by shortest distance
layerData.sort( key=lambda element: element[2] )

# Select the closest feature
layerWithClosestFeature, closestFeatureId, shortestDistance = layerData[0]
layerWithClosestFeature.select( closestFeatureId )

The code above returns early if no workable vector layers were found. It sorts layerData (the list of tuples) by the 3rd element (the distance).

The code then calls QgsVectorLayer.select() to select the closest feature by its feature id.

The plugin should now be finished.

  • Reload the plugin
  • Ensure it works as expected.

Summary

Within this tutorial we’ve worked briefly with the following parts of the QGIS API:

  • Map Tools
  • Map Canvas
  • Vector Layers
  • Features
  • Geometry

Hopefully this has been a useful tutorial. Please feel free to contact us with any specific questions.

You may also like...

Mergin Maps, a field data collection app based on QGIS. Mergin Maps makes field work easy with its simple interface and cloud-based sync. Available on Android, iOS and Windows. Screenshots of the Mergin Maps mobile app for Field Data Collection
Get it on Google Play Get it on Apple store

Posted by Peter Wells