diff --git a/dynamic_layers/definitions.py b/dynamic_layers/definitions.py new file mode 100644 index 0000000..8cf58e6 --- /dev/null +++ b/dynamic_layers/definitions.py @@ -0,0 +1,13 @@ +__copyright__ = 'Copyright 2024, 3Liz' +__license__ = 'GPL version 3' +__email__ = 'info@3liz.org' + +from qgis.PyQt.QtGui import QColor + +GREEN = QColor(175, 208, 126) + + +class CustomProperty: + DynamicDatasourceActive = 'dynamicDatasourceActive' + DynamicDatasourceContent = 'dynamicDatasourceContent' + diff --git a/dynamic_layers/dynamic_layers.py b/dynamic_layers/dynamic_layers.py index 5b0e129..c3435dc 100644 --- a/dynamic_layers/dynamic_layers.py +++ b/dynamic_layers/dynamic_layers.py @@ -26,7 +26,7 @@ from functools import partial from qgis.PyQt.QtCore import Qt, QSettings, QTranslator, QCoreApplication -from qgis.PyQt.QtGui import QAction, QIcon, QTextCursor, QColor +from qgis.PyQt.QtGui import QAction, QIcon, QTextCursor from qgis.PyQt.QtWidgets import qApp, QMessageBox, QTableWidgetItem from qgis.core import Qgis, QgsMapLayer, QgsIconUtils, QgsProject from qgis.utils import OverrideCursor @@ -34,8 +34,7 @@ from dynamic_layers.dynamic_layers_dialog import DynamicLayersDialog from dynamic_layers.dynamic_layers_engine import DynamicLayersEngine from dynamic_layers.tools import resources_path - -GREEN = QColor(175, 208, 126) +from dynamic_layers.definitions import GREEN, CustomProperty class DynamicLayers: @@ -69,6 +68,8 @@ def __init__(self, iface): QCoreApplication.installTranslator(self.translator) + self.project = QgsProject.instance() + # Create the dialog (after translation) and keep reference self.dlg = DynamicLayersDialog() @@ -342,7 +343,7 @@ def populate_layer_table(self): self.dlg.twLayers.setHorizontalHeaderLabels(tuple(columns)) # load content from project layers - for layer in QgsProject.instance().mapLayers().values(): + for layer in self.project.mapLayers().values(): line_data = [] @@ -353,7 +354,7 @@ def populate_layer_table(self): self.dlg.twLayers.setColumnCount(col_count) i = 0 - if layer.customProperty('dynamicDatasourceActive') == str(True): + if layer.customProperty(CustomProperty.DynamicDatasourceActive) == str(True): bg = GREEN else: bg = Qt.transparent @@ -402,11 +403,11 @@ def get_layer_property(layer: QgsMapLayer, prop: str) -> str | None: elif prop == 'uri': return layer.dataProvider().dataSourceUri().split('|')[0] - elif prop == 'dynamicDatasourceActive': - return layer.customProperty('dynamicDatasourceActive') + elif prop == CustomProperty.DynamicDatasourceActive: + return layer.customProperty(CustomProperty.DynamicDatasourceActive) - elif prop == 'dynamicDatasourceContent': - return layer.customProperty('dynamicDatasourceContent') + elif prop == CustomProperty.DynamicDatasourceContent: + return layer.customProperty(CustomProperty.DynamicDatasourceContent) else: return None @@ -435,8 +436,7 @@ def on_row_selection_changed(self): # Get layer layer_id = self.dlg.twLayers.item(row, 0).data(Qt.EditRole) - lr = QgsProject.instance() - layer = lr.mapLayer(layer_id) + layer = self.project.mapLayer(layer_id) if not layer: show_layer_properties = False else: @@ -470,7 +470,7 @@ def on_row_selection_changed(self): widget.setCurrentIndex(list_dic[val]) # "active" checkbox - is_active = layer.customProperty('dynamicDatasourceActive') == str(True) + is_active = layer.customProperty(CustomProperty.DynamicDatasourceActive) == str(True) self.dlg.cbDatasourceActive.setChecked(is_active) def on_cb_datasource_active_change(self): @@ -507,8 +507,8 @@ def on_cb_datasource_active_change(self): self.dlg.twLayers.item(row, 2).setData(Qt.EditRole, input_value) # Record the new value in the project - self.selectedLayer.setCustomProperty('dynamicDatasourceActive', input_value) - QgsProject.instance().setDirty(True) + self.selectedLayer.setCustomProperty(CustomProperty.DynamicDatasourceActive, input_value) + self.project.setDirty(True) def on_layer_property_change(self, key: str): """ @@ -533,7 +533,7 @@ def on_layer_property_change(self, key: str): # Record the new value in the project self.selectedLayer.setCustomProperty(item['xml'], input_value) - QgsProject.instance().setDirty(True) + self.project.setDirty(True) def on_copy_from_layer(self): """ @@ -590,7 +590,7 @@ def populate_variable_table(self): Fill the variable table """ # Get the list of variable from the project - variable_list = QgsProject.instance().readListEntry('PluginDynamicLayers', 'VariableList') + variable_list = self.project.readListEntry('PluginDynamicLayers', 'VariableList') if not variable_list: return @@ -672,9 +672,8 @@ def on_add_variable_clicked(self): self.variableList.append(v_name) # Add variable to the project - project = QgsProject.instance() - project.writeEntry('PluginDynamicLayers', 'VariableList', self.variableList) - project.setDirty(True) + self.project.writeEntry('PluginDynamicLayers', 'VariableList', self.variableList) + self.project.setDirty(True) def on_remove_variable_clicked(self): """ @@ -698,9 +697,8 @@ def on_remove_variable_clicked(self): self.variableList.remove(v_name) # Update project - p = QgsProject.instance() - p.writeEntry('PluginDynamicLayers', 'VariableList', self.variableList) - p.setDirty(True) + self.project.writeEntry('PluginDynamicLayers', 'VariableList', self.variableList) + self.project.setDirty(True) # Remove selected lines self.dlg.twVariableList.removeRow(self.dlg.twVariableList.currentRow()) @@ -740,21 +738,20 @@ def on_copy_from_project_clicked(self): return # Check if project has got some WMS capabilities - project = QgsProject.instance() # Title p_title = '' - if project.readEntry('ProjectTitle', '/PluginDynamicLayers'): - p_title = project.readEntry('ProjectTitle', '/PluginDynamicLayers')[0] - if not p_title and project.readEntry('WMSServiceTitle', "/"): - p_title = project.readEntry('WMSServiceTitle', "/")[0] + if self.project.readEntry('ProjectTitle', '/PluginDynamicLayers'): + p_title = self.project.readEntry('ProjectTitle', '/PluginDynamicLayers')[0] + if not p_title and self.project.readEntry('WMSServiceTitle', "/"): + p_title = self.project.readEntry('WMSServiceTitle', "/")[0] # Abstract p_abstract = '' - if project.readEntry('ProjectAbstract', '/PluginDynamicLayers'): - p_abstract = project.readEntry('ProjectAbstract', '/PluginDynamicLayers')[0] - if not p_abstract and project.readEntry('WMSServiceAbstract', "/"): - p_abstract = project.readEntry('WMSServiceAbstract', "/")[0] + if self.project.readEntry('ProjectAbstract', '/PluginDynamicLayers'): + p_abstract = self.project.readEntry('ProjectAbstract', '/PluginDynamicLayers')[0] + if not p_abstract and self.project.readEntry('WMSServiceAbstract', "/"): + p_abstract = self.project.readEntry('WMSServiceAbstract', "/")[0] ask = False previous_title = self.dlg.inProjectTitle.text() @@ -778,8 +775,8 @@ def on_copy_from_project_clicked(self): if result == QMessageBox.No: return - if not project.readEntry('WMSServiceCapabilities', "/")[1]: - project.writeEntry('WMSServiceCapabilities', "/", str(True)) + if not self.project.readEntry('WMSServiceCapabilities', "/")[1]: + self.project.writeEntry('WMSServiceCapabilities', "/", str(True)) self.dlg.inProjectTitle.setText(p_title) self.dlg.inProjectAbstract.setText(p_abstract) @@ -810,26 +807,21 @@ def on_project_property_changed(self, prop: str) -> str | None: else: return - p = QgsProject.instance() - # Store value into the project xml = self.projectPropertiesInputs[prop]['xml'] - p.writeEntry('PluginDynamicLayers', xml, val) - p.setDirty(True) + self.project.writeEntry('PluginDynamicLayers', xml, val) + self.project.setDirty(True) def populate_project_properties(self): """ Fill in the project properties item from XML """ - - p = QgsProject.instance() - # lr = QgsProject.instance() # Fill the property from the PluginDynamicLayers XML for prop, item in self.projectPropertiesInputs.items(): widget = item['widget'] xml = self.projectPropertiesInputs[prop]['xml'] - val = p.readEntry('PluginDynamicLayers', xml) + val = self.project.readEntry('PluginDynamicLayers', xml) if val: val = val[0] if not val: @@ -862,7 +854,7 @@ def on_apply_variables_clicked(self, source: str = 'table'): dle = DynamicLayersEngine() # Set the dynamic layers list - dle.set_dynamic_layers_list() + dle.set_dynamic_layers_list(self.project) # Set search and replace dictionary # Collect variables names and values @@ -882,7 +874,7 @@ def on_apply_variables_clicked(self, source: str = 'table'): dle.set_dynamic_layers_datasource_from_dic() # Set project properties - dle.set_dynamic_project_properties() + dle.set_dynamic_project_properties(self.project) # Set extent layer extent_layer = self.dlg.inExtentLayer.currentLayer() @@ -895,11 +887,10 @@ def on_apply_variables_clicked(self, source: str = 'table'): dle.set_extent_margin(extent_margin) # Set new extent - dle.set_project_extent() + dle.set_project_extent(self.project) # Set project as dirty - p = QgsProject.instance() - p.setDirty(True) + self.project.setDirty(True) def run(self): """Run method that performs all the real work""" diff --git a/dynamic_layers/dynamic_layers_engine.py b/dynamic_layers/dynamic_layers_engine.py index 5fb2bfd..71ebb16 100644 --- a/dynamic_layers/dynamic_layers_engine.py +++ b/dynamic_layers/dynamic_layers_engine.py @@ -34,6 +34,8 @@ QgsRectangle, ) +from dynamic_layers.definitions import CustomProperty + try: from qgis.utils import iface except Exception: @@ -87,8 +89,8 @@ def __init__( return self.layer = layer - self.dynamic_datasource_active = layer.customProperty('dynamicDatasourceActive') == str(True) - self.dynamic_datasource_content = layer.customProperty('dynamicDatasourceContent') + self.dynamic_datasource_active = layer.customProperty(CustomProperty.DynamicDatasourceActive) == str(True) + self.dynamic_datasource_content = layer.customProperty(CustomProperty.DynamicDatasourceContent) def set_new_source_uri_from_dict(self, search_and_replace_dictionary: dict = None): """ @@ -296,16 +298,15 @@ def set_search_and_replace_dictionary_from_layer(self, layer: QgsVectorLayer, ex self.search_and_replace_dictionary = search_and_replace_dictionary - def set_dynamic_layers_list(self): + def set_dynamic_layers_list(self, project: QgsProject): """ Add the passed layers to the dynamic layers dictionary """ # Get the layers with dynamicDatasourceActive enable - lr = QgsProject.instance() self.dynamic_layers = { - lid: layer for lid, layer in lr.mapLayers().items() if - layer.customProperty('dynamicDatasourceActive') == str(True) and layer.customProperty( - 'dynamicDatasourceContent') + lid: layer for lid, layer in project.mapLayers().items() if + layer.customProperty(CustomProperty.DynamicDatasourceActive) == str(True) and layer.customProperty( + CustomProperty.DynamicDatasourceContent) } def set_dynamic_layers_datasource_from_dic(self): @@ -323,69 +324,66 @@ def set_dynamic_layers_datasource_from_dic(self): a = LayerDataSourceModifier(layer) a.set_new_source_uri_from_dict(self.search_and_replace_dictionary) - if self.iface and layer.renderer() and layer.renderer().type() == 'graduatedSymbol': + if not self.iface: + continue + + if layer.renderer() and layer.renderer().type() == 'graduatedSymbol': layer.triggerRepaint() - if self.iface: - self.iface.actionDraw().trigger() - self.iface.mapCanvas().refresh() + if not self.iface: + return + + self.iface.actionDraw().trigger() + self.iface.mapCanvas().refresh() - def set_dynamic_project_properties(self, title: str = None, abstract: str = None): + def set_dynamic_project_properties(self, project: QgsProject, title: str = None, abstract: str = None): """ Set some project properties : title, abstract based on the templates stored in the project file in and by using the search and replace dictionary """ - # Get project instance - p = QgsProject.instance() - # Make sure WMS Service is active - if not p.readEntry('WMSServiceCapabilities', "/")[1]: - p.writeEntry('WMSServiceCapabilities', "/", "True") + if not project.readEntry('WMSServiceCapabilities', "/")[1]: + project.writeEntry('WMSServiceCapabilities', "/", "True") # title if not title: xml = 'ProjectTitle' - val = p.readEntry('PluginDynamicLayers', xml) + val = project.readEntry('PluginDynamicLayers', xml) if val: title = val[0] - self.set_project_property('title', title) + self.set_project_property(project, 'title', title) # abstract if not abstract: xml = 'ProjectAbstract' - val = p.readEntry('PluginDynamicLayers', xml) + val = project.readEntry('PluginDynamicLayers', xml) if val: abstract = val[0] - self.set_project_property('abstract', abstract) + self.set_project_property(project, 'abstract', abstract) - def set_project_property(self, prop: str, val: str): + def set_project_property(self, project: QgsProject, prop: str, val: str): """ Set a project property And replace variable if found in the properties """ - # Get project instance - p = QgsProject.instance() - # Replace variable in given val via dictionary t = DynamicLayersTools() val = t.search_and_replace_string_by_dictionary(val, self.search_and_replace_dictionary) # Title if prop == 'title': - p.writeEntry('WMSServiceTitle', '', val) + project.writeEntry('WMSServiceTitle', '', val) # Abstract elif prop == 'abstract': - p.writeEntry('WMSServiceAbstract', '',val) + project.writeEntry('WMSServiceAbstract', '', val) - def set_project_extent(self) -> QgsRectangle: + def set_project_extent(self, project: QgsProject) -> QgsRectangle: """ Sets the project extent and corresponding XML property """ - p = QgsProject.instance() - # Get extent from extent layer (if given) p_extent = None if self.extent_layer: @@ -413,7 +411,7 @@ def set_project_extent(self) -> QgsRectangle: p_extent.yMaximum(), ] p_wms_extent = [str(i) for i in p_wms_extent] - p.writeEntry('WMSExtent', '', p_wms_extent) + project.writeEntry('WMSExtent', '', p_wms_extent) # Zoom canvas to extent if self.iface: diff --git a/tests/test_basic_replacement.py b/tests/test_basic_replacement.py index 2534909..f177741 100644 --- a/tests/test_basic_replacement.py +++ b/tests/test_basic_replacement.py @@ -9,6 +9,8 @@ from qgis.core import QgsVectorLayer from qgis.core import QgsProject +from dynamic_layers.definitions import CustomProperty +from dynamic_layers.dynamic_layers_engine import DynamicLayersEngine from tests.base_tests import BaseTests @@ -22,6 +24,41 @@ def test_replacement_map_layer(self): project.addMapLayer(vector) self.assertEqual(1, len(project.mapLayers())) + engine = DynamicLayersEngine() + engine.set_dynamic_layers_list(project) + self.assertDictEqual({}, engine.dynamic_layers) + + vector.setCustomProperty(CustomProperty.DynamicDatasourceActive, str(True)) + dynamic_source = vector.source() + + self.assertIn("folder_1", dynamic_source) + self.assertNotIn("folder_2", dynamic_source) + self.assertNotIn("{$folder}", dynamic_source) + + dynamic_source = dynamic_source.replace("folder_1", "{$folder}") + vector.setCustomProperty(CustomProperty.DynamicDatasourceContent, dynamic_source) + + engine.set_dynamic_layers_list(project) + self.assertDictEqual( + { + vector.id(): vector + }, + engine.dynamic_layers + ) + + # Replace + variables = { + 'folder': 'folder_2', + } + engine.set_search_and_replace_dictionary(variables) + + engine.set_dynamic_layers_datasource_from_dic() + engine.set_dynamic_project_properties(project, "Test title", "Test abstract") + + self.assertIn("folder_2", vector.source()) + self.assertNotIn("folder_1", vector.source()) + self.assertNotIn("{$folder}", vector.source()) + if __name__ == '__main__': unittest.main()