diff --git a/arho_feature_template/core/feature_template_library.py b/arho_feature_template/core/feature_template_library.py index 202ad5d..e6dc6e4 100644 --- a/arho_feature_template/core/feature_template_library.py +++ b/arho_feature_template/core/feature_template_library.py @@ -16,7 +16,7 @@ TemplateSyntaxError, parse_template_library_config, ) -from arho_feature_template.gui.feature_attribute_form import FeatureAttributeForm +from arho_feature_template.gui.template_attribute_form import TemplateAttributeForm from arho_feature_template.gui.template_dock import TemplateLibraryDock from arho_feature_template.resources.template_libraries import library_config_files @@ -181,17 +181,12 @@ def ask_for_feature_attributes(self, feature: QgsFeature) -> None: if not self.active_template: return - attribute_form = FeatureAttributeForm(self.active_template.config.feature) + attribute_form = TemplateAttributeForm(self.active_template.config) if attribute_form.exec_(): layer = get_layer_from_project(self.active_template.config.feature.layer) # Save the feature - for attributes in attribute_form.attribute_widgets.values(): - for attribute, widget in attributes.items(): - feature.setAttribute( - attribute, - widget.text(), - ) + attribute_form.set_feature_attributes(feature) layer.beginEditCommand("Create feature from template") layer.addFeature(feature) diff --git a/arho_feature_template/core/template_library_config.py b/arho_feature_template/core/template_library_config.py index 1512bd1..e8fb8d4 100644 --- a/arho_feature_template/core/template_library_config.py +++ b/arho_feature_template/core/template_library_config.py @@ -113,10 +113,18 @@ class Attribute: attribute: str default: str | None + description: str | None @classmethod def from_dict(cls, data: dict) -> Attribute: - return cls(attribute=data["attribute"], default=data.get("default")) + return cls(attribute=data["attribute"], default=data.get("default"), description=data.get("description")) + + def display(self) -> str: + if self.description is not None: + return self.description + if self.default is not None: + return self.default + return "" def parse_template_library_config(template_library_config: Path) -> TemplateLibraryConfig: diff --git a/arho_feature_template/gui/plan_regulation_group_widget.py b/arho_feature_template/gui/plan_regulation_group_widget.py new file mode 100644 index 0000000..5717327 --- /dev/null +++ b/arho_feature_template/gui/plan_regulation_group_widget.py @@ -0,0 +1,64 @@ +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 pyqtSignal +from qgis.PyQt.QtWidgets import QWidget + +from arho_feature_template.gui.plan_regulation_widget import PlanRegulationWidget + +if TYPE_CHECKING: + from qgis.PyQt.QtWidgets import QFrame, QLineEdit, QPushButton + + from arho_feature_template.core.template_library_config import Feature + +ui_path = resources.files(__package__) / "plan_regulation_group_widget.ui" +FormClass, _ = uic.loadUiType(ui_path) + + +class PlanRegulationGroupWidget(QWidget, FormClass): # type: ignore + """A widget representation of a plan regulation group.""" + + delete_signal = pyqtSignal(QWidget) + + def __init__(self, feature: Feature): + super().__init__() + self.setupUi(self) + + # TYPES + self.frame: QFrame + self.heading: QLineEdit + self.del_btn: QPushButton + + # INIT + self.feature = feature + self.layer = self.feature.layer # Should be plan_regulation_group layer + + self.init_buttons() + self.set_group_heading() + self.add_plan_regulation_widgets() + + def request_delete(self): + self.delete_signal.emit(self) + + def init_buttons(self): + self.del_btn.setIcon(QgsApplication.getThemeIcon("mActionDeleteSelected.svg")) + self.del_btn.clicked.connect(self.request_delete) + + def set_group_heading(self): + for attribute_config in self.feature.attributes: + if attribute_config.attribute == "name": + self.heading.setText(attribute_config.display()) + + def add_plan_regulation_widgets(self): + if self.feature.child_features is not None: + for child in self.feature.child_features: + if child.layer == "plan_requlation": + self.add_plan_regulation_widget(child) + + def add_plan_regulation_widget(self, plan_regulation_feature: Feature): + plan_regulation_widget = PlanRegulationWidget(plan_regulation_feature) + self.frame.layout().addWidget(plan_regulation_widget) diff --git a/arho_feature_template/gui/plan_regulation_group_widget.ui b/arho_feature_template/gui/plan_regulation_group_widget.ui new file mode 100644 index 0000000..9fcd557 --- /dev/null +++ b/arho_feature_template/gui/plan_regulation_group_widget.ui @@ -0,0 +1,81 @@ + + + plan_regulation_group_form + + + + 0 + 0 + 514 + 65 + + + + + 0 + 0 + + + + Form + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + + 30 + 16777215 + + + + + + + + + + + + + + + + + diff --git a/arho_feature_template/gui/plan_regulation_group_widget_old.ui b/arho_feature_template/gui/plan_regulation_group_widget_old.ui new file mode 100644 index 0000000..d7a965c --- /dev/null +++ b/arho_feature_template/gui/plan_regulation_group_widget_old.ui @@ -0,0 +1,196 @@ + + + plan_regulation_group_form + + + + 0 + 0 + 582 + 195 + + + + Form + + + + + + + + + 0 + 0 + + + + Otsikko: + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + + 30 + 16777215 + + + + + + + + + + + + 0 + 0 + + + + + 30 + 16777215 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + + false + + + false + + + + + + + + + 0 + 0 + + + + + 175 + 0 + + + + + 175 + 16777215 + + + + + 75 + true + + + + Kaavamääräyslaji + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + + + + QgsCollapsibleGroupBox + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+
+ + +
diff --git a/arho_feature_template/gui/plan_regulation_widget.py b/arho_feature_template/gui/plan_regulation_widget.py new file mode 100644 index 0000000..4f113d0 --- /dev/null +++ b/arho_feature_template/gui/plan_regulation_widget.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from importlib import resources +from typing import TYPE_CHECKING + +from qgis.PyQt import uic +from qgis.PyQt.QtGui import QIcon +from qgis.PyQt.QtWidgets import ( + QComboBox, + QHBoxLayout, + QLabel, + QLineEdit, + QPlainTextEdit, + QPushButton, + QSizePolicy, + QToolButton, + QWidget, +) + +from arho_feature_template.qgis_plugin_tools.tools.resources import plugin_path + +if TYPE_CHECKING: + from qgis.PyQt.QtWidgets import QFormLayout + + from arho_feature_template.core.template_library_config import Feature + +ui_path = resources.files(__package__) / "plan_regulation_widget.ui" +FormClass, _ = uic.loadUiType(ui_path) + + +class PlanRegulationWidget(QWidget, FormClass): # type: ignore + """A widget representation of a plan regulation group.""" + + def __init__(self, feature: Feature): + super().__init__() + self.setupUi(self) + + # TYPES + self.regulation_kind: QLineEdit + self.form_layout: QFormLayout = self.layout() + + # INITI + self.feature = feature + self.initialize_fields() + + def initialize_fields(self): + for plan_regulation_config in self.feature.attributes: + # Set regulation type / kind + if plan_regulation_config.attribute == "type_of_plan_regulation_id": + self.regulation_kind.setText(plan_regulation_config.display()) + + elif plan_regulation_config.attribute == "numeric_default": + self.add_quantity_input() + + # elif plan_regulation_config.attribute == "text???": + # self.add_text_input() + + if self.feature.child_features is None: + return + + for child in self.feature.child_features: + # Additional information here, what else? + # Assume attribute is "additional_information_of_plan_regulation" + # NOTE: Could additional information be attribute of plan regulation instead of child feature? + + # TBD: Multiple additional feature per plan regulation + for attribute in child.attributes: + # Assume "type_of_additional_information_id" + self.add_additional_information_field(attribute.display()) + + def add_additional_information_field(self, default_value: str | None = None): + label = QLabel("Lisätieto", self) + horizontal_layout = QHBoxLayout(self) + line_edit = QLineEdit(self) + if default_value: + line_edit.setText(default_value) + conf_btn = QPushButton(self) + conf_btn.setIcon(QIcon(plugin_path("resources", "icons", "settings.svg"))) + horizontal_layout.addWidget(line_edit) + horizontal_layout.addWidget(conf_btn) + self.form_layout.addRow(label, horizontal_layout) + + def add_quantity_input(self, quantity_types: list[str] | None = None): + label = QLabel("Arvo", self) + line_edit = QLineEdit(self) + if quantity_types: + quantity_types_selection = QComboBox(self) + quantity_types_selection.addItems(quantity_types) + horizontal_layout = QHBoxLayout(self) + horizontal_layout.addItem(line_edit) + horizontal_layout.addItem(quantity_types_selection) + self.form_layout.addRow(label, horizontal_layout) + else: + self.form_layout.addRow(label, line_edit) + # TODO: Input validation + + def add_text_input(self): + label = QLabel("Arvo", self) + horizontal_layout = QHBoxLayout(self) + text_edit = QPlainTextEdit(self) + text_edit.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) + open_btn = QToolButton(self) + horizontal_layout.addWidget(text_edit) + horizontal_layout.addWidget(open_btn) + self.form_layout.addRow(label, horizontal_layout) diff --git a/arho_feature_template/gui/plan_regulation_widget.ui b/arho_feature_template/gui/plan_regulation_widget.ui new file mode 100644 index 0000000..f9ec55f --- /dev/null +++ b/arho_feature_template/gui/plan_regulation_widget.ui @@ -0,0 +1,41 @@ + + + Form + + + + 0 + 0 + 349 + 43 + + + + + 0 + 0 + + + + Form + + + + + + Kaavamääräyslaji + + + + + + + + + + + + + + + diff --git a/arho_feature_template/gui/template_attribute_form.py b/arho_feature_template/gui/template_attribute_form.py new file mode 100644 index 0000000..915554c --- /dev/null +++ b/arho_feature_template/gui/template_attribute_form.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from importlib import resources +from typing import TYPE_CHECKING + +from qgis.core import QgsApplication, QgsFeature +from qgis.gui import QgsSpinBox +from qgis.PyQt import uic +from qgis.PyQt.QtWidgets import ( + QDialog, + QDialogButtonBox, + QLineEdit, + QPushButton, + QScrollArea, + QSizePolicy, + QSpacerItem, + QTreeWidget, +) + +from arho_feature_template.gui.plan_regulation_group_widget import PlanRegulationGroupWidget + +if TYPE_CHECKING: + from qgis.PyQt.QtWidgets import QWidget + + from arho_feature_template.core.template_library_config import Feature, FeatureTemplate + +ui_path = resources.files(__package__) / "template_attribute_form.ui" +FormClass, _ = uic.loadUiType(ui_path) + + +class TemplateAttributeForm(QDialog, FormClass): # type: ignore + """Parent class for feature template forms for adding and modifying feature attribute data.""" + + def __init__(self, feature_template_config: FeatureTemplate): + super().__init__() + self.setupUi(self) + + # TYPES + self.feature_name: QLineEdit + self.feature_description: QLineEdit + self.feature_underground: QLineEdit + self.feature_vertical_boundaries: QLineEdit + self.plan_regulation_group_scrollarea: QScrollArea + self.plan_regulation_group_scrollarea_contents: QWidget + self.plan_regulation_groups_tree: QTreeWidget + self.add_plan_regulation_group_btn: QPushButton + self.button_box: QDialogButtonBox + + # SIGNALS + self.button_box.accepted.connect(self._on_ok_clicked) + self.add_plan_regulation_group_btn.clicked.connect(self._on_add_plan_regulation_group_clicked) + + # INIT + self.attribute_widgets = { + "name": self.feature_name, + "description": self.feature_description, + "type_of_underground_id": self.feature_underground, + # self.feature_vertical_boundaries + } + self.scroll_area_spacer = None + self.available_plan_regulation_group_configs: list[Feature] = [] + self.add_plan_regulation_group_btn.setIcon(QgsApplication.getThemeIcon("mActionAdd.svg")) + + self.setWindowTitle(feature_template_config.name) + self.init_feature_attributes_from_template(feature_template_config) + self.init_plan_regulation_groups_from_template(feature_template_config) + self.init_plan_regulation_group_library() + + def add_spacer(self): + self.scroll_area_spacer = QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding) + self.plan_regulation_group_scrollarea_contents.layout().addItem(self.scroll_area_spacer) + + def remove_spacer(self): + if self.scroll_area_spacer is not None: + self.plan_regulation_group_scrollarea_contents.layout().removeItem(self.scroll_area_spacer) + self.scroll_area_spacer = None + + def add_plan_regulation_group(self, feature_config: Feature): + new_plan_regulation_group = PlanRegulationGroupWidget(feature_config) + new_plan_regulation_group.delete_signal.connect(self.remove_plan_regulation_group) + self.remove_spacer() + self.plan_regulation_group_scrollarea_contents.layout().addWidget(new_plan_regulation_group) + self.add_spacer() + + def remove_plan_regulation_group(self, plan_regulation_group_widget: PlanRegulationGroupWidget): + self.plan_regulation_group_scrollarea_contents.layout().removeWidget(plan_regulation_group_widget) + plan_regulation_group_widget.deleteLater() + + def _on_add_plan_regulation_group_clicked(self): + selected = self.plan_regulation_groups_tree.selectedItems() + if len(selected) == 0: + # Nothing selected + return + if len(selected) > 1: + # Too many selected, but should allow selecting only 1 item at a time + return + selected_plan_regulation_group = selected[0] + print(f"Trying to add plan regulation group {selected_plan_regulation_group.text(0)}") # noqa: T201 + # self.add_plan_regulation_group() # TODO: Implement + + def init_plan_regulation_group_library(self): + # Now plan regulation group tree widget/view is just static placeholder for demo + pass + + def init_feature_attributes_from_template(self, feature_template_config: FeatureTemplate): + if feature_template_config.feature.attributes is None: + return + for _attribute in feature_template_config.feature.attributes: + # TODO: TO be implemented + pass + + def init_plan_regulation_groups_from_template(self, feature_template_config: FeatureTemplate): + if feature_template_config.feature.child_features is None: + return + for child_feature in feature_template_config.feature.child_features: + if child_feature.layer == "plan_requlation_group": + # Collect encountered plan regulation groups in init + # This does not need to be done if Katja config file is read beforehand and + # that handles available plan regulation groups + self.available_plan_regulation_group_configs.append(child_feature) + self.add_plan_regulation_group(child_feature) + else: + # TODO: Implement + print(f"Encountered child feature with unrecognized layer: {child_feature.layer}") # noqa: T201 + + def _on_ok_clicked(self): + self.accept() + + def set_feature_attributes(self, feature: QgsFeature): + for attribute, widget in self.attribute_widgets.items(): + feature.setAttribute(attribute, self._get_widget_value(widget)) + + def _get_widget_value(self, widget: QWidget) -> str | int | None: + if isinstance(widget, QLineEdit): + return widget.text() + if isinstance(widget, QgsSpinBox): + return widget.value() + return None + # TODO: Implement diff --git a/arho_feature_template/gui/template_attribute_form.ui b/arho_feature_template/gui/template_attribute_form.ui new file mode 100644 index 0000000..5ee8156 --- /dev/null +++ b/arho_feature_template/gui/template_attribute_form.ui @@ -0,0 +1,340 @@ + + + template_form + + + + 0 + 0 + 757 + 749 + + + + Template form + + + + + + + + Kohteen nimi + + + + + + + + + + Kohteen kuvaus + + + + + + + + + + Maanalaisuus + + + + + + + Pystysuuntainen rajaus + + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 10 + + + + + + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 10 + + + + + + + + + + + + Lisää kaavamääräysryhmä + + + + + + + + 0 + 0 + + + + Suodata kaavamääräysryhmiä + + + true + + + true + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoDragDrop + + + false + + + false + + + + 1 + + + + + Aluevaraukset + + + + Asuinrakennusten alue + + + + + Asuinkerrostalojen alue + + + + + Asuinpientalojen alue + + + + + Rivitalojen ja muiden kytkettyjen asuinrakennusten alue + + + + + Erillispientalojen alue + + + + + + Rakennusalat + + + + Kunnan tai kaupunginosas raja + + + + + Korttelialue tai korttelialueen osa + + + + + Sitovan tonttijaon mukainen tontti + + + + + Ohjeellinen tontti / rakennusala + + + + + Rakennusala + + + + + + Numeeriset ja tekstimuotoiset määräykset + + + + Kaupungin- tai kunnanosan numero + + + + + Kaupungin- tai kunnanosan nimi + + + + + Korttelin numero + + + + + Tontin tai rakennuspaikan numero + + + + + Ohjeellisen tontin tai rakennuspaikan numero + + + + + Kadun tai tien nimi + + + + + + + + + Lisää kaavamääräysryhmä + + + + + + + + + + + Kaavamääräykset + + + + + + + true + + + + + 0 + 0 + 469 + 501 + + + + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + QgsFilterLineEdit + QLineEdit +
qgsfilterlineedit.h
+
+
+ + + + button_box + rejected() + template_form + reject() + + + 414 + 729 + + + 319 + 375 + + + + +
diff --git a/arho_feature_template/gui/template_attribute_form_old.ui b/arho_feature_template/gui/template_attribute_form_old.ui new file mode 100644 index 0000000..be51e61 --- /dev/null +++ b/arho_feature_template/gui/template_attribute_form_old.ui @@ -0,0 +1,164 @@ + + + template_form + + + + 0 + 0 + 640 + 752 + + + + Template form + + + + + + + + Kohteen nimi + + + + + + + + + + Kohteen kuvaus + + + + + + + + + + Maanalaisuus + + + + + + + + + + Pystysuuntainen rajaus + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 10 + + + + + + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 10 + + + + + + + + Kaavamääräykset + + + + + + + true + + + + + 0 + 0 + 620 + 509 + + + + + + + + + + + + Lisää kaavamääräysryhmä + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + button_box + rejected() + template_form + reject() + + + 414 + 729 + + + 319 + 375 + + + + + diff --git a/arho_feature_template/resources/icons/settings.svg b/arho_feature_template/resources/icons/settings.svg new file mode 100644 index 0000000..956de27 --- /dev/null +++ b/arho_feature_template/resources/icons/settings.svg @@ -0,0 +1 @@ + diff --git a/arho_feature_template/resources/katja.yaml b/arho_feature_template/resources/katja.yaml new file mode 100644 index 0000000..da0ceb1 --- /dev/null +++ b/arho_feature_template/resources/katja.yaml @@ -0,0 +1,74 @@ +aluevaraukset: + - geometria: Alue + värikoodi: #000000 + kirjaintunnus: A + kaavamääräyksen_otsikko: Asuinrakennusten alue + kaavamääräykset: + - nimi: Asumisen alue + lisätiedot: + - laji: Pääkäyttötarkoitus + + - geometria: Alue + värikoodi: #000000 + kirjaintunnus: AK + kaavamääräyksen_otsikko: Asuinkerrostalojen alue + kaavamääräykset: + - nimi: Asuinkerrostaloalue + lisätiedot: + - laji: Pääkäyttötarkoitus + + - geometria: Alue + värikoodi: "#A9D08E" + kirjaintunnus: AP + kaavamääräyksen_otsikko: Asuinpientalojen alue + kaavamääräykset: + - nimi: Asuinpientaloalue + lisätiedot: + - laji: Pääkäyttötarkoitus + + - geometria: Alue + värikoodi: "#FFD966" + kirjaintunnus: AR + kaavamääräyksen_otsikko: Rivitalojen ja muiden kytkettyjen asuinrakennusten alue + kaavamääräykset: + - nimi: Rivitalojen ja muiden kytkettyjen asuinpientalojen alue + lisätiedot: + - laji: Pääkäyttötarkoitus + + - geometria: Alue + värikoodi: "#E06666" + kirjaintunnus: AO + kaavamääräyksen_otsikko: Erillispientalojen alue + kaavamääräykset: + - nimi: Erillisten asuinpientalojen alue + lisätiedot: + - laji: Pääkäyttötarkoitus + + - geometria: Alue + värikoodi: "#5B9BD5" + kirjaintunnus: AL + kaavamääräyksen_otsikko: Asuin-, liike- ja toimistorakennusten alue + kaavamääräykset: + - nimi: Asumisen alue + - nimi: Liikerakennusten alue + - nimi: Toimistorakennusten alue + lisätiedot: + - laji: Pääkäyttötarkoitus + + - geometria: Alue + värikoodi: "#9BC2E6" + kirjaintunnus: AH + kaavamääräyksen_otsikko: Asumista palveleva yhteiskäyttöinen alue + kaavamääräykset: + - nimi: Asumista palveleva yhteiskäyttöinen alue + lisätiedot: + - laji: Pääkäyttötarkoitus + + - geometria: Alue + värikoodi: "#FFD966" + kirjaintunnus: AM + kaavamääräyksen_otsikko: Maatilojen talouskeskusten alue + kaavamääräykset: + - nimi: Maatilan talouskeskuksen alue + lisätiedot: + - laji: Pääkäyttötarkoitus diff --git a/arho_feature_template/resources/template_libraries/asemakaava-sample.yaml b/arho_feature_template/resources/template_libraries/asemakaava-sample.yaml index b7901f8..b6cf67f 100644 --- a/arho_feature_template/resources/template_libraries/asemakaava-sample.yaml +++ b/arho_feature_template/resources/template_libraries/asemakaava-sample.yaml @@ -32,34 +32,40 @@ templates: attributes: - attribute: type_of_plan_regulation_id default: asumisenAlue + description: Asumisen alue hidden: true child_features: - layer: additional_information_of_plan_regulation attributes: - attribute: type_of_additional_information_id default: paakayttotarkoitus + description: Pääkäyttötarkoitus hidden: true - layer: plan_requlation attributes: - attribute: type_of_plan_regulation_id default: liikerakennustenAlue + description: Liikerakennusten alue hidden: true child_features: - layer: additional_information_of_plan_regulation attributes: - attribute: type_of_additional_information_id default: paakayttotarkoitus + description: Pääkäyttötarkoitus hidden: true - layer: plan_requlation attributes: - attribute: type_of_plan_regulation_id default: toimitilojenAlue + description: Toimitilojen alue hidden: true child_features: - layer: additional_information_of_plan_regulation attributes: - attribute: type_of_additional_information_id default: paakayttotarkoitus + description: Pääkäyttötarkoitus hidden: true - layer: plan_requlation_group attributes: @@ -71,4 +77,5 @@ templates: attributes: - attribute: type_of_plan_regulation_id default: korttelinNumero + description: Korttelin numero - attribute: numeric_default