Skip to content

Commit 11c2b7a

Browse files
authored
feat: Implement bounds, res, and shape, and xy (#9)
1 parent 922d509 commit 11c2b7a

File tree

4 files changed

+82
-22
lines changed

4 files changed

+82
-22
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ morecantile = ["morecantile>=7.0,<8.0"]
1717

1818
[dependency-groups]
1919
dev = [
20+
"affine>=3.0rc1",
2021
"ipykernel>=7.1.0",
2122
"obstore>=0.8.2",
2223
"pytest>=9.0.2",

src/async_geotiff/_geotiff.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from functools import cached_property
34
from typing import TYPE_CHECKING, Literal, Self
45

56
from affine import Affine
@@ -95,14 +96,18 @@ def block_size(self, bidx: int, i: int, j: int) -> int:
9596
"""
9697
raise NotImplementedError()
9798

98-
@property
99+
@cached_property
99100
def bounds(self) -> tuple[float, float, float, float]:
100101
"""Returns the bounds of the dataset in the units of its coordinate reference system.
101102
102103
Returns:
103104
(lower left x, lower left y, upper right x, upper right y)
104105
"""
105-
raise NotImplementedError()
106+
transform = self.transform
107+
(left, top) = transform * (0, 0)
108+
(right, bottom) = transform * (self.width, self.height)
109+
110+
return (left, bottom, right, top)
106111

107112
@property
108113
def colorinterp(self) -> list[str]:
@@ -201,9 +206,13 @@ def is_tiled(self) -> bool:
201206
raise NotImplementedError()
202207

203208
@property
204-
def nodata(self) -> float | int | None:
209+
def nodata(self) -> float | None:
205210
"""The dataset's single nodata value."""
206-
raise NotImplementedError()
211+
nodata = self._primary_ifd.gdal_nodata
212+
if nodata is None:
213+
return None
214+
215+
return float(nodata)
207216

208217
@property
209218
def photometric(self) -> PhotometricInterp | None:
@@ -212,17 +221,18 @@ def photometric(self) -> PhotometricInterp | None:
212221
# https://rasterio.readthedocs.io/en/stable/api/rasterio.enums.html#rasterio.enums.PhotometricInterp
213222
raise NotImplementedError()
214223

215-
@property
224+
@cached_property
216225
def res(self) -> tuple[float, float]:
217226
"""Returns the (width, height) of pixels in the units of its coordinate reference system."""
218-
raise NotImplementedError()
227+
transform = self.transform
228+
return (transform.a, -transform.e)
219229

220230
@property
221231
def shape(self) -> tuple[int, int]:
222232
"""Get the shape (height, width) of the full image."""
223-
raise NotImplementedError()
233+
return (self.height, self.width)
224234

225-
@property
235+
@cached_property
226236
def transform(self) -> Affine:
227237
"""The dataset's georeferencing transformation matrix
228238
@@ -275,12 +285,12 @@ def xy(
275285
self,
276286
row: int,
277287
col: int,
278-
offset: Literal["center", "ul", "ur", "ll", "lr"] = "center",
288+
offset: Literal["center", "ul", "ur", "ll", "lr"] | str = "center",
279289
) -> tuple[float, float]:
280290
"""Get the coordinates x, y of a pixel at row, col.
281291
282292
The pixel's center is returned by default, but a corner can be returned
283-
by setting `offset` to one of `ul, ur, ll, lr`.
293+
by setting `offset` to one of `"ul"`, `"ur"`, `"ll"`, `"lr"`.
284294
285295
Parameters:
286296
row: Pixel row.
@@ -289,7 +299,27 @@ def xy(
289299
pixel or for a corner.
290300
291301
"""
292-
raise NotImplementedError()
302+
transform = self.transform
303+
304+
if offset == "center":
305+
c = col + 0.5
306+
r = row + 0.5
307+
elif offset == "ul":
308+
c = col
309+
r = row
310+
elif offset == "ur":
311+
c = col + 1
312+
r = row
313+
elif offset == "ll":
314+
c = col
315+
r = row + 1
316+
elif offset == "lr":
317+
c = col + 1
318+
r = row + 1
319+
else:
320+
raise ValueError(f"Invalid offset value: {offset}")
321+
322+
return transform * (c, r)
293323

294324

295325
def has_geokeys(ifd: ImageFileDirectory) -> bool:

tests/test_geotiff.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,51 @@
99
if TYPE_CHECKING:
1010
from rasterio.io import DatasetReader
1111

12+
LoadGeoTIFF = Callable[[str], Awaitable[GeoTIFF]]
13+
LoadRasterio = Callable[[str], DatasetReader]
14+
1215

1316
@pytest.mark.asyncio
14-
async def test_height_width(
15-
load_geotiff: Callable[[str], Awaitable[GeoTIFF]],
16-
load_rasterio: Callable[[str], DatasetReader],
17-
) -> None:
17+
async def test_bounds(load_geotiff: LoadGeoTIFF, load_rasterio: LoadRasterio) -> None:
1818
name = "uint8_rgb_deflate_block64_cog"
1919

2020
geotiff = await load_geotiff(name)
2121
with load_rasterio(name) as rasterio_ds:
22-
assert rasterio_ds.height == geotiff.height
23-
assert rasterio_ds.width == geotiff.width
22+
assert rasterio_ds.bounds == geotiff.bounds
2423

2524

2625
@pytest.mark.asyncio
2726
async def test_transform(
28-
load_geotiff: Callable[[str], Awaitable[GeoTIFF]],
29-
load_rasterio: Callable[[str], DatasetReader],
27+
load_geotiff: LoadGeoTIFF, load_rasterio: LoadRasterio
3028
) -> None:
3129
name = "uint8_rgb_deflate_block64_cog"
3230

3331
geotiff = await load_geotiff(name)
3432
with load_rasterio(name) as rasterio_ds:
3533
assert rasterio_ds.transform == geotiff.transform
34+
assert rasterio_ds.height == geotiff.height
35+
assert rasterio_ds.width == geotiff.width
36+
assert rasterio_ds.shape == geotiff.shape
37+
assert rasterio_ds.res == geotiff.res
38+
39+
40+
@pytest.mark.asyncio
41+
async def test_xy(load_geotiff: LoadGeoTIFF, load_rasterio: LoadRasterio) -> None:
42+
name = "uint8_rgb_deflate_block64_cog"
43+
44+
geotiff = await load_geotiff(name)
45+
46+
x = geotiff.width // 2
47+
y = geotiff.height // 2
48+
49+
with load_rasterio(name) as rasterio_ds:
50+
for offset in ["center", "ul", "ur", "ll", "lr"]:
51+
assert rasterio_ds.xy(
52+
x,
53+
y,
54+
offset=offset,
55+
) == geotiff.xy(
56+
x,
57+
y,
58+
offset=offset,
59+
)

uv.lock

Lines changed: 8 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)