From 18b1fce94af208c66357437c99f0bda86b494a30 Mon Sep 17 00:00:00 2001 From: Etienne Trimaille Date: Tue, 8 Aug 2023 16:38:05 +0200 Subject: [PATCH] UX - Start resurecting the log panel : display estimated metadata and simplify geometries --- lizmap/definitions/definitions.py | 1 + lizmap/dialogs/main.py | 24 +++------- lizmap/log_panel.py | 77 +++++++++++++++++++++++++++++++ lizmap/plugin.py | 73 ++++++++++++++++++++++------- lizmap/project_checker_tools.py | 60 +++++++++++++++++++++++- lizmap/saas.py | 41 ++++++++-------- 6 files changed, 220 insertions(+), 56 deletions(-) create mode 100644 lizmap/log_panel.py diff --git a/lizmap/definitions/definitions.py b/lizmap/definitions/definitions.py index a57a4a18..ead04ed8 100755 --- a/lizmap/definitions/definitions.py +++ b/lizmap/definitions/definitions.py @@ -85,6 +85,7 @@ class Html(Enum): H3 = 'h3' H4 = 'h4' Strong = 'strong' + Li = 'li' @unique diff --git a/lizmap/dialogs/main.py b/lizmap/dialogs/main.py index df49d437..64ee3dc5 100755 --- a/lizmap/dialogs/main.py +++ b/lizmap/dialogs/main.py @@ -3,7 +3,6 @@ __email__ = 'info@3liz.org' import logging -import sys from pathlib import Path from typing import Optional @@ -21,6 +20,8 @@ ) from qgis.utils import iface +from lizmap.log_panel import LogPanel + try: from qgis.PyQt.QtWebKitWidgets import QWebView WEBKIT_AVAILABLE = True @@ -28,7 +29,6 @@ WEBKIT_AVAILABLE = False from lizmap.definitions.definitions import ( - Html, LwcVersions, RepositoryComboData, ServerComboData, @@ -70,9 +70,6 @@ def __init__(self, parent=None): self.feature_picker_layout.addWidget(self.dataviz_feature_picker) self.feature_picker_layout.addItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)) - # clear log button clicked - self.button_clear_log.clicked.connect(self.clear_log) - # IGN and google self.inIgnKey.textChanged.connect(self.check_ign_french_free_key) self.inIgnKey.textChanged.connect(self.check_api_key_address) @@ -88,6 +85,9 @@ def __init__(self, parent=None): self.label_link.setToolTip(tooltip) self.inLayerLink.setToolTip(tooltip) + self.log_panel = LogPanel(self.out_log) + self.button_clear_log.clicked.connect(self.log_panel.clear) + self.check_project_thumbnail() self.setup_icons() @@ -400,6 +400,7 @@ def setup_icons(self): i = 0 # Information + # It must be the first tab, wiht index 0. icon = QIcon() icon.addFile(resources_path('icons', '03-metadata-white'), mode=QIcon.Normal) icon.addFile(resources_path('icons', '03-metadata-dark'), mode=QIcon.Selected) @@ -498,6 +499,7 @@ def setup_icons(self): i += 1 # Log + # It must be the last tab, with the higher index # noinspection PyCallByClass,PyArgumentList icon = QIcon(QgsApplication.iconPath('mMessageLog.svg')) self.mOptionsListWidget.item(i).setIcon(icon) @@ -583,15 +585,3 @@ def activateWindow(self): self.check_project_thumbnail() LOGGER.info("Opening the Lizmap dialog.") super().activateWindow() - - def append_log(self, msg, style: Html = None, abort=None): - """ Append text to the log. """ - if abort: - sys.stdout = sys.stderr - if style: - msg = '<{0}>{1}'.format(style.value, msg) - self.out_log.append(msg) - - def clear_log(self): - """ Clear the content of the text area log. """ - self.out_log.clear() diff --git a/lizmap/log_panel.py b/lizmap/log_panel.py new file mode 100644 index 00000000..1a08bafc --- /dev/null +++ b/lizmap/log_panel.py @@ -0,0 +1,77 @@ +__copyright__ = 'Copyright 2023, 3Liz' +__license__ = 'GPL version 3' +__email__ = 'info@3liz.org' + +import sys + +from qgis.core import Qgis +from qgis.PyQt.QtCore import QDateTime, QLocale +from qgis.PyQt.QtWidgets import QTextEdit + +from lizmap.definitions.definitions import Html + + +class LogPanel: + + def __init__(self, widget: QTextEdit): + self.widget = widget + + def separator(self): + """ Add a horizontal separator. """ + # TODO, check for a proper HTML + self.widget.append('=' * 20) + + def append( + self, + msg: str, + style: Html = None, + abort=None, + time: bool = False, + level: Qgis.MessageLevel = Qgis.Info, + ): + """ Append text to the log. """ + if abort: + sys.stdout = sys.stderr + + if time: + now = QDateTime.currentDateTime() + now_str = now.toString(QLocale().timeFormat(QLocale.ShortFormat)) + self.widget.append(now_str) + + if level == Qgis.Warning: + # byte_array = QByteArray() + # QBuffer + # buffer( & byteArray); + # pixmap.save( & buffer, "PNG"); + # QString + # msg += ""; + # msg = ''.format(":images/themes/default/mIconWarning.svg") + pass + + if style: + output = '' + if style in (Html.H1, Html.H2, Html.H3): + output += '
' + output += '<{0}>{1}'.format(style.value, msg) + msg = output + + self.widget.append(msg) + + def clear(self): + """ Clear the content of the text area log. """ + self.widget.clear() + + +if __name__ == '__main__': + """ For manual tests. """ + from qgis.PyQt.QtWidgets import QApplication, QDialog, QHBoxLayout + app = QApplication(sys.argv) + dialog = QDialog() + layout = QHBoxLayout() + dialog.setLayout(layout) + edit = QTextEdit() + layout.addWidget(edit) + logger = LogPanel(edit) + logger.append("Title", Html.H2, time=True) + dialog.exec_() + sys.exit(app.exec_()) diff --git a/lizmap/plugin.py b/lizmap/plugin.py index 4510a866..15acd3f1 100755 --- a/lizmap/plugin.py +++ b/lizmap/plugin.py @@ -48,6 +48,7 @@ QIcon, QPixmap, QStandardItem, + QTextCursor, ) from qgis.PyQt.QtWidgets import ( QAction, @@ -110,6 +111,8 @@ duplicated_layer_with_filter, invalid_int8_primary_key, invalid_tid_field, + simplify_provider_side, + use_estimated_metadata, ) from lizmap.saas import is_lizmap_dot_com_hosting, valid_saas_lizmap_dot_com from lizmap.table_manager.base import TableManager @@ -328,6 +331,7 @@ def write_log_message(message, tag, level): self.dlg.button_edit_dd_dataviz, self.dlg.button_add_plot, self.dlg.combo_plots, + # Baselayers self.dlg.add_group_empty, self.dlg.add_group_baselayers, ] @@ -1430,7 +1434,7 @@ def read_cfg_file(self, skip_tables=False) -> dict: 'The previous .cfg has been saved as .cfg.back') QMessageBox.critical( self.dlg, tr('Lizmap Error'), message, QMessageBox.Ok) - self.dlg.append_log(message, abort=True) + self.dlg.log_panel.append(message, abort=True) LOGGER.critical('Error while reading the CFG file') else: @@ -2051,7 +2055,7 @@ def read_lizmap_config_file(self) -> dict: 'Please re-configure the options in the Layers tab completely' ) QMessageBox.critical(self.dlg, tr('Lizmap Error'), '', QMessageBox.Ok) - self.dlg.append_log(message, abort=True) + self.dlg.log_panel.append(message, abort=True) return {} def populate_layer_tree(self) -> dict: @@ -2732,6 +2736,35 @@ def project_config_file(self, lwc_version: LwcVersions, with_gui: bool = True, c warnings.append(Warnings.DuplicatedLayersWithFilters.value) ScrollMessageBox(self.dlg, QMessageBox.Warning, tr('Optimisation'), text) + show_log = False + results = simplify_provider_side(self.project) + if len(results): + self.dlg.log_panel.append(tr('Simplify on the provider side'), Html.H2) + self.dlg.log_panel.append(tr( + 'These PostgreSQL vector layers can have the simplification on the provider side') + ':') + for layer in results: + self.dlg.log_panel.append('⚫ ' + layer) + self.dlg.log_panel.append(tr('Visit the layer properties, "Rendering" tab to enable it.')) + show_log = True + + results = use_estimated_metadata(self.project) + if len(results): + self.dlg.log_panel.append(tr('Estimated metadata'), Html.H2) + self.dlg.log_panel.append(tr( + 'These PostgreSQL layers can have the use estimated metadata option enabled') + ':') + for layer in results: + self.dlg.log_panel.append('⚫ ' + layer) + self.dlg.log_panel.append(tr( + 'Edit your PostgreSQL connection to enable this option, then change the datasource by right clicking ' + 'on each layer above, then click "Change datasource" in the menu. Finally reselect your layer in the ' + 'new dialog.')) + show_log = True + + if with_gui and show_log: + self.dlg.mOptionsListWidget.setCurrentRow(self.dlg.mOptionsListWidget.count() - 1) + self.dlg.out_log.moveCursor(QTextCursor.Start) + self.dlg.out_log.ensureCursorVisible() + metadata = { 'qgis_desktop_version': qgis_version(), 'lizmap_plugin_version_str': current_version, @@ -3092,6 +3125,11 @@ def check_project_validity(self): validator = QgsProjectServerValidator() valid, results = validator.validate(self.project) + self.dlg.log_panel.append(tr("OGC validation"), style=Html.H2) + self.dlg.log_panel.append(tr("According to OGC standard : {}").format('VALID' if valid else 'NOT valid')) + if not valid: + self.dlg.log_panel.append(tr("According to OGC standard : {}").format('VALID' if valid else 'NOT valid')) + LOGGER.info(f"Project has been detected : {'VALID' if valid else 'NOT valid'} according to OGC validation.") if not valid: @@ -3211,6 +3249,8 @@ def save_cfg_file( Check the user defined data from GUI and save them to both global and project config files. """ + self.dlg.log_panel.clear() + self.dlg.log_panel.append(tr('Start saving the Lizmap configuration'), time=True) variables = self.project.customVariables() variables['lizmap_repository'] = self.dlg.current_repository() self.project.setCustomVariables(variables) @@ -3221,7 +3261,9 @@ def save_cfg_file( defined_env_target = os.getenv('LIZMAP_TARGET_VERSION') if defined_env_target: - LOGGER.warning("Version defined by environment variable : {}".format(defined_env_target)) + msg = "Version defined by environment variable : {}".format(defined_env_target) + LOGGER.warning(msg) + self.dlg.log_panel.append(msg) lwc_version = LwcVersions.find(defined_env_target) lwc_version: LwcVersions @@ -3232,28 +3274,25 @@ def save_cfg_file( if not self.check_dialog_validity(): LOGGER.debug("Leaving the dialog without valid project and/or server.") - # noinspection PyUnresolvedReferences - self.dlg.display_message_bar( - tr("No project or server"), + self.dlg.log_panel.append(tr("No project or server"), Html.H2) + self.dlg.log_panel.append( tr('Either you do not have a server reachable for a long time or you do not have a project opened.'), - Qgis.Warning, + level=Qgis.Warning, ) return False stop_process = tr("The process is stopping.") if not self.server_manager.check_admin_login_provided() and not self.is_dev_version: - QMessageBox.critical( - self.dlg, - tr('Missing login on a server'), - '{}\n\n{}\n\n{}'.format( + self.dlg.log_panel.append(tr('Missing login on a server'), style=Html.H2) + self.dlg.log_panel.append('{}

