Index de l'article

Générer une carte

Nous allons maintenant générer une carte PDF à partir du canvas QGIS. À chaque déplacement dans le canvas, une nouvelle exécution du script final mettra à jour la carte. 

Environnement de travail

Ouvez les couches peaks, eausolIRIS et troncons_routes, fournies en bas de cet article, dans un projet QGIS vierge, ainsi qu'un fond de carte OpenStreetMap.

Vérifiez la bonne supersposition des couches (les projections), puis appliquez une symbologie évidente.

Enregistrez votre projet puis décochez les couches eau, sol, IRIS et troncons_routes.

Ouvrez également le Layouts Manager. De même pour les besoins de la démonstration, faîtes-en sorte que vos écrans vous permettent de visualiser en même temps votre projet QGIS, console et éditeur Python ouverts, le Layouts Manager, et encore un peu de place pour un futur layout.

Zoomez sur la couche peak. Nous allons exécuter les bribes de code suivantes de façon itérative, en les décryptant une par une. Au terme de ce chapitre, vous aurez un code complet vous permettant de générer une carte simple. Sauvegardez petit-à-petit l'intégralité du code dans un fichier texte.

Layout

Nous générons un layout vide, que vous allez voir apparaître dans le Layouts Manager.

  1. project = QgsProject.instance()
  2. manager = project.layoutManager()
  3. layoutName = "PrintLayout1"
  4.  
  5. # Vérification de la non-existence d'un layout de même nom
  6. layouts_list = manager.printLayouts()
  7. for layout in layouts_list:
  8. if layout.name() == layoutName:
  9. manager.removeLayout(layout)
  10.  
  11. # Génération d'un layout vide
  12. layout = QgsPrintLayout(project)
  13. layout.initializeDefaults()
  14. layout.setName(layoutName)
  15.  
  16. manager.addLayout(layout)

Encart cartographique

Ouvrez le layout vide. Nous tirons maintenant une carte vide dans la layout.

  1. # Charger une carte vide
  2. map = QgsLayoutItemMap(layout)
  3. map.setRect(20, 20, 20, 20)
  4.  
  5. # Mettre un canvas basique
  6. rectangle = QgsRectangle(1355502, -46398, 1734534, 137094)
  7. map.setExtent(rectangle)
  8. layout.addLayoutItem(map)

Canvas courant

Nous plaçons le canvas courant dans la carte.

  1. # Mettre finalement le canvas courant
  2. canvas = iface.mapCanvas()
  3. map.setExtent(canvas.extent())
  4.  
  5. layout.addLayoutItem(map)
  6.  
  7. # Redimensionner la carte
  8. map.attemptMove(QgsLayoutPoint(5, 27, QgsUnitTypes.LayoutMillimeters))
  9. map.attemptResize(QgsLayoutSize(220, 178, QgsUnitTypes.LayoutMillimeters))

Légende dynamique

Ci-après le code d'affichage de la légende complète (une légende fixe, statique). Cette portion du code est commentée (encadré dans une balise """, et donc inactive).

En effet juste en dessous, nous allons préférer ne retenir que les couches apparaissant dans la carte (cochées dans le panneau des couches).

  1. """
  2. # Mettre une légende complète
  3. legend = QgsLayoutItemLegend(layout)
  4. legend.setTitle("Légende")
  5. layout.addLayoutItem(legend)
  6. legend.attemptMove(QgsLayoutPoint(246, 5, QgsUnitTypes.LayoutMillimeters))
  7. """
  8.  
  9. # Légende personnalisée
  10. tree_layers = project.layerTreeRoot().children()
  11. checked_layers = [layer.name() for layer in tree_layers if layer.isVisible()]
  12. print(f"Je vais ajouter uniquement {checked_layers} à la légende." )
  13.  
  14. layers_to_remove = [layer for layer in project.mapLayers().values() if layer.name() not in checked_layers]
  15. print(f"Je vais retirer {layers_to_remove} de la légende." )
  16.  
  17. legend = QgsLayoutItemLegend(layout)
  18. legend.setTitle("Légende")
  19.  
  20. layout.addLayoutItem(legend)
  21.  
  22. legend.attemptMove(QgsLayoutPoint(240, 24, QgsUnitTypes.LayoutMillimeters))
  23.  
  24. # Cette ligne permettra de ne pas sortir les couches inutilisées de votre panneau des calques QGIS, mais uniquement de la légende
  25. legend.setAutoUpdateModel(False)
  26.  
  27. m = legend.model()
  28. g = m.rootGroup()
  29. for l in layers_to_remove:
  30. g.removeLayer(l)
  31.  
  32. legend.adjustBoxSize()

