Symbologie avancée sous QGIS 3 🔗

Posted by Médéric Ribreux 🗓 In blog/Qgis/

#gis #GIS #QGIS

Introduction

QGIS est vraiment un outil complet. À ce stade de son développement, je crois qu'on peut dire qu'il possède le meilleur moteur de symbologie du monde SIG. Plus le temps passe et moins j'arrive à le déjouer, c'est-à-dire à me retrouver dans une situation où il est incapable de représenter une information ou un symbole donné.

Néanmoins, un grand pouvoir implique souvent une grande complexité et dernièrement, j'ai dû pousser le moteur de symbologie dans ses retranchements. J'ai eu aussi du mal à trouver des réponses à mes questions, tout simplement parce que la plupart du temps, peu de personnes ont besoin d'aller aussi loin.

Dans cet article, je vous propose de vous présenter deux cas complexes que j'ai eu à gérer et de vous montrer comment je m'en suis sorti. Ça me permettra également de garder trace de ce travail pas si négligeable que ça.

En termes de pré-requis, vous devez savoir comment fonctionne le moteur de symbole dans son ensemble pour comprendre ce qui suit.

Les symboles à représenter

Voici ce que je souhaitais représenter: deux symboles présents sur la carte OACI:

NDB OACI

VOR OACI

J'aurais pu faire un dessin en SVG (avec un éditeur comme inkscape ou même à la main). Mais ça aurait été long et puis il m'aurait fallu un autre outil que QGIS. Donc, j'ai préféré aller plus loin et explorer les fonctions de base.

J'ai utilisé une seule couleur pour la représentation: #0646a5.

Les données peuvent être récupérées depuis le site du SIA. Elles sont stockées dans le système de coordonnées WGS84 de base: ESPG:4326. Par contre, pour l'affichage de la carte, j'utilise la projection Web Mercator (EPSG:3857) et ce détail a son importance.

La balise NDB

Pour ce cas particulier, je souhaitais reproduire le symbole mais également le rendre fixe dans l'espace, c'est-à-dire de le faire apparaître plus petit au fur et à mesure de l'éloignement de la carte. C'est ce qu'on retrouve quand on visualise une carte au format raster et c'est plus pratique dans le cas que je visais que de se retrouver avec une carte de France complètement obfusquée par les diverses balises.

J'ai décomposé le symbole de la balise en deux parties:

Les éléments de base

Ce sont des choses de base qu'on reproduit assez facilement avec deux couches de symbole de type marqueur/marker:

En termes de représentation, ça donne ça:

Base du symbole NDB

Le motif de points

C'est clairement le plus compliqué à obtenir. Pour y parvenir, j'ai essayé d'utiliser une couche de symbole de type remplissage de marqueur. Mais je me suis vite rendu compte que le motif n'était pas du type remplissage à partir d'un cercle. Je ne suis pas parvenu à le rendre conforme au symbole de base.

J'ai alors pris le parti d'explorer le générateur de géométrie. C'est un type de couche de symbole qui permet d'écrire une expression QGIS pour générer d'autres géométries à partir de la géométrie initiale.

Voilà le code que j'ai utilisé pour former une couche de multipoints:

-- Circle stuff
collect_geometries(
array_cat(
  array_foreach(
    generate_series(0, 337.5, 22.5),
    transform(
      project(transform($geometry, 'EPSG:4326', 'EPSG:3857'), 1250, radians(@element)),
      'EPSG:3857', 'EPSG:4326')
  ),
  array_foreach(
    generate_series(0, 31, 1),
    transform(
      project(transform($geometry, 'EPSG:4326', 'EPSG:3857'), 2250, radians(@element * 360 / 32.0)),
      'EPSG:3857', 'EPSG:4326')
  ),
   array_foreach(
    generate_series(0, 21, 1),
    transform(
      project(transform($geometry, 'EPSG:4326', 'EPSG:3857'), 1750, radians(@element * 360 / 22.0)),
      'EPSG:3857', 'EPSG:4326')
  ))
)

