From 63cfbd2cb1602b21fc3b1d26a9796bba196be911 Mon Sep 17 00:00:00 2001 From: "a.adell" Date: Tue, 2 Apr 2024 13:17:13 +0200 Subject: [PATCH] Fixed timeout issue scanning resources on plugin initialization (non-accessible resources will not be shown) --- data/product_metadata.json | 4 +- metadata.txt | 7 +- openicgc.py | 52 +++--- qlib3/base/datedialog.py | 8 +- qlib3/base/multipleinputdialog.py | 272 ++++++++++++++++++++++++++++++ qlib3/base/pluginbase.py | 146 ++++++++++++---- resources3/fme.py | 14 +- resources3/http.py | 15 +- 8 files changed, 454 insertions(+), 64 deletions(-) create mode 100644 qlib3/base/multipleinputdialog.py diff --git a/data/product_metadata.json b/data/product_metadata.json index 1b57823..fc6fea9 100644 --- a/data/product_metadata.json +++ b/data/product_metadata.json @@ -200,14 +200,14 @@ "Cobertura": "Sèrie mensual IRC", "Carpeta": "Satellite infrared orthophoto (monthly serie)", "Formats de fitxer": "", - "Metadades": "https://catalegs.ide.cat/geonetwork/srv/cat/catalog.search#/search?type=dataset&title=sentinel-2-mensual&sortBy=changeDate" + "Metadades": "https://catalegs.ide.cat/geonetwork/srv/cat/catalog.search#/metadata/ortoimatge-sentinel2-mensual-v1r0" }, { "Producte": "Ortoimatge de satèl·lit Sentinel-2 mensual 10 m", "Cobertura": "Sèrie mensual RGB", "Carpeta": "Satellite color orthophoto (monthly serie)", "Formats de fitxer": "", - "Metadades": "https://catalegs.ide.cat/geonetwork/srv/cat/catalog.search#/search?type=dataset&title=sentinel-2-mensual&sortBy=changeDate" + "Metadades": "https://catalegs.ide.cat/geonetwork/srv/cat/catalog.search#/metadata/ortoimatge-sentinel2-mensual-v1r0" }, { "Producte": "Piràmide del mapa topogràfic", diff --git a/metadata.txt b/metadata.txt index 448e497..0d872e1 100644 --- a/metadata.txt +++ b/metadata.txt @@ -28,8 +28,11 @@ email=qgis.openicgc@icgc.cat qgisMinimumVersion=2.99 qgisMaximumVersion=3.99 -version=1.1.16 -changelog=v1.1.16 (2023-09-27) +version=1.1.17 +changelog=v1.1.17 (2024-04-02) + - Fixed timeout issue scanning resources on plugin initialization (non-accessible resources will not be shown) + + v1.1.16 (2023-09-26) - Added UTM (MGRS) grids 1x1 km and 10x10 km v1.1.15 (2023-05-30) diff --git a/openicgc.py b/openicgc.py index d54129f..25a4afd 100644 --- a/openicgc.py +++ b/openicgc.py @@ -669,7 +669,8 @@ def initGui(self, check_qgis_updates=True, check_icgc_updates=False): self.default_map_callback, QIcon(":/lib/qlib3/base/images/cat_topo250k.png"), self.manage_metadata_button("Topographic map (topographical pyramid)"), True), - (self.tr("Territorial topographic referential"), None, QIcon(":/lib/qlib3/base/images/cat_topo5k.png"), enable_http_files, [ + (self.tr("Territorial topographic referential"), None, QIcon(":/lib/qlib3/base/images/cat_topo5k.png"), \ + enable_http_files and len(topo5k_time_series_list) > 0, [ (self.tr("Territorial topographic referential %s (temporal serie)") % topo5k_year, lambda _checked, topo5k_year=topo5k_year:self.add_wms_t_layer(self.tr("[TS] Territorial topographic referential"), None, topo5k_year, None, "default", "image/png", topo5k_time_series_list[::-1], None, 25831, "referer=ICGC&bgcolor=0x000000", self.backgroup_map_group_name, only_one_map_on_group=False, resampling_bilinear=True, set_current=True), QIcon(":/lib/qlib3/base/images/cat_topo5k.png"), @@ -773,7 +774,7 @@ def initGui(self, check_qgis_updates=True, check_icgc_updates=False): self.manage_metadata_button("NDVI (temporal serie)"), True), (self.tr("NDVI (temporal serie)"), lambda _checked:self.add_wms_t_layer(self.tr("[TS] NDVI"), None, ndvi_current_time, None, "default", "image/png", ndvi_time_series_list, None, 25831, "referer=ICGC&bgcolor=0x000000", self.backgroup_map_group_name, only_one_map_on_group=False, set_current=True), - QIcon(":/lib/qlib3/base/images/cat_shadows.png"), enable_http_files, + QIcon(":/lib/qlib3/base/images/cat_shadows.png"), enable_http_files and len(ndvi_time_series_list) > 0, self.manage_metadata_button("NDVI (temporal serie)"), True), "---", (self.tr("Color orthophoto"), None, QIcon(":/lib/qlib3/base/images/cat_ortho5k.png"), [ @@ -1025,7 +1026,11 @@ def get_download_menu(self, fme_services_list, raster_not_vector=None, nested_do """ Create download submenu structure list """ # Filter data type if required if raster_not_vector is not None: - fme_services_list = [(id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, filename, limits, url_pattern, url_ref_or_wms_tuple) for id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, filename, limits, url_pattern, url_ref_or_wms_tuple in fme_services_list if self.is_raster_file(filename) == raster_not_vector] + fme_services_list = [(id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, \ + filename, limits, url_pattern, url_ref_or_wms_tuple, enabled) \ + for id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, \ + filename, limits, url_pattern, url_ref_or_wms_tuple, enabled \ + in fme_services_list if self.is_raster_file(filename) == raster_not_vector] # Define text labels common_label = "%s" @@ -1037,12 +1042,14 @@ def get_download_menu(self, fme_services_list, raster_not_vector=None, nested_do # Prepare nested download submenu if nested_download_submenu: # Add a end null entry - fme_extra_services_list = fme_services_list + [(None, None, None, None, None, None, None, None, None, None, None, None, None)] + fme_extra_services_list = fme_services_list + [ \ + (None, None, None, None, None, None, None, None, None, None, None, None, None, None)] download_submenu = [] product_submenu = [] gsd_info_dict = {} # Create menu with a submenu for every product prefix - for i, (id, _name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, filename, limits, url_pattern, url_ref_or_wms_tuple) in enumerate(fme_extra_services_list): + for i, (id, _name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, \ + filename, limits, url_pattern, url_ref_or_wms_tuple, enabled) in enumerate(fme_extra_services_list): prefix_id = id[:2] if id else None previous_id = fme_extra_services_list[i-1][0] if i > 0 else id previous_prefix_id = previous_id[:2] @@ -1057,19 +1064,21 @@ def get_download_menu(self, fme_services_list, raster_not_vector=None, nested_do else: previous_name1 = self.FME_NAMES_DICT.get(fme_extra_services_list[i-1][0], fme_extra_services_list[i-1][1]) previous_name2 = self.FME_NAMES_DICT.get(fme_extra_services_list[i-2][0], fme_extra_services_list[i-2][1]) - diff_list = [pos for pos in range(min(len(previous_name1), len(previous_name2))) if previous_name1[pos] != previous_name2[pos]] + diff_list = [pos for pos in range(min(len(previous_name1), len(previous_name2))) \ + if previous_name1[pos] != previous_name2[pos]] pos = diff_list[0] if diff_list else min(len(previous_name1), len(previous_name2)) common_name = previous_name1[:pos].replace("1:", "").strip() # Create submenu if gsd_info_dict: # Add single ménu entry with GDS info dict previous_time_list = list(gsd_info_dict.values())[0][6] + previous_enabled = any([info[-1] for info in gsd_info_dict.values()]) download_submenu.append(( product_file_label_pattern % (common_name, os.path.splitext(filename)[1][1:]), (lambda _dummy, id=previous_prefix_id, name=common_name, time_list=previous_time_list, gsd_info_dict=gsd_info_dict: \ self.enable_download_subscene(id, name, None, None, None, None, time_list, None, None, None, None, gsd_info_dict), self.pair_download_checks), QIcon(self.FME_ICON_DICT.get(previous_prefix_id, None)), - True, True, previous_prefix_id, + previous_enabled, True, previous_prefix_id, self.manage_metadata_button(self.FME_METADATA_DICT.get(previous_id, None) \ or self.FME_METADATA_DICT.get(previous_prefix_id, None)), True @@ -1094,7 +1103,8 @@ def get_download_menu(self, fme_services_list, raster_not_vector=None, nested_do file_label = product_file_label_pattern % (self.FME_NAMES_DICT.get(id, id), os.path.splitext(filename)[1][1:]) if gsd: # Store product info in GSD dict - gsd_info_dict[gsd] = (id, file_label, min_side, max_query_area, min_px_side, max_px_area, time_list, download_list, filename, limits, url_ref_or_wms_tuple) + gsd_info_dict[gsd] = (id, file_label, min_side, max_query_area, min_px_side, max_px_area, \ + time_list, download_list, filename, limits, url_ref_or_wms_tuple, enabled) else: # Add entry to temporal product submenu product_submenu.append(( @@ -1102,7 +1112,7 @@ def get_download_menu(self, fme_services_list, raster_not_vector=None, nested_do (lambda _dummy, id=id, name=self.FME_NAMES_DICT.get(id, id), min_side=min_side, max_query_area=max_query_area, min_px_side=min_px_side, max_px_area=max_px_area, time_list=time_list, download_list=download_list, filename=filename, limits=limits, url_ref_or_wms_tuple=url_ref_or_wms_tuple : \ self.enable_download_subscene(id, name, min_side, max_query_area, min_px_side, max_px_area, time_list, download_list, filename, limits, url_ref_or_wms_tuple), self.pair_download_checks), QIcon(self.FME_ICON_DICT.get(prefix_id, None)), - True, True, id, # Indiquem: actiu, checkable i un id d'acció + enabled, True, id, # Indiquem: actiu, checkable i un id d'acció self.manage_metadata_button(self.FME_METADATA_DICT.get(id, None) \ or self.FME_METADATA_DICT.get(prefix_id, None)), True @@ -1112,26 +1122,30 @@ def get_download_menu(self, fme_services_list, raster_not_vector=None, nested_do else: fme_extra_services_list = [] # Add separators on change product prefix - for i, (id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, filename, limits, url_pattern, url_ref_or_wms_tuple) in enumerate(fme_services_list): # 7 params + for i, (id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, \ + filename, limits, url_pattern, url_ref_or_wms_tuple, enabled) in enumerate(fme_services_list): prefix_id = id[:2] if id else None previous_id = fme_extra_services_list[i-1][0] if i > 0 else id previous_prefix_id = previous_id[:2] if previous_id else None # If change 2 first characters the inject a separator if prefix_id != previous_prefix_id: - fme_extra_services_list.append((None, None, None, None, None, None, None, None, None, None, None)) # 10 + 1 (vectorial_not_raster) + fme_extra_services_list.append((None, None, None, None, None, None, None, None, \ + None, None, None, None)) # 11 + 1 (vectorial_not_raster) vectorial_not_raster = not self.is_raster_file(filename) - fme_extra_services_list.append((id, name, min_side, max_query_area, min_px_side, max_px_area, filename, limits, vectorial_not_raster, url_pattern, url_ref_or_wms_tuple)) # 8 params + fme_extra_services_list.append((id, name, min_side, max_query_area, min_px_side, max_px_area, filename, \ + limits, vectorial_not_raster, url_pattern, url_ref_or_wms_tuple, enabled)) # 12 params # Create download menu download_submenu = [ (product_file_label_pattern % (name, os.path.splitext(filename)[1][1:]), (lambda _dummy, id=id, name=name, min_side=min_side, max_query_area=max_query_area, min_px_side=min_px_side, max_px_area=max_px_area, time_list=time_list, download_list=download_list, filename=filename, limits=limits, url_ref_or_wms_tuple=url_ref_or_wms_tuple : \ self.enable_download_subscene(id, name, min_side, max_query_area, min_px_side, max_px_area, time_list, download_list, filename, limits, url_ref_or_wms_tuple), self.pair_download_checks), QIcon(self.FME_ICON_DICT.get(id[:2], None)), - True, True, id, # Indiquem: actiu, checkable i un id d'acció + enabled, True, id, # Indiquem: actiu, checkable i un id d'acció self.manage_metadata_button(self.FME_METADATA_DICT.get(id, None) \ or self.FME_METADATA_DICT.get(prefix_id, None)), True - ) if id else "---" for id, name, min_side, max_query_area, min_px_side, max_px_area, filename, limits, vectorial_not_raster, url_pattern, url_ref_or_wms_tuple in fme_extra_services_list + ) if id else "---" for id, name, min_side, max_query_area, min_px_side, max_px_area, filename, \ + limits, vectorial_not_raster, url_pattern, url_ref_or_wms_tuple, enabled in fme_extra_services_list ] return download_submenu @@ -1204,10 +1218,10 @@ def run(self, _checked=False): # I add checked param, because the mapping of the searches_list = [self.combobox.itemText(i) for i in range(self.combobox.count())][:self.combobox.maxVisibleItems()] self.set_setting_value("last_searches", searches_list) - def add_wms_t_layer(self, layer_name, url, layer_id, time, style, image_format, time_series_list=None, time_series_regex=None, epsg=None, extra_tags="", group_name="", group_pos=None, only_one_map_on_group=False, collapsed=True, visible=True, transparency=None, saturation=None, resampling_bilinear=False, resampling_cubic=False, set_current=False): + def add_wms_t_layer(self, layer_name, url, layer_id, time, style, image_format, time_series_list=None, time_series_regex=None, epsg=None, extra_tags="", group_name="", group_pos=None, only_one_map_on_group=False, only_one_visible_map_on_group=True, collapsed=True, visible=True, transparency=None, saturation=None, resampling_bilinear=False, resampling_cubic=False, set_current=False): """ Add WMS-T layer and enable timeseries dialog """ # Add WMS-T - layer = self.layers.add_wms_t_layer(layer_name, url, layer_id, time, style, image_format, time_series_list, time_series_regex, epsg, extra_tags, group_name, group_pos, only_one_map_on_group, collapsed, visible, transparency, saturation, resampling_bilinear, resampling_cubic, set_current) + layer = self.layers.add_wms_t_layer(layer_name, url, layer_id, time, style, image_format, time_series_list, time_series_regex, epsg, extra_tags, group_name, group_pos, only_one_map_on_group, only_one_visible_map_on_group, collapsed, visible, transparency, saturation, resampling_bilinear, resampling_cubic, set_current) if layer: if type(layer) in [QgsRasterLayer, QgsVectorLayer]: # Show timeseries dialog @@ -1326,11 +1340,11 @@ def enable_download_subscene(self, data_type, name, min_side, max_download_area, if gsd_dict: # With GSD dictionari, integrates all GSD years - time_list_list = [time_list or [] for _data_type, _name, _min_side, _max_download_area, _min_px_side, _max_px_area, time_list, _download_list, _filename, _limits, _url_ref_or_wms_tuple in gsd_dict.values()] + time_list_list = [time_list or [] for _data_type, _name, _min_side, _max_download_area, _min_px_side, _max_px_area, time_list, _download_list, _filename, _limits, _url_ref_or_wms_tuple, _enabled in gsd_dict.values()] time_list = sorted(list(set([item for sublist in time_list_list for item in sublist]))) if not time_list: time_list = [None] - data_dict = {year: {gsd: {description: id for id, description, operation_code in self.FME_DOWNLOADTYPE_LIST if operation_code in download_list} for (gsd, (_data_type, _name, _min_side, _max_download_area, _min_px_side, _max_px_area, _time_list, download_list, _filename, _limits, _url_ref_or_wms_tuple)) in gsd_dict.items() if not year or year in gsd_dict[gsd][6]} for year in time_list} + data_dict = {year: {gsd: {description: id for id, description, operation_code in self.FME_DOWNLOADTYPE_LIST if operation_code in download_list} for (gsd, (_data_type, _name, _min_side, _max_download_area, _min_px_side, _max_px_area, _time_list, download_list, _filename, _limits, _url_ref_or_wms_tuple, _enabled)) in gsd_dict.items() if not year or year in gsd_dict[gsd][6]} for year in time_list} else: # Without GSD dictionari download_type_dict = {description: id for id, description, operation_code in self.FME_DOWNLOADTYPE_LIST if operation_code in download_list} @@ -1352,7 +1366,7 @@ def enable_download_subscene(self, data_type, name, min_side, max_download_area, time_code = self.download_dialog.get_year() gsd = self.download_dialog.get_gsd() if gsd_dict and gsd: - data_type, name, min_side, max_download_area, min_px_side, max_px_area, time_list, download_list, filename, limits, url_ref_or_wms_tuple = gsd_dict[gsd] + data_type, name, min_side, max_download_area, min_px_side, max_px_area, time_list, download_list, filename, limits, url_ref_or_wms_tuple, enabled = gsd_dict[gsd] # Changes icon and tooltip of download button self.gui.set_item_icon("download", diff --git a/qlib3/base/datedialog.py b/qlib3/base/datedialog.py index 35c2096..bcdb407 100644 --- a/qlib3/base/datedialog.py +++ b/qlib3/base/datedialog.py @@ -33,7 +33,7 @@ def __init__(self, description=None, title=None, parent=None, datetime_not_date= # get current date and time from the dialog def dateTime(self): - return self.datetime.dateTime() + return self.datetime.dateTime().toPyDateTime() def date(self): return self.datetime.dateTime().date().toPyDate() @@ -48,6 +48,12 @@ def getDateTime(description=None, title=None, parent=None): status_ok = (dialog.exec_() == QDialog.Accepted) return (dialog.date() if status_ok else None, dialog.time() if status_ok else None, status_ok) + @staticmethod + def getDateTimeInOne(description=None, title=None, parent=None): + dialog = DateDialog(description, title, parent) + status_ok = (dialog.exec_() == QDialog.Accepted) + return (dialog.dateTime() if status_ok else None, status_ok) + @staticmethod def getDate(description=None, title=None, parent=None): dialog = DateDialog(description, title, parent, False) diff --git a/qlib3/base/multipleinputdialog.py b/qlib3/base/multipleinputdialog.py new file mode 100644 index 0000000..10e0fd5 --- /dev/null +++ b/qlib3/base/multipleinputdialog.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +""" +******************************************************************************* +Mòdul amb classe diàleg amb multiples input +--- +Module with a dialog class with multiple input + + ------------------- + begin : 2024-03-13 + author : David Sanz & Albert Adell + email : david.sanz@icgc.cat +******************************************************************************* +""" + +from datetime import date, time, datetime + +from PyQt5.QtCore import Qt, QTime, QDate, QDateTime +from PyQt5.QtWidgets import QApplication, QLineEdit, QCheckBox, QComboBox, QDialogButtonBox +from PyQt5.QtWidgets import QFormLayout, QDialog, QMessageBox, QLabel, QTimeEdit, QDateEdit, QDateTimeEdit + + +class MultipleInputDialog(QDialog): + """ Classe per generar diàlegs dinàmics a partir d'una llista de camps a demanar + + El paràmetre labels és una llista de valors o tuples de 2 o 3 elements: + labels = [, , ...] + labels = [(, ||), ...] + labels = [(, ||, ), ...] + + Si s'especifica un tipus (2n valor) es validarà que les dades introduides es corresponguin amb el tipus + Si s'especifica un valor (2n valor) s'assignarà com valor per defecte i validarà les dades segons el seu tipus + Si s'especifica una llista (2n valor) validarà les dades segons el seu tipus del primer element + Si s'especifica valor requerit (3r valors) es validarà que el camp estigui ple + El tipus de dada per defecte és str. S'accepten tipus / valors: str, int, float, bool, list, time, date, datetime + Per defecte els camps no són requerits. Els camps requerits es mostraran en negreta + No es deixarà sortir del diàleg fins que es compleixin tots els criteris o es cancel·li + + Exemples: + dlg = MultipleInputDialog(labels=[ # (Etiqueta, tipus_valor_llista, requerit) + ('Text', str, True), + ('Text2', "hola", True), + ('Int', int, True), + ('Int2', 10, True), + ('Real', float, True), + ('Real2', 1.2, True), + ('Check', bool, False), # Requerit a false activa tristate + ('Check2', True, True), + ('Llista', [1,2,3], False), # Requerit a false afegeix un element buit al principi + ('Llista2', [1,2,3], True), + ('Llista3', [True, False], True), + ]) + status_ok = dlg.do_moldal() + if status_ok: + values_list = dlg.get_values() + + dlg = MultipleInputDialog(labels=[ + ('Passada', str), + ('Codi de planificació', [11231,22323,3346]), + ], title="Planificació de la pasada") + status_ok = dlg.do_modal() + if status_ok: + values_list = dlg.get_values() + + dlg = MultipleInputDialog(labels=[ + 'Passada', + 'Codi de planificació' + ], title="Planificació de la pasada") + status_ok = dlg.do_modal() + if status_ok: + values_list = dlg.get_values() + """ + + def __init__(self, labels, title="", parent=None): + """ Constructor """ + super().__init__(parent) + self.setWindowTitle(title) + + # Obtenim la configuració dels valors a denamar + self.label_list = self.parse_labels(labels) + # Preparem una llista de valors resultats i errors tots a None + self.value_list = [None] * len(self.label_list) + self.error_list = [None] * len(self.label_list) + + # Afegim controls dinàmicament al diàleg + self.layout, self.item_list = self.create_items(self.label_list) + + def parse_labels(self, label_list): + """ Retorna una llista amb la configuració del items a generar dinàmicament + a partir de la llista d'informació de labels que passa l'usuari + label_list = [(Label:str, [type_or_value_or_list_of_values, [required:bool]]), ...] + """ + label_config_list = [] # [(label, label_type, label_default_value, label_list_type, label_required), ...] + # Recorre la llista de labels que ens passen i depenent del tipus i quantitat de valors + # obtenim la configuració per cada una de les etiquets + for label_info in label_list: + label, label_type, label_default_value, label_list_type, label_required = None, None, None, None, None + + # Segons el tipus del label_info, deduim la informació necessària + if type(label_info) is str: + label, label_type, label_required = label_info, str, False + elif type(label_info) is tuple and len(label_info) == 2: + if type(label_info[0]) is str: + label, label_type, label_required = label_info[0], label_info[1], False + elif type(label_info) is tuple and len(label_info) == 3: + if type(label_info[0]) is str and type(label_info[2]) is bool: + label, label_type, label_required = label_info + + if label: + # El segon valor pot ser un tipus o un valor per defecte + if type(label_type) is not type: + label_default_value = label_type + label_type = type(label_type) + # Si el segon valor és una llista detectem el tipus dels seus elements + if label_type is list and label_default_value: + label_list_type = type(label_default_value[0]) + # Guardem tota la informació obtinguda en una llista + label_config_list.append((label, label_type, label_default_value, label_list_type, label_required)) + else: + # Si no hem pogut deduir la configuració d'un element tornem error + raise Exception("Definició d'etiqueta desconeguda: %s" % str(label_info)) + + return label_config_list + + def create_items(self, label_list): + """ Afegim controls dinàmicament al diàleg segons la llista de labels + Retorna un layout contenidor i la llista de widgets utilitzats + """ + item_list = [] + layout = QFormLayout(self) + for label_text, label_type, label_default_value, label_list_type, label_required in label_list: + # Creem el widget corresponent al tipus de dades + if label_type is list: + item = QComboBox(self) + item.addItems(([] if label_required else [""]) + [str(v) for v in label_default_value]) + elif label_type is bool: + item = QCheckBox(self) + if not label_required: + item.setCheckState(Qt.PartiallyChecked) + if label_default_value is not None: + item.setChecked(label_default_value) + elif label_type is date: + value = label_default_value if label_default_value else date.today() + item = QDateEdit(QDate(value), self) + item.setDisplayFormat("dd/MM/yyyy") + item.setCalendarPopup(True) + elif label_type is time: + value = label_default_value if label_default_value else datetime.now().time() + item = QTimeEdit(QTime(value), self) + elif label_type is datetime: + value = label_default_value if label_default_value else datetime.now() + item = QDateTimeEdit(QDateTime(value), self) + item.setDisplayFormat("dd/MM/yyyy hh:mm:ss") + item.setCalendarPopup(True) + else: + item = QLineEdit(self) + if label_default_value is not None: + item.setText(str(label_default_value)) + # Si tenim un camp requerit el pintem en negreta + if label_required: + label = QLabel(self) + label.setText("%s" % label_text) + else: + label = label_text + layout.addRow(label, item) + # Ens guardem l'item perquè no es destrueixi + item_list.append(item) + + # Afegim els botons d'acceptar/cancel·lar + buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + layout.addWidget(buttonBox) + buttonBox.accepted.connect(self.accept) + buttonBox.rejected.connect(self.reject) + + return layout, item_list + + def parse_values(self, label_list, item_list, error_pattern="Camp \"%s\" (%s): \"%s\" - %s", \ + format_error="Format incorrecte", required_error="Valor requerit"): + """ Retorna les llistes de valors i errors segons les dades introduides + """ + value_list = [] + error_list = [] + for (label, label_type, label_default_value, label_list_type, label_required), item\ + in zip(label_list, item_list): + + error = None + try: + if label_type is str: + value = item.text() + elif label_type is bool: + value = None if item.checkState() == Qt.PartiallyChecked else item.isChecked() + elif label_type is int: + value = item.text() + try: + value = int(value) if value else None + except: + raise Exception(format_error) + elif label_type is float: + value = item.text() + try: + value = float(value) if value else None + except: + raise Exception(format_error) + elif label_type is list: + value = item.currentText() + if label_list_type is bool: + try: + value = (value == "True") if value else None + except: + raise Exception(format_error) + elif label_list_type is int: + try: + value = int(value) if value else None + except: + raise Exception(format_error) + elif label_list_type is float: + try: + value = float(value) if value else None + except: + raise Exception(format_error) + elif label_type is date: + value = item.date().toPyDate() + elif label_type is time: + value = item.time().toPyTime() + elif label_type is datetime: + value = item.dateTime().toPyDateTime() + else: + value = None + + if (value is None or value == "") and label_required: + raise Exception(required_error) + + except Exception as e: + label_type_name = str(label_type).replace("", "") + error = error_pattern % (label, label_type_name, value, str(e).strip()) + + value_list.append(value) + error_list.append(error) + + return value_list, error_list + + def accept(self): + """ Event d'acceptació del diàleg que valida els valors introduits """ + # Carreguem la llista de valors i d'errors + self.value_list, self.error_list = self.parse_values(self.label_list, self.item_list) + + # Detectem errors (error de label diferent de None) i els mostrem + display_error_list = [error for error in self.error_list if error] + if display_error_list: + QMessageBox.warning(self, "Error de dades", "\n\n".join(display_error_list)) + else: + super().accept() + + def do_modal(self): + """ Mostra el diàleg i retorna True/False segons s'ha acceptat o cancel·lat """ + self.show() + return self.exec() == QDialog.Accepted + + def get_values(self): + """ Retorna una llista amb els valors introduits """ + return self.value_list + + @classmethod + def get_inputs(cls, parent, title, labels): + """ Funció estàtica per crear un diàleg multicamp en una linia, retorna + tots els paràmetres demanats més un boleà d'acceptació o cancel·lació + del diàleg """ + dlg = cls(labels, title, parent) + status_ok = dlg.do_modal() + values_list = dlg.get_values() + values_list.append(status_ok) + return values_list + \ No newline at end of file diff --git a/qlib3/base/pluginbase.py b/qlib3/base/pluginbase.py index 72f9881..adc91e3 100644 --- a/qlib3/base/pluginbase.py +++ b/qlib3/base/pluginbase.py @@ -48,7 +48,7 @@ from PyQt5.QtCore import QVariant, QDateTime, QDate, QLocale, QUrl from PyQt5.QtWidgets import QApplication, QAction, QToolBar, QLabel, QMessageBox, QMenu, QToolButton from PyQt5.QtWidgets import QFileDialog, QWidgetAction, QDockWidget, QShortcut, QTableView -from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout +from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, QDialog from PyQt5.QtGui import QPainter, QCursor, QIcon, QColor, QKeySequence, QDesktopServices, QFontDatabase from PyQt5.QtXml import QDomDocument @@ -59,7 +59,7 @@ from qgis.core import QgsRendererCategory, QgsCategorizedSymbolRenderer, QgsRendererRange, QgsGraduatedSymbolRenderer, QgsRenderContext, QgsRendererRangeLabelFormat from qgis.core import QgsSymbol, QgsMarkerSymbol, QgsFillSymbol, QgsBilinearRasterResampler, QgsCubicRasterResampler, QgsSimpleLineSymbolLayer from qgis.core import QgsEditorWidgetSetup, QgsPrintLayout, QgsSpatialIndex, QgsFeatureRequest, QgsMapLayer, QgsField, QgsVectorFileWriter -from qgis.core import QgsLayoutExporter, QgsFields, Qgis +from qgis.core import QgsLayoutExporter, QgsFields, Qgis, QgsExpression from qgis.utils import plugins, reloadPlugin, showPluginHelp from . import resources_rc @@ -596,12 +596,13 @@ def find_action(self, id, actions_list=None): if actions_list == None: actions_list = [action for menu_or_toolbar, action in self.actions] for action in actions_list: - if action and action.objectName() == id: - return action - if action.menu(): - subaction = self.find_action(id, action.menu().actions()) - if subaction: - return subaction + if action: + if action.objectName() == id: + return action + if action.menu(): + subaction = self.find_action(id, action.menu().actions()) + if subaction: + return subaction return None def enable_gui_items(self, menu_or_toolbar, items_id_list, enable=True, recursive=True): @@ -1079,6 +1080,10 @@ def get_icon(self, icon_name): Loads icon by pathname (relative or absolut) or by name (from resource file) """ + # Verifiquem tenir extensió + if not os.path.splitext(icon_name)[1]: + icon_name += ".png" + # Mirem si ens han passat un path complet de la icona if os.path.isabs(icon_name): return QIcon(icon_name) # Cerquem a la carpeta del plugin @@ -1328,17 +1333,59 @@ def get_visible_features(self, layer, features_list): --- Returns visible layer's features (from a list) """ - visible_features = [] - renderer = layer.renderer().clone() - ctx = QgsRenderContext() - renderer.startRender(ctx, QgsFields()) - for feature in features_list: - ctx.expressionContext().setFeature(feature) - if renderer.willRenderFeature(feature, ctx): - visible_features.append(feature) - renderer.stopRender(ctx) + visible_features = [] + if layer and layer.renderer(): + renderer = layer.renderer().clone() + ctx = QgsRenderContext() + renderer.startRender(ctx, QgsFields()) + for feature in features_list: + ctx.expressionContext().setFeature(feature) + if renderer.willRenderFeature(feature, ctx): + visible_features.append(feature) + renderer.stopRender(ctx) return visible_features + def get_feature_by_id(self, idprefix, feature_id, pos=0): + """ Retorna l'element de l'id especificat + --- + Returns feature from specified id + """ + layer = self.get_by_id(idprefix, pos) + if not layer: + return None + return self.get_feature(layer, feature_id) + + def get_feature(self, layer, feature_id): + """ Retorna l'element de l'id especificat + --- + Returns feature from specified id + """ + feature_request = QgsFeatureRequest() + feature_request.setFilterFid(feature_id) + feature_list = list(layer.getFeatures(feature_request)) + return feature_list[0] if feature_list else None + + def get_feature_by_attribute_by_id(self, idprefix, field_name, field_value, pos=0): + """ Retorna l'element especificat pel valor d'attribut + --- + Returns feature from specified attribute value + """ + layer = self.get_by_id(idprefix, pos) + if not layer: + return None + return self.get_feature_by_attribute(layer, field_name, field_value) + + def get_feature_by_attribute(self, layer, field_name, field_value): + """ Retorna l'element de l'id especificat + --- + Returns feature from specified id + """ + expression = QgsExpression("%s = %s" % ( \ + field_name, "'%s'" % field_value if type(field_value)==str else field_value)) + feature_request = QgsFeatureRequest(expression) + feature_list = list(layer.getFeatures(feature_request)) + return feature_list[0] if feature_list else None + def get_feature_attribute_by_id(self, idprefix, entity, field_name, pos=0): """ Retorna el valor d'un camp (columna) d'una entitat d'una capa (fila) --- @@ -1636,17 +1683,17 @@ def get_attributes_by_area(self, layer, fields_name_list, area, area_epsg=None): area = self.parent.crs.transform_bounding_box(area, area_epsg, self.parent.layers.get_epsg(layer)) # Cerquem elements que intersequin amb l'àrea - request = QgsFeatureRequest() - request.setFilterRect(area) - features_list = layer.getFeatures(request) + feature_request = QgsFeatureRequest() + feature_request.setFilterRect(area) + feature_list = layer.getFeatures(feature_request) # Recuperem els camps demanats if len(fields_name_list) == 1: layer_selection = [self.get_feature_attribute(layer, feature, fields_name_list[0]) \ - for feature in features_list if feature.geometry().intersects(area)] + for feature in feature_list if feature.geometry().intersects(area)] else: layer_selection = [tuple([self.get_feature_attribute(layer, feature, field_name) for field_name in fields_name_list]) \ - for feature in features_list if feature.geometry().intersects(area)] + for feature in feature_list if feature.geometry().intersects(area)] return layer_selection def refresh_by_id(self, layer_idprefix, unselect=False, pos=0): @@ -3768,7 +3815,7 @@ def add_remote_vector_files(self, remote_files_list, download_folder=None, group # Retornem la última capa return layers_list - def add_wms_t_layer(self, layer_name, url, layer_id=None, default_time=None, style="default", image_format="image/png", time_series_list=None, time_series_regex=None, epsg=None, extra_tags="", group_name="", group_pos=None, only_one_map_on_group=False, collapsed=True, visible=True, transparency=None, saturation=None, resampling_bilinear=False, resampling_cubic=False, set_current=False): + def add_wms_t_layer(self, layer_name, url, layer_id=None, default_time=None, style="default", image_format="image/png", time_series_list=None, time_series_regex=None, epsg=None, extra_tags="", group_name="", group_pos=None, only_one_map_on_group=False, only_one_visible_map_on_group=True, collapsed=True, visible=True, transparency=None, saturation=None, resampling_bilinear=False, resampling_cubic=False, set_current=False): """ Afegeix una capa WMS-T a partir de la URL base i una capa amb informació temporal. Veure add_wms_layer per la resta de paràmetres --- @@ -3814,13 +3861,13 @@ def add_wms_t_layer(self, layer_name, url, layer_id=None, default_time=None, sty time_layer_name = "%s [%s]" % (layer_name, default_time) if url: layer = self.add_wms_layer(time_layer_name, url_time, [default_layer], [style], - image_format, epsg, extra_tags, group_name, group_pos, only_one_map_on_group, + image_format, epsg, extra_tags, group_name, group_pos, only_one_map_on_group, only_one_visible_map_on_group, collapsed, visible, transparency, saturation, set_current) else: layer = self.add_raster_layer(time_layer_name, url_time, group_name, group_pos, epsg, color_default_expansion=True, visible=visible, expanded=not collapsed, transparency=transparency, saturation=saturation, resampling_bilinear=resampling_bilinear, resampling_cubic=resampling_cubic, - set_current=set_current, only_one_map_on_group=only_one_map_on_group) + set_current=set_current, only_one_map_on_group=only_one_map_on_group, only_one_visible_map_on_group=only_one_visible_map_on_group) # Registrem les capes temporals de la url i refresquem la capa activa self.time_series_dict[layer] = time_series_list @@ -4282,6 +4329,42 @@ def get_attributes_table(self, layer): dialogs_list = [d for d in QApplication.instance().allWidgets() if d.objectName() == 'QgsAttributeTableDialog' and d.windowTitle().split(' - ')[1].split(' :: ')[0] == layer.name()] return dialogs_list[0] if len(dialogs_list) > 0 else None + def show_attributes_dialog_by_id(self, layer_idprefix, feature, edit_mode=False, modal_mode=False, width=100, height=100, pos=0): + """ Mostra el formulari d'edició de camps de l'element amb id especificat o seleccionat + --- + Shows edition feature form of specificated feature id or current selected id + """ + layer = self.get_by_id(layer_idprefix, pos) + if not layer: + return None + return self.show_attributes_dialog(layer, feature, edit_mode, modal_mode, width, height) + + def show_attributes_dialog(self, layer, feature, edit_mode=False, modal_mode=False, width=400, height=200): + """ Mostra el formulari d'edició de camps de l'element amb el valor especificat o seleccionat + --- + Shows edition feature form of specificated feature value or current selected id + """ + # Activem el mode edició si cal + if edit_mode: + layer.startEditing() + # Mostrem el diàleg + dlg = self.iface.getFeatureForm(layer, feature) + dlg.resize(width, height) + if modal_mode or edit_mode: + status_ok = (dlg.exec() == QDialog.Accepted) + else: + dlg.show() + status_ok = dlg.isVisible() + # Guardem o cancel·lem canvis si cal + if edit_mode: + if status_ok: + feature.setAttributes(dlg.feature().attributes()) + layer.commitChanges() + else: + layer.rollBack() + layer.endEditCommand() + return status_ok + def refresh_legend_by_id(self, idprefix, visible=None, expanded=None, pos=0): """ Refresca la llegenda d'una capa a partir del seu id. Pot actualitzar la visibilitat i expansió de la llegenda de la capa @@ -4570,7 +4653,7 @@ def remove_group(self, group): # Esborrem el contingut del grup self.empty_group(group) # Esborrem el grup - self.root.removeChildNode(group) + group.parent().removeChildNode(group) return True def empty_group_by_name(self, group_name, exclude_list=[]): @@ -5953,7 +6036,12 @@ def get_icon(self, icon_name="icon.png"): """ #icon_ref = ":/plugins/%s/%s" % (self.parent.plugin_id.lower(), icon_name) #icon = QIcon(icon_ref) - icon_pathname = os.path.join(self.parent.plugin_path, icon_name) + if not os.path.splitext(icon_name)[1]: + icon_name += ".png" + if not os.path.isabs(icon_name): + icon_pathname = os.path.join(self.parent.plugin_path, icon_name) + else: + icon_pathname = icon_name icon = QIcon(icon_pathname) return icon @@ -6283,7 +6371,7 @@ def show_about(self, checked=None, title=None, pixmap=None): # I add checked par # Show about self.about_dlg.do_modal() - def show_changelog(self, checked=None, title=None): # I add checked param, because the mapping of the signal triggered passes a parameter + def show_changelog(self, checked=None, title=None, width=800): # I add checked param, because the mapping of the signal triggered passes a parameter """ Mostra el diàleg de canvis del plugin --- Show plugin changelog dialog @@ -6296,7 +6384,7 @@ def show_changelog(self, checked=None, title=None): # I add checked param, becau title = "Novedades" else: title = "What's new" - LogInfoDialog(self.metadata.get_changelog(), title, LogInfoDialog.mode_info) + LogInfoDialog(self.metadata.get_changelog(), title, LogInfoDialog.mode_info, width=width) def show_help(self, checked=None, path="help", basename="index"): # I add checked param, because the mapping of the signal triggered passes a parameter """ Mostra l'ajuda del plugin diff --git a/resources3/fme.py b/resources3/fme.py index 4f25e6e..cfd1733 100644 --- a/resources3/fme.py +++ b/resources3/fme.py @@ -144,7 +144,6 @@ (r"ct1m.+c\d_full", "ct1mv22_c.qml"), # ct1mv22sh0f4483707c1_full (Full 1000) ] - def get_historic_ortho_dict(urlbase="https://datacloud.icgc.cat/datacloud/orto-territorial/gpkg_unzip", \ file_pattern=r"(ortofoto-(\w+)-(\d+)(c*m)-(\w+)-(\d{4})\.gpkg)"): """ Obté les ortofotos històriques disponibles per descàrrega """ @@ -227,21 +226,28 @@ def get_historic_ortho_ref(rgb_not_irc, gsd, year): return get_historic_ortho_file(rgb_not_irc, gsd, year) def get_services(): - """ Retorna la llista de productes descarregables """ + """ Retorna una llista de tuples de productes descarregables amb els valors: + (id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, + download_list, default_filename, limits, url_pattern, ref_tuple, enabled) + """ final_services_list = [] - for id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, default_filename, limits, url_pattern, ref_tuple in services_list: + for id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, default_filename, limits, url_pattern, ref_tuple in services_list: # Injectem anys d'ortofoto si cal if time_list == FME_AUTO_SEARCH: color_not_irc = name.lower().find("infraroja") < 0 time_list = get_historic_ortho_years(color_not_irc, gsd) + enabled = len(time_list) > 0 + else: + enabled = True # Injectem el path dels arxiu .qml if ref_tuple and len(ref_tuple) == 2: ref_file, style_file = ref_tuple style_file = os.path.join(os.path.dirname(__file__), "symbols", style_file) ref_tuple = (ref_file, style_file) # Afegim els valors modificats a la llista - final_services_list.append((id, name, min_side, max_query_area, min_px_side, max_px_area, gsd, time_list, download_list, default_filename, limits, url_pattern, ref_tuple)) + final_services_list.append((id, name, min_side, max_query_area, min_px_side, max_px_area, \ + gsd, time_list, download_list, default_filename, limits, url_pattern, ref_tuple, enabled)) log.debug("FME resources URL: %s found: %s", FME_URL.split("/")[0] + "//" + FME_URL.split("@")[1], len(final_services_list)) return final_services_list diff --git a/resources3/http.py b/resources3/http.py index e63a1cc..355c652 100644 --- a/resources3/http.py +++ b/resources3/http.py @@ -56,24 +56,25 @@ def get_http_dir(url, timeout_seconds=0.5, retries=3): context.verify_mode = ssl.CERT_NONE # Llegeixo la pàgina HTTP que informa dels arxius disponibles + response_data = "" remaining_retries = retries while remaining_retries: try: - response = None response = urllib.request.urlopen(url, timeout=timeout_seconds, context=context) - remaining_retries = 0 + if response: + response_data = response.read() + if response_data: + remaining_retries = 0 except socket.timeout: remaining_retries -= 1 log.warning("HTTP resources timeout, retries: %s, URL: %s", retries, url) except Exception as e: remaining_retries -= 1 log.exception("HTTP resources error (%s), retries: %s, URL: %s", e, retries, url) - if not response: - response_data = "" - log.error("HTTP resources error, exhausted retries") - else: - response_data = response.read() + if response_data: response_data = response_data.decode('utf-8') + else: + log.error("HTTP resources error, exhausted retries") return response_data def get_http_files(url, file_regex_pattern, replace_list=[]):