Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions loopstructural/gui/map2loop_tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Map2Loop Tools Widgets

This directory contains self-contained GUI widgets for map2loop processing tools.

## Widgets

### SorterWidget
GUI interface for automatic stratigraphic column sorting using various map2loop sorting algorithms:
- Age-based sorting
- NetworkX topological sorting
- Adjacency α sorting
- Maximise contacts sorting
- Observation projections sorting

**Features:**
- Dynamic UI that shows/hides fields based on selected algorithm
- Support for observation projections with structure and DTM data
- Field mapping for geology layers
- Integrated with QGIS processing framework

### UserDefinedSorterWidget
GUI interface for manually defining stratigraphic columns.

**Features:**
- Table-based interface for entering unit names
- Move up/down buttons for reordering units
- Add/remove row functionality
- Order from youngest (top) to oldest (bottom)

### SamplerWidget
GUI interface for contact sampling using Decimator and Spacing algorithms.

**Features:**
- Support for both Decimator and Spacing samplers
- Dynamic UI based on selected sampler type
- Decimator requires DTM and geology layers
- Spacing works with optional DTM and geology

### BasalContactsWidget
GUI interface for extracting basal contacts from geology layers.

**Features:**
- Support for fault layers
- Stratigraphic order integration
- Units to ignore configuration
- Extracts both basal contacts and all contacts

### ThicknessCalculatorWidget
GUI interface for calculating stratigraphic unit thickness using InterpolatedStructure or StructuralPoint methods.

**Features:**
- Two calculation methods: InterpolatedStructure and StructuralPoint
- Support for orientation data (dip direction or strike)
- DTM and bounding box configuration
- Integration with sampled contacts and basal contacts

## Usage

All widgets follow the same pattern:

1. They extend `QWidget`
2. Load UI from corresponding `.ui` file
3. Connect signals to update UI state dynamically
4. Provide `get_parameters()` and `set_parameters()` methods for reusability
5. Include a "Run" button that executes the corresponding processing algorithm

### Example

```python
from loopstructural.gui.map2loop_tools import SorterWidget

# Create widget
sorter_widget = SorterWidget(parent=None, data_manager=data_manager)

# Get parameters
params = sorter_widget.get_parameters()

# Set parameters
sorter_widget.set_parameters(params)
```

## Integration

The widgets are integrated into the main dock widget through `Map2LoopToolsTab`, which creates a new tab in the `ModellingWidget` containing all map2loop processing tools in collapsible group boxes.

## File Structure

```
map2loop_tools/
├── __init__.py # Package exports
├── sorter_widget.py # Automatic sorter widget
├── sorter_widget.ui # Automatic sorter UI
├── user_defined_sorter_widget.py # User-defined sorter widget
├── user_defined_sorter_widget.ui # User-defined sorter UI
├── sampler_widget.py # Sampler widget
├── sampler_widget.ui # Sampler UI
├── basal_contacts_widget.py # Basal contacts widget
├── basal_contacts_widget.ui # Basal contacts UI
├── thickness_calculator_widget.py # Thickness calculator widget
└── thickness_calculator_widget.ui # Thickness calculator UI
```

## Design Principles

Following the plugin's architectural guidelines:

1. **Thin Interface Layer**: Widgets only handle UI and user interaction, delegating to map2loop algorithms
2. **Modularity**: Each widget is self-contained and encapsulated
3. **Object-Oriented Design**: Clear responsibilities and interfaces
4. **Consistency**: Follows existing widget patterns in the codebase
21 changes: 21 additions & 0 deletions loopstructural/gui/map2loop_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Map2Loop processing tools dialogs.

This module contains GUI dialogs for map2loop processing tools that can be
accessed from the plugin menu.
"""

from .dialogs import (
BasalContactsDialog,
SamplerDialog,
SorterDialog,
ThicknessCalculatorDialog,
UserDefinedSorterDialog,
)

__all__ = [
'BasalContactsDialog',
'SamplerDialog',
'SorterDialog',
'ThicknessCalculatorDialog',
'UserDefinedSorterDialog',
]
165 changes: 165 additions & 0 deletions loopstructural/gui/map2loop_tools/basal_contacts_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""Widget for extracting basal contacts."""

import os

from PyQt5.QtWidgets import QMessageBox, QWidget
from qgis.PyQt import uic


class BasalContactsWidget(QWidget):
"""Widget for configuring and running the basal contacts extractor.

This widget provides a GUI interface for extracting basal contacts
from geology layers.
"""

def __init__(self, parent=None, data_manager=None):
"""Initialize the basal contacts widget.

Parameters
----------
parent : QWidget, optional
Parent widget.
data_manager : object, optional
Data manager for accessing shared data.
"""
super().__init__(parent)
self.data_manager = data_manager

# Load the UI file
ui_path = os.path.join(os.path.dirname(__file__), "basal_contacts_widget.ui")
uic.loadUi(ui_path, self)

