-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathqgis-actions.Rmd
740 lines (514 loc) · 34.5 KB
/
qgis-actions.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
---
title: "QGIS Automation using Actions (Workshop Material)"
subtitle: "Learn how to build and apply PyQGIS Actions to automate tasks in QGIS."
author: "Ujaval Gandhi"
fontsize: 12pt
output:
# pdf_document:
# toc: yes
# toc_depth: 3
# fig_caption: false
html_document:
df_print: paged
toc: yes
toc_depth: 3
highlight: pygments
includes:
after_body: comment.html
# word_document:
# toc: no
# fig_caption: false
header-includes:
- \usepackage{fancyhdr}
- \pagestyle{fancy}
- \renewcommand{\footrulewidth}{0.4pt}
- \fancyhead[LE,RO]{\thepage}
- \geometry{left=1in,top=0.75in,bottom=0.75in}
- \fancyfoot[CE,CO]{{\includegraphics[height=0.5cm]{images/cc-by-nc.png}} Ujaval Gandhi http://www.spatialthoughts.com}
classoption: a4paper
---
\newpage
***
```{r echo=FALSE, fig.align='center', out.width='250pt'}
knitr::include_graphics('images/spatial_thoughts_logo.png')
```
***
\newpage
# Introduction
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.
[![View Presentation](images/qgis_actions/introduction.png){width="400px"}](https://docs.google.com/presentation/d/16CiiFs3mLyQIrDXf4Uc50DFwRHsmCeMRpo1i25JNk-s/edit?usp=sharing){target="_blank"}
[View the Presentation ↗](https://docs.google.com/presentation/d/16CiiFs3mLyQIrDXf4Uc50DFwRHsmCeMRpo1i25JNk-s/edit?usp=sharing){target="_blank"}
# Software
This workshop requires QGIS LTR version 3.34.
Please review [QGIS-LTR Installation Guide](install-qgis-ltr.html) for step-by-step instructions.
# Get the Data Package
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](https://github.com/spatialthoughts/courses/releases/download/data/qgis_actions.zip).
# 1. Hello World
Let's get started by learning the basics of QGIS Actions.
## 1.1 Extract a Feature from a Layer
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.
1. Open QGIS. QGIS comes with a few hidden *Easter Eggs* that can be triggered by typing a keyword in the *Coordinate* box at the bottom of the main window. While many of these are for fun, some provide useful functionality. One such hidden feature is the ability to load a world map by typing a keyword. Enter the keyword **world** into the coordinate box at the bottom of the QGIS window rm and click *Enter*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld1.png')
```
2. A new layer named `World Map` will be added to the *Layers* panel. This layer is the [Admin0 - Countries](https://www.naturalearthdata.com/downloads/10m-cultural-vectors/) boundaries dataset from Natural Earth. Let's define an action on this layer. Right-click the layer and select *Properties*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld2.png')
```
3. Switch to the *Actions* tab and click the *Add a new action* (*+*) button.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld3.png')
```
4. Select **Python** as the *Type*. Enter **Hello World** as the *Description*. This will be the name of the action as it appears in various menu items. Leave the *Action Scopes* to the default selected values of **Feature** and **Canvas**. Under the *Action Text* enter the following Python code and click *OK*.
```{python eval=FALSE}
print('Hello World')
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/helloworld4.png')
```
5. Click *OK* again to get back to the main QGIS Canvas. We now have an action that will print `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*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld5.png')
```
6. Once the action is selected, click on any feature from the layer. You will see *Hello World* printed in the console.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld6.png')
```
7. You now know how to define and trigger a Python action. Let's make the action more useful. Instead of printing a static text, let's modify the action to print the name of the country where we clicked. Open the *Attribute Table* of the `World Map` layer. You will notice that the **NAME** attribute contains the country names.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld7.png')
```
8. Right click the `World Map` layer and select *Properties*. From the *Actions* tab, double click the already defined *Hello World* action.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld8.png')
```
9. Our action is triggered by clicking on a feature and we can access the attributes of that feature. Select the **NAME** attribute and click *Insert*.
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/helloworld9.png')
```
10. You will see the value `[%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.
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/helloworld10.png')
```
11. Update the code to print the attribute value as below.
```{python eval=FALSE}
print('[%NAME%]')
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/helloworld11.png')
```
12. Click *OK* and get back to the main window. Select the action and click on any feature. You will now see the name of the country printed in the console.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld12.png')
```
13. Along with all the attributes of the clicked feature, we also have access to other project and layer variables. Update the code as below to print the values of the feature id and layer id along with the value of the `NAME` attribute. Note that we are using the Python [f-strings](https://realpython.com/python-string-formatting/#3-string-interpolation-f-strings-python-36) for formatting the output.
```{python eval=FALSE}
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}')
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/helloworld13.png')
```
14. Click *OK* and try the action again. This time you will see the feature id and layer id printed along with the feature name.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld14.png')
```
15. Now you know how to access the `@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()`](https://qgis.org/pyqgis/3.0/core/Feature/QgsFeatureSource.html#qgis.core.QgsFeatureSource.materialize) method to create a new memory based vector layer with the query containing the feature id.
```{python eval=FALSE}
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)
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/helloworld15.png')
```
16. Go back to the main Canvas and close the *Python Console*. Select the *Hello World* action and click on any feature. You will see a new layer added to the *Layers* panel that contains the feature. Really useful!
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld16.png')
```
17. We have a small problem. As the new layer is created and added to the *Layers* panel, it becomes the active layer. To trigger the action again, we have to select the `World Map` layer again. To prevent this, we can use [`QgisInterface.setActiveLayer()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.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.
```{python eval=FALSE}
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)
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/helloworld17.png')
```
18. Now you are able to click around and extract multiple countries without having to manually switch layers.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld18.png')
```
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.
## Exercise 1
Update the action to display an *Info* message on the QGIS message bar as shown below.
Hint: You can use [iface.messageBar().pushInfo()](https://qgis.org/pyqgis/3.0/gui/Message/QgsMessageBar.html#qgis.gui.QgsMessageBar.pushInfo) method to display a message.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/helloworld_exercise.png')
```
# 2. Automate Data Editing and Selection
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.
## 2.1 Select Multiple Features
1. Open the `Parcels_Multi_Select.qgz` project from your data package.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/multiselect1.png')
```
2. Select the `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.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/multiselect2.png')
```
3. A city block is a group of parcels that is surrounded by streets. If we select a few parcels belonging to the same city block, you will see that they all have the same value for the **block_num** attribute.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/multiselect3.png')
```
4. A common task while managing such data is to select all parcels belonging to the same block. Let's build an action that allows us to select all parcels from a block with a single click. Right-click the `parcels` layer and select *Properties*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/multiselect4.png')
```
5. Switch to the *Actions* tab and click the *Add a new action* (*+*) button.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/multiselect5.png')
```
6. Select **Python** as the *Type*. Enter **Select all Parcels from the Block** 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 and click *OK*. Click *OK* again to get back to the main QGIS Canvas.
```{python eval=FALSE}
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)
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/multiselect6.png')
```
7. This action is going to print an expression that can be used to select all parcels from the same block as the parcel where you clicked. Let's test this. 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 *Select all Parcels from the Block*. Click on any parcel. You will see an expression printed in the console.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/multiselect7.png')
```
8. The expression should work to select all parcels having the same block number. Let's update the action with the code as follows.
```{python eval=FALSE}
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)
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/multiselect8.png')
```
9. Trigger the action again and click on any parcels. This time you will see that all the parcels from the block will be selected.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/multiselect9.png')
```
10. If you use the action again, the current selection will go away and a new selection will be made. We can change this behavior by supplying an addition *behavior* option to the [`QgsVectorLayer.selectByExpression()`](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer.selectByExpression) method. Update the code as below.
```{python eval=FALSE}
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)
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/multiselect10.png')
```
11. Now as you click on multiple parcels, the new block selection will be added to the previous selections.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/multiselect11.png')
```
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.
## Exercise 2
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.
```{python eval=FALSE}
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()`](https://qgis.org/pyqgis/3.0/core/Feature/QgsFeatureSource.html#qgis.core.QgsFeatureSource.materialize) method we learnt in the previous section.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/multiselect_exercise.png')
```
## 2.2 Update Field Values
1. Open the `Parcels_QA.qgz` project from your data package.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/qa2.png')
```
2. This project contains the same `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.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/qa2.png')
```
3. Right-click the `parcels` layer and select *Properties*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/qa3.png')
```
4. Switch to the *Actions* tab and click the *Add a new action* (*+*) button. Select **Python** as the *Type*. Enter **Mark QA Done** 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 and click *OK*. This code uses [`QgsVectorLayer.changeAttributeValue()`](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.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.
```{python eval=FALSE}
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')
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/qa4.png')
```
5. Click *OK* again to get back to the main QGIS Canvas. Select the action and click on any parcel with the **N** label. You will see that the value is updated and the parcel turns green instantly.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/qa5.png')
```
6. Actions can also be triggered from other interfaces. Let's add this action to the attribute table. Right-click the `parcels` layer and select *Properties*. Switch to the *Actions* tab. At the bottom, check the *Show in Attribute Table* box. Click *OK*.
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/qa6.png')
```
7. Click *OK* again to get back to the main QGIS Canvas. Open the *Attribute Table* for the `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.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/qa7.png')
```
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.
## Exercise 3
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.
```{python eval=FALSE}
feature_id = [%$id%]
layer_id = '[%@layer_id%]'
layer = QgsProject.instance().mapLayer(layer_id)
```
*Hint:* You can use [`QgsVectorLayer.deleteFeature()`](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer.deleteFeature) method to delete a feature by its feature id.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/qa_exercise.png')
```
# 3. Manage Imagery Collections
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.
## 3.1 Load and Remove Raster Layers
1. The first step in our process is to create a tile index layer from a set of raster files in your data package. Open the QGIS Processing Toolbox from **Processing → Toolbox**. Search for the algorithm named **Tile index**. Double-click to open it.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/tileindex1.png')
```
2. In the *Tile Index* dialog, click *...*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/tileindex2.png')
```
3. In the *Input files* dialog, click *Add Directory...* button and browse to the **qgis_actions → imagery** folder. Click *open*. Once the files are listed, click *OK*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/tileindex3.png')
```
4. Back in the *Tile Index* dialog, check the *Store absolute path to the indexed rasters* box. Leave all other parameters to their default value and click *Run*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/tileindex4.png')
```
5. A new layer named *Tile Index* will be loaded in the *Layers* panel. This layer has bounding box polygons for each of the image tiles in the folder along with its path. Open the *Attribute Table*. You will see that the path to the raster tiles is stored in the *location* attribute.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/tileindex5.png')
```
6. Right-click the `Tile Index` layer and select *Properties*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/tileindex6.png')
```
7. Switch to the *Actions* tab and click the *Add a new action* (*+*) button. Select **Python** as the *Type*. Enter **Load 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 and click *OK*.
```{python eval=FALSE}
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)
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/tileindex7.png')
```
8. Click *OK* again to get back to the main QGIS Canvas. Select the action *Load Selected Tile* and click on any polygon to load the raster layer in QGIS. This action allows you to selectively load only the required tiles.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/tileindex8.png')
```
9. Let's setup another action to remove the unwanted tile layers. Right-click the `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()`](https://qgis.org/pyqgis/3.0/core/Project/QgsProject.html#qgis.core.QgsProject.removeMapLayer) method to remove the layer. We must also call `iface.mapCanvas().refresh()` to see the results. Click *OK*.
```{python eval=FALSE}
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.')
```
```{r echo=FALSE, fig.align='center', out.width='60%'}
knitr::include_graphics('images/qgis_actions/tileindex9.png')
```
10. Click *OK* again to get back to the main QGIS Canvas. Select the action *Remove Selected Tile* and click on the canvas over any tile that has been loaded in QGIS. That tile will be removed.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/tileindex10.png')
```
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.
## Exercise 4
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.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/tileindex_exercise.png')
```
# Supplement
## Select Features in a Buffer Zone
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 the `getFeatures()` method which makes use of the spatial index (if it exists) to speed up finding the candidate features.
```{python eval=FALSE}
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.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/buffer_select1.png')
```
## Reversing Line Direction using Processing Algorithm
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](https://docs.qgis.org/testing/en/docs/user_manual/working_with_vector/editing_geometry_attributes.html#the-processing-in-place-layer-modifier). 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.
```{python eval=FALSE}
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.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/reverse_line.png')
```
## Creating Isochrones using ORS Tools plugin
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.
* Visit [OpenRouteService Sign-Up](https://openrouteservice.org/dev/#/signup) page and create an account. Once your account is activated, visit your [Dashboard](https://openrouteservice.org/dev/#/home) and request a token. Select Standard as the Token type and enter **QGIS** as the Token name. Click *CREATE TOKEN*.
* In QGIS, go to **Web → ORS Tools → Provider Settings** and enter the API key created in the previous step.
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.
```{python eval=FALSE}
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.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/isochrone.png')
```
## View Panorama from Mapillary
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](https://www.mapillary.com/developer/api-documentation) 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](https://www.mapillary.com/developer) and register a new application. Click on *View* under *Client Token* and obtain your access token.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/mapillary1.png')
```
Open the code for the action and replace `YOUR_ACCESS_TOKEN` with your access token.
```{python eval=FALSE}
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.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/qgis_actions/mapillary2.png')
```
# Data Credits
* Admin0 boundaries: Made with Natural Earth. Free vector and raster map data @ naturalearthdata.com.
* Parcels and Neighborhood boundaries downloaded from [DataSF Open Data Portal](https://datasf.org/opendata/)
* NAIP 2016 Aerial Imagery for California: The National Agriculture Imagery Program (NAIP). USDA-FSA-APFO Aerial Photography Field Office. Downloaded from [NRCS](https://nrcs.app.box.com/v/naip/folder/18144379349)
* OpenStreetMap (osm) data layers: Data/Maps Copyright 2023 Geofabrik GmbH and OpenStreetMap Contributors.
# License
This workshop material is licensed under a [Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/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](https://spatialthoughts.com)
***