Skip to content
Open
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
16 changes: 8 additions & 8 deletions docs/development/usability-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,30 +410,30 @@ input is the polygon geology layer, so the user cannot select it there.

Tasks:

- [ ] Remove `StratigraphicLayersWidget` from `ModelDefinitionTab`. Step 1 has
- [x] Remove `StratigraphicLayersWidget` from `ModelDefinitionTab`. Step 1 has
only the bounding box, the CRS and the DEM.
- [ ] Add the widget to `StratigraphyStep`, as a collapsible group (see 6.3).
- [ ] The first layer picker depends on the contacts source:
- [x] Add the widget to `StratigraphyStep`, as a collapsible group (see 6.3).
- [x] The first layer picker depends on the contacts source:
- "Calculate from geology polygons": the picker shows polygon layers. It
reads and writes the `geology` and `geology_unit_field` roles. It does not
call `set_basal_contacts`. The Z-coordinate check box is hidden.
- "Use a contacts layer": the picker shows line and point layers, as now. It
reads and writes the `basal_contacts` role.
- Change the group title and the label to match ("Geology layer" or
"Contacts layer").
- [ ] When the user changes the source, do not write the old selection to the
- [x] When the user changes the source, do not write the old selection to the
other role. Keep one selection for each source.
- [ ] The `set_basal_contacts` callback (a tool or a build made a new contacts
- [x] The `set_basal_contacts` callback (a tool or a build made a new contacts
layer) does not change the picker in the "geology" source. The layer is for
display.
- [ ] Keep the geology picker of the column group in sync with the new picker
- [x] Keep the geology picker of the column group in sync with the new picker
(both use the same roles). Decide in the review if one of them must be
removed (see the open questions).
- [x] Move the "Select the geology layer" and "Select the structure layer"
items from `check_data` to `check_stratigraphy`.
- [ ] Update the text that says "in step 1" for these layers
- [x] Update the text that says "in step 1" for these layers
(`derived_refresh.py`, `checks.py`, `pages.py`, the docs).
- [ ] Keep the saved widget settings key `stratigraphic_layers_widget`, so old
- [x] Keep the saved widget settings key `stratigraphic_layers_widget`, so old
state files load.

Acceptance: with "Calculate from geology polygons", the user selects the
Expand Down
8 changes: 4 additions & 4 deletions docs/usage/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The faults trace layer is usually a line layer that contains the trace of the fa

![Fault Layer](../static/fault_layers.png)
### Stratigraphy
Two layers can be used to constrain the stratigraphy of the model:
The **Source layers** group is at the top of step 2. Two layers can be used to constrain the stratigraphy of the model:
1. Basal contacts - this layer defines the basal contacts of the stratigraphy. The layer should contain a line layer with the contact traces. The attributes can be used to define the name of the contact.
2. Structural data - this layer defines the structural data that is used to constrain the model. The layer should contain a point layer with the structural data. The attributes can be used to define the orientation of the data, such as dip and dip direction.

Expand All @@ -58,9 +58,9 @@ You select each layer one time. The plugin keeps the geology layer, the unit nam

The geology layer is the layer that you select in the Stratigraphic Column tab. If you select a layer in one of the tools and no geology layer is set, the plugin uses that layer for the other tools. The plugin saves the shared layers with the application state.

The **Source** setting in the basal contacts group selects where the basal contacts come from:
- **Calculate from geology polygons** (default). The Basal Contacts tool extracts the contacts from the geology layer and the stratigraphic column. When it finishes, the new layer becomes the basal contacts layer of the model.
- **Use a contacts layer**. Your own layer is an input. The plugin does not change it, and the Thickness Calculator uses it.
The **Source** setting in the **Source layers** group of step 2 selects where the basal contacts come from. The first layer picker follows the setting. Each source keeps its own selection:
- **Calculate from geology polygons** (default). The picker shows polygon layers. It sets the geology layer and the unit name field. The Basal Contacts tool extracts the contacts from the geology layer and the stratigraphic column. When it finishes, the new layer becomes the basal contacts layer of the model.
- **Use a contacts layer**. The picker shows line and point layers. Your own layer is an input. The plugin does not change it, and the Thickness Calculator uses it.

## Stratigraphic Column
The stratigraphic column defines the order of the contacts and any unconformable relationships between them. The column is defined by a list of units - these units are ordered from oldest at the bottom to youngest at the top. Unconformities can be inserted between units to define an unconformable relationship. The thicknesses define the true thickness of each unit and are used to parameterise the interpolation. The unit names should match the names of the contacts in the basal contacts layer. Units without basal contacts can be included in the stratigraphic column but will not be constrained by any data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,24 @@

from .bounding_box import BoundingBoxWidget
from .dem import DEMWidget
from .stratigraphic_layers import StratigraphicLayersWidget


class ModelDefinitionTab(BaseTab):
"""The area, the elevation and the source layers (step 1 of the dock).
"""The area and the elevation (step 1 of the dock).

The fault layers are in step 3. Save, Open and Reset are in the header of
the dock.
The geology, contacts and structure layers are in step 2. The fault layers
are in step 3. Save, Open and Reset are in the header of the dock.
"""

def __init__(self, parent=None, data_manager=None):
super().__init__(parent, data_manager, scrollable=True)
# Add widgets to the QToolBox
self.bounding_box = BoundingBoxWidget(self, data_manager)
self.dem = DEMWidget(self, data_manager)
self.stratigraphy_layers = StratigraphicLayersWidget(self, data_manager)

# Set uniform size policy for all widgets
for widget in [self.bounding_box, self.dem, self.stratigraphy_layers]:
for widget in [self.bounding_box, self.dem]:
widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)

self.add_widget(self.bounding_box, 'Bounding Box')
self.add_widget(self.dem, 'DEM')
self.add_widget(self.stratigraphy_layers, 'Stratigraphic Layers')
126 changes: 102 additions & 24 deletions loopstructural/gui/modelling/model_definition/stratigraphic_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def __init__(self, parent=None, data_manager=None):
if data_manager is None:
raise ValueError("data_manager must be provided")
self.data_manager = data_manager
self._loading = False
super().__init__(parent)
ui_path = os.path.join(os.path.dirname(__file__), "stratigraphic_layers.ui")
uic.loadUi(ui_path, self)
Expand Down Expand Up @@ -55,8 +56,58 @@ def __init__(self, parent=None, data_manager=None):
self.useStructuralPointsZCoordinatesCheckBox.stateChanged.connect(
self.onStructuralDataFieldChanged
)
self._guess_layers_and_fields()
self._guess_structure_layer()
self._restore_selection()
self._apply_contacts_source(self.data_manager.layer_roles.contacts_source)

@property
def _from_geology(self):
return self.data_manager.layer_roles.contacts_source == layer_roles.CONTACTS_FROM_GEOLOGY

def _apply_contacts_source(self, source):
"""Set the filter, the labels and the selection of the first picker for a source.

The geology source has one selection (the `geology` roles) and the layer
source has another (the `basal_contacts` role). Each one is kept.
"""
from_geology = source == layer_roles.CONTACTS_FROM_GEOLOGY
self._loading = True
try:
if from_geology:
configure_layer_combo(
self.basalContactsLayer,
QgsMapLayerProxyModel.Filter.PolygonLayer,
allow_empty=True,
)
self.groupBox_basalContacts.setTitle("Geology")
self.basalContactsLabel.setText("Geology layer")
self.label.setText("Unit name field")
roles = self.data_manager.layer_roles
layer = roles.get(layer_roles.GEOLOGY)
field = roles.get(layer_roles.GEOLOGY_UNIT_FIELD)
else:
configure_layer_combo(
self.basalContactsLayer,
QgsMapLayerProxyModel.Filter.LineLayer | QgsMapLayerProxyModel.Filter.PointLayer,
allow_empty=True,
)
self.groupBox_basalContacts.setTitle("Basal contacts")
self.basalContactsLabel.setText("Contacts layer")
self.label.setText("Unit name field")
layer = self.data_manager.layer_roles.get(layer_roles.BASAL_CONTACTS)
field = (self.data_manager._basal_contacts or {}).get('unitname_field')
if layer is None:
self._guess_basal_contacts()
layer = self.basalContactsLayer.currentLayer()
field = self.unitNameField.currentField() or field
self.basalContactsLayer.setLayer(layer)
self.unitNameField.setLayer(layer)
if field:
self.unitNameField.setField(field)
self.useBasalContactsZCoordinatesCheckBox.setVisible(not from_geology)
self.useZCoordinateLabel.setVisible(not from_geology)
finally:
self._loading = False

def _add_contacts_source_combo(self):
"""Add the choice of where the basal contacts come from."""
Expand Down Expand Up @@ -87,9 +138,14 @@ def _on_contacts_source_selected(self, index):
self.data_manager.layer_roles.contacts_source = self.contactsSourceComboBox.itemData(index)

def _on_layer_role_changed(self, role, value):
"""Show a contacts source that was set by a loaded state."""
"""Show a contacts source or a geology layer that something else set."""
if role == 'contacts_source':
self._show_contacts_source(value)
self._apply_contacts_source(value)
elif role in (layer_roles.GEOLOGY, layer_roles.GEOLOGY_UNIT_FIELD) and self._from_geology:
self._apply_contacts_source(layer_roles.CONTACTS_FROM_GEOLOGY)
elif role == layer_roles.BASAL_CONTACTS and not self._from_geology:
self._apply_contacts_source(layer_roles.CONTACTS_FROM_LAYER)

def enableBasalContactsZCheckBox(self, enable):
self.useBasalContactsZCoordinatesCheckBox.setEnabled(enable)
Expand All @@ -106,6 +162,9 @@ def enableStructuralPointsZCheckBox(self, enable):
self.useStructuralPointsZCoordinatesCheckBox.setChecked(False)

def set_basal_contacts(self, layer, unitname_field=None, use_z_coordinate=False):
if self._from_geology:
# The contacts layer is for display. The picker shows the geology layer.
return
self.basalContactsLayer.setLayer(layer)
if layer is not None and layer.isValid():
if layer.wkbType() != QgsWkbTypes.Type.Unknown:
Expand Down Expand Up @@ -153,9 +212,22 @@ def set_orientations_layer(
self.structural_points_use_z = use_z_coordinate
self.useStructuralPointsZCoordinatesCheckBox.setChecked(use_z_coordinate)

def _write_first_picker(self, layer, field):
"""Write the first picker to the role of the current source."""
if self._from_geology:
roles = self.data_manager.layer_roles
roles.set(layer_roles.GEOLOGY, layer)
roles.set(layer_roles.GEOLOGY_UNIT_FIELD, field or None)
else:
self.data_manager.set_basal_contacts(
layer, field, use_z_coordinate=self.basal_contacts_use_z
)

def onBasalContactsChanged(self, layer):
if self._loading:
return
self.unitNameField.setLayer(layer)
self.data_manager.set_basal_contacts(layer, self.unitNameField.currentField())
self._write_first_picker(layer, self.unitNameField.currentField())
self._persist_selection()

def onOrientationTypeChanged(self, index):
Expand Down Expand Up @@ -198,16 +270,13 @@ def onStructuralDataFieldChanged(self, field):
# self.updateDataManager()

def onUnitFieldChanged(self, field):
self.data_manager.set_basal_contacts(
self.basalContactsLayer.currentLayer(),
field,
use_z_coordinate=self.basal_contacts_use_z,
)
if self._loading:
return
self._write_first_picker(self.basalContactsLayer.currentLayer(), field)
self._persist_selection()

# self.updateDataManager()

def _guess_layers_and_fields(self):
def _guess_basal_contacts(self):
"""Select a layer that is named like a contacts layer, in the layer source."""
if not self.data_manager:
return
# Basal contacts
Expand All @@ -222,7 +291,10 @@ def _guess_layers_and_fields(self):
fmatcher = ColumnMatcher(fields)
if unit_match := fmatcher.find_match('UNITNAME'):
self.unitNameField.setField(unit_match)
# Structural data

def _guess_structure_layer(self):
if not self.data_manager:
return
structural_names = get_layer_names(self.structuralDataLayer)
structural_matcher = ColumnMatcher(structural_names)
structural_match = structural_matcher.find_match(
Expand All @@ -244,23 +316,27 @@ def _guess_layers_and_fields(self):
def _persist_selection(self):
if not self.data_manager:
return
settings = {
'basal_layer': (
settings = dict(self.data_manager.get_widget_settings('stratigraphic_layers_widget', {}))
if not self._from_geology:
# In the geology source the first picker holds the geology layer. The
# `geology` roles save it. Keep the contacts layer of the other source.
settings['basal_layer'] = (
self.basalContactsLayer.currentLayer().name()
if self.basalContactsLayer.currentLayer()
else None
),
)
settings['unit_name_field'] = self.unitNameField.currentField()
settings['use_basal_z'] = self.useBasalContactsZCoordinatesCheckBox.isChecked()
settings |= {
'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)
Expand All @@ -271,16 +347,18 @@ def _restore_selection(self):
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)
# The first picker of the geology source comes from the `geology` roles
if not self._from_geology:
if layer_name := settings.get('basal_layer'):
layer = self.data_manager.find_layer_by_name(layer_name)
if layer:
self.basalContactsLayer.setLayer(layer)
if field := settings.get('unit_name_field'):
self.unitNameField.setField(field)
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'):
Expand All @@ -293,7 +371,7 @@ def _restore_selection(self):
)
if idx >= 0:
self.orientationType.setCurrentIndex(idx)
if 'use_basal_z' in settings:
if 'use_basal_z' in settings and not self._from_geology:
self.useBasalContactsZCoordinatesCheckBox.setChecked(settings['use_basal_z'])
if 'use_structural_z' in settings:
self.useStructuralPointsZCoordinatesCheckBox.setChecked(settings['use_structural_z'])
26 changes: 22 additions & 4 deletions loopstructural/gui/modelling/steps/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
QHBoxLayout,
QLabel,
QMenu,
QFrame,
QPushButton,
QScrollArea,
QTabWidget,
QToolButton,
QVBoxLayout,
QWidget,
Expand All @@ -24,6 +27,9 @@
from loopstructural.gui.modelling.geological_model_tab import GeologicalModelTab
from loopstructural.gui.modelling.model_definition import ModelDefinitionTab
from loopstructural.gui.modelling.model_definition.fault_layers import FaultLayersWidget
from loopstructural.gui.modelling.model_definition.stratigraphic_layers import (
StratigraphicLayersWidget,
)

from loopstructural.main.workflow_mode import WORKFLOW_MODE_LABELS, WORKFLOW_MODES

Expand Down Expand Up @@ -64,7 +70,7 @@ def _tool_button(self, text, icon, tooltip, actions):


class DataStep(StepPage):
"""Step 1: the bounding box, the CRS, the DEM and the source layers."""
"""Step 1: the bounding box, the CRS and the DEM."""

key = checks.STEP_DATA

Expand All @@ -77,7 +83,7 @@ def __init__(self, parent=None, **kwargs):
convert.setToolTip("Convert the columns of your map data to the names that the tools use.")
convert.clicked.connect(lambda _checked=False: self._show_tool(launchers.DATA_CONVERSION))
row = QHBoxLayout()
row.addWidget(QLabel("Select the area and the source layers.", self), 1)
row.addWidget(QLabel("Select the area and the elevation.", self), 1)
row.addWidget(convert)
layout.addLayout(row)

Expand Down Expand Up @@ -111,7 +117,7 @@ def _on_mode_changed(self, _index):


class StratigraphyStep(StepPage):
"""Step 2: the stratigraphic column, and the results that come from the map."""
"""Step 2: the source layers, the stratigraphic column and the results from the map."""

key = checks.STEP_STRATIGRAPHY

Expand All @@ -134,6 +140,12 @@ def __init__(self, parent=None, **kwargs):
"Paint the order on the map...", lambda: self._show_tool(launchers.PAINT_STRAT_ORDER)
).setToolTip("Show the order of the column on the geology polygons.")

self.stratigraphy_layers = StratigraphicLayersWidget(self, self.data_manager)
layers_scroll = QScrollArea(self)
layers_scroll.setWidgetResizable(True)
layers_scroll.setFrameShape(QFrame.Shape.NoFrame)
layers_scroll.setWidget(self.stratigraphy_layers)

derive = self._tool_button(
"Derive from map",
QgsApplication.getThemeIcon('mActionSharingExport.svg'),
Expand All @@ -148,7 +160,13 @@ def __init__(self, parent=None, **kwargs):
row.addStretch(1)
row.addWidget(derive)
layout.addLayout(row)
layout.addWidget(self.tab, 1)

# Two tabs, so the page is not tall: the source layers and the column
self.tabs = QTabWidget(self)
self.tabs.addTab(layers_scroll, "Source layers")
self.tabs.addTab(self.tab, "Column")
self.tabs.setCurrentWidget(self.tab)
layout.addWidget(self.tabs, 1)


class FaultsStep(StepPage):
Expand Down
Loading
Loading