# Move layer filter setup out of the .ui (QgsMapLayerProxyModel values in .ui
# can cause import errors outside QGIS). Set filters programmatically
# and preserve the allowEmptyLayer setting for the faults combobox.
try:
from qgis.core import QgsMapLayerProxyModel

# geology layer should only show polygon layers
self.geologyLayerComboBox.setFilters(QgsMapLayerProxyModel.PolygonLayer)

# faults should show line layers and allow empty selection (as set in .ui)
self.faultsLayerComboBox.setFilters(QgsMapLayerProxyModel.LineLayer)
try:
# QgsMapLayerComboBox has setAllowEmptyLayer method in newer QGIS versions
self.faultsLayerComboBox.setAllowEmptyLayer(True)
except Exception:
# Older QGIS bindings may use allowEmptyLayer property; ignore if unavailable
pass
except Exception:
# If QGIS isn't available (e.g. editing the UI outside QGIS), skip setting filters
pass

# Connect signals
self.geologyLayerComboBox.layerChanged.connect(self._on_geology_layer_changed)
self.runButton.clicked.connect(self._run_extractor)

# Set up field combo boxes
self._setup_field_combo_boxes()

def _setup_field_combo_boxes(self):
"""Set up field combo boxes to link to their respective layers."""
geology = self.geologyLayerComboBox.currentLayer()
if geology is not None:
self.unitNameFieldComboBox.setLayer(geology)
self.formationFieldComboBox.setLayer(geology)
else:
# Ensure combo boxes are cleared if no geology layer selected
try:
self.unitNameFieldComboBox.setLayer(None)
self.formationFieldComboBox.setLayer(None)
except Exception:
pass

def _on_geology_layer_changed(self):
"""Update field combo boxes when geology layer changes."""
layer = self.geologyLayerComboBox.currentLayer()
self.unitNameFieldComboBox.setLayer(layer)
self.formationFieldComboBox.setLayer(layer)

def _run_extractor(self):
"""Run the basal contacts extraction algorithm."""
from qgis import processing
from qgis.core import QgsProcessingFeedback

# Validate inputs
if not self.geologyLayerComboBox.currentLayer():
QMessageBox.warning(self, "Missing Input", "Please select a geology layer.")
return

if not self.stratiColumnComboBox.currentLayer():
QMessageBox.warning(self, "Missing Input", "Please select a stratigraphic order layer.")
return

# Parse ignore units
ignore_units = []
if self.ignoreUnitsLineEdit.text().strip():
ignore_units = [
unit.strip() for unit in self.ignoreUnitsLineEdit.text().split(',') if unit.strip()
]

# Prepare parameters
params = {
'GEOLOGY': self.geologyLayerComboBox.currentLayer(),
'UNIT_NAME_FIELD': self.unitNameFieldComboBox.currentField(),
'FORMATION_FIELD': self.formationFieldComboBox.currentField(),
'FAULTS': self.faultsLayerComboBox.currentLayer(),
'STRATIGRAPHIC_COLUMN': self.stratiColumnComboBox.currentLayer(),
'IGNORE_UNITS': ignore_units,
'OUTPUT': 'TEMPORARY_OUTPUT',
'ALL_CONTACTS': 'TEMPORARY_OUTPUT',
}

# Run the algorithm
try:
feedback = QgsProcessingFeedback()
result = processing.run("plugin_map2loop:basal_contacts", params, feedback=feedback)

if result:
QMessageBox.information(self, "Success", "Basal contacts extracted successfully!")
else:
QMessageBox.warning(self, "Error", "Failed to extract basal contacts.")

except Exception as e:
QMessageBox.critical(self, "Error", f"An error occurred: {str(e)}")

def get_parameters(self):
"""Get current widget parameters.

Returns
-------
dict
Dictionary of current widget parameters.
"""
ignore_units = []
if self.ignoreUnitsLineEdit.text().strip():
ignore_units = [
unit.strip() for unit in self.ignoreUnitsLineEdit.text().split(',') if unit.strip()
]

return {
'geology_layer': self.geologyLayerComboBox.currentLayer(),
'unit_name_field': self.unitNameFieldComboBox.currentField(),
'formation_field': self.formationFieldComboBox.currentField(),
'faults_layer': self.faultsLayerComboBox.currentLayer(),
'strati_column': self.stratiColumnComboBox.currentLayer(),
'ignore_units': ignore_units,
}

def set_parameters(self, params):
"""Set widget parameters.

Parameters
----------
params : dict
Dictionary of parameters to set.
"""
if 'geology_layer' in params and params['geology_layer']:
self.geologyLayerComboBox.setLayer(params['geology_layer'])
if 'faults_layer' in params and params['faults_layer']:
self.faultsLayerComboBox.setLayer(params['faults_layer'])
if 'strati_column' in params and params['strati_column']:
self.stratiColumnComboBox.setLayer(params['strati_column'])
if 'ignore_units' in params and params['ignore_units']:
self.ignoreUnitsLineEdit.setText(', '.join(params['ignore_units']))
Loading