{}


{}'.format( tr( "You have set up a server in the first panel of the plugin, but you have not provided a " "login/password." ), tr("Please go back to the server panel and edit the server to add a login."), stop_process - ), QMessageBox.Ok) + )) return False duplicated_in_cfg = duplicated_layer_name_or_group(self.project) @@ -3337,9 +3376,9 @@ def save_cfg_file( self.dlg.cbIgnCadastral.isChecked(), ] - self.dlg.out_log.append('=' * 20) - self.dlg.out_log.append('' + tr('Map - options') + '') - self.dlg.out_log.append('=' * 20) + self.dlg.log_panel.separator() + self.dlg.log_panel.append(tr('Map - options'), Html.Strong) + self.dlg.log_panel.separator() # Checking configuration data # Get the project data from api to check the "coordinate system restriction" of the WMS Server settings @@ -3359,8 +3398,8 @@ def save_cfg_file( # write data in the lizmap json config file self.write_project_config_file(lwc_version, with_gui) - self.dlg.append_log(tr('All the map parameters are correctly set'), abort=False) - self.dlg.append_log(tr('Lizmap configuration file has been updated'), style=Html.Strong, abort=False) + self.dlg.log_panel.append(tr('All the map parameters are correctly set'), abort=False, time=True) + self.dlg.log_panel.append(tr('Lizmap configuration file has been updated'), style=Html.Strong, abort=False) self.get_min_max_scales() msg = tr('Lizmap configuration file has been updated') diff --git a/lizmap/project_checker_tools.py b/lizmap/project_checker_tools.py index a56242e3..b5170d6c 100644 --- a/lizmap/project_checker_tools.py +++ b/lizmap/project_checker_tools.py @@ -2,7 +2,7 @@ __license__ = 'GPL version 3' __email__ = 'info@3liz.org' -from typing import Optional +from typing import List, Optional from qgis.core import ( QgsDataSourceUri, @@ -10,6 +10,7 @@ QgsMapLayer, QgsProject, QgsVectorLayer, + QgsWkbTypes, ) from lizmap.qgis_plugin_tools.tools.i18n import tr @@ -124,3 +125,60 @@ def duplicated_layer_with_filter(project: QgsProject) -> Optional[str]: text += '
' return text + + +def _is_vector_pg(layer: QgsMapLayer, geometry_check=False) -> bool: + """ Return boolean if the layer is stored in PG and is a vector with a geometry. """ + if layer.type() != QgsMapLayer.VectorLayer: + return False + + if layer.dataProvider().name() != 'postgres': + return False + + if not geometry_check: + return True + + if not layer.isSpatial(): + return False + + return True + + +def simplify_provider_side(project: QgsProject) -> List[str]: + """ Return the list of layer name which can be simplified on the server side. """ + results = [] + for layer in project.mapLayers().values(): + if not _is_vector_pg(layer, geometry_check=True): + continue + + if layer.geometryType() == QgsWkbTypes.PointGeometry: + continue + + if not layer.simplifyMethod().forceLocalOptimization(): + continue + + results.append(layer.name()) + # sm.setForceLocalOptimization(False) + # layer.setSimplifyMethod(sm) + + return results + + +def use_estimated_metadata(project: QgsProject) -> List[str]: + """ Return the list of layer name which can use estimated metadata. """ + results = [] + for layer in project.mapLayers().values(): + if not _is_vector_pg(layer, geometry_check=True): + continue + + uri = layer.dataProvider().uri() + if not uri.useEstimatedMetadata(): + results.append(layer.name()) + # uri.setUseEstimatedMetadata(True) + # new_datasource = uri.uri() + # name = layer.name() + # provider_type = layer.providerType() + # options = dp.ProviderOptions() + # layer.setDataSource(new_datasource, name, provider_type, options) + + return results diff --git a/lizmap/saas.py b/lizmap/saas.py index ab610530..59b7dd29 100644 --- a/lizmap/saas.py +++ b/lizmap/saas.py @@ -6,14 +6,14 @@ from pathlib import Path from typing import Dict, Tuple -from qgis._core import QgsRasterLayer from qgis.core import ( QgsDataSourceUri, QgsProject, QgsProviderRegistry, - QgsVectorLayer, + QgsRasterLayer, ) +from lizmap.project_checker_tools import _is_vector_pg from lizmap.qgis_plugin_tools.tools.i18n import tr @@ -37,28 +37,27 @@ def valid_saas_lizmap_dot_com(project: QgsProject) -> Tuple[bool, Dict[str, str] 'The layer "{}" is an ECW. Because of the ECW\'s licence, this format is not compatible with QGIS ' 'server. You should switch to a COG format.').format(layer.name()) - if isinstance(layer, QgsVectorLayer): - if layer.dataProvider().name() == "postgres": - datasource = QgsDataSourceUri(layer.source()) - if datasource.authConfigId() != '': + if _is_vector_pg(layer): + datasource = QgsDataSourceUri(layer.source()) + if datasource.authConfigId() != '': + layer_error[layer.name()] = tr( + 'The layer "{}" is using the QGIS authentication database. You must either use a PostgreSQL ' + 'service or store the login and password in the layer.').format(layer.name()) + connection_error = True + + if datasource.service(): + # We trust the user about login, password etc ... + continue + + # Users might be hosted on lizmap.com but using an external database + if datasource.host().endswith("lizmap.com"): + if not datasource.username() or not datasource.password(): layer_error[layer.name()] = tr( - 'The layer "{}" is using the QGIS authentication database. You must either use a PostgreSQL ' - 'service or store the login and password in the layer.').format(layer.name()) + 'The layer "{}" is missing some credentials. Either the user and/or the password is not in ' + 'the layer datasource.' + ).format(layer.name()) connection_error = True - if datasource.service(): - # We trust the user about login, password etc ... - continue - - # Users might be hosted on lizmap.com but using an external database - if datasource.host().endswith("lizmap.com"): - if not datasource.username() or not datasource.password(): - layer_error[layer.name()] = tr( - 'The layer "{}" is missing some credentials. Either the user and/or the password is not in ' - 'the layer datasource.' - ).format(layer.name()) - connection_error = True - components = QgsProviderRegistry.instance().decodeUri(layer.dataProvider().name(), layer.source()) if 'path' not in components.keys(): # The layer is not file base.