Skip to content

Commit 8baeff9

Browse files
committed
Implement a custom form for a new plan
1 parent 271f046 commit 8baeff9

File tree

9 files changed

+677
-41
lines changed

9 files changed

+677
-41
lines changed

arho_feature_template/core/models.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from qgis.core import QgsGeometry
8+
9+
10+
@dataclass
11+
class Regulation:
12+
type_code: str
13+
value_string: str | None
14+
value_int: int | None
15+
value_float: float | None
16+
unit: str | None
17+
id_: int | None = None
18+
19+
20+
@dataclass
21+
class RegulationGroup:
22+
name: str
23+
short_name: str | None
24+
type_code: str
25+
regulations: list[Regulation]
26+
id_: int | None = None
27+
28+
29+
@dataclass
30+
class Plan:
31+
name: str
32+
description: str | None
33+
plan_type_id: str
34+
lifecycle_status_id: str
35+
record_number: str | None
36+
matter_management_identifier: str | None
37+
permanent_plan_identifier: str | None
38+
producers_plan_identifier: str | None
39+
organisation_id: str | None = None
40+
general_regulations: list[RegulationGroup] = field(default_factory=list)
41+
geom: QgsGeometry | None = None
42+
id_: int | None = None

arho_feature_template/core/plan_manager.py

+89-39
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,87 @@
22

33
import json
44
import logging
5-
6-
from qgis.core import QgsExpressionContextUtils, QgsProject, QgsVectorLayer
5+
from typing import TYPE_CHECKING
6+
7+
from qgis.core import (
8+
QgsExpressionContextUtils,
9+
QgsProject,
10+
QgsVectorLayer,
11+
)
12+
from qgis.gui import QgsMapToolDigitizeFeature
713
from qgis.PyQt.QtWidgets import QDialog, QMessageBox
814
from qgis.utils import iface
915

1016
from arho_feature_template.core.lambda_service import LambdaService
1117
from arho_feature_template.gui.load_plan_dialog import LoadPlanDialog
18+
from arho_feature_template.gui.plan_attribure_form import PlanAttributeForm
1219
from arho_feature_template.gui.serialize_plan import SerializePlan
13-
from arho_feature_template.project.layers.plan_layers import PlanLayer, plan_layers
20+
from arho_feature_template.project.layers.plan_layers import PlanLayer, RegulationGroupLayer, plan_layers
1421
from arho_feature_template.utils.db_utils import get_existing_database_connection_names
15-
from arho_feature_template.utils.misc_utils import get_active_plan_id, handle_unsaved_changes
22+
from arho_feature_template.utils.misc_utils import (
23+
get_active_plan_id,
24+
handle_unsaved_changes,
25+
)
26+
27+
if TYPE_CHECKING:
28+
from qgis.core import QgsFeature
1629

30+
from arho_feature_template.core.models import Plan, RegulationGroup
1731
logger = logging.getLogger(__name__)
1832

1933

34+
class PlanDigitizeMapTool(QgsMapToolDigitizeFeature): ...
35+
36+
2037
class PlanManager:
2138
def __init__(self):
2239
self.json_plan_path = None
2340
self.json_plan_outline_path = None
2441

42+
self.digitize_map_tool = PlanDigitizeMapTool(iface.mapCanvas(), iface.cadDockWidget())
43+
self.digitize_map_tool.digitizingCompleted.connect(self._plan_geom_digitized)
44+
2545
def add_new_plan(self):
2646
"""Initiate the process to add a new plan to the Kaava layer."""
47+
48+
self.previous_map_tool = iface.mapCanvas().mapTool()
49+
self.previous_active_plan_id = get_active_plan_id()
50+
2751
if not handle_unsaved_changes():
2852
return
2953

3054
plan_layer = PlanLayer.get_from_project()
31-
3255
if not plan_layer:
3356
return
57+
3458
self.set_active_plan(None)
3559

60+
self.previously_editable = plan_layer.isEditable()
3661
if not plan_layer.isEditable():
3762
plan_layer.startEditing()
3863

39-
iface.setActiveLayer(plan_layer)
40-
iface.actionAddFeature().trigger()
41-
42-
# Connect the featureAdded signal to a callback method
43-
plan_layer.featureAdded.connect(self._feature_added)
64+
self.digitize_map_tool.setLayer(plan_layer)
65+
iface.mapCanvas().setMapTool(self.digitize_map_tool)
4466

45-
def _feature_added(self):
67+
def _plan_geom_digitized(self, feature: QgsFeature):
4668
"""Callback for when a new feature is added to the Kaava layer."""
4769
plan_layer = PlanLayer.get_from_project()
4870
if not plan_layer:
4971
return
5072

