Skip to content

Commit c5a63fa

Browse files
committed
fix: transform points to the DEM CRS before sampling the DEM
The DEM was sampled with model CRS coordinates. A DEM in another CRS returned no data for every point, so all points got an elevation of 0.
1 parent f0993b2 commit c5a63fa

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

‎loopstructural/main/data_manager.py‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,26 @@ def _refresh_dem_function(self):
355355
`dem_layer`, even when `use_dem` is True).
356356
"""
357357
if self.use_dem and self.dem_layer is not None:
358+
# Callers pass (x, y) in the model CRS, but `sample()` expects the
359+
# DEM's own CRS. Cache the transform and rebuild it only when the
360+
# model or DEM CRS changes, because this is called once per point.
361+
cache = {'model_crs': None, 'dem_crs': None, 'transform': None}
362+
363+
def to_dem_crs(x, y):
364+
model_crs = self.get_model_crs()
365+
dem_crs = self.dem_layer.crs()
366+
if (
367+
model_crs is None
368+
or not model_crs.isValid()
369+
or not dem_crs.isValid()
370+
or model_crs == dem_crs
371+
):
372+
return QgsPointXY(x, y)
373+
if cache['model_crs'] != model_crs or cache['dem_crs'] != dem_crs:
374+
cache['model_crs'] = model_crs
375+
cache['dem_crs'] = dem_crs
376+
cache['transform'] = QgsCoordinateTransform(model_crs, dem_crs, self.project)
377+
return cache['transform'].transform(QgsPointXY(x, y))
358378

359379
def dem_function(x, y):
360380
if not self.dem_layer.isValid():
@@ -363,7 +383,7 @@ def dem_function(x, y):
363383
log_level=2,
364384
)
365385
return 0.0
366-
value = self.dem_layer.dataProvider().sample(QgsPointXY(x, y), 1)[0]
386+
value = self.dem_layer.dataProvider().sample(to_dem_crs(x, y), 1)[0]
367387
# `sample()` returns NaN (not None) for points outside the
368388
# raster's extent or on nodata cells, depending on provider.
369389
if value is None or np.isnan(value):

‎tests/qgis/test_dem_function.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from unittest.mock import Mock
2+
3+
import numpy as np
4+
import pytest
5+
from osgeo import gdal, osr
6+
from qgis.core import (
7+
QgsCoordinateReferenceSystem,
8+
QgsCoordinateTransform,
9+
QgsPointXY,
10+
QgsProject,
11+
QgsRasterLayer,
12+
)
13+
14+
from loopstructural.main.data_manager import ModellingDataManager
15+
16+
DEM_VALUE = 460.0
17+
MODEL_CRS = "EPSG:32755"
18+
19+
20+
def _write_geographic_dem(path):
21+
"""Write a small constant-value GeoTIFF in EPSG:4326 over (147E, 42S)."""
22+
driver = gdal.GetDriverByName("GTiff")
23+
dataset = driver.Create(str(path), 10, 10, 1, gdal.GDT_Float32)
24+
dataset.SetGeoTransform((146.9, 0.02, 0.0, -41.9, 0.0, -0.02))
25+
srs = osr.SpatialReference()
26+
srs.ImportFromEPSG(4326)
27+
dataset.SetProjection(srs.ExportToWkt())
28+
dataset.GetRasterBand(1).WriteArray(np.full((10, 10), DEM_VALUE, dtype=np.float32))
29+
dataset.FlushCache()
30+
dataset = None
31+
32+
33+
@pytest.fixture
34+
def data_manager(tmp_path):
35+
path = tmp_path / "dem.tif"
36+
_write_geographic_dem(path)
37+
dem_layer = QgsRasterLayer(str(path), "dem")
38+
assert dem_layer.isValid()
39+
manager = ModellingDataManager(
40+
project=QgsProject.instance(), mapCanvas=Mock(), logger=Mock()
41+
)
42+
manager.set_model_manager(Mock())
43+
manager.set_model_crs(QgsCoordinateReferenceSystem(MODEL_CRS), use_project_crs=False)
44+
manager.set_dem_layer(dem_layer)
45+
manager.set_use_dem(True)
46+
return manager
47+
48+
49+
def _dem_centre_in_model_crs():
50+
transform = QgsCoordinateTransform(
51+
QgsCoordinateReferenceSystem("EPSG:4326"),
52+
QgsCoordinateReferenceSystem(MODEL_CRS),
53+
QgsProject.instance(),
54+
)
55+
return transform.transform(QgsPointXY(147.0, -42.0))
56+
57+
58+
def test_dem_in_another_crs_is_sampled_at_model_coordinates(data_manager):
59+
"""The DEM is sampled after the model (x, y) is transformed to the DEM CRS."""
60+
centre = _dem_centre_in_model_crs()
61+
assert data_manager.dem_function(centre.x(), centre.y()) == pytest.approx(DEM_VALUE)
62+
63+
64+
def test_point_outside_the_dem_gives_zero(data_manager):
65+
centre = _dem_centre_in_model_crs()
66+
assert data_manager.dem_function(centre.x() + 1e6, centre.y()) == 0.0

0 commit comments

Comments
 (0)