Source code for odc.geo.types

"""Basic types."""
from typing import (
    Generic,
    Iterable,
    Iterator,
    Literal,
    Optional,
    Sequence,
    Tuple,
    TypeVar,
    Union,
    cast,
    overload,
)

MaybeInt = Optional[int]
MaybeFloat = Optional[float]
T = TypeVar("T")


[docs]class XY(Generic[T]): """ Immutable container for anything X/Y. This class is used as a replacement for a plain tuple of two values that could be in X/Y or Y/X order. :param x: Value of type ``T`` for x :param y: Value of type ``T`` for y """ __slots__ = ("_xy",)
[docs] def __init__(self, x: T, y: T) -> None: self._xy = x, y
def __eq__(self, other) -> bool: if not isinstance(other, XY): return False return self._xy == other._xy def __str__(self) -> str: return f"XY(x={self._xy[0]}, y={self._xy[1]})" def __repr__(self) -> str: return f"XY(x={self._xy[0]}, y={self._xy[1]})" def __hash__(self) -> int: return hash(self._xy) @property def x(self) -> T: """Access X value.""" return self._xy[0] @property def y(self) -> T: """Access Y value.""" return self._xy[1] @property def xy(self) -> Tuple[T, T]: """Convert to tuple in X,Y order.""" return self._xy @property def yx(self) -> Tuple[T, T]: """Convert to tuple in Y,X order.""" return self._xy[1], self._xy[0] @property def lon(self) -> T: """Access Longitude value (X).""" return self._xy[0] @property def lat(self) -> T: """Access Latitude value (Y).""" return self._xy[1] @property def lonlat(self) -> Tuple[T, T]: """Convert to tuple in Longitude,Latitude order.""" return self._xy @property def latlon(self) -> Tuple[T, T]: """Convert to tuple in Latitude,Longitude order.""" return self._xy[1], self._xy[0] @property def shape(self) -> Tuple[int, int]: """ Interpret as ``shape`` (Y, X) order. Only valid for ``XY[int]`` case. :raises ValueError: when tuple contains anything but integer values :return: ``(y, x)`` """ x, y = self._xy if isinstance(x, int) and isinstance(y, int): return y, x raise ValueError("Expect (int, int) for shape") @property def wh(self) -> Tuple[int, int]: """ Interpret as ``width, height``, (X, Y) order. Only valid for ``XY[int]`` case. :raises ValueError: when tuple contains anything but integer values :return: ``(x, y)`` """ x, y = self._xy if isinstance(x, int) and isinstance(y, int): return (x, y) raise ValueError("Expect (int, int) for wh")
[docs]class Resolution(XY[float]): """ Resolution for X/Y dimensions. """
[docs] def __init__(self, x: float, y: Optional[float] = None) -> None: if y is None: y = -x super().__init__(float(x), float(y))
def __repr__(self) -> str: return f"Resolution(x={self.x:g}, y={self.y:g})" def __str__(self) -> str: return f"Resolution(x={self.x:g}, y={self.y:g})"
[docs]class Index2d(XY[int]): """ 2d index. """
[docs] def __init__(self, x: int, y: int) -> None: super().__init__(x, y)
def __repr__(self) -> str: return f"Index2d(x={self.x}, y={self.y})" def __str__(self) -> str: return f"Index2d(x={self.x}, y={self.y})"
[docs]class Shape2d(XY[int], Sequence[int]): """ 2d shape. Unlike other XY types, Shape2d does have canonical order: ``Y,X``. This class implements Mapping interfaces, so it can be used as input into ``numpy`` functions that accept shape parameter. It can also be compared directly to a tuple form. It can be concatenated with a tuple. """
[docs] def __init__(self, x: int, y: int) -> None: super().__init__(x, y)
def __repr__(self) -> str: return f"Shape2d(x={self.x}, y={self.y})" def __str__(self) -> str: return f"Shape2d(x={self.x}, y={self.y})" def __len__(self) -> int: return 2 def __iter__(self) -> Iterator[int]: yield from self.shape def __getitem__(self, idx): return self.shape[idx] def __eq__(self, other) -> bool: if isinstance(other, tuple): return self.shape == other return super().__eq__(other) def __add__(self, other): return self.shape.__add__(other) def __radd__(self, other): return other + self.shape
SomeShape = Union[Tuple[int, int], XY[int], Shape2d, Index2d] SomeIndex2d = Union[Tuple[int, int], XY[int], Index2d] SomeResolution = Union[float, int, Resolution] # fmt: off @overload def xy_(x: T, y: T, /) -> XY[T]: ... @overload def xy_(x: Iterable[T], y: Literal[None] = None, /) -> XY[T]: ... @overload def xy_(x: XY[T], y: Literal[None] = None, /) -> XY[T]: ... # fmt: on
[docs]def xy_(x: Union[T, XY[T], Iterable[T]], y: Optional[T] = None) -> XY[T]: """ Construct from X,Y order. .. code-block:: python xy_(0, 1) xy_([0, 1]) xy_(tuple([0, 1])) assert xy_(1, 3).x == 1 assert xy_(1, 3).y == 3 """ if y is not None: return XY(x=cast(T, x), y=y) if isinstance(x, XY): return x if not isinstance(x, Iterable): raise ValueError("Expect 2 arguments or a single iterable.") x, y = cast(Iterable[T], x) return XY(x=x, y=y)
# fmt: off @overload def yx_(y: T, x: T, /) -> XY[T]: ... @overload def yx_(y: Iterable[T], x: Literal[None] = None, /) -> XY[T]: ... @overload def yx_(y: XY[T], x: Literal[None] = None, /) -> XY[T]: ... # fmt: on
[docs]def yx_(y: Union[T, XY[T], Iterable[T]], x: Optional[T] = None, /) -> XY[T]: """ Construct from Y,X order. .. code-block:: python yx_(0, 1) yx_([0, 1]) yx_(tuple([0, 1])) assert yx_(1, 3).x == 3 assert yx_(1, 3).y == 1 """ if x is not None: return XY(x=x, y=cast(T, y)) if isinstance(y, XY): return y if not isinstance(y, Iterable): raise ValueError("Expect 2 arguments or a single iterable.") y, x = cast(Iterable[T], y) return XY(x=x, y=y)
[docs]def res_(x: Union[Resolution, float, int], /) -> Resolution: """Resolution for square pixels with inverted Y axis.""" if isinstance(x, Resolution): return x if isinstance(x, (int, float)): return Resolution(float(x)) raise ValueError("Unsupported input type: res_(x: {type(x)})")
[docs]def resxy_(x: float, y: float, /) -> Resolution: """Construct resolution from X,Y order.""" return Resolution(x=x, y=y)
[docs]def resyx_(y: float, x: float, /) -> Resolution: """Construct resolution from Y,X order.""" return Resolution(x=x, y=y)
# fmt: off @overload def ixy_(x: int, y: int, /) -> Index2d: ... @overload def ixy_(x: Tuple[int, int], y: Literal[None] = None, /) -> Index2d: ... @overload def ixy_(x: Index2d, y: Literal[None] = None, /) -> Index2d: ... @overload def ixy_(x: XY[int], y: Literal[None] = None, /) -> Index2d: ... # fmt: on
[docs]def ixy_( x: Union[int, Tuple[int, int], XY[int], Index2d], y: Optional[int] = None, / ) -> Index2d: """Construct 2d index in X,Y order.""" if y is not None: assert isinstance(x, int) return Index2d(x=x, y=y) if isinstance(x, tuple): x, y = x return Index2d(x=x, y=y) if isinstance(x, Index2d): return x if isinstance(x, XY): x, y = x.xy return Index2d(x=x, y=y) raise ValueError("Expect 2 values or a single tuple/XY/Index2d object")
# fmt: off @overload def iyx_(y: int, x: int, /) -> Index2d: ... @overload def iyx_(y: Tuple[int, int], x: Literal[None] = None, /) -> Index2d: ... @overload def iyx_(y: Index2d, x: Literal[None] = None, /) -> Index2d: ... @overload def iyx_(y: XY[int], x: Literal[None] = None, /) -> Index2d: ... # fmt: on
[docs]def iyx_( y: Union[int, Tuple[int, int], XY[int], Index2d], x: Optional[int] = None, / ) -> Index2d: """Construct 2d index in Y,X order.""" if x is not None: assert isinstance(y, int) return Index2d(x=x, y=y) if isinstance(y, tuple): y, x = y return Index2d(x=x, y=y) if isinstance(y, Index2d): return y if isinstance(y, XY): y, x = y.yx return Index2d(x=x, y=y) raise ValueError("Expect 2 values or a single tuple/XY/Index2d object")
[docs]def wh_(w: int, h: int, /) -> Shape2d: """Shape from width/height.""" return Shape2d(x=w, y=h)
[docs]def shape_(x: SomeShape) -> Shape2d: """Normalise shape representation.""" if isinstance(x, Shape2d): return x if isinstance(x, XY): nx, ny = x.xy return Shape2d(x=nx, y=ny) if isinstance(x, Sequence): ny, nx = x return Shape2d(x=nx, y=ny) raise ValueError(f"Input type not understood: {type(x)}")