Skip to content

Commit 0db2f07

Browse files
committed
ready for review
1 parent 8e3aa92 commit 0db2f07

7 files changed

Lines changed: 154 additions & 22 deletions

File tree

CHANGES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## Changes in 0.2.0 (under development)
2+
3+
- Enhanced the function `bbox_overlap` so that it can handle bounding boxed crossing
4+
the antimeridian
5+
- Added new class method `GridMapping.regular_from_bbox`, which allows creating a
6+
regular grid mapping directly from a bounding box, spatial resolution, and CRS.
7+
- Bug fix: fixed grid mapping creation for irregular grids with decreasing longitude
8+
along axis 1.
9+
110
## Changes in 0.1.1
211

312
- Improved `xcube_resampling.utils.clip_dataset_by_bbox` to support datasets with

tests/gridmapping/test_regular.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,13 @@ def test_to_regular(self):
335335
self.assertEqual(gm_test.crs, CRS_WGS84)
336336
self.assertEqual(gm_test.xy_res, (0.01, 0.01))
337337
self.assertTrue(gm_test.is_j_axis_up)
338+
339+
def test_create_regular_from_bbox(self):
340+
gm_test = GridMapping.regular_from_bbox(
341+
[10, 40, 20, 50], 0.1, "EPSG:4326", tile_size=10
342+
)
343+
self.assertEqual(gm_test.size, (100, 100))
344+
self.assertEqual(gm_test.tile_size, (10, 10))
345+
self.assertEqual(gm_test.crs, CRS_WGS84)
346+
self.assertEqual(gm_test.xy_res, (0.1, 0.1))
347+
self.assertFalse(gm_test.is_j_axis_up)

tests/test_utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,33 @@ def test_bbox_overlap(self):
330330
expected = 36 / 36
331331
self.assertAlmostEqual(bbox_overlap(source, target), expected)
332332

333+
# antimeridian - identical wrapped boxes (e.g. 170°E → -170°W)
334+
source = (170, -10, -170, 10)
335+
target = (170, -10, -170, 10)
336+
self.assertEqual(bbox_overlap(source, target), 1.0)
337+
338+
# antimeridian - source crosses, target fully inside one side
339+
source = (170, 0, -170, 10)
340+
target = (175, 0, 178, 10)
341+
expected = 30 / 200
342+
self.assertAlmostEqual(bbox_overlap(source, target), expected)
343+
344+
# antimeridian - partial overlap across both segments
345+
source = (170, 0, -170, 10)
346+
target = (160, 0, 175, 10)
347+
expected = 50 / 200
348+
self.assertAlmostEqual(bbox_overlap(source, target), expected)
349+
350+
# antimeridian - non-wrapped target covering entire world
351+
source = (170, 0, -170, 10)
352+
target = (-180, -90, 180, 90)
353+
self.assertEqual(bbox_overlap(source, target), 1.0)
354+
355+
# antimeridian - touching at boundary only (no real overlap)
356+
source = (170, 0, -170, 10)
357+
target = (-170, 0, -160, 10)
358+
self.assertAlmostEqual(bbox_overlap(source, target), 0.0)
359+
333360

334361
class TestClipDatasetByBBox(unittest.TestCase):
335362

xcube_resampling/gridmapping/base.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# DEALINGS IN THE SOFTWARE.
2121

