-
Notifications
You must be signed in to change notification settings - Fork 19
/
pyqgis-masterclass.Rmd
1919 lines (1278 loc) · 91.4 KB
/
pyqgis-masterclass.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
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: "PyQGIS Masterclass - Customizing QGIS with Python (Full Course)"
subtitle: "Learn the PyQGIS API from the Ground Up."
author: "Ujaval Gandhi"
fontsize: 12pt
output:
html_document:
df_print: paged
toc: yes
toc_depth: 3
highlight: pygments
includes:
after_body: comment.html
# word_document:
# toc: false
# fig_caption: false
# pdf_document:
# latex_engine: xelatex
# toc: yes
# toc_depth: 3
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='75%', out.width='250pt'}
knitr::include_graphics('images/spatial_thoughts_logo.png')
```
***
\newpage
# Introduction
This class introduces the concepts of Python programming within the QGIS environment. We will cover the full breadth of topics that involve everything from using the Python Console to building a fully functional plugin. We will also explore GUI programming techniques for customizing the QGIS interface using Qt widgets.
This course requires basic knowledge of Python. If you are not familiar with Python, it is strongly recommended you complete our [Python Foundation for Spatial Analysis](https://spatialthoughts.com/training/python-foundation-for-spatial-analysis/) course. We will build upon the exercises covered in that course.
[![Watch the video](https://img.youtube.com/vi/-nJ_8Ph_yPE/mqdefault.jpg)](https://www.youtube.com/watch?v=-nJ_8Ph_yPE&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=-nJ_8Ph_yPE&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE){target="_blank"}
[Access the Presentation ↗](https://docs.google.com/presentation/d/1GT0c6jG3WmOZgsLuiIniEE9T1H8NJ5G6oemWX43VIUA/edit?usp=sharing){target="_blank"}
# Get the Data Package
The code examples in this class use a variety of datasets. All the required layers, project files, icons etc. are supplied to you in the ``pyqgis_masterclass.zip`` file. Unzip this file to the `Downloads` directory. All scripts assume the data is available in the ``<home folder>/Downloads/pyqgis_masterclass/`` directory.
Download [pyqgis_masterclass.zip](https://github.com/spatialthoughts/courses/releases/download/data/pyqgis_masterclass.zip).
> Note: Certification and Support are only available for participants in our paid instructor-led classes.
# Get the Course Videos
The course is accompanied by a set of videos covering the all the modules. These videos are recorded from our live instructor-led classes and are edited to make them easier to consume for self-study. We have 2 versions of the videos:
### YouTube
We have created a YouTube Playlist with separate videos for each notebook and exercise to enable effective online-learning. [Access the YouTube Playlist ↗](https://www.youtube.com/playlist?list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE){target="_blank"}
### Vimeo
We are also making combined full-length video for each module available on Vimeo. These videos can be downloaded for offline learning. [Access the Vimeo Playlist ↗](https://vimeo.com/showcase/11320340?share=copy){target="_blank"}
# Installation and Setting up the Environment
## Install QGIS
This course requires QGIS LTR version 3.34. Please review [QGIS-LTR Installation Guide](install-qgis-ltr.html) for step-by-step instructions.
## Get a Text Editor
Any kind of software development requires a good text editor. If you already have a favorite text editor or an IDE (Integrated Development Environment), you may use it for this course. Otherwise, each platform offers a wide variety of free or paid options for text editors. Choose the one that fits your needs.
Below are my recommendations editors that are simple to use for beginners.
- Windows: [Notepad++](https://notepad-plus-plus.org/downloads/) is a good free editor for windows. Download and install the Notepad++ editor. Tip: Before writing Python code in Notepad+++, make sure to go to Settings → Preferences → Language and enable `Replace by space`. Python is very sensitive about whitespace and this setting will ensure tabs and spaces are treated properly.
- Mac: [TextMate](https://macromates.com/) is an open-source editor for Mac that is currently available for free.
# 1. Hello World!
[![Watch the Video](https://img.youtube.com/vi/p60q4uiCqI0/mqdefault.jpg)](https://www.youtube.com/watch?v=p60q4uiCqI0&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=3){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=p60q4uiCqI0&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=3){target="_blank"}
QGIS Comes with a built-in **Python Console** and a code editor where you can write and run Python code.
Go to **Plugins → Python Console** to open the console.
```{r echo=FALSE, fig.align='center', out.width='75%', out.width='75%'}
knitr::include_graphics('images/pyqgis/console.png')
```
At the `>>>` prompt, type in the following command and press Enter.
```{python eval=FALSE}
print('Hello World!')
```
Here you are running Python's *print()* function with the text 'Hello World'. The output of the statement will be printed below.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/helloworld.png')
```
While console is useful for typing 1-2 lines of code or printing information contained in a variable, you should use the built-in editor for typing longer scripts or code snippets. Click the *Show Editor* button to open the editor panel. Enter the code and click the *Run Script* button to execute it. The results will appear in the console as before. If you are working on a longer script, you can also click the *Save* button in the editor to save the script for future use.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/editor.png')
```
All code snippets below should be run from the Editor.
# 2. Hello PyQGIS!
[![Watch the Video](https://img.youtube.com/vi/7xNNpGoYK9Q/mqdefault.jpg)](https://www.youtube.com/watch?v=7xNNpGoYK9Q&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=4){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=7xNNpGoYK9Q&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=4){target="_blank"}
QGIS provides a Python API (Application Programming Interface), commonly known as PyQGIS. The API is vast and very capable. Almost every operation that you can do using QGIS - can be done using the API. This allows developers to write code to build new tools, customize the interface and automate workflows.
Let's try out the API to perform some GIS data management tasks.
Browse to the data directory and load the `shoreline.shp` layer. Open the *Attribute Table*. This layer has 6 attribute columns. Let's say we want to delete the 2nd column (`SDE_SFGIS_`) from the layer.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/hellopyqgis1.png')
```
This can be done using the QGIS GUI as follows
1. Right-click the `shoreline` layer and click *Open Attribute Table*.
2. In the Attribute Table, click the *Toggle Editing mode* button.
2. Click the *Delete field* button. Select the `SDE_SFGIS_` column and click *OK*.
4. Click the *Save edits* button and click the *Toggle Editing mode* to stop editing.
QGIS Provides an API to accomplish all of this using Python code. We will now do this task - but using only Python code. Open the *Editor* and enter the following code. Click the *Run Script* button to execute it.
> Make sure you have selected the `shoreline` layer in the *Layers* panel before running the code.
```{python eval=FALSE}
layer = iface.activeLayer()
layer.startEditing()
layer.deleteAttribute(1)
layer.commitChanges()
```
You will see that the 2nd column is now deleted from the attribute table.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/hellopyqgis2.png')
```
Let's understand the code step-by-step
1. `layer = iface.activeLayer()`: This line uses the `iface` object and runs the `activeLayer()` method which returns the currently selected layer in QGIS. We will learn more about `iface` in the [QGIS Interface API](#qgis-interface-api-qgisinterface) section. The method returns the reference to the layer which is saved in the `layer` variable.
2. `layer.startEditing()`: This is equivalent to putting the layer in the editing mode.
3. `layer.deleteAttribute(1)`: The `deleteAttribute()` is a method from `QgsVectorLayer` class. It takes the index of the attribute to be deleted. Here we pass on index `1` for the second attribute. (index 0 is the first attribute)
4. `layer.commitChanges()`: This method saves the edit buffer and also disables the editing mode.
This gives you a preview of the power of the API. To harness the full power of the PyQGIS API, we must first understand how classes work.
Note that you can also trigger the Start Editing and Stop Editing actions using the [`QgsVectorLayerTools`](https://qgis.org/pyqgis/3.34/core/QgsVectorLayerTools.html) class. Below is an alternative code that presents the confirmation dialog to the user before committing changes.
```{python eval=FALSE}
layer = iface.activeLayer()
iface.vectorLayerTools().startEditing(layer)
layer.deleteAttribute(1)
iface.vectorLayerTools().stopEditing(layer)
```
# 3. Understanding Classes
Before we dive it to PyQGIS, it is important to understand certain concepts related to C++ and Python Classes. Qt as well as QGIS is written in C++ language. Functionality of each Qt/QGIS Widget is implemented as a class - having certain properties and functions. When we use PyQt or PyQGIS classes, it is executing the code in the C++ classes via the python bindings.
[![Watch the Video](https://img.youtube.com/vi/sUBnrV9McXk/mqdefault.jpg)](https://www.youtube.com/watch?v=sUBnrV9McXk&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=5){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=sUBnrV9McXk&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=5){target="_blank"}
[View the Presentation ↗](https://docs.google.com/presentation/d/1b4i1nEDR_uKJVu0IIqjoqrxeFD2a-tnj88JRXQW0JiA/edit?usp=sharing){target="_blank"}
Here's the code to create a class called `Car` demonstrating the example we covered in the [Classes and Objects](https://docs.google.com/presentation/d/1b4i1nEDR_uKJVu0IIqjoqrxeFD2a-tnj88JRXQW0JiA/edit?usp=sharing) presentation. It creates a class, initializes it to create new instances and demonstrates the concept of inheritance. A new class is defined using the word `class`. All classes have a function called` __init__()`, which is always executed when a new object is being created. There is also the keyword `self` which refers to the current instance of the class. The code uses the `super` keyword to refer to the parent class.
```{python eval=FALSE, code=readLines('code/pyqgis/car.py')}
```
# 4. Using PyQGIS Classes
Let's start working with PyQGIS classes now. QGIS has classes for the whole range of operations - from building the user interface to doing geoprocessing. We will see how to access these classes via the PyQGIS API.
## 4.1 Calculating distance using PyQGIS
A basic but important operation in a GIS is the calculation of distance and areas. We will see how you can use PyQGIS APIs to compute distances.
We will compute distance between the following 2 coordinates
```{python eval=FALSE}
san_francisco = (37.7749, -122.4194)
new_york = (40.661, -73.944)
```
QGIS provides the class `QgsDistanceArea` that has methods to compute distances and areas.
To use this class, we must create an object by instantiating it. Looking at the [class documentation](https://qgis.org/pyqgis/3.34/core/QgsDistanceArea.html), the class constructor doesn't take any arguments. We can use the default constructor to create an object and assign it to the `d` variable.
```{python eval=FALSE}
d = QgsDistanceArea()
```
The documentation mentions that if a valid ellipsoid has been set for the QgsDistanceArea, all calculations will be performed using ellipsoidal algorithms (e.g. using Vincenty's formulas). As we want to compute the distance between a pair of latitude/longitudes, we need to use the ellipsoidal algorithms. Let's set the ellipsoid `WGS84` for our calculation. We can use the method `setEllipsoid()`. Remember, methods should be applied on objects, and not classes directly.
```{python eval=FALSE}
d.setEllipsoid('WGS84')
```
> Tip: All valid ellipsoids can be found by calling `QgsEllipsoidUtils.acronyms()`
Now our object `d` is capable of performing ellipsoidal distance computations. Browsing through the available methods in the `QgsDistanceArea` class, we can see a `measureLine()` method. This method takes a list [QgsPointXY](https://qgis.org/pyqgis/3.34/core/QgsPointXY.html) objects. We can create these objects from our coordinate pairs and pass them on to the `measureLine()` method to get the distance. The output will be in meters. We divide it by 1000 to convert it to kilometers.
```{python eval=FALSE}
lat1, lon1 = san_francisco
lat2, lon2 = new_york
# Remember the order is X,Y
point1 = QgsPointXY(lon1, lat1)
point2 = QgsPointXY(lon2, lat2)
distance = d.measureLine([point1, point2])
print(distance/1000)
```
Putting it all together, below is the complete code to calculate the distance between a pair of coordinates using PyQGIS. When you run the code in the Python Console of QGIS, all the PyQGIS classes are already imported. If you are running this code from a script or a plugin, you must explicitly import the `QgsDistanceArea` class.
```{python eval=FALSE, code=readLines('code/pyqgis/distance.py')}
```
## Exercise 1
Let's say you want to make a stop at Las Vegas on the way from San Francisco to New York.
Calculate the total ellipsoidal distance considering a stop at Las Vegas.
```{python eval=FALSE}
san_francisco = (37.7749, -122.4194)
new_york = (40.661, -73.944)
las_vegas = (36.1699, -115.1398)
```
If your code is correct, you should see the output distance to be `4271.02` kilometers.
## 4.2 Distance Conversion
The distance returned by the `measureLine()` method in the previous section was in meters, and we divided it by 1000 to convert it to kilometers. Rather than doing this conversion manually, we can use the PyQGIS API. The `QgsDistanceArea` class has a method `convertLengthMeasurement()` that can convert the measured distance to any supported unit. The `convertLengthMeasurement()` method takes 2 arguments - the length measured by `measureLine()` method and the unit to convert the measurement to. The unit should be a value of the type `Qgis.DistanceUnit`. The permitted values are defined in the [Qgis.DistanceUnit documentation](https://qgis.org/pyqgis/3.34/core/Qgis.html#qgis.core.Qgis.DistanceUnit). The code below shows how to convert the measured distance to Kilometers and Miles.
> Prior to QGIS 3.34, the unit types were specified as `QgsUnitTypes.DistanceKilometers`.
```{python eval=FALSE, code=readLines('code/pyqgis/distance_conversion.py')}
```
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/distance.png')
```
# 5. Graphical User Interface (GUI) Programming Basics
[![Watch the Video](https://img.youtube.com/vi/g3Vwqg7WAHc/mqdefault.jpg)](https://www.youtube.com/watch?v=g3Vwqg7WAHc&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=9){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=g3Vwqg7WAHc&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=9){target="_blank"}
[View the Presentation ↗](https://docs.google.com/presentation/d/1eK75jjfKx1IznIPrfcEljokLLQwkbNJ2DoStXDkF_f4/edit?usp=sharing){target="_blank"}
## 5.1 Qt and PyQt
[Qt](https://www.qt.io/) is a free and open-source widget toolkit for creating graphical user interfaces as well as cross-platform applications. QGIS is built using the Qt platform. Both Qt and QGIS itself have well-documented APIs that should be used when writing Python code to be run within QGIS.
[PyQt](https://wiki.python.org/moin/PyQt) is the Python interface to Qt. PyQt provides classes and functions to interact with Qt widgets.
## 5.2 Building a Dialog Box
Let's learn how to use PyQt classes to create and interact with GUI elements. Here we will create a simple dialog box that prompts a user for confirmation. You can type the code in the **Editor** and click **Run Script**.
```{python eval=FALSE}
mb = QMessageBox()
```
The `QMessageBox` is a PyQt class for creating a dialog with buttons. To use the class, you create an object by *instantiating* the class. Here `mb` is an object, which is an instance of the `QMessageBox` class, created using the default parameters.
`type()` tells you what is the class of the object
```{python eval=FALSE}
type(mb)
```
`dir` returns list of the attributes and methods of any object
```{python eval=FALSE}
dir(mb)
```
Classes have methods that provide functionality. You can run the class methods on instance objects. For the `QMessageBox` class, `setText()` method will add a text to the dialog.
```{python eval=FALSE}
mb = QMessageBox()
mb.setText('Click OK to confirm')
```
Classes also have class attributes which are shared across all instances.
The `QMessageBox` class has `Ok` and `Cancel` attributes, which can be referred using `QMessageBox.Ok` and `QMessageBox.Cancel`.
```{python eval=FALSE}
mb = QMessageBox()
mb.setText('Click OK to confirm')
mb.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
```
To see the dialog, we need to use the `exec()` method. The user input is then captured and saved in the `return_value` variable.
The complete code snippet is as follows.Try it out and see the result of your action reflect in the Python Console.
```{python eval=FALSE}
mb = QMessageBox()
mb.setText('Click OK to confirm')
mb.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
return_value = mb.exec()
if return_value == QMessageBox.Ok:
print('You pressed OK')
elif return_value == QMessageBox.Cancel:
print('You pressed Cancel')
```
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/messagebox.png')
```
# 6. Deep Dive into PyQGIS
[PyQGIS](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/) is the Python interface to QGIS. It is created using SIP and integrates with PyQt.
> **_Fun Fact:_** Most QGIS class names start with the prefix *Qgs*. **Q** is for Qt and **gs** stands for Gary Sherman - the founder of the QGIS project.
QGIS C++ API documentation is available at https://qgis.org/api/3.34/
QGIS Python API documentation is available at https://qgis.org/pyqgis/3.34/
Both C++ and Python APIs are identical for most part, but certain functions are not available in the Python API. [^1]
[^1]: See https://qgis.org/api/3.4/classQgsProject.html
## 6.1 QGIS Interface API (QgisInterface)
You are ready to dive into the PyQGIS API now. In this section, we will focus on the `QgisInterface` class - which provides methods for interaction with the QGIS environment. When QGIS is running, a variable called `iface` is set up to provide an object of the class `QgisInterface` to interact with the running QGIS environment. This interface allows access to the map canvas, menus, toolbars and other parts of the QGIS application. Python Console and Plugins can use `iface` to access various parts of the QGIS interface.
[![Watch the Video](https://img.youtube.com/vi/rgOu6Qq_6Jg/mqdefault.jpg)](https://www.youtube.com/watch?v=rgOu6Qq_6Jg&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=10){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=rgOu6Qq_6Jg&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=10){target="_blank"}
### 6.1.1 Change Title of QGIS Main Window
```{python eval=FALSE}
title = iface.mainWindow().windowTitle()
new_title = title.replace('QGIS', 'My QGIS')
iface.mainWindow().setWindowTitle(new_title)
```
### 6.1.2 Change Icon of QGIS Main Window
> `os.path.expanduser('~')` returns the path to the home directory of the user.
```{python eval=FALSE}
import os
icon_image = 'qgis-black.png'
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
icon_path = os.path.join(data_dir, icon_image)
icon = QIcon(icon_path)
iface.mainWindow().setWindowIcon(icon)
```
```{r echo=FALSE, fig.align='center', out.width='50%'}
knitr::include_graphics('images/pyqgis/mainwindow2.png')
```
### 6.1.3 Remove Raster and Vector Menus
```{python eval=FALSE}
vector_menu = iface.vectorMenu()
raster_menu = iface.rasterMenu()
menubar = vector_menu.parentWidget()
menubar.removeAction(vector_menu.menuAction())
menubar.removeAction(raster_menu.menuAction())
```
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/menu2.1.png')
```
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/menu2.2.png')
```
### 6.1.4 Understanding Signals and Slots
GUI programming requires responding to user’s actions. All objects in Qt have a mechanism where they can emit a signal when there is a change in status. i.e. when a user *clicks* a button, or a window is *closed*. As a programmer, you can connect the signal to a slot (i.e. a python function) which will be called when the signal is emitted. The general syntax for connecting the signal to a slot is `<object>.<signal>.connect(function)`.
### 6.1.5 Add A New Menu Item
A new button or menu item is created using `QAction()`. Here we create an action and then connect the *click* signal to a method that opens a website.
```{python eval=FALSE}
import webbrowser
def open_website():
webbrowser.open('https://gis.stackexchange.com')
website_action = QAction('Go to gis.stackexchange')
website_action.triggered.connect(open_website)
iface.helpMenu().addSeparator()
iface.helpMenu().addAction(website_action)
```
```{r echo=FALSE, fig.align='center', out.width='50%'}
knitr::include_graphics('images/pyqgis/menu3.png')
```
### 6.1.6 Change Visibility of a Toolbar
```{python eval=FALSE}
iface.pluginToolBar().setVisible(True)
```
### 6.1.7 Add a button to a toolbar
```{python eval=FALSE}
import os
from datetime import datetime
icon = 'question.svg'
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
icon_path = os.path.join(data_dir, icon)
def show_time():
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
iface.messageBar().pushInfo('Current Time', current_time)
action = QAction('Show Time')
action.triggered.connect(show_time)
action.setIcon(QIcon(icon_path))
iface.addToolBarIcon(action)
```
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/toolbar2.png')
```
### Exercise 2
In the previous example, the alert is displayed in the QGIS message bar as an *Info* message. Change the type of the message to a *Warning* message.
Hint: Look at the appropriate method in the [QgsMessageBar](https://qgis.org/pyqgis/3.34/gui/QgsMessageBar.html) class
### 6.1.8 Add New Layers
Data sources are identified by an URI (Uniform Resource Identifier)
- For files on computer the URI is the file path
- For databases, the URI is constructed using the `QgsDataSourceUri` class and encodes,the database path, table, username, password etc.
- For web layers, such as WMF/WFS etc, the URI is the web URL
```{python eval=FALSE}
import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
filename = 'seismic_zones.shp'
uri = os.path.join(data_dir, filename)
iface.addVectorLayer(uri, 'seismic_zones', 'ogr')
filename = 'sf.gpkg|layername=zoning'
uri = os.path.join(data_dir, filename)
iface.addVectorLayer(uri, 'zoning', 'ogr')
```
```{r echo=FALSE, fig.align='center', out.width='50%'}
knitr::include_graphics('images/pyqgis/vectorlayer.png')
```
### 6.1.9 Change name of a Layer
```{python eval=FALSE}
layer = iface.activeLayer()
name = layer.name()
layer.setName('sf_' + name)
```
### Exercise 3
Write a code snippet that checks the active layer selected by the user. If the active layer is named `zoning`, display a success message in the message bar, else display an error message.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/success.png')
```
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/error.png')
```
## 6.2 QGIS Project API (QgsProject)
Another very important QGIS class is `QgsProject`. This class is used for all operations in a QGIS project - including adding/removing map layers, styling, print layouts etc. The `QgsProject` is a **Singleton Class** - meaning it can have only 1 instance at a time. The instance refers to the current QGIS project that is loaded. When QGIS starts, a blank project is created. When you load another project, the existing project is closed and a new project instance is created. You can get the current instance of the QgsProject class by calling the
**`QgsProject.instance()`** method.
[![Watch the Video](https://img.youtube.com/vi/32VVt60TO_A/mqdefault.jpg)](https://www.youtube.com/watch?v=32VVt60TO_A&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=14){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=32VVt60TO_A&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=14){target="_blank"}
### 6.2.1 Load a project
```{python eval=FALSE}
import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
project = QgsProject.instance()
project_name = 'sf.qgz'
project_path = os.path.join(data_dir, project_name)
project.read(project_path)
```
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/project.png')
```
### 6.2.2 Load Projects using a Dropdown Menu
The code snippet below shows how to create a new toolbar with a label and a drop-down menu. Selecting an item will load the project with that name.
```{r echo=FALSE, fig.align='center'}
knitr::include_graphics('images/pyqgis/project_selector.png')
```
```{python eval=FALSE, code=readLines('code/pyqgis/project_toolbar.py')}
```
### 6.2.3 Create a New Vector Layer
We will now create a temporary memory layer using PyQGIS. Memory layers are not saved to the disk and ideal to store intermediate results. The code snippet below creates a polygon layer with the extent of the current map canvas.
```{python eval=FALSE}
mc = iface.mapCanvas()
extent = mc.extent()
vlayer = QgsVectorLayer('Polygon', 'extent', 'memory')
crs = QgsProject.instance().crs()
vlayer.setCrs(crs)
provider = vlayer.dataProvider()
f = QgsFeature()
geometry = QgsGeometry.fromRect(extent)
f.setGeometry(geometry)
provider.addFeature(f)
vlayer.updateExtents()
QgsProject.instance().addMapLayer(vlayer)
```
### Exercise 4
The following code snippet creates a toolbar called *CRS Toolbar* with a label, textbox and a button. When the button is clicked, the function `changeCRS` is called. Implement this function so that it changes the CRS of the current project to the EPSG code entered by the user.
Hint: Use [`QgsProject.instance().setCrs()`](https://qgis.org/pyqgis/3.34/core/QgsProject.html) method.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/crs_selector.png')
```
```{python eval=FALSE}
crsToolbar = iface.addToolBar('CRS Toolbar')
label = QLabel('Enter an EPSG Code', parent=crsToolbar)
crsTextBox = QLineEdit('4326', parent=crsToolbar)
crsTextBox.setFixedWidth(80)
button = QPushButton('Go!', parent=crsToolbar)
crsToolbar.addWidget(label)
crsToolbar.addWidget(crsTextBox)
crsToolbar.addWidget(button)
def changeCrs(crsText):
epsgCode = int(crsTextBox.text())
iface.messageBar().pushInfo('Function called', f'You entered {epsgCode}')
# Add code to change the project CRS to the EPSG code
button.clicked.connect(changeCrs)
```
# 7. Running Python Code at QGIS Launch
[![Watch the Video](https://img.youtube.com/vi/ZQNTKS2y4cE/mqdefault.jpg)](https://www.youtube.com/watch?v=ZQNTKS2y4cE&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=16){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=ZQNTKS2y4cE&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=16){target="_blank"}
It is possible to execute some PyQGIS code every time QGIS starts. QGIS looks for a file named `startup.py` in the user's Python home directory, and if it is found, executes it. This file is very useful in customizing QGIS interface with techniques learnt in the previous section.
If you are running multiple versions of QGIS, a very useful customization is to display the QGIS version number and name in the main window. The version name is stored in a global QGIS variable called `qgis_version`. We can read that variable and set the main window's title with it. We connect this code to the signal `iface.initializationCompleted` signal when the main window is loaded.
Create a new file named `startup.py` with the following code. Note the imports at the top - including `iface`. When we ran the code snippets in the Python Console, we did not have to import any modules since they are done automatically when the console starts. For pyqgis scripts elsewhere, we have to explicitly import the modules (classes) that we want to use.
```{python eval=FALSE, code=readLines('code/pyqgis/startup.py')}
```
This file needs to be copied to the appropriate directory on your system. See [QGIS documentation](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/intro.html#running-python-code-when-qgis-starts) for details on the path for your platform.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/startup.png')
```
Once you copy the file at that location, restart QGIS. The title bar should now have the QGIS version name in it.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/customtitle.png')
```
> Pro Tip: It is possible to put the `startup.py` file on a shared drive for enterprise deployment of QGIS customizations. [Learn more](https://gis.stackexchange.com/questions/319224/central-deployment-of-startup-py-in-qgis/319225).
## Exercise 5
Trying opening a new project in QGIS after you have restarted GIS with `startup.py` file in place. You will notice that the custom title with the version name is replaced with the default title.
Make a change to your `startup.py` so that the customization is applied even when a new project is loaded.
# 8. Running Processing Algorithms
While the PyQGIS API providers many functions to work with layers, features, attributes and geometry - it is a much better practice to use the built-in processing algorithms to alter the layers or do any analysis. This will give you better performance and result in much lesser code. Here are some examples on how to use processing algorithms from Python to do vector and raster layer editing. You will find more information about various options and techniques in the article [Using processing algorithms from the console](https://docs.qgis.org/testing/en/docs/user_manual/processing/console.html) of the QGIS User Guide
[![Watch the Video](https://img.youtube.com/vi/G4RfIsVAq4k/mqdefault.jpg)](https://www.youtube.com/watch?v=G4RfIsVAq4k&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=18){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=G4RfIsVAq4k&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=18){target="_blank"}
## 8.1 Creating Hillshade from a DEM
To use any Processing Algorithm via Python, you need to know how to specify all the required parameters. This is easiest to obtain by running the algorithm via the GUI first.
In this section we will learn how to create a hillshade raster from a DEM. We will first carry out the task using QGIS.
1. Browse to the data directory and load the `srtm.tif` layer. Search and locate the **Processing Toolbox → Raster terrain analysis → Hillshade** algorithm from the *Processing Toolbox*. Double-click to open it.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/processing1.png')
```
2. Select `srtm` as the *Elevation layer* and keep all the other parameters to their default value. Click *Run*.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/processing2.png')
```
3. A new `Hillshade` layer will be added to the *Layers* panel. Now we will locate the Python command for this operation from the *Processing History*. Go to **Processing → History**.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/processing3.png')
```
4. The first entry in the top panel will show the last algorithm that was ran from the toolbox. Click on it to select it. The full Python command will be shown at the bottom. Copy it.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/processing4.png')
```
You can now use the parameters in your Python code and replace the path of the input. Below is the code snippet that runs the same algorithm from a Python script. Note that we are using the `processing.runAndLoadResults()` method that adds the resulting layer to the canvas.
```{python eval=FALSE}
import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
filename = 'srtm.tif'
srtm = os.path.join(data_dir, filename)
iface.addRasterLayer(srtm, 'srtm', 'gdal')
results = processing.runAndLoadResults("native:hillshade",
{'INPUT': srtm,
'Z_FACTOR':2,
'AZIMUTH':300,
'V_ANGLE':40,
'OUTPUT': 'TEMPORARY_OUTPUT'})
```
## 8.2 Running Multiple Processing Algorithms
We can also *chain* multiple processing tools to build a script to build a data processing pipeline. In the example below, we will do 2 steps
1. Clip `srtm.tif` raster using the `shoreline.shp` layer.
2. Calculate the hillshade on the clipped raster and load it in QGIS.
Note that we are using the `processing.run()` method for the first step. This method calculates the output, but does not load the result to QGIS. This allows us to carry out multiple processing steps and not load intermediate layers.
```{python eval=FALSE}
import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
filename = 'srtm.tif'
srtm = os.path.join(data_dir, filename)
filename = 'shoreline.shp'
shoreline = os.path.join(data_dir, filename)
results = processing.run("gdal:cliprasterbymasklayer",
{'INPUT':srtm,
'MASK': shoreline,
'OUTPUT':'TEMPORARY_OUTPUT'})
clipped_dem = results['OUTPUT']
results = processing.runAndLoadResults("native:hillshade",
{'INPUT': clipped_dem,
'Z_FACTOR':2,
'AZIMUTH':300,
'V_ANGLE':40,
'OUTPUT': 'TEMPORARY_OUTPUT'})
```
You can also do batch-processing by iterating through multiple layers and running the processing algorithm in a for-loop. Doing it via Python allows you greater flexibility - such as combining the results into a single layer. See [Running Processing Algorithms via Python](https://www.qgistutorials.com/en/docs/3/processing_algorithms_pyqgis.html){target="_blank"} tutorial for a complete example.
## Exercise 6
Your data package contains a polygon layer of seismic zones in San Francisco. We want to calculate the average elevation within each seismic zone from this layer. The code below reads the vector layer `seismic_zones.shp` of seismic zones and the raster layer `srtm.tif` containing elevation values. The vector layer contains some invalid polygons, so we run the `native:fixgeometries` algorithm to fix them.
Use the resulting layer with the fixes geometries and calculate Zonal Statistics algorithm `native:zonalstatisticsfb` to calculate the average elevation from the raster layer.
```{python eval=FALSE}
import os
data_dir = os.path.join(os.path.expanduser('~'), 'Downloads', 'pyqgis_masterclass')
vector_layer = 'seismic_zones.shp'
vector_layer_path = os.path.join(data_dir, vector_layer)
raster_layer = 'srtm.tif'
raster_layer_path = os.path.join(data_dir, raster_layer)
# Input vector has invalid geometries
# Fix them first
results = processing.run("native:fixgeometries", {
'INPUT':vector_layer_path,
'METHOD': 0,
'OUTPUT':'TEMPORARY_OUTPUT'})
fixed_vector_layer = results['OUTPUT']
# Run Zonal Statistics and load the resulting layer
```
# Assignment
[![Watch the Video](https://img.youtube.com/vi/McwUHDxOTik/mqdefault.jpg)](https://www.youtube.com/watch?v=McwUHDxOTik&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=21){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=McwUHDxOTik&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=21){target="_blank"}
The following assignment is designed to help you practice the skills learnt so far in the course and explore the PyQGIS API.
Your task is to write a PyQGIS Script to show the average value of the selected raster layer within the current map extent. For example, if you load the `srtm` layer from the data package, select it and click the button - it should calculate and display the average elevation within the canvas extent. If you zoom/pan the map and click the button again - it should compute the display the average elevation within the new extent. Here is the recommended structure for your script.
* Add a button to the *Plugins Toolbar* called *Show Raster Statistics* with an icon of your choice.
* Write a function named `show_statistics()` that is called when the button is clicked.
* The function should obtain the current extent of the map canvas and calculate the raster statistics.
* Once the statistics are computed, it should display a message in the message bar with the information.
Hint: [`QgsRasterInterface`](https://qgis.org/pyqgis/3.34/core/QgsRasterInterface.html#module-QgsRasterInterface) class provides a `bandStatistics()` method for calculating statistics from a raster band. You can get the reference to the instance of this class for a raster layer using `layer.dataProvider()`.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/assignment1.png')
```
Extra credit if your script also does error checking and displays an error message on the message bar for the following conditions:
* If the user has not selected any layer or the selected layer is not a raster layer, display an error.
* If the current map canvas extent does not have any valid pixels, display an error.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/assignment1_errors.png')
```
# 9. Writing Plugins
Plugins are a great way to extend the functionality of QGIS. You can write plugins using Python that can range from adding a simple button to sophisticated tool-kits.
[![Watch the Video](https://img.youtube.com/vi/gzwhSOfknck/mqdefault.jpg)](https://www.youtube.com/watch?v=gzwhSOfknck&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=22){target="_blank"}
[Watch the Video ↗](https://www.youtube.com/watch?v=gzwhSOfknck&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=22){target="_blank"}
[View the Presentation ↗](https://docs.google.com/presentation/d/1wegCN-_aQ6G4-VyyN-lO4o0n7Kj9kU-xT2jSz1wL5Ec/edit?usp=sharing){target="_blank"}
> There is a plugin named [Plugin Builder](https://plugins.qgis.org/plugins/pluginbuilder/) that can help you generate a starter plugin. We have published step-by-step instructions for both [GUI plugins](https://www.qgistutorials.com/en/docs/3/building_a_python_plugin.html) and [Processing Plugins](https://www.qgistutorials.com/en/docs/3/processing_python_plugin.html) using the Plugin Builder method. While this method gives you an easy way to have a functional plugin, it is not the ideal way to learn plugin development. We recommend starting from a minimal template and adding elements as and when needed. Here we will learn the basics of plugin framework using a minimal plugin and learn how to add various element to make it a full plugin.
## 9.1 Understanding Plugins
Plugins are much more integrated into the QGIS system than Python Scripts. They are managed by **Plugin Manager** and are initialized when QGIS starts. To understand the required structure, let's see what a minimal plugin looks like. You can learn more about this structure at [QGIS Minimalist Plugin Skeleton](https://github.com/wonder-sk/qgis-minimal-plugin).
We will now build a simple plugin named **Basemap Loader** that adds a button in the *Plugin Toolbar* that loads a basemap from OpenStreetMap to the current project.
The first requirement for plugins is a file called `metadata.txt`. This file contains general info, version, name and some other metadata used by plugins website and plugin manager.
`metadata.txt`
```{python eval=FALSE, code=readLines('code/pyqgis/basemap_loader_minimal/metadata.txt')}
```
Second is the file that contains the main logic of the plugin. It must have `__init__()` method that gives the plugin access to the QGIS Interface (iface). The `initGui()` method is called when the plugin is loaded and `unload()` method which is called when the plugin is unloaded. For now, we are creating a minimal plugin that just add a button and a menu entry that displays message when clicked.
`load_basemap.py`
```{python eval=FALSE, code=readLines('code/pyqgis/basemap_loader_minimal/load_basemap.py')}
```
Third file is called `__init__.py` which is the starting point of the plugin. It imports the plugin class created in the second file and creates an instance of it.
`__init__.py`
```{python eval=FALSE, code=readLines('code/pyqgis/basemap_loader_minimal/__init__.py')}
```
Create these 3 files and put them in a folder named `basemap_loader`. Copy the `logo.png` file from `<home folder>/Downloads/pyqgis_masterclass/logo.png` to this folder. Copy the folder to the python plugins directory at `{profile folder}/python/plugins`.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/pluginbasemapfiles.png')
```
Restart QGIS. Go to **Plugins → Manage and Install plugins... → Installed** and enable the **Basemap Loader** plugin. You will the toolbar icon from the plugin. Click on the button and the *Hello from Plugin* message is displayed.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/pluginbasemaphello.png')
```
## 9.2 Adding Functionality
Now let's build on the basic plugin structure and add the functionality to load a XYZ Tile Layer when the button is clicked. We will be using the [OpenStreetMap Standard](https://wiki.openstreetmap.org/wiki/Raster_tile_providers#Base_maps) XYZ layer. The PyQGIS code to load a XYZ tile layer is adapted from the [PyQGIS Cookbook](https://docs.qgis.org/3.34/en/docs/pyqgis_developer_cookbook/cheat_sheet.html). Modify the `load_basemap.py` file with the content from below.
`load_basemap.py`
```{python eval=FALSE, code=readLines('code/pyqgis/basemap_loader_complete/load_basemap.py')}
```
To see the result of our changes, we must restart QGIS. This can be quite tedious while developing plugins, so there is a handy plugin named **Plugin Reloader** that can reload a selected plugin without having to restart QGIS. Go to *Plugins → Manage and Install plugins... → All* and search for the plugin named **Plugin Reloader**. Click *Install Plugin*. Once the plugin is installed, locate the *Configure* button from the *Plugin Toolbar* and select the **Basemap Loader** plugin. Click *Reload* to reload the plugin.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/pluginbasemapreload.png')
```
Once reloaded, click the *Load Basemap* button from the toolbar and you will see the basemap layer loaded in QGIS.
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/pluginbasemapcomplete.png')
```
## Exercise 7
Load the `places.qgz` project from your data package.
Modify the plugin to change the Project CRS to the crs of the tile layer once the basemap is loaded.
Hint: Use [`QgsProject.instance().setCrs()`](https://qgis.org/pyqgis/3.34/core/QgsProject.html) method.
By default, the new layer will be inserted at the top of the layer tree. If you want to insert the layer at a specific place, you can use the code snippet below.
```{python eval=FALSE}
# Add the layer, but not to the legend
QgsProject.instance().addMapLayer(rlayer, False)
# Insert layer at the bottom of Layer Tree
root = QgsProject.instance().layerTreeRoot()
position = len(root.children())
root.insertLayer(position, rlayer)
```
# 10. Advanced Python Concepts
## 10.1 Understanding Python Iterators
An *Iterator* is a type of Python object that contains items that can be iterator upon. They are similar to other objects, like *lists* - but with a key difference. When you create an iterator, you don't store all the items in memory. The iterator loads a single item at a time and then fetches the next item when asked for it. This makes it very efficient for reading large amounts of data without having to read the entire dataset. QGIS implements iterators for many different object types.
We will continue to work with the `sf.qgz` project. Open the project and select the `blocks` layer. In the example below, the result of calling `layer.getFeatures()` is an iterator. You can call the `next()` function to fetch the next item from the iterator.
```{python eval=FALSE}
layer = iface.activeLayer()
features = layer.getFeatures()
f = next(features)
print(f.attributes())
f = next(features)
print(f.attributes())
```
You can also use for-loops to iterate through an iterator. Here we look up the *Feature ID* of each feature using the `id()` method and store it in a list.
```{python eval=FALSE}
layer = iface.activeLayer()
features = layer.getFeatures()
ids = []
for f in features:
id = f.id()
ids.append(id)
print(ids)
```
## 10.2 List Comprehensions
A common data processing task in Python is to read items from a list or an iterator, doing some processing on each item and creating a new list with the results. The regular way to do this is to first create an empty list, iterate over each item of the existing list, and append the results to the new empty list. Python provides a powerful alternative to this workflow in the form of *List Comprehension*. The snippet below shows the syntax.
```{python eval=FALSE, code=readLines('code/pyqgis/list_comprehensions.py')}
```
## Exercise 8
The following code creates a list of field names for the selected layer. Convert the code to use a list comprehension.
```{python eval=FALSE}
layer = iface.activeLayer()
fields = layer.fields()
field_names = []
for field in fields:
field_names.append(field.name())
print(field_names)
```
# 11. Writing Processing Plugins
The new and preferred way to write plugins in QGIS is using the Processing Framework. It removes the need for you to design the user interface. The resulting plugin integrates seamlessly in the Processing Toolbox and is interoperable with other processing algorithms.
In this section, we will learn how to build a plugin called **Save Attributes** that adds a new algorithm to iterate over all features of a layer, extract their attribute values and save the results as a CSV file. We will learn about different components of a processing plugin and learn the skills required to create a functional processing plugin.
## 11.1 Iterating over Features
Let's start by learning how to iterate over features of a vector layer. We will iterate over each feature and extract the value of all attributes. To keep things simple for now, we will use Pandas library to create a DataFrame and save the results. We will later modify this code to use native PyQGIS API.
Open the Python Console and click the *Show Editor* button. Copy/paste the following code. Select the `blocks` layer and run this script by clicking the *Run Script* button. The script will process the layer and write the file at the given location.
```{python eval=FALSE, code=readLines('code/pyqgis/save_attributes_pandas.py')}
```
## Exercise 9
Modify the above code to save attributes of only selected features. Bonus points if your script checks whether the user has selected any features on the layer and display an error if no features are selected.
Hint1: See the available methods for the [`QgsVectorLayer`](https://qgis.org/pyqgis/3.34/core/QgsVectorLayer.html) class.
Hint2: To check if the layer has any selected features, check for a method that gives you the count of selected features.
## 11.2 Saving Vector Layers
Let's take the script we wrote above and learn how to save the results a vector layer as a file using PyQGIS classes. This is the preferred method while developing scripts that can be used as Processing algorithms or plugins. We will use the [`QgsVectorFileWriter`](https://qgis.org/pyqgis/3.34/core/QgsVectorFileWriter.html) class to create a file in any of the supported vector data formats.
We will use QgsVectorFileWriter.create() method which takes the following parameters
* `fileName`: Path to the file
* `fields`: Fields to write
* `geometryType`: geometry type of output file
* `srs`: CRS of the output file
* `transformContext`: Datum transformation settings
* `options`: Save Options such as format, encoding etc.
Once we initialize a `QgsVectorFileWriter` object, we iterate over the original layer and call the `addFeature()` method to add features to the writer.
Open the Python Console and click the *Show Editor* button. Copy/paste the following code. Select the `blocks` layer and run this script by clicking the *Run Script* button. The script will process the layer and write the file at the given location.
```{python eval=FALSE, code=readLines('code/pyqgis/save_attributes_console.py')}
```
```{r echo=FALSE, fig.align='center', out.width='75%'}
knitr::include_graphics('images/pyqgis/consolescriptrun.png')
```
## 11.3. Writing a Processing Script
We saw how to write a Python script in the QGIS Python Console Code Editor. But there is another way - and it is the preferred approach to write scripts. Whenever you are writing a new script, consider using the built-in **Processing Framework**. This has several advantages. First, taking user input and writing output files is far easier because Processing Framework offers standardized user interface for these. Second, having your script in the Processing Toolbox also allows it to be part of any Processing Model or be run as a Batch job with multiple inputs. This tutorial will show how to write a custom python script that can be part of the Processing Framework in QGIS.
[![Watch the Video](https://img.youtube.com/vi/ABERbwS_5rQ/mqdefault.jpg)](https://www.youtube.com/watch?v=ABERbwS_5rQ&list=PLppGmFLhQ1HKKnk3riKNyOxb-3MTI-7zE&index=30){target="_blank"}