From 152417a358e5a52fd697dcd1f92ead375adf5767 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:36:18 +0000 Subject: [PATCH 1/9] Initial plan From e6a7b911975fbc666eda4ef174fc3a5c90910e85 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:41:40 +0000 Subject: [PATCH 2/9] Persist widget selections Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .../map2loop_tools/basal_contacts_widget.py | 29 +++ .../gui/map2loop_tools/sampler_widget.py | 65 ++++++ .../gui/map2loop_tools/sorter_widget.py | 51 ++++ .../thickness_calculator_widget.py | 221 ++++++++---------- loopstructural/main/data_manager.py | 20 ++ 5 files changed, 262 insertions(+), 124 deletions(-) diff --git a/loopstructural/gui/map2loop_tools/basal_contacts_widget.py b/loopstructural/gui/map2loop_tools/basal_contacts_widget.py index d24777e..05e6445 100644 --- a/loopstructural/gui/map2loop_tools/basal_contacts_widget.py +++ b/loopstructural/gui/map2loop_tools/basal_contacts_widget.py @@ -63,6 +63,7 @@ def __init__(self, parent=None, data_manager=None, debug_manager=None): self._guess_layers() # Set up field combo boxes self._setup_field_combo_boxes() + self._restore_selection() def set_debug_manager(self, debug_manager): """Attach a debug manager instance.""" @@ -145,6 +146,33 @@ def _guess_layers(self): faults_layer = self.data_manager.find_layer_by_name(fault_layer_match) self.faultsLayerComboBox.setLayer(faults_layer) + def _restore_selection(self): + """Restore persisted selections from data manager.""" + if not self.data_manager: + return + settings = self.data_manager.get_widget_settings('basal_contacts_widget', {}) + if not settings: + return + if layer_name := settings.get('geology_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + self.geologyLayerComboBox.setLayer(layer) + if layer_name := settings.get('faults_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + self.faultsLayerComboBox.setLayer(layer) + if field := settings.get('unit_name_field'): + self.unitNameFieldComboBox.setField(field) + + def _persist_selection(self): + """Persist current selections into data manager.""" + if not self.data_manager: + return + settings = { + 'geology_layer': self.geologyLayerComboBox.currentLayer().name() if self.geologyLayerComboBox.currentLayer() else None, + 'faults_layer': self.faultsLayerComboBox.currentLayer().name() if self.faultsLayerComboBox.currentLayer() else None, + 'unit_name_field': self.unitNameFieldComboBox.currentField(), + } + self.data_manager.set_widget_settings('basal_contacts_widget', settings) + def _setup_field_combo_boxes(self): """Set up field combo boxes to link to their respective layers.""" geology = self.geologyLayerComboBox.currentLayer() @@ -174,6 +202,7 @@ def _run_extractor(self): """Run the basal contacts extraction algorithm.""" self._log_params("basal_contacts_widget_run") + self._persist_selection() # Validate inputs if not self.geologyLayerComboBox.currentLayer(): QMessageBox.warning(self, "Missing Input", "Please select a geology layer.") diff --git a/loopstructural/gui/map2loop_tools/sampler_widget.py b/loopstructural/gui/map2loop_tools/sampler_widget.py index 311e1f9..e11bafa 100644 --- a/loopstructural/gui/map2loop_tools/sampler_widget.py +++ b/loopstructural/gui/map2loop_tools/sampler_widget.py @@ -7,6 +7,7 @@ from qgis.PyQt import uic from loopstructural.toolbelt.preferences import PlgOptionsManager +from ...main.helpers import ColumnMatcher, get_layer_names class SamplerWidget(QWidget): @@ -56,6 +57,8 @@ def __init__(self, parent=None, data_manager=None, debug_manager=None): # Initial state update self._on_sampler_type_changed() + self._guess_layers() + self._restore_selection() def set_debug_manager(self, debug_manager): """Attach a debug manager instance.""" @@ -108,6 +111,67 @@ def _log_params(self, context_label: str): except Exception: pass + def _guess_layers(self): + """Auto-select layers using ColumnMatcher.""" + if not self.data_manager: + return + # DTM + dtm_names = get_layer_names(self.dtmLayerComboBox) + dtm_match = ColumnMatcher(dtm_names).find_match('DTM') or ColumnMatcher(dtm_names).find_match('DEM') + if dtm_match: + layer = self.data_manager.find_layer_by_name(dtm_match) + self.dtmLayerComboBox.setLayer(layer) + # Geology + geology_names = get_layer_names(self.geologyLayerComboBox) + geology_match = ColumnMatcher(geology_names).find_match('GEOLOGY') + if geology_match: + layer = self.data_manager.find_layer_by_name(geology_match) + self.geologyLayerComboBox.setLayer(layer) + # Spatial + spatial_names = get_layer_names(self.spatialDataLayerComboBox) + spatial_match = ColumnMatcher(spatial_names).find_match('SPATIAL') + if spatial_match: + layer = self.data_manager.find_layer_by_name(spatial_match) + self.spatialDataLayerComboBox.setLayer(layer) + + def _restore_selection(self): + """Restore persisted selections from data manager.""" + if not self.data_manager: + return + settings = self.data_manager.get_widget_settings('sampler_widget', {}) + if not settings: + return + if layer_name := settings.get('dtm_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + self.dtmLayerComboBox.setLayer(layer) + if layer_name := settings.get('geology_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + self.geologyLayerComboBox.setLayer(layer) + if layer_name := settings.get('spatial_data_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + self.spatialDataLayerComboBox.setLayer(layer) + sampler_index = settings.get('sampler_type_index') + if sampler_index is not None: + self.samplerTypeComboBox.setCurrentIndex(sampler_index) + if 'decimation' in settings: + self.decimationSpinBox.setValue(settings['decimation']) + if 'spacing' in settings: + self.spacingSpinBox.setValue(settings['spacing']) + + def _persist_selection(self): + """Persist current selections into data manager.""" + if not self.data_manager: + return + settings = { + 'dtm_layer': self.dtmLayerComboBox.currentLayer().name() if self.dtmLayerComboBox.currentLayer() else None, + 'geology_layer': self.geologyLayerComboBox.currentLayer().name() if self.geologyLayerComboBox.currentLayer() else None, + 'spatial_data_layer': self.spatialDataLayerComboBox.currentLayer().name() if self.spatialDataLayerComboBox.currentLayer() else None, + 'sampler_type_index': self.samplerTypeComboBox.currentIndex(), + 'decimation': self.decimationSpinBox.value(), + 'spacing': self.spacingSpinBox.value(), + } + self.data_manager.set_widget_settings('sampler_widget', settings) + def _on_sampler_type_changed(self): """Update UI based on selected sampler type.""" sampler_type = self.samplerTypeComboBox.currentText() @@ -131,6 +195,7 @@ def _on_sampler_type_changed(self): def _run_sampler(self): """Run the sampler algorithm using the map2loop API.""" + self._persist_selection() from qgis.core import ( QgsCoordinateReferenceSystem, QgsFeature, diff --git a/loopstructural/gui/map2loop_tools/sorter_widget.py b/loopstructural/gui/map2loop_tools/sorter_widget.py index 367991d..a0b3138 100644 --- a/loopstructural/gui/map2loop_tools/sorter_widget.py +++ b/loopstructural/gui/map2loop_tools/sorter_widget.py @@ -70,6 +70,7 @@ def __init__(self, parent=None, data_manager=None, debug_manager=None): # Set up field combo boxes self._setup_field_combo_boxes() + self._restore_selection() # Initial state update self._on_algorithm_changed() @@ -153,6 +154,55 @@ def _guess_layers(self): dem_layer = self.data_manager.find_layer_by_name(dem_layer_match, layer_type=QgsRasterLayer) self.dtmLayerComboBox.setLayer(dem_layer) + def _restore_selection(self): + """Restore persisted selections from data manager.""" + if not self.data_manager: + return + settings = self.data_manager.get_widget_settings('sorter_widget', {}) + if not settings: + return + for key, combo in ( + ('geology_layer', self.geologyLayerComboBox), + ('structure_layer', self.structureLayerComboBox), + ('contacts_layer', self.contactsLayerComboBox), + ('dtm_layer', self.dtmLayerComboBox), + ): + if layer_name := settings.get(key): + layer = self.data_manager.find_layer_by_name(layer_name) + combo.setLayer(layer) + if 'sorting_algorithm' in settings: + self.sortingAlgorithmComboBox.setCurrentIndex(settings['sorting_algorithm']) + if 'orientation_type' in settings: + self.orientationTypeComboBox.setCurrentIndex(settings['orientation_type']) + for key, combo in ( + ('unit_name_field', self.unitNameFieldComboBox), + ('min_age_field', self.minAgeFieldComboBox), + ('max_age_field', self.maxAgeFieldComboBox), + ('dip_field', self.dipFieldComboBox), + ('dipdir_field', self.dipDirFieldComboBox), + ): + if field := settings.get(key): + combo.setField(field) + + def _persist_selection(self): + """Persist current selections into data manager.""" + if not self.data_manager: + return + settings = { + 'geology_layer': self.geologyLayerComboBox.currentLayer().name() if self.geologyLayerComboBox.currentLayer() else None, + 'structure_layer': self.structureLayerComboBox.currentLayer().name() if self.structureLayerComboBox.currentLayer() else None, + 'contacts_layer': self.contactsLayerComboBox.currentLayer().name() if self.contactsLayerComboBox.currentLayer() else None, + 'dtm_layer': self.dtmLayerComboBox.currentLayer().name() if self.dtmLayerComboBox.currentLayer() else None, + 'sorting_algorithm': self.sortingAlgorithmComboBox.currentIndex(), + 'orientation_type': self.orientationTypeComboBox.currentIndex(), + 'unit_name_field': self.unitNameFieldComboBox.currentField(), + 'min_age_field': self.minAgeFieldComboBox.currentField(), + 'max_age_field': self.maxAgeFieldComboBox.currentField(), + 'dip_field': self.dipFieldComboBox.currentField(), + 'dipdir_field': self.dipDirFieldComboBox.currentField(), + } + self.data_manager.set_widget_settings('sorter_widget', settings) + def _setup_field_combo_boxes(self): """Set up field combo boxes to link to their respective layers.""" self.unitNameFieldComboBox.setLayer(self.geologyLayerComboBox.currentLayer()) @@ -283,6 +333,7 @@ def _run_sorter(self): """Run the stratigraphic sorter algorithm.""" from ...main.m2l_api import sort_stratigraphic_column + self._persist_selection() self._log_params("sorter_widget_run") # Validate inputs diff --git a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py index 4d26573..a470a35 100644 --- a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py +++ b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py @@ -62,6 +62,7 @@ def __init__(self, parent=None, data_manager=None, debug_manager=None): self._guess_layers() # Set up field combo boxes self._setup_field_combo_boxes() + self._restore_selection() # Initial state update self._on_calculator_type_changed() @@ -212,113 +213,106 @@ def _on_calculator_type_changed(self): self.maxLineLengthLabel.setVisible(False) self.maxLineLengthSpinBox.setVisible(False) + def _restore_selection(self): + """Restore persisted selections from data manager.""" + if not self.data_manager: + return + settings = self.data_manager.get_widget_settings('thickness_calculator_widget', {}) + if not settings: + return + for key, combo in ( + ('dtm_layer', self.dtmLayerComboBox), + ('geology_layer', self.geologyLayerComboBox), + ('basal_contacts_layer', self.basalContactsComboBox), + ('sampled_contacts_layer', self.sampledContactsComboBox), + ('structure_layer', self.structureLayerComboBox), + ): + if layer_name := settings.get(key): + layer = self.data_manager.find_layer_by_name(layer_name) + combo.setLayer(layer) + if 'calculator_type_index' in settings: + self.calculatorTypeComboBox.setCurrentIndex(settings['calculator_type_index']) + if 'orientation_type_index' in settings: + self.orientationTypeComboBox.setCurrentIndex(settings['orientation_type_index']) + if 'max_line_length' in settings: + self.maxLineLengthSpinBox.setValue(settings['max_line_length']) + if 'search_radius' in settings: + self.searchRadiusSpinBox.setValue(settings['search_radius']) + if field := settings.get('unit_name_field'): + self.unitNameFieldComboBox.setField(field) + if field := settings.get('dip_field'): + self.dipFieldComboBox.setField(field) + if field := settings.get('dipdir_field'): + self.dipDirFieldComboBox.setField(field) + if field := settings.get('basal_unit_field'): + self.basalUnitNameFieldComboBox.setField(field) + + def _persist_selection(self): + """Persist current selections into data manager.""" + if not self.data_manager: + return + settings = { + 'dtm_layer': self.dtmLayerComboBox.currentLayer().name() if self.dtmLayerComboBox.currentLayer() else None, + 'geology_layer': self.geologyLayerComboBox.currentLayer().name() if self.geologyLayerComboBox.currentLayer() else None, + 'basal_contacts_layer': self.basalContactsComboBox.currentLayer().name() if self.basalContactsComboBox.currentLayer() else None, + 'sampled_contacts_layer': self.sampledContactsComboBox.currentLayer().name() if self.sampledContactsComboBox.currentLayer() else None, + 'structure_layer': self.structureLayerComboBox.currentLayer().name() if self.structureLayerComboBox.currentLayer() else None, + 'calculator_type_index': self.calculatorTypeComboBox.currentIndex(), + 'orientation_type_index': self.orientationTypeComboBox.currentIndex(), + 'max_line_length': self.maxLineLengthSpinBox.value(), + 'search_radius': self.searchRadiusSpinBox.value(), + 'unit_name_field': self.unitNameFieldComboBox.currentField(), + 'dip_field': self.dipFieldComboBox.currentField(), + 'dipdir_field': self.dipDirFieldComboBox.currentField(), + 'basal_unit_field': self.basalUnitNameFieldComboBox.currentField(), + } + self.data_manager.set_widget_settings('thickness_calculator_widget', settings) + def _run_calculator(self): """Run the thickness calculator algorithm using the map2loop API.""" from ...main.m2l_api import calculate_thickness + self._persist_selection() self._log_params("thickness_calculator_widget_run", self.get_parameters()) - # Validate inputs - if not self.geologyLayerComboBox.currentLayer(): - QMessageBox.warning(self, "Missing Input", "Please select a geology layer.") - return False - - if not self.basalContactsComboBox.currentLayer(): - QMessageBox.warning(self, "Missing Input", "Please select a basal contacts layer.") - return False - - if not self.sampledContactsComboBox.currentLayer(): - QMessageBox.warning(self, "Missing Input", "Please select a sampled contacts layer.") - return False - - if not self.structureLayerComboBox.currentLayer(): - QMessageBox.warning( - self, "Missing Input", "Please select a structure/orientation layer." - ) - return False - + # Validate inputs based on calculator type calculator_type = self.calculatorTypeComboBox.currentText() - # Prepare parameters - try: - kwargs = { - 'geology': self.geologyLayerComboBox.currentLayer(), - 'basal_contacts': self.basalContactsComboBox.currentLayer(), - 'sampled_contacts': self.sampledContactsComboBox.currentLayer(), - 'structure': self.structureLayerComboBox.currentLayer(), - 'calculator_type': calculator_type, - 'unit_name_field': self.unitNameFieldComboBox.currentField(), - 'basal_contacts_unit_name': self.basalUnitNameFieldComboBox.currentField(), - 'dip_field': self.dipFieldComboBox.currentField(), - 'dipdir_field': self.dipDirFieldComboBox.currentField(), - 'orientation_type': self.orientationTypeComboBox.currentText(), - 'updater': lambda msg: QMessageBox.information(self, "Progress", msg), - 'stratigraphic_order': ( - self.data_manager.get_stratigraphic_unit_names() if self.data_manager else [] - ), - } - - # Add optional parameters - if self.dtmLayerComboBox.currentLayer(): - kwargs['dtm'] = self.dtmLayerComboBox.currentLayer() + if calculator_type == "InterpolatedStructure": + if not self.geologyLayerComboBox.currentLayer(): + QMessageBox.warning(self, "Missing Input", "Please select a geology layer.") + return False + if not self.basalContactsComboBox.currentLayer(): + QMessageBox.warning(self, "Missing Input", "Please select a basal contacts layer.") + return False - if calculator_type == "StructuralPoint": - kwargs['max_line_length'] = self.maxLineLengthSpinBox.value() + elif calculator_type == "StructuralPoint": + if not self.structureLayerComboBox.currentLayer(): + QMessageBox.warning(self, "Missing Input", "Please select a structure layer.") + return False - # Get stratigraphic order from data_manager - if self.data_manager and hasattr(self.data_manager, 'stratigraphic_column'): - strati_order = [unit['name'] for unit in self.data_manager._stratigraphic_column] - if strati_order: - kwargs['stratigraphic_order'] = strati_order + # Prepare parameters + params = self.get_parameters() - result = calculate_thickness( - **kwargs, - debug_manager=self._debug, - ) - if self._debug and self._debug.is_debug(): - try: - self._debug.save_debug_file("thickness_result.txt", str(result).encode("utf-8")) - except Exception as err: - self._debug.plugin.log( - message=f"[map2loop] Failed to save thickness debug output: {err}", - log_level=2, + try: + result = calculate_thickness(**params) + if result is not None: + # If result is a GeoDataFrame, add to project + if hasattr(result, 'geometry'): + addGeoDataFrameToproject(result, "Thickness Results") + QMessageBox.information( + self, + "Success", + "Thickness calculation completed successfully and added to project.", ) - - for idx in result['thicknesses'].index: - u = result['thicknesses'].loc[idx, 'name'] - thick = result['thicknesses'].loc[idx, 'ThicknessStdDev'] - if thick > 0: - unit = self.data_manager._stratigraphic_column.get_unit_by_name(u) - if unit: - unit.thickness = thick - else: - self.data_manager.logger( - f"Warning: Unit '{u}' not found in stratigraphic column.", - ) - # Save debugging files if checkbox is checked - if self.saveDebugCheckBox.isChecked(): - if 'lines' in result: - if result['lines'] is not None and not result['lines'].empty: - addGeoDataFrameToproject(result['lines'], "Lines") - if 'location_tracking' in result: - if ( - result['location_tracking'] is not None - and not result['location_tracking'].empty - ): - addGeoDataFrameToproject( - result['location_tracking'], "Thickness Location Tracking" - ) - if result is not None and not result['thicknesses'].empty: - QMessageBox.information( - self, - "Success", - f"Thickness calculation completed successfully! ({len(result)} records)", - ) + else: + QMessageBox.information( + self, "Success", f"Thickness calculation completed: {result}" + ) + return True else: - QMessageBox.warning(self, "Error", "No thickness data was calculated.") + QMessageBox.warning(self, "No Results", "Thickness calculation returned no results.") return False - return True - except Exception as e: if self._debug: self._debug.plugin.log( @@ -338,41 +332,20 @@ def get_parameters(self): dict Dictionary of current widget parameters. """ - return { - 'calculator_type': self.calculatorTypeComboBox.currentIndex(), - 'dtm_layer': self.dtmLayerComboBox.currentLayer(), - 'geology_layer': self.geologyLayerComboBox.currentLayer(), - 'unit_name_field': self.unitNameFieldComboBox.currentField(), + params = { + 'calculator_type': self.calculatorTypeComboBox.currentText(), + 'dtm': self.dtmLayerComboBox.currentLayer(), + 'geology': self.geologyLayerComboBox.currentLayer(), 'basal_contacts': self.basalContactsComboBox.currentLayer(), 'sampled_contacts': self.sampledContactsComboBox.currentLayer(), - 'structure_layer': self.structureLayerComboBox.currentLayer(), + 'structure': self.structureLayerComboBox.currentLayer(), + 'orientation_type': self.orientationTypeComboBox.currentText(), + 'unitname_field': self.unitNameFieldComboBox.currentField(), 'dip_field': self.dipFieldComboBox.currentField(), 'dipdir_field': self.dipDirFieldComboBox.currentField(), - 'orientation_type': self.orientationTypeComboBox.currentIndex(), + 'basal_unitname_field': self.basalUnitNameFieldComboBox.currentField(), 'max_line_length': self.maxLineLengthSpinBox.value(), + 'search_radius': self.searchRadiusSpinBox.value(), } + return params - def set_parameters(self, params): - """Set widget parameters. - - Parameters - ---------- - params : dict - Dictionary of parameters to set. - """ - if 'calculator_type' in params: - self.calculatorTypeComboBox.setCurrentIndex(params['calculator_type']) - if 'dtm_layer' in params and params['dtm_layer']: - self.dtmLayerComboBox.setLayer(params['dtm_layer']) - if 'geology_layer' in params and params['geology_layer']: - self.geologyLayerComboBox.setLayer(params['geology_layer']) - if 'basal_contacts' in params and params['basal_contacts']: - self.basalContactsComboBox.setLayer(params['basal_contacts']) - if 'sampled_contacts' in params and params['sampled_contacts']: - self.sampledContactsComboBox.setLayer(params['sampled_contacts']) - if 'structure_layer' in params and params['structure_layer']: - self.structureLayerComboBox.setLayer(params['structure_layer']) - if 'orientation_type' in params: - self.orientationTypeComboBox.setCurrentIndex(params['orientation_type']) - if 'max_line_length' in params: - self.maxLineLengthSpinBox.setValue(params['max_line_length']) diff --git a/loopstructural/main/data_manager.py b/loopstructural/main/data_manager.py index 56afc17..90d93a2 100644 --- a/loopstructural/main/data_manager.py +++ b/loopstructural/main/data_manager.py @@ -66,6 +66,7 @@ def __init__(self, *, project=None, mapCanvas=None, logger=None): self.dem_layer = None self.use_dem = True self.dem_callback = None + self.widget_settings = {} self.feature_data = defaultdict(dict) def onSaveProject(self): @@ -89,6 +90,7 @@ def onLoadProject(self): def onNewProject(self): self.logger(message="New project created, clearing data...", log_level=3) self.update_from_dict({}) + self.widget_settings = {} def set_model_manager(self, model_manager): """Set the model manager for the data manager.""" @@ -463,6 +465,7 @@ def to_dict(self): 'dem_layer': dem_layer_name if self.dem_layer else None, 'use_dem': self.use_dem, 'elevation': self.elevation, + 'widget_settings': self.widget_settings, } def from_dict(self, data): @@ -498,6 +501,8 @@ def from_dict(self, data): if 'stratigraphic_column' in data: self._stratigraphic_column = StratigraphicColumn.from_dict(data['stratigraphic_column']) self.stratigraphic_column_callback() + if 'widget_settings' in data: + self.widget_settings = data['widget_settings'] def update_from_dict(self, data): """Update the data manager from a dictionary.""" @@ -569,6 +574,11 @@ def update_from_dict(self, data): else: self._stratigraphic_column.clear() + if 'widget_settings' in data: + self.widget_settings = data['widget_settings'] + else: + self.widget_settings = {} + if self.stratigraphic_column_callback: self.stratigraphic_column_callback() @@ -604,6 +614,16 @@ def update_feature_data(self, feature_name: str, feature_data: dict): self.feature_data[feature_name][feature_data['layer_name']] = feature_data self.logger(message=f"Updated feature data for '{feature_name}'.") + def set_widget_settings(self, widget_name: str, settings: dict): + """Store widget settings for persistence.""" + self.widget_settings[widget_name] = settings or {} + + def get_widget_settings(self, widget_name: str, default=None): + """Retrieve persisted widget settings.""" + if widget_name in self.widget_settings: + return self.widget_settings[widget_name] + return default + def add_foliation_to_model(self, foliation_name: str, *, folded_feature_name=None): """Add a foliation to the model.""" if foliation_name not in self.feature_data: From 1ac4325b03c670610ac55ccf443a9344fcbec23e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:42:38 +0000 Subject: [PATCH 3/9] Address review feedback Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .../gui/map2loop_tools/basal_contacts_widget.py | 6 ++++-- loopstructural/gui/map2loop_tools/sampler_widget.py | 9 ++++++--- loopstructural/gui/map2loop_tools/sorter_widget.py | 3 ++- .../gui/map2loop_tools/thickness_calculator_widget.py | 4 ++-- loopstructural/main/data_manager.py | 5 ++++- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/loopstructural/gui/map2loop_tools/basal_contacts_widget.py b/loopstructural/gui/map2loop_tools/basal_contacts_widget.py index 05e6445..b20f1bc 100644 --- a/loopstructural/gui/map2loop_tools/basal_contacts_widget.py +++ b/loopstructural/gui/map2loop_tools/basal_contacts_widget.py @@ -155,10 +155,12 @@ def _restore_selection(self): return if layer_name := settings.get('geology_layer'): layer = self.data_manager.find_layer_by_name(layer_name) - self.geologyLayerComboBox.setLayer(layer) + if layer: + self.geologyLayerComboBox.setLayer(layer) if layer_name := settings.get('faults_layer'): layer = self.data_manager.find_layer_by_name(layer_name) - self.faultsLayerComboBox.setLayer(layer) + if layer: + self.faultsLayerComboBox.setLayer(layer) if field := settings.get('unit_name_field'): self.unitNameFieldComboBox.setField(field) diff --git a/loopstructural/gui/map2loop_tools/sampler_widget.py b/loopstructural/gui/map2loop_tools/sampler_widget.py index e11bafa..3ee62ec 100644 --- a/loopstructural/gui/map2loop_tools/sampler_widget.py +++ b/loopstructural/gui/map2loop_tools/sampler_widget.py @@ -143,13 +143,16 @@ def _restore_selection(self): return if layer_name := settings.get('dtm_layer'): layer = self.data_manager.find_layer_by_name(layer_name) - self.dtmLayerComboBox.setLayer(layer) + if layer: + self.dtmLayerComboBox.setLayer(layer) if layer_name := settings.get('geology_layer'): layer = self.data_manager.find_layer_by_name(layer_name) - self.geologyLayerComboBox.setLayer(layer) + if layer: + self.geologyLayerComboBox.setLayer(layer) if layer_name := settings.get('spatial_data_layer'): layer = self.data_manager.find_layer_by_name(layer_name) - self.spatialDataLayerComboBox.setLayer(layer) + if layer: + self.spatialDataLayerComboBox.setLayer(layer) sampler_index = settings.get('sampler_type_index') if sampler_index is not None: self.samplerTypeComboBox.setCurrentIndex(sampler_index) diff --git a/loopstructural/gui/map2loop_tools/sorter_widget.py b/loopstructural/gui/map2loop_tools/sorter_widget.py index a0b3138..4f9efcb 100644 --- a/loopstructural/gui/map2loop_tools/sorter_widget.py +++ b/loopstructural/gui/map2loop_tools/sorter_widget.py @@ -169,7 +169,8 @@ def _restore_selection(self): ): if layer_name := settings.get(key): layer = self.data_manager.find_layer_by_name(layer_name) - combo.setLayer(layer) + if layer: + combo.setLayer(layer) if 'sorting_algorithm' in settings: self.sortingAlgorithmComboBox.setCurrentIndex(settings['sorting_algorithm']) if 'orientation_type' in settings: diff --git a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py index a470a35..3da83fe 100644 --- a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py +++ b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py @@ -229,7 +229,8 @@ def _restore_selection(self): ): if layer_name := settings.get(key): layer = self.data_manager.find_layer_by_name(layer_name) - combo.setLayer(layer) + if layer: + combo.setLayer(layer) if 'calculator_type_index' in settings: self.calculatorTypeComboBox.setCurrentIndex(settings['calculator_type_index']) if 'orientation_type_index' in settings: @@ -348,4 +349,3 @@ def get_parameters(self): 'search_radius': self.searchRadiusSpinBox.value(), } return params - diff --git a/loopstructural/main/data_manager.py b/loopstructural/main/data_manager.py index 90d93a2..02c78f3 100644 --- a/loopstructural/main/data_manager.py +++ b/loopstructural/main/data_manager.py @@ -616,7 +616,10 @@ def update_feature_data(self, feature_name: str, feature_data: dict): def set_widget_settings(self, widget_name: str, settings: dict): """Store widget settings for persistence.""" - self.widget_settings[widget_name] = settings or {} + if settings is None: + self.widget_settings[widget_name] = {} + else: + self.widget_settings[widget_name] = settings def get_widget_settings(self, widget_name: str, default=None): """Retrieve persisted widget settings.""" From 4669478fc55a5a65fd24f975e1f80e2bbed8bec2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:43:42 +0000 Subject: [PATCH 4/9] Refine persistence and params Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .../gui/map2loop_tools/sampler_widget.py | 3 +- .../thickness_calculator_widget.py | 53 +++++++++++++------ loopstructural/main/data_manager.py | 5 +- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/loopstructural/gui/map2loop_tools/sampler_widget.py b/loopstructural/gui/map2loop_tools/sampler_widget.py index 3ee62ec..c190f45 100644 --- a/loopstructural/gui/map2loop_tools/sampler_widget.py +++ b/loopstructural/gui/map2loop_tools/sampler_widget.py @@ -117,7 +117,8 @@ def _guess_layers(self): return # DTM dtm_names = get_layer_names(self.dtmLayerComboBox) - dtm_match = ColumnMatcher(dtm_names).find_match('DTM') or ColumnMatcher(dtm_names).find_match('DEM') + dtm_matcher = ColumnMatcher(dtm_names) + dtm_match = dtm_matcher.find_match('DTM') or dtm_matcher.find_match('DEM') if dtm_match: layer = self.data_manager.find_layer_by_name(dtm_match) self.dtmLayerComboBox.setLayer(layer) diff --git a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py index 3da83fe..bbba7db 100644 --- a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py +++ b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py @@ -297,23 +297,40 @@ def _run_calculator(self): try: result = calculate_thickness(**params) - if result is not None: - # If result is a GeoDataFrame, add to project - if hasattr(result, 'geometry'): - addGeoDataFrameToproject(result, "Thickness Results") - QMessageBox.information( - self, - "Success", - "Thickness calculation completed successfully and added to project.", - ) - else: - QMessageBox.information( - self, "Success", f"Thickness calculation completed: {result}" - ) - return True - else: + if not result: QMessageBox.warning(self, "No Results", "Thickness calculation returned no results.") return False + + # Expect result as dict with components; fall back to direct layer + if isinstance(result, dict): + thicknesses = result.get('thicknesses') + lines = result.get('lines') + location_tracking = result.get('location_tracking') + if thicknesses is not None: + addGeoDataFrameToproject(thicknesses, "Thickness Results") + if lines is not None: + addGeoDataFrameToproject(lines, "Thickness Lines") + if location_tracking is not None: + addGeoDataFrameToproject(location_tracking, "Thickness Locations") + QMessageBox.information( + self, + "Success", + "Thickness calculation completed successfully and added to project.", + ) + return True + + if hasattr(result, 'geometry'): + addGeoDataFrameToproject(result, "Thickness Results") + QMessageBox.information( + self, + "Success", + "Thickness calculation completed successfully and added to project.", + ) + return True + + QMessageBox.information(self, "Success", f"Thickness calculation completed: {result}") + return True + except Exception as e: if self._debug: self._debug.plugin.log( @@ -347,5 +364,11 @@ def get_parameters(self): 'basal_unitname_field': self.basalUnitNameFieldComboBox.currentField(), 'max_line_length': self.maxLineLengthSpinBox.value(), 'search_radius': self.searchRadiusSpinBox.value(), + 'updater': getattr(self.data_manager, 'stratigraphic_column_callback', None), + 'stratigraphic_order': ( + self.data_manager.get_stratigraphic_column().order + if self.data_manager and hasattr(self.data_manager, 'get_stratigraphic_column') + else None + ), } return params diff --git a/loopstructural/main/data_manager.py b/loopstructural/main/data_manager.py index 02c78f3..0b0f3db 100644 --- a/loopstructural/main/data_manager.py +++ b/loopstructural/main/data_manager.py @@ -616,10 +616,7 @@ def update_feature_data(self, feature_name: str, feature_data: dict): def set_widget_settings(self, widget_name: str, settings: dict): """Store widget settings for persistence.""" - if settings is None: - self.widget_settings[widget_name] = {} - else: - self.widget_settings[widget_name] = settings + self.widget_settings[widget_name] = settings def get_widget_settings(self, widget_name: str, default=None): """Retrieve persisted widget settings.""" From 48c3f3c36f33c8e7f03ad2402c7f5cbf6f0af2dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:44:36 +0000 Subject: [PATCH 5/9] Tighten matching and params Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- loopstructural/gui/map2loop_tools/sampler_widget.py | 6 ++++-- .../gui/map2loop_tools/thickness_calculator_widget.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/loopstructural/gui/map2loop_tools/sampler_widget.py b/loopstructural/gui/map2loop_tools/sampler_widget.py index c190f45..7ecf639 100644 --- a/loopstructural/gui/map2loop_tools/sampler_widget.py +++ b/loopstructural/gui/map2loop_tools/sampler_widget.py @@ -124,13 +124,15 @@ def _guess_layers(self): self.dtmLayerComboBox.setLayer(layer) # Geology geology_names = get_layer_names(self.geologyLayerComboBox) - geology_match = ColumnMatcher(geology_names).find_match('GEOLOGY') + geology_matcher = ColumnMatcher(geology_names) + geology_match = geology_matcher.find_match('GEOLOGY') if geology_match: layer = self.data_manager.find_layer_by_name(geology_match) self.geologyLayerComboBox.setLayer(layer) # Spatial spatial_names = get_layer_names(self.spatialDataLayerComboBox) - spatial_match = ColumnMatcher(spatial_names).find_match('SPATIAL') + spatial_matcher = ColumnMatcher(spatial_names) + spatial_match = spatial_matcher.find_match('SPATIAL') if spatial_match: layer = self.data_manager.find_layer_by_name(spatial_match) self.spatialDataLayerComboBox.setLayer(layer) diff --git a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py index bbba7db..55f187d 100644 --- a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py +++ b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py @@ -364,10 +364,10 @@ def get_parameters(self): 'basal_unitname_field': self.basalUnitNameFieldComboBox.currentField(), 'max_line_length': self.maxLineLengthSpinBox.value(), 'search_radius': self.searchRadiusSpinBox.value(), - 'updater': getattr(self.data_manager, 'stratigraphic_column_callback', None), + 'updater': (lambda msg: QMessageBox.information(self, "Progress", msg)), 'stratigraphic_order': ( - self.data_manager.get_stratigraphic_column().order - if self.data_manager and hasattr(self.data_manager, 'get_stratigraphic_column') + self.data_manager.get_stratigraphic_unit_names() + if self.data_manager and hasattr(self.data_manager, 'get_stratigraphic_unit_names') else None ), } From 77f9712fb6894bcc002a9ea20cb0f4af9ab7f973 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:45:33 +0000 Subject: [PATCH 6/9] Fix thickness params Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .../gui/map2loop_tools/thickness_calculator_widget.py | 5 ++--- loopstructural/main/data_manager.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py index 55f187d..27838e6 100644 --- a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py +++ b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py @@ -358,12 +358,11 @@ def get_parameters(self): 'sampled_contacts': self.sampledContactsComboBox.currentLayer(), 'structure': self.structureLayerComboBox.currentLayer(), 'orientation_type': self.orientationTypeComboBox.currentText(), - 'unitname_field': self.unitNameFieldComboBox.currentField(), + 'unit_name_field': self.unitNameFieldComboBox.currentField(), 'dip_field': self.dipFieldComboBox.currentField(), 'dipdir_field': self.dipDirFieldComboBox.currentField(), - 'basal_unitname_field': self.basalUnitNameFieldComboBox.currentField(), + 'basal_contacts_unit_name': self.basalUnitNameFieldComboBox.currentField(), 'max_line_length': self.maxLineLengthSpinBox.value(), - 'search_radius': self.searchRadiusSpinBox.value(), 'updater': (lambda msg: QMessageBox.information(self, "Progress", msg)), 'stratigraphic_order': ( self.data_manager.get_stratigraphic_unit_names() diff --git a/loopstructural/main/data_manager.py b/loopstructural/main/data_manager.py index 0b0f3db..ebdaa76 100644 --- a/loopstructural/main/data_manager.py +++ b/loopstructural/main/data_manager.py @@ -249,7 +249,6 @@ def get_stratigraphic_unit_names(self): for u in self._stratigraphic_column.order: if u.element_type == StratigraphicColumnElementType.UNIT: units.append(u.name) - print(f"Unit: {u.name}") return units def add_to_stratigraphic_column(self, unit_data): From fef404a52678d69415838dcce94c9791a9ce85fe Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Mon, 26 Jan 2026 12:52:15 +1030 Subject: [PATCH 7/9] don't guess sampler layer --- .../gui/map2loop_tools/sampler_widget.py | 72 +------------------ 1 file changed, 1 insertion(+), 71 deletions(-) diff --git a/loopstructural/gui/map2loop_tools/sampler_widget.py b/loopstructural/gui/map2loop_tools/sampler_widget.py index 7ecf639..1fcf84e 100644 --- a/loopstructural/gui/map2loop_tools/sampler_widget.py +++ b/loopstructural/gui/map2loop_tools/sampler_widget.py @@ -7,7 +7,6 @@ from qgis.PyQt import uic from loopstructural.toolbelt.preferences import PlgOptionsManager -from ...main.helpers import ColumnMatcher, get_layer_names class SamplerWidget(QWidget): @@ -57,8 +56,6 @@ def __init__(self, parent=None, data_manager=None, debug_manager=None): # Initial state update self._on_sampler_type_changed() - self._guess_layers() - self._restore_selection() def set_debug_manager(self, debug_manager): """Attach a debug manager instance.""" @@ -111,73 +108,6 @@ def _log_params(self, context_label: str): except Exception: pass - def _guess_layers(self): - """Auto-select layers using ColumnMatcher.""" - if not self.data_manager: - return - # DTM - dtm_names = get_layer_names(self.dtmLayerComboBox) - dtm_matcher = ColumnMatcher(dtm_names) - dtm_match = dtm_matcher.find_match('DTM') or dtm_matcher.find_match('DEM') - if dtm_match: - layer = self.data_manager.find_layer_by_name(dtm_match) - self.dtmLayerComboBox.setLayer(layer) - # Geology - geology_names = get_layer_names(self.geologyLayerComboBox) - geology_matcher = ColumnMatcher(geology_names) - geology_match = geology_matcher.find_match('GEOLOGY') - if geology_match: - layer = self.data_manager.find_layer_by_name(geology_match) - self.geologyLayerComboBox.setLayer(layer) - # Spatial - spatial_names = get_layer_names(self.spatialDataLayerComboBox) - spatial_matcher = ColumnMatcher(spatial_names) - spatial_match = spatial_matcher.find_match('SPATIAL') - if spatial_match: - layer = self.data_manager.find_layer_by_name(spatial_match) - self.spatialDataLayerComboBox.setLayer(layer) - - def _restore_selection(self): - """Restore persisted selections from data manager.""" - if not self.data_manager: - return - settings = self.data_manager.get_widget_settings('sampler_widget', {}) - if not settings: - return - if layer_name := settings.get('dtm_layer'): - layer = self.data_manager.find_layer_by_name(layer_name) - if layer: - self.dtmLayerComboBox.setLayer(layer) - if layer_name := settings.get('geology_layer'): - layer = self.data_manager.find_layer_by_name(layer_name) - if layer: - self.geologyLayerComboBox.setLayer(layer) - if layer_name := settings.get('spatial_data_layer'): - layer = self.data_manager.find_layer_by_name(layer_name) - if layer: - self.spatialDataLayerComboBox.setLayer(layer) - sampler_index = settings.get('sampler_type_index') - if sampler_index is not None: - self.samplerTypeComboBox.setCurrentIndex(sampler_index) - if 'decimation' in settings: - self.decimationSpinBox.setValue(settings['decimation']) - if 'spacing' in settings: - self.spacingSpinBox.setValue(settings['spacing']) - - def _persist_selection(self): - """Persist current selections into data manager.""" - if not self.data_manager: - return - settings = { - 'dtm_layer': self.dtmLayerComboBox.currentLayer().name() if self.dtmLayerComboBox.currentLayer() else None, - 'geology_layer': self.geologyLayerComboBox.currentLayer().name() if self.geologyLayerComboBox.currentLayer() else None, - 'spatial_data_layer': self.spatialDataLayerComboBox.currentLayer().name() if self.spatialDataLayerComboBox.currentLayer() else None, - 'sampler_type_index': self.samplerTypeComboBox.currentIndex(), - 'decimation': self.decimationSpinBox.value(), - 'spacing': self.spacingSpinBox.value(), - } - self.data_manager.set_widget_settings('sampler_widget', settings) - def _on_sampler_type_changed(self): """Update UI based on selected sampler type.""" sampler_type = self.samplerTypeComboBox.currentText() @@ -201,7 +131,7 @@ def _on_sampler_type_changed(self): def _run_sampler(self): """Run the sampler algorithm using the map2loop API.""" - self._persist_selection() + from qgis.core import ( QgsCoordinateReferenceSystem, QgsFeature, From 271ca04993bfa143745cd8a1624c0a0ac8440ed2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 02:25:13 +0000 Subject: [PATCH 8/9] Persist selections in model setup Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com> --- .../gui/modelling/model_definition/dem.py | 46 ++++++++++ .../model_definition/fault_layers.py | 62 +++++++++++++ .../model_definition/stratigraphic_layers.py | 92 +++++++++++++++++++ 3 files changed, 200 insertions(+) diff --git a/loopstructural/gui/modelling/model_definition/dem.py b/loopstructural/gui/modelling/model_definition/dem.py index 0bf35f7..a2df606 100644 --- a/loopstructural/gui/modelling/model_definition/dem.py +++ b/loopstructural/gui/modelling/model_definition/dem.py @@ -4,6 +4,8 @@ from qgis.core import QgsMapLayerProxyModel from qgis.PyQt import uic +from ...main.helpers import ColumnMatcher, get_layer_names + class DEMWidget(QWidget): def __init__(self, parent=None, data_manager=None): @@ -16,6 +18,8 @@ def __init__(self, parent=None, data_manager=None): self.elevationQgsDoubleSpinBox.valueChanged.connect(self.onElevationChanged) self.onElevationChanged() self.data_manager.set_dem_callback(self.set_dem_layer) + self._guess_layer() + self._restore_selection() def set_dem_layer(self, layer): """Set the DEM layer in the combo box.""" @@ -25,6 +29,7 @@ def set_dem_layer(self, layer): else: self.demLayerQgsMapLayerComboBox.setCurrentIndex(-1) self.useDEMCheckBox.setChecked(False) + self._persist_selection() def onUseDEMClicked(self): @@ -48,9 +53,50 @@ def onDEMLayerChanged(self): else: self.data_manager.set_dem_layer(None) self.data_manager.set_use_dem(True) + self._persist_selection() def onElevationChanged(self): """Handle changes to the elevation value.""" elevation = self.elevationQgsDoubleSpinBox.value() self.data_manager.set_elevation(elevation) self.data_manager.set_use_dem(False) + + def _guess_layer(self): + if not self.data_manager: + return + layer_names = get_layer_names(self.demLayerQgsMapLayerComboBox) + matcher = ColumnMatcher(layer_names) + match = matcher.find_match('DEM') or matcher.find_match('DTM') + if match: + layer = self.data_manager.find_layer_by_name(match) + if layer: + self.demLayerQgsMapLayerComboBox.setLayer(layer) + + def _persist_selection(self): + if not self.data_manager: + return + settings = { + 'dem_layer': ( + self.demLayerQgsMapLayerComboBox.currentLayer().name() + if self.demLayerQgsMapLayerComboBox.currentLayer() + else None + ), + 'use_dem': self.useDEMCheckBox.isChecked(), + 'elevation': self.elevationQgsDoubleSpinBox.value(), + } + self.data_manager.set_widget_settings('dem_widget', settings) + + def _restore_selection(self): + if not self.data_manager: + return + settings = self.data_manager.get_widget_settings('dem_widget', {}) + if not settings: + return + if layer_name := settings.get('dem_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + if layer: + self.demLayerQgsMapLayerComboBox.setLayer(layer) + if 'use_dem' in settings: + self.useDEMCheckBox.setChecked(settings['use_dem']) + if 'elevation' in settings: + self.elevationQgsDoubleSpinBox.setValue(settings['elevation']) diff --git a/loopstructural/gui/modelling/model_definition/fault_layers.py b/loopstructural/gui/modelling/model_definition/fault_layers.py index dbaf486..2137e3e 100644 --- a/loopstructural/gui/modelling/model_definition/fault_layers.py +++ b/loopstructural/gui/modelling/model_definition/fault_layers.py @@ -4,6 +4,8 @@ from qgis.core import QgsFieldProxyModel, QgsMapLayerProxyModel, QgsWkbTypes from qgis.PyQt import uic +from ...main.helpers import ColumnMatcher, get_layer_names + class FaultLayersWidget(QWidget): def __init__(self, parent=None, data_manager=None): @@ -26,6 +28,8 @@ def __init__(self, parent=None, data_manager=None): self.useZCoordinateCheckBox.stateChanged.connect(self.onUseZCoordinateClicked) self.useZCoordinateCheckBox.stateChanged.connect(self.onFaultFieldChanged) self.useZCoordinate = False + self._guess_layer_and_fields() + self._restore_selection() def enableZCheckbox(self, enable): """Enable or disable the Z coordinate checkbox.""" @@ -80,6 +84,7 @@ def onFaultTraceLayerChanged(self, layer): fault_displacement_field=None, use_z_coordinate=self.useZCoordinate, ) + self._persist_selection() def onFaultFieldChanged(self): self.data_manager.set_fault_trace_layer( @@ -89,3 +94,60 @@ def onFaultFieldChanged(self): fault_displacement_field=self.faultDisplacementField.currentField(), use_z_coordinate=self.useZCoordinate, ) + self._persist_selection() + + def _guess_layer_and_fields(self): + if not self.data_manager: + return + layer_names = get_layer_names(self.faultTraceLayer) + matcher = ColumnMatcher(layer_names) + match = matcher.find_match('FAULT') + if match: + layer = self.data_manager.find_layer_by_name(match) + if layer: + self.faultTraceLayer.setLayer(layer) + fields = [field.name() for field in layer.fields()] + field_matcher = ColumnMatcher(fields) + if name_match := field_matcher.find_match('FAULT_NAME') or field_matcher.find_match('NAME'): + self.faultNameField.setField(name_match) + if dip_match := field_matcher.find_match('DIP'): + self.faultDipField.setField(dip_match) + if disp_match := field_matcher.find_match('DISPLACEMENT') or field_matcher.find_match( + 'SLIP' + ): + self.faultDisplacementField.setField(disp_match) + + def _persist_selection(self): + if not self.data_manager: + return + settings = { + 'fault_layer': ( + self.faultTraceLayer.currentLayer().name() + if self.faultTraceLayer.currentLayer() + else None + ), + 'fault_name_field': self.faultNameField.currentField(), + 'fault_dip_field': self.faultDipField.currentField(), + 'fault_displacement_field': self.faultDisplacementField.currentField(), + 'use_z': self.useZCoordinateCheckBox.isChecked(), + } + self.data_manager.set_widget_settings('fault_layers_widget', settings) + + def _restore_selection(self): + if not self.data_manager: + return + settings = self.data_manager.get_widget_settings('fault_layers_widget', {}) + if not settings: + return + if layer_name := settings.get('fault_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + if layer: + self.faultTraceLayer.setLayer(layer) + if field := settings.get('fault_name_field'): + self.faultNameField.setField(field) + if field := settings.get('fault_dip_field'): + self.faultDipField.setField(field) + if field := settings.get('fault_displacement_field'): + self.faultDisplacementField.setField(field) + if 'use_z' in settings: + self.useZCoordinateCheckBox.setChecked(settings['use_z']) diff --git a/loopstructural/gui/modelling/model_definition/stratigraphic_layers.py b/loopstructural/gui/modelling/model_definition/stratigraphic_layers.py index 8df3de3..60d519d 100644 --- a/loopstructural/gui/modelling/model_definition/stratigraphic_layers.py +++ b/loopstructural/gui/modelling/model_definition/stratigraphic_layers.py @@ -5,6 +5,8 @@ from qgis.core import QgsMapLayerProxyModel, QgsWkbTypes from qgis.PyQt import uic +from ...main.helpers import ColumnMatcher, get_layer_names + class StratigraphicLayersWidget(QWidget): def __init__(self, parent=None, data_manager=None): @@ -50,6 +52,8 @@ def __init__(self, parent=None, data_manager=None): self.useStructuralPointsZCoordinatesCheckBox.stateChanged.connect( self.onStructuralDataFieldChanged ) + self._guess_layers_and_fields() + self._restore_selection() def enableBasalContactsZCheckBox(self, enable): self.useBasalContactsZCoordinatesCheckBox.setEnabled(enable) @@ -116,6 +120,7 @@ def set_orientations_layer( def onBasalContactsChanged(self, layer): self.unitNameField.setLayer(layer) self.data_manager.set_basal_contacts(layer, self.unitNameField.currentField()) + self._persist_selection() def onOrientationTypeChanged(self, index): if index == 0: @@ -153,6 +158,7 @@ def onStructuralDataFieldChanged(self, field): self.orientationType.currentText(), use_z_coordinate=self.structural_points_use_z, ) + self._persist_selection() # self.updateDataManager() def onUnitFieldChanged(self, field): @@ -161,5 +167,91 @@ def onUnitFieldChanged(self, field): field, use_z_coordinate=self.basal_contacts_use_z, ) + self._persist_selection() # self.updateDataManager() + + def _guess_layers_and_fields(self): + if not self.data_manager: + return + # Basal contacts + basal_names = get_layer_names(self.basalContactsLayer) + basal_matcher = ColumnMatcher(basal_names) + basal_match = basal_matcher.find_match('BASAL_CONTACTS') + if basal_match: + layer = self.data_manager.find_layer_by_name(basal_match) + if layer: + self.basalContactsLayer.setLayer(layer) + fields = [f.name() for f in layer.fields()] + fmatcher = ColumnMatcher(fields) + if unit_match := fmatcher.find_match('UNITNAME'): + self.unitNameField.setField(unit_match) + # Structural data + structural_names = get_layer_names(self.structuralDataLayer) + structural_matcher = ColumnMatcher(structural_names) + structural_match = structural_matcher.find_match('STRUCTURE') or structural_matcher.find_match( + 'ORIENTATION' + ) + if structural_match: + layer = self.data_manager.find_layer_by_name(structural_match) + if layer: + self.structuralDataLayer.setLayer(layer) + fields = [f.name() for f in layer.fields()] + fmatcher = ColumnMatcher(fields) + if strike_match := fmatcher.find_match('STRIKE') or fmatcher.find_match('DIPDIR'): + self.orientationField.setField(strike_match) + if dip_match := fmatcher.find_match('DIP'): + self.dipField.setField(dip_match) + if unit_match := fmatcher.find_match('UNITNAME'): + self.structuralDataUnitName.setField(unit_match) + + def _persist_selection(self): + if not self.data_manager: + return + settings = { + 'basal_layer': self.basalContactsLayer.currentLayer().name() + if self.basalContactsLayer.currentLayer() + else None, + 'structural_layer': self.structuralDataLayer.currentLayer().name() + if self.structuralDataLayer.currentLayer() + else None, + 'unit_name_field': self.unitNameField.currentField(), + 'orientation_field': self.orientationField.currentField(), + 'dip_field': self.dipField.currentField(), + 'structural_unit_field': self.structuralDataUnitName.currentField(), + 'orientation_type': self.orientationType.currentText(), + 'use_basal_z': self.useBasalContactsZCoordinatesCheckBox.isChecked(), + 'use_structural_z': self.useStructuralPointsZCoordinatesCheckBox.isChecked(), + } + self.data_manager.set_widget_settings('stratigraphic_layers_widget', settings) + + def _restore_selection(self): + if not self.data_manager: + return + settings = self.data_manager.get_widget_settings('stratigraphic_layers_widget', {}) + if not settings: + return + if layer_name := settings.get('basal_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + if layer: + self.basalContactsLayer.setLayer(layer) + if layer_name := settings.get('structural_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + if layer: + self.structuralDataLayer.setLayer(layer) + if field := settings.get('unit_name_field'): + self.unitNameField.setField(field) + if field := settings.get('orientation_field'): + self.orientationField.setField(field) + if field := settings.get('dip_field'): + self.dipField.setField(field) + if field := settings.get('structural_unit_field'): + self.structuralDataUnitName.setField(field) + if 'orientation_type' in settings: + idx = self.orientationType.findText(settings['orientation_type'], Qt.MatchFixedString) + if idx >= 0: + self.orientationType.setCurrentIndex(idx) + if 'use_basal_z' in settings: + self.useBasalContactsZCoordinatesCheckBox.setChecked(settings['use_basal_z']) + if 'use_structural_z' in settings: + self.useStructuralPointsZCoordinatesCheckBox.setChecked(settings['use_structural_z']) From 9f595a4f7fa65fae6e7bd6274361624427061cb7 Mon Sep 17 00:00:00 2001 From: Lachlan Grose Date: Mon, 26 Jan 2026 14:16:56 +1030 Subject: [PATCH 9/9] updating imports and adding faults loop up table --- .../gui/modelling/model_definition/dem.py | 4 ++-- .../model_definition/fault_layers.py | 12 ++++++---- .../model_definition/stratigraphic_layers.py | 24 +++++++++++-------- loopstructural/main/data_manager.py | 7 ++++-- loopstructural/main/helpers.py | 9 +++++++ 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/loopstructural/gui/modelling/model_definition/dem.py b/loopstructural/gui/modelling/model_definition/dem.py index a2df606..8879e09 100644 --- a/loopstructural/gui/modelling/model_definition/dem.py +++ b/loopstructural/gui/modelling/model_definition/dem.py @@ -4,7 +4,7 @@ from qgis.core import QgsMapLayerProxyModel from qgis.PyQt import uic -from ...main.helpers import ColumnMatcher, get_layer_names +from ....main.helpers import ColumnMatcher, get_layer_names class DEMWidget(QWidget): @@ -31,7 +31,6 @@ def set_dem_layer(self, layer): self.useDEMCheckBox.setChecked(False) self._persist_selection() - def onUseDEMClicked(self): if self.useDEMCheckBox.isChecked(): self.demLayerQgsMapLayerComboBox.setEnabled(True) @@ -67,6 +66,7 @@ def _guess_layer(self): layer_names = get_layer_names(self.demLayerQgsMapLayerComboBox) matcher = ColumnMatcher(layer_names) match = matcher.find_match('DEM') or matcher.find_match('DTM') + print("DEMWidget: guessed layer match:", match) if match: layer = self.data_manager.find_layer_by_name(match) if layer: diff --git a/loopstructural/gui/modelling/model_definition/fault_layers.py b/loopstructural/gui/modelling/model_definition/fault_layers.py index 2137e3e..aacc65e 100644 --- a/loopstructural/gui/modelling/model_definition/fault_layers.py +++ b/loopstructural/gui/modelling/model_definition/fault_layers.py @@ -4,7 +4,7 @@ from qgis.core import QgsFieldProxyModel, QgsMapLayerProxyModel, QgsWkbTypes from qgis.PyQt import uic -from ...main.helpers import ColumnMatcher, get_layer_names +from ....main.helpers import ColumnMatcher, get_layer_names class FaultLayersWidget(QWidget): @@ -108,13 +108,15 @@ def _guess_layer_and_fields(self): self.faultTraceLayer.setLayer(layer) fields = [field.name() for field in layer.fields()] field_matcher = ColumnMatcher(fields) - if name_match := field_matcher.find_match('FAULT_NAME') or field_matcher.find_match('NAME'): + if name_match := field_matcher.find_match('FAULT_NAME') or field_matcher.find_match( + 'NAME' + ): self.faultNameField.setField(name_match) if dip_match := field_matcher.find_match('DIP'): self.faultDipField.setField(dip_match) - if disp_match := field_matcher.find_match('DISPLACEMENT') or field_matcher.find_match( - 'SLIP' - ): + if disp_match := field_matcher.find_match( + 'DISPLACEMENT' + ) or field_matcher.find_match('SLIP'): self.faultDisplacementField.setField(disp_match) def _persist_selection(self): diff --git a/loopstructural/gui/modelling/model_definition/stratigraphic_layers.py b/loopstructural/gui/modelling/model_definition/stratigraphic_layers.py index 60d519d..8eb3939 100644 --- a/loopstructural/gui/modelling/model_definition/stratigraphic_layers.py +++ b/loopstructural/gui/modelling/model_definition/stratigraphic_layers.py @@ -5,7 +5,7 @@ from qgis.core import QgsMapLayerProxyModel, QgsWkbTypes from qgis.PyQt import uic -from ...main.helpers import ColumnMatcher, get_layer_names +from ....main.helpers import ColumnMatcher, get_layer_names class StratigraphicLayersWidget(QWidget): @@ -189,9 +189,9 @@ def _guess_layers_and_fields(self): # Structural data structural_names = get_layer_names(self.structuralDataLayer) structural_matcher = ColumnMatcher(structural_names) - structural_match = structural_matcher.find_match('STRUCTURE') or structural_matcher.find_match( - 'ORIENTATION' - ) + structural_match = structural_matcher.find_match( + 'STRUCTURE' + ) or structural_matcher.find_match('ORIENTATION') if structural_match: layer = self.data_manager.find_layer_by_name(structural_match) if layer: @@ -209,12 +209,16 @@ def _persist_selection(self): if not self.data_manager: return settings = { - 'basal_layer': self.basalContactsLayer.currentLayer().name() - if self.basalContactsLayer.currentLayer() - else None, - 'structural_layer': self.structuralDataLayer.currentLayer().name() - if self.structuralDataLayer.currentLayer() - else None, + 'basal_layer': ( + self.basalContactsLayer.currentLayer().name() + if self.basalContactsLayer.currentLayer() + else None + ), + 'structural_layer': ( + self.structuralDataLayer.currentLayer().name() + if self.structuralDataLayer.currentLayer() + else None + ), 'unit_name_field': self.unitNameField.currentField(), 'orientation_field': self.orientationField.currentField(), 'dip_field': self.dipField.currentField(), diff --git a/loopstructural/main/data_manager.py b/loopstructural/main/data_manager.py index ebdaa76..3063a60 100644 --- a/loopstructural/main/data_manager.py +++ b/loopstructural/main/data_manager.py @@ -2,10 +2,10 @@ from collections import defaultdict import numpy as np -from LoopStructural.datatypes import BoundingBox from qgis.core import QgsPointXY, QgsProject, QgsVectorLayer from LoopStructural import FaultTopology, StratigraphicColumn +from LoopStructural.datatypes import BoundingBox from LoopStructural.modelling.core.stratigraphic_column import StratigraphicColumnElementType from .vectorLayerWrapper import qgsLayerToGeoDataFrame @@ -596,10 +596,13 @@ def find_layer_by_name(self, layer_name, layer_type=QgsVectorLayer): log_level=2, ) i = 0 + while i < len(layers) and not issubclass(type(layers[i]), layer_type): i += 1 - + if i >= len(layers): + self.logger(message=f"Layer '{layer_name}' is not a vector layer.", log_level=2) + return None if issubclass(type(layers[i]), layer_type): return layers[i] else: diff --git a/loopstructural/main/helpers.py b/loopstructural/main/helpers.py index a4e5cf0..186b49d 100644 --- a/loopstructural/main/helpers.py +++ b/loopstructural/main/helpers.py @@ -65,6 +65,15 @@ class ColumnMatcher: 'MIN_AGE': ['min_age', 'minage', 'age_min', 'younger', 'min_age_ma', 'age_low'], 'MAX_AGE': ['max_age', 'maxage', 'age_max', 'older', 'max_age_ma', 'age_high'], 'GROUP': ['group', 'group_name', 'groupname', 'series', 'supergroup'], + 'FAULT': [ + 'fault', + 'faults', + 'fault_layer', + 'faults_layer', + 'faultid', + 'fault_id', + 'fault_name', + ], 'X': ['x', 'easting', 'longitude', 'lon', 'long', 'x_coord'], 'Y': ['y', 'northing', 'latitude', 'lat', 'y_coord'], 'Z': ['z', 'elevation', 'altitude', 'height', 'elev', 'z_coord'],