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
70 changes: 4 additions & 66 deletions src/async_geotiff/_geotiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,25 @@

from dataclasses import dataclass, field
from functools import cached_property
from typing import TYPE_CHECKING, Literal, Self
from typing import TYPE_CHECKING, Self

from affine import Affine
from async_tiff import TIFF
from async_tiff.enums import PhotometricInterpretation

from async_geotiff._crs import crs_from_geo_keys
from async_geotiff._overview import Overview
from async_geotiff._transform import TransformMixin
from async_geotiff.enums import Compression, Interleaving

if TYPE_CHECKING:
from collections.abc import Callable

import pyproj
from async_tiff import GeoKeyDirectory, ImageFileDirectory, ObspecInput
from async_tiff.store import ObjectStore # type: ignore # noqa: PGH003


@dataclass(frozen=True, init=False, kw_only=True, repr=False)
class GeoTIFF:
class GeoTIFF(TransformMixin):
"""A class representing a GeoTIFF image."""

_tiff: TIFF
Expand Down Expand Up @@ -222,27 +221,6 @@ def height(self) -> int:
"""The height (number of rows) of the full image."""
return self._primary_ifd.image_height

def index(
self,
x: float,
y: float,
op: Callable[[float, float], tuple[int, int]] | None = None,
) -> tuple[int, int]:
"""Get the (row, col) index of the pixel containing (x, y).

Args:
x: x value in coordinate reference system
y: y value in coordinate reference system
op: function, optional (default: numpy.floor)
Function to convert fractional pixels to whole numbers
(floor, ceiling, round)

Returns:
(row index, col index)

"""
raise NotImplementedError

def indexes(self) -> list[int]:
"""Return the 1-based indexes of each band in the dataset.

Expand Down Expand Up @@ -283,7 +261,7 @@ def photometric(self) -> PhotometricInterpretation | None:
# https://rasterio.readthedocs.io/en/stable/api/rasterio.enums.html#rasterio.enums.PhotometricInterp
raise NotImplementedError

@cached_property
@property
def res(self) -> tuple[float, float]:
"""Return the (width, height) of pixels in the units of its CRS."""
transform = self.transform
Expand Down Expand Up @@ -344,46 +322,6 @@ def width(self) -> int:
"""The width (number of columns) of the full image."""
return self._primary_ifd.image_width

def xy(
self,
row: int,
col: int,
offset: Literal["center", "ul", "ur", "ll", "lr"] | str = "center",
) -> tuple[float, float]:
"""Get the coordinates x, y of a pixel at row, col.

The pixel's center is returned by default, but a corner can be returned
by setting `offset` to one of `"ul"`, `"ur"`, `"ll"`, `"lr"`.

Args:
row: Pixel row.
col: Pixel column.
offset: Determines if the returned coordinates are for the center of the
pixel or for a corner.

"""
transform = self.transform

if offset == "center":
c = col + 0.5
r = row + 0.5
elif offset == "ul":
c = col
r = row
elif offset == "ur":
c = col + 1
r = row
elif offset == "ll":
c = col
r = row + 1
elif offset == "lr":
c = col + 1
r = row + 1
else:
raise ValueError(f"Invalid offset value: {offset}")

return transform * (c, r)


def has_geokeys(ifd: ImageFileDirectory) -> bool:
"""Check if an IFD has GeoTIFF keys.
Expand Down
4 changes: 3 additions & 1 deletion src/async_geotiff/_overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@

from affine import Affine

from async_geotiff._transform import TransformMixin

if TYPE_CHECKING:
from async_tiff import GeoKeyDirectory, ImageFileDirectory

from async_geotiff import GeoTIFF


@dataclass(init=False, frozen=True, kw_only=True, eq=False, repr=False)
class Overview:
class Overview(TransformMixin):
"""An overview level of a Cloud-Optimized GeoTIFF image."""

_geotiff: GeoTIFF
Expand Down
91 changes: 91 additions & 0 deletions src/async_geotiff/_transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Mixin class for coordinate transformation methods."""

from __future__ import annotations

from math import floor
from typing import TYPE_CHECKING, Literal, Protocol

if TYPE_CHECKING:
from collections.abc import Callable

from affine import Affine


class HasTransform(Protocol):
"""Protocol for objects that have an affine transform."""

@property
def transform(self) -> Affine: ...


class TransformMixin:
"""Mixin providing coordinate transformation methods.

Classes using this mixin must have a `transform` property that returns
an `Affine` transformation matrix.
"""

def index(
self: HasTransform,
x: float,
y: float,
op: Callable[[float], int] = floor,
) -> tuple[int, int]:
"""Get the (row, col) index of the pixel containing (x, y).

Args:
x: x value in coordinate reference system.
y: y value in coordinate reference system.
op: Function to convert fractional pixels to whole numbers
(floor, ceiling, round). Defaults to math.floor.

Returns:
(row index, col index)

"""
inv_transform = ~self.transform
# Affine * (x, y) returns tuple[float, float] for 2D coordinates
col_frac, row_frac = inv_transform * (x, y) # type: ignore[misc]

return (op(row_frac), op(col_frac))

def xy(
self: HasTransform,
row: int,
col: int,
offset: Literal["center", "ul", "ur", "ll", "lr"] = "center",
) -> tuple[float, float]:
"""Get the coordinates (x, y) of a pixel at (row, col).

The pixel's center is returned by default, but a corner can be returned
by setting `offset` to one of `"ul"`, `"ur"`, `"ll"`, `"lr"`.

Args:
row: Pixel row.
col: Pixel column.
offset: Determines if the returned coordinates are for the center of the
pixel or for a corner.

Returns:
(x, y) coordinates in the dataset's CRS.

"""
if offset == "center":
c = col + 0.5
r = row + 0.5
elif offset == "ul":
c = col
r = row
elif offset == "ur":
c = col + 1
r = row
elif offset == "ll":
c = col
r = row + 1
elif offset == "lr":
c = col + 1
r = row + 1
else:
raise ValueError(f"Invalid offset value: {offset}")

return self.transform * (c, r)
Loading