Skip to content
27 changes: 14 additions & 13 deletions docs/development/usability-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,27 +304,27 @@ Acceptance: a new user can go from an empty project to a solved model with the

### Phase 4: Model step and direct interpolation

- [ ] Replace "Initialize Model", "Solve Model" and "Update Model Data" with one
- [x] Replace "Initialize Model", "Solve Model" and "Update Model Data" with one
primary button. Its text and action come from the model state.
- [ ] Show the problems from all steps before the build.
- [ ] Before the build, calculate all out-of-date derived data again: basal
- [x] Show the problems from all steps before the build.
- [x] Before the build, calculate all out-of-date derived data again: basal
contacts first, then calculated thicknesses. Run this in a background
task with progress. If the calculation fails, stop the build and show
the error.
- [ ] With "Calculate from geology polygons", give the extracted contacts
- [x] With "Calculate from geology polygons", give the extracted contacts
directly to the model. Update the project contacts layer for display
only.
- [ ] Add the start choice: "Build from a geological map" or "Interpolate
- [x] Add the start choice: "Build from a geological map" or "Interpolate
surfaces from constraints". Save the choice with the state.
- [ ] Constraint list for each feature: source layer, constraint type, field
- [x] Constraint list for each feature: source layer, constraint type, field
mapping, weight, Z source (layer Z, DEM or constant).
- [ ] Add the constraint types: value, interface, gradient/normal, tangent,
- [x] Add the constraint types: value, interface, gradient/normal, tangent,
inequality, pairwise inequality.
- [ ] Show generated constraints as read-only rows. Add "Detach" to make a
- [x] Show generated constraints as read-only rows. Add "Detach" to make a
generated feature editable.
- [ ] Build and preview one feature: an isoline on the map canvas, or a surface
- [x] Build and preview one feature: an isoline on the map canvas, or a surface
in the 3D view.
- [ ] Implement "Add Fault" in the model step.
- [x] Implement "Add Fault" in the model step.

Files: `geological_model_tab/*.py`, `layer_selection_table.py`,
`feature_details_panel/*.py`, `main/model_manager.py`.
Expand Down Expand Up @@ -363,9 +363,10 @@ model, the model uses basal contacts and thicknesses that match the new order.
done), or only a guide? Recommendation: only a guide. Show problems, but do
not lock steps.
2. Is the Data Conversion dialog part of step 1, or a separate tool?
3. In direct mode, which constraint types does each interpolator (FDI, PLI,
surfe) support? The UI must hide types that the selected interpolator does
not support.
3. ~~In direct mode, which constraint types does each interpolator (FDI, PLI,
surfe) support?~~ Resolved: all interpolators accept the same constraint
types. LoopStructural converts them as needed. The UI shows the same types
for every interpolator and does not hide any.
4. Is there a minimum QGIS version for the new widgets?
5. Must the Processing algorithms also use the derived-data record, or only
the dock? Recommendation: only the dock. Processing runs are single runs
Expand Down
159 changes: 138 additions & 21 deletions loopstructural/gui/modelling/geological_model_tab/add_fault_dialog.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,150 @@
import os
from qgis.PyQt.QtWidgets import (
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFormLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QVBoxLayout,
)

from qgis.PyQt.QtWidgets import QDialog
from qgis.PyQt.uic import loadUi
from ....main import parametric_fault


def _spin(low, high, decimals=2, step=1.0):
spin = QDoubleSpinBox()
spin.setRange(low, high)
spin.setDecimals(decimals)
spin.setSingleStep(step)
return spin


class AddFaultDialog(QDialog):
def __init__(self, parent=None):
"""Ask for a fault that is given by numbers: a centre, a strike, a dip and a size.

Faults that have a trace on the map come from the fault layer in step 3.
The start values come from the bounding box of the model.
"""

def __init__(self, parent=None, *, model_manager=None):
super().__init__(parent)
ui_path = os.path.join(os.path.dirname(__file__), 'add_fault_dialog.ui')
loadUi(ui_path, self)
self.setWindowTitle('Add Fault Feature')
# You can access widgets by their objectName from the .ui file
# Example: self.strike_input, self.dip_input, etc.
self.model_manager = model_manager
self.setWindowTitle('Add Fault')

layout = QVBoxLayout(self)
name_form = QFormLayout()
self.name_input = QLineEdit()
name_form.addRow("Name:", self.name_input)
layout.addLayout(name_form)

