Générer des cartes
Nous allons maintenant générer 341 cartes en PDF, chacune zoomant sur nos 341 sommets (mais pour avancer plus efficacement, utilisez plutôt l'extraction nommée peaks_selection, en pièce jointe de cet article, seulement une dizaine de sommets choisis, qui permettra d'exécuter nos scripts plus rapidement).
Ces cartographies automatiques sont donc en quelque sorte d'une reproduction de la fonction Atlas de QGIS, mais nous allons pouvoir aller beaucoup plus loin : appliquer des zooms différents par cartes, des symbologies conditionnelles différentes, des classifications, ajouter des contenus web, utiliser des APIs, lancer des géo-traitements, jointures, intersections... Bref, c'est pas trop mal.
Et pour les plus pressés🧐, un exemple de code complet est disponible ici.
Sélection itérative
Précédemment nous sélectionnions notre sommet, star de notre carte, sur la base de son identifiant. Mais maintenant que nous souhaitons itérer sur toute la couche peaks, nous ne pouvons plus nommément utiliser les identifiants (en fait si : nous pourrions les écrire un par un dans le code, dans un IN
, mais ce n'est pas le but du jeu, une entité ajoutée dans les données ne serait pas prise en compte, et une entité supprimée ferait bugger le code).
Il nous faut donc boucler sur les entités sommitales.
Inspirez vous de la Sélection sans fonction plus haut dans ce court, pour créer une sélection d'un sommet sans connaître son identifiant.
Votre sélection des sommets se trouvera donc dans une boucle. Nous ne pourrons donc plus zoomer ou nous déplacer manuellement sur la couche pour générer les canvas de nos cartes.
Pour plus de clarté commencez par mettre votre expression dans une variable à part (5ème ligne ci-dessous). Utilisez des print
pour visualiser progressivement le contenu de vos variables.
mylayer = QgsProject.instance().mapLayersByName("peaks_selection")[0] for feat in mylayer.getFeatures(): id_peak = feat['OSM_ID'] expr = u"OSM_ID = '{}'".format(id_peak) myselect = mylayer.getFeatures( QgsFeatureRequest().setFilterExpression ( expr )) mylayer.selectByIds( [ f.id() for f in myselect ] ) iface.mapCanvas().zoomToSelected(mylayer)
Bien, vous êtes capable de lister les différents sommets à l'intérieur d'une boucle.
Profitons-en pour ajouter une variable contenant leurs noms (champ NAME) et affichons là dans un print
.
Contrôlons également le niveau de zoom avec la méthode zoomScale()
.
mylayer = QgsProject.instance().mapLayersByName("peaks_selection")[0] for feat in mylayer.getFeatures(): id_peak = feat['OSM_ID'] peak_name = feat['NAME'] expr = u"OSM_ID = '{}'".format(id_peak) myselect = mylayer.getFeatures( QgsFeatureRequest().setFilterExpression ( expr )) print(f"Voici {peak_name}" ) mylayer.selectByIds( [ f.id() for f in myselect ] ) iface.mapCanvas().zoomToSelected(mylayer) iface.mapCanvas().zoomScale(23000)
Notez que le code ci-dessous se termine en sélectionnant la dernière entité listée, mais il a bel et bien listé, sélectionné puis zoomé une par une sur chaque entité. Même si cela ne s'est pas vu !
QGIS en a même gardé une trace dans son cache : si vous jouez avec le bouton Zoom Précédent du menu de QGIS, puis examinez son nom en comparant avec votre liste print
, vous vous en apercevrez.
Utiliser les données dans la carte
Maintenant vous allez adaptez dans cette boucle le code complet du chapitre Générer une carte, afin de nommer votre layout, votre carte, lui mettre un titre, l'id OSM en guise de sous-titre, et un export correctement nommé également.
Vous allez devoir sélectionner des blocs de code et jouer avec le bouton Tab (indentation) et l'alliance Shift+Tab (retrait d'indentation) afin d'indenter correctement votre code dans la nouvelle boucle.
Testez votre code de façon itérative et procédurale :
- D'abord la création des layouts, allez ensuite vérifier qu'ils existent bien.
- Ensuite le zoom de chaque layout sur chaque sommet, vérifiez-en quelques uns.
- ...
Layouts, canvas et légende
Il vous faudra passer votre variable peak_name
en chaîne de texte pour éviter des erreurs d'interprêtation de la variable layoutName
. Utilisez la méthode str()
.
N'oubliez pas non plus de tout mettre dans la boucle ! Soit une indentation à ajouter au code ci-dessous.
... layoutName = str(peak_name) project = QgsProject.instance() manager = project.layoutManager() # Vérification de la non-existence d'un layout de même nom layouts_list = manager.printLayouts() for layout in layouts_list: if layout.name() == layoutName: manager.removeLayout(layout) # Génération d'un layout vide layout = QgsPrintLayout(project) layout.initializeDefaults() layout.setName(layoutName) manager.addLayout(layout) # Charger une carte vide map = QgsLayoutItemMap(layout) map.setRect(20, 20, 20, 20) # Mettre un canvas basique rectangle = QgsRectangle(1355502, -46398, 1734534, 137094) map.setExtent(rectangle) layout.addLayoutItem(map) # Mettre finalement le canvas courant canvas = iface.mapCanvas() map.setExtent(canvas.extent()) layout.addLayoutItem(map) # Redimensionner la carte map.attemptMove(QgsLayoutPoint(5, 27, QgsUnitTypes.LayoutMillimeters)) map.attemptResize(QgsLayoutSize(220, 178, QgsUnitTypes.LayoutMillimeters)) map.setFrameEnabled(True) # Légende personnalisée tree_layers = project.layerTreeRoot().children() checked_layers = [layer.name() for layer in tree_layers if layer.isVisible()] # print(f"Je vais ajouter uniquement {checked_layers} à la légende." ) layers_to_remove = [layer for layer in project.mapLayers().values() if layer.name() not in checked_layers] # print(f"Je vais retirer {layers_to_remove} de la légende." ) legend = QgsLayoutItemLegend(layout) legend.setTitle("Légende") legend.setLinkedMap(map) layout.addLayoutItem(legend) legend.attemptMove(QgsLayoutPoint(240, 24, QgsUnitTypes.LayoutMillimeters)) # Cette ligne permettra de ne pas sortir les couches inutilisées de votre panneau des calques QGIS, mais uniquement de la légende legend.setAutoUpdateModel(False) m = legend.model() g = m.rootGroup() for l in layers_to_remove: g.removeLayer(l) # Ajuster la légende legend.adjustBoxSize() # Forcer la mise à jour de la légende legend.updateLegend() iface.mapCanvas().refresh() ...
Titres et sous-titres
La variable peak_name
contenant le nom des sommets, à utiliser dans le titre de nos cartes, à déjà été passée en texte, avec la méthode str()
. Elle peut donc être utilisée telle quelle.
Mais ce n'est pas le cas de l'identifiant OSM, que nous voulons ajouter dans le sous-titre. Nous allons le faire mais en y laissant un peu de texte, grâce à une chaîne formatée.
... # Titre title = QgsLayoutItemLabel(layout) title.setText(layoutName) title.setFont(QFont("Verdana", 28)) title.adjustSizeToText() layout.addLayoutItem(title) title.attemptMove(QgsLayoutPoint(5, 4, QgsUnitTypes.LayoutMillimeters)) title.attemptResize(QgsLayoutSize(220, 20, QgsUnitTypes.LayoutMillimeters)) # Sous-titre subtitle = QgsLayoutItemLabel(layout) subtitle.setText("Identifiant OSM : %s" % (str(id_peak))) subtitle.setFont(QFont("Verdana", 17)) subtitle.adjustSizeToText() layout.addLayoutItem(subtitle) subtitle.attemptMove(QgsLayoutPoint(5, 19, QgsUnitTypes.LayoutMillimeters)) subtitle.attemptResize(QgsLayoutSize(220, 20, QgsUnitTypes.LayoutMillimeters)) ...
Échelle
L'échelle doit être légèrement adaptée, puisque nous avons modifié le niveau de zoom :
... # Échelle scalebar = QgsLayoutItemScaleBar(layout) scalebar.setStyle('Single Box') scalebar.setUnits(QgsUnitTypes.DistanceKilometers) scalebar.setNumberOfSegments(2) scalebar.setNumberOfSegmentsLeft(0) scalebar.setUnitsPerSegment(1) scalebar.setLinkedMap(map) scalebar.setUnitLabel('km') scalebar.setFont(QFont('Verdana', 20)) scalebar.update() layout.addLayoutItem(scalebar) scalebar.attemptMove(QgsLayoutPoint(10, 185, QgsUnitTypes.LayoutMillimeters)) ...
Logo
Le logo ne change pas :
... # Logo Logo = QgsLayoutItemPicture(layout) Logo.setPicturePath("https://master-geomatique.org/templates/theme3005/images/logo-ucp-cyu.png") layout.addLayoutItem(Logo) Logo.attemptResize(QgsLayoutSize(40, 15, QgsUnitTypes.LayoutMillimeters)) Logo.attemptMove(QgsLayoutPoint(250,4, QgsUnitTypes.LayoutMillimeters)) ...
Texte
Le texte fera l'objet d'un chapite dédié, ne le mettez pas pour l'instant.
Le bug de la 1ère carte
Dépendant du niveau de zoom mentionné dans votre code, mais aussi de la projection utilisée dans votre projet, à la 1ère ouverture de QGIS et à la 1ère exécution du code, avec le code tel quel, la 1ère carte exportée, a minima, ne va pas prendre le niveau de zomm souhaité, oups...
En fonction de votre situation (puisque dépendant de beaucoup de choses, mdr), à la 2nd exécution du code, ce bug va être corrigé.
Mouais, pas très pro cette histoire... Nous allons donc forcer la mise à jour du niveau de zoom, non pas seulement dans le canvas, mais dans l'objet carte (map
), cela avec un map.setExtent
et un map.setScale
:
... # Zoom on single peak in canvas extent = sommet_unique.extent() iface.mapCanvas().setExtent(extent) iface.mapCanvas().zoomScale(20000) iface.mapCanvas().refresh() # Zoom on single peak in the map (to force zoom on the 1st map) map.setExtent(iface.mapCanvas().extent()) map.setScale(100000) # Layout layout.addLayoutItem(map) ...
Export
Là encore, chaîne formatée pour correctement nommer nos carte grâce à la variable layoutName
.
Attention : le répertoire d'accueil de vos cartes doit exister !
... # Export PDF manager = QgsProject.instance().layoutManager() layout = manager.layoutByName(layoutName) exporter = QgsLayoutExporter(layout) exporter.exportToPdf("C:/Users/georg/Downloads/cartes/%s.pdf" % (layoutName),QgsLayoutExporter.PdfExportSettings())
Le bug des layouts
Bien, on a déjà de belles cartes. Mais si vous allez checker les mises en page dans le gestionnaire d'impressions, vous verrez que malgré le bon export des carte, chaque impression a pris la légende et la symbologie du dernier sommet, arf...
Ceci n'est pas un bug, mais tient au comportement des renderers (renderer()
). En effet à chaque fois qu'on lance un renderer sur une couche, celui-ci s'applique naturellement à toute la couche, puis vient mettre à jour toutes les impressions utilisant cette couche, yep 🤣
Logiquement donc, à chaque itération de la boucle, la carte est bien exporté à partir de l'impression sur le sommet itéré, mais toutes les impressions sont aussi mises à jour sur le sommet itéré.
Cela est donc sans conséquence sur nos exports et nous resteront ainsi. Si cela était un vrai problème, en fonction du contexte, il est possible de gérer les symbologies autrement, avec des presets ou des themes de cartes.
Date du jour
Nous allons ajouter la date du jour dans les noms de nos fichiers.
Ouvrez la console de votre machine (CMD si vous êtes sous Windows) et entrez simplement:
python
La console passe en mode Python et affiche quelques informations de version. Maintenant ce code pour récupérer la date du jour :
import datetime date = datetime.date.today() str(date)
Le même code va fonctionner dans la console Python de QGIS. La 1ère ligne est une importation d'un module natif, à inclure en haut de votre fichier.
Le date peut être stockée hors de la boucle, nous la stockerons dans une variable today
.
import datetime mylayer = QgsProject.instance().mapLayersByName("peaks_selection")[0] date = datetime.date.today() today = str(date) for feat in mylayer.getFeatures(): id_peak = feat['OSM_ID'] peak_name = feat['NAME'] ...
Avant de l'inclure dans le nom de nos exports dans la chaîne formatée.
... exporter.exportToPdf\ ("C:/Users/georg/Downloads/cartes/%s-%s.pdf"\ % (today, layoutName),QgsLayoutExporter.PdfExportSettings())
Bien, pour y voir plus clair, supprimez manuellement toutes vos cartes PDF existantes et lancez votre code.
Ça ne marche pas trop mal😎, mais nous pouvons encore aller plus loin, et sortir ainsi d'un simple atlas.