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 @@ -282,17 +282,17 @@ Files: `stratigraphic_column/*.py`, `stratigraphic_column/*.ui`,

### Phase 3: Step-based dock

- [ ] Add a step navigation widget with a status for each step.
- [ ] Add a `check()` function for each step. It returns a status and a list of
- [x] Add a step navigation widget with a status for each step.
- [x] Add a `check()` function for each step. It returns a status and a list of
problems.
- [ ] Move the existing tabs into the steps, in the order of the target design.
- [ ] Move Save, Open, Reset and Settings into the dock header.
- [ ] Move the dialogs into their steps as buttons or menus. Keep the dialogs.
- [x] Move the existing tabs into the steps, in the order of the target design.
- [x] Move Save, Open, Reset and Settings into the dock header.
- [x] Move the dialogs into their steps as buttons or menus. Keep the dialogs.
Do not rewrite them in this phase.
- [ ] Move the Fault Topology Calculator into step 3.
- [ ] Reduce the toolbar to three actions. Put the dialogs in a "Tools"
- [x] Move the Fault Topology Calculator into step 3.
- [x] Reduce the toolbar to three actions. Put the dialogs in a "Tools"
submenu.
- [ ] Keep the `separate_dock_widgets` setting.
- [x] Keep the `separate_dock_widgets` setting.

Files: `loop_widget.py`, `modelling/modelling_widget.py`, `plugin_main.py`,
new `gui/modelling/steps/` module.
Expand Down
13 changes: 13 additions & 0 deletions docs/usage/interface.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Plugin interface

## The steps of the dock
The LoopStructural dock has five steps. The step buttons are at the top of the dock. Each button shows its status: a check mark (done), a warning triangle (a problem) or a hollow circle (not done). Point at a button to see the reasons. The footer shows the most important message of the current step, with **Back** and **Next** buttons. The steps are a guide: you can go to any step at any time.

1. **Data**: bounding box, DEM and the source layers. **Convert data...** opens the data conversion tool.
2. **Stratigraphy**: the stratigraphic column. The **Build column** menu has the sorters and Paint Order. The **Derive from map** menu has Basal Contacts, Thickness and Sampler.
3. **Faults**: the fault layer, **Calculate topology...** and the adjacency tables. This step is optional.
4. **Model**: the features and the build of the model.
5. **View**: the 3D view. With the setting "separate dock widgets", this step has a button that opens the 3D view dock.

The header of the dock has **Save**, **Open**, **Reset** and **Settings**. They apply to all of the plugin.

The toolbar has three actions: LoopStructural (the dock), 3D View and Help. The **Tools** submenu of the Plugins menu has the map2loop dialogs for advanced users.

## Selecting Layers
The LoopStructural plugin interfaces with QGIS to define the model input data and parameters.

Expand Down
43 changes: 27 additions & 16 deletions loopstructural/gui/loop_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,68 @@
interface for interacting with LoopStructural features inside QGIS.
"""

from qgis.PyQt.QtWidgets import QTabWidget, QVBoxLayout, QWidget
from qgis.PyQt.QtWidgets import QVBoxLayout, QWidget

from .modelling.modelling_widget import ModellingWidget
from .modelling.steps import checks
from .visualisation.visualisation_widget import VisualisationWidget


class LoopWidget(QWidget):
"""Main dock widget that contains modelling and visualisation tools.

The widget composes multiple tabs and controls used to construct and
inspect geological models.
The widget has the steps of the modelling workflow. The last step is the
3D view. With ``separate_docks``, the 3D view is a widget for its own dock,
and the last step has a button that opens that dock.
"""

def __init__(
self, parent=None, *, mapCanvas=None, logger=None, data_manager=None, model_manager=None
self,
parent=None,
*,
mapCanvas=None,
logger=None,
data_manager=None,
model_manager=None,
separate_docks=False,
):
"""Initialize the Loop widget.

