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 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:
AAGENTS.md | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtem_loader/core.py | 12++++++++++++
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, })