From 6b0d5125d5aeae4d2f058856941ace679c84caee Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Fri, 17 Jan 2025 14:32:47 +0200 Subject: [PATCH 1/6] add plan propositions - add plan proposition model - add widget for plan proposition - handle saving and loading plan propositions for plan regulation groups - add possibility to define plan propositions for custom plan regulation groups - add PlanThemeLayer code layer model - remove spacer and horizontal line layout items from plan regulation widget --- arho_feature_template/core/models.py | 12 ++ arho_feature_template/core/plan_manager.py | 23 ++- .../gui/components/plan_proposition_widget.py | 112 +++++++++++++ .../gui/components/plan_proposition_widget.ui | 154 ++++++++++++++++++ .../plan_regulation_group_widget.py | 18 +- .../plan_regulation_group_widget.ui | 18 ++ .../gui/components/plan_regulation_widget.ui | 102 ++++-------- .../dialogs/new_plan_regulation_group_form.py | 40 ++++- .../dialogs/new_plan_regulation_group_form.ui | 32 +++- .../project/layers/code_layers.py | 4 + .../project/layers/plan_layers.py | 41 ++++- 11 files changed, 472 insertions(+), 84 deletions(-) create mode 100644 arho_feature_template/gui/components/plan_proposition_widget.py create mode 100644 arho_feature_template/gui/components/plan_proposition_widget.ui diff --git a/arho_feature_template/core/models.py b/arho_feature_template/core/models.py index bb0eeb7..5a68b8d 100644 --- a/arho_feature_template/core/models.py +++ b/arho_feature_template/core/models.py @@ -265,6 +265,16 @@ class Regulation: # value_number_pair: tuple[Number, Number] | None +@dataclass +class Proposition: + name: str + value: str + theme_id: str | None = None + proposition_number: int | None = None + regulation_group_id_: int | None = None + id_: int | None = None + + @dataclass class RegulationGroup: type_code_id: str | None @@ -273,6 +283,7 @@ class RegulationGroup: color_code: str | None group_number: int | None = None regulations: list[Regulation] = field(default_factory=list) + propositions: list[Proposition] = field(default_factory=list) id_: int | None = None @classmethod @@ -308,6 +319,7 @@ def from_config_data(cls, data: dict) -> RegulationGroup: color_code=data.get("color_code"), group_number=data.get("group_number"), regulations=regulations, + propositions=[], id_=None, ) diff --git a/arho_feature_template/core/plan_manager.py b/arho_feature_template/core/plan_manager.py index 60b102d..8bb0fdd 100644 --- a/arho_feature_template/core/plan_manager.py +++ b/arho_feature_template/core/plan_manager.py @@ -33,6 +33,7 @@ OtherPointLayer, PlanFeatureLayer, PlanLayer, + PlanPropositionLayer, PlanRegulationLayer, RegulationGroupAssociationLayer, RegulationGroupLayer, @@ -51,7 +52,7 @@ if TYPE_CHECKING: from qgis.core import QgsFeature - from arho_feature_template.core.models import Regulation, RegulationGroup + from arho_feature_template.core.models import Proposition, Regulation, RegulationGroup logger = logging.getLogger(__name__) @@ -517,6 +518,12 @@ def save_regulation_group(regulation_group: RegulationGroup, plan_id: str | None regulation.regulation_group_id_ = feature["id"] # Updating regulation group ID save_regulation(regulation) + # Handle propositions + if regulation_group.propositions: + for proposition in regulation_group.propositions: + proposition.regulation_group_id_ = feature["id"] # Updating regulation group ID + save_proposition(proposition) + return feature @@ -545,3 +552,17 @@ def save_regulation(regulation: Regulation) -> QgsFeature: ) return feature + + +def save_proposition(proposition: Proposition) -> QgsFeature: + feature = PlanPropositionLayer.feature_from_model(proposition) + layer = PlanPropositionLayer.get_from_project() + + _save_feature( + feature=feature, + layer=layer, + id_=proposition.id_, + edit_text="Kaavasuosituksen lisäys" if proposition.id_ is None else "Kaavasuosituksen muokkaus", + ) + + return feature diff --git a/arho_feature_template/gui/components/plan_proposition_widget.py b/arho_feature_template/gui/components/plan_proposition_widget.py new file mode 100644 index 0000000..8736891 --- /dev/null +++ b/arho_feature_template/gui/components/plan_proposition_widget.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from importlib import resources +from typing import TYPE_CHECKING + +from qgis.core import QgsApplication +from qgis.PyQt import uic +from qgis.PyQt.QtCore import Qt, pyqtSignal +from qgis.PyQt.QtWidgets import QFormLayout, QLabel, QMenu, QTextEdit, QToolButton, QWidget + +from arho_feature_template.core.models import Proposition +from arho_feature_template.gui.components.code_combobox import CodeComboBox +from arho_feature_template.gui.components.plan_regulation_input_widgets import IntegerInputWidget +from arho_feature_template.project.layers.code_layers import PlanThemeLayer + +if TYPE_CHECKING: + from qgis.PyQt.QtWidgets import QLineEdit, QPushButton + +ui_path = resources.files(__package__) / "plan_proposition_widget.ui" +FormClass, _ = uic.loadUiType(ui_path) + +# TO BE REPLACED +LANGUAGE = "fin" + + +class PropositionWidget(QWidget, FormClass): # type: ignore + """A widget representation of a plan proposition.""" + + delete_signal = pyqtSignal(QWidget) + + def __init__(self, proposition: Proposition, parent=None): + super().__init__(parent) + self.setupUi(self) + + # TYPES + self.name: QLineEdit + self.value_label: QLabel + self.text_input: QTextEdit + self.add_field_btn: QPushButton + self.del_btn: QPushButton + self.form_layout: QFormLayout + self.expand_hide_btn: QToolButton + + # INIT + self.proposition = proposition + self.proposition_number_widget: IntegerInputWidget | None = None + self.theme_widget: CodeComboBox | None = None + + # List of widgets for hiding / showing + self.widgets: list[tuple[QLabel, QWidget]] = [(self.value_label, self.text_input)] + + add_field_menu = QMenu(self) + add_field_menu.addAction("Suositusnumero").triggered.connect(self._add_proposition_number) + add_field_menu.addAction("Kaavoitusteema").triggered.connect(self._add_theme) + self.add_field_btn.setMenu(add_field_menu) + self.add_field_btn.setIcon(QgsApplication.getThemeIcon("mActionAdd.svg")) + + self.del_btn.setIcon(QgsApplication.getThemeIcon("mActionDeleteSelected.svg")) + self.del_btn.clicked.connect(lambda: self.delete_signal.emit(self)) + + self.expanded = True + self.expand_hide_btn.clicked.connect(self._on_expand_hide_btn_clicked) + + self.name.setText(proposition.name) + self.text_input.setText(proposition.value) + self._add_theme(proposition.theme_id) + self._add_proposition_number(proposition.proposition_number) + + def _add_widgets(self, label: QLabel, widget: QWidget): + self.form_layout.addRow(label, widget) + self.widgets.append((label, widget)) + if not self.expanded: + self._on_expand_hide_btn_clicked() + + def _add_proposition_number(self, default_value: int | None = None): + if not self.proposition_number_widget: + self.proposition_number_widget = IntegerInputWidget(default_value, None, True) + self._add_widgets(QLabel("Suositusnumero"), self.proposition_number_widget) + + def _add_theme(self, default_value: str | None = None): + if not self.theme_widget: + self.theme_widget = CodeComboBox() + self.theme_widget.populate_from_code_layer(PlanThemeLayer) + if default_value: + self.theme_widget.set_value(default_value) + self._add_widgets(QLabel("Kaavoitusteema"), self.theme_widget) + + def _on_expand_hide_btn_clicked(self): + if self.expanded: + for label, value_widget in self.widgets: + self.form_layout.removeWidget(label) + label.hide() + self.form_layout.removeWidget(value_widget) + value_widget.hide() + self.expand_hide_btn.setArrowType(Qt.ArrowType.DownArrow) + self.expanded = False + else: + for label, value_widget in self.widgets: + self.form_layout.addRow(label, value_widget) + label.show() + value_widget.show() + self.expand_hide_btn.setArrowType(Qt.ArrowType.UpArrow) + self.expanded = True + + def into_model(self) -> Proposition: + return Proposition( + name=self.name.text(), + value=self.text_input.toPlainText(), + theme_id=self.theme_widget.value() if self.theme_widget else None, + proposition_number=self.proposition_number_widget.get_value() if self.proposition_number_widget else None, + id_=self.proposition.id_, + ) diff --git a/arho_feature_template/gui/components/plan_proposition_widget.ui b/arho_feature_template/gui/components/plan_proposition_widget.ui new file mode 100644 index 0000000..73bbcb1 --- /dev/null +++ b/arho_feature_template/gui/components/plan_proposition_widget.ui @@ -0,0 +1,154 @@ + + + Form + + + + 0 + 0 + 441 + 207 + + + + + 0 + 0 + + + + + 0 + 160 + + + + Form + + + + + + + + + + + + 11 + 75 + true + + + + Kaavasuositus + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Muu tieto + + + + + + + + + + + 0 + 0 + + + + + 30 + 16777215 + + + + + + + + + + + + + + + Otsikko + + + + + + + + + false + + + + + + + Laajenna + + + false + + + Qt::UpArrow + + + + + + + + + Sisältö + + + + + + + + 0 + 0 + + + + + 600 + 120 + + + + + + + + + + + diff --git a/arho_feature_template/gui/components/plan_regulation_group_widget.py b/arho_feature_template/gui/components/plan_regulation_group_widget.py index b90ab93..cc8b79e 100644 --- a/arho_feature_template/gui/components/plan_regulation_group_widget.py +++ b/arho_feature_template/gui/components/plan_regulation_group_widget.py @@ -8,7 +8,8 @@ from qgis.PyQt.QtCore import pyqtSignal from qgis.PyQt.QtWidgets import QWidget -from arho_feature_template.core.models import Regulation, RegulationGroup +from arho_feature_template.core.models import Proposition, Regulation, RegulationGroup +from arho_feature_template.gui.components.plan_proposition_widget import PropositionWidget from arho_feature_template.gui.components.plan_regulation_widget import RegulationWidget from arho_feature_template.project.layers.code_layers import PlanRegulationGroupTypeLayer @@ -41,6 +42,9 @@ def __init__(self, regulation_group_data: RegulationGroup, layer_name: str): self.regulation_widgets: list[RegulationWidget] = [ self.add_regulation_widget(regulation) for regulation in self.regulation_group_data.regulations ] + self.proposition_widgets: list[PropositionWidget] = [ + self.add_proposition_widget(proposition) for proposition in self.regulation_group_data.propositions + ] regulation_group_data.type_code_id = PlanRegulationGroupTypeLayer.get_id_by_feature_layer_name(layer_name) self.name.setText(self.regulation_group_data.name if self.regulation_group_data.name else "") @@ -59,6 +63,17 @@ def delete_regulation_widget(self, regulation_widget: RegulationWidget): self.regulation_widgets.remove(regulation_widget) regulation_widget.deleteLater() + def add_proposition_widget(self, proposition: Proposition) -> PropositionWidget: + widget = PropositionWidget(proposition=proposition, parent=self.frame) + widget.delete_signal.connect(self.delete_proposition_widget) + self.frame.layout().addWidget(widget) + return widget + + def delete_proposition_widget(self, proposition_widget: RegulationWidget): + self.frame.layout().removeWidget(proposition_widget) + self.proposition_widgets.remove(proposition_widget) + proposition_widget.deleteLater() + def into_model(self) -> RegulationGroup: return RegulationGroup( type_code_id=self.regulation_group_data.type_code_id, @@ -66,5 +81,6 @@ def into_model(self) -> RegulationGroup: short_name=None if not self.short_name.text() else self.short_name.text(), color_code=self.regulation_group_data.color_code, regulations=[widget.into_model() for widget in self.regulation_widgets], + propositions=[widget.into_model() for widget in self.proposition_widgets], id_=self.regulation_group_data.id_, ) diff --git a/arho_feature_template/gui/components/plan_regulation_group_widget.ui b/arho_feature_template/gui/components/plan_regulation_group_widget.ui index ef0691b..6a66d13 100644 --- a/arho_feature_template/gui/components/plan_regulation_group_widget.ui +++ b/arho_feature_template/gui/components/plan_regulation_group_widget.ui @@ -34,9 +34,27 @@ QFrame::Raised + + 2 + + + + + + 13 + 75 + false + true + + + + Kaavamääräysryhmä + + + diff --git a/arho_feature_template/gui/components/plan_regulation_widget.ui b/arho_feature_template/gui/components/plan_regulation_widget.ui index e767ec4..8bcc1ba 100644 --- a/arho_feature_template/gui/components/plan_regulation_widget.ui +++ b/arho_feature_template/gui/components/plan_regulation_widget.ui @@ -6,7 +6,7 @@ 0 0 - 694 + 489 131 @@ -25,6 +25,20 @@ + + + + + 11 + 75 + true + + + + Kaavamääräys + + + @@ -40,16 +54,22 @@ - + Lisätieto + + + - + Muu tieto + + + @@ -66,8 +86,11 @@ 16777215 + + Poista kaavamääräys + - Poista + @@ -78,7 +101,7 @@ - Kaavamääräyslaji + Laji @@ -104,75 +127,6 @@ - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - - 0 - 20 - - - - Qt::Horizontal - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 15 - - - - - - diff --git a/arho_feature_template/gui/dialogs/new_plan_regulation_group_form.py b/arho_feature_template/gui/dialogs/new_plan_regulation_group_form.py index 3a257b6..233ef29 100644 --- a/arho_feature_template/gui/dialogs/new_plan_regulation_group_form.py +++ b/arho_feature_template/gui/dialogs/new_plan_regulation_group_form.py @@ -3,18 +3,26 @@ from importlib import resources from typing import TYPE_CHECKING +from qgis.core import QgsApplication from qgis.PyQt import uic from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QTextBrowser, QTreeWidgetItem, QVBoxLayout -from arho_feature_template.core.models import Regulation, RegulationConfig, RegulationGroup, RegulationLibrary +from arho_feature_template.core.models import ( + Proposition, + Regulation, + RegulationConfig, + RegulationGroup, + RegulationLibrary, +) +from arho_feature_template.gui.components.plan_proposition_widget import PropositionWidget from arho_feature_template.gui.components.plan_regulation_widget import RegulationWidget from arho_feature_template.gui.components.tree_with_search_widget import TreeWithSearchWidget from arho_feature_template.project.layers.code_layers import PlanRegulationGroupTypeLayer if TYPE_CHECKING: from qgis.gui import QgsSpinBox - from qgis.PyQt.QtWidgets import QBoxLayout, QLineEdit, QWidget + from qgis.PyQt.QtWidgets import QBoxLayout, QLineEdit, QPushButton, QWidget from arho_feature_template.gui.components.code_combobox import CodeComboBox @@ -41,6 +49,10 @@ def __init__(self): self.regulations_layout: QBoxLayout self.regulation_info: QTextBrowser + self.propositions_layout: QVBoxLayout + self.propositions_scroll_contents: QWidget + self.add_proposition_btn: QPushButton + self.button_box: QDialogButtonBox # INIT @@ -54,16 +66,19 @@ def __init__(self): self.type_of_regulation_group.populate_from_code_layer(PlanRegulationGroupTypeLayer) self.type_of_regulation_group.removeItem(0) # Remove NULL from combobox as underground data is required self.button_box.accepted.connect(self._on_ok_clicked) + self.add_proposition_btn.clicked.connect(self.add_proposition) + self.add_proposition_btn.setIcon(QgsApplication.getThemeIcon("mActionAdd.svg")) + self.regulation_widgets: list[RegulationWidget] = [] + self.proposition_widgets: list[PropositionWidget] = [] self.save_as_config = False def _initalize_regulation_from_config(self, config: RegulationConfig, parent: QTreeWidgetItem | None = None): item = self.regulations_selection_widget.add_item_to_tree(config.name, config, parent) # Initialize plan regulations recursively - if config.child_regulations: - for child_config in config.child_regulations: - self._initalize_regulation_from_config(child_config, item) + for child_config in config.child_regulations: + self._initalize_regulation_from_config(child_config, item) def update_selected_regulation(self, item: QTreeWidgetItem, column: int): config: RegulationConfig = item.data(column, Qt.UserRole) # Retrieve the associated config @@ -84,10 +99,24 @@ def add_regulation(self, config: RegulationConfig): self.regulation_widgets.append(widget) def delete_regulation(self, regulation_widget: RegulationWidget): + regulation_widget.delete_signal.disconnect() self.regulations_layout.removeWidget(regulation_widget) self.regulation_widgets.remove(regulation_widget) regulation_widget.deleteLater() + def add_proposition(self): + proposition = Proposition(name="", value="") + widget = PropositionWidget(proposition, parent=self.propositions_scroll_contents) + widget.delete_signal.connect(self.delete_proposition) + self.propositions_layout.insertWidget(1, widget) + self.proposition_widgets.append(widget) + + def delete_proposition(self, proposition_widget: PropositionWidget): + proposition_widget.delete_signal.disconnect() + self.propositions_layout.removeWidget(proposition_widget) + self.proposition_widgets.remove(proposition_widget) + proposition_widget.deleteLater() + def into_model(self) -> RegulationGroup: return RegulationGroup( type_code_id=self.type_of_regulation_group.value(), @@ -96,6 +125,7 @@ def into_model(self) -> RegulationGroup: color_code=self.color_code.text(), group_number=self.group_number.value() if self.group_number.value() > 0 else None, regulations=[widget.into_model() for widget in self.regulation_widgets], + propositions=[widget.into_model() for widget in self.proposition_widgets], id_=None, ) diff --git a/arho_feature_template/gui/dialogs/new_plan_regulation_group_form.ui b/arho_feature_template/gui/dialogs/new_plan_regulation_group_form.ui index 7d902e4..973d0e4 100644 --- a/arho_feature_template/gui/dialogs/new_plan_regulation_group_form.ui +++ b/arho_feature_template/gui/dialogs/new_plan_regulation_group_form.ui @@ -180,7 +180,7 @@ p, li { white-space: pre-wrap; } - + Suositukset @@ -190,7 +190,7 @@ p, li { white-space: pre-wrap; } true - + 0 @@ -199,6 +199,34 @@ p, li { white-space: pre-wrap; } 577 + + + + + + 0 + 0 + + + + Lisää suositus + + + + + + + Qt::Vertical + + + + 20 + 5 + + + + + diff --git a/arho_feature_template/project/layers/code_layers.py b/arho_feature_template/project/layers/code_layers.py index 0aeb926..1d57ece 100644 --- a/arho_feature_template/project/layers/code_layers.py +++ b/arho_feature_template/project/layers/code_layers.py @@ -26,6 +26,10 @@ class UndergroundTypeLayer(AbstractCodeLayer): name = "Maanalaisuuden tyyppi" +class PlanThemeLayer(AbstractCodeLayer): + name = "Kaavoitusteemat" + + class PlanRegulationGroupTypeLayer(AbstractCodeLayer): name = "Kaavamääräysryhmän tyyppi" diff --git a/arho_feature_template/project/layers/plan_layers.py b/arho_feature_template/project/layers/plan_layers.py index c2954c4..6dee314 100644 --- a/arho_feature_template/project/layers/plan_layers.py +++ b/arho_feature_template/project/layers/plan_layers.py @@ -10,7 +10,14 @@ from qgis.core import NULL, QgsExpressionContextUtils, QgsFeature, QgsProject, QgsVectorLayerUtils from qgis.utils import iface -from arho_feature_template.core.models import Plan, PlanFeature, Regulation, RegulationGroup, RegulationLibrary +from arho_feature_template.core.models import ( + Plan, + PlanFeature, + Proposition, + Regulation, + RegulationGroup, + RegulationLibrary, +) from arho_feature_template.exceptions import FeatureNotFoundError, LayerEditableError, LayerNotFoundError from arho_feature_template.project.layers import AbstractLayer from arho_feature_template.project.layers.code_layers import PlanRegulationTypeLayer @@ -202,6 +209,10 @@ def model_from_feature(cls, feature: QgsFeature) -> RegulationGroup: PlanRegulationLayer.model_from_feature(feat) for feat in PlanRegulationLayer.regulations_with_group_id(feature["id"]) ], + propositions=[ + PlanPropositionLayer.model_from_feature(feat) + for feat in PlanPropositionLayer.propositions_with_group_id(feature["id"]) + ], id_=feature["id"], ) @@ -350,6 +361,34 @@ class PlanPropositionLayer(AbstractPlanLayer): ) ) + @classmethod + def feature_from_model(cls, model: Proposition) -> QgsFeature: + feature = cls.initialize_feature_from_model(model) + + feature["name"] = {"fin": model.name} + feature["text_value"] = {"fin": model.value} + feature["plan_regulation_group_id"] = model.regulation_group_id_ + feature["ordering"] = model.proposition_number + feature["plan_theme_id"] = model.theme_id + feature["id"] = model.id_ if model.id_ else feature["id"] + + return feature + + @classmethod + def model_from_feature(cls, feature: QgsFeature) -> Proposition: + return Proposition( + name=feature["name"]["fin"], + value=feature["text_value"]["fin"], + regulation_group_id_=feature["plan_regulation_group_id"], + proposition_number=feature["ordering"], + theme_id=feature["plan_theme_id"], + id_=feature["id"], + ) + + @classmethod + def propositions_with_group_id(cls, group_id: str) -> list[QgsFeature]: + return [feat for feat in cls.get_features() if feat["plan_regulation_group_id"] == group_id] + class DocumentLayer(AbstractPlanLayer): name = "Asiakirjat" From 484c8f51546b142df92ba32e00d91e982de8b97c Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Tue, 21 Jan 2025 16:07:26 +0200 Subject: [PATCH 2/6] refactor and extend use of optimized feature requests - use LANGUAGE constant, defined in misc_utils for now - add AdditionalInformationTypeLayer and define get_additional_information_name for it - add get_attribute_value_by_another_attribute_value for AbstractLayer --- arho_feature_template/core/lambda_service.py | 5 ++- arho_feature_template/core/models.py | 13 +++--- arho_feature_template/core/plan_manager.py | 5 ++- .../gui/components/code_combobox.py | 7 +-- .../gui/components/plan_proposition_widget.py | 3 -- .../gui/components/plan_regulation_widget.py | 8 ++-- .../gui/tools/inspect_plan_features_tool.py | 3 -- .../project/layers/__init__.py | 7 +++ .../project/layers/code_layers.py | 33 +++++++------- .../project/layers/plan_layers.py | 44 +++++++++++-------- arho_feature_template/utils/misc_utils.py | 32 +------------- 11 files changed, 70 insertions(+), 90 deletions(-) diff --git a/arho_feature_template/core/lambda_service.py b/arho_feature_template/core/lambda_service.py index f2f211d..e0649e6 100644 --- a/arho_feature_template/core/lambda_service.py +++ b/arho_feature_template/core/lambda_service.py @@ -9,7 +9,8 @@ from qgis.PyQt.QtNetwork import QNetworkAccessManager, QNetworkProxy, QNetworkReply, QNetworkRequest from qgis.PyQt.QtWidgets import QMessageBox -from arho_feature_template.utils.misc_utils import get_active_plan_id, get_plan_name, get_settings +from arho_feature_template.project.layers.plan_layers import PlanLayer +from arho_feature_template.utils.misc_utils import get_active_plan_id, get_settings class LambdaService(QObject): @@ -113,7 +114,7 @@ def _process_json_reply(self, response_json: dict): if plan_json: geographical_area = plan_json.get("geographicalArea") if geographical_area: - outline_name = get_plan_name(plan_id, language="fin") + outline_name = PlanLayer.get_plan_name(plan_id) outline_json = { "type": "Feature", "properties": {"name": outline_name}, diff --git a/arho_feature_template/core/models.py b/arho_feature_template/core/models.py index 5a68b8d..024d2a5 100644 --- a/arho_feature_template/core/models.py +++ b/arho_feature_template/core/models.py @@ -11,11 +11,9 @@ from arho_feature_template.exceptions import ConfigSyntaxError from arho_feature_template.qgis_plugin_tools.tools.resources import resources_path -from arho_feature_template.utils.misc_utils import get_layer_by_name, iface +from arho_feature_template.utils.misc_utils import LANGUAGE, get_layer_by_name, iface if TYPE_CHECKING: - from typing import Literal - from qgis.core import QgsFeature, QgsGeometry @@ -149,7 +147,6 @@ def initialize( cls, config_path: Path = DEFAULT_PLAN_REGULATIONS_CONFIG_PATH, type_of_plan_regulations_layer_name="Kaavamääräyslaji", - language: Literal["fin", "eng", "swe"] = "fin", ) -> RegulationLibrary: # Initialize RegulationLibrary and RegulationConfigs from QGIS layer and config file @@ -165,7 +162,7 @@ def initialize( # 3. Initialize regulation configs from layer. Storing them by their ID is handy for adding childs later id_to_regulation_map: dict[str, RegulationConfig] = { - feature["id"]: RegulationConfig.from_feature(feature, language) for feature in layer.getFeatures() + feature["id"]: RegulationConfig.from_feature(feature) for feature in layer.getFeatures() } # 4. Add information from config file (value, unit, category only) and link child regulations @@ -226,7 +223,7 @@ class RegulationConfig: # NOTE: Perhaps this ("model_from_feature") should be method of PlanTypeLayer class? @classmethod - def from_feature(cls, feature: QgsFeature, language: str = "fin") -> RegulationConfig: + def from_feature(cls, feature: QgsFeature) -> RegulationConfig: """ Initialize PlanRegulationConfig from QgsFeature. @@ -235,8 +232,8 @@ def from_feature(cls, feature: QgsFeature, language: str = "fin") -> RegulationC return cls( id=feature["id"], regulation_code=feature["value"], - name=feature["name"][language], - description=feature["description"][language], + name=feature["name"][LANGUAGE], + description=feature["description"][LANGUAGE], status=feature["status"], level=feature["level"], parent_id=feature["parent_id"], diff --git a/arho_feature_template/core/plan_manager.py b/arho_feature_template/core/plan_manager.py index 8bb0fdd..43605ce 100644 --- a/arho_feature_template/core/plan_manager.py +++ b/arho_feature_template/core/plan_manager.py @@ -43,6 +43,7 @@ from arho_feature_template.resources.libraries.regulation_groups import regulation_group_library_config_files from arho_feature_template.utils.db_utils import get_existing_database_connection_names from arho_feature_template.utils.misc_utils import ( + LANGUAGE, check_layer_changes, get_active_plan_id, handle_unsaved_changes, @@ -378,9 +379,9 @@ def unload(self): def regulation_group_library_from_active_plan() -> RegulationGroupLibrary: category_features = list(PlanRegulationGroupTypeLayer.get_features()) - category_id_to_name: dict[str, str] = {category["id"]: category["name"]["fin"] for category in category_features} + category_id_to_name: dict[str, str] = {category["id"]: category["name"][LANGUAGE] for category in category_features} category_regulation_group_map: dict[str, list[QgsFeature]] = { - feature["name"]["fin"]: [] for feature in category_features + feature["name"][LANGUAGE]: [] for feature in category_features } for feat in RegulationGroupLayer.get_features(): diff --git a/arho_feature_template/gui/components/code_combobox.py b/arho_feature_template/gui/components/code_combobox.py index 05e6da4..183e61d 100644 --- a/arho_feature_template/gui/components/code_combobox.py +++ b/arho_feature_template/gui/components/code_combobox.py @@ -7,6 +7,7 @@ from qgis.PyQt.QtWidgets import QComboBox, QTreeWidget, QTreeWidgetItem from arho_feature_template.exceptions import LayerNotFoundError +from arho_feature_template.utils.misc_utils import LANGUAGE if TYPE_CHECKING: from arho_feature_template.project.layers.code_layers import ( @@ -33,7 +34,7 @@ def populate_from_code_layer(self, layer_type: type[AbstractCodeLayer]) -> None: return for i, feature in enumerate(layer.getFeatures(), start=1): - self.addItem(feature["name"]["fin"]) + self.addItem(feature["name"][LANGUAGE]) self.setItemData(i, feature["id"]) def value(self) -> str: @@ -83,9 +84,9 @@ def populate_from_code_layer(self, layer_type: type[AbstractCodeLayer]) -> None: item = QTreeWidgetItem() items[code_feature["id"]] = item - text = code_feature["name"]["fin"] + text = code_feature["name"][LANGUAGE] item.setText(0, text) - description = code_feature["description"]["fin"] + description = code_feature["description"][LANGUAGE] item.setToolTip(0, description) item.setData(0, Qt.UserRole, code_feature["id"]) diff --git a/arho_feature_template/gui/components/plan_proposition_widget.py b/arho_feature_template/gui/components/plan_proposition_widget.py index 8736891..c875d35 100644 --- a/arho_feature_template/gui/components/plan_proposition_widget.py +++ b/arho_feature_template/gui/components/plan_proposition_widget.py @@ -19,9 +19,6 @@ ui_path = resources.files(__package__) / "plan_proposition_widget.ui" FormClass, _ = uic.loadUiType(ui_path) -# TO BE REPLACED -LANGUAGE = "fin" - class PropositionWidget(QWidget, FormClass): # type: ignore """A widget representation of a plan proposition.""" diff --git a/arho_feature_template/gui/components/plan_regulation_widget.py b/arho_feature_template/gui/components/plan_regulation_widget.py index 7dd550d..53b565a 100644 --- a/arho_feature_template/gui/components/plan_regulation_widget.py +++ b/arho_feature_template/gui/components/plan_regulation_widget.py @@ -24,7 +24,8 @@ MultilineTextInputWidget, SinglelineTextInputWidget, ) -from arho_feature_template.utils.misc_utils import get_additional_information_name, get_layer_by_name, iface +from arho_feature_template.project.layers.code_layers import AdditionalInformationTypeLayer +from arho_feature_template.utils.misc_utils import LANGUAGE, get_layer_by_name, iface if TYPE_CHECKING: from qgis.PyQt.QtWidgets import QPushButton @@ -32,9 +33,6 @@ ui_path = resources.files(__package__) / "plan_regulation_widget.ui" FormClass, _ = uic.loadUiType(ui_path) -# TO BE REPLACED -LANGUAGE = "fin" - class RegulationWidget(QWidget, FormClass): # type: ignore """A widget representation of a plan regulation.""" @@ -218,7 +216,7 @@ def _add_additional_info(self, info_type: str, default_value: str | float | None # NOTE: Now info type is the name / readable version when this is triggered by user # Might need to refactor this later.. - name = get_additional_information_name(info_type) + name = AdditionalInformationTypeLayer.get_additional_information_name(info_type) self._add_widgets(QLabel("Lisätiedonlaji"), SinglelineTextInputWidget(name, False)) # NOTE: Does not support multiple instances of same additional information kind, diff --git a/arho_feature_template/gui/tools/inspect_plan_features_tool.py b/arho_feature_template/gui/tools/inspect_plan_features_tool.py index 5569709..c33da0c 100644 --- a/arho_feature_template/gui/tools/inspect_plan_features_tool.py +++ b/arho_feature_template/gui/tools/inspect_plan_features_tool.py @@ -89,9 +89,6 @@ def create_menu(self, click_pos: QPoint, nearby_features: dict[QgsVectorLayer, l for layer, features in nearby_features.items(): for feature in features: menu_text = layer.name() - # feat_name = feature["name"]["fin"] - # if feat_name: - # menu_text += f" — {feat_name}" action = menu.addAction(menu_text) action.triggered.connect(partial(self.edit_feature_requested.emit, feature, layer.name())) action.hovered.connect(partial(self.highlight_feature, feature, layer)) diff --git a/arho_feature_template/project/layers/__init__.py b/arho_feature_template/project/layers/__init__.py index af2efd4..7c9ba79 100644 --- a/arho_feature_template/project/layers/__init__.py +++ b/arho_feature_template/project/layers/__init__.py @@ -59,3 +59,10 @@ def get_attribute_values_by_another_attribute_value( request.setFlags(QgsFeatureRequest.NoGeometry) for feature in layer.getFeatures(request): yield feature[target_attribute] + + @classmethod + def get_attribute_value_by_another_attribute_value( + cls, target_attribute: str, filter_attribute: str, filter_value: str + ) -> Any | None: + gen = cls.get_attribute_values_by_another_attribute_value(target_attribute, filter_attribute, filter_value) + return next(gen, None) diff --git a/arho_feature_template/project/layers/code_layers.py b/arho_feature_template/project/layers/code_layers.py index 1d57ece..2478da3 100644 --- a/arho_feature_template/project/layers/code_layers.py +++ b/arho_feature_template/project/layers/code_layers.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import ClassVar - -from qgis.core import QgsFeatureRequest +from typing import ClassVar, cast +from arho_feature_template.exceptions import LayerNameNotFoundError from arho_feature_template.project.layers import AbstractLayer +from arho_feature_template.utils.misc_utils import LANGUAGE class AbstractCodeLayer(AbstractLayer): ... @@ -30,6 +30,15 @@ class PlanThemeLayer(AbstractCodeLayer): name = "Kaavoitusteemat" +class AdditionalInformationTypeLayer(AbstractCodeLayer): + name = "Lisätiedonlaji" + + @classmethod + def get_additional_information_name(cls, info_type: str) -> str | None: + attribute_value = cls.get_attribute_value_by_another_attribute_value("name", "value", info_type) + return cast(str, attribute_value[LANGUAGE]) if attribute_value else None + + class PlanRegulationGroupTypeLayer(AbstractCodeLayer): name = "Kaavamääräysryhmän tyyppi" @@ -45,13 +54,10 @@ class PlanRegulationGroupTypeLayer(AbstractCodeLayer): @classmethod def get_id_by_feature_layer_name(cls, layer_name: str) -> str | None: regulation_group_type = cls.LAYER_NAME_TO_REGULATION_GROUP_TYPE_MAP.get(layer_name) - - request = QgsFeatureRequest().setFilterExpression(f"\"value\"='{regulation_group_type}'") - feature = next(cls.get_from_project().getFeatures(request), None) - if feature: - return feature["id"] - - return None + if not regulation_group_type: + raise LayerNameNotFoundError(layer_name) + attribute_value = cls.get_attribute_value_by_another_attribute_value("id", "value", regulation_group_type) + return cast(str, attribute_value) if attribute_value else attribute_value class PlanRegulationTypeLayer(AbstractCodeLayer): @@ -59,11 +65,8 @@ class PlanRegulationTypeLayer(AbstractCodeLayer): @classmethod def get_regulation_type_by_id(cls, _id: str) -> str | None: - for feature in cls.get_from_project().getFeatures(): - if feature["id"] == _id: - return feature["value"] - - return None + attribute_value = cls.get_attribute_value_by_another_attribute_value("value", "id", _id) + return cast(str, attribute_value) if attribute_value else attribute_value code_layers = AbstractCodeLayer.__subclasses__() diff --git a/arho_feature_template/project/layers/plan_layers.py b/arho_feature_template/project/layers/plan_layers.py index 6dee314..b229a8d 100644 --- a/arho_feature_template/project/layers/plan_layers.py +++ b/arho_feature_template/project/layers/plan_layers.py @@ -21,6 +21,7 @@ from arho_feature_template.exceptions import FeatureNotFoundError, LayerEditableError, LayerNotFoundError from arho_feature_template.project.layers import AbstractLayer from arho_feature_template.project.layers.code_layers import PlanRegulationTypeLayer +from arho_feature_template.utils.misc_utils import LANGUAGE logger = logging.getLogger(__name__) @@ -75,8 +76,8 @@ def feature_from_model(cls, model: Plan) -> QgsFeature: message = "Plan must have a geometry to be added to the layer" raise ValueError(message) - feature["name"] = {"fin": model.name} - feature["description"] = {"fin": model.description} + feature["name"] = {LANGUAGE: model.name} + feature["description"] = {LANGUAGE: model.description} feature["permanent_plan_identifier"] = model.permanent_plan_identifier feature["record_number"] = model.record_number feature["producers_plan_identifier"] = model.producers_plan_identifier @@ -95,8 +96,8 @@ def model_from_feature(cls, feature: QgsFeature) -> Plan: ] return Plan( geom=feature.geometry(), - name=feature["name"]["fin"], - description=feature["description"]["fin"], + name=feature["name"][LANGUAGE], + description=feature["description"][LANGUAGE], permanent_plan_identifier=feature["permanent_plan_identifier"], record_number=feature["record_number"], producers_plan_identifier=feature["producers_plan_identifier"], @@ -112,6 +113,11 @@ def model_from_feature(cls, feature: QgsFeature) -> Plan: id_=feature["id"], ) + @classmethod + def get_plan_name(cls, plan_id: str) -> str: + attribute_value = cls.get_attribute_value_by_another_attribute_value("name", "id", plan_id) + return attribute_value[LANGUAGE] if attribute_value else "Nimetön" + class PlanFeatureLayer(AbstractPlanLayer): @classmethod @@ -122,9 +128,9 @@ def feature_from_model(cls, model: PlanFeature, plan_id: str | None = None) -> Q feature = cls.initialize_feature_from_model(model) feature.setGeometry(model.geom) - feature["name"] = {"fin": model.name if model.name else ""} + feature["name"] = {LANGUAGE: model.name if model.name else ""} feature["type_of_underground_id"] = model.type_of_underground_id - feature["description"] = {"fin": model.description if model.description else ""} + feature["description"] = {LANGUAGE: model.description if model.description else ""} feature["plan_id"] = ( plan_id if plan_id @@ -143,8 +149,8 @@ def model_from_feature(cls, feature: QgsFeature) -> PlanFeature: geom=feature.geometry(), type_of_underground_id=feature["type_of_underground_id"], layer_name=cls.get_from_project().name(), - name=feature["name"]["fin"], - description=feature["description"]["fin"], + name=feature["name"][LANGUAGE], + description=feature["description"][LANGUAGE], regulation_groups=[ RegulationGroupLayer.model_from_feature(feat) for feat in regulation_group_features if feat is not None ], @@ -187,7 +193,7 @@ def feature_from_model(cls, model: RegulationGroup, plan_id: str | None = None) feature = cls.initialize_feature_from_model(model) feature["short_name"] = model.short_name if model.short_name else None - feature["name"] = {"fin": model.name} + feature["name"] = {LANGUAGE: model.name} feature["type_of_plan_regulation_group_id"] = model.type_code_id feature["plan_id"] = ( plan_id @@ -201,7 +207,7 @@ def feature_from_model(cls, model: RegulationGroup, plan_id: str | None = None) def model_from_feature(cls, feature: QgsFeature) -> RegulationGroup: return RegulationGroup( type_code_id=feature["type_of_plan_regulation_group_id"], - name=feature["name"]["fin"], + name=feature["name"][LANGUAGE], short_name=feature["short_name"], color_code=None, group_number=None, @@ -309,9 +315,9 @@ def feature_from_model(cls, model: Regulation) -> QgsFeature: feature["plan_regulation_group_id"] = model.regulation_group_id_ feature["type_of_plan_regulation_id"] = model.config.id feature["unit"] = model.config.unit - feature["text_value"] = {"fin": model.value if isinstance(model.value, str) else ""} + feature["text_value"] = {LANGUAGE: model.value if isinstance(model.value, str) else ""} feature["numeric_value"] = model.value if isinstance(model.value, Number) else NULL - feature["name"] = {"fin": model.topic_tag if model.topic_tag else ""} + feature["name"] = {LANGUAGE: model.topic_tag if model.topic_tag else ""} feature["id"] = model.id_ if model.id_ else feature["id"] # feature["plan_theme_id"] # feature["type_of_verbal_plan_regulation_id"] @@ -331,7 +337,7 @@ def model_from_feature(cls, feature: QgsFeature) -> Regulation: return Regulation( config=config, # Assuming only either text_value or numeric_value is defined - value=feature["text_value"]["fin"] if feature["text_value"]["fin"] else feature["numeric_value"], + value=feature["text_value"][LANGUAGE] if feature["text_value"][LANGUAGE] else feature["numeric_value"], additional_information=None, regulation_number=None, files=[], @@ -365,8 +371,8 @@ class PlanPropositionLayer(AbstractPlanLayer): def feature_from_model(cls, model: Proposition) -> QgsFeature: feature = cls.initialize_feature_from_model(model) - feature["name"] = {"fin": model.name} - feature["text_value"] = {"fin": model.value} + feature["name"] = {LANGUAGE: model.name} + feature["text_value"] = {LANGUAGE: model.value} feature["plan_regulation_group_id"] = model.regulation_group_id_ feature["ordering"] = model.proposition_number feature["plan_theme_id"] = model.theme_id @@ -377,8 +383,8 @@ def feature_from_model(cls, model: Proposition) -> QgsFeature: @classmethod def model_from_feature(cls, feature: QgsFeature) -> Proposition: return Proposition( - name=feature["name"]["fin"], - value=feature["text_value"]["fin"], + name=feature["name"][LANGUAGE], + value=feature["text_value"][LANGUAGE], regulation_group_id_=feature["plan_regulation_group_id"], proposition_number=feature["ordering"], theme_id=feature["plan_theme_id"], @@ -386,8 +392,8 @@ def model_from_feature(cls, feature: QgsFeature) -> Proposition: ) @classmethod - def propositions_with_group_id(cls, group_id: str) -> list[QgsFeature]: - return [feat for feat in cls.get_features() if feat["plan_regulation_group_id"] == group_id] + def propositions_with_group_id(cls, group_id: str) -> Generator[QgsFeature]: + return cls.get_features_by_attribute_value("plan_regulation_group_id", group_id) class DocumentLayer(AbstractPlanLayer): diff --git a/arho_feature_template/utils/misc_utils.py b/arho_feature_template/utils/misc_utils.py index cb7a106..178c268 100644 --- a/arho_feature_template/utils/misc_utils.py +++ b/arho_feature_template/utils/misc_utils.py @@ -9,8 +9,6 @@ from qgis.utils import iface if TYPE_CHECKING: - from typing import Literal - from qgis.core import QgsMapLayer from qgis.gui import QgisInterface @@ -18,6 +16,8 @@ PLUGIN_PATH = os.path.dirname(os.path.dirname(__file__)) +LANGUAGE = "fin" + # NOTE: Consider creating "layer_utils.py" or similar for layer related utils in the future def get_layer_by_name(layer_name: str) -> QgsMapLayer | None: @@ -34,21 +34,6 @@ def get_layer_by_name(layer_name: str) -> QgsMapLayer | None: return None -def get_additional_information_name(info_type: str, language: Literal["fin", "eng", "swe"] = "fin") -> str: - """ - Retrieve name of input additional information type from associated QGIS layer. - - Returns input `info_type` if name is not found. - """ - type_of_additional_information_layer_name = "Lisätiedonlaji" - layer = get_layer_by_name(type_of_additional_information_layer_name) - if layer: - for feature in layer.getFeatures(): - if feature["value"] == info_type: - return feature["name"][language] - return info_type - - def check_layer_changes() -> bool: """Check if there are unsaved changes in any QGIS layers.""" project = QgsProject.instance() @@ -112,16 +97,3 @@ def get_settings(): proxy_port = settings.value("proxy_port", "5443") lambda_url = settings.value("lambda_url", "https://t5w26iqnsf.execute-api.eu-central-1.amazonaws.com/v0/ryhti") return proxy_host, proxy_port, lambda_url - - -def get_plan_name(plan_id: str, language: Literal["fin", "eng", "swe"] = "fin") -> str: - """Retrieve the name of a plan from the 'Kaava' layer based on its ID.""" - layer = get_layer_by_name("Kaava") - if layer: - for feature in layer.getFeatures(): - if feature["id"] == plan_id: - name_field = feature["name"] - name = name_field.get(language, "") - # Return "Nimetön" if the name is an empty string - return name if name.strip() else "Nimetön" - return "Nimetön" From 031dd656a441d38ffa41700979f49b92f32b0b1e Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Wed, 22 Jan 2025 11:27:38 +0200 Subject: [PATCH 3/6] fix (remove) NULL index setting in CodeComboboxes --- arho_feature_template/gui/components/code_combobox.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/arho_feature_template/gui/components/code_combobox.py b/arho_feature_template/gui/components/code_combobox.py index 183e61d..d22257c 100644 --- a/arho_feature_template/gui/components/code_combobox.py +++ b/arho_feature_template/gui/components/code_combobox.py @@ -48,8 +48,6 @@ def set_value(self, value: str | None) -> None: index = self.findData(value) if index != -1: self.setCurrentIndex(index) - else: - self.setCurrentIndex(0) # Set selection to NULL if item with `value` was not found class HierarchicalCodeComboBox(QComboBox): @@ -120,7 +118,6 @@ def _find_item_recursive(self, item: QTreeWidgetItem, value: str) -> QTreeWidget def set_value(self, value: str | None) -> None: # Set selection to NULL if `value` is None if value is None: - self.setCurrentIndex(self.null_index) return # Loop top level tree items @@ -137,5 +134,3 @@ def set_value(self, value: str | None) -> None: self.setCurrentIndex(idx.row()) self.setRootModelIndex(self.null_index.parent()) return - - self.setCurrentIndex(self.null_index) # Set selection to NULL if item with `value` was not found From e62c00fa0026af643580fe71948ef64498d5f595 Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Wed, 22 Jan 2025 11:29:58 +0200 Subject: [PATCH 4/6] fix invalid regulation code in katja_asemakaava.yaml --- .../resources/libraries/regulation_groups/katja_asemakaava.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arho_feature_template/resources/libraries/regulation_groups/katja_asemakaava.yaml b/arho_feature_template/resources/libraries/regulation_groups/katja_asemakaava.yaml index 73c833b..6566e37 100644 --- a/arho_feature_template/resources/libraries/regulation_groups/katja_asemakaava.yaml +++ b/arho_feature_template/resources/libraries/regulation_groups/katja_asemakaava.yaml @@ -1387,7 +1387,7 @@ categories: geometry: Alue short_name: sm plan_regulations: - - regulation_code: muinaismuistoalue + - regulation_code: muinaismuistoAlue additional_information: - type: osaAlue From 2a52bfc9e1473e9efef239a01492cf61d25eb36c Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Wed, 22 Jan 2025 11:39:02 +0200 Subject: [PATCH 5/6] fix geometry setting when creating a new plan --- arho_feature_template/project/layers/plan_layers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arho_feature_template/project/layers/plan_layers.py b/arho_feature_template/project/layers/plan_layers.py index b229a8d..50df721 100644 --- a/arho_feature_template/project/layers/plan_layers.py +++ b/arho_feature_template/project/layers/plan_layers.py @@ -70,12 +70,12 @@ class PlanLayer(AbstractPlanLayer): @classmethod def feature_from_model(cls, model: Plan) -> QgsFeature: - feature = cls.initialize_feature_from_model(model) - if not model.geom: message = "Plan must have a geometry to be added to the layer" raise ValueError(message) + feature = cls.initialize_feature_from_model(model) + feature.setGeometry(model.geom) feature["name"] = {LANGUAGE: model.name} feature["description"] = {LANGUAGE: model.description} feature["permanent_plan_identifier"] = model.permanent_plan_identifier From 6581c537eb2e8856adecfb61d822e2768f9c9b37 Mon Sep 17 00:00:00 2001 From: Niko Aarnio Date: Wed, 22 Jan 2025 11:42:24 +0200 Subject: [PATCH 6/6] fix initialization of proposition widget --- .../gui/components/plan_proposition_widget.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/arho_feature_template/gui/components/plan_proposition_widget.py b/arho_feature_template/gui/components/plan_proposition_widget.py index c875d35..dbff0ad 100644 --- a/arho_feature_template/gui/components/plan_proposition_widget.py +++ b/arho_feature_template/gui/components/plan_proposition_widget.py @@ -60,8 +60,10 @@ def __init__(self, proposition: Proposition, parent=None): self.name.setText(proposition.name) self.text_input.setText(proposition.value) - self._add_theme(proposition.theme_id) - self._add_proposition_number(proposition.proposition_number) + if proposition.theme_id: + self._add_theme(proposition.theme_id) + if proposition.proposition_number: + self._add_proposition_number(proposition.proposition_number) def _add_widgets(self, label: QLabel, widget: QWidget): self.form_layout.addRow(label, widget)