Symbologie avancée sous QGIS 3🔗
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:
- Les balises NDB (le truc bleu avec les motifs):
- Les balises VOR (le cercle avec les graduations + le symbole au centre):
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, à savoir, le cercle et le point central.
- le motif de points.
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:
- Pour le point central, j'utilise un marqueur de type cercle/circle, avec remplissage sur la couleur de référence et une taille de 250 mètres à l'échelle (meters at scale).
- Pour le cercle en dehors du point central, j'utilise le même marqueur que le point avec la même couleur mais sans remplissage et avec une taille de 1500 mètres.
En termes de représentation, ça donne ça:
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:
- On projette la géométrie de base (
$geometry
) de WGS84 à EPSG:3857, pour obtenir une représentation parfaite dans la projection de la carte, sinon, on a des décalages. C'est aussi pratique parce que la projection EPSG:4326 a une unité de distance en angle alors que EPSG:3857 est en mètres. - Ensuite, avec ce point re-projeté, on effectue un déplacement à une distance donnée (1750m ou 2250 ou 1750, suivant la série), le tout réparti sur un azimut qui varie suivant la position dans la série. C'est la fonction project. Dans notre cas, et comme nous avons une série, ça permet de répartir les points déplacés autour d'un cercle.
- On re-projette tous les points de EPSG:3857 vers la projection de la couche.
- Le nombre de points varie suivant la série. On a des séries de 16, 22 ou 32 points.
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:
Puis le symbole 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:
- le symbole central hexagonal.
- le cercle autour du centre.
- Les graduations de 10° en 10° et de 30° en 30°
- les 3 flèches des points cardinaux.
- la flèche du nord.
On va déjà se débarrasser du trivial, à savoir, le symbole de base qui est composé de:
- un point central, comme pour la balise NDB.
- un marqueur de type hexagone, de taille 2900 mètres à l'échelle et d'une rotation de 90°.
Ça donne ça à l'écran:
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:
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:
- les 4 flèches cardinales (angles 0, 90, 180 et 270).
- tous les 30 degrés, la ligne est plus longue.
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:
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:
- flèche de type unique/inversée (j'aurais pu inverser le sens de la ligne dans le code aussi).
- flèche pleine.
- toutes les valeurs de mesure à 0 unité sauf…
- …la largeur de la flèche au départ de 1250m à l'échelle.
- le tout avec la couleur officielle.
Au final, ça donne ça:
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:
- largeur de la flèche: 250m à l'échelle.
- largeur de la flèche au départ: 250m à l'échelle.
- longueur de la tête: 2750m à l'échelle.
- épaisseur de la tête: 750m à l'échelle.
Au final, ça donne ça:
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…