commit ceefff40dade5965c9d48cc8e43a8cdec1702b87
parent 5398f115e1bb370afbd22e3812182ecc60d07e5a
Author: Anders Damsgaard <anders@adamsgaard.dk>
Date: Sun, 10 May 2026 00:10:28 +0200
feat(qgis): load imported layers from GeoPackage
Diffstat:
| M | tem_loader/tem_loader.py | | | 73 | +++++++++++++++++++++++++++++++++++++++++++------------------------------ |
| M | test/test_core.py | | | 220 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
2 files changed, 258 insertions(+), 35 deletions(-)
diff --git a/tem_loader/tem_loader.py b/tem_loader/tem_loader.py
@@ -96,18 +96,6 @@ def _write_geopackage_layer(rows, gpkg_path, layer_name, wkb_type, crs,
del writer
-def _build_delimited_text_uri(csv_path, geom_type, crs_str):
- base_uri = csv_path.resolve().as_uri()
- return (
- f'{base_uri}?type={geom_type}'
- f'&crs={crs_str}'
- f'&wktField=Geometry'
- f'&delimiter=,'
- f'&xField=X&yField=Y'
- f'&spatialIndex=yes'
- )
-
-
class TEMLoaderPlugin:
def __init__(self, iface):
self.iface = iface
@@ -143,13 +131,7 @@ class TEMLoaderPlugin:
'\n'.join(failed),
)
- def _load_xyz(self, filepath):
- points, doi_points, layers = core.process_xyz(filepath)
- pts_csv = core.write_csv(points, filepath, '.points.csv')
- doi_csv = core.write_csv(doi_points, filepath, '.doi.csv') if doi_points else None
- lyr_csv = core.write_csv(layers, filepath, '.layers.csv')
-
- project = QgsProject.instance()
+ def _resolve_crs(self, filepath, project):
crs = None
source_epsg = core.detect_source_epsg(filepath)
if source_epsg:
@@ -162,18 +144,49 @@ class TEMLoaderPlugin:
if not crs.isValid():
crs = QgsCoordinateReferenceSystem()
crs.createFromString('EPSG:4326')
- crs_str = crs.authid()
+ return crs
- loaded_layers = {}
- source_layers = [
- ('layers', lyr_csv, 'LineString'),
+ def _load_xyz(self, filepath):
+ points, doi_points, layers = core.process_xyz(filepath)
+
+ project = QgsProject.instance()
+ crs = self._resolve_crs(filepath, project)
+ gpkg_path = filepath.with_suffix('.gpkg')
+ transform_context = project.transformContext()
+
+ output_layers = [
+ ('layers', layers, Qgis.WkbType.LineStringZ),
]
- if doi_csv is not None:
- source_layers.append(('doi', doi_csv, 'Point'))
- source_layers.append(('points', pts_csv, 'Point'))
- for name, csv_path, geom_type in source_layers:
- uri = _build_delimited_text_uri(csv_path, geom_type, crs_str)
- layer = QgsVectorLayer(uri, name, 'delimitedtext')
+ if doi_points:
+ output_layers.append(('doi', doi_points, Qgis.WkbType.PointZ))
+ output_layers.append(('points', points, Qgis.WkbType.PointZ))
+
+ written_layer_names = []
+ first_layer = True
+ for name, rows, wkb_type in output_layers:
+ if not rows:
+ continue
+ action = (
+ QgsVectorFileWriter.CreateOrOverwriteFile
+ if first_layer
+ else QgsVectorFileWriter.CreateOrOverwriteLayer
+ )
+ _write_geopackage_layer(
+ rows,
+ gpkg_path,
+ name,
+ wkb_type,
+ crs,
+ transform_context,
+ action,
+ )
+ written_layer_names.append(name)
+ first_layer = False
+
+ loaded_layers = {}
+ for name in written_layer_names:
+ uri = _build_geopackage_uri(gpkg_path, name)
+ layer = QgsVectorLayer(uri, name, 'ogr')
if not layer.isValid():
continue
@@ -198,7 +211,7 @@ class TEMLoaderPlugin:
group.insertLayer(insert_index, layer)
insert_index += 1
- failed = [name for name, _, _ in source_layers if name not in loaded_layers]
+ failed = [name for name in written_layer_names if name not in loaded_layers]
if failed:
QMessageBox.warning(
self.iface.mainWindow(),
diff --git a/test/test_core.py b/test/test_core.py
@@ -444,13 +444,89 @@ class PluginTests(unittest.TestCase):
self.features.extend(features)
return self._add_features_result
+ class FakeCoordinateReferenceSystem:
+ def __init__(self, authid="EPSG:32632", valid=True):
+ self._authid = authid
+ self._valid = valid
+
+ def createFromString(self, authid):
+ self._authid = authid
+ self._valid = True
+ return True
+
+ def isValid(self):
+ return self._valid
+
+ def authid(self):
+ return self._authid
+
+ class FakeLayerGroup:
+ def __init__(self, name):
+ self.name = name
+ self.layers = []
+
+ def insertLayer(self, index, layer):
+ self.layers.insert(index, layer)
+
+ class FakeLayerTreeRoot:
+ def __init__(self):
+ self.groups = []
+
+ def insertGroup(self, index, name):
+ group = FakeLayerGroup(name)
+ self.groups.insert(index, group)
+ return group
+
+ class FakeProject:
+ _instance = None
+
+ def __init__(self):
+ self._crs = FakeCoordinateReferenceSystem()
+ self._transform_context = object()
+ self.root = FakeLayerTreeRoot()
+ self.layers = []
+
+ @classmethod
+ def instance(cls):
+ if cls._instance is None:
+ cls._instance = cls()
+ return cls._instance
+
+ def crs(self):
+ return self._crs
+
+ def transformContext(self):
+ return self._transform_context
+
+ def addMapLayer(self, layer, add_to_legend=True):
+ self.layers.append((layer, add_to_legend))
+
+ def layerTreeRoot(self):
+ return self.root
+
+ class FakeVectorLayer:
+ created = []
+ valid_by_name = {}
+
+ def __init__(self, uri, name, provider):
+ self.uri = uri
+ self.name = name
+ self.provider = provider
+ self.styles = []
+ self._valid = self.valid_by_name.get(name, True)
+ FakeVectorLayer.created.append(self)
+
+ def isValid(self):
+ return self._valid
+
+ def loadNamedStyle(self, style_path):
+ self.styles.append(style_path)
+
qgis_core = types.ModuleType("qgis.core")
qgis_core.Qgis = FakeQgis
- qgis_core.QgsProject = type("QgsProject", (), {})
- qgis_core.QgsVectorLayer = type("QgsVectorLayer", (), {})
- qgis_core.QgsCoordinateReferenceSystem = type(
- "QgsCoordinateReferenceSystem", (), {}
- )
+ qgis_core.QgsProject = FakeProject
+ qgis_core.QgsVectorLayer = FakeVectorLayer
+ qgis_core.QgsCoordinateReferenceSystem = FakeCoordinateReferenceSystem
qgis_core.QgsFeature = FakeFeature
qgis_core.QgsField = FakeField
qgis_core.QgsFields = FakeFields
@@ -613,3 +689,137 @@ class PluginTests(unittest.TestCase):
object(),
module.QgsVectorFileWriter.CreateOrOverwriteFile,
)
+
+ def test_load_xyz_writes_geopackage_layers(self):
+ module, _, _ = self._import_plugin_module()
+ points = [
+ {
+ "X": 1.0,
+ "Y": 2.0,
+ "Z": 3.0,
+ "Line": "1",
+ "StationNo": "1_00001",
+ "NumLayers": 1,
+ "Geometry": "POINT Z (1 2 3)",
+ }
+ ]
+ doi_points = [
+ {
+ "X": 1.0,
+ "Y": 2.0,
+ "Z": -4.0,
+ "DOI": 7.0,
+ "ZDOI": -4.0,
+ "Geometry": "POINT Z (1 2 -4)",
+ }
+ ]
+ layers = [
+ {
+ "X": 1.0,
+ "Y": 2.0,
+ "Z": 3.0,
+ "ZTop": 3.0,
+ "ZMid": 2.5,
+ "ZBottom": 2.0,
+ "DepthTop": 0.0,
+ "DepthBottom": 1.0,
+ "Resistivity": 10.0,
+ "Color": "#008cff",
+ "Layer": 1,
+ "Geometry": "LINESTRING Z (1 2 3, 1 2 2)",
+ }
+ ]
+ module.core.process_xyz = Mock(return_value=(points, doi_points, layers))
+ module.core.detect_source_epsg = Mock(return_value=None)
+ module.core.write_csv = Mock()
+ plugin = module.TEMLoaderPlugin(Mock())
+
+ plugin._load_xyz(Path("/tmp/model.xyz"))
+
+ self.assertFalse(module.core.write_csv.called)
+ self.assertEqual(
+ [call["layerName"] for call in module.QgsVectorFileWriter.calls],
+ ["layers", "doi", "points"],
+ )
+ self.assertEqual(
+ [call["geometryType"] for call in module.QgsVectorFileWriter.calls],
+ [
+ module.Qgis.WkbType.LineStringZ,
+ module.Qgis.WkbType.PointZ,
+ module.Qgis.WkbType.PointZ,
+ ],
+ )
+ self.assertEqual(
+ [
+ call["actionOnExistingFile"]
+ for call in module.QgsVectorFileWriter.calls
+ ],
+ [
+ module.QgsVectorFileWriter.CreateOrOverwriteFile,
+ module.QgsVectorFileWriter.CreateOrOverwriteLayer,
+ module.QgsVectorFileWriter.CreateOrOverwriteLayer,
+ ],
+ )
+ gpkg_uri = str(Path("/tmp/model.gpkg").resolve())
+ self.assertEqual(
+ [
+ (layer.uri, layer.name, layer.provider)
+ for layer in module.QgsVectorLayer.created
+ ],
+ [
+ (f"{gpkg_uri}|layername=layers", "layers", "ogr"),
+ (f"{gpkg_uri}|layername=doi", "doi", "ogr"),
+ (f"{gpkg_uri}|layername=points", "points", "ogr"),
+ ],
+ )
+ project = module.QgsProject.instance()
+ self.assertEqual(
+ [(layer.name, add_to_legend) for layer, add_to_legend in project.layers],
+ [("layers", False), ("doi", False), ("points", False)],
+ )
+ self.assertEqual(project.root.groups[0].name, "model")
+ self.assertEqual(
+ [layer.name for layer in project.root.groups[0].layers],
+ ["points", "doi", "layers"],
+ )
+
+ def test_load_xyz_skips_empty_doi_geopackage_layer(self):
+ module, _, _ = self._import_plugin_module()
+ points = [
+ {
+ "X": 1.0,
+ "Y": 2.0,
+ "Z": 3.0,
+ "Geometry": "POINT Z (1 2 3)",
+ }
+ ]
+ layers = [
+ {
+ "X": 1.0,
+ "Y": 2.0,
+ "Z": 3.0,
+ "Layer": 1,
+ "Geometry": "LINESTRING Z (1 2 3, 1 2 2)",
+ }
+ ]
+ module.core.process_xyz = Mock(return_value=(points, [], layers))
+ module.core.detect_source_epsg = Mock(return_value=None)
+ module.core.write_csv = Mock()
+ plugin = module.TEMLoaderPlugin(Mock())
+
+ plugin._load_xyz(Path("/tmp/sci.xyz"))
+
+ self.assertFalse(module.core.write_csv.called)
+ self.assertEqual(
+ [call["layerName"] for call in module.QgsVectorFileWriter.calls],
+ ["layers", "points"],
+ )
+ self.assertEqual(
+ [layer.name for layer in module.QgsVectorLayer.created],
+ ["layers", "points"],
+ )
+ project = module.QgsProject.instance()
+ self.assertEqual(
+ [layer.name for layer in project.root.groups[0].layers],
+ ["points", "layers"],
+ )