TD 2 - Intéraction avec l'utilisateur¶
Dans ce TD nous allons créer des commandes et des scripts Python qui seront disponibles depuis l'interface. L'objectif est comprendre comment on peut mettre à disposition d'un utilisateur (soi-même, un collègue, des partenaires, etc) soit des fonctions permettant de faciliter l'usage de QGIS pour certaines tâches soit des algorithmes complets de traitement de données géographiques. Pour cela on aura besoin de la documentation de PyQt5 (disponible sur la page Informations)
Prérequis¶
- Bases en Python.
- Bases en Python-QGIS.
1 - ToolBox¶
On va créer des petits outils à ajouter à l'interface via des QAction que l'on va regrouper dans un menu ToolBox.
1.1 Créer un menu personnalisé¶
QGIS permet à un utilisateur de placer un fichier startup.py à un emplacement particulier qui est executé au lancement de l'application. Ce fichier permet notamment d'ajouter des éléments personnalisés à QGIS.
1.1.1 Ajouter un fichier startup.py
1.1.1 Ajouter un fichier startup.py¶
Suivez les instructions pour créer le fichier startup.py. Utilisez la méthode print() pour que lorsque votre fichier sera exécuté un message s'affiche dans le terminal après le lancement de l'application QGIS depuis le terminal.
1.1.2 Menu
1.1.2 Menu¶
Ajoutez dans votre fichier startup.py la création d'un Menu avec la classe QMenu.
Tip
- Vous pouvez importez iface depuis
qgis.utils - Importez QMenu depuis
qgis.PyQt.QtWidgets - Regardez comment créer un QMenu sur la documentation PyQt5. Cherchez la définition de la classe.
- Utilisez iface pour avoir accès au
Widgetparent, l'interface principale de QGIS.
Solution
# startup.py
from qgis.utils import iface
from qgis.PyQt.QtWidgets import QAction, QMenu
menu = QMenu("ToolBox", iface.mainWindow())
menu.setObjectName("ToolBox")
iface.mainWindow().menuBar().addMenu(menu)
1.2 Actions¶
On utilise des QAction pour encaspsuler des fonctions qui effectuent des actions. Dans ces fonctions vous pouvez accéder aux différentes fonctions et classes vues dans le TD 1. Le principe est assez simple:
- Définissez un fonction qui fera l'action souhaitée.
- Créez une QAction avec le nom de l'action. Le parent du Widget QAction doit être la fenêtre principale (comme pour le menu).
- Connectez le signal trigger à la fonction que vous avez défini.
- Ajoutez l'action au menu.
Info
La documentation est assez succinte pour cette partie. N'hésitez pas à faire des recherches internet sur comment utilisez ces QAction et les connectées.
1.2.1 Zoom to selection
1.2.1 Zoom to selection¶
Créez une action pour effectuer un zoom sur les features séléctionnées. Assurez vous d'informer l'utilisateur si:
- Aucune couche n'est active.
- Aucune feature n'est séléctionnée.
- La couche n'est pas une couche vectorielle.
Ajoutez cette action au menu dans startup.py et relancez QGIS pour tester votre action.
Solution
# startup.py - Zoom to selection
def zoom_to_selection():
"""Zoom to selected features."""
layer: QgsVectorLayer = iface.activeLayer()
if layer and layer.selectedFeatureCount() > 0:
iface.mapCanvas().zoomToSelected(layer)
elif not layer:
iface.messageBar().pushWarning("Zoom", "No active layer.")
elif not isinstance(layer, QgsVectorLayer):
iface.messageBar().pushWarning("Zoom", f"Active layer is not vector layer but {type(layer)}")
elif layer.selectedFeatureCount() == 0:
iface.messageBar().pushWarning("Zoom", f"No features selected.")
action_zoom = QAction("Zoom to selection", iface.mainWindow())
action_zoom.triggered.connect(zoom_to_selection)
menu.addAction(action_zoom)
1.2.2 Coloriser des features
1.2.2 Coloriser des features¶
Créez une action pour appliquer une couleur sur les features séléctionnées. Assurez vous d'informer l'utilisateur si:
- Aucune couche n'est active.
- Aucune feature n'est séléctionnée.
- La couche n'est pas une couche vectorielle.
Ajoutez cette action au menu dans startup.py et relancez QGIS pour tester votre action. Utilisez la classe QgsHighlight pour coloriser des features. Pour la couleur prenez une couleur définie par Qt.
Tip
Importez Qt depuis qgis.PyQt.QtCore et QColor depuis qgis.PyQt.QtCore. Pour avoir un objet couleur utilisable faites QColor(Qt.red) ou une autre couleur de base. Attention l'example de la documentation est faux, pour créer un QgsHighlight il ne faut pas la feature mais sa géométrie. N'hésitez pas à segmenter la tâche en plusieurs fonctions:
- Une fonction pour appliquer le highlight sur uen feature.
- Une fonction qui parcours les features séléctionnées et appelle la première fonction dessus.
Solution
# startup.py - Coloriser des features
def green_highlight(feature: QgsFeature, layer: QgsVectorLayer):
""""Apply green highlight on given feature"
Args:
feature (QgsFeature): Feature to highlight.
layer (QgsVectorLayer): Layer of the feature.
"""
color = QColor(Qt.green)
highlight = QgsHighlight(iface.mapCanvas(), feature, layer)
highlight.setColor(color)
color.setAlpha(50)
highlight.setFillColor(color)
highlight.show()
def highlight_selection():
"""Apply green highlight on selected features."""
layer: QgsVectorLayer = iface.activeLayer()
features = layer.selectedFeatures()
if layer and features:
for feat in features:
green_highlight(feat, layer)
elif not layer:
iface.messageBar().pushWarning("Zoom", "No active layer.")
elif not isinstance(layer, QgsVectorLayer):
iface.messageBar().pushWarning("Zoom", f"Active layer is not vector layer but {type(layer)}")
elif layer.selectedFeatureCount() == 0:
iface.messageBar().pushWarning("Zoom", f"No features selected.")
action_highlight = QAction("Highlight", iface.mainWindow())
action_highlight.triggered.connect(highlight_selection)
menu.addAction(action_highlight)
1.2.3 Supprimer la colorisation
1.2.3 Supprimer la colorisation¶
Ajoutez une action pour supprimer les colorisations actuelles. Modifiez les fonctions utilisées dans la question 1.2.2 pour stocker les highlights à supprimer dans une varaible globale.
Tip
- Utilisez l'instruction global pour accéder à une variable définie en dehors de vore fonction.
- Pour squpprimer un objet Qt utilisez iface.mapCanvas().scene().removeItem(obj)
Solution
# startup.py - Coloriser des features
def green_highlight(feature: QgsFeature, layer: QgsVectorLayer):
""""Apply green highlight on given feature"
Args:
feature (QgsFeature): Feature to highlight.
layer (QgsVectorLayer): Layer of the feature.
"""
global highlights
color = QColor(Qt.green)
highlight = QgsHighlight(iface.mapCanvas(), feature, layer)
highlight.setColor(color)
color.setAlpha(50)
highlight.setFillColor(color)
highlight.show()
highlights.append(highlight)
def highlight_selection():
"""Apply green highlight on selected features."""
layer: QgsVectorLayer = iface.activeLayer()
features = layer.selectedFeatures()
if layer and features:
for feat in features:
green_highlight(feat, layer)
elif not layer:
iface.messageBar().pushWarning("Zoom", "No active layer.")
elif not isinstance(layer, QgsVectorLayer):
iface.messageBar().pushWarning("Zoom", f"Active layer is not vector layer but {type(layer)}")
elif layer.selectedFeatureCount() == 0:
iface.messageBar().pushWarning("Zoom", f"No features selected.")
action_highlight = QAction("Highlight", iface.mainWindow())
action_highlight.triggered.connect(highlight_selection)
menu.addAction(action_highlight)
# startup.py - Supprimer la colorisation
def clear_highlight():
"""Delete all highlights."""
global highlights
for h in highlights:
iface.mapCanvas().scene().removeItem(h)
action_highlight_clear = QAction("Clear highlights", iface.mainWindow())
action_highlight_clear.triggered.connect(clear_highlight)
menu.addAction(action_highlight_clear)
1.2.4 Rotation de géométrie.
1.2.4 Rotation de géométrie.¶
Créez une action pour faire une rotation d'angle aléatoire de la géométrie des features séléctionnées. Faites les modifications sans le dataProvider afin que l'utilisateur soit obligé d'activer l'édition. Vous n'avez pas à gérer le buffer d'édition puisque l'utilisateur le fera en cliquant sur le bouton associé.
Tip
- Utilisez le module random pour générer un angle aléatoire.
- Vous pouvez créer le clone d'une géométrie en faisant QgsGeometry(geometry).
Solution
def random_rotate(feature: QgsFeature, layer: QgsVectorLayer) -> bool:
"""Replace feature geometry by random rotated geometry.
Args:
feature (QgsFeature): Feature to update geometry.
layer (QgsVectorLayer): Feature's layer.
"""
geom = feature.geometry()
angle = rd.randint(1,359)
center = geom.centroid()
rotate_geom = QgsGeometry(geom)
rotate_geom.rotate(angle, center.asPoint())
feature.setGeometry(rotate_geom)
success = layer.updateFeature(feature)
return success
def features_rotation():
"""Randomly rotate selected features."""
layer: QgsVectorLayer = iface.activeLayer()
features = layer.selectedFeatures()
if not layer.isEditable():
iface.messageBar().pushWarning("Rotation", f"Layer is not editable.")
elif layer and features:
for feat in features:
random_rotate(feat, layer)
elif not layer:
iface.messageBar().pushWarning("Rotation", "No active layer.")
elif not isinstance(layer, QgsVectorLayer):
iface.messageBar().pushWarning("Rotation", f"Active layer is not vector layer but {type(layer)}")
elif layer.selectedFeatureCount() == 0:
iface.messageBar().pushWarning("Rotation", f"No features selected.")
action_rotation = QAction("Rotation", iface.mainWindow())
action_rotation.triggered.connect(features_rotation)
menu.addAction(action_rotation)
1.2.5 Rotation et freeze.
1.2.4 Rotation de géométrie.¶
Répétez l'opération de rotation 10 fois pour chaque feature en mettant à jour l'action de la question 1.2.4. Appliquez l'action sur un grand nombre de feature. L'interface de QGIS devrait s'arrêter de fonctionner un court moment.
Info
Les actions que nous avons crées présentent deux problèmes:
- On ne peut traiter que des couches présentent sur l'interface QGIS.
- Les tâches s'éxécutent dans la même thread que l'interface principale. Comme QGIS doit gérer les instructions de l'action il ne peut plus gérer l'interface en même temps ce qui entraîne un arrêt temporaire de l'application.
La solution pour régler ces deux problèmes est de dévelloper des algorithmes de traitement qui seront disponibles directement depuis la boîte à outil.
2 - Processing¶
Il y a plusieurs manières d'ajouter des algortihmes processing à la boîte à outil de QGIS:
- Créer un plugin entier et l'installé.
- Créer des scripts individuels Python et ajouter les script à la boîte à outils.
2.1 Scripts processing¶
On va commencer par créer un algorihtme processing dans un script individuel.
2.1.1 Buffer processing
2.1.1 Question title¶
Copiez l'exemple de la documentation pour créez un algorihtme processing qui applique un buffer dans un script Python. Ajoutez ce script à la boîte à outil. Testez le script.
Info
C'est dans la méthode processAlgorithm() que l'on écrit réellement l'algorithme. Il faut récupérer les paramètres depuis le dictionnaire de paramètre, appliquer les opérations et retourner un dictionnaire de sorties. QGIS gère le reste. La définition des paramètres ce fait dans la méthode initAlgorithm().
2.1.2 Personnaliser un script
2.1.2 Personnaliser un script¶
Changez le nom des paramètres d'entrée et de sortie. Modifiez également le nom de l'algorihme et son groupe. Ajoutez le nouveau script à la boîte à outil et observez les changements dans l'affichage de la boîte à outil et dans la fenêtre de dialogue de l'algorithme.
Solution
from qgis.core import (
QgsProcessingAlgorithm,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterNumber,
QgsProcessingParameterFeatureSink,
QgsFeatureSink,)
class BufferAlgorithm(QgsProcessingAlgorithm):
LAYER = "LAYER"
BUFFER = "BUFFER"
BUFFER_LAYER = "BUFFER_LAYER"
def initAlgorithm(self, config=None):
self.addParameter(QgsProcessingParameterFeatureSource(self.LAYER, "LAYER"))
self.addParameter(
QgsProcessingParameterNumber(self.BUFFER, "BUFFER", defaultValue=100.0)
)
self.addParameter(
QgsProcessingParameterFeatureSink(self.BUFFER_LAYER, "BUFFER_LAYER")
)
def processAlgorithm(self, parameters, context, feedback):
source = self.parameterAsSource(parameters, self.LAYER, context)
distance = self.parameterAsDouble(parameters, self.BUFFER, context)
(sink, dest_id) = self.parameterAsSink(
parameters,
self.BUFFER_LAYER,
context,
source.fields(),
source.wkbType(),
source.sourceCrs(),
)
for f in source.getFeatures():
f.setGeometry(f.geometry().buffer(distance, 5))
sink.addFeature(f, QgsFeatureSink.FastInsert)
return {self.BUFFER_LAYER: dest_id}
def name(self):
return "buffer rename"
def displayName(self):
return "Buffer Features rename"
def group(self):
return "Examples"
def groupId(self):
return "examples"
def createInstance(self):
return BufferAlgorithm()
2.2 Payasage et biodiversité processing¶
2.2.1 Paysage et biodiversité
2.2.1 Algorithme Biodiversité et paysage¶
Reprenez le code que vous avez écrit dans le TD 1 pour l'exercice 6.1 (ou la solution) et adaptez le pour qu'il soit utilisable dans un algortihme processing via un script Python. Puis testez le.
2.2.2 Barre de progression
2.2.2 Barre de progression¶
Utilisez la variable feedback (instance de QgsProcessingFeedback) pour afficher une barre de progression qui met à jour l'avancement à chaque itération sur les cellules de la grille. Faites en sorte que l'avancement passe de 0 à 100 avec des intervalles réguliers.