Skip to content

Commit 17caa13

Browse files
committed
ready for review
1 parent b1ce618 commit 17caa13

4 files changed

Lines changed: 96 additions & 3 deletions

File tree

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## Changes in 0.3.4
2+
3+
- Added `utils.transform_resolution` to convert spatial resolution between coordinate
4+
reference systems (CRS) and introduced deprecation warnings for
5+
`utils.resolution_degrees_to_meters` and `utils.resolution_meters_to_degrees`, which
6+
are superseded by `utils.transform_resolution`.
7+
18
## Changes in 0.3.3
29

310
- Added function `utils.resolution_degrees_to_meters` to convert spatial resolution

tests/test_utils.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
reproject_bbox,
3131
resolution_degrees_to_meters,
3232
resolution_meters_to_degrees,
33+
transform_resolution,
3334
)
3435

3536
from .sampledata import create_2x4x4_dataset_with_irregular_coords
@@ -418,9 +419,34 @@ def test_bbox_overlap(self):
418419
target = (-170, 0, -160, 10)
419420
self.assertAlmostEqual(0.0, bbox_overlap(source, target))
420421

422+
def test_transform_resolution(self):
423+
# EPSG:4326 → UTM (meters)
424+
res = transform_resolution((3, 60), 1.0, "EPSG:4326", "EPSG:32631")
425+
self.assertAlmostEqual(55777.9, res[0], places=1)
426+
self.assertAlmostEqual(111376.2, res[1], places=1)
427+
428+
# UTM (meters) → EPSG:4326
429+
res = transform_resolution(
430+
(500_000, 6_651_411), 111376, "EPSG:32631", "EPSG:4326"
431+
)
432+
self.assertAlmostEqual(2.0, res[0], places=2)
433+
self.assertAlmostEqual(1.0, res[1], places=2)
434+
435+
# UTM → UTM (same CRS, should remain ~unchanged in magnitude)
436+
res = transform_resolution(
437+
(500000, 0), (1000, 1000), "EPSG:32631", "EPSG:32631"
438+
)
439+
self.assertAlmostEqual(1000, res[0])
440+
self.assertAlmostEqual(1000, res[1])
441+
442+
# US survey foot to meter
443+
res = transform_resolution((0, 0), (1, 1), "EPSG:3561", "EPSG:32605")
444+
self.assertAlmostEqual(0.30525, res[0], places=4)
445+
self.assertAlmostEqual(0.30525, res[1], places=4)
446+
421447
def test_resolution_meters_to_degrees(self):
422448
# 111320 m ≈ 1 degree at equator
423-
lat_deg, lon_deg = resolution_meters_to_degrees(111320, 0)
449+
lon_deg, lat_deg = resolution_meters_to_degrees(111320, 0)
424450
self.assertAlmostEqual(1.0, lat_deg, places=6)
425451
self.assertAlmostEqual(1.0, lon_deg, places=6)
426452

@@ -436,7 +462,7 @@ def test_resolution_meters_to_degrees(self):
436462

437463
def test_resolution_degrees_to_meters(self):
438464
# 111320 m ≈ 1 degree at equator
439-
lat_deg, lon_deg = resolution_degrees_to_meters(1, 0)
465+
lon_deg, lat_deg = resolution_degrees_to_meters(1, 0)
440466
self.assertAlmostEqual(111320, lat_deg, places=6)
441467
self.assertAlmostEqual(111320, lon_deg, places=6)
442468

xcube_resampling/utils.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence
2323
from dataclasses import dataclass
24+
import warnings
2425