orientation = QGroupBox("Orientation")
form = QFormLayout(orientation)
self.strike_input = _spin(0, 360)
self.strike_input.setToolTip("Degrees clockwise from north. The fault dips to the right.")
self.dip_input = _spin(0.01, 90)
self.pitch_input = _spin(-180, 180)
self.pitch_input.setToolTip(
"The angle of the slip in the fault plane, from the strike direction towards "
"the down-dip direction. 0 is strike-slip. 90 is dip-slip."
)
self.displacement_input = _spin(-1e9, 1e9)
self.displacement_input.setToolTip("The size of the slip, in model units.")
form.addRow("Strike (°):", self.strike_input)
form.addRow("Dip (°):", self.dip_input)
form.addRow("Pitch (°):", self.pitch_input)
form.addRow("Displacement:", self.displacement_input)
layout.addWidget(orientation)

position = QGroupBox("Position")
form = QFormLayout(position)
centre_row = QHBoxLayout()
self.centre_inputs = [_spin(-1e9, 1e9) for _ in range(3)]
for label, spin in zip("XYZ", self.centre_inputs):
centre_row.addWidget(QLabel(label))
centre_row.addWidget(spin)
form.addRow("Centre:", centre_row)
layout.addWidget(position)

size = QGroupBox("Size")
form = QFormLayout(size)
self.major_input = _spin(0.01, 1e9)
self.major_input.setToolTip(
"The full length of the fault in its plane, at right angles to the slip. "
"With a pitch of 90 (dip slip), this is along the strike."
)
self.intermediate_input = _spin(0.01, 1e9)
self.intermediate_input.setToolTip(
"How far the fault extends from the centre in the direction of the slip. "
"With a pitch of 90 (dip slip), this is down the dip."
)
self.minor_input = _spin(0.01, 1e9)
self.minor_input.setToolTip("How far the fault changes the field, across the fault.")
form.addRow("Length across the slip:", self.major_input)
form.addRow("Extent along the slip:", self.intermediate_input)
form.addRow("Influence distance:", self.minor_input)
layout.addWidget(size)

self.problem_label = QLabel()
self.problem_label.setWordWrap(True)
layout.addWidget(self.problem_label)
self.button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)

self._set_start_values()
for spin in (
self.strike_input,
self.dip_input,
self.pitch_input,
self.displacement_input,
self.major_input,
self.intermediate_input,
self.minor_input,
*self.centre_inputs,
):
spin.valueChanged.connect(self._validate)
self.name_input.textChanged.connect(self._validate)
self._validate()

def _set_start_values(self):
model = getattr(self.model_manager, 'model', None)
box = getattr(model, 'bounding_box', None)
if box is None:
spec = parametric_fault.default_spec([0, 0, 0], [1000, 1000, 1000])
else:
spec = parametric_fault.default_spec(box.origin, box.maximum)
self.strike_input.setValue(spec['strike'])
self.dip_input.setValue(spec['dip'])
self.pitch_input.setValue(spec['pitch'])
self.displacement_input.setValue(spec['displacement'])
for spin, value in zip(self.centre_inputs, spec['centre']):
spin.setValue(value)
self.major_input.setValue(spec['major_axis'])
self.intermediate_input.setValue(spec['intermediate_axis'])
self.minor_input.setValue(spec['minor_axis'])

def _taken_names(self):
return self.model_manager.used_names() if self.model_manager is not None else set()

def _validate(self, *args):
found = parametric_fault.problems(self.get_fault_data(), self._taken_names())
self.problem_label.setText("\n".join(found))
self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(not found)

def get_fault_data(self):
return {
'name': self.name_input.text().strip(),
'strike': self.strike_input.value(),
'dip': self.dip_input.value(),
'centre': (
self.centre_x_input.value(),
self.centre_y_input.value(),
self.centre_z_input.value(),
),
'ellipsoid_extents': (
self.extent_x_input.value(),
self.extent_y_input.value(),
self.extent_z_input.value(),
),
'displacement': self.displacement_input.value(),
'pitch': self.pitch_input.value(),
'name': self.name_input.text(),
'displacement': self.displacement_input.value(),
'centre': tuple(spin.value() for spin in self.centre_inputs),
'major_axis': self.major_input.value(),
'intermediate_axis': self.intermediate_input.value(),
'minor_axis': self.minor_input.value(),
}
141 changes: 0 additions & 141 deletions loopstructural/gui/modelling/geological_model_tab/add_fault_dialog.ui

This file was deleted.

Loading
Loading