Skip to content

Commit b1dc17a

Browse files
committedMar 18, 2025·
add form for importing plan features
1 parent 1d7cbfb commit b1dc17a

File tree

5 files changed

+457
-14
lines changed

5 files changed

+457
-14
lines changed
 

‎arho_feature_template/core/plan_manager.py

+7-14
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
RegulationGroupLibrary,
2323
)
2424
from arho_feature_template.exceptions import UnsavedChangesError
25+
from arho_feature_template.gui.dialogs.import_features_form import ImportFeaturesForm
2526
from arho_feature_template.gui.dialogs.lifecycle_editor import LifecycleEditor
2627
from arho_feature_template.gui.dialogs.load_plan_dialog import LoadPlanDialog
2728
from arho_feature_template.gui.dialogs.plan_attribute_form import PlanAttributeForm
@@ -33,16 +34,11 @@
3334
from arho_feature_template.gui.tools.inspect_plan_features_tool import InspectPlanFeatures
3435
from arho_feature_template.project.layers.code_layers import PlanRegulationGroupTypeLayer, code_layers
3536
from arho_feature_template.project.layers.plan_layers import (
37+
FEATURE_LAYER_NAME_TO_CLASS_MAP,
3638
AdditionalInformationLayer,
3739
DocumentLayer,
38-
LandUseAreaLayer,
39-
LandUsePointLayer,
4040
LegalEffectAssociationLayer,
4141
LifeCycleLayer,
42-
LineLayer,
43-
OtherAreaLayer,
44-
OtherPointLayer,
45-
PlanFeatureLayer,
4642
PlanLayer,
4743
PlanPropositionLayer,
4844
PlanRegulationLayer,
@@ -72,14 +68,6 @@
7268

7369
logger = logging.getLogger(__name__)
7470

75-
FEATURE_LAYER_NAME_TO_CLASS_MAP: dict[str, type[PlanFeatureLayer]] = {
76-
LandUsePointLayer.name: LandUsePointLayer,
77-
OtherPointLayer.name: OtherPointLayer,
78-
LineLayer.name: LineLayer,
79-
OtherAreaLayer.name: OtherAreaLayer,
80-
LandUseAreaLayer.name: LandUseAreaLayer,
81-
}
82-
8371

8472
class PlanDigitizeMapTool(QgsMapToolDigitizeFeature): ...
8573

@@ -158,6 +146,11 @@ def initialize_libraries(self):
158146
]
159147
self.new_feature_dock.initialize_feature_template_libraries(self.feature_template_libraries)
160148

