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
22 changes: 11 additions & 11 deletions docs/development/usability-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,26 +223,26 @@ visible. No modal dialog opens for a successful action.

### Phase 1: Shared layer roles

- [ ] Add layer roles to the data manager: `geology`, `geology_unit_field`,
- [x] Add layer roles to the data manager: `geology`, `geology_unit_field`,
`fault_traces`, `structure`, `basal_contacts`, `dem`. Save them with the
application state.
- [ ] Add a signal or observer event when a role changes.
- [ ] Make the "Load Data" widgets write to the roles.
- [ ] Make each map2loop dialog read the roles as its default values. The user
- [x] Add a signal or observer event when a role changes.
- [x] Make the "Load Data" widgets write to the roles.
- [x] Make each map2loop dialog read the roles as its default values. The user
can still change the value in the dialog.
- [ ] When Basal Contacts makes a new layer, set it as the `basal_contacts`
- [x] When Basal Contacts makes a new layer, set it as the `basal_contacts`
role.
- [ ] The stratigraphic column "geology layer" pickers use the `geology` role.
- [ ] Add the contacts source setting: "Calculate from geology polygons" or
- [x] The stratigraphic column "geology layer" pickers use the `geology` role.
- [x] Add the contacts source setting: "Calculate from geology polygons" or
"Use a contacts layer". Save it with the application state.
- [ ] Add a derived-data record to the data manager. For each result (basal
- [x] Add a derived-data record to the data manager. For each result (basal
contacts, calculated thickness, styled fields), keep a hash of its inputs
and a status: current or out of date.
- [ ] Compare the hash when the column, a layer role or a tool setting changes.
- [x] Compare the hash when the column, a layer role or a tool setting changes.
Send an event when a status changes.
- [ ] Record the source of each unit thickness: "typed" or "calculated". A
- [x] Record the source of each unit thickness: "typed" or "calculated". A
thickness that the user types in the column list is "typed".
- [ ] In step 2 (until phase 3, in the Stratigraphic Column tab), show the
- [x] In step 2 (until phase 3, in the Stratigraphic Column tab), show the
out-of-date results and an **Update** button.

Files: `main/data_manager.py`,
Expand Down
14 changes: 14 additions & 0 deletions docs/usage/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ Two layers can be used to constrain the stratigraphy of the model:

![Stratigraphic Layer](../static/stratigraphic_layer.png)

### Shared layers
You select each layer one time. The plugin keeps the geology layer, the unit name field, the fault traces, the structure layer, the basal contacts and the DEM as shared layers. The map2loop tools (Basal Contacts, Thickness Calculator, Sorter, Sampler and Paint Stratigraphic Order) show these layers as their default values. You can select another layer in a tool for one run.

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.

## 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 All @@ -37,6 +46,11 @@ The stratigraphic column can be initialised from the basal contacts layer by cli

![Stratigraphic Column](../static/stratigraphic_column_04.png)

### Out-of-date results
The basal contacts, the calculated thicknesses and the `strat_order` field on a map layer depend on the stratigraphic column and on the layers and settings of the tools that made them. When one of these inputs changes, the Stratigraphic Column tab shows a message, for example "Basal contacts are out of date (the order of the units changed)", with an **Update** button. The button runs the tool again with the settings of the project. The plugin does not calculate again after each change, so you can move many units first. If you move a unit and then move it back, the results stay current.

The plugin records if the thickness of a unit was typed or calculated. The Thickness Calculator does not replace a thickness that you typed. To use a calculated value, set the thickness of the unit to 0 first.


## Fault topology relationships

Expand Down
42 changes: 42 additions & 0 deletions loopstructural/gui/map2loop_tools/basal_contacts_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
from qgis.PyQt.QtCore import pyqtSignal
from qgis.PyQt.QtWidgets import QMessageBox, QWidget

from ...main import derived_data, layer_roles
from ...main.helpers import ColumnMatcher, get_layer_names
from ...main.m2l_api import extract_basal_contacts
from ...main.vectorLayerWrapper import addGeoDataFrameToproject
from ..background_task import finish_background_task, start_background_task
from ..compatibility import configure_layer_combo
from ..messages import push_success, push_warning
from .layer_defaults import adopt_geology_role, apply_layer_role_defaults


class BasalContactsWidget(QWidget):
Expand Down Expand Up @@ -63,6 +65,15 @@ def __init__(self, parent=None, data_manager=None, debug_manager=None):
# Set up field combo boxes
self._setup_field_combo_boxes()
self._restore_selection()
# The layers of the shared roles are the defaults of the tool
apply_layer_role_defaults(
self.data_manager,
{
layer_roles.GEOLOGY: self.geologyLayerComboBox,
layer_roles.FAULT_TRACES: self.faultsLayerComboBox,
},
unit_field_combo=self.unitNameFieldComboBox,
)

def set_debug_manager(self, debug_manager):
"""Attach a debug manager instance."""
Expand Down Expand Up @@ -164,6 +175,8 @@ def _restore_selection(self):
self.unitNameFieldComboBox.setField(field)
if units := settings.get('basal_override_units'):
self.basalOverrideUnitsLineEdit.setText(', '.join(units))
if units := settings.get('ignore_units'):
self.ignoreUnitsLineEdit.setText(', '.join(units))

def _persist_selection(self):
"""Persist current selections into data manager."""
Expand All @@ -182,6 +195,7 @@ def _persist_selection(self):
),
'unit_name_field': self.unitNameFieldComboBox.currentField(),
'basal_override_units': self._units_from_line_edit(self.basalOverrideUnitsLineEdit),
'ignore_units': self._units_from_line_edit(self.ignoreUnitsLineEdit),
}
self.data_manager.set_widget_settings('basal_contacts_widget', settings)

