From 1576e66084afecf18620f886cca14b3dba252c2b Mon Sep 17 00:00:00 2001 From: Etienne Trimaille Date: Fri, 22 Nov 2024 16:43:30 +0100 Subject: [PATCH] Add new dialog to generate all projects --- dynamic_layers/core/generate_projects.py | 13 ++ dynamic_layers/dynamic_layers.py | 46 +++-- dynamic_layers/generate_projects.py | 162 ++++++++++++++++ .../processing_provider/generate_projects.py | 3 +- .../resources/ui/generate_projects.ui | 174 ++++++++++++++++++ 5 files changed, 372 insertions(+), 26 deletions(-) create mode 100644 dynamic_layers/generate_projects.py create mode 100644 dynamic_layers/resources/ui/generate_projects.ui diff --git a/dynamic_layers/core/generate_projects.py b/dynamic_layers/core/generate_projects.py index f340bb4..5bff00f 100644 --- a/dynamic_layers/core/generate_projects.py +++ b/dynamic_layers/core/generate_projects.py @@ -14,6 +14,7 @@ QgsProject, QgsVectorLayer, ) +from qgis.PyQt.QtWidgets import QApplication from dynamic_layers.core.dynamic_layers_engine import DynamicLayersEngine from dynamic_layers.tools import ( @@ -35,6 +36,7 @@ def __init__( destination: Path, copy_side_car_files: bool, feedback: QgsProcessingFeedback = None, + limit: int = None, ): """ Constructor. """ self.project = project @@ -44,6 +46,7 @@ def __init__( self.expression_destination = expression_destination self.copy_side_car_files = copy_side_car_files self.feedback = feedback + self.limit = limit def process(self) -> bool: """ Generate all projects needed according to the coverage layer. """ @@ -64,12 +67,22 @@ def process(self) -> bool: request = QgsFeatureRequest() # noinspection PyUnresolvedReferences request.setFlags(QgsFeatureRequest.NoGeometry) + if self.limit >= 0: + # For debug only + request.setLimit(self.limit) + if total >= self.limit: + total = self.limit + for i, feature in enumerate(self.coverage.getFeatures(request)): if self.feedback: if self.feedback.isCanceled(): break self.feedback.pushDebugInfo(tr('Feature : {}').format(feature.id())) + if hasattr(self.feedback, 'widget'): + # It's the own Feedback object + QApplication.processEvents() + engine.set_layer_and_feature(self.coverage, feature) engine.update_dynamic_layers_datasource() if self.feedback: diff --git a/dynamic_layers/dynamic_layers.py b/dynamic_layers/dynamic_layers.py index a59bca6..69c3c28 100644 --- a/dynamic_layers/dynamic_layers.py +++ b/dynamic_layers/dynamic_layers.py @@ -2,8 +2,7 @@ __license__ = 'GPL version 3' __email__ = 'info@3liz.org' -from qgis import processing -from qgis.core import Qgis, QgsApplication, QgsMessageLog, QgsProject +from qgis.core import Qgis, QgsApplication, QgsMessageLog from qgis.gui import QgisInterface from qgis.PyQt.QtCore import QCoreApplication, QSettings, QTranslator from qgis.PyQt.QtGui import QIcon @@ -11,7 +10,7 @@ from dynamic_layers.definitions import PLUGIN_MESSAGE from dynamic_layers.dynamic_layers_dialog import DynamicLayersDialog -from dynamic_layers.processing_provider.provider import Provider +from dynamic_layers.generate_projects import GenerateProjectsDialog from dynamic_layers.tools import open_help, plugin_path, resources_path, tr @@ -21,7 +20,7 @@ class DynamicLayers: def __init__(self, iface: QgisInterface): """Constructor.""" self.iface = iface - self.provider = None + # self.provider = None self.help_action_about_menu = None self.menu = None @@ -40,22 +39,23 @@ def __init__(self, iface: QgisInterface): QCoreApplication.installTranslator(self.translator) # noinspection PyPep8Naming - def initProcessing(self): - """ Init processing provider. """ - self.provider = Provider() - # noinspection PyArgumentList - QgsApplication.processingRegistry().addProvider(self.provider) + # def initProcessing(self): + # """ Init processing provider. """ + # self.provider = Provider() + # # noinspection PyArgumentList + # QgsApplication.processingRegistry().addProvider(self.provider) # noinspection PyPep8Naming def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" + # noinspection PyArgumentList main_icon = QIcon(str(resources_path('icons', 'icon.png'))) self.menu = QMenu("Dynamic Layers") self.menu.setIcon(main_icon) self.main_dialog_action = QAction(main_icon, tr("Setup the project"), self.iface.mainWindow()) # noinspection PyUnresolvedReferences - self.main_dialog_action.triggered.connect(self.run) + self.main_dialog_action.triggered.connect(self.open_single_project_dialog) self.menu.addAction(self.main_dialog_action) # noinspection PyArgumentList @@ -65,12 +65,12 @@ def initGui(self): self.iface.mainWindow() ) # noinspection PyUnresolvedReferences - self.generate_projects_action.triggered.connect(self.generate_projects_clicked) + self.generate_projects_action.triggered.connect(self.open_generate_projects_dialog) self.menu.addAction(self.generate_projects_action) self.iface.pluginMenu().addMenu(self.menu) - self.initProcessing() + # self.initProcessing() # Open the online help self.help_action_about_menu = QAction(main_icon, tr('Project generator'), self.iface.mainWindow()) @@ -80,9 +80,9 @@ def initGui(self): def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" - if self.provider: - # noinspection PyArgumentList - QgsApplication.processingRegistry().removeProvider(self.provider) + # if self.provider: + # # noinspection PyArgumentList + # QgsApplication.processingRegistry().removeProvider(self.provider) if self.generate_projects_action: self.iface.removePluginMenu("Dynamic Layers", self.generate_projects_action) @@ -97,17 +97,15 @@ def unload(self): del self.help_action_about_menu @staticmethod - def generate_projects_clicked(): - """ Open the Processing algorithm dialog. """ - # noinspection PyUnresolvedReferences - processing.execAlgorithmDialog( - "dynamic_layers:generate_projects", - {} - ) + def open_generate_projects_dialog(): + """ Open the generate projects dialog. """ + dialog = GenerateProjectsDialog() + dialog.exec() + del dialog @staticmethod - def run(): - """ Open the plugin dialog. """ + def open_single_project_dialog(): + """ Open the single project dialog. """ dialog = DynamicLayersDialog() dialog.populate_layer_table() dialog.populate_variable_table() diff --git a/dynamic_layers/generate_projects.py b/dynamic_layers/generate_projects.py new file mode 100644 index 0000000..9845d1e --- /dev/null +++ b/dynamic_layers/generate_projects.py @@ -0,0 +1,162 @@ +__copyright__ = 'Copyright 2024, 3Liz' +__license__ = 'GPL version 3' +__email__ = 'info@3liz.org' + + +from pathlib import Path + +from qgis.core import ( + QgsApplication, + QgsExpressionContext, + QgsExpressionContextUtils, + QgsMapLayerProxyModel, + QgsProcessingException, + QgsProcessingFeedback, + QgsProject, +) +from qgis.gui import QgsExpressionBuilderDialog, QgsFileWidget +from qgis.PyQt import uic +from qgis.PyQt.QtGui import QIcon +from qgis.PyQt.QtWidgets import ( + QDialog, + QDialogButtonBox, + QPlainTextEdit, + QProgressBar, +) +from qgis.utils import OverrideCursor + +from dynamic_layers.core.generate_projects import GenerateProjects +from dynamic_layers.definitions import QtVar +from dynamic_layers.tools import open_help, tr + +folder = Path(__file__).resolve().parent +ui_file = folder / 'resources' / 'ui' / 'generate_projects.ui' +FORM_CLASS, _ = uic.loadUiType(ui_file) + + +class GenerateProjectsDialog(QDialog, FORM_CLASS): + # noinspection PyArgumentList + def __init__(self, parent: QDialog = None): + """Constructor.""" + # noinspection PyArgumentList + super().__init__(parent) + self.setupUi(self) + self.setWindowTitle(tr("Generate many QGIS projects")) + self.project = QgsProject.instance() + + self.button_box.button(QDialogButtonBox.Apply).clicked.connect(self.generate_projects) + self.button_box.button(QDialogButtonBox.Help).clicked.connect(open_help) + self.button_box.button(QDialogButtonBox.Cancel).clicked.connect(self.close) + + self.expression.setText("") + self.expression.setToolTip(tr("Open the expression builder")) + self.expression.setIcon(QIcon(QgsApplication.iconPath('mIconExpression.svg'))) + self.expression.clicked.connect(self.open_expression_builder) + + self.coverage.setFilters(QgsMapLayerProxyModel.Filter.VectorLayer) + self.coverage.layerChanged.connect(self.layer_changed) + + self.destination.setStorageMode(QgsFileWidget.StorageMode.GetDirectory) + self.field.setAllowEmptyFieldName(False) + self.layer_changed() + self.debug_limit.setValue(0) + + # DEBUG + self.file_name.setText('"schema" || \'/test_\' || "schema" || \'.qgs\'') + self.destination.setFilePath('/tmp/demo_cartophyl') + self.debug_limit.setValue(5) + + def layer_changed(self): + self.field.setLayer(self.coverage.currentLayer()) + + def open_expression_builder(self): + """ Open the expression builder. """ + layer = self.coverage.currentLayer() + if not layer: + return + context = QgsExpressionContext() + context.appendScope(QgsExpressionContextUtils.globalScope()) + context.appendScope(QgsExpressionContextUtils.projectScope(QgsProject.instance())) + context.appendScope(QgsExpressionContextUtils.layerScope(layer)) + + dialog = QgsExpressionBuilderDialog(layer, context=context) + dialog.setExpressionText(self.file_name.text()) + result = dialog.exec() + + if result != QDialog.Accepted: + return + + content = dialog.expressionText() + self.file_name.setText(content) + + def generate_projects(self): + """The OK button to generate all projects. """ + layer = self.coverage.currentLayer() + if not layer: + return + + feedback = TextFeedBack(self.logs, self.progress) + + if self.project.isDirty(): + feedback.reportError(tr("You must save your project first.")) + + self.button_box.button(QDialogButtonBox.Apply).setEnabled(False) + result = False + with OverrideCursor(QtVar.WaitCursor): + self.logs.clear() + + generator = GenerateProjects( + self.project, + layer, + self.field.currentField(), + self.file_name.text(), + Path(self.destination.filePath()), + self.copy_side_care_files.isChecked(), + feedback, + limit=self.debug_limit.value(), + ) + + try: + result = generator.process() + except QgsProcessingException as e: + feedback.reportError(str(e)) + except Exception as e: + feedback.reportError(str(e)) + + if result: + feedback.pushInfo(tr("End") + " 👍") + feedback.pushInfo(tr("Dialog can be closed")) + # In case of success, the button is not enabled again + else: + feedback.pushWarning(tr("End, but there was an error")) + self.button_box.button(QDialogButtonBox.Apply).setEnabled(True) + + +class TextFeedBack(QgsProcessingFeedback): + + def __init__(self, widget: QPlainTextEdit, progress: QProgressBar): + super().__init__() + self.widget = widget + self.progress = progress + + def setProgressText(self, text): + pass + + def setProgress(self, i: int): + self.progress.setValue(i) + + def pushInfo(self, text): + self.widget.appendHtml(f"

{text}

") + + def pushCommandInfo(self, text): + self.widget.appendHtml(f"

{text}

") + + def pushDebugInfo(self, text): + self.widget.appendHtml(f"

{text}

") + + def pushConsoleInfo(self, text): + self.widget.appendHtml(f"

{text}

") + + def reportError(self, text, fatal_error=False): + _ = fatal_error + self.widget.appendHtml(f"

{text}

") diff --git a/dynamic_layers/processing_provider/generate_projects.py b/dynamic_layers/processing_provider/generate_projects.py index e597c60..227dccf 100644 --- a/dynamic_layers/processing_provider/generate_projects.py +++ b/dynamic_layers/processing_provider/generate_projects.py @@ -5,12 +5,11 @@ from pathlib import Path from typing import Tuple -from qgis.core import ( +from qgis.core import ( # QgsFeatureRequest, QgsExpression, QgsProcessing, QgsProcessingAlgorithm, QgsProcessingException, - # QgsFeatureRequest, QgsProcessingParameterBoolean, QgsProcessingParameterExpression, QgsProcessingParameterFeatureSource, diff --git a/dynamic_layers/resources/ui/generate_projects.ui b/dynamic_layers/resources/ui/generate_projects.ui new file mode 100644 index 0000000..26749d0 --- /dev/null +++ b/dynamic_layers/resources/ui/generate_projects.ui @@ -0,0 +1,174 @@ + + + Dialog + + + + 0 + 0 + 843 + 696 + + + + Dialog + + + + + + Coverage layer + + + + + + + + + + Field having unique values + + + + + + + + + + Copy all project side-car files, for instance copy "project_photo.qgs.png" if the file is existing. + + + + + + + QGS Expression to format the final filename. It must end with .qgs or .qgz. + + + + + + + + + + + + E + + + + + + + + + Destination folder + + + + + + + + + + Logs from the execution + + + + + + + true + + + + + + + 0 + + + + + + + Debug + + + true + + + true + + + + + + For debug only, set 0 to disable it. Maximum number of features on the coverage layer. + + + true + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help + + + + + + + + QgsCollapsibleGroupBox + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsFieldComboBox + QComboBox +
qgsfieldcombobox.h
+
+ + QgsFileWidget + QWidget +
qgsfilewidget.h
+
+ + QgsMapLayerComboBox + QComboBox +
qgsmaplayercombobox.h
+
+
+ + +