Parameters
----------
*args, **kwargs
Forwarded to the parent widget constructor.
separate_docks : bool
If True, the visualisation widget is not in the steps. The caller
puts it in its own dock.
"""
super().__init__(parent)
self.mapCanvas = mapCanvas
self.logger = logger
self.data_manager = data_manager
self.model_manager = model_manager

mainLayout = QVBoxLayout(self)
self.setLayout(mainLayout)
tabWidget = QTabWidget(self)
tabWidget.setTabPosition(QTabWidget.TabPosition.South)
mainLayout.addWidget(tabWidget)
self.modelling_widget = ModellingWidget(
self.visualisation_widget = VisualisationWidget(
self,
mapCanvas=self.mapCanvas,
logger=self.logger,
data_manager=self.data_manager,
model_manager=self.model_manager,
)

self.visualisation_widget = VisualisationWidget(
self.modelling_widget = ModellingWidget(
self,
mapCanvas=self.mapCanvas,
logger=self.logger,
data_manager=self.data_manager,
model_manager=self.model_manager,
view_widget=None if separate_docks else self.visualisation_widget,
)
tabWidget.addTab(self.modelling_widget, "Modelling")
tabWidget.addTab(self.visualisation_widget, "Visualisation")

mainLayout = QVBoxLayout(self)
mainLayout.setContentsMargins(0, 0, 0, 0)
mainLayout.addWidget(self.modelling_widget)

def show_view_step(self):
"""Go to the last step, which has the 3D view."""
self.modelling_widget.show_step(checks.STEP_VIEW)

def get_modelling_widget(self):
"""Return the modelling widget instance.
Expand Down
57 changes: 57 additions & 0 deletions loopstructural/gui/map2loop_tools/launchers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Open the tool dialogs from the dock steps and from the plugin menu."""

SAMPLER = 'sampler'
SORTER = 'sorter'
USER_SORTER = 'user_sorter'
BASAL_CONTACTS = 'basal_contacts'
THICKNESS = 'thickness'
PAINT_STRAT_ORDER = 'paint_strat_order'
FAULT_TOPOLOGY = 'fault_topology'
DATA_CONVERSION = 'data_conversion'


def _dialog_class(tool):
"""Import the dialog class of a tool when it is needed (the imports are slow)."""
if tool == DATA_CONVERSION:
from loopstructural.gui.data_conversion import AutomaticConversionDialog

return AutomaticConversionDialog
if tool == FAULT_TOPOLOGY:
from loopstructural.gui.map2loop_tools.fault_topology_widget import FaultTopologyWidget

return FaultTopologyWidget
from loopstructural.gui import map2loop_tools

names = {
SAMPLER: 'SamplerDialog',
SORTER: 'SorterDialog',
USER_SORTER: 'UserDefinedSorterDialog',
BASAL_CONTACTS: 'BasalContactsDialog',
THICKNESS: 'ThicknessCalculatorDialog',
PAINT_STRAT_ORDER: 'PaintStratigraphicOrderDialog',
}
return getattr(map2loop_tools, names[tool])


def show_tool_dialog(tool, parent, *, data_manager, debug_manager=None):
"""Open the dialog of a tool and wait until the user closes it.

