QGIS allows you to define custom Actions on map layers. Actions can launch commands or run python code when the user clicks on a feature from the layer. This workshop will cover QGIS Actions in detail along with use cases on how you can harness its power to automate GIS workflows. We will focus on Python Actions and go through various examples of implementing new functionality and automating tasks with just a few lines of PyQGIS code.
This workshop requires prior knowledge of Python and familiarity with PyQGIS API.
This workshop requires QGIS LTR version 3.34.
Please review QGIS-LTR Installation Guide for step-by-step instructions.
The code examples in this workshop use a variety of datasets. All the
required layers, project files etc. are supplied to you in the zip file
qgis-actions.zip
. Unzip this file to the
Downloads
directory.
The data package also comes with a solutions
folder that
contain model solutions for each section.
Download qgis-actions.zip.
Let’s get started by learning the basics of QGIS Actions.
We will create an action that takes a layer of all countries in the world and allows you to extract any country polygon by clicking on it.
World Map
will be added to the
Layers panel. This layer is the Admin0
- Countries boundaries dataset from Natural Earth. Let’s define an
action on this layer. Right-click the layer and select
Properties.Hello World
when we click on
a feature. To see the output, open the Python Console from
Plugins → Python Console. Locate the Actions
button on the Attributes Toolbar. Click the dropdown menu next
to it and select Hello World.World Map
layer. You will
notice that the NAME attribute contains the country
names.World Map
layer and select
Properties. From the Actions tab, double click the
already defined Hello World action.[%NAME%]
entered in the
Action Text text box. This is a special expression syntax which
indicates that the value surrounded by [%
and
%]
will be replaced with the value of the attribute from
the feature when the action is triggered.NAME
attribute. Note that we are using the Python f-strings
for formatting the output.feature_name = '[%NAME%]'
feature_id = [%$id%]
layer_id = '[%@layer_id%]'
print(f'feature name: {feature_name}')
print(f'feature id: {feature_id}')
print(f'layer id: {layer_id}')
@layer_id
and
$id
values of the current feature. We can now use it to
extract the current feature and create a new layer from it. Replace the
Action Text with the following code which uses QgsFeatureSource.materialize()
method to create a new memory based vector layer with the query
containing the feature id.feature_name = '[%NAME%]'
feature_id = [%$id%]
layer_id = '[%@layer_id%]'
layer = QgsProject.instance().mapLayer(layer_id)
new_layer = layer.materialize(
QgsFeatureRequest().setFilterFids([feature_id]))
new_layer.setName(feature_name)
QgsProject.instance().addMapLayer(new_layer)
World Map
layer again.
To prevent this, we can use QgisInterface.setActiveLayer()
method to set the current layer as the active layer. We import
iface
in the code to access the current instance of the
QgisInterface
class.from qgis.utils import iface
feature_name = '[%NAME%]'
feature_id = [%$id%]
layer_id = '[%@layer_id%]'
layer = QgsProject.instance().mapLayer(layer_id)
new_layer = layer.materialize(
QgsFeatureRequest().setFilterFids([feature_id]))
new_layer.setName(feature_name)
QgsProject.instance().addMapLayer(new_layer)
iface.setActiveLayer(layer)
We have now finished this section and you are ready to do the
exercise. Your can load the HelloWorld_Checkpoint1.qgz
file
in the solutions
folder to catch up to this point.
Update the action to display an Info message on the QGIS message bar as shown below.
Hint: You can use iface.messageBar().pushInfo() method to display a message.
In this section, we will work with a dataset of land parcels and learn how QGIS Actions can be used to speed up data selection and editing.
Parcels_Multi_Select.qgz
project from your
data package.parcels
layer and open the Attribute
Table. The mapblklot attribute contains a unique
identifier for each parcels and the block_num attribute
has a unique identifier for each city block.parcels
layer and select Properties.layer_id = '[%@layer_id%]'
layer = QgsProject.instance().mapLayer(layer_id)
field_name = 'block_num'
field_value = '[%block_num%]'
expression = f'"{field_name}" = \'{field_value}\''
print(expression)
layer_id = '[%@layer_id%]'
layer = QgsProject.instance().mapLayer(layer_id)
field_name = 'block_num'
field_value = '[%block_num%]'
expression = f'"{field_name}" = \'{field_value}\''
layer.selectByExpression(expression)
QgsVectorLayer.selectByExpression()
method. Update the code as below.layer_id = '[%@layer_id%]'
layer = QgsProject.instance().mapLayer(layer_id)
field_name = 'block_num'
field_value = '[%block_num%]'
expression = f'"{field_name}" = \'{field_value}\''
layer.selectByExpression(expression, QgsVectorLayer.AddToSelection)
We have now finished this section and you are ready to do the
exercise. Your can load the
Parcels_Multi_Select_Checkpoint1.qgz
file in the
solutions
folder to catch up to this point.
Add another action to the parcels
layer called
Extract Selected Features. This action should create a new
memory layer from the selected parcels. The following code block shows
how to get a list of selected feature ids that can be used in your
solution.
from qgis.utils import iface
layer_id = '[%@layer_id%]'
layer = QgsProject.instance().mapLayer(layer_id)
selected_ids = [feature.id() for feature in layer.selectedFeatures()]
Hint: Use QgsFeatureSource.materialize()
method we learnt in the previous section.
Parcels_QA.qgz
project from your data
package.parcels
layer that has
been styled and labeled using the values from the
checked column. This column contains the value of
either Y or N. Let’s say we are tasked
with checking each feature and then updating the value of this field to
Y once it has been checked. Let’s setup an action to
automate this QA process.parcels
layer and select
Properties.QgsVectorLayer.changeAttributeValue()
method to update the field checked’s value to
Y. Note that we are using the
with edit(layer)
statement to wrap our editing code in a
more semantic code block. This will ensure that the changes committed at
the end or rolled back appropriately if there are any errors.from qgis.utils import iface
feature_id = [%$id%]
layer_id = '[%@layer_id%]'
field_name = 'checked'
layer = QgsProject().instance().mapLayer(layer_id)
field = layer.fields().lookupField(field_name)
with edit(layer):
layer.changeAttributeValue(feature_id, field, 'Y')
iface.messageBar().pushInfo('Success', 'Field Value Updated')
parcels
layer and select Properties. Switch to the Actions
tab. At the bottom, check the Show in Attribute Table box.
Click OK.parcels
layer. You
will notice that there is a new Actions column with the
Mark QA Done action added to the table. You can click on the
button to trigger the action for each feature.We have now finished this section and you are ready to do the
exercise. Your can load the Parcels_QA_Checkpoint1.qgz
file
in the solutions
folder to catch up to this point.
Add another action to the parcels
layer called
Delete Parcel. This action should delete the feature when
selected. Use the following code block to start working on your
solution.
Hint: You can use QgsVectorLayer.deleteFeature()
method to delete a feature by its feature id.
Actions also provide a simple and intuitive way to manage large imagery collections using QGIS. In this section, we will learn how to create a Tile Index and setup actions to interactively load and remove raster layers of interest.
Tile Index
layer and select
Properties.import os
from qgis.utils import iface
path = r'[%location%]'
iface.addRasterLayer(path)
file_name = os.path.basename(path)
iface.messageBar().pushSuccess(
'Success', f'Raster tile {file_name} loaded')
layer_id = '[%@layer_id%]'
layer = QgsProject.instance().mapLayer(layer_id)
iface.setActiveLayer(layer)
Tile Index
layer and select
Properties. Switch to the Actions tab and click the
Add a new action (+) button. Select
Python as the Type. Enter Remove
Selected Tile as the Description. Leave the Action
Scopes to the default selected values of Feature
and Canvas. Under the Action Text enter the
following Python code. Here we use the QgsProject.removeMapLayer()
method to remove the layer. We must also call
iface.mapCanvas().refresh()
to see the results. Click
OK.import os
from qgis.utils import iface
path = r'[%location%]'
file_name = os.path.basename(path)
layer_name = os.path.splitext(file_name)[0]
layer_list = QgsProject.instance().mapLayersByName(layer_name)
if layer_list:
QgsProject.instance().removeMapLayer(layer_list[0])
iface.mapCanvas().refresh()
iface.messageBar().pushSuccess(
'Success', f'Raster tile {file_name} removed.')
We have now finished this section and you are ready to do the
exercise. Your can load the Tile_Index_Checkpoint1.qgz
file
in the solutions
folder to catch up to this point.
The action Load Selected Tile allows you to load any tile multiple times. This is not desirable since it will create duplicate layers. Update the action to load a tile only if it is not previously loaded in QGIS. It should display an error message when you try to load a tile that is already loaded.
Hint: Use iface.messageBar().pushCritical()
to display
an error message.
Another useful application of action is to select features from a
layer within a buffer zone. Open the Buffer_Select.qgz
project from your data package. This project contains a
roads
and a buildings
layer. The
roads
layer has an action defined called Select
Buildings within Buffer that selects all buildings that are
within 20 meters of the selected road segment.
Note: This example uses
line_geometry.boundingBox()
in thegetFeatures()
method which makes use of the spatial index (if it exists) to speed up finding the candidate features.
layer_id = '[%@layer_id%]'
fid = [% $id %]
distance = 20
line_layer = QgsProject.instance().mapLayer(layer_id)
line_feature = line_layer.getFeature(fid)
line_geometry = line_feature.geometry().buffer(distance, 5)
polygon_layer_name = 'buildings'
polygon_layer = QgsProject.instance().mapLayersByName(polygon_layer_name)[0]
nearby_features = [
p.id()
for p in polygon_layer.getFeatures(line_geometry.boundingBox())
if p.geometry().intersects(line_geometry)
]
if nearby_features:
polygon_layer.selectByIds(nearby_features)
Select the action and click on any road feature. All the buildings within the buffer zone will be selected.
The QGIS Processing Toolbox contains many useful algorithms. You can call any algorithms using Python from Actions. This example shows how to setup an action to run a processing algorithm a line feature to reverse direction.
We will use the Vector Geometry → Reverse line
direction algorithm. By default, all Processing algorithms
create new layer. If we want to edit the layer, we must enable the Edit
Features In-Place mode. The same mode can be invoked from PyQGIS via
execute_in_place()
method.
Note: The Advanced Digitizing Toolbar already has a tool that allows you reverse the line direction for the selected feature. This example is an implementation of a similar functionality using an Action.
Open the Network_Linedirection.qgz
project. It contains
a line layer named street_centerlines
which has been styled
to show an arrow in the direction of the line. We have defined an action
Reverse Line Direction on this layer with the following
code.
from processing.gui.AlgorithmExecutor import execute_in_place
layer_id = '[%@layer_id%]'
layer = QgsProject.instance().mapLayer(layer_id)
fid = [% $id %]
layer.selectByIds([fid])
registry = QgsApplication.instance().processingRegistry()
algorithm = registry.algorithmById('native:reverselinedirection')
parameters = {
'INPUT': layer,
'selectedFeaturesOnly': True,
'featureLimit': -1,
' geometryCheck': QgsFeatureRequest.GeometryAbortOnInvalid
}
with edit(layer):
execute_in_place(algorithm, parameters)
layer.removeSelection()
Select the action and click on any line segment. You will see the line direction will get reversed. Our Python action saves many extra clicks compared to executing this via the Toolbox.
Similar to native algorithms, we can also call any third-party algorithms added from QGIS Plugins. This example shows how to use the ORS Tools → Isochrones → Isochrones from point algorithm to generate a walking-directions isochrone from a point layer.
Before using this example, you must install the ORS Tools plugin and configure it. Follow the steps below for configuration after you have installed the plugin.
Now you are ready to try out the action. Open the
Stations_Isochrones.qgz
project from your data package.
This project has a san_francisco_stations
layer with a few
actions such as Calculate Isochrone (1km) defined with
code similar to below.
import processing
from qgis.utils import iface
x_coord = [%@click_x%]
y_coord = [%@click_y%]
layer_id = '[%@layer_id%]'
# Distance in meters
distance = 1000
# Check if ORS Tools in available
providers = [x.name() for x in QgsApplication.processingRegistry().providers()]
if 'ORS Tools' not in providers:
iface.messageBar().pushCritical('Error', 'This action requires the ORS Tools plugin. Please install and configure the plugin.')
else:
input_point = f'{x_coord},{y_coord}'
result = processing.run(
'ORS Tools:isochrones_from_point',{
'INPUT_PROVIDER':0,'INPUT_PROFILE':6,'INPUT_POINT': input_point,
'INPUT_METRIC':1,'INPUT_RANGES':f'{distance}', 'INPUT_AVOID_FEATURES':[],
'INPUT_AVOID_BORDERS':None,'INPUT_AVOID_COUNTRIES':'',
'INPUT_SMOOTHING': None, 'LOCATION_TYPE': 0,
'INPUT_AVOID_POLYGONS':None,'OUTPUT':'TEMPORARY_OUTPUT'
}
)
new_layer = result['OUTPUT']
new_layer.setName(f'Isochrone Polygon ({distance}m)')
QgsProject.instance().addMapLayer(new_layer)
layer = QgsProject.instance().mapLayer(layer_id)
iface.setActiveLayer(layer)
Select the layer and use the action Calculate Isochrone (1km) and click on any point. The OpenRouteService API will generate a isochrone using the OpenStreetMap(OSM) network and you will see a polygon of all areas reachable by walk within 1 km of the station. Similarly, you can try out other actions for different distances.
QGIS actions can also be used to query an external API and display the results. The example below shows how to use the Mapillary API to fetch street-level imagery and display them in QGIS.
To try the action, open the Mapillary_Images.qgz
project
from the data package. The pharmacies
layer has an action
defined called Show Panorama from Mapillary with the
code below. Before you can use this action, you will need a Client
Token from Mapillary. Sign-up for a Mapillary
Developer Account and register a new application. Click on
View under Client Token and obtain your access
token.
Open the code for the action and replace
YOUR_ACCESS_TOKEN
with your access token.
import requests
from qgis.PyQt.QtCore import QUrl
from qgis.PyQt.QtWebKitWidgets import QWebView
from qgis.utils import iface
# Sign up at https://www.mapillary.com/developer
# Replace YOUR_ACCESS_TOKEN with your own token below
parameters = {
'access_token': 'YOUR_ACCESS_TOKEN',
'bbox': '{},{},{},{}'.format([%$x%]-0.001,[%$y%]-0.001, [%$x%]+0.001, [%$y%]+0.001),
'fields': 'thumb_1024_url',
'limit': 1
}
response = requests.get(
'https://graph.mapillary.com/images', params=parameters)
if response.status_code == 200:
data_json = response.json()
if data_json['data']:
url = data_json['data'][0]['thumb_1024_url']
myWV = QWebView(None)
myWV.load(QUrl(url))
myWV.show()
else:
qgis.utils.iface.messageBar().pushMessage('No images found')
Once the token is updated, you can select the action and click on any pharmacy location. The action will send a request to mapillary API to fetch the nearest imagery. It will be then displayed in a new window in QGIS.
This workshop material is licensed under a Creative Commons Attribution 4.0 International (CC BY 4.0). You are free to re-use and adapt the material but are required to give appropriate credit to the original author as below:
QGIS Automation using Actions by Ujaval Gandhi www.spatialthoughts.com
If you want to report any issues with this page, please comment below.