En termes d'explications, globalement, on voit que c'est une collection de géométrie (collect_geometries) basée sur un tableau qui est lui-même la concaténation (array_cat) de trois autres tableaux qui sont chacun générés (array_for_each) par une série (generate_series) de points.

Si on décompose la série de points, on obtient:

Avec trois séries de points rassemblées sur 3 distances différentes et avec 3 séries plus ou moins nombreuses, on parvient à obtenir notre motif de points.

Il reste à utiliser une couche de symbologie pour chaque géométrie et on va simplement utiliser un symbole de type marqueur cercle d'une taille de 250m à l'échelle. Bien entendu, la couche de symbole de type générateur de géométrie utilise les unités de la carte.

A la fin, on obtient le symbole suivant:

Motif de points NDB

Puis le symbole complet:

NDB complet

Le VOR

Même topo que pour la balise NDB: on part d'un point et on veut obtenir une forme plus complexe que je décompose en 4 parties:

On va déjà se débarrasser du trivial, à savoir, le symbole de base qui est composé de:

Ça donne ça à l'écran:

Base du symbole de VOR

Le cercle du VOR

Maintenant, on va s'atteler à faire un cercle à partir du point. Pour cela, vous aurez besoin d'ajouter une nouvelle couche de symbole du type Générateur de géométrie/Geometry generator et d'y coller le code qui suit:

transform(
  buffer(
    transform($geometry, 'EPSG:4326', 'EPSG:3857'),
    18500, 20
  ), 'EPSG:3857', 'EPSG:4326'
)

Pour les explications, on va d'abord transformer notre géométrie (qui est un point) dans le référentiel visé: transform($geometry, 'EPSG:4326', 'EPSG:3857'). C'est lié à la projection en cours et au fait qu'on va utiliser un tampon/buffer pour générer le cercle. Comme je veux un cercle parfait en EPSG:3857, je convertis le point dans cette projection.

Ensuite, je crée un tampon d'un rayon de 18500 mètres, l'unité du système de projection EPSG:3857, avec 20 points pour la circonférence. C'est l'instruction buffer.

Enfin, je refais une projection de EPSG:3857 vers le système de projection d'origine pour obtenir mon cercle au bon endroit et avec la bonne forme.

Le style de la géométrie en sortie est une couche de symbole ligne avec une taille de 250 mètres à l'échelle et la couleur sus-mentionnée en introduction.

À la fin, on obtient ça:

Image du cercle VOR

Les graduations

Ici, on a besoin de faire des graduations de dix degrés en dix degrés sur le cercle. Mais on peut remarquer que certaines sont différentes des autres:

Pour ma part, j'ai obtenu les bons résultats en utilisant le code suivant:

-- Projection de base (pour le respect des distances)
with_variable('sgeom', transform($geometry, 'EPSG:4326', 'EPSG:3857'),
  collect_geometries(
    array_cat(
    -- Premier tableau avec les graduations de 10°
    array_foreach(
      array(1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19,20, 22,23, 25, 26, 28, 29, 31, 32, 34, 35),
      make_line(
        transform(
          project(@sgeom, 18500, radians(@element * 360 / 36.0)),
          'EPSG:3857', 'EPSG:4326')
       ,transform(
         project(@sgeom, 17200, radians(@element * 360 / 36.0)),
         'EPSG:3857', 'EPSG:4326')
      )
    ),
    -- Second tableau avec les graduations de 30°
    array_foreach(
      array(3, 6, 12, 15, 21, 24, 30, 33),
      make_line(
        transform(
          project(@sgeom, 18500, radians(@element * 360 / 36.0)),
          'EPSG:3857', 'EPSG:4326')
       ,transform(
         project(@sgeom, 16000, radians(@element * 360 / 36.0)),
         'EPSG:3857', 'EPSG:4326')
      )
    )
    )
  )
)

Pour le tableau array qui contient toutes les valeurs, je n'ai pas trouvé de manière plus élégante d'indiquer les graduations que je voulais voir figurer. J'aurais pu générer une série et enlever des valeurs mais c'était plus long à écrire.

