Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions src/rasterix/raster_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ def concat(

def _new_with_bbox(self, bbox: BoundingBox) -> RasterIndex:
affine = self.transform()
new_affine, Nx, Ny = bbox_to_affine(bbox, rx=affine.a, ry=affine.e)
new_affine, Nx, Ny = bbox_to_affine(bbox, affine)
# TODO: set xdim, ydim explicitly
new_index = self.from_transform(new_affine, width=Nx, height=Ny)
assert new_index.bbox == bbox
Expand Down Expand Up @@ -984,18 +984,28 @@ def get_indexer(off, our_off, start, stop, spacing, tol, size) -> np.ndarray:
return idxr


def bbox_to_affine(bbox: BoundingBox, rx, ry) -> tuple[Affine, int, int]:
def bbox_to_affine(bbox: BoundingBox, affine: Affine) -> tuple[Affine, int, int]:
# Fraction of a pixel that can be ignored, defaults to 1/100. Bounding box of the output
# geobox is allowed to be smaller than supplied bounding box by that amount.
# FIXME: translate user-provided `tolerance` to `tol`
tol: float = 0.01

offx, nx = snap_grid(bbox.left, bbox.right, rx, 0, tol=tol)
offy, ny = snap_grid(bbox.bottom, bbox.top, ry, 0, tol=tol)
rx = affine.a
ry = affine.e

affine = Affine.translation(offx, offy) * Affine.scale(rx, ry)
# Calculate the pixel fraction offset from the affine's grid alignment.
# This ensures the new grid aligns with the original grid rather than snapping
# to a global grid at x=0, y=0.
# See https://github.com/xarray-contrib/rasterix/issues/67
off_pix_x = (affine.c / abs(rx)) % 1.0
off_pix_y = (affine.f / abs(ry)) % 1.0

return affine, nx, ny
offx, nx = snap_grid(bbox.left, bbox.right, rx, off_pix_x, tol=tol)
offy, ny = snap_grid(bbox.bottom, bbox.top, ry, off_pix_y, tol=tol)

new_affine = Affine.translation(offx, offy) * Affine.scale(rx, ry)

return new_affine, nx, ny


def as_compatible_bboxes(*indexes: RasterIndex, concat_dim: Hashable | None) -> tuple[BoundingBox, ...]:
Expand Down
33 changes: 33 additions & 0 deletions tests/test_raster_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,39 @@ def test_align():
aligned = xr.align(*datasets, join="exact")


def test_join_partially_overlapping_indexes():
"""Test joining two partially overlapping indexes with non-grid-aligned coordinates.

Regression test for https://github.com/xarray-contrib/rasterix/issues/67

This tests the case where the transform coordinates are not aligned to a global
grid at x=0, y=0 (e.g., 399955.0 / 10.0 = 39995.5, which has a 0.5 pixel offset).
"""
transform1 = Affine.from_gdal(399955.0, 10.0, 0.0, 5500025.0, 0.0, -10.0)
transform2 = Affine.from_gdal(399955.0, 10.0, 0.0, 5497625.0, 0.0, -10.0)

index1 = RasterIndex.from_transform(transform1, width=256, height=256)
index2 = RasterIndex.from_transform(transform2, width=256, height=256)

# Inner join should produce a 256x16 pixel index (the overlap region)
result = index1.join(index2, how="inner")

assert result.xy_shape == (256, 16)
assert result.bbox.left == 399955.0
assert result.bbox.right == 402515.0
assert result.bbox.bottom == 5497465.0
assert result.bbox.top == 5497625.0

# Outer join should produce a 256x496 pixel index (combined coverage)
result_outer = index1.join(index2, how="outer")

assert result_outer.xy_shape == (256, 496) # 256 + 256 - 16 = 496
assert result_outer.bbox.left == 399955.0
assert result_outer.bbox.right == 402515.0
assert result_outer.bbox.bottom == 5495065.0
assert result_outer.bbox.top == 5500025.0


def test_repr_inline() -> None:
index1 = RasterIndex.from_transform(Affine.identity(), width=12, height=10)
ds1 = xr.Dataset(coords=xr.Coordinates.from_xindex(index1))
Expand Down
Loading