149+
def open_import_features_dialog(self):
150+
import_features_form = ImportFeaturesForm(self.active_plan_regulation_group_library)
151+
if import_features_form.exec_():
152+
pass
153+
161154
def update_active_plan_regulation_group_library(self):
162155
self.active_plan_regulation_group_library = regulation_group_library_from_active_plan()
163156
self.regulation_groups_dock.update_regulation_groups(self.active_plan_regulation_group_library)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
from __future__ import annotations
2+
3+
from importlib import resources
4+
from typing import TYPE_CHECKING
5+
6+
from qgis.core import (
7+
QgsFeature,
8+
QgsFeatureIterator,
9+
QgsFeatureRequest,
10+
QgsFieldProxyModel,
11+
QgsVectorLayer,
12+
)
13+
from qgis.PyQt import uic
14+
from qgis.PyQt.QtWidgets import QCheckBox, QDialog, QDialogButtonBox, QProgressBar
15+
16+
from arho_feature_template.core.models import PlanFeature, RegulationGroupLibrary
17+
from arho_feature_template.project.layers.code_layers import UndergroundTypeLayer, code_layers
18+
from arho_feature_template.project.layers.plan_layers import (
19+
FEATURE_LAYER_NAME_TO_CLASS_MAP,
20+
RegulationGroupAssociationLayer,
21+
plan_feature_layers,
22+
plan_layers,
23+
)
24+
from arho_feature_template.utils.misc_utils import iface, use_wait_cursor
25+
26+
if TYPE_CHECKING:
27+
from qgis.gui import QgsCheckableComboBox, QgsFieldComboBox, QgsFieldExpressionWidget, QgsMapLayerComboBox
28+
29+
from arho_feature_template.gui.components.code_combobox import CodeComboBox
30+
31+
32+
ui_path = resources.files(__package__) / "import_features_form.ui"
33+
FormClass, _ = uic.loadUiType(ui_path)
34+
35+
36+
class ImportFeaturesForm(QDialog, FormClass):
37+
def __init__(self, active_plan_regulation_groups_library: RegulationGroupLibrary):
38+
super().__init__()
39+
self.setupUi(self)
40+
41+
# TYPES
42+
self.source_layer_selection: QgsMapLayerComboBox
43+
self.filter_expression: QgsFieldExpressionWidget
44+
self.selected_features_only: QCheckBox
45+
46+
self.name_selection: QgsFieldComboBox
47+
self.description_selection: QgsFieldComboBox
48+
self.feature_type_of_underground_selection: CodeComboBox
49+
self.regulation_groups_selection: QgsCheckableComboBox
50+
51+
self.target_layer_selection: QgsMapLayerComboBox
52+
53+
self.progress_bar: QProgressBar
54+
self.process_button_box: QDialogButtonBox
55+
56+
# INIT
57+
self.process_button_box.button(QDialogButtonBox.Ok).setText("Import")
58+
self.process_button_box.accepted.connect(self.import_features)
59+
self.process_button_box.rejected.connect(self.reject)
60+
61+
# Source layer initialization
62+
# Exclude all project layers from valid source layers
63+
# NOTE: Some project layers are not included in either `plan_layers` or `code_layers`?
64+
self.source_layer_selection.setLayer(iface.activeLayer())
65+
excluded_layers = [layer.get_from_project() for layer in plan_layers + code_layers]
66+
self.source_layer_selection.setExceptedLayerList(excluded_layers)
67+
self.source_layer_selection.layerChanged.connect(self._on_layer_selections_changed)
68+
69+
# Target layer initialization
70+
# Set only plan feature layers as valid target layers
71+
self.target_layer_selection.clear()
72+
self.target_layer_selection.setAdditionalLayers(layer.get_from_project() for layer in plan_feature_layers)
73+
self.target_layer_selection.setCurrentIndex(0)
74+
self.target_layer_selection.layerChanged.connect(self._on_layer_selections_changed)
75+
76+
# Name field initialization
77+
self.name_selection.setAllowEmptyFieldName(True)
78+
self.name_selection.setFilters(QgsFieldProxyModel.Filter.String)
79+
self.name_selection.setField("")
80+
81+
# Description field initialization
82+
self.description_selection.setAllowEmptyFieldName(True)
83+
self.description_selection.setFilters(QgsFieldProxyModel.Filter.String)
84+
self.description_selection.setField("")
85+
86+
# Underground type initialization
87+
# Remove NULL from the selections and set Maanpäällinen as default
88+
self.feature_type_of_underground_selection.populate_from_code_layer(UndergroundTypeLayer)
89+
self.feature_type_of_underground_selection.remove_item_by_text("NULL")
90+
self.feature_type_of_underground_selection.setCurrentIndex(1) # Set default to Maanpäällinen (index 1)
91+
92+
# Regulation groups initialization
93+
# Only regulation group already in DB are shown and they are not categorized right now
94+
# NOTE: This means groups that are "Aluevaraus" groups can be given to "Osa-alue" for example
95+
i = 0
96+
for category in active_plan_regulation_groups_library.regulation_group_categories:
97+
for group in category.regulation_groups:
98+
self.regulation_groups_selection.addItem(str(group))
99+
self.regulation_groups_selection.setItemData(i, group.id_)
100+
i += 1
101+
102+
self._on_layer_selections_changed(self.source_layer_selection.currentLayer())
103+
104+
def _on_layer_selections_changed(self, _: QgsVectorLayer):
105+
self.source_layer: QgsVectorLayer = self.source_layer_selection.currentLayer()
106+
self.source_layer_name: str = self.source_layer.name()
107+
self.target_layer: QgsVectorLayer = self.target_layer_selection.currentLayer()
108+
self.target_layer_name: str = self.target_layer.name()
109+
110+
self.filter_expression.setLayer(self.source_layer)
111+
self.name_selection.setLayer(self.source_layer)
112+
self.description_selection.setLayer(self.source_layer)
113+
114+
if self.source_and_target_layer_types_match():
115+
self.process_button_box.button(QDialogButtonBox.Ok).setEnabled(True)
116+
else:
117+
self.process_button_box.button(QDialogButtonBox.Ok).setEnabled(False)
118+
119+
def source_and_target_layer_types_match(self) -> bool:
120+
if not self.source_layer or not self.target_layer:
121+
return False
122+
return self.source_layer.wkbType() is self.target_layer.wkbType()
123+
124+
@use_wait_cursor
125+
def import_features(self):
126+
if not self.source_layer or not self.target_layer:
127+
return
128+
129+
self.progress_bar.setValue(0)
130+
source_features = list(self.get_source_features(self.source_layer))
131+
if not source_features:
132+
iface.messageBar().pushInfo("", "Yhtään kohdetta ei tuotu.")
133+
return
134+
135+
# Create and add new plan features
136+
plan_features = self.create_plan_features(source_features)
137+
total_count = len(plan_features)
138+
failed_count = 0
139+
success_count = 0
140+
for i, feat in enumerate(plan_features):
141+
self.progress_bar.setValue(int((i + 1) / total_count * 100))
142+
if self._save_feature(feat, self.target_layer, None, "Kaavakohteen lisääminen"):
143+
success_count += 1
144+
else:
145+
failed_count += 1
146+
147+
# If regulation groups are defined, associate them with the plan features
148+
if len(self.regulation_groups_selection.checkedItems()) > 0:
149+
associations = self.create_regulation_group_associations(plan_features)
150+
associations_layer = RegulationGroupAssociationLayer.get_from_project()
151+
for association in associations:
152+
self._save_feature(association, associations_layer, None, "Kaavamääräysryhmän assosiaation lisääminen")
153+
154+
if failed_count == 0:
155+
iface.messageBar().pushSuccess("", "Kaavakohteet tuotiin onnistuneesti.")
156+
else:
157+
iface.messageBar().pushInfo("", f"Osa kaavakohteista tuotiin epäonnistuneesti ({failed_count}).")
158+
159+
self.progress_bar.setValue(100)
160+
161+
def get_source_features(self, source_layer: QgsVectorLayer) -> QgsFeatureIterator | list[QgsFeature]:
162+
expression_text = self.filter_expression.currentText()
163+
164+
# Case 1: Both selection and expression
165+
if self.selected_features_only.isChecked() and expression_text:
166+
selected_features = source_layer.selectedFeatures()
167+
request = QgsFeatureRequest().setFilterExpression(expression_text)
168+
source_features = [feat for feat in source_layer.getFeatures(request) if feat in selected_features]
169+
170+
# Case 2: Only selection
171+
elif self.selected_features_only.isChecked():
172+
source_features = source_layer.selectedFeatures()
173+
174+
# Case 3: Only expression
175+
elif expression_text:
176+
request = QgsFeatureRequest().setFilterExpression(expression_text)
177+
source_features = source_layer.getFeatures(request)
178+
179+
# Case 4: No expression or selection
180+
else:
181+
source_features = source_layer.getFeatures()
182+
183+
return source_features
184+
185+
def create_plan_features(self, source_features: QgsFeatureIterator | list[QgsFeature]) -> list[QgsFeature]:
186+
type_of_underground_id = self.feature_type_of_underground_selection.value()
187+
source_layer_name_field = self.name_selection.currentField()
188+
source_layer_description_field = self.description_selection.currentField()
189+
layer_class = FEATURE_LAYER_NAME_TO_CLASS_MAP.get(self.target_layer_name)
190+
if not layer_class:
191+
msg = f"Could not find plan feature layer class for layer name {self.target_layer_name}"
192+
raise ValueError(msg)
193+
194+
return [
195+
layer_class.feature_from_model(
196+
PlanFeature(
197+
geom=feature.geometry(),
198+
type_of_underground_id=type_of_underground_id,
199+
layer_name=self.target_layer_name,
200+
name=feature[source_layer_name_field] if source_layer_name_field else None,
201+
description=feature[source_layer_description_field] if source_layer_description_field else None,
202+
)
203+
)
204+
for feature in source_features
205+
]
206+
207+
def create_regulation_group_associations(self, plan_features: list[QgsFeature]) -> list[QgsFeature]:
208+
return [
209+
RegulationGroupAssociationLayer.feature_from(
210+
regulation_group_id, self.target_layer_name, plan_feature["id"]
211+
)
212+
for regulation_group_id in self.regulation_groups_selection.checkedItemsData()
213+
for plan_feature in plan_features
214+
]
215+
216+
@staticmethod
217+
def _save_feature(feature: QgsFeature, layer: QgsVectorLayer, id_: str | None, edit_text: str = "") -> bool:
218+
if not layer.isEditable():
219+
layer.startEditing()
220+
layer.beginEditCommand(edit_text)
221+
222+
if id_ is None:
223+
layer.addFeature(feature)
224+
else:
225+
layer.updateFeature(feature)
226+
227+
layer.endEditCommand()
228+
return layer.commitChanges(stopEditing=False)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>Dialog</class>
4+
<widget class="QDialog" name="Dialog">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>607</width>
10+
<height>473</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Tuo kaavakohteita</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout">
17+
<item>
18+
<widget class="QGroupBox" name="groupBox_2">
19+
<property name="title">
20+
<string>Tasot</string>
21+
</property>
22+
<layout class="QFormLayout" name="formLayout">
23+
<item row="0" column="0">
24+
<widget class="QLabel" name="label">
25+
<property name="text">
26+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; color:#ff0000;&quot;&gt;*&lt;/span&gt; Tuontitaso:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
27+
</property>
28+
</widget>
29+
</item>
30+
<item row="0" column="1">
31+
<widget class="QgsMapLayerComboBox" name="source_layer_selection"/>
32+
</item>
33+
<item row="1" column="0">
34+
<widget class="QLabel" name="label_3">
35+
<property name="text">
36+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; color:#ff0000;&quot;&gt;*&lt;/span&gt; Kohdetaso:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
37+
</property>
38+
</widget>
39+
</item>
40+
<item row="1" column="1">
41+
<widget class="QgsMapLayerComboBox" name="target_layer_selection"/>
42+
</item>
43+
</layout>
44+
</widget>
45+
</item>
46+
<item>
47+
<widget class="QGroupBox" name="groupBox_5">
48+
<property name="title">
49+
<string>Tuotavien kohteiden valinta</string>
50+
</property>
51+
<layout class="QFormLayout" name="formLayout_5">
52+
<item row="0" column="0">
53+
<widget class="QLabel" name="label_4">
54+
<property name="text">
55+
<string>Valintalauseke</string>
56+
</property>
57+
</widget>
58+
</item>
59+
<item row="0" column="1">
60+
<widget class="QgsFieldExpressionWidget" name="filter_expression"/>
61+
</item>
62+
<item row="1" column="1">
63+
<widget class="QCheckBox" name="selected_features_only">
64+
<property name="text">
65+
<string>Tuo vain valitut kohteet</string>
66+
</property>
67+
</widget>
68+
</item>
69+
<item row="1" column="0">
70+
<widget class="QLabel" name="label_8">
71+
<property name="text">
72+
<string/>
73+
</property>
74+
</widget>
75+
</item>
76+
</layout>
77+
</widget>
78+
</item>
79+
<item>
80+
<widget class="QGroupBox" name="groupBox">
81+
<property name="title">
82+
<string>Tuontitasolta kopioitavat tiedot</string>
83+
</property>
84+
<layout class="QFormLayout" name="formLayout_2">
85+
<item row="0" column="0">
86+
<widget class="QLabel" name="label_2">
87+
<property name="text">
88+
<string>Nimikenttä tuontitasolla</string>
89+
</property>
90+
</widget>
91+
</item>
92+
<item row="0" column="1">
93+
<widget class="QgsFieldComboBox" name="name_selection"/>
94+
</item>
95+
<item row="1" column="0">
96+
<widget class="QLabel" name="label_6">
97+
<property name="text">
98+
<string>Kuvauskenttä tuontitasolla</string>
99+
</property>
100+
</widget>
101+
</item>
102+
<item row="1" column="1">
103+
<widget class="QgsFieldComboBox" name="description_selection"/>
104+
</item>
105+
</layout>
106+
</widget>
107+
</item>
108+
<item>
109+
<widget class="QGroupBox" name="groupBox_4">
110+
<property name="title">
111+
<string>Kaikille kohteille tulevat tiedot</string>
112+
</property>
113+
<layout class="QFormLayout" name="formLayout_4">
114+
<item row="0" column="0">
115+
<widget class="QLabel" name="label_7">
116+
<property name="text">
117+
<string>Maanalaisuuden laji</string>
118+
</property>
119+
</widget>
120+
</item>
121+
<item row="0" column="1">
122+
<widget class="CodeComboBox" name="feature_type_of_underground_selection"/>
123+
</item>
124+
<item row="1" column="0">
125+
<widget class="QLabel" name="label_5">
126+
<property name="text">
127+
<string>Kaavamääräysryhmät</string>
128+
</property>
129+
</widget>
130+
</item>
131+
<item row="1" column="1">
132+
<widget class="QgsCheckableComboBox" name="regulation_groups_selection"/>
133+
</item>
134+
</layout>
135+
</widget>
136+
</item>
137+
<item>
138+
<spacer name="verticalSpacer">
139+
<property name="orientation">
140+
<enum>Qt::Vertical</enum>
141+
</property>
142+
<property name="sizeHint" stdset="0">
143+
<size>
144+
<width>20</width>
145+
<height>13</height>
146+
</size>
147+
</property>
148+
</spacer>
149+
</item>
150+
<item>
151+
<layout class="QHBoxLayout" name="horizontalLayout">
152+
<item>
153+
<widget class="QProgressBar" name="progress_bar">
154+
<property name="value">
155+
<number>0</number>
156+
</property>
157+
</widget>
158+
</item>
159+
<item>
160+
<widget class="QDialogButtonBox" name="process_button_box">
161+
<property name="sizePolicy">
162+
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
163+
<horstretch>0</horstretch>
164+
<verstretch>0</verstretch>
165+
</sizepolicy>
166+
</property>
167+
<property name="standardButtons">
168+
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
169+
</property>
170+
</widget>
171+
</item>
172+
</layout>
173+
</item>
174+
</layout>
175+
</widget>
176+
<customwidgets>
177+
<customwidget>
178+
<class>QgsCheckableComboBox</class>
179+
<extends>QComboBox</extends>
180+
<header>qgscheckablecombobox.h</header>
181+
</customwidget>
182+
<customwidget>
183+
<class>QgsFieldComboBox</class>
184+
<extends>QComboBox</extends>
185+
<header>qgsfieldcombobox.h</header>
186+
</customwidget>
187+
<customwidget>
188+
<class>QgsFieldExpressionWidget</class>
189+
<extends>QWidget</extends>
190+
<header>qgsfieldexpressionwidget.h</header>
191+
</customwidget>
192+
<customwidget>
193+
<class>QgsMapLayerComboBox</class>
194+
<extends>QComboBox</extends>
195+
<header>qgsmaplayercombobox.h</header>
196+
</customwidget>
197+
<customwidget>
198+
<class>CodeComboBox</class>
199+
<extends>QComboBox</extends>
200+
<header>arho_feature_template.gui.components.code_combobox</header>
201+
</customwidget>
202+
</customwidgets>
203+
<resources/>
204+
<connections/>
205+
</ui>

