Skip to content

Commit ac11647

Browse files
committed
merge: bring the #138 fixes from feat/strat-layers-step-2
Keep the one scroll area of #140 in the geological history tab. The geology group is a section of the column stack.
2 parents 0ba0250 + 583eeac commit ac11647

43 files changed

Lines changed: 1082 additions & 347 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/bump-loopstructural-pin.yml‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
check-and-bump:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v4
16+
- uses: actions/checkout@v7
1717

1818
- name: Get latest LoopStructural release
1919
id: latest
@@ -23,7 +23,11 @@ jobs:
2323
# can return whichever component's release was published last, not
2424
# necessarily the LoopStructural package. Read the manifest instead —
2525
# it maps package name to version unambiguously.
26-
latest=$(curl -s https://raw.githubusercontent.com/Loop3D/LoopStructural/master/.release-please-manifest.json | jq -r '.["LoopStructural"]')
26+
latest=$(curl -fsS https://raw.githubusercontent.com/Loop3D/LoopStructural/master/.release-please-manifest.json | jq -r '.["LoopStructural"] // empty')
27+
if ! [[ "$latest" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
28+
echo "::error::Could not read a valid LoopStructural version from the release manifest (got '${latest}')."
29+
exit 1
30+
fi
2731
echo "version=$latest" >> "$GITHUB_OUTPUT"
2832
2933
- name: Bump pins if minor version changed
@@ -44,7 +48,7 @@ jobs:
4448
4549
- name: Open pull request
4650
if: steps.bump.outputs.changed == '1'
47-
uses: peter-evans/create-pull-request@v6
51+
uses: peter-evans/create-pull-request@v8
4852
with:
4953
commit-message: "fix: bump LoopStructural pin to ${{ steps.latest.outputs.version }}"
5054
title: "Bump LoopStructural pin to ${{ steps.latest.outputs.version }}"

‎.github/workflows/tester.yml‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,19 @@ on:
2020

2121
env:
2222
PROJECT_FOLDER: "loopstructural"
23-
PYTHON_VERSION: 3.9
2423

2524

2625
jobs:
2726
tests-unit:
2827
runs-on: ubuntu-latest
2928

29+
# 3.10 is the oldest Python of a supported QGIS build (3.44 LTR on Ubuntu 22.04).
30+
# 3.12 is the Python of the QGIS Windows (OSGeo4W) installers.
31+
strategy:
32+
fail-fast: false
33+
matrix:
34+
python-version: ["3.10", "3.12"]
35+
3036
steps:
3137
- name: Get source code
3238
uses: actions/checkout@v7
@@ -36,7 +42,7 @@ jobs:
3642
with:
3743
cache: "pip"
3844
cache-dependency-path: "requirements/testing.txt"
39-
python-version: ${{ env.PYTHON_VERSION }}
45+
python-version: ${{ matrix.python-version }}
4046

4147
- name: Install Python requirements
4248
run: |
@@ -54,7 +60,7 @@ jobs:
5460
if: always()
5561
uses: actions/upload-artifact@v7
5662
with:
57-
name: coverage-unit
63+
name: coverage-unit-py${{ matrix.python-version }}
5864
path: |
5965
coverage.xml
6066
htmlcov/
@@ -64,8 +70,14 @@ jobs:
6470
test-qgis:
6571
runs-on: ubuntu-latest
6672

73+
# 3.44-jammy: the current LTR, with Python 3.10. 4.2: the latest release, with Qt6.
74+
strategy:
75+
fail-fast: false
76+
matrix:
77+
qgis-image: ["3.44-jammy", "4.2"]
78+
6779
container:
68-
image: qgis/qgis:release-3_28
80+
image: qgis/qgis:${{ matrix.qgis-image }}
6981
env:
7082
CI: true
7183
DISPLAY: ":1"
@@ -88,13 +100,20 @@ jobs:
88100
set -e
89101
apt-get update
90102
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
91-
build-essential gdal-bin libgdal-dev proj-bin libproj-dev libgeos-dev python3-dev xvfb
103+
build-essential gdal-bin libgdal-dev proj-bin libproj-dev libgeos-dev python3-dev python3-venv xvfb
92104
rm -rf /var/lib/apt/lists/*
93105
94106
- name: Setup plugin
95107
run: bash /tests_directory/scripts/qgis_setup.sh loopstructural
96108

97109

110+
# Install into a venv that can see the system packages (QGIS). Then pip and setuptools
111+
# can be upgraded without changes to the Debian packages (PEP 668, no RECORD files).
112+
- name: Create virtual environment
113+
run: |
114+
python3 -m venv --system-site-packages /opt/venv
115+
echo "/opt/venv/bin" >> "$GITHUB_PATH"
116+
98117
- name: Install Python requirements
99118
run: |
100119
python3 -m pip install -U pip setuptools wheel
@@ -114,7 +133,7 @@ jobs:
114133
if: always()
115134
uses: actions/upload-artifact@v7
116135
with:
117-
name: coverage-qgis
136+
name: coverage-qgis-${{ matrix.qgis-image }}
118137
path: |
119138
coverage.xml
120139
htmlcov/

‎docs/development/usability-plan.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ of date with its contacts, and update it at build time.
194194
3. Each step has a check function. The step header shows its result.
195195
4. Use QGIS widgets and theme icons: `QgsColorButton`, `QgsCollapsibleGroupBox`,
196196
`QgsApplication.getThemeIcon`. Do not use emoji as icons.
197-
5. Do not put a scroll area around a widget that scrolls.
197+
5. A list that scrolls must fill its tab. If the tab has an outer scroll area,
198+
the scroll area must scroll only when the dock is shorter than the tab.
198199
6. Derived data records its inputs. The UI shows when it is out of date. A
199200
model build never uses out-of-date derived data.
200201

@@ -275,7 +276,8 @@ styling) is merged.
275276
- [x] Rows: use `QgsColorButton`, a theme delete icon, and no repeated
276277
"Thickness:" label. Give unconformity rows a different style.
277278
- [x] Show text in the empty list.
278-
- [x] Remove the outer scroll area from the tab.
279+
- [x] Make the unit list fill the tab. The outer scroll area scrolls only in a
280+
dock that is shorter than the column.
279281

280282
Files: `stratigraphic_column/*.py`, `stratigraphic_column/*.ui`,
281283
`geological_history_tab.py`.

‎linters/pylintrc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ additional-builtins=
128128
[FORMAT]
129129

130130
# Maximum number of characters on a single line.
131-
max-line-length=88
131+
max-line-length=100
132132

133133
# Regexp for a line that is allowed to be longer than the limit.
134134
ignore-long-lines=^\s*(# )?<?https?://\S+>?$

‎loopstructural/gui/background_task.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,14 @@ def start_background_task(
7474
on_progress,
7575
on_finished,
7676
on_error,
77+
modal=False,
7778
):
78-
"""Start `target(progress_callback)` on a background QThread, with a
79-
non-modal indeterminate QProgressDialog parented to `widget`.
79+
"""Start `target(progress_callback)` on a background QThread, with an
80+
indeterminate QProgressDialog parented to `widget`.
81+
82+
The dialog is non-modal unless `modal` is True. A modal dialog blocks the
83+
window of `widget` until the task ends, so the user cannot start other
84+
work (for example a model build) that changes the data of the task.
8085
8186
`on_progress`, `on_finished` and `on_error` MUST be bound methods of
8287
`widget` (e.g. `self._on_sampler_finished`), never lambdas/closures --
@@ -90,7 +95,9 @@ def start_background_task(
9095
the thread is running, the thread is destroyed with it.
9196
"""
9297
progress_dialog = QProgressDialog(initial_label, "Cancel", 0, 0, widget)
93-
progress_dialog.setWindowModality(Qt.WindowModality.NonModal)
98+
progress_dialog.setWindowModality(
99+
Qt.WindowModality.WindowModal if modal else Qt.WindowModality.NonModal
100+
)
94101
progress_dialog.setWindowTitle(title)
95102
progress_dialog.setCancelButton(None)
96103
progress_dialog.setMinimumDuration(0)

‎loopstructural/gui/map2loop_tools/basal_contacts_widget.py‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -247,18 +247,9 @@ def _run_extractor(self):
247247
unit_name_field = self.unitNameFieldComboBox.currentField()
248248
adopt_geology_role(self.data_manager, geology=geology, unit_field=unit_name_field)
249249
# The inputs of this run, read before the run starts. A change during
250-
# the run makes the result out of date.
251-
self._run_inputs = (
252-
self.data_manager.basal_contacts_inputs(
253-
geology=geology,
254-
unit_field=unit_name_field,
255-
faults=self.faultsLayerComboBox.currentLayer(),
256-
ignore_units=self._units_from_line_edit(self.ignoreUnitsLineEdit),
257-
override_units=self._units_from_line_edit(self.basalOverrideUnitsLineEdit),
258-
)
259-
if self.data_manager
260-
else None
261-
)
250+
# the run makes the result out of date. These are read from the same
251+
# source as the status check, after the settings were saved.
252+
self._run_inputs = self.data_manager.basal_contacts_inputs() if self.data_manager else None
262253

263254
self.setEnabled(False)
264255
self._extractor_thread, self._extractor_worker, self._extractor_progress = (

‎loopstructural/gui/map2loop_tools/paint_stratigraphic_order_widget.py‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,11 @@ def _run_painter(self):
171171
adopt_geology_role(
172172
self.data_manager, geology=geology_layer, unit_field=unit_name_field
173173
)
174-
self.data_manager.derived.record(
175-
derived_data.STYLED_FIELDS,
176-
inputs=self.data_manager.styled_fields_inputs(),
177-
detail={'layer': geology_layer.name(), 'field': unit_name_field},
178-
)
174+
run_inputs = self.data_manager.styled_fields_inputs()
175+
# The layer that has the field at the end: the geology layer, or
176+
# its styled copy
177+
styled_layer = geology_layer
178+
ramp_name = None
179179

180180
# If requested, duplicate layer and apply style using selected colour ramp
181181
try:
@@ -242,6 +242,7 @@ def _run_painter(self):
242242
mem_dp.addFeatures(new_feats)
243243
mem_layer.updateExtents()
244244
QgsProject.instance().addMapLayer(mem_layer)
245+
styled_layer = mem_layer
245246
except Exception as e:
246247
QMessageBox.warning(self, 'Duplicate Layer', f'Failed to create copy: {e}')
247248
return False
@@ -346,6 +347,18 @@ def _run_painter(self):
346347
self, 'Duplicate Layer', f'Failed to apply graduated styling: {e}'
347348
)
348349

350+
if self.data_manager:
351+
# Update writes the field again to the layer that is recorded
352+
self.data_manager.derived.record(
353+
derived_data.STYLED_FIELDS,
354+
inputs=run_inputs,
355+
detail={
356+
'layer': styled_layer.name(),
357+
'field': unit_name_field,
358+
'ramp': ramp_name,
359+
},
360+
)
361+
349362
except Exception as e:
350363
QMessageBox.warning(self, 'Paint Stratigraphic Order', f'Operation failed: {e}')
351364
return False

‎loopstructural/gui/map2loop_tools/thickness_calculator_widget.py‎

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ def _persist_selection(self):
337337
),
338338
'calculator_type': self.calculatorTypeComboBox.currentText(),
339339
'calculator_type_index': self.calculatorTypeComboBox.currentIndex(),
340+
'orientation_type': self.orientationTypeComboBox.currentText(),
340341
'orientation_type_index': self.orientationTypeComboBox.currentIndex(),
341342
'max_line_length': self.maxLineLengthSpinBox.value(),
342343
'sampling_frequency': self.samplingFrequencySpinBox.value(),
@@ -408,19 +409,9 @@ def _run_calculator(self):
408409
unit_field=self.unitNameFieldComboBox.currentField(),
409410
)
410411
# The inputs of this run, read before the run starts. A change during
411-
# the run makes the result out of date.
412-
self._run_inputs = (
413-
self.data_manager.thickness_inputs(
414-
geology=self.geologyLayerComboBox.currentLayer(),
415-
unit_field=self.unitNameFieldComboBox.currentField(),
416-
contacts_layer=self.basalContactsComboBox.currentLayer(),
417-
calculator_type=calculator_type,
418-
structure=self.structureLayerComboBox.currentLayer(),
419-
cross_sections=self.crossSectionLayerComboBox.currentLayer(),
420-
)
421-
if self.data_manager
422-
else None
423-
)
412+
# the run makes the result out of date. These are read from the same
413+
# source as the status check, after the settings were saved.
414+
self._run_inputs = self.data_manager.thickness_inputs() if self.data_manager else None
424415

425416
def target(progress_callback):
426417
return calculate_thickness(updater=progress_callback, **params)
@@ -466,8 +457,7 @@ def _on_calculator_finished(self, result):
466457
# A thickness that the user typed does not change.
467458
if thicknesses is not None and self.data_manager:
468459
skipped = self._apply_thicknesses(thicknesses)
469-
if self.data_manager and getattr(self, '_run_inputs', None) is not None:
470-
self.data_manager.derived.record(derived_data.THICKNESS, inputs=self._run_inputs)
460+
self._record_run()
471461
# if thicknesses is not None:
472462
# addGeoDataFrameToproject(thicknesses, "Thickness Results")
473463
if lines is not None:
@@ -483,6 +473,7 @@ def _on_calculator_finished(self, result):
483473
self.task_succeeded.emit()
484474
return
485475

476+
self._record_run()
486477
if hasattr(result, 'geometry'):
487478
addGeoDataFrameToproject(result, "Thickness Results")
488479
push_success(
@@ -495,6 +486,11 @@ def _on_calculator_finished(self, result):
495486
push_success("Thickness Calculator", f"Thickness calculation completed: {result}")
496487
self.task_succeeded.emit()
497488

489+
def _record_run(self):
490+
"""Record the inputs of the run that finished."""
491+
if self.data_manager and getattr(self, '_run_inputs', None) is not None:
492+
self.data_manager.derived.record(derived_data.THICKNESS, inputs=self._run_inputs)
493+
498494
def _apply_thicknesses(self, thicknesses):
499495
"""Set the calculated thicknesses on the units of the column.
500496

‎loopstructural/gui/messages.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ def push_success(title: str, text: str, duration: int = 5):
1111
confirmation before a destructive action.
1212
"""
1313
if iface is not None:
14-
iface.messageBar().pushMessage(title, text, level=Qgis.MessageLevel.Success, duration=duration)
14+
iface.messageBar().pushMessage(
15+
title, text, level=Qgis.MessageLevel.Success, duration=duration
16+
)
1517
else:
1618
# No main window, for example in the tests
1719
QgsMessageLog.logMessage(f"{title}: {text}", "LoopStructural", Qgis.MessageLevel.Success)
@@ -28,6 +30,8 @@ def push_info(title: str, text: str, duration: int = 5):
2830
def push_warning(title: str, text: str, duration: int = 10):
2931
"""Show a warning in the message bar of QGIS, for a result that is not an error."""
3032
if iface is not None:
31-
iface.messageBar().pushMessage(title, text, level=Qgis.MessageLevel.Warning, duration=duration)
33+
iface.messageBar().pushMessage(
34+
title, text, level=Qgis.MessageLevel.Warning, duration=duration
35+
)
3236
else:
3337
QgsMessageLog.logMessage(f"{title}: {text}", "LoopStructural", Qgis.MessageLevel.Warning)

‎loopstructural/gui/modelling/geological_model_tab/add_fault_dialog.py‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,19 @@ def __init__(self, parent=None, *, model_manager=None):
7070
size = QGroupBox("Size")
7171
form = QFormLayout(size)
7272
self.major_input = _spin(0.01, 1e9)
73-
self.major_input.setToolTip("How far the fault extends along its strike.")
73+
self.major_input.setToolTip(
74+
"The full length of the fault in its plane, at right angles to the slip. "
75+
"With a pitch of 90 (dip slip), this is along the strike."
76+
)
7477
self.intermediate_input = _spin(0.01, 1e9)
75-
self.intermediate_input.setToolTip("How far the fault extends down its dip.")
78+
self.intermediate_input.setToolTip(
79+
"How far the fault extends from the centre in the direction of the slip. "
80+
"With a pitch of 90 (dip slip), this is down the dip."
81+
)
7682
self.minor_input = _spin(0.01, 1e9)
7783
self.minor_input.setToolTip("How far the fault changes the field, across the fault.")
78-
form.addRow("Length along strike:", self.major_input)
79-
form.addRow("Extent down dip:", self.intermediate_input)
84+
form.addRow("Length across the slip:", self.major_input)
85+
form.addRow("Extent along the slip:", self.intermediate_input)
8086
form.addRow("Influence distance:", self.minor_input)
8187
layout.addWidget(size)
8288

0 commit comments

Comments
 (0)