From 483ac35f08c06116460781014297a7e878e9da7c Mon Sep 17 00:00:00 2001 From: Ana-Maria Rosu Date: Fri, 20 Jan 2023 16:44:07 +0100 Subject: [PATCH 1/7] feat(qview): create qgis view --- scripts/create_qgis_view.py | 193 ++++++++++++++++++++++++++++++++++-- scripts/process_qlayers.py | 33 ++++++ 2 files changed, 217 insertions(+), 9 deletions(-) create mode 100644 scripts/process_qlayers.py diff --git a/scripts/create_qgis_view.py b/scripts/create_qgis_view.py index c95fb7408..ece46a2fe 100644 --- a/scripts/create_qgis_view.py +++ b/scripts/create_qgis_view.py @@ -3,11 +3,27 @@ import argparse import re +import json import os.path +import platform from copy import deepcopy +import numpy as np from lxml import etree from osgeo import gdal +from qgis.core import ( + QgsApplication, + QgsProject, + QgsCoordinateReferenceSystem, + QgsColorRampShader, + QgsPalettedRasterRenderer, + QgsFields, + QgsField, + QgsWkbTypes, +) +from qgis.PyQt.QtGui import QColor +from qgis.PyQt.QtCore import QVariant from process_requests import check_get_post, response2pyobj, xml_from_wmts +from process_qlayers import add_layer_to_map, create_vector def read_args(): @@ -27,9 +43,16 @@ def read_args(): help="style for ortho to be exported to xml (default: RVB)", type=str, default='RVB', choices=['RVB', 'IR', 'IRC']) parser.add_argument('-o', '--output', - help="output path (default: ./)", - type=str, default='./') - parser.add_argument("-v", "--verbose", + help="output qgis view path (default: ./view.qgz)", + type=str, default='./view.qgz') + parser.add_argument('-z', '--zoom', nargs=2, + help="zoom levels as zmin zmax (default: 3025 10000000)\ + -> graph layer visibility scale [1:zmax,1:zmin]", + type=int, default=[3025, 10000000]) + parser.add_argument('-m', '--macros', + help="macros file path", + type=str) + parser.add_argument('-v', '--verbose', help="verbose (default: 0, meaning no verbose)", type=int, default=0) argum = parser.parse_args() @@ -58,8 +81,21 @@ def suppress_cachetag(xml_in, xml_out): ARG = read_args() + +def print_info_add_layer(layer_name): + """ print info when layer added to view """ + if ARG.verbose > 0: + print(f"-> '{layer_name}' layer added to view") + + +def print_info_visib_scale(layer_name, zmin, zmax): + """ print info on visibility scale """ + if ARG.verbose > 0: + print(f'\t{layer_name} layer visibility scale: [1:{zmax},1:{zmin}]') + + # check input url -url_pattern = '^https?:\/\/[0-9A-z.]+\:[0-9]+$' +url_pattern = r'^https?:\/\/[0-9A-z.]+\:[0-9]+$' if not re.match(url_pattern, ARG.url): raise SystemExit(f"ERROR: URL '{ARG.url}' is invalid") @@ -74,6 +110,34 @@ def suppress_cachetag(xml_in, xml_out): branch_name = ARG.branch_name.strip() if not branch_name: raise SystemExit('ERROR: Empty branch name') + +# check input macros filepath +if ARG.macros and not os.path.isfile(ARG.macros): + raise SystemExit(f"ERROR: Unable to open macros file '{ARG.macros}'") + +# check output directory +dirpath_out = os.path.dirname(os.path.normpath(ARG.output)) +if not os.path.isdir(dirpath_out): + raise SystemExit(f"ERROR: '{dirpath_out}' is not a valid directory") + +# check overviews file and get info +overviews_path = cache['path'] + '/overviews.json' +try: + with open(overviews_path, 'r', encoding='utf-8') as fileOverviews: + overviews = json.load(fileOverviews) + slab_width = overviews['slabSize']['width'] + slab_height = overviews['slabSize']['height'] + if slab_width is None or slab_height is None: + raise SystemExit(f"ERROR: No 'slabSize' values in '{overviews_path}'!") + if slab_width != slab_height: + print(f"WARNING: Slab width(={slab_width}) <> height(={slab_height}) \ +in '{overviews_path}'!") + tms = overviews['identifier'] + if tms is None: + raise SystemExit(f"ERROR: No 'identifier' value in '{overviews_path}'") +except IOError: + raise SystemExit(f"ERROR: Unable to open file '{overviews_path}'") + # ---------- create new branch on cache ---------- req_post_branch = ARG.url + '/branch?name=' + branch_name + \ '&idCache=' + str(ARG.cache_id) @@ -90,11 +154,6 @@ def suppress_cachetag(xml_in, xml_out): wmts_ortho = f'{wmts_url},layer=ortho,style={ARG.style_ortho}' wmts_graph = f'{wmts_url},layer=graph' -# check if valid output directory -dirpath_out = os.path.normpath(ARG.output) -if not os.path.isdir(dirpath_out): - raise SystemExit(f"ERROR: '{dirpath_out}' is not a valid directory") - xml_ortho_tmp = dirpath_out + '/ortho_tmp.xml' xml_graph_tmp = dirpath_out + '/graph_tmp.xml' xml_from_wmts(wmts_ortho, xml_ortho_tmp) @@ -173,3 +232,119 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast tree.write(vrt_final) print(f"File '{vrt_final}' written") # TODO: suppress vrt_tmp + +# --------------- create qgz view ------------- +# TODO: check if only needed for Linux +if platform.system() == 'Linux': + os.environ['QT_QPA_PLATFORM'] = 'offscreen' + +QgsApplication.setPrefixPath('/usr/', True) +qgs = QgsApplication([], False) +qgs.initQgis() +project = QgsProject.instance() + +# ---- add ortho layer to map ---- +ortho_lname = 'ortho' +ortho_layer = add_layer_to_map(xml_ortho, ortho_lname, project, 'gdal') +print_info_add_layer(ortho_lname) + +# get crs from layer +crs = ortho_layer.crs() +# set project crs +project.setCrs(QgsCoordinateReferenceSystem(crs)) + +# ---- add graph layer to map ---- +graph_lname = 'graph' +graph_layer = add_layer_to_map(xml_graph, graph_lname, project, 'gdal') +# print(f'{ortho_layer.extent()=}') +# print(f'{graph_layer.extent()=}') +graph_layer.renderer().setOpacity(0.3) +graph_layer.setScaleBasedVisibility(True) +# check and set zoom min, max inputs for visibility scale of graph layer +zoom_min_graph, zoom_max_graph = ARG.zoom if ARG.zoom[0] <= ARG.zoom[1]\ + else (ARG.zoom[1], ARG.zoom[0]) +graph_layer.setMinimumScale(zoom_max_graph) +graph_layer.setMaximumScale(zoom_min_graph) +print_info_add_layer(graph_lname) +print_info_visib_scale(graph_lname, zoom_min_graph, zoom_max_graph) + +# ---- add contour layer to map ---- +contour_lname = 'graphe_contour' +contour_layer = add_layer_to_map(vrt_final, contour_lname, project, 'gdal') +# set zoom min, max for visibility scale of contour layer +zoom_max_contour = zoom_min_graph - 1 +zoom_min_contour = int(np.floor(zoom_max_contour/slab_width)) +contour_layer.setScaleBasedVisibility(True) +contour_layer.setMinimumScale(zoom_max_contour) +contour_layer.setMaximumScale(zoom_min_contour) +# set renderer +colors = [QgsColorRampShader.ColorRampItem(255, QColor('#ff0000'), '255')] +renderer = QgsPalettedRasterRenderer(contour_layer.dataProvider(), 1, + QgsPalettedRasterRenderer. + colorTableToClassData(colors)) +contour_layer.setRenderer(renderer) +contour_layer.triggerRepaint() +print_info_add_layer(contour_lname) +print_info_visib_scale(contour_lname, zoom_min_contour, zoom_max_contour) + +# ---- add opi layer to map ---- +# get 1st opi +opi_name = next(iter(overviews['list_OPI'])) +opi_uri_params = f'crs={crs.authid()}&format=image/png&layers=opi&'\ + f'styles={ARG.style_ortho}&tileDimensions=Name={opi_name}&'\ + f'tileMatrixSet={tms}&'\ + f'url={ARG.url}/{branch_id}/wmts' +opi_lname = 'OPI' +opi_layer = add_layer_to_map(opi_uri_params, opi_lname, project, 'wms') +opi_layer.renderer().setOpacity(0.5) +project.layerTreeRoot().findLayer(opi_layer).setItemVisibilityChecked(False) +print_info_add_layer(opi_lname) + +# ---- create patches layer and add to map ----- +patches_fname = dirpath_out + '/patches.gpkg' +patches_fields = QgsFields() +patches_fields.append(QgsField('fid', QVariant.Int)) +patches_geom_type = QgsWkbTypes.Polygon +create_vector(patches_fname, patches_fields, patches_geom_type, crs, project) +patches_lname = 'patches' +patches_layer = add_layer_to_map(patches_fname, patches_lname, + project, 'ogr', is_raster=False) +print_info_add_layer(patches_lname) + +# ---- create advancement layer and add to map ----- +advancement_fname = dirpath_out + '/avancement.gpkg' +advancement_fields = QgsFields() +advancement_fields.append(QgsField('fid', QVariant.Int)) +advancement_geom_type = QgsWkbTypes.Polygon +create_vector(advancement_fname, advancement_fields, advancement_geom_type, crs, project) +advancement_lname = 'avancement' +advancement_layer = add_layer_to_map(advancement_fname, advancement_lname, + project, 'ogr', is_raster=False) +print_info_add_layer(advancement_lname) + +# ---- add macros to map ---- +if ARG.macros: + # adapt macros to working data + words_to_replace = {'__IDBRANCH__': branch_id, + '__URLSERVER__': ARG.url+'/', + '__TILEMATRIXSET__': tms} + words_not_found = [] + with open(ARG.macros, 'r', encoding='utf-8') as file_macro_in: + macros_data = file_macro_in.read() + for key, val in words_to_replace.items(): + regex_word = re.compile(f'(\'|\")?\\b{key}\\b(\'|\")?') + macros_data, nb_occ = regex_word.subn(f"'{val}'", macros_data) + if nb_occ == 0: + words_not_found.append(key) + if len(words_not_found) > 0: + raise SystemExit(f"ERROR: {words_not_found} not found in '{ARG.macros}'") + # add adapted macros + QgsProject.instance().writeEntry("Macros", "/pythonCode", macros_data) + if ARG.verbose > 0: + print('-> macros added to view') + +# ---- write qgz view output file ---- +project.write(ARG.output) +print(f"File '{ARG.output}' written") + +qgs.exitQgis() diff --git a/scripts/process_qlayers.py b/scripts/process_qlayers.py new file mode 100644 index 000000000..54c153691 --- /dev/null +++ b/scripts/process_qlayers.py @@ -0,0 +1,33 @@ +# coding: utf-8 +""" This script handles QGIS layers """ + +from qgis.core import QgsRasterLayer, QgsVectorLayer, QgsVectorFileWriter + + +def add_layer_to_map(data_source, layer_name, qgs_project, provider_name, is_raster=True): + """ add layer to map """ + layer = QgsRasterLayer(data_source, layer_name, provider_name) if is_raster\ + else QgsVectorLayer(data_source, layer_name, provider_name) + if not layer or not layer.isValid(): + raise SystemExit(f"ERROR: Layer '{layer_name}' failed to load! - " + f'{layer.error().summary()}') + qgs_project.addMapLayer(layer) + return layer + + +def create_vector(vector_filename, fields, geom_type, crs, qgs_project, driver_name='GPKG'): + """ create vector """ + transform_context = qgs_project.transformContext() + save_options = QgsVectorFileWriter.SaveVectorOptions() + save_options.driverName = driver_name + save_options.fileEncoding = "UTF-8" + wrt = QgsVectorFileWriter.create(vector_filename, + fields, + geom_type, + crs, + transform_context, + save_options) + if wrt.hasError() != QgsVectorFileWriter.NoError: + raise SystemExit(f"ERROR when creating vector '{vector_filename}': {wrt.errorMessage()}") + # flush to disk + del wrt From fe5199c651517129485642cef9fd2bbd94493dd6 Mon Sep 17 00:00:00 2001 From: Ana-Maria Rosu Date: Tue, 24 Jan 2023 13:25:01 +0100 Subject: [PATCH 2/7] docs(qview): info on qgis view --- README.md | 48 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7531721ce..6a8dc1d60 100644 --- a/README.md +++ b/README.md @@ -309,9 +309,9 @@ Si un cache a une taille de dalle (slabSize) différente de 16x16 tuiles ou une ## Préparation des éléments de la vue PackO pour QGIS -Dans le cas d'utilisation d'un client pour PackO basé sur QGIS, on peut générer des éléments de la vue du chantier en utilisant le script **create_qgis_view.py** : +Dans le cas d'utilisation d'un client pour PackO basé sur QGIS, on peut créer automatiquement la vue contenant les éléments du chantier en utilisant le script **create_qgis_view.py** : ```` -usage: create_qgis_view.py [-h] [-u URL] -c CACHE_ID [-b BRANCH_NAME] [-s STYLE_ORTHO] [-o OUTPUT] [-v VERBOSE] +usage: create_qgis_view.py [-h] [-u URL] -c CACHE_ID [-b BRANCH_NAME] [-s {RVB,IR,IRC}] [-o OUTPUT] [-z ZOOM ZOOM] [-m MACROS] [-v VERBOSE] options: -h, --help show this help message and exit @@ -323,19 +323,49 @@ options: -s {RVB,IR,IRC}, --style_ortho {RVB,IR,IRC} style for ortho to be exported to xml (default: RVB) -o OUTPUT, --output OUTPUT - output path (default: ./) + output qgis view path (default: ./view.qgz) + -z ZOOM ZOOM, --zoom ZOOM ZOOM + zoom levels as zmin zmax (default: 3025 10000000) -> graph layer visibility scale [1:zmax,1:zmin] + -m MACROS, --macros MACROS + macros file path -v VERBOSE, --verbose VERBOSE verbose (default: 0, meaning no verbose) ```` où **-c** est l'identifiant du cache de travail dans la base de données : pour le récupérer, on peut demander à l'API la liste des caches en utilisant l'url `http://[serveur]:[port]/caches` ou la commande curl `curl [-v] -X "GET" "http://[serveur]:[PORT]/caches" -H "accept: */*"`. -Actuellement, les éléments de la vue générés avec ce script sont : +Les éléments de la vue générés avec ce script sont : - une nouvelle branche PackO créée sur le cache indiqué ; le nom de la branche est par défaut "newBranch", nom de branche à indiquer avec **-b**. -- **ortho.xml** et **graph.xml** : les couches ortho et graphe de la nouvelle branche, exportées sous forme de fichiers xml plus des modifications pour QGIS, dans le dossier de sortie (chemin à indiquer avec **-o**). Pour l'ortho, si le style est différent de celui par défaut ("RVB"), il faut l'indiquer avec **-s**. -- **graph_contour.vrt** : la couche contour de graphe générée à partir de graph.xml avec des ajouts et modifications pour QGIS, dans le dossier de sortie - -Pour le bon fonctionnement dans QGIS, il est impératif de mettre la variable d'environnement **GDAL_VRT_ENABLE_PYTHON** à **YES**. - +- **ortho.xml** et **graph.xml** : les couches ortho et graphe de la nouvelle branche, exportées sous forme de fichiers xml plus des modifications pour QGIS, dans le dossier de sortie (le chemin de la vue à indiquer avec **-o**). Pour l'ortho, si le style est différent de celui par défaut ("RVB"), il faut l'indiquer avec **-s**. L'échelle de visibilité de la couche *graphe* est définie avec **-z**. +- **graph_contour.vrt** : la couche contour de graphe générée à partir de graph.xml avec des ajouts et modifications pour QGIS, dans le dossier de sortie. L'échelle de visibilité de la couche *graphe_contour* est définie à partir de celle de la couche *graphe* +- **patches.gpkg** : la couche vecteur, initialement vide, utilisée pour les retouches +- **avancement.gpkg** : la couche vecteur, initialement vide, utilisée pour garder la trace des zones contrôlées + +Ces éléments sont des couches de la vue PackO pour QGIS (par défaut **view.qgz**), auxquelles s'ajoute une couche OPI générée en important la couche WMTS OPI de la branche du cache. + +Pour intégrer un fichier de macros QGIS à la vue, il faut indiquer le chemin vers le fichier macros prototype avec **-m**. Ce fichier sera adapté au chantier avant d'être intégré à la vue, en remplaçant les clés `__IDBRANCH__`, `__URLSERVER__` et `__TILEMATRIXSET__` avec les valeurs correspondantes pour le chantier - exemple : + + - Extrait prototype macros, avant adaptation : + ``` + id_branch = __IDBRANCH__ + url_server = __URLSERVER__ + tile_matrix_set = __TILEMATRIXSET__ + ``` + - Extrait macros, après adaptation : + ``` + id_branch = '32' + url_server = 'http://localhost:8081/' + tile_matrix_set = 'LAMB93_20cm' + ``` + +Pour le bon fonctionnement dans QGIS, il est impératif de mettre la variable d'environnement **GDAL_VRT_ENABLE_PYTHON** à **YES**. Il faut également définir les variables d'environnement (où `` doit être remplacé par le chemin d'accès au dossier d'installation de QGIS) : +- **PYTHONPATH** : + - sous Linux : `export PYTHONPATH=//share/qgis/python` + - sous Windows : `set PYTHONPATH=C:\\python` +- **LD_LIBRARY_PATH** : + - sous Linux : `export LD_LIBRARY_PATH=//lib` + - sous Windows: `set PATH=C:\\bin;C:\\apps\\bin;%PATH%` (où `` devrait être remplacé avec le type de release ciblé (ex : qgis-ltr, qgis, qgis-dev) + +Si la vue contient des macros, il faut activer leur utilisation lors du chargement de la vue dans QGIS. ## Traitement d'un chantier From 24154960ff8e681624787a2a5a6a2dad5511c5b5 Mon Sep 17 00:00:00 2001 From: Ana-Maria Rosu Date: Wed, 15 Mar 2023 13:48:12 +0100 Subject: [PATCH 3/7] ci(qview): linting --- .github/workflows/lint-python.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-python.yml b/.github/workflows/lint-python.yml index e774a7c5b..7cf0f30a6 100644 --- a/.github/workflows/lint-python.yml +++ b/.github/workflows/lint-python.yml @@ -21,10 +21,10 @@ jobs: - name: Analysing the code with pylint run: | - pylint scripts/update_cache.py scripts/create_cache.py scripts/cache.py scripts/cache_def.py scripts/export_mtd.py scripts/prep_vectorise_graph.py scripts/vectorise_graph.py + pylint scripts/update_cache.py scripts/create_cache.py scripts/cache.py scripts/cache_def.py scripts/export_mtd.py scripts/prep_vectorise_graph.py scripts/vectorise_graph.py scripts/create_qgis_view.py scripts/process_requests.py scripts/process_qlayers.py continue-on-error: true - name: Analysing the code with flake8 run: | - flake8 --max-line-length 100 scripts/update_cache.py scripts/create_cache.py scripts/cache.py scripts/cache_def.py scripts/export_mtd.py scripts/prep_vectorise_graph.py scripts/vectorise_graph.py + flake8 --max-line-length 100 scripts/update_cache.py scripts/create_cache.py scripts/cache.py scripts/cache_def.py scripts/export_mtd.py scripts/prep_vectorise_graph.py scripts/vectorise_graph.py scripts/create_qgis_view.py scripts/process_requests.py scripts/process_qlayers.py From 42485a7ac1daff44d3200ab39c93413deaea6628 Mon Sep 17 00:00:00 2001 From: Ana-Maria Rosu Date: Tue, 14 Mar 2023 16:23:47 +0100 Subject: [PATCH 4/7] feat(qview): enhancements 1 --- scripts/create_qgis_view.py | 90 +++++++++++++++++++++---------------- scripts/process_qlayers.py | 15 ++++++- 2 files changed, 65 insertions(+), 40 deletions(-) diff --git a/scripts/create_qgis_view.py b/scripts/create_qgis_view.py index ece46a2fe..827479142 100644 --- a/scripts/create_qgis_view.py +++ b/scripts/create_qgis_view.py @@ -3,7 +3,6 @@ import argparse import re -import json import os.path import platform from copy import deepcopy @@ -19,11 +18,13 @@ QgsFields, QgsField, QgsWkbTypes, + QgsLayerTreeLayer, ) +from qgis.gui import QgsMapCanvas from qgis.PyQt.QtGui import QColor from qgis.PyQt.QtCore import QVariant from process_requests import check_get_post, response2pyobj, xml_from_wmts -from process_qlayers import add_layer_to_map, create_vector +from process_qlayers import add_layer_to_map, create_vector, set_layer_resampling def read_args(): @@ -43,8 +44,8 @@ def read_args(): help="style for ortho to be exported to xml (default: RVB)", type=str, default='RVB', choices=['RVB', 'IR', 'IRC']) parser.add_argument('-o', '--output', - help="output qgis view path (default: ./view.qgz)", - type=str, default='./view.qgz') + help="output qgis view path (default: ./view.qgs)", + type=str, default='./view.qgs') parser.add_argument('-z', '--zoom', nargs=2, help="zoom levels as zmin zmax (default: 3025 10000000)\ -> graph layer visibility scale [1:zmax,1:zmin]", @@ -120,23 +121,20 @@ def print_info_visib_scale(layer_name, zmin, zmax): if not os.path.isdir(dirpath_out): raise SystemExit(f"ERROR: '{dirpath_out}' is not a valid directory") -# check overviews file and get info -overviews_path = cache['path'] + '/overviews.json' -try: - with open(overviews_path, 'r', encoding='utf-8') as fileOverviews: - overviews = json.load(fileOverviews) - slab_width = overviews['slabSize']['width'] - slab_height = overviews['slabSize']['height'] - if slab_width is None or slab_height is None: - raise SystemExit(f"ERROR: No 'slabSize' values in '{overviews_path}'!") - if slab_width != slab_height: - print(f"WARNING: Slab width(={slab_width}) <> height(={slab_height}) \ -in '{overviews_path}'!") - tms = overviews['identifier'] - if tms is None: - raise SystemExit(f"ERROR: No 'identifier' value in '{overviews_path}'") -except IOError: - raise SystemExit(f"ERROR: Unable to open file '{overviews_path}'") +# get info from overviews file +req_get_overviews = ARG.url + '/json/overviews?cachePath=' + str(cache['path']) +resp_get_overviews = check_get_post(req_get_overviews) +overviews = resp_get_overviews.json() +slab_width = overviews['slabSize']['width'] +slab_height = overviews['slabSize']['height'] +if slab_width is None or slab_height is None: + raise SystemExit(f"ERROR: No 'slabSize' values in '{overviews}'!") +if slab_width != slab_height: + print(f"WARNING: Slab width(={slab_width}) <> height(={slab_height}) \ +in '{overviews}'!") +tms = overviews['identifier'] +if tms is None: + raise SystemExit(f"ERROR: No 'identifier' value in '{overviews}'") # ---------- create new branch on cache ---------- req_post_branch = ARG.url + '/branch?name=' + branch_name + \ @@ -155,19 +153,19 @@ def print_info_visib_scale(layer_name, zmin, zmax): wmts_graph = f'{wmts_url},layer=graph' xml_ortho_tmp = dirpath_out + '/ortho_tmp.xml' -xml_graph_tmp = dirpath_out + '/graph_tmp.xml' +xml_graph_tmp = dirpath_out + '/graphe_surface_tmp.xml' xml_from_wmts(wmts_ortho, xml_ortho_tmp) xml_from_wmts(wmts_graph, xml_graph_tmp) # suppress Cache tag from previous graph and ortho xml to avoid creation of local cache xml_ortho = dirpath_out + '/ortho.xml' -xml_graph = dirpath_out + '/graph.xml' +xml_graph = dirpath_out + '/graphe_surface.xml' suppress_cachetag(xml_ortho_tmp, xml_ortho) suppress_cachetag(xml_graph_tmp, xml_graph) # TODO: suppress xml_ortho_tmp and xml_graph_tmp # --------- create contours vrt from graph.xml ----------- -vrt_tmp = dirpath_out + '/graph_contour_tmp.vrt' +vrt_tmp = dirpath_out + '/graphe_contour_tmp.vrt' ds = gdal.BuildVRT(vrt_tmp, xml_graph) ds = None print(f"File '{vrt_tmp}' written") @@ -226,15 +224,14 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast else: raise SystemExit(f"ERROR: 'VRTRasterBand' not found in '{vrt_tmp}'") -vrt_final = dirpath_out + '/graph_contour.vrt' +vrt_final = dirpath_out + '/graphe_contour.vrt' etree.tail = '\n' etree.indent(root) tree.write(vrt_final) print(f"File '{vrt_final}' written") # TODO: suppress vrt_tmp -# --------------- create qgz view ------------- -# TODO: check if only needed for Linux +# --------------- create qgis view ------------- if platform.system() == 'Linux': os.environ['QT_QPA_PLATFORM'] = 'offscreen' @@ -246,6 +243,8 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast # ---- add ortho layer to map ---- ortho_lname = 'ortho' ortho_layer = add_layer_to_map(xml_ortho, ortho_lname, project, 'gdal') +# set resampling +set_layer_resampling(ortho_layer) print_info_add_layer(ortho_lname) # get crs from layer @@ -253,11 +252,18 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast # set project crs project.setCrs(QgsCoordinateReferenceSystem(crs)) -# ---- add graph layer to map ---- -graph_lname = 'graph' -graph_layer = add_layer_to_map(xml_graph, graph_lname, project, 'gdal') -# print(f'{ortho_layer.extent()=}') -# print(f'{graph_layer.extent()=}') +# set extent +canvas = QgsMapCanvas() +canvas.setExtent(ortho_layer.extent()) +canvas.refresh() + +# ------ create group for graph elements -------- +graph_group = project.layerTreeRoot().insertGroup(0, 'GRAPHE') +graph_group.setExpanded(False) + +# --- create graph layer and add to group ---- +graph_lname = 'graphe_surface' +graph_layer = add_layer_to_map(xml_graph, graph_lname, project, 'gdal', show=False) graph_layer.renderer().setOpacity(0.3) graph_layer.setScaleBasedVisibility(True) # check and set zoom min, max inputs for visibility scale of graph layer @@ -265,14 +271,18 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast else (ARG.zoom[1], ARG.zoom[0]) graph_layer.setMinimumScale(zoom_max_graph) graph_layer.setMaximumScale(zoom_min_graph) +# set resampling +set_layer_resampling(graph_layer) +# add to group +graph_group.insertChildNode(0, QgsLayerTreeLayer(graph_layer)) print_info_add_layer(graph_lname) print_info_visib_scale(graph_lname, zoom_min_graph, zoom_max_graph) -# ---- add contour layer to map ---- +# --- create contour layer and add to group ---- contour_lname = 'graphe_contour' -contour_layer = add_layer_to_map(vrt_final, contour_lname, project, 'gdal') +contour_layer = add_layer_to_map(vrt_final, contour_lname, project, 'gdal', show=False) # set zoom min, max for visibility scale of contour layer -zoom_max_contour = zoom_min_graph - 1 +zoom_max_contour = zoom_min_graph zoom_min_contour = int(np.floor(zoom_max_contour/slab_width)) contour_layer.setScaleBasedVisibility(True) contour_layer.setMinimumScale(zoom_max_contour) @@ -284,6 +294,10 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast colorTableToClassData(colors)) contour_layer.setRenderer(renderer) contour_layer.triggerRepaint() +# set resampling +set_layer_resampling(contour_layer) +# add to group +graph_group.insertChildNode(1, QgsLayerTreeLayer(contour_layer)) print_info_add_layer(contour_lname) print_info_visib_scale(contour_lname, zoom_min_contour, zoom_max_contour) @@ -301,12 +315,12 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast print_info_add_layer(opi_lname) # ---- create patches layer and add to map ----- -patches_fname = dirpath_out + '/patches.gpkg' +patches_fname = dirpath_out + '/retouches_graphe.gpkg' patches_fields = QgsFields() patches_fields.append(QgsField('fid', QVariant.Int)) patches_geom_type = QgsWkbTypes.Polygon create_vector(patches_fname, patches_fields, patches_geom_type, crs, project) -patches_lname = 'patches' +patches_lname = 'retouches_graphe' patches_layer = add_layer_to_map(patches_fname, patches_lname, project, 'ogr', is_raster=False) print_info_add_layer(patches_lname) @@ -343,7 +357,7 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast if ARG.verbose > 0: print('-> macros added to view') -# ---- write qgz view output file ---- +# ---- write qgis view output file ---- project.write(ARG.output) print(f"File '{ARG.output}' written") diff --git a/scripts/process_qlayers.py b/scripts/process_qlayers.py index 54c153691..02bd32e3d 100644 --- a/scripts/process_qlayers.py +++ b/scripts/process_qlayers.py @@ -4,14 +4,15 @@ from qgis.core import QgsRasterLayer, QgsVectorLayer, QgsVectorFileWriter -def add_layer_to_map(data_source, layer_name, qgs_project, provider_name, is_raster=True): +def add_layer_to_map(data_source, layer_name, qgs_project, provider_name, + is_raster=True, show=True): """ add layer to map """ layer = QgsRasterLayer(data_source, layer_name, provider_name) if is_raster\ else QgsVectorLayer(data_source, layer_name, provider_name) if not layer or not layer.isValid(): raise SystemExit(f"ERROR: Layer '{layer_name}' failed to load! - " f'{layer.error().summary()}') - qgs_project.addMapLayer(layer) + qgs_project.addMapLayer(layer, show) return layer @@ -31,3 +32,13 @@ def create_vector(vector_filename, fields, geom_type, crs, qgs_project, driver_n raise SystemExit(f"ERROR when creating vector '{vector_filename}': {wrt.errorMessage()}") # flush to disk del wrt + + +def set_layer_resampling(raster_layer, resampling_method_zoomedin=None, + resampling_method_zoomedout=None, max_oversampling=1.0): + """ set zoomed in and out resampling methods (None means nearest neighbor) + and max oversampling""" + resample_filter = raster_layer.resampleFilter() + resample_filter.setZoomedInResampler(resampling_method_zoomedin) + resample_filter.setZoomedOutResampler(resampling_method_zoomedout) + resample_filter.setMaxOversampling(max_oversampling) From 949dfb5df6aeb466548969012d99685e49963449 Mon Sep 17 00:00:00 2001 From: Ana-Maria Rosu Date: Wed, 15 Mar 2023 11:38:27 +0100 Subject: [PATCH 5/7] docs(qview): info on enhancements 1 --- README.md | 8 +- ressources/example_macros_qgis.py | 235 ++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 ressources/example_macros_qgis.py diff --git a/README.md b/README.md index 6a8dc1d60..6df9597d9 100644 --- a/README.md +++ b/README.md @@ -323,7 +323,7 @@ options: -s {RVB,IR,IRC}, --style_ortho {RVB,IR,IRC} style for ortho to be exported to xml (default: RVB) -o OUTPUT, --output OUTPUT - output qgis view path (default: ./view.qgz) + output qgis view path (default: ./view.qgs) -z ZOOM ZOOM, --zoom ZOOM ZOOM zoom levels as zmin zmax (default: 3025 10000000) -> graph layer visibility scale [1:zmax,1:zmin] -m MACROS, --macros MACROS @@ -340,7 +340,7 @@ Les éléments de la vue générés avec ce script sont : - **patches.gpkg** : la couche vecteur, initialement vide, utilisée pour les retouches - **avancement.gpkg** : la couche vecteur, initialement vide, utilisée pour garder la trace des zones contrôlées -Ces éléments sont des couches de la vue PackO pour QGIS (par défaut **view.qgz**), auxquelles s'ajoute une couche OPI générée en important la couche WMTS OPI de la branche du cache. +Ces éléments sont des couches de la vue PackO pour QGIS (par défaut **view.qgs**), auxquelles s'ajoute une couche OPI générée en important la couche WMTS OPI de la branche du cache. Pour intégrer un fichier de macros QGIS à la vue, il faut indiquer le chemin vers le fichier macros prototype avec **-m**. Ce fichier sera adapté au chantier avant d'être intégré à la vue, en remplaçant les clés `__IDBRANCH__`, `__URLSERVER__` et `__TILEMATRIXSET__` avec les valeurs correspondantes pour le chantier - exemple : @@ -357,7 +357,9 @@ Pour intégrer un fichier de macros QGIS à la vue, il faut indiquer le chemin v tile_matrix_set = 'LAMB93_20cm' ``` -Pour le bon fonctionnement dans QGIS, il est impératif de mettre la variable d'environnement **GDAL_VRT_ENABLE_PYTHON** à **YES**. Il faut également définir les variables d'environnement (où `` doit être remplacé par le chemin d'accès au dossier d'installation de QGIS) : +Un exemple de fichier macros prototype est fourni dans le dossier *ressources*. + +Pour le bon fonctionnement dans QGIS, il est impératif de mettre la variable d'environnement **GDAL_VRT_ENABLE_PYTHON** à **YES**. Il faut également définir les variables d'environnement (où `` doit être remplacé par le chemin d'accès au dossier d'installation de QGIS ; exemples de `` sous Linux : **/usr** , sous Windows **C:\Program Files\QGIS XXX\apps\qgis\python** où 'XXX' est à remplacer avec la version de QGIS) : - **PYTHONPATH** : - sous Linux : `export PYTHONPATH=//share/qgis/python` - sous Windows : `set PYTHONPATH=C:\\python` diff --git a/ressources/example_macros_qgis.py b/ressources/example_macros_qgis.py new file mode 100644 index 000000000..2b1473fde --- /dev/null +++ b/ressources/example_macros_qgis.py @@ -0,0 +1,235 @@ +def openProject(): + pass + +def saveProject(): + pass + +def closeProject(): + pass + +import json, requests +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from qgis.utils import iface +from qgis.gui import * +from qgis.core import * +import time + +# =================================== +# ==== CONFIG A VERIFIER ============ +# =================================== +id_branch = __IDBRANCH__ +url_server = __URLSERVER__ +tile_matrix_set = __TILEMATRIXSET__ +# =================================== + +url_graph = url_server + id_branch + '/graph' +url_patch = url_server + id_branch + '/patch' +url_undo = url_server + id_branch + '/patch/undo' +url_redo = url_server + id_branch + '/patch/redo' +url_wmts = url_server + id_branch + '/wmts' +source='contextualWMSLegend=0&crs=EPSG:2154&dpiMode=7&featureCount=10&format=image/png&layers=opi&styles=RVB&tileDimensions=Name%3DXXX&tileMatrixSet='+tile_matrix_set+'&url='+url_wmts+'?SERVICE%3DWMTS%26REQUEST%3DGetCapabilities%26VERSION%3D1.0.0' +OPI=None +color=None +opi_layer = None +ortho_layer = None +patch_layer = None +graph_layer = None +for layer in QgsProject.instance().mapLayers().values(): + name = layer.name()[0:3].upper() + if (name == 'OPI'): + opi_layer = layer + if (name == 'ORT'): + ortho_layer = layer + if (name == 'PAT'): + patch_layer = layer + if (name == 'GRA'): + graph_layer = layer + + +#print("POC PACKO") +# iface.mapCanvas().setCachingEnabled(False) + + +def sendPatch(feature, OPI, color): + #print("sendPatch:", feature, OPI, color) + exporter=QgsJsonExporter() + patch = json.loads(exporter.exportFeatures([feature])) + #print(patch) + patch['crs']={'type': 'name', 'properties': {'name': 'urn:ogc:def:crs:EPSG::2154'}} + patch['features'][0]['properties']={'color': color, 'opiName': OPI} + res = requests.post(url_patch, json=patch) + return res.text + +def selectOPI(x, y): + #print("selectOPI") + res = requests.get(url_graph, params={'x':x, 'y':y}) + sel=json.loads(res.text) + #print(sel) + if 'opiName' in sel.keys(): + return sel['opiName'], sel['color'] + else: + return None, None + +def on_key(event): + global OPI + global color + #print("on_key") + touche = event.key() + #print(touche) + iface.messageBar().clearWidgets() + if (touche == Qt.Key_M): + iface.messageBar().pushMessage("PATCH ", "EN COURS : ", level=Qgis.Warning, duration=0) + nb_features = patch_layer.featureCount() + if (OPI is None) or (color is None): + msg = QMessageBox() + msg.setIcon(QMessageBox.Information) + msg.setText("PAS D'OPI SELECTIONNEE'") + msg.setWindowTitle("ERREUR") + msg.setStandardButtons(QMessageBox.Ok ) + msg.exec_() + OPI = None + return + if nb_features == 0: + msg = QMessageBox() + msg.setIcon(QMessageBox.Information) + msg.setText("PAS DE RETOUCHE") + msg.setWindowTitle("ERREUR") + msg.setStandardButtons(QMessageBox.Ok ) + msg.exec_() + OPI = None + return + if nb_features > 1: + msg = QMessageBox() + msg.setIcon(QMessageBox.Information) + msg.setText("UNE SEULE RETOUCHE A LA FOIS") + msg.setWindowTitle("ERREUR") + msg.setStandardButtons(QMessageBox.Ok ) + msg.exec_() + OPI = None + return + patch_layer.startEditing() + feature = list(patch_layer.getFeatures())[0] + mess = sendPatch(feature, OPI, color) + print(mess) + patch_layer.deleteFeature(feature.id()) + patch_layer.commitChanges() + iface.messageBar().pushMessage("PATCH ", "APPLIQUÉ : ", level=Qgis.Success, duration=0) + graph_layer.setDataSource(graph_layer.source(), "graphe_contour", "gdal") + ortho_layer.setDataSource(ortho_layer.source(), "ortho", "gdal") + OPI = None + # pour ne pas a avoir a remettre en mode edition pour la prochiane saisie + patch_layer.startEditing() + return + + if (touche == Qt.Key_P): + # Pick OPI + lastPoint = iface.mapCanvas().mouseLastXY() + lastPointTerr = iface.mapCanvas().getCoordinateTransform().toMapCoordinates(lastPoint.x(), lastPoint.y()) + OPI, color = selectOPI(lastPointTerr.x(), lastPointTerr.y()) + if OPI: + #print("ready: ", opi_layer, OPI, color) + opi_layer.setDataSource(source.replace('XXX', OPI), "OPI--"+OPI, "wms") + iface.messageBar().pushMessage("OPI ", "sélection actuelle : " + OPI + ' | ' + str(color), level=Qgis.Success, duration=0) + else: + #print("no OPI selected") + iface.messageBar().pushMessage("OPI ", "sélection impossible", level=Qgis.Critical, duration=0) + + return + + if (touche == Qt.Key_U): + #print("undo") + res = requests.put(url_undo) + #print(res.text) + #iface.mapCanvas().refreshAllLayers() + graph_layer.setDataSource(graph_layer.source(), "graphe_contour", "gdal") + ortho_layer.setDataSource(ortho_layer.source(), "ortho", "gdal") + iface.messageBar().pushMessage(res.text, level=Qgis.Success, duration=0) + return + + if (touche == Qt.Key_R): + #print("redo") + res = requests.put(url_redo) + #print(res.text) + #iface.mapCanvas().refreshAllLayers() + graph_layer.setDataSource(graph_layer.source(), "graphe_contour", "gdal") + ortho_layer.setDataSource(ortho_layer.source(), "ortho", "gdal") + iface.messageBar().pushMessage(res.text, level=Qgis.Success, duration=0) + return + + if (touche == Qt.Key_O): + id_opi_layer = QgsProject.instance().layerTreeRoot().findLayer(opi_layer.id()) + if id_opi_layer.isVisible() : + id_opi_layer.setItemVisibilityChecked(False) + else : + id_opi_layer.setItemVisibilityChecked(True) + return + + if (touche == Qt.Key_V): + id_groupe_vecteur = QgsProject.instance().layerTreeRoot().findGroup('VECTEURS') + if id_groupe_vecteur.isVisible() : + id_groupe_vecteur.setItemVisibilityChecked(False) + else : + id_groupe_vecteur.setItemVisibilityChecked(True) + return + + if (touche == Qt.Key_G): + id_graph_layer = QgsProject.instance().layerTreeRoot().findLayer(graph_layer.id()) + if id_graph_layer.isVisible() : + id_graph_layer.setItemVisibilityChecked(False) + else : + id_graph_layer.setItemVisibilityChecked(True) + return + + if (touche == Qt.Key_Less): + + avcmt = QgsProject.instance().mapLayersByName('avancement')[0] + avcmt.startEditing() + largeur_canvas = iface.mapCanvas().size().width() + hauteur_canvas = iface.mapCanvas().size().height() + coords_HG = iface.mapCanvas().getCoordinateTransform().toMapCoordinates(1, 1) + coords_HD = iface.mapCanvas().getCoordinateTransform().toMapCoordinates(largeur_canvas-2, 1) + coords_BD = iface.mapCanvas().getCoordinateTransform().toMapCoordinates(largeur_canvas-2, hauteur_canvas-2) + coords_BG = iface.mapCanvas().getCoordinateTransform().toMapCoordinates(1, hauteur_canvas-2) + geom = QgsGeometry.fromPolygonXY([[coords_HG,coords_HD,coords_BD,coords_BG]]) + f = QgsFeature(avcmt.fields()) + heurecomplete = time.localtime() + heure = str(heurecomplete.tm_hour).zfill(2) + minute = str(heurecomplete.tm_min).zfill(2) + seconde = str(heurecomplete.tm_sec).zfill(2) + str_heure = heure + minute + seconde + f.setGeometry(geom) + f.setAttribute("H_SAISIE", str_heure) + avcmt.addFeatures([f]) + iface.vectorLayerTools().saveEdits(avcmt) + iface.vectorLayerTools().stopEditing(avcmt) + return + + Direction={ + Qt.Key_1:"coords_BG - coords_HD", + Qt.Key_2:"coords_BG - coords_HG", + Qt.Key_3:"coords_BD - coords_HG", + Qt.Key_4:"coords_HG - coords_HD", + Qt.Key_6:"coords_HD - coords_HG", + Qt.Key_7:"coords_HG - coords_BD", + Qt.Key_8:"coords_HG - coords_BG", + Qt.Key_9:"coords_HD - coords_BG" + } + + if touche in Direction : + + largeur_canvas = iface.mapCanvas().size().width() + hauteur_canvas = iface.mapCanvas().size().height() + coords_HG = iface.mapCanvas().getCoordinateTransform().toMapCoordinates(1, 1) + coords_HD = iface.mapCanvas().getCoordinateTransform().toMapCoordinates(largeur_canvas-2, 1) + coords_BG = iface.mapCanvas().getCoordinateTransform().toMapCoordinates(1, hauteur_canvas-2) + coords_BD = iface.mapCanvas().getCoordinateTransform().toMapCoordinates(largeur_canvas-2, hauteur_canvas-2) + + decalage = eval(Direction[touche]) + centre = iface.mapCanvas().center() + new_centre = centre + decalage + iface.mapCanvas().setCenter(new_centre) + iface.mapCanvas().redrawAllLayers() + +iface.mapCanvas().keyReleased.connect(on_key) From da8c2955de867863b4e3654cd97eda12e8b710e7 Mon Sep 17 00:00:00 2001 From: Ana-Maria Rosu Date: Tue, 4 Apr 2023 15:04:48 +0200 Subject: [PATCH 6/7] feat(qview): enhancements 2 --- scripts/create_qgis_view.py | 286 ++++++++++++++++++++++++++---------- scripts/process_qlayers.py | 2 +- scripts/process_requests.py | 2 + 3 files changed, 210 insertions(+), 80 deletions(-) diff --git a/scripts/create_qgis_view.py b/scripts/create_qgis_view.py index 827479142..b06ddfa68 100644 --- a/scripts/create_qgis_view.py +++ b/scripts/create_qgis_view.py @@ -3,12 +3,19 @@ import argparse import re +import glob import os.path import platform +import sys from copy import deepcopy -import numpy as np from lxml import etree from osgeo import gdal +if platform.system() == 'Windows': + osgeo_root = os.environ['OSGEO4W_ROOT'] + if osgeo_root is None: + raise SystemExit("ERROR: 'OSGEO4W_ROOT' not found; unable to set 'PYTHONPATH'") + sys.path.append(osgeo_root + r'\apps\qgis\python') +# pylint: disable=locally-disabled, wrong-import-position from qgis.core import ( QgsApplication, QgsProject, @@ -26,6 +33,8 @@ from process_requests import check_get_post, response2pyobj, xml_from_wmts from process_qlayers import add_layer_to_map, create_vector, set_layer_resampling +gdal.UseExceptions() + def read_args(): """ Handle arguments """ @@ -46,10 +55,16 @@ def read_args(): parser.add_argument('-o', '--output', help="output qgis view path (default: ./view.qgs)", type=str, default='./view.qgs') - parser.add_argument('-z', '--zoom', nargs=2, - help="zoom levels as zmin zmax (default: 3025 10000000)\ - -> graph layer visibility scale [1:zmax,1:zmin]", - type=int, default=[3025, 10000000]) + parser.add_argument('-z', '--zoom_pivot', + help="layer visibility scale for surface graph [1:10000000,1:zoom_pivot]\ + & for contour graph [1:zoom_pivot,1:1] (default:3025)", + type=int, default=3025) + parser.add_argument('--vect', + help="vectors folder path", + type=str) + parser.add_argument('--bbox', nargs=4, + help="bounding box defining the view extent (Xmin Ymin Xmax Ymax)", + type=float) parser.add_argument('-m', '--macros', help="macros file path", type=str) @@ -65,7 +80,7 @@ def read_args(): def suppress_cachetag(xml_in, xml_out): - """ Suppress Cache tag from xml file """ + """ Suppress Cache tag from xml file to avoid creation of local cache """ tree_xml = etree.parse(xml_in) root_xml = tree_xml.getroot() all_cache_tags = [] @@ -75,11 +90,41 @@ def suppress_cachetag(xml_in, xml_out): for tg in all_cache_tags: root_xml.remove(tg) else: - print(f"WARNING: 'Cache' not found in '{xml_in}' => '{xml_out}' identical to '{xml_in}'") + print(f"WARNING: 'Cache' tag not found in '{xml_in}'=>'{xml_out}' identical to '{xml_in}'") + tree_xml.write(xml_out) + print(f"File '{xml_out}' written") + + +def set_extent_xml(xml_in, xml_out, extent_xmin, extent_ymin, extent_xmax, extent_ymax): + """ Set extent limits in an xml file """ + tree_xml = etree.parse(xml_in) + root_xml = tree_xml.getroot() + data_window = root_xml.iter('DataWindow') + if not data_window: + raise SystemExit(f"ERROR: 'DataWindow' tag not found in '{xml_in}'") + ul_x = root_xml.find('DataWindow/UpperLeftX') + ul_y = root_xml.find('DataWindow/UpperLeftY') + lr_x = root_xml.find('DataWindow/LowerRightX') + lr_y = root_xml.find('DataWindow/LowerRightY') + if ul_x is None or ul_y is None or lr_x is None or lr_y is None: + raise SystemExit(f"ERROR: Missing tag child in 'DataWindow' in {xml_in}'") + ul_x.text = str(extent_xmin) + ul_y.text = str(extent_ymax) + lr_x.text = str(extent_xmax) + lr_y.text = str(extent_ymin) tree_xml.write(xml_out) print(f"File '{xml_out}' written") +def modify_xml(xml_in, xml_out, extent_xmin=None, extent_ymin=None, + extent_xmax=None, extent_ymax=None): + """ Suppress cache tag and set extent in an xml file """ + suppress_cachetag(xml_in, xml_out) + if extent_xmin and extent_ymin and extent_xmax and extent_ymax: + set_extent_xml(xml_out, xml_out, extent_xmin, extent_ymin, extent_xmax, extent_ymax) + print(f"File '{xml_out}' written") + + ARG = read_args() @@ -89,12 +134,6 @@ def print_info_add_layer(layer_name): print(f"-> '{layer_name}' layer added to view") -def print_info_visib_scale(layer_name, zmin, zmax): - """ print info on visibility scale """ - if ARG.verbose > 0: - print(f'\t{layer_name} layer visibility scale: [1:{zmax},1:{zmin}]') - - # check input url url_pattern = r'^https?:\/\/[0-9A-z.]+\:[0-9]+$' if not re.match(url_pattern, ARG.url): @@ -121,20 +160,51 @@ def print_info_visib_scale(layer_name, zmin, zmax): if not os.path.isdir(dirpath_out): raise SystemExit(f"ERROR: '{dirpath_out}' is not a valid directory") +# check zoom_pivot input +if ARG.zoom_pivot not in range(1, 10000000): + raise SystemExit(f"ERROR: zoom_pivot={ARG.zoom_pivot} invalid value") + +# check external vectors input +list_vect = [] +if ARG.vect: + dirpath_vect = os.path.normpath(ARG.vect) + if not os.path.isdir(dirpath_vect): + raise SystemExit(f"ERROR: Unable to open vectors directory '{ARG.vect}'") + list_gpkg_vect = glob.glob(os.path.join(dirpath_vect, '*.gpkg')) + list_shp_vect = glob.glob(os.path.join(dirpath_vect, '*.shp')) + list_vect = [*list_gpkg_vect, *list_shp_vect] + if len(list_vect) == 0: + raise SystemExit(f"ERROR: No gpkg, nor shp files in '{ARG.vect}'") + # get info from overviews file req_get_overviews = ARG.url + '/json/overviews?cachePath=' + str(cache['path']) resp_get_overviews = check_get_post(req_get_overviews) overviews = resp_get_overviews.json() -slab_width = overviews['slabSize']['width'] -slab_height = overviews['slabSize']['height'] -if slab_width is None or slab_height is None: - raise SystemExit(f"ERROR: No 'slabSize' values in '{overviews}'!") -if slab_width != slab_height: - print(f"WARNING: Slab width(={slab_width}) <> height(={slab_height}) \ -in '{overviews}'!") tms = overviews['identifier'] if tms is None: - raise SystemExit(f"ERROR: No 'identifier' value in '{overviews}'") + raise SystemExit("ERROR: No 'identifier' value in overviews") +dataset_bbox = overviews['dataSet']['boundingBox'] +dataset_bbox_lowc = dataset_bbox['LowerCorner'] +dataset_bbox_upc = dataset_bbox['UpperCorner'] +if dataset_bbox is None or dataset_bbox_lowc is None or dataset_bbox_upc is None: + raise SystemExit("ERROR: Incorrect 'boundingBox' values in overviews") +ds_extent = [dataset_bbox_lowc[0], dataset_bbox_lowc[1], dataset_bbox_upc[0], dataset_bbox_upc[1]] + +# check bbox input +bbox_xmin = bbox_ymin = bbox_xmax = bbox_ymax = None +bbox = None +if ARG.bbox: + bbox_coord = ARG.bbox + if any(coord < 0 for coord in bbox_coord): + raise SystemExit(f"ERROR: Negative value in '{bbox_coord}'") + bbox_xmin = min(bbox_coord[0], bbox_coord[2]) + bbox_xmax = max(bbox_coord[0], bbox_coord[2]) + bbox_ymin = min(bbox_coord[1], bbox_coord[3]) + bbox_ymax = max(bbox_coord[1], bbox_coord[3]) + bbox = [bbox_xmin, bbox_ymin, bbox_xmax, bbox_ymax] + if bbox_xmin < dataset_bbox_lowc[0] or bbox_ymin < dataset_bbox_lowc[1] or \ + bbox_xmax > dataset_bbox_upc[0] or bbox_ymax > dataset_bbox_upc[1]: + raise SystemExit(f"ERROR: Input bbox '{bbox} exceeds dataset extent '{ds_extent}'") # ---------- create new branch on cache ---------- req_post_branch = ARG.url + '/branch?name=' + branch_name + \ @@ -147,26 +217,26 @@ def print_info_visib_scale(layer_name, zmin, zmax): branch_id = branch['id'] print(f"Branch '{branch_name}' created (idBranch={branch_id}) on cache '{cache['name']}'") -# ---------- export ortho and graph xml --------- +# ---------- create ortho and graph xml --------- +# export ortho and graph xml wmts_url = f'WMTS:{ARG.url}/{branch_id}/wmts?SERVICE=WMTS&REQUEST=GetCapabilities&VERSION=1.0.0' wmts_ortho = f'{wmts_url},layer=ortho,style={ARG.style_ortho}' wmts_graph = f'{wmts_url},layer=graph' - -xml_ortho_tmp = dirpath_out + '/ortho_tmp.xml' -xml_graph_tmp = dirpath_out + '/graphe_surface_tmp.xml' +xml_ortho_tmp = os.path.join(dirpath_out, 'ortho_tmp.xml') +xml_graph_tmp = os.path.join(dirpath_out, 'graphe_surface_tmp.xml') xml_from_wmts(wmts_ortho, xml_ortho_tmp) xml_from_wmts(wmts_graph, xml_graph_tmp) - -# suppress Cache tag from previous graph and ortho xml to avoid creation of local cache -xml_ortho = dirpath_out + '/ortho.xml' -xml_graph = dirpath_out + '/graphe_surface.xml' -suppress_cachetag(xml_ortho_tmp, xml_ortho) -suppress_cachetag(xml_graph_tmp, xml_graph) -# TODO: suppress xml_ortho_tmp and xml_graph_tmp +# suppress Cache tag and set extent +xml_ortho = os.path.join(dirpath_out, 'ortho.xml') +xml_graph = os.path.join(dirpath_out, 'graphe_surface.xml') +modify_xml(xml_ortho_tmp, xml_ortho, bbox_xmin, bbox_ymin, bbox_xmax, bbox_ymax) +modify_xml(xml_graph_tmp, xml_graph, bbox_xmin, bbox_ymin, bbox_xmax, bbox_ymax) +# TODO: suppress xml ortho_tmp and graph_tmp - for now, useful for comparison # --------- create contours vrt from graph.xml ----------- -vrt_tmp = dirpath_out + '/graphe_contour_tmp.vrt' -ds = gdal.BuildVRT(vrt_tmp, xml_graph) +vrt_tmp = os.path.join(dirpath_out, 'graphe_contour_tmp.vrt') +ds_options = gdal.BuildVRTOptions(outputBounds=bbox) +ds = gdal.BuildVRT(vrt_tmp, xml_graph, options=ds_options) ds = None print(f"File '{vrt_tmp}' written") # modify vrt @@ -223,13 +293,12 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast pycode2.text = etree.CDATA(data) else: raise SystemExit(f"ERROR: 'VRTRasterBand' not found in '{vrt_tmp}'") - -vrt_final = dirpath_out + '/graphe_contour.vrt' +vrt_final = os.path.join(dirpath_out, 'graphe_contour.vrt') etree.tail = '\n' etree.indent(root) tree.write(vrt_final) print(f"File '{vrt_final}' written") -# TODO: suppress vrt_tmp +# TODO: suppress vrt_tmp - for now, useful for comparison # --------------- create qgis view ------------- if platform.system() == 'Linux': @@ -240,35 +309,47 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast qgs.initQgis() project = QgsProject.instance() -# ---- add ortho layer to map ---- +# ------ create group for ortho elements -------- +ortho_group = project.layerTreeRoot().insertGroup(1, 'ORTHOS') +# --- add ortho layer to group --- ortho_lname = 'ortho' ortho_layer = add_layer_to_map(xml_ortho, ortho_lname, project, 'gdal') # set resampling set_layer_resampling(ortho_layer) +# add to group +ortho_group.insertChildNode(1, QgsLayerTreeLayer(ortho_layer)) print_info_add_layer(ortho_lname) - # get crs from layer crs = ortho_layer.crs() # set project crs project.setCrs(QgsCoordinateReferenceSystem(crs)) - -# set extent -canvas = QgsMapCanvas() -canvas.setExtent(ortho_layer.extent()) -canvas.refresh() +# --- add opi layer to group --- +opi_name = next(iter(overviews['list_OPI'])) # get first opi name +opi_uri_params = f'crs={crs.authid()}&format=image/png&layers=opi&'\ + f'styles={ARG.style_ortho}&tileDimensions=Name={opi_name}&'\ + f'tileMatrixSet={tms}&'\ + f'url={ARG.url}/{branch_id}/wmts' +opi_lname = 'OPI' +opi_layer = add_layer_to_map(opi_uri_params, opi_lname, project, 'wms') +opi_layer.renderer().setOpacity(0.5) +# set resampling +set_layer_resampling(opi_layer) +# add to group +ortho_group.insertChildNode(0, QgsLayerTreeLayer(opi_layer)) +project.layerTreeRoot().findLayer(opi_layer).setItemVisibilityChecked(False) +print_info_add_layer(opi_lname) # ------ create group for graph elements -------- graph_group = project.layerTreeRoot().insertGroup(0, 'GRAPHE') graph_group.setExpanded(False) - # --- create graph layer and add to group ---- graph_lname = 'graphe_surface' -graph_layer = add_layer_to_map(xml_graph, graph_lname, project, 'gdal', show=False) +graph_layer = add_layer_to_map(xml_graph, graph_lname, project, 'gdal') graph_layer.renderer().setOpacity(0.3) graph_layer.setScaleBasedVisibility(True) -# check and set zoom min, max inputs for visibility scale of graph layer -zoom_min_graph, zoom_max_graph = ARG.zoom if ARG.zoom[0] <= ARG.zoom[1]\ - else (ARG.zoom[1], ARG.zoom[0]) +# set visibility scale +zoom_min_graph = ARG.zoom_pivot +zoom_max_graph = 10000000 graph_layer.setMinimumScale(zoom_max_graph) graph_layer.setMaximumScale(zoom_min_graph) # set resampling @@ -276,14 +357,12 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast # add to group graph_group.insertChildNode(0, QgsLayerTreeLayer(graph_layer)) print_info_add_layer(graph_lname) -print_info_visib_scale(graph_lname, zoom_min_graph, zoom_max_graph) - # --- create contour layer and add to group ---- contour_lname = 'graphe_contour' -contour_layer = add_layer_to_map(vrt_final, contour_lname, project, 'gdal', show=False) -# set zoom min, max for visibility scale of contour layer -zoom_max_contour = zoom_min_graph -zoom_min_contour = int(np.floor(zoom_max_contour/slab_width)) +contour_layer = add_layer_to_map(vrt_final, contour_lname, project, 'gdal') +# set visibility scale +zoom_max_contour = ARG.zoom_pivot +zoom_min_contour = 1 contour_layer.setScaleBasedVisibility(True) contour_layer.setMinimumScale(zoom_max_contour) contour_layer.setMaximumScale(zoom_min_contour) @@ -299,43 +378,87 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast # add to group graph_group.insertChildNode(1, QgsLayerTreeLayer(contour_layer)) print_info_add_layer(contour_lname) -print_info_visib_scale(contour_lname, zoom_min_contour, zoom_max_contour) - -# ---- add opi layer to map ---- -# get 1st opi -opi_name = next(iter(overviews['list_OPI'])) -opi_uri_params = f'crs={crs.authid()}&format=image/png&layers=opi&'\ - f'styles={ARG.style_ortho}&tileDimensions=Name={opi_name}&'\ - f'tileMatrixSet={tms}&'\ - f'url={ARG.url}/{branch_id}/wmts' -opi_lname = 'OPI' -opi_layer = add_layer_to_map(opi_uri_params, opi_lname, project, 'wms') -opi_layer.renderer().setOpacity(0.5) -project.layerTreeRoot().findLayer(opi_layer).setItemVisibilityChecked(False) -print_info_add_layer(opi_lname) -# ---- create patches layer and add to map ----- -patches_fname = dirpath_out + '/retouches_graphe.gpkg' +# ------ create group for patches elements -------- +patch_group = project.layerTreeRoot().insertGroup(0, 'SAISIE') +# --- create patches layer and add to group ---- +patches_fname = os.path.join(dirpath_out, 'retouches_graphe.gpkg') patches_fields = QgsFields() patches_fields.append(QgsField('fid', QVariant.Int)) -patches_geom_type = QgsWkbTypes.Polygon -create_vector(patches_fname, patches_fields, patches_geom_type, crs, project) +create_vector(patches_fname, patches_fields, QgsWkbTypes.Polygon, crs, project) patches_lname = 'retouches_graphe' patches_layer = add_layer_to_map(patches_fname, patches_lname, project, 'ogr', is_raster=False) +# add to group +patch_group.insertChildNode(1, QgsLayerTreeLayer(patches_layer)) print_info_add_layer(patches_lname) - -# ---- create advancement layer and add to map ----- -advancement_fname = dirpath_out + '/avancement.gpkg' +# --- create infographic patches layer and add to group ---- +patches_infogr_fname = os.path.join(dirpath_out, 'retouches_info.gpkg') +patches_infogr_fields = QgsFields() +patches_infogr_fields.append(QgsField('fid', QVariant.Int)) +create_vector(patches_infogr_fname, patches_infogr_fields, QgsWkbTypes.Polygon, crs, project) +patches_infogr_lname = 'retouches_info' +patches_infogr_layer = add_layer_to_map(patches_infogr_fname, patches_infogr_lname, + project, 'ogr', is_raster=False) +# add to group +patch_group.insertChildNode(1, QgsLayerTreeLayer(patches_infogr_layer)) +print_info_add_layer(patches_infogr_lname) +# --- create remarks layer and add to group ---- +remarks_fname = os.path.join(dirpath_out, 'remarques.gpkg') +remarks_fields = QgsFields() +attr_comment = QgsField('commentaire', QVariant.String) +attr_comment.setLength(255) +remarks_fields.append(attr_comment) +attr_default = QgsField('defaut', QVariant.String) +attr_default.setLength(255) +remarks_fields.append(attr_default) +create_vector(remarks_fname, remarks_fields, QgsWkbTypes.Point, crs, project) +remarks_lname = 'remarques' +remarks_layer = add_layer_to_map(remarks_fname, remarks_lname, + project, 'ogr', is_raster=False) +# add to group +patch_group.insertChildNode(0, QgsLayerTreeLayer(remarks_layer)) +print_info_add_layer(remarks_lname) + +# ------ create group for information elements -------- +info_group = project.layerTreeRoot().insertGroup(1, 'INFOS') +# --- create info save layer and add to group ---- +info_save_fname = os.path.join(dirpath_out, 'retouches_info_sauv.gpkg') +info_save_fields = QgsFields() +attr_name = QgsField('NOM', QVariant.String) +attr_name.setLength(20) +info_save_fields.append(attr_name) +create_vector(info_save_fname, info_save_fields, QgsWkbTypes.Polygon, crs, project) +info_save_lname = 'retouches_info_sauv' +info_save_layer = add_layer_to_map(info_save_fname, info_save_lname, + project, 'ogr', is_raster=False) +# add to group +info_group.insertChildNode(0, QgsLayerTreeLayer(info_save_layer)) +print_info_add_layer(info_save_lname) +# --- create advancement layer and add to group ---- +advancement_fname = os.path.join(dirpath_out, 'avancement.gpkg') advancement_fields = QgsFields() -advancement_fields.append(QgsField('fid', QVariant.Int)) -advancement_geom_type = QgsWkbTypes.Polygon -create_vector(advancement_fname, advancement_fields, advancement_geom_type, crs, project) +advancement_fields.append(QgsField('H_SAISIE', QVariant.DateTime)) +create_vector(advancement_fname, advancement_fields, QgsWkbTypes.Polygon, crs, project) advancement_lname = 'avancement' advancement_layer = add_layer_to_map(advancement_fname, advancement_lname, project, 'ogr', is_raster=False) +# add to group +info_group.insertChildNode(1, QgsLayerTreeLayer(advancement_layer)) print_info_add_layer(advancement_lname) +# ------ create group for external vectors -------- +if len(list_vect) > 0: + vect_group = project.layerTreeRoot().insertGroup(2, 'VECTEURS') + for vect in list_vect: + # create layer + vect_lname = os.path.basename(vect).split('.')[0] + vect_layer = add_layer_to_map(vect, vect_lname, project, 'ogr', is_raster=False) + # add to group + vect_group.insertChildNode(1, QgsLayerTreeLayer(vect_layer)) + if ARG.verbose > 0: + print('-> vector layers added to view') + # ---- add macros to map ---- if ARG.macros: # adapt macros to working data @@ -357,6 +480,11 @@ def color_to_contour(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, rast if ARG.verbose > 0: print('-> macros added to view') +# ---- set canvas extent ---- +canvas = QgsMapCanvas() +canvas.setExtent(ortho_layer.extent()) +canvas.refresh() + # ---- write qgis view output file ---- project.write(ARG.output) print(f"File '{ARG.output}' written") diff --git a/scripts/process_qlayers.py b/scripts/process_qlayers.py index 02bd32e3d..c1f86c338 100644 --- a/scripts/process_qlayers.py +++ b/scripts/process_qlayers.py @@ -5,7 +5,7 @@ def add_layer_to_map(data_source, layer_name, qgs_project, provider_name, - is_raster=True, show=True): + is_raster=True, show=False): """ add layer to map """ layer = QgsRasterLayer(data_source, layer_name, provider_name) if is_raster\ else QgsVectorLayer(data_source, layer_name, provider_name) diff --git a/scripts/process_requests.py b/scripts/process_requests.py index 3ed1f1df1..303568772 100644 --- a/scripts/process_requests.py +++ b/scripts/process_requests.py @@ -5,6 +5,8 @@ import requests from osgeo import gdal +gdal.UseExceptions() + def check_get_post(req, is_get=True): """ Check GET or POST request """ From 8ca366229b9355b5da4170d432bf5d843c9a1cc2 Mon Sep 17 00:00:00 2001 From: Ana-Maria Rosu Date: Tue, 11 Apr 2023 14:46:51 +0200 Subject: [PATCH 7/7] docs(qview): info on enhancements 2 --- README.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6df9597d9..bd0148cb6 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ Si un cache a une taille de dalle (slabSize) différente de 16x16 tuiles ou une Dans le cas d'utilisation d'un client pour PackO basé sur QGIS, on peut créer automatiquement la vue contenant les éléments du chantier en utilisant le script **create_qgis_view.py** : ```` -usage: create_qgis_view.py [-h] [-u URL] -c CACHE_ID [-b BRANCH_NAME] [-s {RVB,IR,IRC}] [-o OUTPUT] [-z ZOOM ZOOM] [-m MACROS] [-v VERBOSE] +usage: create_qgis_view.py [-h] [-u URL] -c CACHE_ID [-b BRANCH_NAME] [-s {RVB,IR,IRC}] [-o OUTPUT] [-z ZOOM_PIVOT] [--vect VECT] [--bbox BBOX BBOX BBOX BBOX] [-m MACROS] [-v VERBOSE] options: -h, --help show this help message and exit @@ -324,8 +324,11 @@ options: style for ortho to be exported to xml (default: RVB) -o OUTPUT, --output OUTPUT output qgis view path (default: ./view.qgs) - -z ZOOM ZOOM, --zoom ZOOM ZOOM - zoom levels as zmin zmax (default: 3025 10000000) -> graph layer visibility scale [1:zmax,1:zmin] + -z ZOOM_PIVOT, --zoom_pivot ZOOM_PIVOT + layer visibility scale for surface graph [1:10000000,1:zoom_pivot] & for contour graph [1:zoom_pivot,1:1] (default:3025) + --vect VECT vectors folder path + --bbox BBOX BBOX BBOX BBOX + bounding box defining the view extent (Xmin Ymin Xmax Ymax) -m MACROS, --macros MACROS macros file path -v VERBOSE, --verbose VERBOSE @@ -335,13 +338,20 @@ où **-c** est l'identifiant du cache de travail dans la base de données : pour Les éléments de la vue générés avec ce script sont : - une nouvelle branche PackO créée sur le cache indiqué ; le nom de la branche est par défaut "newBranch", nom de branche à indiquer avec **-b**. -- **ortho.xml** et **graph.xml** : les couches ortho et graphe de la nouvelle branche, exportées sous forme de fichiers xml plus des modifications pour QGIS, dans le dossier de sortie (le chemin de la vue à indiquer avec **-o**). Pour l'ortho, si le style est différent de celui par défaut ("RVB"), il faut l'indiquer avec **-s**. L'échelle de visibilité de la couche *graphe* est définie avec **-z**. -- **graph_contour.vrt** : la couche contour de graphe générée à partir de graph.xml avec des ajouts et modifications pour QGIS, dans le dossier de sortie. L'échelle de visibilité de la couche *graphe_contour* est définie à partir de celle de la couche *graphe* -- **patches.gpkg** : la couche vecteur, initialement vide, utilisée pour les retouches -- **avancement.gpkg** : la couche vecteur, initialement vide, utilisée pour garder la trace des zones contrôlées +- **ortho.xml** et **graphe_surface.xml** : couches ortho et graphe de la nouvelle branche, exportées sous forme de fichiers xml plus des modifications pour QGIS, dans le dossier de sortie (le chemin de la vue à indiquer avec **-o**). Pour l'ortho, si le style est différent de celui par défaut ("RVB"), il faut l'indiquer avec **-s**. L'échelle de visibilité de la couche *graphe_surface* est définie avec l'option **-z** (zoom_pivot, par défaut 3025) : [1:10000000, 1:zoom_pivot]. +- **graphe_contour.vrt** : couche contour de graphe générée à partir de graphe_surface.xml avec des ajouts et modifications pour QGIS, dans le dossier de sortie. L'échelle de visibilité de la couche *graphe_contour* : [1:zoom_pivot, 1:1] +- **retouches_graphe.gpkg** : couche vecteur, initialement vide, utilisée pour les retouches du graphe +- **avancement.gpkg** : couche vecteur, initialement vide, utilisée pour garder la trace des zones contrôlées +- **retouches_info.gpkg** : couche vecteur, initialement vide, utilisée pour les retouches infographiques +- **retouches_info_sauv.gpkg** : couche vecteur, initialement vide, utilisée pour les sauvegardes liées aux retouches infographiques +- **remarques.gpkg** : couche vecteur, initialement vide, utilisée pour les remarques sur la vue Ces éléments sont des couches de la vue PackO pour QGIS (par défaut **view.qgs**), auxquelles s'ajoute une couche OPI générée en important la couche WMTS OPI de la branche du cache. +Des vecteurs externes *.shp* et *.gpkg* peuvent être intégrés à la vue en indiquant le chemin vers le dossier les contenant avec **--vect**. + +Dans le cas où l'on veut avoir une emprise pour la vue (emprise de travail) plus petite que l'emprise du chantier (emprise du cache), elle est à indiquer avec **--bbox** Xmin Ymin Xmax Ymax. + Pour intégrer un fichier de macros QGIS à la vue, il faut indiquer le chemin vers le fichier macros prototype avec **-m**. Ce fichier sera adapté au chantier avant d'être intégré à la vue, en remplaçant les clés `__IDBRANCH__`, `__URLSERVER__` et `__TILEMATRIXSET__` avec les valeurs correspondantes pour le chantier - exemple : - Extrait prototype macros, avant adaptation : @@ -359,10 +369,10 @@ Pour intégrer un fichier de macros QGIS à la vue, il faut indiquer le chemin v Un exemple de fichier macros prototype est fourni dans le dossier *ressources*. -Pour le bon fonctionnement dans QGIS, il est impératif de mettre la variable d'environnement **GDAL_VRT_ENABLE_PYTHON** à **YES**. Il faut également définir les variables d'environnement (où `` doit être remplacé par le chemin d'accès au dossier d'installation de QGIS ; exemples de `` sous Linux : **/usr** , sous Windows **C:\Program Files\QGIS XXX\apps\qgis\python** où 'XXX' est à remplacer avec la version de QGIS) : +Pour le bon fonctionnement dans QGIS, il est impératif de mettre la variable d'environnement **GDAL_VRT_ENABLE_PYTHON** à **YES**. Il faut également définir les variables d'environnement (où `` doit être remplacé par le chemin d'accès au dossier d'installation de QGIS ; exemples de `` sous Linux : **/usr** , sous Windows **C:\Program Files\QGIS XXX\apps\qgis** où 'XXX' est à remplacer avec la version de QGIS) : - **PYTHONPATH** : - sous Linux : `export PYTHONPATH=//share/qgis/python` - - sous Windows : `set PYTHONPATH=C:\\python` + - sous Windows : `set PYTHONPATH=C:\\python` ; sous windows, si elle n'existe pas, création automatique de cette variable d'environnement à partir de la variable d'environnement OSGEO4W_ROOT. - **LD_LIBRARY_PATH** : - sous Linux : `export LD_LIBRARY_PATH=//lib` - sous Windows: `set PATH=C:\\bin;C:\\apps\\bin;%PATH%` (où `` devrait être remplacé avec le type de release ciblé (ex : qgis-ltr, qgis, qgis-dev)