51-
# Disconnect the signal to avoid repeated triggers
52-
plan_layer.featureAdded.disconnect(self._feature_added)
53-
54-
feature_ids_before_commit = plan_layer.allFeatureIds()
55-
56-
if plan_layer.isEditable():
57-
if not plan_layer.commitChanges():
58-
iface.messageBar().pushMessage("Error", "Failed to commit changes to the layer.", level=3)
59-
return
60-
else:
61-
iface.messageBar().pushMessage("Error", "Layer is not editable.", level=3)
62-
return
73+
attribute_form = PlanAttributeForm()
74+
if attribute_form.exec_():
75+
plan_attributes = attribute_form.get_plan_attributes()
76+
plan_attributes.geom = feature.geometry()
77+
feature = save_plan(plan_attributes)
78+
plan_layer = PlanLayer.get_from_project()
79+
if plan_layer.isEditable():
80+
plan_layer.commitChanges()
81+
self.set_active_plan(feature["id"])
82+
if self.previously_editable:
83+
plan_layer.startEditing()
6384

64-
feature_ids_after_commit = plan_layer.allFeatureIds()
65-
new_feature_id = next(
66-
(fid for fid in feature_ids_after_commit if fid not in feature_ids_before_commit),
67-
None,
68-
)
69-
70-
if new_feature_id is not None:
71-
new_feature = plan_layer.getFeature(new_feature_id)
72-
if new_feature.isValid():
73-
feature_id_value = new_feature["id"]
74-
self.set_active_plan(feature_id_value)
75-
else:
76-
iface.messageBar().pushMessage("Error", "Invalid feature retrieved.", level=3)
77-
else:
78-
iface.messageBar().pushMessage("Error", "No new feature was added.", level=3)
85+
iface.mapCanvas().setMapTool(self.previous_map_tool)
7986

8087
def set_active_plan(self, plan_id: str | None):
8188
"""Update the project layers based on the selected land use plan."""
@@ -147,4 +154,47 @@ def save_plan_jsons(self, plan_json, outline_json):
147154
with open(self.json_plan_outline_path, "w", encoding="utf-8") as outline_file:
148155
json.dump(outline_json, outline_file, ensure_ascii=False, indent=2)
149156

