diff --git a/metadata.txt b/metadata.txt index eb3a0f9..4669243 100644 --- a/metadata.txt +++ b/metadata.txt @@ -28,8 +28,13 @@ email=qgis.openicgc@icgc.cat qgisMinimumVersion=2.99 qgisMaximumVersion=3.99 -version=1.1.14 -changelog=v1.1.14 (2023-05-23) +version=1.1.15 +changelog=v1.1.15 (2023-05-30) + - Fixed problem unloading / uninstalling plugin due to loading extra fonts + - Fixed problem sorting and accessing search results + - Updated delimitation files http address + + v1.1.14 (2023-05-23) - Fixed problems with download types "all available" and "catalonia" v1.1.13 (2023-04-18) diff --git a/openicgc.py b/openicgc.py index 6618208..162510b 100644 --- a/openicgc.py +++ b/openicgc.py @@ -61,9 +61,10 @@ from .qlib3.downloaddialog.downloaddialog import DownloadDialog # Import wms resources access functions from .resources3.wms import get_historic_ortho, get_lastest_ortoxpres, get_superexpedita_ortho, get_full_ortho + #from .resources3.wfs import get_delimitations as get_wfs_delimitations from .resources3.fme import get_clip_data_url, get_services, get_historic_ortho_code, get_historic_ortho_ref from .resources3.fme import get_regex_styles as get_fme_regex_styles, FME_DOWNLOAD_EPSG, FME_MAX_POLYGON_POINTS - from .resources3.http import get_dtms, get_sheets, get_delimitations, get_ndvis, get_topographic_5k, get_regex_styles as get_http_regex_styles + from .resources3.http import get_dtms, get_sheets, get_delimitations, get_ndvis, get_topographic_5k from .resources3 import http as http_resources, wms as wms_resources, fme as fme_resources else: # Import basic plugin functionalities @@ -92,13 +93,16 @@ import resources3.wms reload(resources3.wms) from resources3.wms import get_historic_ortho, get_lastest_ortoxpres, get_superexpedita_ortho, get_full_ortho + #import resources3.wfs + #reload(resources3.wfs) + #from resources3.wfs import get_delimitations as get_wfs_delimitations import resources3.fme reload(resources3.fme) from resources3.fme import get_clip_data_url, get_services, get_historic_ortho_code, get_historic_ortho_ref from resources3.fme import get_regex_styles as get_fme_regex_styles, FME_DOWNLOAD_EPSG, FME_MAX_POLYGON_POINTS import resources3.http reload(resources3.http) - from resources3.http import get_dtms, get_sheets, get_delimitations, get_ndvis, get_topographic_5k, get_regex_styles as get_http_regex_styles + from resources3.http import get_dtms, get_sheets, get_delimitations, get_ndvis, get_topographic_5k from resources3 import http as http_resources, wms as wms_resources, fme as fme_resources # Global function to set HTML tags to apply fontsize to QInputDialog text @@ -348,7 +352,10 @@ def __init__(self, iface, debug_mode=False): fme_resources.log = self.log # Load extra fonts (Fira Sans) - self.load_fonts() + fonts_status, self.font_id_list = self.load_fonts(copy_to_temporal_folder=True) + self.log.info("Load fonts folder %s: %s" % (self.get_fonts_temporal_folder(), fonts_status)) + if not fonts_status: + self.log.warning("Error loading extra fonts") # Translated long tooltip text self.TOOLTIP_HELP = self.tr("""Find: @@ -391,15 +398,15 @@ def __init__(self, iface, debug_mode=False): # Initialize references names (with translation) self.HTTP_NAMES_DICT = { - "caps-municipi": self.tr("Municipal capitals"), + "caps-municipi": self.tr("Municipal capitals"), # Available HTTP + "capmunicipi": self.tr("Municipal capitals"), # Available WFS + "capcomarca": self.tr("County capitals"), # # Available WFS "municipis": self.tr("Municipalities"), "comarques": self.tr("Counties"), - "vegueries": self.tr("Vegueries"), + "vegueries": self.tr("Vegueries"), # Available HTTP "provincies": self.tr("Provinces"), - "catalunya": self.tr("Catalonia"), + "catalunya": self.tr("Catalonia"), #Available HTTP } - # Get download services regex styles - self.http_regex_styles_list = get_http_regex_styles() # Initialize download names (with translation) self.FME_NAMES_DICT = { @@ -522,6 +529,7 @@ def unload(self): photo_search_layer.selectionChanged.disconnect(self.on_change_photo_selection) if self.photo_search_dialog: photo_search_layer.willBeDeleted.disconnect(self.photo_search_dialog.reset) + self.log.debug("Disconnected signals") # Remove photo dialog if self.photo_search_dialog: self.photo_search_dialog.visibilityChanged.disconnect() @@ -531,10 +539,14 @@ def unload(self): # Remove photo search groups self.legend.remove_group_by_name(self.photos_group_name) self.legend.remove_group_by_name(self.download_group_name) + self.log.debug("Removed groups") # Remove GeoFinder dialog self.geofinder_dialog = None # Remove Download dialog self.download_dialog = None + self.log.debug("Removed dialogs") + # Unload fonts + self.log.debug("Removed fonts: %s" % self.unload_fonts(self.font_id_list)) # Log plugin unloaded self.log.info("Unload %s%s", self.metadata.get_name(), " Lite" if self.lite else "") # Parent PluginBase class release all GUI resources created with their functions @@ -591,6 +603,7 @@ def initGui(self, check_qgis_updates=True, check_icgc_updates=False): # Get Available delimitations delimitations_list = get_delimitations() + #wfs_delimitations_url, wfs_delimitations_list = get_wfs_delimitations() # Gets available Sheets sheets_list = get_sheets() @@ -698,17 +711,28 @@ def initGui(self, check_qgis_updates=True, check_icgc_updates=False): self.manage_metadata_button("Administrative divisions"), True), "---", ] + [ - (self.HTTP_NAMES_DICT.get(name, name), - (lambda _checked, name=name, scale_list=scale_list:self.layers.add_vector_layer(self.HTTP_NAMES_DICT.get(name, name), scale_list[0][1], group_name=self.backgroup_map_group_name, only_one_map_on_group=False, set_current=True, regex_styles_list=self.http_regex_styles_list) if len(scale_list) == 1 else None), + (self.HTTP_NAMES_DICT.get(name, name), + (lambda _checked, name=name, scale_list=scale_list, style_file=style_file:self.layers.add_vector_layer(self.HTTP_NAMES_DICT.get(name, name), scale_list[0][1], group_name=self.backgroup_map_group_name, only_one_map_on_group=False, set_current=True, style_file=style_file) if len(scale_list) == 1 else None), QIcon(":/lib/qlib3/base/images/cat_vector.png"), ([ ("%s 1:%s" % (self.HTTP_NAMES_DICT.get(name, name), self.format_scale(scale)), - lambda _checked, name=name, scale=scale, url=url:self.layers.add_vector_layer("%s 1:%s" % (self.HTTP_NAMES_DICT.get(name, name), self.format_scale(scale)), url, group_name=self.backgroup_map_group_name, only_one_map_on_group=False, set_current=True, regex_styles_list=self.http_regex_styles_list), + lambda _checked, name=name, scale=scale, url=url, style_file=style_file:self.layers.add_vector_layer("%s 1:%s" % (self.HTTP_NAMES_DICT.get(name, name), self.format_scale(scale)), url, group_name=self.backgroup_map_group_name, only_one_map_on_group=False, set_current=True, style_file=style_file), QIcon(":/lib/qlib3/base/images/cat_vector.png"), self.manage_metadata_button("Administrative divisions"), True) for scale, url in scale_list] if len(scale_list) > 1 \ else self.manage_metadata_button("Administrative divisions")), len(scale_list) == 1) - for name, scale_list in delimitations_list + for name, scale_list, style_file in delimitations_list + #(self.HTTP_NAMES_DICT.get(name, name), + #(lambda _checked, name=name, scale_list=scale_list, style_file=style_file:self.layers.add_wfs_layer(self.HTTP_NAMES_DICT.get(name, name), wfs_delimitations_url, [scale_list[0][1]], epsg=25831, group_name=self.backgroup_map_group_name, only_one_map_on_group=False, set_current=True, style_file=style_file) if len(scale_list) == 1 else None), + #QIcon(":/lib/qlib3/base/images/cat_vector.png"), ([ + # ("%s 1:%s" % (self.HTTP_NAMES_DICT.get(name, name), self.format_scale(scale)), + # lambda _checked, name=name, scale=scale, style_file=style_file, layer_id=layer_id:self.layers.add_wfs_layer("%s 1:%s" % (self.HTTP_NAMES_DICT.get(name, name), self.format_scale(scale)), wfs_delimitations_url, [layer_id], epsg=25831, group_name=self.backgroup_map_group_name, only_one_map_on_group=False, set_current=True, style_file=style_file), + # QIcon(":/lib/qlib3/base/images/cat_vector.png"), + # self.manage_metadata_button("Administrative divisions"), True) + # for scale, layer_id in scale_list] if len(scale_list) > 1 \ + # else self.manage_metadata_button("Administrative divisions")), + # len(scale_list) == 1) + #for name, scale_list, style_file in wfs_delimitations_list ]), (self.tr("Cartographic series"), None, QIcon(":/lib/qlib3/base/images/sheets.png"), enable_http_files, [ (self.tr("%s serie") % sheet_name, diff --git a/qlib3/base/pluginbase.py b/qlib3/base/pluginbase.py index 3ef42ff..15f2ad4 100644 --- a/qlib3/base/pluginbase.py +++ b/qlib3/base/pluginbase.py @@ -33,6 +33,8 @@ import base64 import zipfile import io +import tempfile +import shutil from xml.etree import ElementTree from importlib import reload try: @@ -6436,25 +6438,56 @@ def set_setting_value(self, key, value, group_name=None): # self.settings.endArray() # self.settings.endGroup(); - def load_fonts(self, fonts_path=None, file_filter_list=["*.ttf", "*.otf"]): + def get_fonts_temporal_folder(self): + """ Retorna la carpeta temporal on copiar les fonts + --- + Returns temporal fonts folder where we copy fonts + """ + tmp_path = os.path.join(tempfile.gettempdir(), "%sFonts" % self.plugin_id) + return tmp_path + + def load_fonts(self, fonts_path=None, file_filter_list=["*.ttf", "*.otf"], copy_to_temporal_folder=True): """ Carrega totes les fonts trobades en una carpeta. Per defecte en \fonts + Retorna: (bool, [,...]) True si ha anat tot bé i la llista d'ids de font --- Load all found fonts in a folder. Default \fonts + Returns: (bool, [,...]) True if ok and a font id's list """ - if not fonts_path: + # Si no ens passen un path, utilitzem el path per defecte: \fonts + if not fonts_path: fonts_path = os.path.join(self.plugin_path, "fonts") if not os.path.exists(fonts_path): - return False + return False, [] + + # Creem una carpeta carpeta temporal si cal: + if copy_to_temporal_folder: + tmp_path = self.get_fonts_temporal_folder() + if not os.path.exists(tmp_path): + os.mkdir(tmp_path) + + # Cerquem fonts dins la carpeta indicada status = True + font_id_list = [] for file_filter in file_filter_list: for font_pathname in glob.glob(os.path.join(fonts_path, file_filter)): - status &= self.load_font(font_pathname) - return status + # Copiem la font a una carpeta temporal si cal + if copy_to_temporal_folder: + temporal_font_pathname = os.path.join(tmp_path, os.path.basename(font_pathname)) + if not os.path.exists(temporal_font_pathname): + shutil.copyfile(font_pathname, temporal_font_pathname) + font_pathname = temporal_font_pathname + # Carreguem la font + font_id = self.load_font(font_pathname) + font_id_list.append(font_id) + status &= (font_id >=0) + return status, font_id_list def load_font(self, font_pathname_or_filename, file_types_list=["ttf", "otf"]): """ Carrega un fitxer de font (si no és un path absolut utlitza /fonts com a path base) + Retorna l'identificador de font o -1 si error --- Loads a font file (if not it is a absolute path use /fonts as source path) + Returns font id or -1 if error """ # Afegim el path si cal font_pathname = font_pathname_or_filename if os.path.isabs(font_pathname_or_filename) \ @@ -6464,6 +6497,38 @@ def load_font(self, font_pathname_or_filename, file_types_list=["ttf", "otf"]): for file_type in file_types_list: if os.path.exists(font_pathname + "." + file_type): font_pathname += ("." + file_type) - break + break # Carreguem la font - return QFontDatabase.addApplicationFont(font_pathname) == 0 + return QFontDatabase.addApplicationFont(font_pathname) + + def unload_fonts(self, font_id_list, remove_temporal_fonts_folder=False): + """ Descarrega una llista de fonts d'aplicació + Retona: booleà, Cert si ok si no fals + --- + Unload a list of application fonts + Returns: bool, True if ok else False + """ + # Descarrega totes les fonts + global_status = True + for font_id in font_id_list: + if font_id >= 0: + status = self.unload_font(font_id) + global_status &= status + + # ATENCIÓ!!! ELS ARXIUS DE FONTS ES QUEDEN BLOQUEJATS I DÓNA ERROR A L'ESBORRAR-LOS + # DE MOMENT DEIXO LA OPCIÓ DESACTIVADA PER DEFECTE + # Esborrar la carpeta de fonts temporal si cal + if remove_temporal_fonts_folder: + tmp_path = self.get_fonts_temporal_folder() + if os.path.exists(tmp_path): + shutil.rmtree(tmp_path) + return global_status + + def unload_font(self, font_id): + """ Descarrega una font d'aplicació + Retona: booleà, Cert si ok si no fals + --- + Unload a application font + Returns: bool, True if ok else False + """ + return QFontDatabase.removeApplicationFont(font_id) diff --git a/qlib3/geofinderdialog/geofinderdialog.py b/qlib3/geofinderdialog/geofinderdialog.py index 401d059..a3e792d 100644 --- a/qlib3/geofinderdialog/geofinderdialog.py +++ b/qlib3/geofinderdialog/geofinderdialog.py @@ -80,7 +80,6 @@ def setupUi(self, title, columns_list, keep_scale_text, default_scale): self.tableWidget.setSelectionMode(QAbstractItemView.SingleSelection) self.tableWidget.verticalHeader().setVisible(False) self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) - self.tableWidget.setSortingEnabled(True) # Setup column names if columns_list: @@ -96,6 +95,7 @@ def setupUi(self, title, columns_list, keep_scale_text, default_scale): self.comboBox_scale.setCurrentIndex(pos) def set_data(self, topodata_list): + self.tableWidget.setSortingEnabled(False) self.tableWidget.setRowCount(len(topodata_list)) # topodata_list example: @@ -105,9 +105,12 @@ def set_data(self, topodata_list): self.tableWidget.setItem(i, 1, QTableWidgetItem(topodata['nomTipus'])) ## + " (" + topodata['idTipus'] + ")")) self.tableWidget.setItem(i, 2, QTableWidgetItem(topodata['nomMunicipi'])) self.tableWidget.setItem(i, 3, QTableWidgetItem(topodata['nomComarca'])) + # Ens guardem la posició original de l'item per si després s'ordena la llista i canvia l'index + self.tableWidget.item(i, 0).setData(Qt.UserRole, i); self.tableWidget.resizeColumnsToContents() self.tableWidget.resizeRowsToContents() + self.tableWidget.setSortingEnabled(True) if len(topodata_list) > 0: self.tableWidget.selectRow(0) @@ -120,7 +123,9 @@ def do_modal(self): def get_selection_index(self): """ Return number of selected dialog row """ - return self.tableWidget.currentRow() if self.status else -1 + if not self.status or self.tableWidget.currentRow() < 0: + return -1 + return self.tableWidget.item(self.tableWidget.currentRow(), 0).data(Qt.UserRole) def find(self, text, default_epsg): # Find text diff --git a/resources3/http.py b/resources3/http.py index 667ace1..10c5c13 100644 --- a/resources3/http.py +++ b/resources3/http.py @@ -24,14 +24,24 @@ log.addHandler(logging.NullHandler()) -styles_list = [ # (regex_file_pattern, qml_style) - (r".+-caps-municipi-.+", "divisions-administratives-caps-municipi-ref.qml"), # ct1mv22sh0f4483707eaxt1 (Textos) - (r".+-municipis-.+", "divisions-administratives-municipis-ref.qml"), # ct1mv22sh0f4483707eaxt1 (Textos) - (r".+-comarques-.+", "divisions-administratives-comarques-ref.qml"), # ct1mv22sh0f4483707eaxt1 (Textos) - (r".+-vegueries-.+", "divisions-administratives-vegueries-ref.qml"), # ct1mv22sh0f4483707eaxt1 (Textos) - (r".+-provincies-.+", "divisions-administratives-provincies-ref.qml"), # ct1mv22sh0f4483707eaxt1 (Textos) - (r".+-catalunya-.+", "divisions-administratives-catalunya-ref.qml"), # ct1mv22sh0f4483707eaxt1 (Textos) - ] +styles_path = os.path.join(os.path.dirname(__file__), "symbols") +style_dict = { + "caps-municipi": os.path.join(styles_path, "divisions-administratives-caps-municipi-ref.qml"), + "municipis": os.path.join(styles_path, "divisions-administratives-municipis-ref.qml"), + "comarques": os.path.join(styles_path, "divisions-administratives-comarques-ref.qml"), + "vegueries": os.path.join(styles_path, "divisions-administratives-vegueries-ref.qml"), + "provincies": os.path.join(styles_path, "divisions-administratives-provincies-ref.qml"), + "catalunya": os.path.join(styles_path, "divisions-administratives-catalunya-ref.qml"), + } +styles_order_dict = { + "caps-municipi": 1, + "municipis": 2, + "comarques": 3, + "vegueries": 4, + "provincies": 5, + "catalunya": 6 + } + def get_http_dir(url, timeout_seconds=0.5, retries=3): """ Obté el codi HTML d'una pàgina web amb fitxers @@ -181,20 +191,12 @@ def get_delimitations(delimitations_urlbase="https://datacloud.icgc.cat/dataclou if last_name != name or last_scale != scale: delimitations_dict[name] = delimitations_dict.get(name, []) + [(int(scale) if scale else None, "%s/%s" % (delimitations_urlbase, filename))] last_name = name - last_scale = scale - - # Ordenem els arxius... - order_dict = { - "caps-municipi": 1, - "municipis": 2, - "comarques": 3, - "vegueries": 4, - "provincies": 5, - "catalunya": 6 - } - #delimitations_list = sorted(list(delimitations_dict.items()), key=lambda d: order_dict.get(d[0], 0) * 1000000 + d[1][0]) # index(name) * 1000000 + scale - delimitations_list = sorted(list(delimitations_dict.items()), key=lambda d: order_dict[d[0]]) - delimitations_list = [(name, sorted(scale_list, key=lambda s: s[0])) for name, scale_list in delimitations_list] + last_scale = scale + # Ordenem les delimitacions + delimitations_list = sorted(list(delimitations_dict.items()), key=lambda d: styles_order_dict[d[0]]) + # Ordenem les escales i afegim arxiu d'estil + delimitations_list = [(name, sorted(scale_list, key=lambda s: s[0]), style_dict.get(name, None)) \ + for name, scale_list in delimitations_list] return delimitations_list def get_ndvis(urlbase="https://datacloud.icgc.cat/datacloud/ndvi/tif", @@ -224,12 +226,3 @@ def get_topographic_5k(urlbase="https://datacloud.icgc.cat/datacloud/topografia- file_tuple_list = [(year, "%s/%s" % (urlbase, filename)) for filename, year in info_list] file_tuple_list.sort(key=lambda f : f[0], reverse=True) # Ordenem per any return file_tuple_list - -def get_regex_styles(): - """ Retorna la llista d'estils disponibles amb el path al seu QML """ - final_styles_list = [ - (style_regex, - # Injectem el path dels arxiu .qml - os.path.join(os.path.dirname(__file__), "symbols", style_qml) if style_qml else None - ) for style_regex, style_qml in styles_list] - return final_styles_list diff --git a/resources3/wfs.py b/resources3/wfs.py new file mode 100644 index 0000000..28eb038 --- /dev/null +++ b/resources3/wfs.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +""" +******************************************************************************* +Module with functions to recover data to make WMS connections to ICGC resources + + ------------------- + begin : 2023-05-24 + author : Albert Adell + email : albert.adell@icgc.cat +******************************************************************************* +""" + +import urllib +import urllib.request +import html +import socket +import re +import os + +# Configure internal library logger (Default is dummy logger) +import logging +log = logging.getLogger('dummy') +log.addHandler(logging.NullHandler()) + + +styles_path = os.path.join(os.path.dirname(__file__), "symbols") +style_dict = { + "capmunicipi": os.path.join(styles_path, "divisions-administratives-caps-municipi-ref.qml"), + "capcomarca": os.path.join(styles_path, "divisions-administratives-caps-municipi-ref.qml"), + "municipis": os.path.join(styles_path, "divisions-administratives-municipis-ref.qml"), + "comarques": os.path.join(styles_path, "divisions-administratives-comarques-ref.qml"), + "vegueries": os.path.join(styles_path, "divisions-administratives-vegueries-ref.qml"), + "provincies": os.path.join(styles_path, "divisions-administratives-provincies-ref.qml"), + "catalunya": os.path.join(styles_path, "divisions-administratives-catalunya-ref.qml"), + } +order_dict = { + "capmunicipi": 1, + "capcomarca": 2, + "municipis": 3, + "comarques": 4, + "vegueries": 5, + "provincies": 6, + "catalunya": 7 + } + +def get_wfs_capabilities(url, version="2.0.0", timeout_seconds=10, retries=3): + """ Obté el text del capabilities d'un servei WFS + --- + Gets capabilities text from WFS service + """ + capabilities_url = "%s?REQUEST=GetCapabilities&SERVICE=WFS&VERSION=%s" % (url, version) + while retries: + try: + response = None + response = urllib.request.urlopen(capabilities_url, timeout=timeout_seconds) + retries = 0 + except socket.timeout: + retries -= 1 + log.warning("WFS resources timeout, retries: %s, URL: %s", retries, capabilities_url) + except Exception as e: + retries -= 1 + log.exception("WFS resources error (%s), retries: %s, URL: %s", retries, e, capabilities_url) + if not response: + response_data = "" + log.error("WFS resources error, exhausted retries") + else: + response_data = response.read() + response_data = response_data.decode('utf-8') + return response_data + +def get_wfs_capabilities_info(url, reg_ex_filter): + """ Extreu informació del capabilies d'un WFS via expresions regulars + --- + Extract info from WFS capabilities using regular expressions + """ + response_data = get_wfs_capabilities(url) + data_list = re.findall(reg_ex_filter, response_data) + log.debug("WFS resources info URL: %s pattern: %s found: %s", url, reg_ex_filter, len(data_list)) + return data_list + +def get_delimitations(url="https://geoserveis.icgc.cat/servei/catalunya/divisions-administratives/wfs", + reg_ex_filter=r"(.+)"): + """ Obté la URL del servidor de delimitacions de l'ICGC i la llista de capes disponibles + Retorna: URL, [(product_name, [(scale, layer_id), style_file])] + --- + Gets the URL of the ICGC delimitations server and the list of available layers +        Returns: URL, [(product_name, [(scale, layer_id)], style_file)] + """ + # Llegeixo la pàgina HTTP que informa dels arxius disponibles (canvio "caps-municipi" per parsejar-lo més fàcil...) + delimitations_id_list = get_wfs_capabilities_info(url, reg_ex_filter) + # Obtenim un nom de producte simplificat i separem la informació de la escala + delimitations_info_list = [( + layer_id.split("_")[-2 if layer_id.split("_")[-1].isdigit() else -1], \ + int(layer_id.split("_")[-1]) if layer_id.split("_")[-1].isdigit() else None, + layer_id \ + ) \ + for layer_id in delimitations_id_list] + # Agrupem les escales de cada producte + delimitations_dict = {} + for product_name, scale, layer_id in delimitations_info_list: + delimitations_dict[product_name] = delimitations_dict.get(product_name, []) + [(scale, layer_id)] + # Ordenem els productes + delimitations_list = sorted(list(delimitations_dict.items()), key=lambda d:order_dict.get(d[0], 10)) + # Ordenem les escales dins de cada producte i afegim arxiu d'estil + delimitations_list = [(product_name, sorted(scale_list, key=lambda s:s[0]), style_dict.get(product_name, None)) \ + for product_name, scale_list in delimitations_list] + # retornem les dades + return url, delimitations_list