Skip to content

Commit 63baf69

Browse files
Merge pull request #51 from gregory-halverson-jpl/main
fixing transformations
2 parents 4c125d9 + 64dc94a commit 63baf69

4 files changed

Lines changed: 114 additions & 11 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "rasters"
7-
version = "1.10.0"
7+
version = "1.11.0"
88
description = "raster processing toolkit"
99
readme = "README.md"
1010
authors = [

rasters/raster_geometry.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -590,9 +590,10 @@ def geolocation(self) -> RasterGeolocation:
590590

591591
return RasterGeolocation(x=self.x, y=self.y, crs=self.crs)
592592

593-
def to_crs(self, crs: Union[CRS, str] = WGS84) -> RasterGeolocation:
593+
def to_crs(self, crs: Union[CRS, str] = WGS84) -> 'RasterGeolocation':
594594
from .CRS import CRS
595595
from .raster_geolocation import RasterGeolocation
596+
from .transform_xy import transform_xy
596597

597598
# validate destination CRS
598599
if not isinstance(crs, CRS):
@@ -601,15 +602,7 @@ def to_crs(self, crs: Union[CRS, str] = WGS84) -> RasterGeolocation:
601602
if self.crs == crs:
602603
return self
603604

604-
if self.is_geographic:
605-
x, y = shapely_transform(self.crs, crs, self.y, self.x)
606-
else:
607-
x, y = shapely_transform(self.crs, crs, self.x, self.y)
608-
609-
if crs.is_geographic:
610-
x = np.where(np.logical_or(x < -180, x > 180), np.nan, x)
611-
y = np.where(np.logical_or(y < -90, y > 90), np.nan, y)
612-
605+
x, y = transform_xy(x=self.x, y=self.y, source_crs=self.crs, target_crs=crs)
613606
geolocation = RasterGeolocation(x=x, y=y, crs=crs)
614607

615608
return geolocation

rasters/transform_xy.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from pyproj import Transformer
2+
import numpy as np
3+
4+
def transform_xy(
5+
x: np.ndarray,
6+
y: np.ndarray,
7+
source_crs,
8+
target_crs
9+
) -> tuple[np.ndarray, np.ndarray]:
10+
"""
11+
Transform arrays of x and y coordinates from a source CRS to a target CRS.
12+
13+
Parameters
14+
----------
15+
x : np.ndarray
16+
Array of x coordinates (e.g., longitude or easting).
17+
y : np.ndarray
18+
Array of y coordinates (e.g., latitude or northing).
19+
source_crs : pyproj.CRS, str, or custom CRS
20+
The source coordinate reference system. Can be a pyproj.CRS, proj4 string, EPSG code, or custom CRS class.
21+
target_crs : pyproj.CRS, str, or custom CRS
22+
The target coordinate reference system. Can be a pyproj.CRS, proj4 string, EPSG code, or custom CRS class.
23+
24+
Returns
25+
-------
26+
x_t : np.ndarray
27+
Transformed x coordinates in the target CRS.
28+
y_t : np.ndarray
29+
Transformed y coordinates in the target CRS.
30+
31+
Notes
32+
-----
33+
- Handles both geographic and projected CRS.
34+
- If the target CRS is geographic, output coordinates outside valid bounds are set to np.nan.
35+
- Accepts pyproj.CRS, string, or custom CRS class with 'is_geographic' attribute.
36+
"""
37+
try:
38+
from .CRS import CRS
39+
if not isinstance(source_crs, CRS):
40+
source_crs = CRS(source_crs)
41+
if not isinstance(target_crs, CRS):
42+
target_crs = CRS(target_crs)
43+
except ImportError:
44+
pass
45+
46+
transformer = Transformer.from_crs(source_crs, target_crs, always_xy=True)
47+
x_t, y_t = transformer.transform(x, y)
48+
49+
# If target CRS is geographic, clip to valid bounds
50+
if hasattr(target_crs, 'is_geographic') and target_crs.is_geographic:
51+
x_t = np.where(np.logical_or(x_t < -180, x_t > 180), np.nan, x_t)
52+
y_t = np.where(np.logical_or(y_t < -90, y_t > 90), np.nan, y_t)
53+
54+
return x_t, y_t

tests/test_transform_xy.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import numpy as np
2+
from pyproj import CRS
3+
from rasters.transform_xy import transform_xy
4+
5+
def test_transform_xy_geographic_to_projected():
6+
# WGS84 geographic CRS
7+
source_crs = CRS.from_epsg(4326)
8+
# UTM zone 33N projected CRS
9+
target_crs = CRS.from_epsg(32633)
10+
# Example coordinates: longitude, latitude
11+
x = np.array([12.0, 13.0])
12+
y = np.array([55.0, 56.0])
13+
x_t, y_t = transform_xy(x, y, source_crs, target_crs)
14+
# Check output shape and type
15+
assert x_t.shape == x.shape
16+
assert y_t.shape == y.shape
17+
assert np.all(np.isfinite(x_t))
18+
assert np.all(np.isfinite(y_t))
19+
# Check that transformed coordinates are not equal to input
20+
assert not np.allclose(x, x_t)
21+
assert not np.allclose(y, y_t)
22+
23+
def test_transform_xy_projected_to_geographic():
24+
# UTM zone 33N projected CRS
25+
source_crs = CRS.from_epsg(32633)
26+
# WGS84 geographic CRS
27+
target_crs = CRS.from_epsg(4326)
28+
# Example coordinates: easting, northing
29+
x = np.array([500000, 600000])
30+
y = np.array([6100000, 6200000])
31+
x_t, y_t = transform_xy(x, y, source_crs, target_crs)
32+
# Check output shape and type
33+
assert x_t.shape == x.shape
34+
assert y_t.shape == y.shape
35+
assert np.all(np.isfinite(x_t))
36+
assert np.all(np.isfinite(y_t))
37+
# Check that transformed coordinates are not equal to input
38+
assert not np.allclose(x, x_t)
39+
assert not np.allclose(y, y_t)
40+
41+
def test_transform_xy_clip_geographic():
42+
# WGS84 geographic CRS
43+
source_crs = CRS.from_epsg(4326)
44+
target_crs = CRS.from_epsg(4326)
45+
# Coordinates outside valid bounds
46+
x = np.array([-200, 0, 200])
47+
y = np.array([-100, 0, 100])
48+
x_t, y_t = transform_xy(x, y, source_crs, target_crs)
49+
# Out-of-bounds should be nan
50+
assert np.isnan(x_t[0])
51+
assert np.isnan(x_t[2])
52+
assert np.isnan(y_t[0])
53+
assert np.isnan(y_t[2])
54+
# Valid should remain
55+
assert x_t[1] == 0
56+
assert y_t[1] == 0

0 commit comments

Comments
 (0)