Skip to content

Commit 1a0cc15

Browse files
committed
Fix undo/redo actions for the registry memory layer. Overall, improve
performance when adding and removing groups in the registry during large editing sessions
1 parent 57fdf39 commit 1a0cc15

File tree

3 files changed

+179
-90
lines changed

3 files changed

+179
-90
lines changed

core/registry.py

Lines changed: 152 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""
33
/***************************************************************************
44
ThRasE
5-
5+
66
A powerful and fast thematic raster editor Qgis plugin
77
-------------------
88
copyright : (C) 2019-2025 by Xavier Corredor Llano, SMByC
@@ -18,7 +18,7 @@
1818
* *
1919
***************************************************************************/
2020
"""
21-
21+
import itertools
2222
import os
2323

2424
from qgis.PyQt.QtGui import QColor
@@ -61,7 +61,7 @@ def __init__(self, idx, group_idx, center_x, center_y, px_size_x, px_size_y, mem
6161
feature = QgsFeature(memory_layer.fields())
6262
feature.setGeometry(geom)
6363
feature.setAttributes([self.idx, self.group_idx, center_x, center_y])
64-
64+
6565
# add feature to memory layer
6666
memory_layer.dataProvider().addFeature(feature)
6767
self.feature_id = feature.id()
@@ -89,7 +89,7 @@ def show(self):
8989
# display tile borders without fill by applying filter to memory layer
9090
if not self.tiles:
9191
return
92-
92+
9393
# set filter to show only tiles from this group
9494
filter_expr = f'"group_idx" = {self.idx}'
9595
self.memory_layer.setSubsetString(filter_expr)
@@ -108,11 +108,11 @@ def center(self):
108108
tiles_extent = self.tiles_extent()
109109
if tiles_extent.isEmpty():
110110
return
111-
111+
112112
# get the center point of the group
113113
center_x = tiles_extent.center().x()
114114
center_y = tiles_extent.center().y()
115-
115+
116116
# center all active views on this point without changing scale
117117
for view_widget in ThRasEDialog.view_widgets:
118118
if not view_widget.is_active:
@@ -135,11 +135,11 @@ def __init__(self, layer_to_edit):
135135
self.current_group = None
136136
self.tiles_color = QColor("#ff00ff")
137137
self.enabled = True
138-
138+
139139
# create memory vector layer for tiles
140140
self.memory_layer = None
141141
self.create_memory_layer()
142-
142+
143143
# single renderer for both display modes
144144
self.renderer = None
145145
self.setup_renderer()
@@ -148,11 +148,11 @@ def create_memory_layer(self):
148148
"""Create memory vector layer to store tile geometries."""
149149
crs = self.layer_to_edit.qgs_layer.crs()
150150
self.memory_layer = QgsVectorLayer(
151-
f"Polygon?crs={crs.authid()}",
152-
"ThRasE Registry",
151+
f"Polygon?crs={crs.authid()}",
152+
"ThRasE Registry",
153153
"memory"
154154
)
155-
155+
156156
# add fields
157157
provider = self.memory_layer.dataProvider()
158158
provider.addAttributes([
@@ -162,19 +162,19 @@ def create_memory_layer(self):
162162
QgsField("center_y", QVariant.Double),
163163
])
164164
self.memory_layer.updateFields()
165-
165+
166166
# set initial state (hidden by default)
167167
self.memory_layer.setSubsetString("FALSE") # hide all initially
168168

169169
def setup_renderer(self, color=None):
170170
"""Setup single renderer for draw the registry tiles.
171-
171+
172172
Args:
173173
color: QColor to use for border. If None, uses the default color
174174
"""
175175
if color is None:
176176
color = self.tiles_color
177-
177+
178178
# Simple border renderer - no fill, 0.3 border width
179179
border_symbol = QgsFillSymbol.createSimple({
180180
'color': 'transparent',
@@ -184,7 +184,7 @@ def setup_renderer(self, color=None):
184184
'outline_style': 'solid'
185185
})
186186
self.renderer = QgsSingleSymbolRenderer(border_symbol)
187-
187+
188188
def update_registry_layer_in_canvases(self):
189189
"""Refresh render layers in all active canvases to update registry layer visibility."""
190190
from ThRasE.gui.main_dialog import ThRasEDialog
@@ -196,7 +196,7 @@ def delete(self):
196196
self.clear()
197197
self.groups = []
198198
self.current_group = None
199-
199+
200200
# clear memory layer
201201
if self.memory_layer:
202202
self.memory_layer.dataProvider().truncate()
@@ -207,7 +207,7 @@ def clear(self):
207207
if self.memory_layer:
208208
self.memory_layer.setSubsetString("FALSE")
209209
self.memory_layer.triggerRepaint()
210-
210+
211211
def refresh_all_canvases(self):
212212
"""Refresh all active view widget canvases."""
213213
from ThRasE.gui.main_dialog import ThRasEDialog
@@ -219,13 +219,13 @@ def show_all(self):
219219
"""Display all tiles with border."""
220220
if not self.groups or not self.memory_layer:
221221
return
222-
222+
223223
# ensure layer is in canvases
224224
self.update_registry_layer_in_canvases()
225-
225+
226226
# apply renderer
227227
self.memory_layer.setRenderer(self.renderer.clone())
228-
228+
229229
# show all features (remove filter)
230230
self.memory_layer.setSubsetString("")
231231
self.memory_layer.triggerRepaint()
@@ -238,12 +238,12 @@ def clear_show_all(self):
238238

239239
# check if registry widget is visible and enabled, and if we have a current group
240240
from ThRasE.thrase import ThRasE
241-
registry_visible = (ThRasE.dialog and
242-
ThRasE.dialog.registry_widget and
243-
ThRasE.dialog.registry_widget.isVisible() and
244-
self.enabled and
241+
registry_visible = (ThRasE.dialog and
242+
ThRasE.dialog.registry_widget and
243+
ThRasE.dialog.registry_widget.isVisible() and
244+
self.enabled and
245245
self.current_group)
246-
246+
247247
if registry_visible:
248248
# restore current group display
249249
self.memory_layer.setRenderer(self.renderer.clone())
@@ -252,92 +252,178 @@ def clear_show_all(self):
252252
else:
253253
# hide all features
254254
self.memory_layer.setSubsetString("FALSE")
255-
255+
256256
self.memory_layer.triggerRepaint()
257257
self.refresh_all_canvases()
258258

259259
def update_color(self):
260260
"""Update the border color for current display."""
261261
# recreate renderer with current color
262262
self.setup_renderer(self.tiles_color)
263-
263+
264264
# if currently displaying something, update renderer
265265
if self.memory_layer and self.memory_layer.subsetString() != "FALSE":
266266
self.memory_layer.setRenderer(self.renderer.clone())
267267
self.memory_layer.triggerRepaint()
268268
self.refresh_all_canvases()
269269

270-
def update(self):
271-
# clear previous
272-
self.delete()
273-
274-
# recreate memory layer
275-
self.create_memory_layer()
276-
self.setup_renderer()
270+
def update(self, force_rebuild=False):
271+
"""Update registry state after pixel edits."""
272+
grouped_logs = {gid: list(logs) for gid, logs
273+
in itertools.groupby((pl for pl in self.layer_to_edit.pixel_log_store.values()
274+
if pl.group_id is not None), key=lambda pl: pl.group_id)}
277275

278-
# group PixelLogs by group_id
279-
group_id_to_logs = {}
280-
for pixel_log in self.layer_to_edit.pixel_log_store.values():
281-
if pixel_log.group_id is None:
282-
# skip logs without group id
283-
continue
284-
group_id_to_logs.setdefault(pixel_log.group_id, []).append(pixel_log)
276+
if not grouped_logs:
277+
if self.groups:
278+
self.delete()
279+
return False
285280

286-
if not group_id_to_logs:
281+
if force_rebuild:
282+
return self.add_registry_groups(grouped_logs, reset=True)
283+
284+
existing_ids = {group.group_id for group in self.groups}
285+
current_ids = set(grouped_logs.keys())
286+
287+
removed_ids = existing_ids - current_ids
288+
new_ids = current_ids - existing_ids
289+
290+
changed = False
291+
292+
if removed_ids and self.remove_registry_groups(removed_ids):
293+
changed = True
294+
295+
if new_ids and self.add_registry_groups(grouped_logs, group_ids=new_ids, reset=False):
296+
changed = True
297+
298+
return changed or bool(self.groups)
299+
300+
def remove_registry_groups(self, group_ids):
301+
"""Remove registry groups to the memory layer."""
302+
if not self.memory_layer or not self.groups or not group_ids:
287303
return False
288304

289-
# sort groups by edit_date
290-
grouped = []
291-
for gid, logs in group_id_to_logs.items():
292-
# sort pixels inside by edit_date
305+
target_ids = set(group_ids)
306+
to_remove = [group for group in self.groups if group.group_id in target_ids]
307+
if not to_remove:
308+
return False
309+
310+
provider = self.memory_layer.dataProvider()
311+
feature_ids = []
312+
for group in to_remove:
313+
feature_ids.extend(tile.feature_id for tile in group.tiles)
314+
315+
remaining_groups = [group for group in self.groups if group.group_id not in target_ids]
316+
previous_current_id = self.current_group.group_id if self.current_group else None
317+
subset_before = self.memory_layer.subsetString()
318+
319+
self.memory_layer.startEditing()
320+
if feature_ids:
321+
provider.deleteFeatures(feature_ids)
322+
323+
attr_updates = {}
324+
group_idx_field = self.memory_layer.fields().indexOf("group_idx")
325+
for new_idx, group in enumerate(remaining_groups, start=1):
326+
if group.idx == new_idx:
327+
continue
328+
group.idx = new_idx
329+
for tile in group.tiles:
330+
tile.group_idx = new_idx
331+
if group_idx_field >= 0:
332+
attr_updates.setdefault(tile.feature_id, {})[group_idx_field] = new_idx
333+
334+
if attr_updates:
335+
provider.changeAttributeValues(attr_updates)
336+
337+
self.memory_layer.commitChanges()
338+
self.memory_layer.updateExtents()
339+
340+
self.groups = remaining_groups
341+
342+
if previous_current_id and previous_current_id not in target_ids:
343+
self.current_group = next((g for g in remaining_groups if g.group_id == previous_current_id), None)
344+
elif remaining_groups:
345+
self.current_group = remaining_groups[0]
346+
else:
347+
self.current_group = None
348+
349+
subset_after = subset_before
350+
if subset_before.startswith('"group_idx"'):
351+
if self.current_group:
352+
subset_after = f'"group_idx" = {self.current_group.idx}'
353+
else:
354+
subset_after = "FALSE"
355+
elif subset_before == "" and not self.groups:
356+
subset_after = "FALSE"
357+
358+
self.memory_layer.setSubsetString(subset_after)
359+
self.memory_layer.triggerRepaint()
360+
self.refresh_all_canvases()
361+
362+
return True
363+
364+
def add_registry_groups(self, group_id_to_logs, group_ids=None, reset=False):
365+
"""Add registry tile groups to the registry memory layer."""
366+
if reset:
367+
self.delete()
368+
self.create_memory_layer()
369+
self.setup_renderer()
370+
next_idx = 1
371+
else:
372+
next_idx = len(self.groups) + 1
373+
374+
entries = []
375+
iterable = (
376+
((gid, group_id_to_logs.get(gid)) for gid in group_ids)
377+
if group_ids is not None else group_id_to_logs.items()
378+
)
379+
for gid, logs in iterable:
380+
if not logs:
381+
continue
293382
logs_sorted = sorted(logs, key=lambda pl: pl.edit_date)
294383
first_date = logs_sorted[0].edit_date
295-
grouped.append((gid, first_date, logs_sorted))
384+
entries.append((gid, first_date, logs_sorted))
385+
386+
if not entries:
387+
return False
296388

297-
grouped.sort(key=lambda item: item[1])
389+
entries.sort(key=lambda item: item[1])
298390

299-
# build RegistryTileGroup list
300391
psx = self.layer_to_edit.qgs_layer.rasterUnitsPerPixelX()
301392
psy = self.layer_to_edit.qgs_layer.rasterUnitsPerPixelY()
302393

303-
# start editing mode for batch feature addition
304394
self.memory_layer.startEditing()
305395

306-
idx_group = 1
307-
for gid, fdate, logs_sorted in grouped:
396+
for gid, fdate, logs_sorted in entries:
308397
tiles = []
309398
for idx, pl in enumerate(logs_sorted, start=1):
310399
cx = pl.pixel.x()
311400
cy = pl.pixel.y()
312-
tiles.append(RegistryTile(idx, idx_group, cx, cy, psx, psy, self.memory_layer))
313-
self.groups.append(RegistryTileGroup(idx_group, gid, fdate, tiles, self.memory_layer, self))
314-
idx_group += 1
401+
tiles.append(RegistryTile(idx, next_idx, cx, cy, psx, psy, self.memory_layer))
402+
self.groups.append(RegistryTileGroup(next_idx, gid, fdate, tiles, self.memory_layer, self))
403+
next_idx += 1
315404

316-
# commit changes to memory layer
317405
self.memory_layer.commitChanges()
318406
self.memory_layer.updateExtents()
319407

320-
# init current group
321-
self.current_group = self.groups[0] if self.groups else None
322-
if self.current_group is None:
323-
return False
408+
if reset:
409+
self.current_group = self.groups[0] if self.groups else None
324410

325411
return True
326412

327413
def set_current_group(self, idx_group):
328414
from ThRasE.thrase import ThRasE
329-
415+
330416
self.current_group = next((g for g in self.groups if g.idx == idx_group), None)
331417
if not self.current_group:
332418
return
333-
419+
334420
# ensure layer is in canvases
335421
self.update_registry_layer_in_canvases()
336-
422+
337423
# apply renderer for current group
338424
if self.memory_layer:
339425
self.memory_layer.setRenderer(self.renderer.clone())
340-
426+
341427
if ThRasE.dialog.registry_widget.autoCenter.isChecked():
342428
self.current_group.center()
343429
self.current_group.show()
@@ -359,7 +445,7 @@ def export_registry(self, output_file_path):
359445
if ext not in [".gpkg", ".shp", ".geojson"]:
360446
output_file_path = path + ".gpkg"
361447
ext = ".gpkg"
362-
448+
363449
driver_name = {".gpkg": "GPKG", ".shp": "ESRI Shapefile", ".geojson": "GeoJSON"}.get(ext, "GPKG")
364450

365451
# create mapping from UUID group_id
@@ -383,7 +469,7 @@ def export_registry(self, output_file_path):
383469
for pl in self.layer_to_edit.pixel_log_store.values():
384470
cx = pl.pixel.x()
385471
cy = pl.pixel.y()
386-
472+
387473
# create geometry
388474
rect = QgsRectangle(cx - half_psx, cy - half_psy, cx + half_psx, cy + half_psy)
389475
geom = QgsGeometry.fromRect(rect)

0 commit comments

Comments
 (0)