diff --git a/loopstructural/gui/map2loop_tools/basal_contacts_widget.py b/loopstructural/gui/map2loop_tools/basal_contacts_widget.py index d24777e..b20f1bc 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,35 @@ 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) + if layer: + self.geologyLayerComboBox.setLayer(layer) + if layer_name := settings.get('faults_layer'): + layer = self.data_manager.find_layer_by_name(layer_name) + if layer: + 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 +204,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..1fcf84e 100644 --- a/loopstructural/gui/map2loop_tools/sampler_widget.py +++ b/loopstructural/gui/map2loop_tools/sampler_widget.py @@ -131,6 +131,7 @@ def _on_sampler_type_changed(self): def _run_sampler(self): """Run the sampler algorithm using the map2loop API.""" + 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..4f9efcb 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,56 @@ 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) + if layer: + 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 +334,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..27838e6 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,111 +213,122 @@ 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) + if layer: + 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 + # Validate inputs based on calculator type + calculator_type = self.calculatorTypeComboBox.currentText() - if not self.structureLayerComboBox.currentLayer(): - QMessageBox.warning( - self, "Missing Input", "Please select a structure/orientation layer." - ) - return False + 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 - calculator_type = self.calculatorTypeComboBox.currentText() + elif calculator_type == "StructuralPoint": + if not self.structureLayerComboBox.currentLayer(): + QMessageBox.warning(self, "Missing Input", "Please select a structure layer.") + return False # 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 [] - ), - } + params = self.get_parameters() - # Add optional parameters - if self.dtmLayerComboBox.currentLayer(): - kwargs['dtm'] = self.dtmLayerComboBox.currentLayer() - - if calculator_type == "StructuralPoint": - kwargs['max_line_length'] = self.maxLineLengthSpinBox.value() + try: + result = calculate_thickness(**params) + if not result: + QMessageBox.warning(self, "No Results", "Thickness calculation returned no results.") + 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 + # 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 - 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, - ) - - 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: + if hasattr(result, 'geometry'): + addGeoDataFrameToproject(result, "Thickness Results") QMessageBox.information( self, "Success", - f"Thickness calculation completed successfully! ({len(result)} records)", + "Thickness calculation completed successfully and added to project.", ) - else: - QMessageBox.warning(self, "Error", "No thickness data was calculated.") - return False + return True + + QMessageBox.information(self, "Success", f"Thickness calculation completed: {result}") return True except Exception as e: @@ -338,41 +350,24 @@ 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(), + 'unit_name_field': self.unitNameFieldComboBox.currentField(), 'dip_field': self.dipFieldComboBox.currentField(), 'dipdir_field': self.dipDirFieldComboBox.currentField(), - 'orientation_type': self.orientationTypeComboBox.currentIndex(), + 'basal_contacts_unit_name': self.basalUnitNameFieldComboBox.currentField(), 'max_line_length': self.maxLineLengthSpinBox.value(), + 'updater': (lambda msg: QMessageBox.information(self, "Progress", msg)), + 'stratigraphic_order': ( + self.data_manager.get_stratigraphic_unit_names() + if self.data_manager and hasattr(self.data_manager, 'get_stratigraphic_unit_names') + else None + ), } - - 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']) + return params diff --git a/loopstructural/gui/modelling/model_definition/dem.py b/loopstructural/gui/modelling/model_definition/dem.py index 0bf35f7..8879e09 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,7 +29,7 @@ def set_dem_layer(self, layer): else: self.demLayerQgsMapLayerComboBox.setCurrentIndex(-1) self.useDEMCheckBox.setChecked(False) - + self._persist_selection() def onUseDEMClicked(self): if self.useDEMCheckBox.isChecked(): @@ -48,9 +52,51 @@ 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') + print("DEMWidget: guessed layer match:", match) + 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..aacc65e 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,62 @@ 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..8eb3939 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,95 @@ 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']) diff --git a/loopstructural/main/data_manager.py b/loopstructural/main/data_manager.py index 56afc17..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 @@ -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.""" @@ -247,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): @@ -463,6 +464,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 +500,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 +573,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() @@ -587,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: @@ -604,6 +616,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 + + 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: 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'],