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:
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)