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 3c79885b68e52a986c065fc614fe6f482c2c01c1
parent 9193f1d07b7567a5350ab944be9b4196df4768be
Author: Anders Damsgaard <anders@adamsgaard.dk>
Date:   Fri, 15 May 2026 22:07:39 +0200

feat: merge layer opacity feature

Diffstat:
AAGENTS.md | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtem_loader/core.py | 14++++++++++++++
Mtem_loader/styles/layers.qml | 266+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mtem_loader/tem_loader.py | 2+-
Mtest/test_core.py | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
5 files changed, 369 insertions(+), 43 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_mid, doi): + if doi is None: + return ABOVE_DOI_OPACITY + return ABOVE_DOI_OPACITY if depth_mid <= 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 = ( @@ -297,6 +306,7 @@ def process_xyz(path): depth_top = cum_depth depth_bottom = cum_depth + thick + depth_mid = (depth_top + depth_bottom) / 2 z_top = z - depth_top z_bot = z - depth_bottom z_mid = (z_top + z_bot) / 2 @@ -314,6 +324,7 @@ def process_xyz(path): 'DepthBottom': depth_bottom, 'Resistivity': res, 'Color': resistivity_color(res), + 'Opacity': layer_opacity(depth_mid, doi), 'Layer': i + 1, 'Geometry': layer_wkt, }) @@ -356,6 +367,7 @@ def _append_atem_sci_row(row, res_cols, elev_cols, points, doi_points, layers): z_mid = (z_top + z_bot) / 2 depth_top = z - z_top depth_bottom = z - z_bot + depth_mid = (depth_top + depth_bottom) / 2 layer_rows.append({ 'X': x, 'Y': y, @@ -367,6 +379,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_mid, doi), 'Layer': i + 1, 'Geometry': f'LINESTRING Z ({x} {y} {z_top}, {x} {y} {z_bot})', }) @@ -461,6 +474,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, }) diff --git a/tem_loader/styles/layers.qml b/tem_loader/styles/layers.qml @@ -1,6 +1,6 @@ <!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'> -<qgis autoRefreshMode="Disabled" autoRefreshTime="0" hasScaleBasedVisibilityFlag="0" labelsEnabled="0" layerType="Vector" maxScale="0" minScale="100000000" readOnly="0" simplifyAlgorithm="0" simplifyDrawingHints="1" simplifyDrawingTol="1" simplifyLocal="1" simplifyMaxScale="1" styleCategories="AllStyleCategories" symbologyReferenceScale="-1" version="4.0.1-Norrköping"> - <renderer-3d layer="layers_219f0a67_bfa6_4c9e_97ae_f671f176d748" type="vector"> +<qgis autoRefreshMode="Disabled" autoRefreshTime="0" hasScaleBasedVisibilityFlag="0" labelsEnabled="0" layerType="Vector" maxScale="0" minScale="100000000" readOnly="0" simplifyAlgorithm="0" simplifyDrawingHints="1" simplifyDrawingTol="1" simplifyLocal="1" simplifyMaxScale="1" styleCategories="AllStyleCategories" symbologyReferenceScale="-1" version="4.0.2-Norrköping"> + <renderer-3d layer="layers_12b54bb4_518f_47ff_a696_ce88ee7fec58" type="vector"> <vector-layer-3d-tiling max-chunk-features="1000" show-bounding-boxes="0" zoom-levels-count="3"/> <symbol material_type="simpleline" type="line"> <data alt-binding="centroid" alt-clamping="absolute" extrusion-height="0" offset="0" simple-lines="1" width="6"/> @@ -27,7 +27,7 @@ <Searchable>1</Searchable> <Private>0</Private> </flags> - <temporal accumulate="0" durationField="X" durationUnit="min" enabled="0" endExpression="" endField="" fixedDuration="0" limitMode="0" mode="0" startExpression="" startField=""> + <temporal accumulate="0" durationField="fid" durationUnit="min" enabled="0" endExpression="" endField="" fixedDuration="0" limitMode="0" mode="0" startExpression="" startField=""> <fixedRange> <start></start> <end></end> @@ -198,7 +198,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -245,7 +251,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -292,7 +304,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -339,7 +357,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -386,7 +410,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -433,7 +463,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -480,7 +516,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -527,7 +569,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -574,7 +622,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -621,7 +675,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -668,7 +728,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -715,7 +781,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -762,7 +834,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -809,7 +887,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -856,7 +940,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -903,7 +993,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -950,7 +1046,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -997,7 +1099,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -1044,7 +1152,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -1091,7 +1205,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -1138,7 +1258,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -1185,7 +1311,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -1232,7 +1364,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -1279,7 +1417,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -1326,7 +1470,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -1375,7 +1525,13 @@ <data_defined_properties> <Option type="Map"> <Option name="name" type="QString" value=""/> - <Option name="properties"/> + <Option name="properties" type="Map"> + <Option name="alpha" type="Map"> + <Option name="active" type="bool" value="true"/> + <Option name="field" type="QString" value="Opacity"/> + <Option name="type" type="int" value="2"/> + </Option> + </Option> <Option name="type" type="QString" value="collection"/> </Option> </data_defined_properties> @@ -1518,6 +1674,13 @@ <referencedLayers/> <referencingLayers/> <fieldConfiguration> + <field configurationFlags="NoFlag" name="fid"> + <editWidget type="TextEdit"> + <config> + <Option/> + </config> + </editWidget> + </field> <field configurationFlags="NoFlag" name="X"> <editWidget type="TextEdit"> <config> @@ -1588,6 +1751,13 @@ </config> </editWidget> </field> + <field configurationFlags="NoFlag" name="Opacity"> + <editWidget type="Range"> + <config> + <Option/> + </config> + </editWidget> + </field> <field configurationFlags="NoFlag" name="Layer"> <editWidget type="Range"> <config> @@ -1597,19 +1767,22 @@ </field> </fieldConfiguration> <aliases> - <alias field="X" index="0" name=""/> - <alias field="Y" index="1" name=""/> - <alias field="Z" index="2" name=""/> - <alias field="ZTop" index="3" name=""/> - <alias field="ZMid" index="4" name=""/> - <alias field="ZBottom" index="5" name=""/> - <alias field="DepthTop" index="6" name=""/> - <alias field="DepthBottom" index="7" name=""/> - <alias field="Resistivity" index="8" name=""/> - <alias field="Color" index="9" name=""/> - <alias field="Layer" index="10" name=""/> + <alias field="fid" index="0" name=""/> + <alias field="X" index="1" name=""/> + <alias field="Y" index="2" name=""/> + <alias field="Z" index="3" name=""/> + <alias field="ZTop" index="4" name=""/> + <alias field="ZMid" index="5" name=""/> + <alias field="ZBottom" index="6" name=""/> + <alias field="DepthTop" index="7" name=""/> + <alias field="DepthBottom" index="8" name=""/> + <alias field="Resistivity" index="9" name=""/> + <alias field="Color" index="10" name=""/> + <alias field="Opacity" index="11" name=""/> + <alias field="Layer" index="12" name=""/> </aliases> <defaults> + <default applyOnUpdate="0" expression="" field="fid"/> <default applyOnUpdate="0" expression="" field="X"/> <default applyOnUpdate="0" expression="" field="Y"/> <default applyOnUpdate="0" expression="" field="Z"/> @@ -1620,9 +1793,11 @@ <default applyOnUpdate="0" expression="" field="DepthBottom"/> <default applyOnUpdate="0" expression="" field="Resistivity"/> <default applyOnUpdate="0" expression="" field="Color"/> + <default applyOnUpdate="0" expression="" field="Opacity"/> <default applyOnUpdate="0" expression="" field="Layer"/> </defaults> <constraints> + <constraint constraints="3" exp_strength="0" field="fid" notnull_strength="1" unique_strength="1"/> <constraint constraints="0" exp_strength="0" field="X" notnull_strength="0" unique_strength="0"/> <constraint constraints="0" exp_strength="0" field="Y" notnull_strength="0" unique_strength="0"/> <constraint constraints="0" exp_strength="0" field="Z" notnull_strength="0" unique_strength="0"/> @@ -1633,9 +1808,11 @@ <constraint constraints="0" exp_strength="0" field="DepthBottom" notnull_strength="0" unique_strength="0"/> <constraint constraints="0" exp_strength="0" field="Resistivity" notnull_strength="0" unique_strength="0"/> <constraint constraints="0" exp_strength="0" field="Color" notnull_strength="0" unique_strength="0"/> + <constraint constraints="0" exp_strength="0" field="Opacity" notnull_strength="0" unique_strength="0"/> <constraint constraints="0" exp_strength="0" field="Layer" notnull_strength="0" unique_strength="0"/> </constraints> <constraintExpressions> + <constraint desc="" exp="" field="fid"/> <constraint desc="" exp="" field="X"/> <constraint desc="" exp="" field="Y"/> <constraint desc="" exp="" field="Z"/> @@ -1646,6 +1823,7 @@ <constraint desc="" exp="" field="DepthBottom"/> <constraint desc="" exp="" field="Resistivity"/> <constraint desc="" exp="" field="Color"/> + <constraint desc="" exp="" field="Opacity"/> <constraint desc="" exp="" field="Layer"/> </constraintExpressions> <expressionfields/> @@ -1665,6 +1843,8 @@ <column hidden="0" name="Resistivity" type="field" width="-1"/> <column hidden="0" name="Layer" type="field" width="-1"/> <column hidden="0" name="Color" type="field" width="-1"/> + <column hidden="0" name="fid" type="field" width="-1"/> + <column hidden="0" name="Opacity" type="field" width="-1"/> <column hidden="1" type="actions" width="-1"/> </columns> </attributetableconfig> @@ -1701,6 +1881,7 @@ def my_form_open(dialog, layer, feature): <field editable="1" name="DepthBottom"/> <field editable="1" name="DepthTop"/> <field editable="1" name="Layer"/> + <field editable="1" name="Opacity"/> <field editable="1" name="Resistivity"/> <field editable="1" name="X"/> <field editable="1" name="Y"/> @@ -1708,12 +1889,14 @@ def my_form_open(dialog, layer, feature): <field editable="1" name="ZBottom"/> <field editable="1" name="ZMid"/> <field editable="1" name="ZTop"/> + <field editable="1" name="fid"/> </editable> <labelOnTop> <field labelOnTop="0" name="Color"/> <field labelOnTop="0" name="DepthBottom"/> <field labelOnTop="0" name="DepthTop"/> <field labelOnTop="0" name="Layer"/> + <field labelOnTop="0" name="Opacity"/> <field labelOnTop="0" name="Resistivity"/> <field labelOnTop="0" name="X"/> <field labelOnTop="0" name="Y"/> @@ -1721,12 +1904,14 @@ def my_form_open(dialog, layer, feature): <field labelOnTop="0" name="ZBottom"/> <field labelOnTop="0" name="ZMid"/> <field labelOnTop="0" name="ZTop"/> + <field labelOnTop="0" name="fid"/> </labelOnTop> <reuseLastValuePolicy> <field name="Color" reuseLastValuePolicy="NotAllowed"/> <field name="DepthBottom" reuseLastValuePolicy="NotAllowed"/> <field name="DepthTop" reuseLastValuePolicy="NotAllowed"/> <field name="Layer" reuseLastValuePolicy="NotAllowed"/> + <field name="Opacity" reuseLastValuePolicy="NotAllowed"/> <field name="Resistivity" reuseLastValuePolicy="NotAllowed"/> <field name="X" reuseLastValuePolicy="NotAllowed"/> <field name="Y" reuseLastValuePolicy="NotAllowed"/> @@ -1734,6 +1919,7 @@ def my_form_open(dialog, layer, feature): <field name="ZBottom" reuseLastValuePolicy="NotAllowed"/> <field name="ZMid" reuseLastValuePolicy="NotAllowed"/> <field name="ZTop" reuseLastValuePolicy="NotAllowed"/> + <field name="fid" reuseLastValuePolicy="NotAllowed"/> </reuseLastValuePolicy> <dataDefinedFieldProperties/> <widgets/> diff --git a/tem_loader/tem_loader.py b/tem_loader/tem_loader.py @@ -20,7 +20,7 @@ from . import core STYLES_DIR = Path(__file__).parent / 'styles' GEOMETRY_FIELD = 'Geometry' STRING_FIELDS = {'Line', 'StationNo', 'Color'} -INTEGER_FIELDS = {'NumLayers', 'Layer'} +INTEGER_FIELDS = {'NumLayers', 'Layer', 'Opacity'} def _build_geopackage_uri(gpkg_path, layer_name): diff --git a/test/test_core.py b/test/test_core.py @@ -9,7 +9,10 @@ from unittest.mock import Mock, patch import xml.etree.ElementTree as ET from tem_loader.core import ( + ABOVE_DOI_OPACITY, + BELOW_DOI_OPACITY, detect_source_epsg, + layer_opacity, process_xyz, resistivity_color, write_csv, @@ -167,6 +170,69 @@ class ProcessXYZTests(unittest.TestCase): self.assertAlmostEqual(layers[0]["Resistivity"], 0.2732) self.assertEqual(layers[0]["Color"], resistivity_color(0.2732)) + def test_opacity_constants(self): + self.assertEqual(ABOVE_DOI_OPACITY, 100) + self.assertEqual(BELOW_DOI_OPACITY, 10) + + def test_layer_opacity_returns_above_when_doi_is_none(self): + self.assertEqual(layer_opacity(50.0, None), ABOVE_DOI_OPACITY) + self.assertEqual(layer_opacity(0.0, None), ABOVE_DOI_OPACITY) + self.assertEqual(layer_opacity(999.0, None), ABOVE_DOI_OPACITY) + + def test_layer_opacity_returns_above_when_midpoint_depth_at_doi(self): + self.assertEqual(layer_opacity(10.0, 10.0), ABOVE_DOI_OPACITY) + self.assertEqual(layer_opacity(0.0, 50.0), ABOVE_DOI_OPACITY) + + def test_layer_opacity_returns_below_when_midpoint_depth_exceeds_doi(self): + self.assertEqual(layer_opacity(10.1, 10.0), BELOW_DOI_OPACITY) + self.assertEqual(layer_opacity(200.0, 50.0), BELOW_DOI_OPACITY) + + def test_crossing_layer_is_above_when_midpoint_is_at_doi(self): + with TemporaryDirectory() as tmp: + path = Path(tmp) / "midpoint.xyz" + path.write_text( + "/ Line StationNo X Y Z DOI DataResidual NumLayers " + "Res_001 Res_002 Res_003 Thick_001 Thick_002 Thick_003\n" + "1 1 0 0 100 15 0 3 10 20 30 10 10 10\n" + ) + + _, _, layers = process_xyz(path) + + self.assertEqual(layers[0]["Opacity"], ABOVE_DOI_OPACITY) + self.assertEqual(layers[1]["DepthTop"], 10.0) + self.assertEqual(layers[1]["DepthBottom"], 20.0) + self.assertEqual(layers[1]["Opacity"], ABOVE_DOI_OPACITY) + self.assertEqual(layers[2]["Opacity"], BELOW_DOI_OPACITY) + + def test_fixture_layers_have_correct_opacity(self): + # TEMImage fixture has DOI, so opacity depends on midpoint depth vs DOI. + # Each sounding has its own DOI; we verify the first sounding's layers. + path = FIXTURE_DIR / "profiler_temimager_4_0_4_6.xyz" + points, doi_points, layers = process_xyz(path) + self.assertTrue(all("Opacity" in row for row in layers)) + # Every layer's opacity is one of the two valid values + self.assertTrue( + all(row["Opacity"] in (ABOVE_DOI_OPACITY, BELOW_DOI_OPACITY) for row in layers) + ) + # The first sounding's DOI and layer count + first_doi = doi_points[0]["DOI"] + first_n = points[0]["NumLayers"] + first_layers = layers[:first_n] + for layer in first_layers: + depth_mid = (layer["DepthTop"] + layer["DepthBottom"]) / 2 + expected = ABOVE_DOI_OPACITY if depth_mid <= first_doi else BELOW_DOI_OPACITY + self.assertEqual( + layer["Opacity"], + expected, + f"Layer {layer['Layer']} midpoint depth {depth_mid} vs DOI {first_doi}", + ) + + def test_sci_fixture_layers_all_above_opacity(self): + # SCI format has no DOI, so all layers get ABOVE_DOI_OPACITY + path = FIXTURE_DIR / "sci_workbench_2026_1.xyz" + _, _, layers = process_xyz(path) + self.assertTrue(all(row["Opacity"] == ABOVE_DOI_OPACITY for row in layers)) + def test_resistivity_color_buckets(self): self.assertEqual(resistivity_color(-5), "#000091") self.assertEqual(resistivity_color(0.5), "#000091") @@ -598,6 +664,7 @@ class PluginTests(unittest.TestCase): "StationNo": "1_00001", "NumLayers": 30, "Layer": 1, + "Opacity": 100, "Color": "#00ff00", "Geometry": "POINT Z (1 2 3)", } @@ -607,7 +674,7 @@ class PluginTests(unittest.TestCase): self.assertEqual( [field.name() for field in fields], - ["X", "Line", "StationNo", "NumLayers", "Layer", "Color"], + ["X", "Line", "StationNo", "NumLayers", "Layer", "Opacity", "Color"], ) self.assertEqual( [field.field_type for field in fields], @@ -617,6 +684,7 @@ class PluginTests(unittest.TestCase): module.QMetaType.Type.QString, module.QMetaType.Type.Int, module.QMetaType.Type.Int, + module.QMetaType.Type.Int, module.QMetaType.Type.QString, ], ) @@ -645,6 +713,7 @@ class PluginTests(unittest.TestCase): "X": 1.0, "Y": 2.0, "Layer": 1, + "Opacity": 10, "Color": "#00ff00", "Geometry": "LINESTRING Z (1 2 3, 1 2 2)", } @@ -682,7 +751,7 @@ class PluginTests(unittest.TestCase): ) self.assertEqual( writer.features[0].attributes, - [1.0, 2.0, 1, "#00ff00"], + [1.0, 2.0, 1, 10, "#00ff00"], ) def test_write_geopackage_layer_reports_writer_error(self): @@ -740,6 +809,7 @@ class PluginTests(unittest.TestCase): "DepthTop": 0.0, "DepthBottom": 1.0, "Resistivity": 10.0, + "Opacity": 100, "Color": "#008cff", "Layer": 1, "Geometry": "LINESTRING Z (1 2 3, 1 2 2)",