Pour comprendre, globalement, pour chaque graduation qui nous intéresse, on crée une ligne (make_line). Cette ligne est créée grâce à deux points qui sont situés à deux distances différentes de notre point central, suivant un azimut décrit par l'angle formé par notre numéro de graduation tous les 10°, soit tous les 36°. C'est la même utilisation de la fonction project que dans la balise NDB.

J'ai assemblé les deux tableaux avec la fonction array_cat. Dans le tableau de 10° en 10° comparé à celui de 30° en 30°, seule la distance change.

with_variable permet de déclarer des variables utilisées régulièrement dans le code de l'expression. Ici, j'ai simplement utilisé le point de la couche et je l'ai re-projeté.

Ce code de génération de géométries renvoie une géométrie de type multi-lignes qui peut être stylée avec une simple couche de symbologie de ligne avec une taille de 250m à l'échelle.

Au final, les graduations donnent ça:

Image de la graduation 10°/30° du cercle VOR

Les flèches cardinales

Pour la géométrie, on va simplement créer 3 lignes qui pointent vers l'Est, le Sud et l'Ouest, niveau du cercle. C'est comme les graduations mais sur 3 objets.

with_variable('sgeom', transform($geometry, 'EPSG:4326', 'EPSG:3857'),
collect_geometries(
  array_foreach(
    array(1, 2, 3),
    make_line(
      transform(
        project(@sgeom, 18500, radians(@element * 360 / 4.0)),
        'EPSG:3857', 'EPSG:4326')
     ,transform(
        project(@sgeom, 15500, radians(@element * 360 / 4.0)),
       'EPSG:3857', 'EPSG:4326'))
    )
  )
)

Cette fois le code est plus simple et reprend ce qu'on a vu avec les graduations. Rien de nouveau.

Par contre, pour la représentation, j'ai choisi le type flèche/arrow avec les paramètres suivants:

Au final, ça donne ça:

Image des flèches cardinales du cercle VOR

La flèche du nord

Bon, cette fois, rien de bien compliqué avec la génération de géométrie:

with_variable('sgeom', transform($geometry, 'EPSG:4326', 'EPSG:3857'),
  make_line(
    transform(project(@sgeom, 18500, 0.0), 'EPSG:3857', 'EPSG:4326'),
    transform(project(@sgeom, 4000, 0.0), 'EPSG:3857', 'EPSG:4326')
  )
)

C'est une simple ligne, mais un peu plus longue que dans les exemples précédents.

Pour la représentation, ici aussi on utilise une flèche mais avec une configuration différente:

Au final, ça donne ça:

Image du symbole VOR complet

Conclusions

Bon, ces solutions exploitent la richesse du générateur de géométrie dans la symbologie. Néanmoins, elles présentent les avancées du moteur de symbologie de QGIS ainsi que celles du moteur d'expression. Car je n'ai rien eu à coder en Python ou en C++. Je me suis basé sur QGIS3 de base, sans plugin ou ajout.

Bien entendu, je suis persuadé qu'il existe d'autres manières de faire, c'est souvent le cas dans QGIS. Mais, on voit bien qu'en utilisant une expression dans une couche de génération de géométrie, on peut pratiquement faire ce qu'on veut.

D'un point de vue des performances, ça reste lent à afficher. Mais pour contrecarrer cet effet, QGIS met en cache les géométries générées à l'ouverture du projet. Ce dernier est un poil long à ouvrir (ça reste raisonnable) mais après, la navigation dans la carte est fluide, comme si on n'utilisait aucun symbole compliqué. C'est plutôt bien pensé.

Enfin, même si c'est un cas complexe, il ne donne encore une fois qu'un aperçu des fonctionnalités du moteur. Par exemple, je n'ai pas abordé les histoires de couches de symbole de type masque qui disposent d'un onglet dédié et qui permettent de masquer tout ou partie d'un symbole et ce, suivant des règles inter-couches.

Tout ça pour dire que QGIS c'est devenu très puissant avec le temps...