commit 0decc379714b3ec3c42330021355f0f6db9d4b46
parent 9193f1d07b7567a5350ab944be9b4196df4768be
Author: Anders Damsgaard <anders@adamsgaard.dk>
Date: Fri, 15 May 2026 17:53:52 +0200
feat(core): add layer_opacity helper and Opacity field to layer rows
Diffstat:
2 files changed, 68 insertions(+), 0 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -0,0 +1,56 @@
+# TEM Loader Plugin
+
+QGIS 3.40+ plugin for loading Time-Domain Electromagnetic (TEM) inversion XYZ files as styled 3D vector layers. Python, no external dependencies beyond QGIS itself.
+
+## Commands
+
+```sh
+make test # run the unittest suite
+python3 -m unittest discover -s test -p 'test_*.py' # same test runner without Make
+python3 -m unittest test.test_core.ProcessXYZTests.test_temimage_4_0_4_6_fixture # single test
+make package # build tem_loader.zip for installation
+make clean # remove the zip
+```
+
+CI runs `make test` on Python 3.12; the GitLab pipeline also produces `tem_loader.zip` as an artifact.
+
+## Boundaries
+
+**Allowed without asking:**
+- Read any file, list directories, run single-file type checks and tests
+- Create or edit files in `tem_loader/`, `test/`, and `tem_loader/styles/`
+
+**Ask first:**
+- Installing new Python dependencies (project has none)
+- Deleting test fixtures from `test/data/`
+- Pushing to git (`git-push.sh` pushes to origin, own, and geus remotes)
+
+**Never touch:**
+- `.env`, secrets, or credentials
+- Binary video files (`*.mov`, `*.mp4`) — they are demos, not code
+- Large XYZ fixture data in the project root (development samples only)
+
+## Architecture
+
+- `tem_loader/core.py` — XYZ parsing, CSV writing, resistivity color mapping. No QGIS dependency; fully unit-testable.
+- `tem_loader/tem_loader.py` — QGIS plugin glue: dialog, layer creation, CRS resolution, styling, group organization.
+- `tem_loader/styles/*.qml` — QGIS layer style files for points, DOI, and layers (supports 3D renderer with dynamic color).
+- `test/data/` — fixture XYZ files for the three supported export families: TEMImage, Aarhus Workbench, and SCI Workbench.
+
+## Conventions
+
+- Fallible operations raise `ValueError` with descriptive messages; callers catch and present them to the user (not swallowed).
+- Multiple file processing continues after errors per-file, collecting failures into a single warning dialog.
+- CRS resolution: source EPSG from file metadata > project CRS > EPSG:4326 fallback.
+- Test fixtures live in `test/data/`; tests assert counts, specific field values, and QXML structure (for style verification).
+
+## Git
+
+- Commit messages follow conventional commits, for example `feat(parser): ...`, `fix(qgis): ...`, `build: ...`, and `chore(release): ...`.
+- Do not push without approval; `git-push.sh` pushes to `origin`, `own`, and `geus`.
+
+## Gotchas
+
+- `core.py` must remain free of QGIS imports — it is the only module tested without a running QGIS instance.
+- The SCI Workbench format aggregates per-layer rows into soundings at parse time; its code path diverges from the other two formats.
+- XYZ files with an extra leading index column (pandas-style) must be handled by dropping the first value before zipping.
diff --git a/tem_loader/core.py b/tem_loader/core.py
@@ -45,6 +45,15 @@ HEADER_ALIASES = {
'UTMY': 'Y',
}
+ABOVE_DOI_OPACITY = 100
+BELOW_DOI_OPACITY = 10
+
+
+def layer_opacity(depth_bottom, doi):
+ if doi is None:
+ return ABOVE_DOI_OPACITY
+ return ABOVE_DOI_OPACITY if depth_bottom <= doi else BELOW_DOI_OPACITY
+
# Resistivity ranges mirror the graduated symbology in styles/layers.qml.
# Each entry is (upper_exclusive, '#RRGGBB'); the final entry is the catch-all.
RESISTIVITY_COLOR_RAMP = (
@@ -314,6 +323,7 @@ def process_xyz(path):
'DepthBottom': depth_bottom,
'Resistivity': res,
'Color': resistivity_color(res),
+ 'Opacity': layer_opacity(depth_bottom, doi),
'Layer': i + 1,
'Geometry': layer_wkt,
})
@@ -367,6 +377,7 @@ def _append_atem_sci_row(row, res_cols, elev_cols, points, doi_points, layers):
'DepthBottom': depth_bottom,
'Resistivity': res,
'Color': resistivity_color(res),
+ 'Opacity': layer_opacity(depth_bottom, doi),
'Layer': i + 1,
'Geometry': f'LINESTRING Z ({x} {y} {z_top}, {x} {y} {z_bot})',
})
@@ -461,6 +472,7 @@ def _process_sci_rows(f, headers, header_line_number):
'DepthBottom': depth_bottom,
'Resistivity': res,
'Color': resistivity_color(res),
+ 'Opacity': layer_opacity(depth_bottom, None),
'Layer': layer_no,
'Geometry': layer_wkt,
})