diff --git a/loopstructural/gui/map2loop_tools/basal_contacts_widget.py b/loopstructural/gui/map2loop_tools/basal_contacts_widget.py index d792222..d24777e 100644 --- a/loopstructural/gui/map2loop_tools/basal_contacts_widget.py +++ b/loopstructural/gui/map2loop_tools/basal_contacts_widget.py @@ -177,7 +177,7 @@ def _run_extractor(self): # Validate inputs if not self.geologyLayerComboBox.currentLayer(): QMessageBox.warning(self, "Missing Input", "Please select a geology layer.") - return + return False try: result, contact_type = self._extract_contacts() @@ -197,6 +197,7 @@ def _run_extractor(self): message=f"[map2loop] Failed to save basal contacts debug output: {err}", log_level=2, ) + return True except Exception as err: if self._debug: self._debug.plugin.log( @@ -205,6 +206,7 @@ def _run_extractor(self): ) raise err QMessageBox.critical(self, "Error", f"An error occurred: {err}") + return False def get_parameters(self): """Get current widget parameters. diff --git a/loopstructural/gui/map2loop_tools/dialogs.py b/loopstructural/gui/map2loop_tools/dialogs.py index 8837880..b335d3b 100644 --- a/loopstructural/gui/map2loop_tools/dialogs.py +++ b/loopstructural/gui/map2loop_tools/dialogs.py @@ -36,8 +36,8 @@ def setup_ui(self): def _run_and_accept(self): """Run the sampler and accept dialog if successful.""" - self.widget._run_sampler() - # Dialog stays open so user can see the result + if self.widget._run_sampler(): + self.accept() class SorterDialog(QDialog): @@ -73,7 +73,8 @@ def setup_ui(self): def _run_and_accept(self): """Run the sorter and accept dialog if successful.""" - self.widget._run_sorter() + if self.widget._run_sorter(): + self.accept() class UserDefinedSorterDialog(QDialog): @@ -101,16 +102,15 @@ def setup_ui(self): layout.addWidget(self.widget) # Replace the run button with dialog buttons - # self.widget.runButton.hide() - - # self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) - # self.button_box.accepted.connect(self._run_and_accept) - # self.button_box.rejected.connect(self.reject) - # layout.addWidget(self.button_box) + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + self.button_box.accepted.connect(self._run_and_accept) + self.button_box.rejected.connect(self.reject) + layout.addWidget(self.button_box) def _run_and_accept(self): """Run the sorter and accept dialog if successful.""" - self.widget._run_sorter() + if self.widget._run_sorter(): + self.accept() class BasalContactsDialog(QDialog): @@ -146,7 +146,8 @@ def setup_ui(self): def _run_and_accept(self): """Run the extractor and accept dialog if successful.""" - self.widget._run_extractor() + if self.widget._run_extractor(): + self.accept() class ThicknessCalculatorDialog(QDialog): @@ -182,7 +183,8 @@ def setup_ui(self): def _run_and_accept(self): """Run the calculator and accept dialog if successful.""" - self.widget._run_calculator() + if self.widget._run_calculator(): + self.accept() class PaintStratigraphicOrderDialog(QDialog): @@ -218,5 +220,5 @@ def setup_ui(self): def _run_and_accept(self): """Run the painter and accept dialog if successful.""" - self.widget._run_painter() - + if self.widget._run_painter(): + self.accept() diff --git a/loopstructural/gui/map2loop_tools/fault_topology_widget.py b/loopstructural/gui/map2loop_tools/fault_topology_widget.py index ed14caa..585c7ba 100644 --- a/loopstructural/gui/map2loop_tools/fault_topology_widget.py +++ b/loopstructural/gui/map2loop_tools/fault_topology_widget.py @@ -90,3 +90,4 @@ def _run_topology(self): # addGeoDataFrameToproject(gdf, "Input Faults") # addGeoDataFrameToproject(df, "Fault Topology Table") QMessageBox.information(self, "Success", f"Calculated fault topology for {len(df)} pairs.") + return True diff --git a/loopstructural/gui/map2loop_tools/paint_stratigraphic_order_widget.py b/loopstructural/gui/map2loop_tools/paint_stratigraphic_order_widget.py index f01a106..5ed058e 100644 --- a/loopstructural/gui/map2loop_tools/paint_stratigraphic_order_widget.py +++ b/loopstructural/gui/map2loop_tools/paint_stratigraphic_order_widget.py @@ -150,188 +150,194 @@ def _on_geology_layer_changed(self): break def _run_painter(self): - """Run the paint stratigraphic order algorithm.""" + """Run the paint stratigraphic order algorithm. + + Returns + ------- + bool + True if operation completed without unhandled exceptions, False otherwise. + """ - geology_layer = self.geologyLayerComboBox.currentLayer() - unit_name_field = self.unitNameFieldComboBox.currentField() - stratigraphic_order = self.data_manager.stratigraphic_order = ( - self.data_manager.get_stratigraphic_unit_names() if self.data_manager else [] - ) - paint_stratigraphic_order( - geology_layer, stratigraphic_order, unit_name_field, debug_manager=self._debug - ) - - # If requested, duplicate layer and apply style using selected colour ramp try: - duplicate = ( - getattr(self, 'duplicateLayerCheckBox', None) - and self.duplicateLayerCheckBox.isChecked() + geology_layer = self.geologyLayerComboBox.currentLayer() + unit_name_field = self.unitNameFieldComboBox.currentField() + stratigraphic_order = self.data_manager.stratigraphic_order = ( + self.data_manager.get_stratigraphic_unit_names() if self.data_manager else [] + ) + paint_stratigraphic_order( + geology_layer, stratigraphic_order, unit_name_field, debug_manager=self._debug ) - except Exception: - duplicate = False - if duplicate: - # Get chosen ramp name + # If requested, duplicate layer and apply style using selected colour ramp try: - ramp_name = self.colorRampComboBox.currentText() + duplicate = ( + getattr(self, 'duplicateLayerCheckBox', None) + and self.duplicateLayerCheckBox.isChecked() + ) except Exception: - ramp_name = None + duplicate = False + + if duplicate: + # Get chosen ramp name + try: + ramp_name = self.colorRampComboBox.currentText() + except Exception: + ramp_name = None + + # Step 1: create a memory copy of the geology layer and copy attributes/geometry + try: + from PyQt5.QtCore import QVariant + from qgis.core import ( + QgsFeature, + QgsField, + QgsGraduatedSymbolRenderer, + QgsProject, + QgsRendererRange, + QgsStyle, + QgsSymbol, + QgsVectorLayer, + QgsWkbTypes, + ) - # Step 1: create a memory copy of the geology layer and copy attributes/geometry - try: - from PyQt5.QtCore import QVariant - from qgis.core import ( - QgsFeature, - QgsField, - QgsGraduatedSymbolRenderer, - QgsProject, - QgsRendererRange, - QgsStyle, - QgsSymbol, - QgsVectorLayer, - QgsWkbTypes, - ) + geom_type = QgsWkbTypes.displayString(geology_layer.wkbType()) + crs_auth = ( + geology_layer.crs().authid() if hasattr(geology_layer, 'crs') else None + ) + uri = f"{geom_type}?crs={crs_auth}" if crs_auth else f"{geom_type}" + mem_layer = QgsVectorLayer(uri, f"{geology_layer.name()}_strat", "memory") - geom_type = QgsWkbTypes.displayString(geology_layer.wkbType()) - crs_auth = geology_layer.crs().authid() if hasattr(geology_layer, 'crs') else None - uri = f"{geom_type}?crs={crs_auth}" if crs_auth else f"{geom_type}" - mem_layer = QgsVectorLayer(uri, f"{geology_layer.name()}_strat", "memory") - - mem_dp = mem_layer.dataProvider() - mem_dp.addAttributes(list(geology_layer.fields())) - mem_layer.updateFields() - - # copy each feature and its attributes explicitly - src_field_names = [f.name() for f in geology_layer.fields()] - new_feats = [] - for src_feat in geology_layer.getFeatures(): - nf = QgsFeature() - nf.setGeometry(src_feat.geometry()) - nf.setFields(mem_layer.fields()) - attrs = [] - for f in mem_layer.fields(): - fname = f.name() - if fname in src_field_names: - try: - attrs.append(src_feat[fname]) - except Exception: - attrs.append(None) - else: - attrs.append(None) - nf.setAttributes(attrs) - new_feats.append(nf) - mem_dp.addFeatures(new_feats) - mem_layer.updateExtents() - QgsProject.instance().addMapLayer(mem_layer) - except Exception as e: - QMessageBox.warning(self, 'Duplicate Layer', f'Failed to create copy: {e}') - return - - # Step 2: ensure 'strat_order' exists on memory layer and populate numeric values by matching geometries - field_name = 'strat_order' - try: - if field_name not in [f.name() for f in mem_layer.fields()]: - mem_layer.startEditing() - mem_dp.addAttributes([QgsField(field_name, QVariant.Int)]) + mem_dp = mem_layer.dataProvider() + mem_dp.addAttributes(list(geology_layer.fields())) mem_layer.updateFields() - mem_layer.commitChanges() - - # build mapping from geometry WKB -> numeric strat value from original layer - geom_to_val = {} - for of in geology_layer.getFeatures(): - try: - raw = of[field_name] - except Exception: - raw = None - if raw is None: - continue - try: - val = int(raw) - except Exception: + + # copy each feature and its attributes explicitly + src_field_names = [f.name() for f in geology_layer.fields()] + new_feats = [] + for src_feat in geology_layer.getFeatures(): + nf = QgsFeature() + nf.setGeometry(src_feat.geometry()) + nf.setFields(mem_layer.fields()) + attrs = [] + for f in mem_layer.fields(): + fname = f.name() + if fname in src_field_names: + try: + attrs.append(src_feat[fname]) + except Exception: + attrs.append(None) + else: + attrs.append(None) + nf.setAttributes(attrs) + new_feats.append(nf) + mem_dp.addFeatures(new_feats) + mem_layer.updateExtents() + QgsProject.instance().addMapLayer(mem_layer) + except Exception as e: + QMessageBox.warning(self, 'Duplicate Layer', f'Failed to create copy: {e}') + return False + + # Step 2: ensure 'strat_order' exists on memory layer and populate numeric values by matching geometries + field_name = 'strat_order' + try: + if field_name not in [f.name() for f in mem_layer.fields()]: + mem_layer.startEditing() + mem_dp.addAttributes([QgsField(field_name, QVariant.Int)]) + mem_layer.updateFields() + mem_layer.commitChanges() + + # build mapping from geometry WKB -> numeric strat value from original layer + geom_to_val = {} + for of in geology_layer.getFeatures(): try: - val = int(float(raw)) + raw = of[field_name] except Exception: + raw = None + if raw is None: continue - try: - geom_to_val[of.geometry().asWkb()] = val - except Exception: - # fallback to WKT if asWkb unavailable try: - geom_to_val[of.geometry().asWkt()] = val + val = int(raw) except Exception: - continue - - if geom_to_val: - mem_layer.startEditing() - strat_idx = mem_layer.fields().indexFromName(field_name) - for mf in mem_layer.getFeatures(): + try: + val = int(float(raw)) + except Exception: + continue try: - key = mf.geometry().asWkb() + geom_to_val[of.geometry().asWkb()] = val except Exception: try: - key = mf.geometry().asWkt() + geom_to_val[of.geometry().asWkt()] = val except Exception: - key = None - if key is None: - continue - val = geom_to_val.get(key, None) - if val is not None: - mem_layer.changeAttributeValue(mf.id(), strat_idx, int(val)) - mem_layer.commitChanges() - except Exception: - # continue; styling will fallback if numeric data missing - pass + continue - # Step 3: build graduated renderer using explicit ranges per unique strat value - try: - vals = set() - for f in mem_layer.getFeatures(): - try: - v = f[field_name] - except Exception: - v = None - if v is None: - continue - try: - vals.add(float(v)) - except Exception: - continue - unique_vals = sorted(vals) - - if not unique_vals: - QMessageBox.information( - self, - 'Styling', - "No 'strat_order' values found on duplicated layer; leaving default styling.", - ) - else: - from qgis.core import QgsRendererRange - - ramp = QgsStyle().defaultStyle().colorRamp(ramp_name) if ramp_name else None - ranges = [] - n = len(unique_vals) - for i, v in enumerate(unique_vals): - lower = v - 0.5 - upper = v + 0.5 - symbol = QgsSymbol.defaultSymbol(mem_layer.geometryType()) - if ramp: + if geom_to_val: + mem_layer.startEditing() + strat_idx = mem_layer.fields().indexFromName(field_name) + for mf in mem_layer.getFeatures(): try: - color = ramp.color(i / (n - 1) if n > 1 else 0) - symbol.setColor(color) + key = mf.geometry().asWkb() except Exception: - pass - label = str(int(v)) if float(v).is_integer() else str(v) - ranges.append(QgsRendererRange(lower, upper, symbol, label)) - renderer = QgsGraduatedSymbolRenderer(field_name, ranges) - mem_layer.setRenderer(renderer) - mem_layer.triggerRepaint() - except Exception as e: - QMessageBox.warning( - self, 'Duplicate Layer', f'Failed to apply graduated styling: {e}' - ) + try: + key = mf.geometry().asWkt() + except Exception: + key = None + if key is None: + continue + val = geom_to_val.get(key, None) + if val is not None: + mem_layer.changeAttributeValue(mf.id(), strat_idx, int(val)) + mem_layer.commitChanges() + except Exception: + # continue; styling will fallback if numeric data missing + pass + + # Step 3: build graduated renderer using explicit ranges per unique strat value + try: + vals = set() + for f in mem_layer.getFeatures(): + try: + v = f[field_name] + except Exception: + v = None + if v is None: + continue + try: + vals.add(float(v)) + except Exception: + continue + unique_vals = sorted(vals) + + if not unique_vals: + QMessageBox.information( + self, + 'Styling', + "No 'strat_order' values found on duplicated layer; leaving default styling.", + ) + else: + ramp = QgsStyle().defaultStyle().colorRamp(ramp_name) if ramp_name else None + ranges = [] + n = len(unique_vals) + for i, v in enumerate(unique_vals): + lower = v - 0.5 + upper = v + 0.5 + symbol = QgsSymbol.defaultSymbol(mem_layer.geometryType()) + if ramp: + try: + color = ramp.color(i / (n - 1) if n > 1 else 0) + symbol.setColor(color) + except Exception: + pass + label = str(int(v)) if float(v).is_integer() else str(v) + ranges.append(QgsRendererRange(lower, upper, symbol, label)) + renderer = QgsGraduatedSymbolRenderer(field_name, ranges) + mem_layer.setRenderer(renderer) + mem_layer.triggerRepaint() + except Exception as e: + QMessageBox.warning( + self, 'Duplicate Layer', f'Failed to apply graduated styling: {e}' + ) + + except Exception as e: + QMessageBox.warning(self, 'Paint Stratigraphic Order', f'Operation failed: {e}') + return False - # QMessageBox.information( - # self, - # "Paint Stratigraphic Order", - # "Stratigraphic order has been painted onto the geology layer.", - # ) + return True diff --git a/loopstructural/gui/map2loop_tools/sampler_widget.py b/loopstructural/gui/map2loop_tools/sampler_widget.py index 1b80306..311e1f9 100644 --- a/loopstructural/gui/map2loop_tools/sampler_widget.py +++ b/loopstructural/gui/map2loop_tools/sampler_widget.py @@ -149,7 +149,7 @@ def _run_sampler(self): # Validate inputs if not self.spatialDataLayerComboBox.currentLayer(): QMessageBox.warning(self, "Missing Input", "Please select a spatial data layer.") - return + return False sampler_type = self.samplerTypeComboBox.currentText() @@ -158,10 +158,10 @@ def _run_sampler(self): QMessageBox.warning( self, "Missing Input", "Geology layer is required for Decimator." ) - return + return False if not self.dtmLayerComboBox.currentLayer(): QMessageBox.warning(self, "Missing Input", "DTM layer is required for Decimator.") - return + return False # Run the sampler API try: @@ -274,6 +274,8 @@ def _run_sampler(self): ) else: QMessageBox.warning(self, "Warning", "No samples were generated.") + return False + return True except Exception as e: if self._debug: @@ -284,6 +286,7 @@ def _run_sampler(self): if PlgOptionsManager.get_debug_mode(): raise e QMessageBox.critical(self, "Error", f"An error occurred: {str(e)}") + return False def get_parameters(self): """Get current widget parameters. diff --git a/loopstructural/gui/map2loop_tools/sorter_widget.py b/loopstructural/gui/map2loop_tools/sorter_widget.py index 71f2845..367991d 100644 --- a/loopstructural/gui/map2loop_tools/sorter_widget.py +++ b/loopstructural/gui/map2loop_tools/sorter_widget.py @@ -288,11 +288,11 @@ def _run_sorter(self): # Validate inputs if not self.geologyLayerComboBox.currentLayer(): QMessageBox.warning(self, "Missing Input", "Please select a geology layer.") - return + return False if not self.contactsLayerComboBox.currentLayer(): QMessageBox.warning(self, "Missing Input", "Please select a contacts layer.") - return + return False algorithm_index = self.sortingAlgorithmComboBox.currentIndex() algorithm_name = self.sorting_algorithms[algorithm_index] @@ -305,12 +305,12 @@ def _run_sorter(self): "Missing Input", "Structure layer is required for observation projections.", ) - return + return False if not self.dtmLayerComboBox.currentLayer(): QMessageBox.warning( self, "Missing Input", "DTM layer is required for observation projections." ) - return + return False # Run the sorter API try: @@ -366,6 +366,7 @@ def _run_sorter(self): ) else: QMessageBox.warning(self, "Error", "Failed to create stratigraphic column.") + return True except Exception as e: if self._debug: @@ -376,6 +377,7 @@ def _run_sorter(self): if PlgOptionsManager.get_debug_mode(): raise e QMessageBox.critical(self, "Error", f"An error occurred: {str(e)}") + return False def get_parameters(self): """Get current widget parameters. diff --git a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py index b2890a6..4d26573 100644 --- a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py +++ b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py @@ -221,21 +221,21 @@ def _run_calculator(self): # Validate inputs if not self.geologyLayerComboBox.currentLayer(): QMessageBox.warning(self, "Missing Input", "Please select a geology layer.") - return + return False if not self.basalContactsComboBox.currentLayer(): QMessageBox.warning(self, "Missing Input", "Please select a basal contacts layer.") - return + return False if not self.sampledContactsComboBox.currentLayer(): QMessageBox.warning(self, "Missing Input", "Please select a sampled contacts layer.") - return + return False if not self.structureLayerComboBox.currentLayer(): QMessageBox.warning( self, "Missing Input", "Please select a structure/orientation layer." ) - return + return False calculator_type = self.calculatorTypeComboBox.currentText() @@ -316,6 +316,8 @@ def _run_calculator(self): ) else: QMessageBox.warning(self, "Error", "No thickness data was calculated.") + return False + return True except Exception as e: if self._debug: @@ -326,6 +328,7 @@ def _run_calculator(self): if PlgOptionsManager.get_debug_mode(): raise e QMessageBox.critical(self, "Error", f"An error occurred: {str(e)}") + return False def get_parameters(self): """Get current widget parameters. diff --git a/loopstructural/processing/provider.py b/loopstructural/processing/provider.py index 93550a3..1205def 100644 --- a/loopstructural/processing/provider.py +++ b/loopstructural/processing/provider.py @@ -16,7 +16,6 @@ from .algorithms import ( BasalContactsAlgorithm, - PaintStratigraphicOrderAlgorithm, SamplerAlgorithm, StratigraphySorterAlgorithm, ThicknessCalculatorAlgorithm,