-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfeature_template_library.py
252 lines (187 loc) · 9.54 KB
/
feature_template_library.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
from __future__ import annotations
import logging
from collections import defaultdict
from qgis.core import QgsFeature, QgsProject, QgsVectorLayer
from qgis.gui import QgsMapToolDigitizeFeature
from qgis.PyQt.QtCore import QItemSelectionModel
from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel
from qgis.utils import iface
from arho_feature_template.core.template_library_config import (
FeatureTemplate,
TemplateLibraryConfig,
TemplateLibraryVersionError,
TemplateSyntaxError,
parse_template_library_config,
)
from arho_feature_template.gui.feature_attribute_form import FeatureAttributeForm
from arho_feature_template.gui.template_dock import TemplateLibraryDock
from arho_feature_template.resources.template_libraries import library_config_files
logger = logging.getLogger(__name__)
class LayerNotFoundError(Exception):
def __init__(self, layer_name: str):
super().__init__(f"Layer {layer_name} not found")
class LayerNotVectorTypeError(Exception):
def __init__(self, layer_name: str):
super().__init__(f"Layer {layer_name} is not a vector layer")
def get_layer_from_project(layer_name: str) -> QgsVectorLayer:
project = QgsProject.instance()
if not project:
raise LayerNotFoundError(layer_name)
layers = project.mapLayersByName(layer_name)
if not layers:
raise LayerNotFoundError(layer_name)
if len(layers) > 1:
logger.warning("Multiple layers with the same name found. Using the first one.")
layer = layers[0]
if not isinstance(layer, QgsVectorLayer):
raise LayerNotVectorTypeError(layer_name)
return layer
class TemplateItem(QStandardItem):
def __init__(self, template_config: FeatureTemplate) -> None:
self.config = template_config
super().__init__(template_config.name)
def is_valid(self) -> bool:
"""Check if the template is valid agains current QGIS project
Checks if the layer and attributes defined in the template acutally exists"""
try:
get_layer_from_project(self.config.feature.layer) # TODO: check child features recursively
except (LayerNotFoundError, LayerNotVectorTypeError):
return False
else:
return True
class TemplateGeometryDigitizeMapTool(QgsMapToolDigitizeFeature): ...
class FeatureTemplater:
def __init__(self) -> None:
self.library_configs: dict[str, TemplateLibraryConfig] = {}
self.template_dock = TemplateLibraryDock()
self.template_dock.hide()
self.template_model = QStandardItemModel()
self.template_dock.template_list.setModel(self.template_model)
# Set the selection mode to allow single selection
self.template_dock.template_list.setSelectionMode(self.template_dock.template_list.SingleSelection)
self._read_library_configs()
self.template_dock.library_selection.addItems(self.get_library_names())
# Update template tree when library selection changes
self.template_dock.library_selection.currentIndexChanged.connect(
lambda: self.set_active_library(self.template_dock.library_selection.currentText())
)
# Update template tree when search text changes
self.template_dock.search_box.valueChanged.connect(self.on_template_search_text_changed)
# Activate map tool when template is selected
self.template_dock.template_list.clicked.connect(self.on_template_item_clicked)
self.digitize_map_tool = TemplateGeometryDigitizeMapTool(iface.mapCanvas(), iface.cadDockWidget())
self.digitize_map_tool.digitizingCompleted.connect(self.ask_for_feature_attributes)
self.digitize_map_tool.deactivated.connect(self.template_dock.template_list.clearSelection)
def on_template_item_clicked(self, index):
item = self.template_model.itemFromIndex(index)
# Do nothing if clicked item is a group
if item.hasChildren():
return
try:
layer = get_layer_from_project(item.config.feature.layer)
except (LayerNotFoundError, LayerNotVectorTypeError):
logger.exception("Failed to activate template")
return
self.active_template = item
self.start_digitizing_for_layer(layer)
# Reselect as a workaround for first selection visual clarity
self.template_dock.template_list.selectionModel().select(
index, QItemSelectionModel.Select | QItemSelectionModel.Rows
)
def on_template_search_text_changed(self, search_text: str) -> None:
search_text = search_text.lower()
for row in range(self.template_model.rowCount()):
group_item = self.template_model.item(row)
group_visible = False
for child_row in range(group_item.rowCount()):
child_item = group_item.child(child_row)
if child_item.hasChildren():
subgroup_visible = False
for template_row in range(child_item.rowCount()):
template_item = child_item.child(template_row)
matches = search_text in template_item.text().lower()
template_item.setEnabled(matches)
if matches:
subgroup_visible = True
child_item.setEnabled(subgroup_visible)
group_visible = group_visible or subgroup_visible
index = self.template_model.indexFromItem(child_item)
self.template_dock.template_list.setExpanded(index, subgroup_visible)
else:
template_item = child_item
matches = search_text in template_item.text().lower()
template_item.setEnabled(matches)
if matches:
group_visible = True
group_item.setEnabled(group_visible)
index = self.template_model.indexFromItem(group_item)
self.template_dock.template_list.setExpanded(index, group_visible)
def start_digitizing_for_layer(self, layer: QgsVectorLayer) -> None:
self.digitize_map_tool.clean()
self.digitize_map_tool.setLayer(layer)
if not layer.isEditable():
succeeded = layer.startEditing()
if not succeeded:
logger.warning("Failed to start editing layer %s", layer.name())
return
iface.mapCanvas().setMapTool(self.digitize_map_tool)
def ask_for_feature_attributes(self, feature: QgsFeature) -> None:
"""Shows a dialog to ask for feature attributes and creates the feature"""
if not self.active_template:
return
attribute_form = FeatureAttributeForm(self.active_template.config.feature)
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(),
)
layer.beginEditCommand("Create feature from template")
layer.addFeature(feature)
layer.commitChanges(stopEditing=False)
def get_library_names(self) -> list[str]:
return list(self.library_configs.keys())
def set_active_library(self, library_name: str) -> None:
self.template_model.clear()
grouped_templates: defaultdict[str, defaultdict[str, list[FeatureTemplate]]] = defaultdict(
lambda: defaultdict(list)
)
# Group templates by 'group' and 'sub_group'
for template in self.library_configs[library_name].templates:
group = getattr(template, "group", None) or "Ryhmittelemättömät"
sub_group = getattr(template, "sub_group", None)
if sub_group:
grouped_templates[group][sub_group].append(template)
else:
grouped_templates[group][""].append(template)
for group_name, sub_group_dict in grouped_templates.items():
group_item = QStandardItem(group_name)
group_item.setEditable(False)
for sub_group_name, templates in sub_group_dict.items():
if sub_group_name:
sub_group_item = QStandardItem(sub_group_name)
sub_group_item.setEditable(False)
group_item.appendRow(sub_group_item)
target_item = sub_group_item
else:
# If template has no sub_group set, list it directly under group
target_item = group_item
for template in templates:
template_item = TemplateItem(template)
template_item.setEditable(False)
target_item.appendRow(template_item)
self.template_model.appendRow(group_item)
self.template_dock.template_list.expandAll()
def _read_library_configs(self) -> None:
for config_file in library_config_files():
try:
config = parse_template_library_config(config_file)
self.library_configs[config.meta.name] = config
except (TemplateLibraryVersionError, TemplateSyntaxError) as e:
logger.warning("Failed to parse template library configuration: %s", e)
first_library_name = next(iter(self.library_configs), None)
if first_library_name:
self.set_active_library(first_library_name)