‎arho_feature_template/plugin.py

+9
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,15 @@ def initGui(self) -> None: # noqa N802
259259
status_tip="Tallenna aktiivinen kaava geotiff muodossa",
260260
)
261261

262+
self.import_features_action = self.add_action(
263+
text="Tuo kaavakohteita",
264+
icon=QgsApplication.getThemeIcon("mActionSharingImport.svg"),
265+
triggered_callback=self.plan_manager.open_import_features_dialog,
266+
add_to_menu=True,
267+
add_to_toolbar=True,
268+
status_tip="Tuo kaavakohteita tietokantaan toisilta vektoritasoilta",
269+
)
270+
262271
self.plugin_settings_action = self.add_action(
263272
text="Asetukset",
264273
triggered_callback=self.open_settings,

‎arho_feature_template/project/layers/plan_layers.py

+8
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,14 @@ def get_features_by_plan_id(cls, plan_id: str) -> list[QgsFeature]:
721721
return list(cls.get_features_by_attribute_value("plan_id", plan_id))
722722

723723

724+
FEATURE_LAYER_NAME_TO_CLASS_MAP: dict[str, type[PlanFeatureLayer]] = {
725+
LandUsePointLayer.name: LandUsePointLayer,
726+
OtherPointLayer.name: OtherPointLayer,
727+
LineLayer.name: LineLayer,
728+
OtherAreaLayer.name: OtherAreaLayer,
729+
LandUseAreaLayer.name: LandUseAreaLayer,
730+
}
731+
724732
plan_layers = AbstractPlanLayer.__subclasses__()
725733
plan_layers.remove(PlanFeatureLayer)
726734

0 commit comments

Comments
 (0)
Please sign in to comment.