Parameters
----------
tool : str
One of the names in this module, for example `BASAL_CONTACTS`.
parent : QWidget
Parent of the dialog.
data_manager : ModellingDataManager
The dialogs read the shared layers from the data manager.
debug_manager : DebugManager, optional
If this is None, the debug manager of the data manager is used.
"""
dialog_class = _dialog_class(tool)
if tool == DATA_CONVERSION:
dialog = dialog_class(parent, project=data_manager.project if data_manager else None)
else:
if debug_manager is None and data_manager is not None:
debug_manager = data_manager.debug_manager
dialog = dialog_class(parent, data_manager=data_manager, debug_manager=debug_manager)
dialog.exec()
4 changes: 2 additions & 2 deletions loopstructural/gui/modelling/geological_history_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ class GeologialHistoryTab(BaseTab):
def __init__(self, parent=None, data_manager=None):
super().__init__(parent, data_manager, scrollable=False)
# Load the UI file for Tab 1
stratigraphic_column_widget = StratColumnWidget(self, data_manager=data_manager)
self.stratigraphic_column_widget = StratColumnWidget(self, data_manager=data_manager)
# Add the loaded UI widget to the container layout
self.add_widget(stratigraphic_column_widget, group_box=False)
self.add_widget(self.stratigraphic_column_widget, group_box=False)
119 changes: 10 additions & 109 deletions loopstructural/gui/modelling/model_definition/model_definition_tab.py
Original file line number Diff line number Diff line change
@@ -1,129 +1,30 @@
from qgis.core import QgsApplication
from qgis.PyQt.QtWidgets import (
QFileDialog,
QHBoxLayout,
QMessageBox,
QPushButton,
QSizePolicy,
QWidget,
)
from qgis.PyQt.QtWidgets import QSizePolicy

from loopstructural.gui.messages import push_success
from loopstructural.gui.modelling.base_tab import BaseTab

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

STATE_FILE_SUFFIX = '.loopstate.json'
STATE_FILE_FILTER = f"LoopStructural State (*{STATE_FILE_SUFFIX})"


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

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.fault_layers = FaultLayersWidget(self, data_manager)
self.stratigraphy_layers = StratigraphicLayersWidget(self, data_manager)

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

actions_widget = QWidget(self)
actions_layout = QHBoxLayout(actions_widget)
actions_layout.setContentsMargins(0, 0, 0, 0)

self.saveStateButton = QPushButton("Save State")
self.saveStateButton.setIcon(QgsApplication.getThemeIcon("mActionFileSave.svg"))
self.saveStateButton.setToolTip(
"Save the currently loaded data and geological model to a file."
)
self.saveStateButton.clicked.connect(self.on_save_state_clicked)

self.loadStateButton = QPushButton("Load State")
self.loadStateButton.setIcon(QgsApplication.getThemeIcon("mActionFileOpen.svg"))
self.loadStateButton.setToolTip(
"Load a previously saved application state, replacing all currently "
"loaded data and the geological model."
)
self.loadStateButton.clicked.connect(self.on_load_state_clicked)

self.resetStateButton = QPushButton("Reset Application State")
self.resetStateButton.setIcon(QgsApplication.getThemeIcon("mActionUndo.svg"))
self.resetStateButton.setToolTip(
"Clear all loaded data, the stratigraphic column, fault topology and "
"the geological model, restoring the plugin to its initial state."
)
self.resetStateButton.clicked.connect(self.on_reset_state_clicked)

actions_layout.addWidget(self.saveStateButton)
actions_layout.addWidget(self.loadStateButton)
actions_layout.addWidget(self.resetStateButton)
self.add_widget(actions_widget, group_box=False)

self.add_widget(self.bounding_box, 'Bounding Box') # , "Bounding Box")
self.add_widget(self.bounding_box, 'Bounding Box')
self.add_widget(self.dem, 'DEM')
self.add_widget(self.fault_layers, 'Fault Layers') # , "Fault Layers")
self.add_widget(
self.stratigraphy_layers, 'Stratigraphic Layers'
) # , "Stratigraphic Layers")

def on_save_state_clicked(self):
"""Prompt for a destination file and save the current app state to it."""
filepath, _ = QFileDialog.getSaveFileName(
self, "Save Application State", "", STATE_FILE_FILTER
)
if not filepath:
return
if not filepath.endswith(STATE_FILE_SUFFIX):
filepath += STATE_FILE_SUFFIX
try:
self.data_manager.save_state(filepath)
except Exception as err:
QMessageBox.critical(
self, "Save Application State", f"Failed to save application state:\n{err}"
)
else:
push_success("Save Application State", f"Application state saved to: {filepath}")

def on_load_state_clicked(self):
"""Prompt for a state file and, after confirmation, load it."""
filepath, _ = QFileDialog.getOpenFileName(
self, "Load Application State", "", STATE_FILE_FILTER
)
if not filepath:
return
reply = QMessageBox.question(
self,
"Load Application State",
"Loading a saved state will replace all currently loaded data and "
"the geological model. This cannot be undone.\n\nContinue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
try:
self.data_manager.load_state(filepath)
except Exception as err:
QMessageBox.critical(
self, "Load Application State", f"Failed to load application state:\n{err}"
)

def on_reset_state_clicked(self):
"""Prompt for confirmation, then reset the data and model managers."""
reply = QMessageBox.question(
self,
"Reset Application State",
"This will clear all loaded data, the stratigraphic column, fault "
"topology and the geological model. This cannot be undone.\n\n"
"Continue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
self.data_manager.reset()
self.add_widget(self.stratigraphy_layers, 'Stratigraphic Layers')
Loading
Loading