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}{0}>'.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}{0}>'.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.