La première partie du projet Financement Collaboratif pour l’Intégration de Données Marines dans QGIS est arrivée à son terme grâce aux contributions d’ORANGE Marine, Geoconceptos Uruguay, Janez Avzec et Gaetan Mas. Nous les remercions vivement. Nous publions donc le contenu final de cette partie du travail.
Etant donné le volume du résultat, nous le publions en deux parties: la première concerne la création et la gestion de la base de données, la deuxième la symbologie S57 sous QGis.
Le format S57 est complexe et ne respecte pas totalement les standards des formats SIG. Sa conversion avec GDAL est donc soit complexe, soit incomplète. Dans cet article nous allons rentrer dans le détail de la transformation complète du S57 en tables de fichier Geopackage.
Les types de géométrie
Les « couches » S57 sont des classes d’objets. Par exemple, les différentes zones terrestres sont codées dans la classe d’objets LNDARE.
La définition de cette classe d’objets est:
Geo object: Land area (LNDARE) (P,L,A)
Attributes: CONDTN OBJNAM NOBJNM STATUS INFORM NINFOM
Si tous les objets LNDARE possèdent les mêmes attributs, il n’en est pas de même du type de géométrie. L’information (P,L,A) indique que, dans cette classe d’objets on peut trouver des points, des lignes et des polygones. Contrairement aux standards SIG, les trois types de géométrie coexistent au sein de la même classe d’objets.
Quel qu’il soit le format choisi pour l’intégrer dans QGis, nous aurons à créer une couche par type de géométrie. Si on ne le fait pas, GDAL va créer le type de couche en fonction de la première entité trouvée lors de la conversion. Si elle est de type point, la couche créée sera de type point et les entités lignes et polygones seront ignorées. De même, si la première est de type ligne, ce seront les points et les polygones qui seront ignorés.
Il faut aussi noter que le format S57 n’a aucune contrainte sur les classes d’objets: si dans la zone couverte par le fichier S57 il n’y a pas une classe d’objet, il n’y aura rien la concernant dans le fichier (pas de couche vide). De même, s’il n’y a qu’un type de géométrie présente, alors que les trois types sont possibles, il n’y aura pas de trace des autres types de géométrie.
Il faudra alors décomposer le traitement d’un fichier S57 avec ogr2ogr en trois étapes, une pour chaque type de géométrie. Les options suivantes permettent de traiter chaque classe d’objet S57 en ne sélectionnant qu’un type de géométrie :
-where « OGR_GEOMETRY=’POINT’ or OGR_GEOMETRY=’MULTIPOINT’ »
-where « OGR_GEOMETRY=’LINESTRING’ or OGR_GEOMETRY=’MULTILINESTRING’ »
-where « OGR_GEOMETRY=’POLYGON’ or OGR_GEOMETRY=’MULTIPOLYGON’ »
GDAL permet pour certains types de drivers de créer des préfixes dans les tables en sortie. Dans ce cas on pourrait créer toutes les tables (points, lignes, polygones) dans un seul geopackage, en les préfixant par exemple avec pt_, li_ et pl_. Le problème est que le driver S57 de GDAL ne permet pas cette option. On doit alors créer trois geopackages distincts, dans lesquels les tables seront créées selon le type de géométrie. Dans chaque geopackage il y aura alors une table de même nom mais de géométrie différente. Si cette solution est la plus simple, elle n’est pas la plus élégante et pratique. Nous allons utiliser ici trois geopackages d’import pour les commandes ogr2ogr dans lesquels nous allons importer les tables d’un fichier S57. Nous les appellerons pointsENC, LinesENC et PolysENC. D’autre part nous allons créer un geopackage unique appelé ENC pour la base de données complète. Pour chaque carte ENC nous allons importer son contenu dans les trois geopackages d’import avec ogr2ogr puis nous allons exécuter des scripts Python pour mettre à jour la base de données du geopackage ENC avec les nouvelles tables importées. Nous sommes obligés d’utiliser les scripts Python au lieu de requêtes et fonctions SQL car la version SQL de SQLite, utilisée pour les geopackages, est très limitée et ne permet pas la création de procédures ou fonctions.
Si vous avez un seul fichier S57 à importer le processus est le suivant:
- Création du geopackage ENC, vide.
- Chargement des classes d’objets Point dans le geopackage pointsENC avec ogr2ogr
- Chargement des classes d’objets Lignes dans le geopackage LinesENC avec ogr2ogr
- Chargement des classes d’objets Polygone dans le geopackage PolysENC avec ogr2ogr
- Suppression des tables vides des trois geopackages et ajout des attributs carte_enc et echelle..
- Clonage des tables des trois geopackages d’import dans le geopackage ENC, en préfixant les tables de pointsENC par pt_, de LinesENC par li_ et celles de PolysENC par pl_.
Si vous avez un lot de fichiers S57 à charger simultanément, le processus est le suivant:
- Chargement des classes d’objets Point dans le geopackage pointsENC avec ogr2ogr avec suppression des tables vides et ajout du nom du fichier enc et de l’échelle dans les attributs de toutes les tables.
- Chargement des classes d’objets Lignes dans le geopackage LinesENC avec ogr2ogr avec suppression des tables vides et ajout du nom du fichier enc et de l’échelle dans les attributs de toutes les tables.
- Chargement des classes d’objets Polygone dans le geopackage PolysENC avec ogr2ogr avec suppression des tables vides et ajout du nom du fichier enc et de l’échelle dans les attributs de toutes les tables.
- Mise à jour des tables existantes dans le geopackage ENC à partir des trois geopackages d’import ET
- Clonage des tables des trois geopackages d’import qui n’étaient pas présentes dans le geopackage ENC,
- Détection et effacement des doublons éventuels résultant de la mise à jour des tables du geopackage ENC (optionnel)
Le traitement des sondes bathymétriques
Les sondes bathymétriques posent plusieurs problèmes lors de la conversion de format. Le premier est que la valeur de profondeur n’est pas contenue dans un attribut, mais en tant que Z dans la géométrie (XYZ). Le deuxième c’est que les sondes ne sont pas du type Point mais Multipoint. Pour avoir directement la valeur des sondes dans un champ attributaire, il faut donc ajouter deux paramètres à la ligne de commande ogr2ogr:
Récupération des identifiants « parents »
Certains objets peuvent être regroupés par un objet non géographique parent. Par exemple, les secteurs de feu sont codés avec un secteur par enregistrement de la table « LIGHTS ». Les différents secteurs d’un même feu n’ont aucun attribut particulier qui puisse indiquer qu’ils correspondant à la même entité. Cette information se trouve dans un enregistrement « parent ». Pour récupérer cet identifiant en tant qu’attribut de la table LIGTHS, il faut ajouter une option à la ligne de commande:
Cette option crée un attribut name_rcid qui est commun à tous les secteurs d’un même feu, mais créé aussi une série de champs (name_rcnm,ORNT,USAG,MASK).
Le traitement des type « Liste »
Le format S57 utilise, en plus des types de champ classiques (entier, réel, texte) un type particulier: les listes, de chaînes de caractères ou d’entiers. Si on retrouve ces types de champ dans PostgreSQL, il n’en va pas de même dans les shapefiles et le geopackage.
En absence des options spécifiques, il y aura des messages d’avertissement indiquant que le format n’est pas supporté et le contenu de ces champs sera ignoré. Pour ne pas perdre le contenu, il faut ajouter dans la ligne de commande ogr2ogr l’option :
Cette option prend une liste {..,…} et génère une chaîne de texte avec un premier chiffre indiquant le nombre d’éléments de la liste, deux points, puis les éléments de la liste séparés par des virgules:
{3} -> ‘(1:3)’
{3,1} -> ‘(2:3,1)’
Forcément, ceci complique un peu le traitement de ces champs mais l’information n’est pas perdue. Par contre, le driver GDAL présente un problème pour la gestion du format String résultant.
Tout d’abord, pour ce qui est des attributs des objets S57 il n’y a aucun problème. Le champ de la table geopackage est de type ‘Text’, tout court, ce qui permet la création d’une chaîne de caractères de n’importe quelle longueur.
Par contre, pour les « identifiants parents », la définition utilisée par le driver est ‘Text()’, c’est à dire une chaîne de caractères avec une longueur maximum. Cette longueur est, a priori, déterminée par le contenu du premier enregistrement traité. C’est dire que ce n’est que du hasard si vous avez un champ avec la longueur nécessaire pour intégrer toutes les valeurs du fichier.
Vous verrez alors des messages du type:
WARNING : Value of field ‘NAME_RCID’ has 371 characters, whereas maximum allowed is 30
La solution n’est pas compliquée mais implique pas mal de temps de travail. Partez de la base que pour l’affichage classique des cartes ENC, ceci ne représente pas un problème et vous pouvez tout simplement ignorer ces messages d’avertissement.
Par contre si vous avez besoin de traitements plus poussés incluant ces informations, voici le seul moyen que j’ai trouvé, pour le moment:
Workflow pour le chargement d’un seul fichier S57
Création du geopackage ENC
Nous allons créer un geopackage vide qui recevra les tables importées, puis les mises à jour successives lors des chargements d’autres fichiers S57.
Dans le panneau Explorateur de QGis, faites un clic droit sur Geopackages et sélectionnez Créer une base de données
Dans la fenêtre qui s’ouvre, renseignez le nom du geopackage, laissez l’option par défaut pour le nom de la table et sélectionnez Pas de géométrie dans Type de géométrie.
Cliquez sur OK pour avoir le nouveau geopackage vide ajouté aux connexions geopackage de QGis. Il est conseillé d’effacer la table vide ENC qui est créée lors de la création du geopackage. Pour cela ouvrez le gestionnaire de base de données de QGis à partir du menu principal, positionnez-vous sur la table ENC du geopackage ENC.gpkg et avec un clic droit sélectionnez Effacer…
Commandes ogr2ogr pour créer les geopackages d’import
Fort de tous ces éléments, voici donc les trois lignes de commandes ogr2ogr pour créer les tables dans les geopackages d’import:
Vous devrez modifier le texte
C:/testgpkg/pointsENC.gpkg C:\testgpkg\US1GC09M\US1GC09M.000
par l’emplacement des geopackages d’import que vous souhaitez et par le chemin et nom de fichier du fichier S547 à charger.
Pour exécuter ces lignes de commande il suffit d’ouvrir la fenêtre de shell d’OSGeo4W:
La fenêtre de Shell s’ouvre
Rentrez alors les lignes de commande, en faisant attention à bien rentrer le chemin et nom de fichier avec l’extension .000 .
On aura alors comme résultat une série de tables créées dans chaque geopackage d’import. Par contre il y aura des tables complètement vides. En effet, si le type de géométrie est possible pour une classe d’objet, la table sera créée mais s’il n’y a aucune occurrence dans le fichier S57 traité, la table n’aura aucun enregistrement.
Utilisation des scripts Python pour la gestion de la base de données
Pour toutes les opérations de gestion de la base de données nous allons utiliser la console Python de QGis.
Pour l’ouvrir, allez dans le menu Extensions -> Console Python. Les panneaux de la console Python s’ouvrent:
Pour exécuter un script Python, chargez le code dans le panneau Éditeur. Vous pouvez faire un copier-coller ou bien utiliser l’icône Répertoire pour pointer sur le fichier .py. L’indentation étant cruciale pour les scripts Python, privilégiez cette deuxième option.
Suppression des tables vides et ajout du nom de la carte et de l’échelle
Comme nous l’avons vu plus haut, lors de l’exécution des commandes ogr2ogr, des tables vides sont créées pour les couches où il n’y a pas la géométrie demandée dans la ligne de commande. Nous allons donc supprimer ces tables vides et profiter du script Python pour ajouter deux attributs à toutes les tables: le nom de la carte ENC et l’échelle de celle-ci.
Pour ce qui est du nom, il peut être utile dans la maintenance ultérieure de la base de données de pouvoir sélectionner les entités correspondantes à un fichier source S57. Cette information n’est contenue dans aucun attribut du fichier S57. Nous devons donc la renseigner manuellement.
Il en va de même pour l’échelle de la carte. Si un attribut est bien prévu dans le S57 pour rentrer l’échelle de la carte « papier », l’attribut CSCALE dans la table M_CSCL, il est bien rare que cette table et son attribut soient présents dans les fichiers S57.
Si le nom de la carte est d’une utilité relative, il n’en va pas de même de l’échelle, car c’est un critère indispensable dans la recherche de doublons. En effet, lors du mélange des entités en provenance de plusieurs fichiers S57 il y aura une coexistence plus ou moins heureuse de données issues de différentes échelles.
Le code Python suivant permet de supprimer les tables vides d’un schéma d’import et d’ajouter deux attributs (enc_chart et scale) à toutes les tables:
Attention à respecter l’indentation du code Python. Pour simplifier la tâche, téléchargez directement le fichier .py avec ce lien: delete_empty_tables_in_geopackages.py.
Pour l’exécution, veillez à modifier les paramètres en entrée du script:
Les geopackage_paths vous n’aurez qu’à les rentrer que la première fois que vous exécuterez le script. Par contre le nom de la carte ENC et l’échelle seront à modifier lors de l’import de chaque nouveau fichier S57.
A l’issue de cette exécution vous aurez dans chaque table les nouveaux attributs:
Workflow pour le chargement de plusieurs fichiers S57 en même temps
Les lignes de commandes des paragraphes précédents assument que vous avez un seul fichier S57 à charger. Mais vous pouvez charger plusieurs fichiers simultanément. L’utilisation des options -append -update font que vous pouvez traiter en boucle tous les fichiers .000 d’un répertoire. Vous aurez ainsi tous les fichiers chargés dans les geopackages d’import.
Contrairement au chargement d’un seul fichier, lors du chargement d’un lot de fichiers il faut intégrer l’ajout du nom du fichier et de l’échelle à la boucle de transcodage S57->geopackage. Pour cela, nous utiliserons un script python qui ajoute les attributs enc_chart et scale aux tables créées par la commande ogr2ogr et qui les renseigne avec le nom du fichier correspondant et une échelle définie par le producteur. Le script efface aussi les tables vides présentes dans le geopackage d’import.
Merci à Janez AVSEC pour l’idée d’utiliser la table DSID pour récupérer l’échelle de compilation des données.
Les fichiers .bat doivent être exécutés à partir d’une fenêtre OSGeo4W pour pouvoir bénéficier de l’environnement Python de QGis.
L’appel de chaque fichier .bat est de la forme:
.\fichier.bat « répertoire contenant les fichiers S57 à charger » « nom du geopackage d’import »
Par exemple:
.\mass_load_s57_polys.bat « c:/enc_db/enc000 » « c:/enc_db/polys.gpkg »
Les fichiers .bat pour réaliser cette tâche sont les suivants:
Pour importer les tables de type point:
Vous remarquerez que, en plus de la commande ogr2ogr pour importer les couches de points, on importe deux tables sans géométrie: DSID et C_AGGR.
Pour importer les tables de type ligne:
Pour importer les tables de type polygone:
Les fichiers .bat contiennent la commande ogr2ogr ainsi qu’une ligne pour exécuter un script Python, update_geopackage_dsid.py. Faites attention à modifier le chemin de ce script dans le fichier .bat pour qu’il corresponde à celui de votre choix. Ce script ajoute à toutes les tables du geopackage les deux attributs enc_chart et scale.
Le code python utilisé est le suivant:
On pourrait faire un seul fichier.bat pour les trois traitements, mais il convient de bien vérifier le résultat de chacun des types de géométrie.
Vous pouvez télécharger les fichiers .bat et le code Python en cliquant ici.
Suite quel qu’il soit le nombre de fichiers chargés
Clonage et/ou ajout des tables importées dans le geopackage ENC
Dans le cas du premier chargement S57, le script Python suivant créé toutes les tables présentes dans les trois geopackages d’import. Pour les chargements ultérieurs, si la table existe déjà dans le geopackage ENC, les enregistrements de la table du geopackage d’import sont ajoutés aux enregistrements déjà présents dans ENC. Si, par contre, la table n’existe pas encore, elle est créée et alimentée par les enregistrements du geopackage d’import. Dans le fichier geopackage ENC les tables sont préfixées par pt_, li_ et pl_ pour indiquer leur type de géométrie.
Le clonage est réalisé avec le code suivant:
Veillez à modifier la ligne
input_geopackages = [« c:/testgpkg/pointsENC.gpkg », « c:/testgpkg/LinesENC.gpkg », « c:/testgpkg/PolysENC.gpkg »]
avec les geopackages d’import que vous avez choisi, ainsi que la ligne
output_gpkg = ogr.Open(« c:/testgpkg/ENC.gpkg », 1)
avec le geopackage de base de données de votre choix.
Vous pouvez télécharger le fichier .py avec ce lien: clone_or_append_tables_with_prefix.py.
Gestion des doublons
Quand deux cartes ENC se superposent, les entités présentes dans la zone de superposition vont être insérées autant de fois que de superpositions il y en a. Il faut donc, pour éviter des doublons et plus, nettoyer la base de données après chaque import de fichier S57.
Mais il y a deux types de doublons différents. D’une part, les zones de recouvrement de deux cartes d’échelle similaire vont produire de « vrais » doublons qu’il faudra nettoyer. Mais la superposition de cartes d’échelle très différente est àç traiter différemment. La carte moins détaillé (petite échelle) aura une sélection des données de la carte plus détaillé (grande échelle). Dans ce deuxième cas il n’est pas conseillé de supprimer cette information.
Le code Python ci-après va supprimer les « vrais » doublons. Pour ce qui est des autres doublons, il est conseillé de travailler avec des filtres QGis. Selon vos besoins, vous pourrez définir, grâce à l’attribut « scale » que nous avons ajouté lors de la suppression des tables vides, quelles sont les données affichées ou pas sur votre carte.
Dans cet exemple, seules les entités correspondantes à une échelle comprise entre 12500 et 45000 seront affichées sur votre carte.
Cette opération correspond au filtrage d’une couche, mais… que faire quand vous en avez 130 couches affichées? Pour vous éviter de faire 130 fois cette opération, voici un code Python qui vous permet de filtrer toutes les couches du projet:
Et un autre pour enlever le filtre à toutes les couches affichées:
Télécharger les codes python ici.
Le code suivant crée, pour chaque table du geopackage ENC, une table temporaire (temp_table
). Puis il remplit cette table avec les enregistrements distincts basés sur LNAM. Ensuite, il supprime la table d’origine et renomme la table temporaire avec le nom de la table d’origine.
Ce traitement s’applique à toutes les tables S57, sauf:
- aux tables des feux (LIGHTS) et des sondes bathymétriques (SOUNDG) où l’attribut LNAM n’est pas utile. En effet, chaque secteur de feu a un même LNAM et
name_rcid
mais correspond à une couleur est un secteur géographique différents. De même, le LNAM etname_rcid
des sondes bathymétriques est le même pour tous les points de sondes de l’entité multipoint d’origine. Dans le premier cas nous devons chercher les couleurs et secteurs en double pour un mêmename_rcid
, et pour les sondes nous devons trouver les sondes ayant les mêmes XY dans leur géométrie. - aux tables SEAARE, LNDRGN et ADMARE qui couvrent des vastes zones et qui sont découpées selon l’emprise de la carte, tout en gardant le même LNAM. Dans ce cas même si le LNAM est le même, les polygones n’ont pas la même forme et surface.
- les tables DSID et C_AGGR qui n’ont pas de LNAM et qui, par prinipe, ne peuvent pas avoir des doublons.
Veillez à modifier la ligne
geopackage_path = « c:/testgpkg/ENC.gpkg »
avec votre geopackage de base de données.
Vous pouvez télécharger le fichier .py à partir de ce lien : supprime_doublons.py. N’oubliez pas de modifier les chemins et noms de fichiers pour qu’ils correspondent aux vôtres. Remarquez aussi la ligne :
if table_name not in [« pl_SEAARE », »pl_LNDRGN », »pl_ADMARE », »layer_styles », »natsurf », »DSID », »C_AGGR »]:
En principe, vous aurez que les tables préfixées et les tables layer_styles et natsurf. Cette ligne permet de ne pas traiter pl_SEAARE, pl_LNDRGN et pl_ADMARE, mais surtout, elle empêche de vider les tables layer_styles et natsurf que nous utilisons pour les symbologies. Si vous êtes amené à créer d’autres tables que celles issues du fichier s57 dans le geopackage, il faut prendre en compte les noms de ces tables dans ce script, en les ajoutant à cette ligne d’exceptions.