Skip to content

Commit 752f217

Browse files
committed
Merge branch 'dev-0.1.12' into copilot/add-structured-logging-feature
2 parents ff55f3f + 4a0ba19 commit 752f217

File tree

6 files changed

+245
-136
lines changed

6 files changed

+245
-136
lines changed

loopstructural/gui/map2loop_tools/sorter_widget.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def _run_sorter(self):
286286
'geology': self.geologyLayerComboBox.currentLayer(),
287287
'contacts': self.contactsLayerComboBox.currentLayer(),
288288
'sorting_algorithm': algorithm_name,
289-
'unit_name_column': self.unitNameFieldComboBox.currentField(),
289+
'unit_name_field': self.unitNameFieldComboBox.currentField(),
290290
'updater': lambda msg: QMessageBox.information(self, "Progress", msg),
291291
}
292292

loopstructural/gui/map2loop_tools/thickness_calculator_widget.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import os
44

5-
from PyQt5.QtWidgets import QMessageBox, QWidget
5+
from PyQt5.QtWidgets import QLabel, QMessageBox, QWidget
66
from qgis.PyQt import uic
77

88
from loopstructural.toolbelt.preferences import PlgOptionsManager
@@ -256,8 +256,14 @@ def _run_calculator(self):
256256
u = result['thicknesses'].loc[idx, 'name']
257257
thick = result['thicknesses'].loc[idx, 'ThicknessStdDev']
258258
if thick > 0:
259-
260-
self.data_manager._stratigraphic_column.get_unit_by_name(u).thickness = thick
259+
unit = self.data_manager._stratigraphic_column.get_unit_by_name(u)
260+
if unit:
261+
unit.thickness = thick
262+
else:
263+
self.data_manager.logger(
264+
f"Warning: Unit '{u}' not found in stratigraphic column.",
265+
level=QLabel.Warning,
266+
)
261267
# Save debugging files if checkbox is checked
262268
if self.saveDebugCheckBox.isChecked():
263269
if 'lines' in result:

loopstructural/gui/modelling/stratigraphic_column/stratigraphic_unit.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,20 +130,46 @@ def setData(self, data: Optional[dict] = None):
130130
data : dict or None
131131
Dictionary containing 'name' and 'colour' keys. If None, defaults are used.
132132
"""
133+
# Safely update internal state first
133134
if data:
134135
self.name = str(data.get("name", ""))
135136
self.colour = data.get("colour", "")
136-
self.lineEditName.setText(self.name)
137-
self.setStyleSheet(f"background-color: {self.colour};" if self.colour else "")
138-
# self.lineEditColour.setText(self.colour)
139137
else:
140138
self.name = ""
141139
self.colour = ""
142-
self.lineEditName.clear()
143-
self.setStyleSheet("")
144-
# self.lineEditColour.clear()
145140

146-
self.validateFields()
141+
# Guard all direct Qt calls since the wrapped C++ objects may have been deleted
142+
try:
143+
if data:
144+
if hasattr(self, 'lineEditName') and self.lineEditName is not None:
145+
try:
146+
self.lineEditName.setText(self.name)
147+
except RuntimeError:
148+
# Widget has been deleted; abort GUI updates
149+
return
150+
try:
151+
self.setStyleSheet(f"background-color: {self.colour};" if self.colour else "")
152+
except RuntimeError:
153+
return
154+
else:
155+
if hasattr(self, 'lineEditName') and self.lineEditName is not None:
156+
try:
157+
self.lineEditName.clear()
158+
except RuntimeError:
159+
return
160+
try:
161+
self.setStyleSheet("")
162+
except RuntimeError:
163+
return
164+
165+
# Validate fields if widgets still exist
166+
try:
167+
self.validateFields()
168+
except RuntimeError:
169+
return
170+
except RuntimeError:
171+
# Catch any unexpected RuntimeError from underlying Qt objects
172+
return
147173

148174
def getData(self) -> dict:
149175
"""Return the widget data as a dictionary.

