qgis-tem-loader

qgis plugin for loading TEM geophysical inversion XYZ files as 3D objects
git clone git://src.adamsgaard.dk/qgis-tem-loader # fast
git clone https://src.adamsgaard.dk/qgis-tem-loader.git # slow
Log | Files | Refs | README | LICENSE Back to index

commit 95bb5095c8c979afd0484b97ea56e0ab1adfe381
parent 8c22a5490b8306a038164278800dcfbd731195d0
Author: Anders Damsgaard <anders@adamsgaard.dk>
Date:   Fri, 15 May 2026 22:52:47 +0200

feat(qgis): add import options dialog

Diffstat:
Mtem_loader/tem_loader.py | 58+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtest/test_core.py | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 202 insertions(+), 3 deletions(-)

diff --git a/tem_loader/tem_loader.py b/tem_loader/tem_loader.py @@ -1,7 +1,17 @@ from pathlib import Path from qgis.PyQt.QtCore import QMetaType -from qgis.PyQt.QtWidgets import QAction, QFileDialog, QMessageBox +from qgis.PyQt.QtWidgets import ( + QAction, + QCheckBox, + QDialog, + QDialogButtonBox, + QFileDialog, + QFormLayout, + QMessageBox, + QSpinBox, + QVBoxLayout, +) from qgis.core import ( Qgis, QgsCoordinateReferenceSystem, @@ -96,6 +106,52 @@ def _write_geopackage_layer(rows, gpkg_path, layer_name, wkb_type, crs, del writer +class _ImportOptionsDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle('TEM Loader Options') + + self._mask_checkbox = QCheckBox( + 'Mask out layers below depth of interest (DOI)' + ) + self._mask_checkbox.setChecked(True) + + self._opacity_spinbox = QSpinBox() + self._opacity_spinbox.setRange(0, 100) + self._opacity_spinbox.setSuffix('%') + self._opacity_spinbox.setValue(core.BELOW_DOI_OPACITY) + self._opacity_spinbox.setEnabled(self._mask_checkbox.isChecked()) + self._mask_checkbox.toggled.connect(self._opacity_spinbox.setEnabled) + + form = QFormLayout() + form.addRow('Opacity', self._opacity_spinbox) + + buttons = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + + layout = QVBoxLayout() + layout.addWidget(self._mask_checkbox) + layout.addLayout(form) + layout.addWidget(buttons) + self.setLayout(layout) + + def options(self): + return { + 'mask_below_doi': self._mask_checkbox.isChecked(), + 'below_doi_opacity': self._opacity_spinbox.value(), + } + + +def _exec_dialog(dialog): + execute = getattr(dialog, 'exec', None) + if execute is None: + execute = dialog.exec_ + return execute() + + class TEMLoaderPlugin: def __init__(self, iface): self.iface = iface diff --git a/test/test_core.py b/test/test_core.py @@ -434,8 +434,15 @@ class ProcessXYZTests(unittest.TestCase): class PluginTests(unittest.TestCase): def _import_plugin_module(self): class FakeSignal: - def connect(self, _callback): - pass + def __init__(self): + self._callbacks = [] + + def connect(self, callback): + self._callbacks.append(callback) + + def emit(self, *args): + for callback in self._callbacks: + callback(*args) class FakeAction: def __init__(self, *_args, **_kwargs): @@ -455,6 +462,92 @@ class PluginTests(unittest.TestCase): def warning(*args): FakeMessageBox.warnings.append(args) + class FakeDialog: + Accepted = 1 + Rejected = 0 + + def __init__(self, *_args, **_kwargs): + self.title = None + self.layout = None + + def setWindowTitle(self, title): + self.title = title + + def setLayout(self, layout): + self.layout = layout + + def accept(self): + pass + + def reject(self): + pass + + class FakeCheckBox: + def __init__(self, text): + self.text = text + self._checked = False + self.toggled = FakeSignal() + + def setChecked(self, checked): + checked = bool(checked) + changed = checked != self._checked + self._checked = checked + if changed: + self.toggled.emit(checked) + + def isChecked(self): + return self._checked + + class FakeDialogButtonBox: + Ok = 1 + Cancel = 2 + + def __init__(self, buttons): + self.buttons = buttons + self.accepted = FakeSignal() + self.rejected = FakeSignal() + + class FakeFormLayout: + def __init__(self): + self.rows = [] + + def addRow(self, label, widget): + self.rows.append((label, widget)) + + class FakeSpinBox: + def __init__(self): + self.minimum = None + self.maximum = None + self.suffix = None + self._value = None + self.enabled = True + + def setRange(self, minimum, maximum): + self.minimum = minimum + self.maximum = maximum + + def setSuffix(self, suffix): + self.suffix = suffix + + def setValue(self, value): + self._value = value + + def setEnabled(self, enabled): + self.enabled = bool(enabled) + + def value(self): + return self._value + + class FakeVBoxLayout: + def __init__(self): + self.items = [] + + def addWidget(self, widget): + self.items.append(widget) + + def addLayout(self, layout): + self.items.append(layout) + class FakeQMetaType: class Type: QString = "QString" @@ -466,8 +559,14 @@ class PluginTests(unittest.TestCase): qtwidgets = types.ModuleType("qgis.PyQt.QtWidgets") qtwidgets.QAction = FakeAction + qtwidgets.QCheckBox = FakeCheckBox + qtwidgets.QDialog = FakeDialog + qtwidgets.QDialogButtonBox = FakeDialogButtonBox qtwidgets.QFileDialog = FakeFileDialog + qtwidgets.QFormLayout = FakeFormLayout qtwidgets.QMessageBox = FakeMessageBox + qtwidgets.QSpinBox = FakeSpinBox + qtwidgets.QVBoxLayout = FakeVBoxLayout class FakeQgis: class WkbType: @@ -694,6 +793,50 @@ class PluginTests(unittest.TestCase): self.assertIn("bad.xyz", message_box.warnings[0][2]) self.assertIn("Row 3 has 4 columns, expected 6", message_box.warnings[0][2]) + def test_import_options_dialog_defaults_and_options(self): + module, _, _ = self._import_plugin_module() + + dialog = module._ImportOptionsDialog(object()) + + self.assertEqual(dialog.title, "TEM Loader Options") + self.assertEqual( + dialog._mask_checkbox.text, + "Mask out layers below depth of interest (DOI)", + ) + self.assertEqual(dialog._opacity_spinbox.minimum, 0) + self.assertEqual(dialog._opacity_spinbox.maximum, 100) + self.assertEqual(dialog._opacity_spinbox.suffix, "%") + self.assertTrue(dialog._opacity_spinbox.enabled) + self.assertEqual( + dialog.options(), + { + "mask_below_doi": True, + "below_doi_opacity": module.core.BELOW_DOI_OPACITY, + }, + ) + + dialog._opacity_spinbox.setValue(35) + dialog._mask_checkbox.setChecked(False) + + self.assertFalse(dialog._opacity_spinbox.enabled) + self.assertEqual( + dialog.options(), + { + "mask_below_doi": False, + "below_doi_opacity": 35, + }, + ) + + def test_exec_dialog_supports_exec_and_exec_apis(self): + module, _, _ = self._import_plugin_module() + exec_dialog = Mock() + exec_dialog.exec.return_value = module.QDialog.Accepted + exec_legacy_dialog = Mock(spec=["exec_"]) + exec_legacy_dialog.exec_.return_value = module.QDialog.Rejected + + self.assertEqual(module._exec_dialog(exec_dialog), module.QDialog.Accepted) + self.assertEqual(module._exec_dialog(exec_legacy_dialog), module.QDialog.Rejected) + def test_build_geopackage_uri_uses_layername(self): module, _, _ = self._import_plugin_module() gpkg_path = Mock()