2526
import dask.array as da
2627
import numpy as np
@@ -313,12 +314,54 @@ def _split_bbox_antimeridian(bbox: Sequence[FloatInt]) -> Sequence[Sequence[Floa
313314
return [bbox]
314315

315316

317+
def transform_resolution(
318+
ref_point: tuple[FloatInt, FloatInt],
319+
resolution: FloatInt | tuple[FloatInt, FloatInt],
320+
src_crs: str | pyproj.CRS,
321+
dst_crs: str | pyproj.CRS,
322+
) -> tuple[FloatInt, FloatInt]:
323+
"""Estimate local spatial resolution in the destination CRS using
324+
finite differences.
325+
326+
Args:
327+
ref_point: Reference point (easting, northing) in the source CRS.
328+
resolution: Spatial resolution in the source CRS. Can be a single number
329+
(applied equally to both axes) or a tuple `(x_res, y_res)`.
330+
src_crs: Source coordinate reference system.
331+
dst_crs: Destination coordinate reference system.
332+
333+
Returns:
334+
tuple: Estimated local resolution in the destination CRS as `(x_res, y_res)`.
335+
"""
336+
transformer = pyproj.Transformer.from_crs(src_crs, dst_crs, always_xy=True)
337+
if not isinstance(resolution, tuple):
338+
resolution = (resolution, resolution)
339+
340+
# reference point
341+
x0, y0 = transformer.transform(*ref_point)
342+
343+
# step in x direction
344+
x1, y1 = transformer.transform(ref_point[0] + resolution[0], ref_point[1])
345+
346+
# step in y direction
347+
x2, y2 = transformer.transform(ref_point[0], ref_point[1] + resolution[1])
348+
349+
# Euclidean distances
350+
res_x = np.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
351+
res_y = np.sqrt((x2 - x0) ** 2 + (y2 - y0) ** 2)
352+
353+
return res_x, res_y
354+
355+
316356
def resolution_meters_to_degrees(
317357
resolution: FloatInt | tuple[FloatInt, FloatInt], latitude: FloatInt
318358
) -> tuple[FloatInt, FloatInt]:
319359
"""Convert spatial resolution from meters to degrees in longitude and latitude
320360
at a given geographic latitude.
321361
362+
This function is deprecated and will be removed in a future release.
363+
Use `transform_resolution` instead for CRS-aware and more accurate conversions.
364+
322365
Args:
323366
resolution: Spatial resolution in meters. Can be a single number
324367
(applied equally to both axes) or a tuple ``(x_res, y_res)``.
@@ -333,6 +376,13 @@ def resolution_meters_to_degrees(
333376
- 1 degree of longitude ≈ 111,320 * cos(latitude) meters.
334377
335378
"""
379+
warnings.warn(
380+
"resolution_meters_to_degrees is deprecated and will be removed in a future release. "
381+
"Use `transform_resolution` instead for accurate CRS-based resolution handling.",
382+
DeprecationWarning,
383+
stacklevel=2,
384+
)
385+
336386
if not isinstance(resolution, tuple):
337387
resolution = (resolution, resolution)
338388
return (
@@ -346,6 +396,9 @@ def resolution_degrees_to_meters(
346396
) -> tuple[FloatInt, FloatInt]:
347397
"""Convert spatial resolution from degrees to meters at a given geographic latitude.
348398
399+
This function is deprecated and will be removed in a future release.
400+
Use `transform_resolution` instead for CRS-aware and more accurate conversions.
401+
349402
Args:
350403
resolution: Spatial resolution in degrees. Can be a single number
351404
(applied equally to both axes) or a tuple ``(lon_res, lat_res)``.
@@ -360,6 +413,13 @@ def resolution_degrees_to_meters(
360413
- 1 degree of longitude ≈ 111,320 * cos(latitude) meters.
361414
362415
"""
416+
warnings.warn(
417+
"resolution_degrees_to_meters is deprecated and will be removed in a future release. "
418+
"Use `transform_resolution` instead for accurate CRS-based resolution handling.",
419+
DeprecationWarning,
420+
stacklevel=2,
421+
)
422+
363423
if not isinstance(resolution, tuple):
364424
resolution = (resolution, resolution)
365425
return (

xcube_resampling/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2020
# DEALINGS IN THE SOFTWARE.
2121

22-
__version__ = "0.3.3"
22+
__version__ = "0.3.4"

0 commit comments

Comments
 (0)