loopstructural/main/m2l_api.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ def sort_stratigraphic_column(
100100
unitname1_field=None,
101101
unitname2_field=None,
102102
structure=None,
103-
unit_name_column=None,
104103
dip_field="DIP",
105104
dipdir_field="DIPDIR",
106105
orientation_type="Dip Direction",
@@ -154,22 +153,29 @@ def sort_stratigraphic_column(
154153
# Convert layers to GeoDataFrames
155154
geology_gdf = qgsLayerToGeoDataFrame(geology)
156155
contacts_gdf = qgsLayerToGeoDataFrame(contacts)
157-
print(geology_gdf.columns)
158156
# Build units DataFrame
159-
units_df = geology_gdf[['UNITNAME']].drop_duplicates().reset_index(drop=True)
160-
if unit_name_field and unit_name_field != 'UNITNAME' and unit_name_field in geology_gdf.columns:
157+
if (
158+
unit_name_field
159+
and unit_name_field != unit_name_field
160+
and unit_name_field in geology_gdf.columns
161+
):
161162
units_df = geology_gdf[[unit_name_field]].drop_duplicates().reset_index(drop=True)
162-
units_df.columns = ['UNITNAME']
163+
units_df = units_df.rename(columns={unit_name_field: unit_name_field})
164+
165+
elif unit_name_field in geology_gdf.columns:
166+
units_df = geology_gdf[[unit_name_field]].drop_duplicates().reset_index(drop=True)
167+
else:
168+
raise ValueError(f"Unit name field '{unit_name_field}' not found in geology data")
163169
if min_age_field and min_age_field in geology_gdf.columns:
164170
units_df = units_df.merge(
165-
geology_gdf[['UNITNAME', min_age_field]].drop_duplicates(),
166-
on='UNITNAME',
171+
geology_gdf[[unit_name_field, min_age_field]].drop_duplicates(),
172+
on=unit_name_field,
167173
how='left',
168174
)
169175
if max_age_field and max_age_field in geology_gdf.columns:
170176
units_df = units_df.merge(
171-
geology_gdf[['UNITNAME', max_age_field]].drop_duplicates(),
172-
on='UNITNAME',
177+
geology_gdf[[unit_name_field, max_age_field]].drop_duplicates(),
178+
on=unit_name_field,
173179
how='left',
174180
)
175181
# Build relationships DataFrame (contacts without geometry)
@@ -195,7 +201,7 @@ def sort_stratigraphic_column(
195201
'orientation_type': orientation_type,
196202
'dtm': dtm,
197203
'updater': updater,
198-
'unit_name_column': unit_name_column,
204+
'unit_name_column': unit_name_field,
199205
}
200206

201207
# Only pass required arguments to the sorter
@@ -399,10 +405,10 @@ def calculate_thickness(
399405
geology_gdf = geology_gdf.rename(columns={unit_name_field: 'UNITNAME'})
400406
units = geology_gdf.copy()
401407

402-
units_unique = units.drop_duplicates(subset=[unit_name_field]).reset_index(drop=True)
403-
units = pd.DataFrame({'name': units_unique[unit_name_field]})
408+
units_unique = units.drop_duplicates(subset=['UNITNAME']).reset_index(drop=True)
409+
units = pd.DataFrame({'name': units_unique['UNITNAME']})
404410
basal_contacts_gdf['type'] = 'BASAL' # required by calculator
405-
411+
406412
thickness = calculator.compute(
407413
units,
408414
stratigraphic_order,

loopstructural/plugin_main.py

Lines changed: 98 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@
4747

4848

4949
class LoopstructuralPlugin:
50+
def show_fault_topology_dialog(self):
51+
"""Show the fault topology calculator dialog."""
52+
from loopstructural.gui.map2loop_tools.fault_topology_widget import FaultTopologyWidget
53+
54+
dialog = FaultTopologyWidget(self.iface.mainWindow())
55+
dialog.exec_()
56+
5057
"""QGIS plugin entrypoint for LoopStructural.
5158
5259
This class initializes plugin resources, UI elements and data/model managers
@@ -113,6 +120,11 @@ def initGui(self):
113120
self.iface.registerOptionsWidgetFactory(self.options_factory)
114121

115122
# -- Actions
123+
self.action_fault_topology = QAction(
124+
"Fault Topology Calculator",
125+
self.iface.mainWindow(),
126+
)
127+
self.action_fault_topology.triggered.connect(self.show_fault_topology_dialog)
116128
self.action_help = QAction(
117129
QgsApplication.getThemeIcon("mActionHelpContents.svg"),
118130
self.tr("Help"),
@@ -142,6 +154,7 @@ def initGui(self):
142154
)
143155

144156
self.toolbar.addAction(self.action_modelling)
157+
self.toolbar.addAction(self.action_fault_topology)
145158
# -- Menu
146159
self.iface.addPluginToMenu(__title__, self.action_settings)
147160
self.iface.addPluginToMenu(__title__, self.action_help)
@@ -185,12 +198,14 @@ def initGui(self):
185198
self.toolbar.addAction(self.action_user_sorter)
186199
self.toolbar.addAction(self.action_basal_contacts)
187200
self.toolbar.addAction(self.action_thickness)
201+
self.toolbar.addAction(self.action_fault_topology)
188202

189203
self.iface.addPluginToMenu(__title__, self.action_sampler)
190204
self.iface.addPluginToMenu(__title__, self.action_sorter)
191205
self.iface.addPluginToMenu(__title__, self.action_user_sorter)
192206
self.iface.addPluginToMenu(__title__, self.action_basal_contacts)
193207
self.iface.addPluginToMenu(__title__, self.action_thickness)
208+
self.iface.addPluginToMenu(__title__, self.action_fault_topology)
194209
self.action_basal_contacts.triggered.connect(self.show_basal_contacts_dialog)
195210

196211
# Add all map2loop tool actions to the toolbar
@@ -403,58 +418,90 @@ def initProcessing(self):
403418
QgsApplication.processingRegistry().addProvider(self.provider)
404419

405420
def unload(self):
406-
"""Clean up when plugin is disabled or uninstalled."""
421+
"""Clean up when plugin is disabled or uninstalled.
422+
423+
This implementation is defensive: initGui may not have been run when
424+
QGIS asks the plugin to unload (plugin reloader), so attributes may be
425+
missing. Use getattr to check for presence and guard removals/deletions.
426+
"""
407427
# -- Clean up dock widgets
408-
if self.loop_dockwidget:
409-
self.iface.removeDockWidget(self.loop_dockwidget)
410-
del self.loop_dockwidget
411-
if self.modelling_dockwidget:
412-
self.iface.removeDockWidget(self.modelling_dockwidget)
413-
del self.modelling_dockwidget
414-
if self.visualisation_dockwidget:
415-
self.iface.removeDockWidget(self.visualisation_dockwidget)
416-
del self.visualisation_dockwidget
417-
418-
# -- Clean up menu
419-
self.iface.removePluginMenu(__title__, self.action_help)
420-
self.iface.removePluginMenu(__title__, self.action_settings)
421-
self.iface.removePluginMenu(__title__, self.action_sampler)
422-
self.iface.removePluginMenu(__title__, self.action_sorter)
423-
self.iface.removePluginMenu(__title__, self.action_user_sorter)
424-
self.iface.removePluginMenu(__title__, self.action_basal_contacts)
425-
self.iface.removePluginMenu(__title__, self.action_thickness)
426-
# self.iface.removeMenu(self.menu)
428+
for dock_attr in ("loop_dockwidget", "modelling_dockwidget", "visualisation_dockwidget"):
429+
dock = getattr(self, dock_attr, None)
430+
if dock:
431+
try:
432+
self.iface.removeDockWidget(dock)
433+
except Exception:
434+
# ignore errors during unload
435+
pass
436+
try:
437+
delattr(self, dock_attr)
438+
except Exception:
439+
pass
440+
441+
# -- Clean up menu/actions (only remove if they exist)
442+
for attr in (
443+
"action_help",
444+
"action_settings",
445+
"action_sampler",
446+
"action_sorter",
447+
"action_user_sorter",
448+
"action_basal_contacts",
449+
"action_thickness",
450+
"action_fault_topology",
451+
"action_modelling",
452+
"action_visualisation",
453+
):
454+
act = getattr(self, attr, None)
455+
if act:
456+
try:
457+
self.iface.removePluginMenu(__title__, act)
458+
except Exception:
459+
pass
460+
try:
461+
delattr(self, attr)
462+
except Exception:
463+
pass
464+
427465
# -- Clean up preferences panel in QGIS settings
428-
self.iface.unregisterOptionsWidgetFactory(self.options_factory)
429-
# -- Unregister processing
430-
QgsApplication.processingRegistry().removeProvider(self.provider)
466+
options_factory = getattr(self, "options_factory", None)
467+
if options_factory:
468+
try:
469+
self.iface.unregisterOptionsWidgetFactory(options_factory)
470+
except Exception:
471+
pass
472+
try:
473+
delattr(self, "options_factory")
474+
except Exception:
475+
pass
476+
477+
# -- Unregister processing provider
478+
provider = getattr(self, "provider", None)
479+
if provider:
480+
try:
481+
QgsApplication.processingRegistry().removeProvider(provider)
482+
except Exception:
483+
pass
484+
try:
485+
delattr(self, "provider")
486+
except Exception:
487+
pass
431488

432489
# remove from QGIS help/extensions menu
433-
if self.action_help_plugin_menu_documentation:
434-
self.iface.pluginHelpMenu().removeAction(self.action_help_plugin_menu_documentation)
435-
436-
# remove actions
437-
del self.action_settings
438-
del self.action_help
439-
del self.toolbar
440-
441-
def run(self):
442-
"""Run main process.
443-
444-
Raises
445-
------
446-
Exception
447-
If there is no item in the feed.
448-
"""
449-
try:
450-
self.log(
451-
message=self.tr("Everything ran OK."),
452-
log_level=3,
453-
push=False,
454-
)
455-
except Exception as err:
456-
self.log(
457-
message=self.tr("Houston, we've got a problem: {}".format(err)),
458-
log_level=2,
459-
push=True,
460-
)
490+
help_action = getattr(self, "action_help_plugin_menu_documentation", None)
491+
if help_action:
492+
try:
493+
self.iface.pluginHelpMenu().removeAction(help_action)
494+
except Exception:
495+
pass
496+
try:
497+
delattr(self, "action_help_plugin_menu_documentation")
498+
except Exception:
499+
pass
500+
501+
# remove toolbar if present
502+
if getattr(self, "toolbar", None):
503+
try:
504+
# There's no explicit removeToolbar API; deleting reference is fine.
505+
delattr(self, "toolbar")
506+
except Exception:
507+
pass

0 commit comments

Comments
 (0)