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 6e06b0672898a2cdb99ad95fbdc87d7ebda16054
parent 1d74ee7c867dad73ae16506f0e53cf4422ca21b1
Author: Anders Damsgaard <anders@adamsgaard.dk>
Date:   Thu,  9 Apr 2026 14:38:47 +0200

fix(crs): create detected EPSG auth ids explicitly during import

Diffstat:
MREADME.md | 5+++--
Mtem_loader/core.py | 17+++++++++++++++++
Mtem_loader/tem_loader.py | 13+++++++++++--
Mtest/test_core.py | 10+++++++++-
4 files changed, 40 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md @@ -25,10 +25,11 @@ Each file gets its own layer group. Layers are styled with pre-built QML styles ## Usage -1. Open a QGIS project and set the project CRS (falls back to EPSG:4326 if unset). +1. Open a QGIS project. 2. Go to **Plugins > TEM Loader > Load TEM XYZ files…**. 3. Select one or more `.xyz` inversion files. -4. Three CSV files (`.points.csv`, `.doi.csv`, `.layers.csv`) are written beside each source file, and the corresponding layers are added to the project with `points` above `doi` above `layers`. +4. If the file metadata declares an EPSG code, imported layers use that CRS; otherwise the loader falls back to the project CRS, then to EPSG:4326. +5. Three CSV files (`.points.csv`, `.doi.csv`, `.layers.csv`) are written beside each source file, and the corresponding layers are added to the project with `points` above `doi` above `layers`. ## XYZ File Format diff --git a/tem_loader/core.py b/tem_loader/core.py @@ -1,6 +1,7 @@ import csv import math from pathlib import Path +import re TEMIMAGE_REQUIRED_COLUMNS = {'X', 'Y', 'Z', 'DOI', 'DataResidual', 'NumLayers'} @@ -16,6 +17,8 @@ AARHUS_REQUIRED_COLUMNS = { 'DOI_STANDARD', } +EPSG_PATTERN = re.compile(r"\bepsg\s*:\s*(\d+)\b", re.IGNORECASE) + def normalize_header_tokens(line): normalized = [] @@ -50,6 +53,20 @@ def count_header_lines(path, comment_char='/'): return 0 +def detect_source_epsg(path, comment_char='/'): + with open(path, 'r') as f: + for line in f: + stripped = line.lstrip() + if is_header_line(stripped): + break + if not stripped.startswith(comment_char): + break + match = EPSG_PATTERN.search(stripped) + if match is not None: + return f'EPSG:{match.group(1)}' + return None + + def get_numbered_columns(headers, prefix): numbered = [] for name in headers: diff --git a/tem_loader/tem_loader.py b/tem_loader/tem_loader.py @@ -45,9 +45,18 @@ class TEMLoaderPlugin: lyr_csv = core.write_csv(layers, filepath, '.layers.csv') project = QgsProject.instance() - crs = project.crs() + crs = None + source_epsg = core.detect_source_epsg(filepath) + if source_epsg: + candidate = QgsCoordinateReferenceSystem() + if candidate.createFromString(source_epsg) and candidate.isValid(): + crs = candidate + + if crs is None: + crs = project.crs() if not crs.isValid(): - crs = QgsCoordinateReferenceSystem('EPSG:4326') + crs = QgsCoordinateReferenceSystem() + crs.createFromString('EPSG:4326') crs_str = crs.authid() group_name = filepath.stem diff --git a/test/test_core.py b/test/test_core.py @@ -4,7 +4,7 @@ import shutil import unittest import xml.etree.ElementTree as ET -from tem_loader.core import process_xyz, write_csv +from tem_loader.core import detect_source_epsg, process_xyz, write_csv FIXTURE_DIR = Path(__file__).parent / "data" @@ -58,6 +58,14 @@ class ProcessXYZTests(unittest.TestCase): self.assertEqual(layers[0]["DepthBottom"], 2.0) self.assertAlmostEqual(layers[-1]["DepthBottom"], 599.977) + def test_detect_source_epsg_for_aarhus_workbench_fixture(self): + path = FIXTURE_DIR / "sci_workbench.xyz" + self.assertEqual(detect_source_epsg(path), "EPSG:32637") + + def test_detect_source_epsg_returns_none_when_not_declared(self): + path = FIXTURE_DIR / "profiler_temimager_4_0_7_8.xyz" + self.assertIsNone(detect_source_epsg(path)) + def test_write_csv_writes_expected_headers(self): source = FIXTURE_DIR / "profiler_temimager_4_0_4_6.xyz" points, _, _ = process_xyz(source)