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
27 changes: 14 additions & 13 deletions docs/development/usability-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,27 +302,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 @@ -361,9 +361,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
153 changes: 132 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,144 @@
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("How far the fault extends along its strike.")
self.intermediate_input = _spin(0.01, 1e9)
self.intermediate_input.setToolTip("How far the fault extends down its 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 along strike:", self.major_input)
form.addRow("Extent down dip:", 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