Jouez avec les print et la console python avec votre layout ouvert pour bien comprendre ce qui se passe.

Algorithmique

Pour la génération de notre légende dynamique ci-dessus, nous avons commencer par lister 3 variables.

  • Une contenant toutes les couches (tree_layers)
  • Une autre contenant uniquement les couches affichées (checked_layers)
  • Enfin une dernière ne contenant que les couches non-cochées (layers_to_remove), grâce à une 1ère soustraction des couches cochées

En algorithmique :

layers_to_remove = tree_layers - checked_layers
  • Ensuite nous affichons la légende complète puis en retirons layers_to_remove dans une 2nd soustraction.

Toujours en algorithmique :

my_legend = legend - layers_to_remove
  • Note personnelle : on pourrait aller plus loin dans cette légende dynamique en masquant de la légende les couches dont aucune entité n'apparaît dans le canvas courant (la couche adresse notamment).

Titre

  1. # Titre
  2. title = QgsLayoutItemLabel(layout)
  3. title.setText("Ma Jolie Carte")
  4. title.setFont(QFont("Verdana", 28))
  5. title.adjustSizeToText()
  6.  
  7. layout.addLayoutItem(title)
  8.  
  9. title.attemptMove(QgsLayoutPoint(5, 4, QgsUnitTypes.LayoutMillimeters))

Tout comme avec la légende, vous connaissez maintenant la méthode attemptMove qui vous permet de positioner les éléments dans votre layout.

Les chiffres en argument sont dans l'ordre : la distance à partir du bord gauche du layout, puis la distance à partir du bord haut.

Sous-titre

  1. # Sous-titre
  2. subtitle = QgsLayoutItemLabel(layout)
  3. subtitle.setText("Est-elle belle ?")
  4. subtitle.setFont(QFont("Verdana", 17))
  5. subtitle.adjustSizeToText()
  6.  
  7. layout.addLayoutItem(subtitle)
  8.  
  9. subtitle.attemptMove(QgsLayoutPoint(5, 19, QgsUnitTypes.LayoutMillimeters))

Échelle

  1. # Échelle
  2. scalebar = QgsLayoutItemScaleBar(layout)
  3. scalebar.setStyle('Single Box')
  4. scalebar.setUnits(QgsUnitTypes.DistanceKilometers)
  5. scalebar.setNumberOfSegments(2)
  6. scalebar.setNumberOfSegmentsLeft(0)
  7. scalebar.setUnitsPerSegment(5)
  8. scalebar.setLinkedMap(map)
  9. scalebar.setUnitLabel('km')
  10. scalebar.setFont(QFont('Verdana', 20))
  11. scalebar.update()
  12.  
  13. layout.addLayoutItem(scalebar)
  14.  
  15. scalebar.attemptMove(QgsLayoutPoint(10, 185, QgsUnitTypes.LayoutMillimeters))

Logo

  1. # Logo
  2. Logo = QgsLayoutItemPicture(layout)
  3. Logo.setPicturePath("https://master-geomatique.org/templates/theme3005/images/logo-ucp-cyu.png")
  4.  
  5. layout.addLayoutItem(Logo)
  6.  
  7. Logo.attemptResize(QgsLayoutSize(40, 15, QgsUnitTypes.LayoutMillimeters))
  8. Logo.attemptMove(QgsLayoutPoint(250,4, QgsUnitTypes.LayoutMillimeters))