2222
import abc
23+
from collections.abc import Sequence
2324
import copy
2425
import math
2526
import threading
@@ -61,22 +62,20 @@ class GridMapping(abc.ABC):
6162
a transformation from image pixel coordinates to spatial Earth coordinates
6263
defined in a well-known coordinate reference system (CRS).
6364
64-
This class cannot be instantiated directly. Use one of its factory methods
65+
This class cannot be instantiated directly. Use one of its class methods
6566
to create instances:
6667
67-
* :meth:`regular`
68-
* :meth:`from_dataset`
69-
* :meth:`from_coords`
68+
* `regular`
69+
* `regular_from_bbox`
70+
* `from_dataset`
71+
* `from_coords`
7072
7173
Some instance methods can be used to derive new instances:
7274
73-
* :meth:`derive`
74-
* :meth:`scale`
75-
* :meth:`transform`
76-
* :meth:`to_regular`
77-
78-
This class is thread-safe.
79-
75+
* `derive`
76+
* `scale`
77+
* `transform`
78+
* `to_regular`
8079
"""
8180

8281
def __init__(
@@ -737,6 +736,36 @@ def regular(
737736
is_j_axis_up=is_j_axis_up,
738737
)
739738

739+
@classmethod
740+
def regular_from_bbox(
741+
cls,
742+
bbox: Sequence[FloatInt],
743+
xy_res: float | tuple[float, float],
744+
crs: str | pyproj.crs.CRS,
745+
tile_size: int | tuple[int, int] = None,
746+
is_j_axis_up: bool = False,
747+
) -> "GridMapping":
748+
"""Creates a regular grid mapping for a given coordinate reference system based
749+
on the given bounding box and spatial resolution.
750+
751+
Args:
752+
bbox: Bounding box coordinates in the format [west, south, east, north].
753+
The values must be in the given CRS.
754+
xy_res: Resolution in x- and y-directions.
755+
crs: Coordinate reference system (e.g., "EPSG:4326").
756+
tile_size: Chunk size for the grid; if a single int is given,
757+
square chunk size is assumed. Defaults to 1024.
758+
is_j_axis_up: Whether positive j-axis points up. Defaults to False.
759+
760+
Returns:
761+
A regular grid mapping object.
762+
"""
763+
from .regular import new_regular_grid_mapping_from_bbox
764+
765+
return new_regular_grid_mapping_from_bbox(
766+
bbox, xy_res, crs, tile_size=tile_size, is_j_axis_up=is_j_axis_up
767+
)
768+
740769
def to_regular(
741770
self, tile_size: int | tuple[int, int] | None = None, is_j_axis_up: bool = False
742771
) -> "GridMapping":

xcube_resampling/gridmapping/regular.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2020
# DEALINGS IN THE SOFTWARE.
2121

22+
from collections.abc import Sequence
23+
2224
import dask.array as da
25+
import numpy as np
2326
import pyproj
2427
import xarray as xr
2528

@@ -33,6 +36,7 @@
3336
_normalize_number_pair,
3437
_to_int_or_float,
3538
)
39+
from xcube_resampling.constants import FloatInt
3640

3741

3842
class RegularGridMapping(GridMapping):
@@ -164,3 +168,38 @@ def to_regular_grid_mapping(
164168
tile_size=tile_size,
165169
is_j_axis_up=is_j_axis_up,
166170
)
171+
172+
173+
def new_regular_grid_mapping_from_bbox(
174+
bbox: Sequence[FloatInt],
175+
xy_res: FloatInt | tuple[FloatInt, FloatInt],
176+
crs: str | pyproj.CRS,
177+
tile_size: int | tuple[int, int] = 1024,
178+
is_j_axis_up: bool = False,
179+
) -> GridMapping:
180+
"""Creates a regular grid mapping for a given coordinate reference system based on
181+
the given bounding box and spatial resolution.
182+
183+
Args:
184+
bbox: Bounding box coordinates in the format [west, south, east, north].
185+
The values must be in the given CRS.
186+
spatial_res: Spatial resolution of the grid.
187+
crs: Coordinate reference system (e.g., "EPSG:4326").
188+
tile_size: Chunk size for the grid; if a single int is given,
189+
square chunk size is assumed. Defaults to 1024.
190+
191+
Returns:
192+
A regular grid mapping object.
193+
"""
194+
if xy_res is not tuple:
195+
xy_res = (xy_res, xy_res)
196+
x_size = int(np.ceil((bbox[2] - bbox[0]) / xy_res[1]))
197+
y_size = int(np.ceil(abs(bbox[3] - bbox[1]) / xy_res[0]))
198+
return new_regular_grid_mapping(
199+
size=(x_size, y_size),
200+
xy_min=(bbox[0], bbox[1]),
201+
xy_res=xy_res,
202+
crs=crs,
203+
tile_size=tile_size,
204+
is_j_axis_up=is_j_axis_up,
205+
)

xcube_resampling/utils.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def bbox_overlap(
236236
) -> float:
237237
"""
238238
Calculate the fraction of the source bounding box that overlaps with the target
239-
bounding box.
239+
bounding box relative to the area of the source bounding box.
240240
241241
Args:
242242
source_bbox: (min_x, min_y, max_x, max_y)
@@ -245,19 +245,37 @@ def bbox_overlap(
245245
Returns:
246246
float in [0, 1]
247247
"""
248-
inter_min_x = max(source_bbox[0], target_bbox[0])
249-
inter_min_y = max(source_bbox[1], target_bbox[1])
250-
inter_max_x = min(source_bbox[2], target_bbox[2])
251-
inter_max_y = min(source_bbox[3], target_bbox[3])
252-
253-
inter_w = max(0, inter_max_x - inter_min_x)
254-
inter_h = max(0, inter_max_y - inter_min_y)
255-
inter_area = inter_w * inter_h
256-
area_source = (source_bbox[2] - source_bbox[0]) * (source_bbox[3] - source_bbox[1])
248+
source_bboxes = _split_bbox_antimeridian(source_bbox)
249+
target_bboxes = _split_bbox_antimeridian(target_bbox)
250+
251+
inter_area = 0.0
252+
for source_bbox in source_bboxes:
253+
for target_bbox in target_bboxes:
254+
inter_min_x = max(source_bbox[0], target_bbox[0])
255+
inter_min_y = max(source_bbox[1], target_bbox[1])
256+
inter_max_x = min(source_bbox[2], target_bbox[2])
257+
inter_max_y = min(source_bbox[3], target_bbox[3])
258+
259+
inter_w = max(0, inter_max_x - inter_min_x)
260+
inter_h = max(0, inter_max_y - inter_min_y)
261+
inter_area += inter_w * inter_h
262+
area_source = 0.0
263+
for source_bbox in source_bboxes:
264+
area_source += (source_bbox[2] - source_bbox[0]) * (
265+
source_bbox[3] - source_bbox[1]
266+
)
257267

258268
return inter_area / area_source
259269

260270

271+
def _split_bbox_antimeridian(bbox: Sequence[FloatInt]):
272+
min_x, min_y, max_x, max_y = bbox
273+
274+
if max_x < min_x:
275+
return [(min_x, min_y, 180, max_y), (-180, min_y, max_x, max_y)]
276+
return [bbox]
277+
278+
261279
def normalize_grid_mapping(ds: xr.Dataset, gm: GridMapping) -> xr.Dataset:
262280
"""
263281
Normalize the grid mapping of a dataset to use a standard "spatial_ref" coordinate.

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.1.1"
22+
__version__ = "0.2.0.dev0"

0 commit comments

Comments
 (0)