Expand Down Expand Up @@ -229,6 +243,23 @@ def _run_extractor(self):

target = self._make_extract_contacts_target()

geology = self.geologyLayerComboBox.currentLayer()
unit_name_field = self.unitNameFieldComboBox.currentField()
adopt_geology_role(self.data_manager, geology=geology, unit_field=unit_name_field)
# The inputs of this run, read before the run starts. A change during
# the run makes the result out of date.
self._run_inputs = (
self.data_manager.basal_contacts_inputs(
geology=geology,
unit_field=unit_name_field,
faults=self.faultsLayerComboBox.currentLayer(),
ignore_units=self._units_from_line_edit(self.ignoreUnitsLineEdit),
override_units=self._units_from_line_edit(self.basalOverrideUnitsLineEdit),
)
if self.data_manager
else None
)

self.setEnabled(False)
self._extractor_thread, self._extractor_worker, self._extractor_progress = (
start_background_task(
Expand Down Expand Up @@ -268,6 +299,7 @@ def _on_extractor_finished(self, payload):
self.data_manager.apply_stratigraphic_colours_to_layer(
basal_layer, 'basal_unit'
)
self._use_new_contacts(basal_layer)
else:
contact_type = None
if contact_type is None:
Expand All @@ -290,6 +322,16 @@ def _on_extractor_finished(self, payload):
else:
self.task_failed.emit()

def _use_new_contacts(self, basal_layer):
"""Make the new layer the basal contacts of the model, and record the run.

With the contacts source "Use a contacts layer", the contacts layer
of the user is an input and does not change.
"""
if self.data_manager.layer_roles.contacts_source == layer_roles.CONTACTS_FROM_GEOLOGY:
self.data_manager.set_basal_contacts(basal_layer, unitname_field='basal_unit')
self.data_manager.derived.record(derived_data.BASAL_CONTACTS, inputs=self._run_inputs)

def _on_extractor_error(self, traceback_text):
finish_background_task(
self._extractor_thread, self._extractor_worker, self._extractor_progress
Expand Down
57 changes: 57 additions & 0 deletions loopstructural/gui/map2loop_tools/layer_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Set the default layers of a map2loop tool from the shared layer roles.

The user selects a layer for a role one time. Each tool shows that layer as
its default. The user can select another layer in the tool for one run.
"""

from loopstructural.main import layer_roles


def apply_layer_role_defaults(data_manager, combos, *, unit_field_combo=None):
"""Select the layer of each role in the layer combo boxes.

Parameters
----------
data_manager : ModellingDataManager or None
Data manager that has the layer roles.
combos : dict
Role name -> `QgsMapLayerComboBox`. A role with no layer does not
change its combo box.
unit_field_combo : QgsFieldComboBox, optional
Combo box of the unit name field of the geology layer. It shows the
`geology_unit_field` role when the field is in the geology layer.
"""
if data_manager is None:
return
for role, combo in combos.items():
layer = data_manager.layer_roles.get(role)
if layer is not None:
combo.setLayer(layer)
if unit_field_combo is not None:
geology = data_manager.layer_roles.get(layer_roles.GEOLOGY)
field = data_manager.layer_roles.get(layer_roles.GEOLOGY_UNIT_FIELD)
if geology is not None and field and geology.fields().indexFromName(field) >= 0:
if unit_field_combo.layer() is None or unit_field_combo.layer().id() != geology.id():
unit_field_combo.setLayer(geology)
unit_field_combo.setField(field)


def adopt_geology_role(data_manager, *, geology=None, unit_field=None):
"""Give the geology role the layer that a tool used, if it has no layer.

The other roles follow the Load Data widgets, so a tool does not set
them. A role that has a layer does not change. The unit field is set only
when the geology layer of the role is the layer of the tool.
"""
if data_manager is None:
return
data_manager.adopt_layer_roles(geology=geology)
current = data_manager.layer_roles.get(layer_roles.GEOLOGY)
if (
unit_field
and geology is not None
and current is not None
and current.id() == geology.id()
and data_manager.layer_roles.get(layer_roles.GEOLOGY_UNIT_FIELD) is None
):
data_manager.layer_roles.set(layer_roles.GEOLOGY_UNIT_FIELD, unit_field)
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from qgis.PyQt import uic
from qgis.PyQt.QtWidgets import QMessageBox, QWidget

from ...main import derived_data, layer_roles
from ...main.m2l_api import paint_stratigraphic_order
from ..compatibility import configure_layer_combo
from .layer_defaults import adopt_geology_role, apply_layer_role_defaults


class PaintStratigraphicOrderWidget(QWidget):
Expand Down Expand Up @@ -69,6 +71,12 @@ def __init__(self, parent=None, data_manager=None, debug_manager=None):

# Set up field combo boxes
self._setup_field_combo_boxes()
# The geology layer of the shared role is the default of the tool
apply_layer_role_defaults(
self.data_manager,
{layer_roles.GEOLOGY: self.geologyLayerComboBox},
unit_field_combo=self.unitNameFieldComboBox,
)

def set_debug_manager(self, debug_manager):
"""Attach a debug manager instance."""
Expand Down Expand Up @@ -159,6 +167,15 @@ def _run_painter(self):
paint_stratigraphic_order(
geology_layer, stratigraphic_order, unit_name_field, debug_manager=self._debug
)
if self.data_manager:
adopt_geology_role(
self.data_manager, geology=geology_layer, unit_field=unit_name_field
)
self.data_manager.derived.record(
derived_data.STYLED_FIELDS,
inputs=self.data_manager.styled_fields_inputs(),
detail={'layer': geology_layer.name(), 'field': unit_name_field},
)

# If requested, duplicate layer and apply style using selected colour ramp
try:
Expand Down
11 changes: 11 additions & 0 deletions loopstructural/gui/map2loop_tools/sampler_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@

from loopstructural.gui.background_task import finish_background_task, start_background_task
from loopstructural.gui.compatibility import configure_layer_combo
from loopstructural.main import layer_roles
from loopstructural.toolbelt.preferences import PlgOptionsManager

from .layer_defaults import apply_layer_role_defaults


class SamplerWidget(QWidget):
"""Widget for configuring and running the sampler.
Expand Down Expand Up @@ -55,6 +58,14 @@ def __init__(self, parent=None, data_manager=None, debug_manager=None):
QgsMapLayerProxyModel.Filter.LineLayer | QgsMapLayerProxyModel.Filter.PointLayer,
)
# spatialData can be any type, leave default
# The layers of the shared roles are the defaults of the tool
apply_layer_role_defaults(
self.data_manager,
{
layer_roles.DEM: self.dtmLayerComboBox,
layer_roles.GEOLOGY: self.geologyLayerComboBox,
},
)

# Initialize sampler types
self.sampler_types = ["Decimator", "Spacing"]
Expand Down
18 changes: 18 additions & 0 deletions loopstructural/gui/map2loop_tools/sorter_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from loopstructural.gui.background_task import finish_background_task, start_background_task
from loopstructural.gui.compatibility import configure_layer_combo
from loopstructural.main import layer_roles
from loopstructural.main.helpers import get_layer_names
from loopstructural.main.m2l_api import (
PARAMETERS_DICTIONARY,
Expand All @@ -17,6 +18,8 @@
)
from loopstructural.toolbelt.preferences import PlgOptionsManager

from .layer_defaults import adopt_geology_role, apply_layer_role_defaults


class SorterWidget(QWidget):
"""Widget for configuring and running the automatic stratigraphic sorter.
Expand Down Expand Up @@ -80,6 +83,16 @@ def __init__(self, parent=None, data_manager=None, debug_manager=None):
# Set up field combo boxes
self._setup_field_combo_boxes()
self._restore_selection()
# The layers of the shared roles are the defaults of the tool
apply_layer_role_defaults(
self.data_manager,
{
layer_roles.GEOLOGY: self.geologyLayerComboBox,
layer_roles.STRUCTURE: self.structureLayerComboBox,
layer_roles.DEM: self.dtmLayerComboBox,
},
unit_field_combo=self.unitNameFieldComboBox,
)

# Initial state update
self._on_algorithm_changed()
Expand Down Expand Up @@ -365,6 +378,11 @@ def _run_sorter(self):
if not self.geologyLayerComboBox.currentLayer():
QMessageBox.warning(self, "Missing Input", "Please select a geology layer.")
return False
adopt_geology_role(
self.data_manager,
geology=self.geologyLayerComboBox.currentLayer(),
unit_field=self.unitNameFieldComboBox.currentField(),
)

algorithm_index = self.sortingAlgorithmComboBox.currentIndex()
algorithm_name = self.sorting_algorithms[algorithm_index]
Expand Down
Loading
Loading