Vous utilisez ici la méthode attemptResize pour redimensionner l'élément. Les chiffres en argument sont dans l'ordre la largeur, puis la hauteur.

Petit exercice

Maintenant que vous savez mettre une image, ajoutez donc une flèche nord correctement positionnée.

Vous pouvez faire autrement si vous le souhaitez.

Texte

  1. # Texte
  2. TextCustom = QgsLayoutItemLabel(layout)
  3. TextCustom.setText("Le massif des Écrins est un grand massif montagneux des Alpes françaises situé dans les Hautes-Alpes et en Isère.\
  4. \n\nIl abrite d'importants glaciers, tant en nombre qu'en taille et possède deux sommets de plus de 4 000 mètres.\
  5. \n\nIl était autrefois également nommé massif du Pelvoux.\
  6. \n\nL'Oisans (bassin de la Romanche) au nord-ouest, le Champsaur (haut-bassin du Drac) au sud-ouest, et le Briançonnais (bassin de la Guisane) au nord-est recouvrent une partie du massif.")
  7. TextCustom.setFont(QFont("Verdana", 11))
  8.  
  9. layout.addLayoutItem(TextCustom)
  10.  
  11. TextCustom.attemptResize(QgsLayoutSize(60, 120, QgsUnitTypes.LayoutMillimeters))
  12. TextCustom.attemptMove(QgsLayoutPoint(230, 80, QgsUnitTypes.LayoutMillimeters))

Petit exercice

Maintenant que vous savez mettre un texte (un simple label en réalité, comme les titre et sous-titre), ajoutez la source.

  • Note personnelle : on pourrait aller plus loin en allant chercher les sources directement dans les méta-données de nos couches.

Export

Vous connaissez déjà la manœuvre pour un export. Pensez à adapter le chemin utilisé ci-dessous.

  1. # Export PDF
  2. manager = QgsProject.instance().layoutManager()
  3. layout = manager.layoutByName("PrintLayout1")
  4. exporter = QgsLayoutExporter(layout)
  5. exporter.exportToPdf("C:/Users/Georges/Downloads/qgis/scripts/Ma Jolie Carte.pdf",QgsLayoutExporter.PdfExportSettings())

En ouvrant le PDF dans votre navigateur, en vous déplaçant sur la carte, en cochant-décochant certaines couches puis en re-lançant l'intégralité du code ci-dessus, vous verrez votre PDF exporté être mis à jour en fonction.

  • Note personnelle : on pourrait aller plus loin en ajoutant une vraie flèche nord, dynamique, suivant l'orientation du canvas.
Liens ou pièces jointes
Accéder à cette adresse URL (https://hg-map.fr/extern/data/shapes/france/chemin_de_fer.zip)chemin_de_fer.zip[ ]0 Ko
Télécharger ce fichier (data_BDTOPO_V3_Dep05_adresse.zip)data_BDTOPO_V3_Dep05_adresse.zip[ ]3889 Ko
Télécharger ce fichier (data_IRIS_2019.zip)data_IRIS_2019.zip[ ]45905 Ko
Télécharger ce fichier (decathlon_france.zip)decathlon_france.zip[308 magasins Décathlon français depuis OSM le 27 décembre 2020]11 Ko
Accéder à cette adresse URL (https://hg-map.fr/extern/data/shapes/france/eau.zip)eau.zip[ ]0 Ko
Télécharger ce fichier (glaciers.zip)glaciers.zip[ ]231 Ko
Télécharger ce fichier (iso_iris.zip)iso_iris.zip[Des zones isochrones à 15 minutes autour de 308 POIs.]12125 Ko
Télécharger ce fichier (MasterGeom2_ProgPython_DevoirMaison.pdf)Devoir-maison[ ]422 Ko
Télécharger ce fichier (peaks.zip)peaks.zip[ ]14 Ko
Accéder à cette adresse URL (https://hg-map.fr/extern/data/shapes/france/troncons_routes.zip)troncons_routes.zip[ ]0 Ko