150-
QMessageBox.information(None, "Tallennus onnistui", "Kaava ja sen ulkoraja tallennettu onnistuneesti.")
157+
QMessageBox.information(
158+
None,
159+
"Tallennus onnistui",
160+
"Kaava ja sen ulkoraja tallennettu onnistuneesti.",
161+
)
162+
163+
164+
def save_plan(plan_data: Plan) -> QgsFeature:
165+
plan_layer = PlanLayer.get_from_project()
166+
in_edit_mode = plan_layer.isEditable()
167+
if not in_edit_mode:
168+
plan_layer.startEditing()
169+
170+
edit_message = "Kaavan lisäys" if plan_data.id_ is None else "Kaavan muokkaus"
171+
plan_layer.beginEditCommand(edit_message)
172+
173+
plan_data.organisation_id = "99e20d66-9730-4110-815f-5947d3f8abd3"
174+
plan_feature = PlanLayer.feature_from_model(plan_data)
175+
176+
if plan_data.id_ is None:
177+
plan_layer.addFeature(plan_feature)
178+
else:
179+
plan_layer.updateFeature(plan_feature)
180+
181+
plan_layer.endEditCommand()
182+
plan_layer.commitChanges(stopEditing=False)
183+
184+
if plan_data.general_regulations:
185+
for regulation_group in plan_data.general_regulations:
186+
plan_id = plan_feature["id"]
187+
regulation_group_feature = save_regulation_group(regulation_group, plan_id)
188+
save_regulation_grop_assosiation(plan_id, regulation_group_feature["id"])
189+
190+
return plan_feature
191+
192+
193+
def save_regulation_group(regulation_group: RegulationGroup, plan_id: str) -> QgsFeature:
194+
feature = RegulationGroupLayer.feature_from_model(regulation_group)
195+
feature["plan_id"] = plan_id
196+
return feature
197+
198+
199+
def save_regulation_grop_assosiation(plan_id: str, regulation_group_id: str):
200+
pass
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import TYPE_CHECKING
5+
6+
from qgis.PyQt.QtCore import Qt
7+
from qgis.PyQt.QtWidgets import QComboBox, QTreeWidget, QTreeWidgetItem
8+
9+
from arho_feature_template.exceptions import LayerNotFoundError
10+
11+
if TYPE_CHECKING:
12+
from arho_feature_template.project.layers.code_layers import (
13+
AbstractCodeLayer,
14+
)
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class CodeComboBox(QComboBox):
20+
def __init__(self, parent=None):
21+
super().__init__(parent)
22+
23+
def populate_from_code_layer(self, layer_type: type[AbstractCodeLayer]) -> None:
24+
try:
25+
layer = layer_type.get_from_project()
26+
except LayerNotFoundError:
27+
logger.warning("Layer % not found.", layer_type.name)
28+
return
29+
30+
self.addItem("NULL")
31+
self.setItemData(0, None)
32+
33+
for i, feature in enumerate(layer.getFeatures(), start=1):
34+
self.addItem(feature["name"]["fin"])
35+
self.setItemData(i, feature["id"])
36+
37+
def value(self) -> str:
38+
return self.currentData()
39+
40+
41+
class HierarchicalCodeComboBox(QComboBox):
42+
def __init__(self, parent=None):
43+
super().__init__(parent)
44+
45+
self.tree_widget = QTreeWidget()
46+
self.tree_widget.setColumnCount(1)
47+
self.tree_widget.setHeaderHidden(True)
48+
49+
self.setModel(self.tree_widget.model())
50+
self.setView(self.tree_widget)
51+
52+
self.tree_widget.viewport().installEventFilter(self)
53+
54+
def populate_from_code_layer(self, layer_type: type[AbstractCodeLayer]) -> None:
55+
try:
56+
layer = layer_type.get_from_project()
57+
except LayerNotFoundError:
58+
logger.warning("Layer % not found.", layer_type.name)
59+
return
60+
61+
null_item = QTreeWidgetItem(["NULL"])
62+
self.tree_widget.addTopLevelItem(null_item)
63+
64+
codes = {feature["id"]: feature for feature in layer.getFeatures()}
65+
items: dict[str, QTreeWidgetItem] = {}
66+
for code_feature in sorted(codes.values(), key=lambda feature: feature["level"]):
67+
item = QTreeWidgetItem()
68+
items[code_feature["id"]] = item
69+
70+
text = code_feature["name"]["fin"]
71+
item.setText(0, text)
72+
description = code_feature["description"]["fin"]
73+
item.setToolTip(0, description)
74+
item.setData(0, Qt.UserRole, code_feature["id"])
75+
76+
if code_feature["level"] == 1:
77+
self.tree_widget.addTopLevelItem(item)
78+
item.setFlags(item.flags() & ~Qt.ItemIsSelectable)
79+
else:
80+
parent = items[code_feature["parent_id"]]
81+
parent.addChild(item)
82+
83+
self.tree_widget.expandAll()
84+
85+
def value(self) -> str:
86+
item = self.tree_widget.selectedItems()[0]
87+
return item.data(0, Qt.UserRole)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
from importlib import resources
4+
from typing import TYPE_CHECKING
5+
6+
from qgis.PyQt import uic
7+
from qgis.PyQt.QtWidgets import QDialog
8+
9+
from arho_feature_template.core.models import Plan
10+
from arho_feature_template.project.layers.code_layers import (
11+
LifeCycleStatusLayer,
12+
PlanTypeLayer,
13+
)
14+
15+
if TYPE_CHECKING:
16+
from qgis.PyQt.QtWidgets import QLineEdit, QTextEdit
17+
18+
from arho_feature_template.gui.code_combobox import CodeComboBox, HierarchicalCodeComboBox
19+
20+
ui_path = resources.files(__package__) / "plan_attribute_form.ui"
21+
FormClass, _ = uic.loadUiType(ui_path)
22+
23+
24+
class PlanAttributeForm(QDialog, FormClass): # type: ignore
25+
permanent_identifier_line_edit: QLineEdit
26+
name_line_edit: QLineEdit
27+
description_text_edit: QTextEdit
28+
plan_type_combo_box: HierarchicalCodeComboBox
29+
lifecycle_status_combo_box: CodeComboBox
30+
record_number_line_edit: QLineEdit
31+
producers_plan_identifier_line_edit: QLineEdit
32+
matter_management_identifier_line_edit: QLineEdit
33+
34+
def __init__(self, parent=None):
35+
super().__init__(parent)
36+
37+
self.setupUi(self)
38+
39+
self.plan_type_combo_box.populate_from_code_layer(PlanTypeLayer)
40+
self.lifecycle_status_combo_box.populate_from_code_layer(LifeCycleStatusLayer)
41+
42+
def get_plan_attributes(self) -> Plan:
43+
return Plan(
44+
id_=None,
45+
name=self.name_line_edit.text(),
46+
description=self.description_text_edit.toPlainText() or None,
47+
plan_type_id=self.plan_type_combo_box.value(),
48+
permanent_plan_identifier=self.permanent_identifier_line_edit.text() or None,
49+
record_number=self.record_number_line_edit.text() or None,
50+
producers_plan_identifier=self.producers_plan_identifier_line_edit.text() or None,
51+
matter_management_identifier=self.matter_management_identifier_line_edit.text() or None,
52+
lifecycle_status_id=self.lifecycle_status_combo_box.value(),
53+
general_regulations=[],
54+
)

0 commit comments

Comments
 (0)