|
| 1 | +--- |
| 2 | +title: "OSM Data : des données SIG jusqu'au serveur cartographique" |
| 3 | +subtitle: OSM Data 2/5 |
| 4 | +authors: |
| 5 | + - Karl TAYOU |
| 6 | + - Romain LATAPIE |
| 7 | +categories: |
| 8 | + - article |
| 9 | +comments: true |
| 10 | +date: 2025-03-10 |
| 11 | +description: "OSM DATA 3D : mécanismes d'ingestion de données jusqu'à leur diffusion en flux WFS/WMS." |
| 12 | +icon: material/database-import-outline |
| 13 | +image: https://cdn.geotribu.fr/img/articles-blog-rdp/articles/2025/osm_data/article_2/parcours_des_donnees_avant_visualisation_400.png |
| 14 | +license: default |
| 15 | +robots: index, follow |
| 16 | +tags: |
| 17 | + - OpenStreetMap |
| 18 | + - QGIS |
| 19 | + - QGIS SERVER |
| 20 | + - WFS |
| 21 | + - WMS |
| 22 | + - PostgreSQL |
| 23 | + - GeoPandas |
| 24 | + - SQLAlchemy |
| 25 | + - PyQGIS |
| 26 | +--- |
| 27 | + |
| 28 | +# OSM DATA V2 : Des données à la cartographie |
| 29 | + |
| 30 | +:calendar: Date de publication initiale : {{ page.meta.date | date_localized }} |
| 31 | + |
| 32 | +## Introduction |
| 33 | + |
| 34 | +Dans l'article précédent, nous avons présenté les différentes possibilités d'ajout de données à [OSM DATA](https://demo.openstreetmap.fr/map?profil=1&layers=8,15,layer;3,15,layer;253,11,layer;396,11,layer;36,11,layer;519,16,layer&pos=259312,6253359,260.6,258894.7,6253800.6,0). L'objet de cet article est d'expliciter techniquement les mécanismes mis en place pour ajouter les données. |
| 35 | + |
| 36 | +Pour rappel, quatre étapes principales permettent l'affichage des données : |
| 37 | + |
| 38 | +1. La définition puis la validation de la conformité des fichiers / requêtes SQL |
| 39 | +2. L'intégration en base de données des données sous forme de table ou de vues matérialisées |
| 40 | +3. La création d'un projet QGIS et la définition de la symbologie associée à chaque couche |
| 41 | +4. La création des flux WMS et WFS à l'aide des projets QGIS créé |
| 42 | + |
| 43 | +{: .img-center loading=lazy } |
| 44 | + |
| 45 | +## Définition et validation de la conformité des fichiers / requêtes SQL |
| 46 | + |
| 47 | +Dans l'article précédent, le module d'ajout des données est brièvement présenté, une fonction (optionnelle) de définition du type de géométrie est aussi incluse. Pour s'assurer d'une intégration dans les meilleures conditions, un seul type de géométrie est considéré pour chaque ajout. |
| 48 | + |
| 49 | +{: .img-center loading=lazy } |
| 50 | + |
| 51 | +### A partir d'un fichier SIG |
| 52 | + |
| 53 | +Pour l'ajout de fichiers SIG avec GeoPandas, la première étape est de créer un `GeoDataFrame` à partir des données : |
| 54 | + |
| 55 | +```python title="Import d'un fichier SIG avec GeoPandas" |
| 56 | +# Importation du fichier SIG |
| 57 | +import geopandas |
| 58 | +gpdSource: geopandas.GeoDataFrame = geopandas.read_file(self.file) |
| 59 | +``` |
| 60 | + |
| 61 | +Une fois le fichier interprété avec succès et le `GeoDataFrame` créé, deux étapes de validation sont réalisées : |
| 62 | + |
| 63 | +- Présence d'un seul type de géométrie : |
| 64 | + |
| 65 | + ```python title="Vérification du nombre de types de géométries" |
| 66 | + # Vérification du nombre de types de géométries |
| 67 | + if len(gpdSource.geom_type.unique()) > 1 : |
| 68 | + raise Exception("Votre fichier contient plusieurs types de géométries :(") |
| 69 | + ``` |
| 70 | + |
| 71 | +Dans le cadre d'une mise à jour du jeu de données, on vérifie également la conformité du type de géométrie entre les données sources et les données de mise à jour. Si ce n'est pas le cas, le jeu de données de mise à jour est considéré invalide (sa définition/symbologie dans QGIS dépendant du type de géométrie du jeu de données source). |
| 72 | + |
| 73 | +- Présence d'entités au sein du fichier : |
| 74 | + |
| 75 | + ```python title="Vérification que le fichier n'est pas vide" |
| 76 | + # Vérification que le fichier n'est pas vide |
| 77 | + if gpdSource.empty is False : |
| 78 | + raise Exception("Votre fichier ne contient aucune donnée :(") |
| 79 | + ``` |
| 80 | + |
| 81 | +Une fois ces vérification effectuées, on prépare la connexion à la base de données avec `SQLAlchemy` : |
| 82 | + |
| 83 | +```python title="Connexion à la base de données avec SQLAlchemy" |
| 84 | +# Connexion à la base de données |
| 85 | +from sqlalchemy import create_engine |
| 86 | +engine = create_engine("postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}") |
| 87 | +``` |
| 88 | + |
| 89 | +Enfin, l'importation en base de données est réalisé à l'aide de la commande `to_postgis` : |
| 90 | + |
| 91 | +```python title="Intégration d'un GeoDataFrame en base de données" |
| 92 | +# Intégration en base de données |
| 93 | +gpdSource.to_postgis( |
| 94 | + name="nom_de_notre_table", |
| 95 | + con=engine, |
| 96 | + index=True, |
| 97 | + index_label="id", |
| 98 | +) |
| 99 | +``` |
| 100 | + |
| 101 | +La couche est insérée en base de données ! :fireworks: |
| 102 | + |
| 103 | +### À partir d'une requête SQL |
| 104 | + |
| 105 | +En ce qui concerne l'ajout d'un jeu de données à partir d'une requête SQL, on considère qu'une base de données est fournie avec des données d'OpenStreetMap par le biais d'osm2pgsql, un article détaille la procédure [ici](../2022/2022-06-28_import-donnees-osm-postgresql-osm2pgsql-osmium.md). |
| 106 | + |
| 107 | +On possède donc les trois principales tables de données d'OpenStreetMap à savoir `planet_osm_point`, `planet_osm_line` et `planet_osm_polygon`. |
| 108 | + |
| 109 | +La définition d'une requête SQL sur les données OpenStreetMap est simplifiée : seule la clause de restriction (`WHERE`) de la requête peut être définie. |
| 110 | +Ainsi, un utilisateur qui ne maitrise pas le SQL ou le schéma de la base d'OSM peut définir de nouvelles couches en s'appuyant uniquement sur le [Wiki d'OpenStreetMap (exemple des stations de métro)](https://wiki.openstreetmap.org/wiki/Tag:station%3Dsubway). La majorité des 350 couches présentes aujourd'hui a été créée à l'aide de cette fonctionnalité. |
| 111 | + |
| 112 | +L'utilisateur peut aussi éditer la clause de projection (`SELECT`), ci-dessous un exemple de sélection des métros dans la base de données OSM. Pour cette clause, en plus de ce que l'utilisateur a défini, d'autres champs sont automatiquement ajoutés : `geom`, `osm_id`, `name` et `tags`; Le dernier champ `tags` regroupent les autres attributs d'OpenStreetMap disponibles sous forme de [`hstore`](https://www.postgresql.org/docs/current/hstore.html). |
| 113 | + |
| 114 | +{: .img-center loading=lazy } |
| 115 | + |
| 116 | +La clause source (`FROM`) dépend elle du type de géométrie défini lors de la création du jeu de données : |
| 117 | + |
| 118 | +- Pour les géométrie de type `Point`, la sélection est réalisée sur l'union des tables `planet_osm_point` et les centroïdes issus de la table `planet_osm_polygon`. |
| 119 | +- Pour les géométrie de type `Polyline`, seule la table `planet_osm_line` est considérée. |
| 120 | +- Pour les géométrie de type `Polygon`, seule la table `planet_osm_polygon` est considérée. |
| 121 | + |
| 122 | +Une fois les clauses définies, un assemblage de celles-ci permet de former la requête entière. Pour l' exemple des métros : |
| 123 | + |
| 124 | +```sql title="Requête entière pour les métros" |
| 125 | +SELECT |
| 126 | + A.osm_id, |
| 127 | + A.name, |
| 128 | + hstore_to_json(A.tags), |
| 129 | + ST_TRANSFORM(A.way,4326) as geom, |
| 130 | + tags->'wheelchair' = 'yes' as "has_wheelchair" |
| 131 | +FROM planet_osm_point as A |
| 132 | +WHERE ( A.railway='station' AND A.tags->'station'='subway' ) |
| 133 | +UNION ALL |
| 134 | +SELECT |
| 135 | + B.osm_id, |
| 136 | + B.name, |
| 137 | + hstore_to_json(B.tags), |
| 138 | + ST_TRANSFORM(st_centroid(B.way),4326) as geom, |
| 139 | + tags->'wheelchair' = 'yes' as "has_wheelchair" |
| 140 | +FROM planet_osm_polygon as B |
| 141 | +WHERE ( B.railway='station' AND B.tags->'station'='subway' ) |
| 142 | +``` |
| 143 | + |
| 144 | +Pour valider la requête SQL, il suffit de vérifier que son exécution ne génère pas d'erreurs ! Pour rapidement connaitre la conformité de la requête, son exécution n'est pas effectuée sur toute la base de données, on limite son exécution à une seule entité par l'ajout de la contrainte `LIMIT 1` en fin de script. La requête de validation est donc : |
| 145 | + |
| 146 | +```sql title="Validation d'une requête SQL" |
| 147 | +SELECT |
| 148 | + A.osm_id, |
| 149 | + A.name, |
| 150 | + hstore_to_json(A.tags), |
| 151 | + ST_TRANSFORM(A.way,4326) as geom, |
| 152 | + tags->'wheelchair' = 'yes' as "has_wheelchair" |
| 153 | +FROM planet_osm_point as A |
| 154 | +WHERE ( A.railway='station' AND A.tags->'station'='subway' ) |
| 155 | +UNION ALL |
| 156 | +SELECT |
| 157 | + B.osm_id, |
| 158 | + B.name, |
| 159 | + hstore_to_json(B.tags), |
| 160 | + ST_TRANSFORM(st_centroid(B.way),4326) as geom, |
| 161 | + tags->'wheelchair' = 'yes' as "has_wheelchair" |
| 162 | +FROM planet_osm_polygon as B |
| 163 | +WHERE ( B.railway='station' AND B.tags->'station'='subway' ) |
| 164 | +LIMIT 1 |
| 165 | +``` |
| 166 | + |
| 167 | +En l'absence d'erreurs, une vue matérialisée est créée dans PostgreSQL. Les données d'une vue sont toujours à jour, car elles sont directement dérivées des tables sources chaque fois qu'on y accède. Les vues matérialisées offrent un mécanisme puissant pour améliorer les performances des requêtes en pré-calculant et en stockant le jeu de résultats d'une requête sous forme de table physique. |
| 168 | + |
| 169 | +Pour OSM DATA, les requêtes peuvent faire appel à des milliers d'entités, résultant de plusieurs tables sous-jacentes, des conditions sur des champs indexés (ou non) et qui sont mises à jour une seule fois par jour. Pour toutes ces raisons, les vues matérialisées semblent être l'option la plus approriée pour notre solution. |
| 170 | + |
| 171 | +A l'aide de la requête d'assemblage créée, la vue matérialisée est créée de la manière suivante : |
| 172 | + |
| 173 | +```sql title="Création de la vue matérialisée" |
| 174 | +CREATE MATERIALIZED VIEW {NOM_DE_LA_VUE} AS ({REQUETE_SQL_ENTIERE}) |
| 175 | +``` |
| 176 | + |
| 177 | +Pour information, l'utilisateur PostgreSQL exécutant la requête SQL détient uniquement des droits de création de tables ou de vues dans certains schémas de la base de données afin d'éviter les mauvaises surprises. |
| 178 | + |
| 179 | +La couche est insérée en base de données ! :fireworks: |
| 180 | + |
| 181 | +## Création d'un projet QGIS et de la symbologie associée au jeu de données |
| 182 | + |
| 183 | +### Création d'un projet |
| 184 | + |
| 185 | +La création d'un projet QGIS permet, à partir des données stockées dans la base de données PostgreSQL et de QGIS Server, de créer les flux WMS/WFS nécessaires à la visualisation des couches sur le web. |
| 186 | + |
| 187 | + **Pourquoi QGIS et QGIS Server ? Pourquoi ne pas avoir utilisé Mapserver ou Geoserver ? En deux mots : interopérabilité et efficacité**. QGIS :heart: dispose d'un moteur de style puissant et dont les capacités ne cessent de s'étoffer. Couplé à QGIS Server, la visualisation avec symbologie synchronisée *desktop*/*web* permet de créer rapidement et interactivement des symbologies, [Mathieu Rajerison](https://x.com/datagistips?s=21) a par exemple mis en place différents styles : |
| 188 | + |
| 189 | +- Les lampadaires sont discriminés en fonction de leur type de mât, du nombre de sources lumineuses, de leurs intensités... |
| 190 | + |
| 191 | +{: .img-center loading=lazy } |
| 192 | + |
| 193 | +- Les fontaines à eau sont représentées dépendammant du caractère potable ou non de l'eau |
| 194 | + |
| 195 | +{: .img-center loading=lazy } |
| 196 | + |
| 197 | +Techniquement, c'est beau, très beau, trop beau... et vous n'êtes certainement pas amoureux de [Brad Pitt](https://www.marieclaire.fr/anne-escroquee-par-un-faux-brad-pitt-un-porte-parole-de-l-acteur-s-exprime-apres-l-arnaque-de-830-000-euros,1487356.asp) donc oui il y a quelques contraintes ! |
| 198 | + |
| 199 | +QGIS est avant tout un logiciel *desktop* donc en l'ouvrant, il initialise par défaut son environnement avec l'ensemble des dépendances qu'il juge nécessaires à une utilisation *desktop*. Compte tenu de notre utilisation, certaines contraintes sont soient superflues, soient limitantes en termes de performance, il est donc nécessaire de paramétrer les variables d'environnement afin de désactiver le lancement de certaines fonctionnalités (voir ci-dessous *Diffusion des flux OGC WMS/WFS)*). |
| 200 | + |
| 201 | +Aussi, si on considère le lancement d'un projet ne contenant qu'une couche, l'initialisation peut être rapide, lorsque le projet dispose de 350 couches, c'est moins évident. |
| 202 | + |
| 203 | +{: .img-center loading=lazy } |
| 204 | + |
| 205 | +Pour ces raisons et sans avoir encore eu l'occasion de faire une analyse de l'outil [PerfSuite](https://github.com/qgis/QGIS-Server-PerfSuite/tree/master?tab=readme-ov-file), il a été décidé de répartir les couches entre plusieurs projets QGIS avec un maximum de 5 couches par projet. Sur OSM DATA, il y a actuellement 139 projets répertoriés : |
| 206 | + |
| 207 | +```sh |
| 208 | +# Décompte des projets QGIS d'OSM DATA |
| 209 | +debian@osm_data:provider/qgis/project$ ls | grep '\.qgs$' | wc -l |
| 210 | +139 |
| 211 | +# Liste des projets QGIS d'OSM DATA |
| 212 | +debian@osm_data:provider/qgis/project$ ls | grep '\.qgs$' |
| 213 | +projet_0.qgs |
| 214 | +projet_1.qgs |
| 215 | +projet_10.qgs |
| 216 | +projet_100.qgs |
| 217 | +... |
| 218 | +``` |
| 219 | + |
| 220 | +Pour créer un projet QGIS relié à un jeu de données, il est donc nécessaire d'établir une nomenclature structurée dépendante de la contrainte de "5 jeux de données maximum par projet". Ainsi, pour lancer la création d'un projet associé à un nouveau jeu de données, voici le script utilisé : |
| 221 | + |
| 222 | +```python title="Création d'un projet QGIS avec PyQGIS" |
| 223 | +from qgis.core import QgsProject |
| 224 | + |
| 225 | +# Nomenclature du projet QGIS |
| 226 | +path_to_qgis_project = "projet" + "_" + str(int({nombre_total_de_couches_existantes} / 5)) + ".qgs" |
| 227 | + |
| 228 | +# Création du projet QGIS |
| 229 | +project = QgsProject() |
| 230 | +project.read(path_to_qgis_project) |
| 231 | +project.write() |
| 232 | +``` |
| 233 | + |
| 234 | +Si le projet existe déja il est utilisé, et s'il n'existe pas, le projet est créé. |
| 235 | + |
| 236 | +Une fois le projet QGIS sélectionné/créé, on ajoute le nouveau jeu de données avec pour source la table ou la vue matérialisée précédement créée : |
| 237 | + |
| 238 | +```python title="Création d'une couche dans un projet QGIS" |
| 239 | +from qgis.core import QgsProject, QgsDataSourceUri |
| 240 | + |
| 241 | +# Création de la connexion à la base de données |
| 242 | +uri = QgsDataSourceUri() |
| 243 | +uri.setConnection(host, port, database, user, password) |
| 244 | + |
| 245 | +# Création de la source de données avec le nom de la table ou la vue, le schéma, le champ de géométrie et celui de la clé primaire |
| 246 | +uri.setDataSource({schema_de_la_table_ou_vue}, {table_ou_la_vue}, {champ_de_geometrie}, "", {cle_primaire}) |
| 247 | + |
| 248 | +# Création d'une couche de type vecteur avec la source définie |
| 249 | +vector_layer = QgsVectorLayer(uri.uri(False), {layer_name}, "postgres") |
| 250 | + |
| 251 | +# Validation d'accès à la table / vue et ajout de la couche au projet |
| 252 | +if vector_layer.isValid(): |
| 253 | + project.addMapLayer(vector_layer) |
| 254 | + |
| 255 | +# Pour que la couche soit disponible en WFS, ajout de celle-ci dans la balise WFSLayers. |
| 256 | +project.writeEntry("WFSLayers", "", [vector_layer.id()]) |
| 257 | +project.writeEntry("WMSAddWktGeometry", "", "true") |
| 258 | + |
| 259 | +# Sauvegarde du projet |
| 260 | +project.write() |
| 261 | +``` |
| 262 | + |
| 263 | +Une fois le projet QGIS créé, les données peuvent déjà être diffusées sous forme de flux WMS/WFS avec QGIS Server ! Cependant, afin d'améliorer leur visualisation, le style doit être défini. |
| 264 | + |
| 265 | +### Définition et application du style par l'utilisateur |
| 266 | + |
| 267 | +Dans la fenêtre de définition d'une couche sur OSM DATA, un onglet Styles permet de définir une ou plusieurs symbologies pour chaque jeu de données : |
| 268 | + |
| 269 | +{: .img-center loading=lazy } |
| 270 | + |
| 271 | +Cette caractéristique multi-styles découle de la fonctionnalité déjà présente au sein de QGIS. Les différentes manières de définir un style dans OSM DATA permettent de faciliter l'administration des jeux de données depuis l'interface, deux options sont disponibles : |
| 272 | + |
| 273 | +- A l'aide d'un fichier QML directement préparé à partir de QGIS |
| 274 | + |
| 275 | +{: .img-center loading=lazy } |
| 276 | + |
| 277 | +- A l'aide du moteur de style intégré d'OSM DATA : |
| 278 | + - Sous la forme d'un icone ponctuel : L'utilisateur fournit un icône (raster ou vecteur), un style est créé avec [`QgsSingleSymbolRenderer`](https://api.qgis.org/api/classQgsSingleSymbolRenderer.html) et PyQGIS pour l'appliquer au jeu de données. Le détail de l'implémentation est disponible sur le [GitHub](https://github.com/data-osm/geosm-backend/blob/master/provider/qgis/customStyle/point_icon_simple.py#L24) du projet. |
| 279 | + - Sous la forme d'un regroupement de point (*cluster*) : L'utilisateur fournit un icône, un style est créé avec [`QgsPointClusterRenderer`](https://api.qgis.org/api/classQgsPointClusterRenderer.html) pour l'appliquer au jeu de données. Le détail de l'implémentation est disponible sur le [GitHub](https://github.com/data-osm/geosm-backend/blob/master/provider/qgis/customStyle/cluster.py#L24) du projet. |
| 280 | + |
| 281 | +{: .img-center loading=lazy } |
| 282 | + |
| 283 | +Au besoin, un dernier article peut compléter cette série pour expliciter davantage la création de styles avec PyQGIS. |
| 284 | + |
| 285 | +## Diffusion des flux OGC WMS/WFS |
| 286 | + |
| 287 | +Pour utiliser QGIS Server, rien de plus simple ! Il suffit d'enregistrer le projet QGIS dans un dossier et [cet article](../2010/2010-09-03_creer_diffuser_services_wms_avec_qgis.md) détaille les étapes d'exploitation de ce dossier pour créer les flux OGC. |
| 288 | + |
| 289 | +Cependant, nous avons évoqué plus haut qu'à ce jour 139 projets QGIS sont présents. Une seule instance QGIS ne peut pas gérer l'ensemble de ces données de manière efficace. Pour cela, [py-qgis-server](https://github.com/3liz/py-qgis-server) est utilisé, il permet de définir plusieurs instances sur plusieurs *workers*, améliorant ainsi les performances. De plus certaines variables d'environnement QGIS sont directement exposées, voici celles qui sont actuellement activées sur OSM DATA lors de l'initialisation d'un projet : |
| 290 | + |
| 291 | +- Ignorance des composeurs d'impression du projet |
| 292 | +- Ignorance de la validité des couches |
| 293 | + |
| 294 | +Après avoir exploré le mécanisme d’ingestion et de diffusion des données par OSM DATA, nous pouvons désormais nous intéresser à ses fonctionnalités récentes, notamment la visualisation des données en 3D. Ce sera l’objectif du prochain article. |
| 295 | + |
| 296 | +[1 : Introduction à OSM Data 3D :fontawesome-solid-forward-step:](./2025-03-03_osm-data-3D-01-introduction.md "Introduction à OSM Data 3D"){: .md-button } |
| 297 | +{: align=middle } |
| 298 | + |
| 299 | +<!-- geotribu:authors-block --> |
| 300 | + |
| 301 | +{% include "licenses/default.md" %} |
0 commit comments