Tools for ENC marine map projects in QGis

This article completes the series of articles on creating a geopackage database to manage ENC nautical charts in QGis and the symbology of the various data layers. Working with a small number of maps presents no particular problems, but as soon as the number increases there is a series of operations that become either complicated or time-consuming.
We have developed a series of Python scripts that simplify work on large geopackage databases.

1- Loading layers

The first problem is the large number of layers to be loaded into the QGis project. While one map may have around twenty layers, as soon as the number of maps increases you can quickly find yourself with over a hundred layers to load.

QGis allows you to load all the layers in a geopackage en bloc. So you don’t have to load the hundred or so layers one by one. But once they are loaded, there are two tasks to perform:

  • remove irrelevant layers such as metadata layers
  • order the display of layers to prevent certain layers from being completely invisible due to overlapping with other layers. Surface layers such as DEPARE and LNDARE (depths and terrain) must always be below marine and terrestrial objects, otherwise these objects are not visible.

What’s more, it’s always more pleasant to work with minimum display scales, so that only certain layers are shown when zooming in at very small scales, and detailed information is only displayed when zooming in at larger scales.

Selecting layers to load

We have provided a python script that solves all these problems on its own. You can download it from this link.

The script includes a list of 210 S57 layers, ordered for correct display:

Surface layers represented by solid polygons, then surface layers represented by empty polygons (only the perimeters are displayed), then linear type layers and finally point type layers. Within each category, the order has been designed to avoid masking information.

Each layer is accompanied by a minimum display scale value, the default being 100000000.

Here’s an example:

#Liste des couches dans l’ordre de chargement
couches_a_charger = [
(‘pl_DEPARE’, 100000000),
(‘pl_UNSARE’, 100000000),
(‘pl_TIDEWY’, 100000000),
(‘pl_DAMCON’, 100000000),
(‘pl_CAUSWY’, 100000000),
(‘pl_HULKES’, 100000000),
(‘pl_LOKBSN’, 100000000),
(‘pl_OBSTRN’, 100000000),
(‘pl_PONTON’, 100000000),
(‘pl_PYLONS’, 100000000),

If you don’t want the script to load layers you’re not interested in, you can simply comment out the corresponding line by adding a ‘#’ at the beginning of the line:

#Liste des couches dans l’ordre de chargement
couches_a_charger = [
(‘pl_DEPARE’, 100000000),
(‘pl_UNSARE’, 100000000),
#(‘pl_TIDEWY’, 100000000),
#(‘pl_DAMCON’, 100000000),

In this example, the TIDEWY and DAMCON layers will not be loaded.

In addition, if you wish to define a minimum display scale for a layer, simply modify the corresponding value:

‘pl_DEPARE’, 100000000),
(‘pl_UNSARE’, 100000000),
(‘pl_TIDEWY’, 100000),
#(‘pl_DAMCON’, 100000000),
#(‘pl_CAUSWY’, 100000000),
(‘pl_HULKES’, 100000000),
(‘pl_LOKBSN’, 100000000),
(‘pl_OBSTRN’, 50000),

In this example, the TIDEWY layer will only be displayed when the map window zoom value is below 100000, and the OBSTRN layer when the value is below 50000.

To use the script:

  • download the .py file,
  • save it in a directory of your choice
  • Open the QGis Python console (Extensions ->Python console)

  • Open the Editor window (1)
  • Click on the Browse icon (2) and point to the downloaded .py file
  • Change the geopackage path to the path and name of your geopackage (3)
  • Run the script (4)

Before running the script, you can make modifications to prevent layers from being loaded or to assign them a different minimum scale. Don’t forget to save these modifications before closing your project.

The script code is as follows:

load_layers.py

from qgis.core import QgsProject

# Chemin vers le geopackage
chemin_geopackage = 'c:/testgpkgV3/ENC2.gpkg'

## Liste des couches dans l'ordre de chargement
couches_a_charger = [
    ('pl_DEPARE', 100000000),
    ('pl_UNSARE', 100000000),
    ('pl_TIDEWY', 100000000),
    ('pl_DAMCON', 100000000),
    ('pl_CAUSWY', 100000000),
    ('pl_HULKES', 100000000),
    ('pl_LOKBSN', 100000000),
    ('pl_OBSTRN', 100000000),
    ('pl_PONTON', 100000000),
    ('pl_PYLONS', 100000000),
    ('pl_SBDARE', 100000000),
    ('pl_DRGARE', 100000000),
    ('pl_TSEZNE', 100000000),
    ('pl_WRECKS', 100000000),
    ('pl_FLODOC', 100000000),
    ('pl_LNDARE', 100000000),
    ('pl_CANALS', 100000000),
    ('pl_LAKARE', 100000000),
    ('pl_RIVERS', 100000000),
    ('pl_BUAARE', 100000000),
    ('pl_BUISLG', 100000000),
    ('pl_CHKPNT', 100000000),
    ('pl_CONVYR', 100000000),
    ('pl_DOCARE', 100000000),
    ('pl_ROADWY', 100000000),
    ('pl_RUNWAY', 100000000),
    ('pl_DRYDOC', 100000000),
    ('pl_DYKCON', 100000000),
    ('pl_FORSTC', 100000000),
    ('pl_GATCON', 100000000),
    ('pl_LNDMRK', 100000000),
    ('pl_SLCONS', 100000000),
    ('pl_BRIDGE', 100000000),
    ('pl_WEDKLP', 100000000),
    ('pl_WATTUR', 100000000),
    ('pl_VEGATN', 100000000),
    ('pl_TWRTPT', 100000000),
    ('pl_TUNNEL', 100000000),
    ('pl_TSSLPT', 100000000),
    ('pl_TESARE', 100000000),
    ('pl_SWPARE', 100000000),
    ('pl_SPLARE', 100000000),
    ('pl_SNDWAV', 100000000),
    ('pl_SMCFAC', 100000000),
    ('pl_SLOGRD', 100000000),
    ('pl_SILTNK', 100000000),
    ('pl_SEAARE', 100000000),
    ('pl_RESARE', 100000000),
    ('pl_RCTLPT', 100000000),
    ('pl_PRDARE', 100000000),
    ('pl_PRCARE', 100000000),
    ('pl_PIPARE', 100000000),
    ('pl_PILBOP', 100000000),
    ('pl_OSPARE', 100000000),
    ('pl_OFSPLF', 100000000),
    ('pl_MORFAC', 100000000),
    ('pl_MIPARE', 100000000),
    ('pl_MARCUL', 100000000),
    ('pl_LOGPON', 100000000),
    ('pl_LNDRGN', 100000000),
    ('pl_ISTZNE', 100000000),
    ('pl_ICEARE', 100000000),
    ('pl_HRBFAC', 100000000),
    ('pl_HRBARE', 100000000),
    ('pl_GRIDRN', 100000000),
    ('pl_FSHZNE', 100000000),
    ('pl_FSHGRD', 100000000),
    ('pl_FSHFAC', 100000000),
    ('pl_FRPARE', 100000000),
    ('pl_FERYRT', 100000000),
    ('pl_FAIRWY', 100000000),
    ('pl_EXEZNE', 100000000),
    ('pl_DWRTPT', 100000000),
    ('pl_DMPGRD', 100000000),
    ('pl_CTSARE', 100000000),
    ('pl_CTNARE', 100000000),
    ('pl_CBLARE', 100000000),
    ('pl_BERTHS', 100000000),
    ('pl_AIRARE', 100000000),
    ('pl_ADMARE', 100000000),
    ('pl_ACHBRT', 100000000),
    ('pl_ACHARE', 100000000),
    ('pl_CRANES', 100000000),
	('li_RAPIDS', 100000000),
	('li_MARCUL', 100000000),
	('li_FLODOC', 100000000),
	('li_LNDMRK', 100000000),
	('li_FERYRT', 100000000),
	('li_CBLSUB', 100000000),
	('li_COALNE', 100000000),
	('li_DEPARE', 100000000),
	('li_DEPCNT', 100000000),
	('li_LNDARE', 100000000),
	('li_RIVERS', 100000000),
	('li_SLCONS', 100000000),
	('li_PIPOHD', 100000000),
	('li_MAGVAR', 100000000),
	('li_RECTRC', 100000000),
	('li_PIPSOL', 100000000),
	('li_BRIDGE', 100000000),
	('li_CONVYR', 100000000),
	('li_SBDARE', 100000000),
	('li_LNDELV', 100000000),
	('li_SLOTOP', 100000000),
	('li_DAMCON', 100000000),
	('li_OBSTRN', 100000000),
	('li_RADLNE', 100000000),
	('li_RAILWY', 100000000),
	('li_ROADWY', 100000000),
	('li_CAUSWY', 100000000),
	('li_WATFAL', 100000000),
	('li_CBLOHD', 100000000),
	('li_TSSBND', 100000000),
	('li_WATTUR', 100000000),
	('li_MORFAC', 100000000),
	('li_GATCON', 100000000),
	('li_TSELNE', 100000000),
	('li_DYKCON', 100000000),
	('li_VEGATN', 100000000),
	('li_RUNWAY', 100000000),
	('li_FNCLNE', 100000000),
	('li_RDOCAL', 100000000),
	('li_STSLNE', 100000000),
	('li_NAVLNE', 100000000),
	('li_OILBAR', 100000000),
	('li_CANALS', 100000000),
	('li_FORSTC', 100000000),
	('li_DWRTCL', 100000000),
	('li_TIDEWY', 100000000),
	('li_TUNNEL', 100000000),
	('li_BERTHS', 100000000),
	('li_RCRTCL', 100000000),
	('li_FSHFAC', 100000000),
	('li_PONTON', 100000000),
	('pt_ROADWY', 100000000),
	('pt_BUAARE', 100000000),
	('pt_DMPGRD', 100000000),
	('pt_BOYCAR', 100000000),
	('pt_BOYSAW', 100000000),
	('pt_ACHARE', 100000000),
	('pt_BOYINB', 100000000),
	('pt_PILBOP', 100000000),
	('pt_OFSPLF', 100000000),
	('pt_BOYSPP', 100000000),
	('pt_FOGSIG', 100000000),
	('pt_LNDARE', 100000000),
	('pt_LNDELV', 100000000),
	('pt_SPLARE', 100000000),
	('pt_LNDRGN', 100000000),
	('pt_LIGHTS', 100000000),
	('pt_SILTNK', 100000000),
	('pt_WATTUR', 100000000),
	('pt_ICNARE', 100000000),
	('pt_MIPARE', 100000000),
	('pt_TS_TIS', 100000000),
	('pt_OBSTRN', 100000000),
	('pt_PILPNT', 100000000),
	('pt_RTPBCN', 100000000),
	('pt_SBDARE', 100000000),
	('pt_RETRFL', 100000000),
	('pt_SOUNDG', 100000000),
	('pt_TOPMAR', 100000000),
	('pt_HULKES', 100000000),
	('pt_LOGPON', 100000000),
	('pt_WEDKLP', 100000000),
	('pt_WRECKS', 100000000),
	('pt_NEWOBJ', 100000000),
	('pt_UWTROC', 100000000),
	('pt_AIRARE', 100000000),
	('pt_CURENT', 100000000),
	('pt_LNDMRK', 100000000),
	('pt_LOCMAG', 100000000),
	('pt_SEAARE', 100000000),
	('pt_LITFLT', 100000000),
	('pt_BOYISD', 100000000),
	('pt_CTNARE', 100000000),
	('pt_FSHFAC', 100000000),
	('pt_HRBFAC', 100000000),
	('pt_MORFAC', 100000000),
	('pt_VEGATN', 100000000),
	('pt_PIPSOL', 100000000),
	('pt_GATCON', 100000000),
	('pt_SMCFAC', 100000000),
	('pt_BUISGL', 100000000),
	('pt_BCNLAT', 100000000),
	('pt_BCNSPP', 100000000),
	('pt_CTRPNT', 100000000),
	('pt_FORSTC', 100000000),
	('pt_RDOSTA', 100000000),
	('pt_DAMCON', 100000000),
	('pt_LITVES', 100000000),
	('pt_BCNCAR', 100000000),
	('pt_RUNWAY', 100000000),
	('pt_PYLONS', 100000000),
	('pt_CGUSTA', 100000000),
	('pt_RCTLPT', 100000000),
	('pt_TS_FEB', 100000000),
	('pt_BRIDGE', 100000000),
	('pt_SPRING', 100000000),
	('pt_ACHBRT', 100000000),
	('pt_RDOCAL', 100000000),
	('pt_BOYLAT', 100000000),
	('pt_TS_PAD', 100000000),
	('pt_TS_PRH', 100000000),
	('pt_DISMAR', 100000000),
	('pt_SLOGRD', 100000000),
	('pt_SNDWAV', 100000000),
	('pt_PRDARE', 100000000),
	('pt_SISTAW', 100000000),
	('pt_RADSTA', 100000000),
	('pt_CRANES', 100000000),
	('pt_MARCUL', 100000000),
	('pt_BERTHS', 100000000),
	('pt_RSCSTA', 100000000),
	('pt_BCNSAW', 100000000),
	('pt_SISTAT', 100000000),
	('pt_SLCONS', 100000000),
	('pt_BCNISD', 100000000),
	('pt_DAYMAR', 100000000),
	('pt_WATFAL', 100000000)

    # Ajoutez d'autres noms de couche avec leur échelle minimale dans le même format
]

# Récupérer le projet en cours
projet = QgsProject.instance()

# Charger chaque couche dans l'ordre spécifié
for nom_couche, echelle_min in couches_a_charger:
    # Construire le chemin complet de la couche
    chemin_couche = '{}|layername={}'.format(chemin_geopackage, nom_couche)
    # Charger la couche
    couche = QgsVectorLayer(chemin_couche, nom_couche, 'ogr')
    # Vérifier si la couche est valide
    if couche.isValid():
        # Ajouter la couche au projet
        projet.addMapLayer(couche)
        # Définir l'échelle minimale pour la couche
        couche.setScaleBasedVisibility(True)
        couche.setMinimumScale(echelle_min)
        print(f"L'échelle d'affichage de la couche {nom_couche} a été définie sur {echelle_min}.")
    else:
        print("Impossible de charger la couche :", nom_couche)

print("Chargement des couches terminé.")

Display scale management

The previous script is used to define a minimum scale when layers are first loaded into the project. When you save your project, the value in the layer’s Min_scale field is saved. When the project is opened, the display parameters at the time the project is closed are taken into account. You will therefore only use the previous script at the start of work on a project. However, the scales defined in the load_layers script may not be suitable for your work area. You may therefore need to modify the min scale values for certain layers.

We provide you with a Python script that allows you to define the minimum display scale for all layers selected in the Layers panel. Once your project has been saved, these settings will be used each time the project is opened.

To use it, first load it into the QGis Python console, select (highlight) the layers you wish to modify in the Layers panel, and, most importantly, modify the line

#Define minimum scale (for example, 1:50000)

min_scale = 250000

with the desired value for the minimum display scale.

The Python script for setting the minimum display scale for selected layers is as follows:

setminscale.py

# Récupérer la vue des couches
layer_tree_view = iface.layerTreeView()

# Récupérer les couches sélectionnées
selected_layers = layer_tree_view.selectedLayers()

# Définir l'échelle minimale pour chaque couche sélectionnée
for layer in selected_layers:
    # Définir l'échelle minimale (par exemple, 1:50000)
    min_scale = 250000
    
    # Définir l'échelle minimale d'affichage pour la couche
    layer.setScaleBasedVisibility(True)
    layer.setMinimumScale(min_scale)
    print(f"L'échelle d'affichage de la couche {layer.name()} a été définie sur {min_scale}.")


You can download it here.

A Python script to filter layers by “purpose

The tables in your geopackage include an attribute called “purpose”, a value between 1 and 6 that corresponds to the map’s main objective:

  • 1 : Overview
  • 2 : General
  • 3 : Coastal
  • 4 : Approach
  • 5 : Port
  • 6: Docking

If you have maps with different purposes covering the same area, for example a General map and an Approach map, you will have duplicate information. If the entities on two maps are not of the same type (a surface entity on an Approach map will correspond to a point entity on the General map, for example), rendering can quickly become very messy. The proposed solution is simple

filter_purpose.py

# Importer le module QGIS
from qgis.core import QgsProject, QgsMapLayerType

# Définir le valeur de l’attribut “purpose” à filtrer
valeur_purpose = 3

# Obtenir le projet actif
projet = QgsProject.instance()

# Obtenir la liste des couches chargées dans le projet
couches = projet.mapLayers().values()

# Parcourir toutes les couches
for couche in couches:
# Vérifier si la couche est de type vecteur
if couche.type() == QgsMapLayerType.VectorLayer:
# Vérifier si la couche a un champ nommé “purpose”
if couche.fields().indexFromName(‘purpose’) != -1:
# Vérifier si le nom de la couche commence par l’un des préfixes spécifiés
if couche.name().startswith((‘pt_’, ‘li_’, ‘pl_’)):
# Définir le filtre sur l’attribut “purpose” égal à la valeur spécifiée
filtre = f'”purpose” = {valeur_purpose}’
couche.setSubsetString(filtre)
print(f”Filtre défini pour la couche {couche.name()}”)
else:
print(f”La couche {couche.name()} ne commence pas par ‘pt_’, ‘li_’, ou ‘pl_'”)
else:
print(f”La couche {couche.name()} n’a pas d’attribut ‘purpose'”)
else:
print(f”La couche {couche.name()} n’est pas une couche vecteur”)

You can download the script here.

Load the script in the python console, modify the line:

#Define the value of the “purpose” attribute to be filtered

purpose_value = 5

with the desired purpose value, then run the script. All layers present in the project’s Layers panel will be filtered.

If you wish to remove all active filters from all layers in the project, the following script allows you to do so:

unfilter.py

# Importer le module QGIS
from qgis.core import QgsProject

# Obtenir le projet actif
projet = QgsProject.instance()

# Obtenir la liste des couches chargées dans le projet
couches = projet.mapLayers().values()

# Parcourir toutes les couches
for couche in couches:
    # Vérifier si la couche est une couche vectorielle
    if couche.type() == QgsMapLayer.VectorLayer:
        # Supprimer le filtre de la couche
        couche.setSubsetString('')
        print(f"Filtre supprimé pour la couche {couche.name()}")

You can download the script here.

Si cet article vous a intéressé et que vous pensez qu'il pourrait bénéficier à d'autres personnes, n'hésitez pas à le partager sur vos réseaux sociaux en utilisant les boutons ci-dessous. Votre partage est apprécié !

Leave a Reply

Your email address will not be published. Required fields are marked *