Source code for odc.geo.types

"""Basic types."""

from enum import Enum
from typing import (
    Callable,
    Generic,
    Iterable,
    Iterator,
    List,
    Literal,
    Mapping,
    Optional,
    Protocol,
    Sequence,
    Tuple,
    TypeVar,
    Union,
    cast,
    overload,
)

MaybeInt = Optional[int]
MaybeFloat = Optional[float]
Nodata = Union[float, int, str]
MaybeNodata = Optional[Nodata]
T = TypeVar("T")
T1 = TypeVar("T1")
T2 = TypeVar("T2")
TK = TypeVar("TK")
TV = TypeVar("TV")


class Unset:
    """
    Marker for unset values.

    Used where ``None`` can be a valid value.
    """

    __slots__ = ()


[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") def map(self, op: Callable[[T], T2]) -> "XY[T2]": """ Apply function to x and y and return new XY value. """ return xy_(op(self.x), op(self.y)) @property def aspect(self) -> float: """Aspect ratio (X/Y).""" return float(self.x) / float(self.y) # type: ignore
[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 def shrink2(self) -> "Shape2d": return Shape2d(x=self._xy[0] // 2, y=self._xy[1] // 2)
SomeShape = Union[Tuple[int, int], XY[int], Shape2d, Index2d] SomeIndex2d = Union[Tuple[int, int], XY[int], Index2d] SomeResolution = Union[float, int, Resolution] Chunks2d = Tuple[Tuple[int, ...], Tuple[int, ...]] class SupportsCoords(Protocol[T]): """ Needed for Point geometry -> XY conversion. """ @property def coords(self) -> List[Tuple[T, T]]: ... # fmt: off @overload def xy_(x: T, y: T, /) -> XY[T]: ... @overload def xy_(x: XY[T], y: Literal[None] = None, /) -> XY[T]: ... @overload def xy_(x: SupportsCoords[T], y: Literal[None] = None, /) -> XY[T]: ... @overload def xy_(x: Iterable[T], y: Literal[None] = None, /) -> XY[T]: ... # fmt: on
[docs] def xy_( x: Union[T, XY[T], SupportsCoords[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 (coords := getattr(x, "coords", None)) is not None: ((x, y),) = cast(List[Tuple[T, T]], coords) return XY(x=x, y=y) 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(f"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: Union[SomeShape, Tuple[int, ...]]) -> Shape2d: """Normalise shape representation.""" if isinstance(x, Shape2d): return x if isinstance(x, XY): nx, ny = x.map(int).xy return Shape2d(x=nx, y=ny) if isinstance(x, Sequence): ny, nx = map(int, x) return Shape2d(x=nx, y=ny) raise ValueError(f"Input type not understood: {type(x)}")
# fmt: off class NormalizedSlice(Protocol): """ Type for ``slice`` with start/stop set to integer values. """ @property def start(self) -> int: ... @property def stop(self) -> int: ... @property def step(self) -> Optional[int]: ... # fmt: on SomeSlice = Union[slice, int, NormalizedSlice] """ Slice index into ndarray or a single int. Single index is equivalent to ``slice(idx, idx+1)``. """ NdROI = Union[SomeSlice, Tuple[SomeSlice, ...]] """ Any dimensional slice into ndarray. This could be a single ``int`` or slice ``slice`` or a tuple of any number of those things. """ ROI = Tuple[SomeSlice, SomeSlice] """2d slice into an image plane.""" NormalizedROI = Tuple[NormalizedSlice, NormalizedSlice] """Normalized 2d slice into an image plane.""" OutlineMode = Union[ Literal["native"], Literal["pixel"], Literal["geo"], Literal["auto"] ]
[docs] class AnchorEnum(Enum): """ Defines which way to snap geobox pixel grid. """ EDGE = 0 """Snap pixel edges to multiples of pixel size.""" CENTER = 1 """Snap pixel centers to multiples of pixel size.""" FLOATING = 2 """Turn off pixel snapping."""
class _Func2Map(Mapping[TK, TV]): """ Turn ``f(K)`` into ``ff[K]``. """ __slots__ = ("_func", "_keys") def __init__(self, func: Callable[[TK], TV], keys: Sequence[TK]): self._func = func self._keys = keys def __getitem__(self, idx: TK) -> TV: return self._func(idx) def __len__(self) -> int: return len(self._keys) def __iter__(self) -> Iterator[TK]: yield from self._keys def func2map( f: Callable[[TK], TV], keys: Optional[Sequence[TK]] = None, ) -> Mapping[TK, TV]: """ Turn ``f(K) -> V`` into ``ff[K] -> V``. :param f: Callable mapping ``K -> V`` :param keys: Optional sequence of valid keys """ return _Func2Map(f, [] if keys is None else keys)