Skip to main content
`Ctrl`+`K`

# Source code for pystac.extensions.sar

    """Implements the :stac-ext:`Synthetic-Aperture Radar (SAR) Extension <sar>`."""

    from __future__ import annotations

    from collections.abc import Iterable
    from typing import (
        Any,
        Generic,
        Literal,
        TypeVar,
        cast,
    )

    import pystac
    from pystac.extensions.base import (
        ExtensionManagementMixin,
        PropertiesExtension,
        SummariesExtension,
    )
    from pystac.extensions.hooks import ExtensionHooks
    from pystac.serialization.identify import STACJSONDescription, STACVersionID
    from pystac.summaries import RangeSummary
    from pystac.utils import StringEnum, get_required, map_opt

    #: Generalized version of :class:`~pystac.Item`, :class:`~pystac.Asset` or
    #: :class:`~pystac.ItemAssetDefinition`
    T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition)

    SCHEMA_URI: str = "https://stac-extensions.github.io/sar/v1.0.0/schema.json"
    PREFIX: str = "sar:"

    # Required
    INSTRUMENT_MODE_PROP: str = PREFIX + "instrument_mode"
    FREQUENCY_BAND_PROP: str = PREFIX + "frequency_band"
    POLARIZATIONS_PROP: str = PREFIX + "polarizations"
    PRODUCT_TYPE_PROP: str = PREFIX + "product_type"

    # Not required
    CENTER_FREQUENCY_PROP: str = PREFIX + "center_frequency"
    RESOLUTION_RANGE_PROP: str = PREFIX + "resolution_range"
    RESOLUTION_AZIMUTH_PROP: str = PREFIX + "resolution_azimuth"
    PIXEL_SPACING_RANGE_PROP: str = PREFIX + "pixel_spacing_range"
    PIXEL_SPACING_AZIMUTH_PROP: str = PREFIX + "pixel_spacing_azimuth"
    LOOKS_RANGE_PROP: str = PREFIX + "looks_range"
    LOOKS_AZIMUTH_PROP: str = PREFIX + "looks_azimuth"
    LOOKS_EQUIVALENT_NUMBER_PROP: str = PREFIX + "looks_equivalent_number"
    OBSERVATION_DIRECTION_PROP: str = PREFIX + "observation_direction"

    class FrequencyBand(StringEnum):
        P = "P"
        L = "L"
        S = "S"
        C = "C"
        X = "X"
        KU = "Ku"
        K = "K"
        KA = "Ka"

    class Polarization(StringEnum):
        HH = "HH"
        VV = "VV"
        HV = "HV"
        VH = "VH"

    class ObservationDirection(StringEnum):
        LEFT = "left"
        RIGHT = "right"

    class SarExtension(
        Generic[T],
        PropertiesExtension,
        ExtensionManagementMixin[pystac.Item | pystac.Collection],
    ):
        """An abstract class that can be used to extend the properties of an
        :class:`~pystac.Item` or :class:`~pystac.Asset` with properties from the
        :stac-ext:`SAR Extension <sar>`. This class is generic over the type of
        STAC Object to be extended (e.g. :class:`~pystac.Item`,
        :class:`~pystac.Asset`).

        To create a concrete instance of :class:`SarExtension`, use the
        :meth:`SarExtension.ext` method. For example:

        .. code-block:: python

           >>> item: pystac.Item = ...
           >>> sar_ext = SarExtension.ext(item)
        """

        name: Literal["sar"] = "sar"

        def apply(
            self,
            instrument_mode: str,
            frequency_band: FrequencyBand,
            polarizations: list[Polarization],
            product_type: str,
            center_frequency: float | None = None,
            resolution_range: float | None = None,
            resolution_azimuth: float | None = None,
            pixel_spacing_range: float | None = None,
            pixel_spacing_azimuth: float | None = None,
            looks_range: int | None = None,
            looks_azimuth: int | None = None,
            looks_equivalent_number: float | None = None,
            observation_direction: ObservationDirection | None = None,
        ) -> None:
            """Applies sar extension properties to the extended Item.

            Args:
                instrument_mode : The name of the sensor acquisition mode that is
                    commonly used. This should be the short name, if available. For example,
                    WV for "Wave mode."
                frequency_band : The common name for the frequency band to
                    make it easier to search for bands across instruments. See section
                    "Common Frequency Band Names" for a list of accepted names.
                polarizations : Any combination of polarizations.
                product_type : The product type, for example SSC, MGD, or SGC.
                center_frequency : Optional center frequency of the instrument in
                    gigahertz (GHz).
                resolution_range : Optional range resolution, which is the maximum
                    ability to distinguish two adjacent targets perpendicular to the flight
                    path, in meters (m).
                resolution_azimuth : Optional azimuth resolution, which is the
                    maximum ability to distinguish two adjacent targets parallel to the
                    flight path, in meters (m).
                pixel_spacing_range : Optional range pixel spacing, which is the
                    distance between adjacent pixels perpendicular to the flight path,
                    in meters (m). Strongly RECOMMENDED to be specified for
                    products of type GRD.
                pixel_spacing_azimuth : Optional azimuth pixel spacing, which is the
                    distance between adjacent pixels parallel to the flight path, in
                    meters (m). Strongly RECOMMENDED to be specified for products of
                    type GRD.
                looks_range : Optional number of groups of signal samples (looks)
                    perpendicular to the flight path.
                looks_azimuth : Optional number of groups of signal samples (looks)
                    parallel to the flight path.
                looks_equivalent_number : Optional equivalent number of looks (ENL).
                observation_direction : Optional Antenna pointing
                    direction relative to the flight trajectory of the satellite.
            """
            self.instrument_mode = instrument_mode
            self.frequency_band = frequency_band
            self.polarizations = polarizations
            self.product_type = product_type
            if center_frequency:
                self.center_frequency = center_frequency
            if resolution_range:
                self.resolution_range = resolution_range
            if resolution_azimuth:
                self.resolution_azimuth = resolution_azimuth
            if pixel_spacing_range:
                self.pixel_spacing_range = pixel_spacing_range
            if pixel_spacing_azimuth:
                self.pixel_spacing_azimuth = pixel_spacing_azimuth
            if looks_range:
                self.looks_range = looks_range
            if looks_azimuth:
                self.looks_azimuth = looks_azimuth
            if looks_equivalent_number:
                self.looks_equivalent_number = looks_equivalent_number
            if observation_direction:
                self.observation_direction = observation_direction

        @property
        def instrument_mode(self) -> str:
            """Gets or sets an instrument mode string for the item."""
            return get_required(
                self._get_property(INSTRUMENT_MODE_PROP, str), self, INSTRUMENT_MODE_PROP
            )

        @instrument_mode.setter
        def instrument_mode(self, v: str) -> None:
            self._set_property(INSTRUMENT_MODE_PROP, v, pop_if_none=False)

        @property
        def frequency_band(self) -> FrequencyBand:
            """Gets or sets a FrequencyBand for the item."""
            return get_required(
                map_opt(
                    lambda x: FrequencyBand(x), self._get_property(FREQUENCY_BAND_PROP, str)
                ),
                self,
                FREQUENCY_BAND_PROP,
            )

        @frequency_band.setter
        def frequency_band(self, v: FrequencyBand) -> None:
            self._set_property(FREQUENCY_BAND_PROP, v.value, pop_if_none=False)

        @property
        def polarizations(self) -> list[Polarization]:
            """Gets or sets a list of polarizations for the item."""
            return get_required(
                map_opt(
                    lambda values: [Polarization(v) for v in values],
                    self._get_property(POLARIZATIONS_PROP, list[str]),
                ),
                self,
                POLARIZATIONS_PROP,
            )

        @polarizations.setter
        def polarizations(self, values: list[Polarization]) -> None:
            if not isinstance(values, list):
                raise pystac.STACError(f'polarizations must be a list. Invalid "{values}"')
            self._set_property(
                POLARIZATIONS_PROP, [v.value for v in values], pop_if_none=False
            )

        @property
        def product_type(self) -> str:
            """Gets or sets a product type string for the item."""
            return get_required(
                self._get_property(PRODUCT_TYPE_PROP, str), self, PRODUCT_TYPE_PROP
            )

        @product_type.setter
        def product_type(self, v: str) -> None:
            self._set_property(PRODUCT_TYPE_PROP, v, pop_if_none=False)

        @property
        def center_frequency(self) -> float | None:
            """Gets or sets a center frequency for the item."""
            return self._get_property(CENTER_FREQUENCY_PROP, float)

        @center_frequency.setter
        def center_frequency(self, v: float | None) -> None:
            self._set_property(CENTER_FREQUENCY_PROP, v)

        @property
        def resolution_range(self) -> float | None:
            """Gets or sets a resolution range for the item."""
            return self._get_property(RESOLUTION_RANGE_PROP, float)

        @resolution_range.setter
        def resolution_range(self, v: float | None) -> None:
            self._set_property(RESOLUTION_RANGE_PROP, v)

        @property
        def resolution_azimuth(self) -> float | None:
            """Gets or sets a resolution azimuth for the item."""
            return self._get_property(RESOLUTION_AZIMUTH_PROP, float)

        @resolution_azimuth.setter
        def resolution_azimuth(self, v: float | None) -> None:
            self._set_property(RESOLUTION_AZIMUTH_PROP, v)

        @property
        def pixel_spacing_range(self) -> float | None:
            """Gets or sets a pixel spacing range for the item."""
            return self._get_property(PIXEL_SPACING_RANGE_PROP, float)

        @pixel_spacing_range.setter
        def pixel_spacing_range(self, v: float | None) -> None:
            self._set_property(PIXEL_SPACING_RANGE_PROP, v)

        @property
        def pixel_spacing_azimuth(self) -> float | None:
            """Gets or sets a pixel spacing azimuth for the item."""
            return self._get_property(PIXEL_SPACING_AZIMUTH_PROP, float)

        @pixel_spacing_azimuth.setter
        def pixel_spacing_azimuth(self, v: float | None) -> None:
            self._set_property(PIXEL_SPACING_AZIMUTH_PROP, v)

        @property
        def looks_range(self) -> int | None:
            """Gets or sets a looks range for the item."""
            return self._get_property(LOOKS_RANGE_PROP, int)

        @looks_range.setter
        def looks_range(self, v: int | None) -> None:
            self._set_property(LOOKS_RANGE_PROP, v)

        @property
        def looks_azimuth(self) -> int | None:
            """Gets or sets a looks azimuth for the item."""
            return self._get_property(LOOKS_AZIMUTH_PROP, int)

        @looks_azimuth.setter
        def looks_azimuth(self, v: int | None) -> None:
            self._set_property(LOOKS_AZIMUTH_PROP, v)

        @property
        def looks_equivalent_number(self) -> float | None:
            """Gets or sets a looks equivalent number for the item."""
            return self._get_property(LOOKS_EQUIVALENT_NUMBER_PROP, float)

        @looks_equivalent_number.setter
        def looks_equivalent_number(self, v: float | None) -> None:
            self._set_property(LOOKS_EQUIVALENT_NUMBER_PROP, v)

        @property
        def observation_direction(self) -> ObservationDirection | None:
            """Gets or sets an observation direction for the item."""
            return map_opt(
                ObservationDirection, self._get_property(OBSERVATION_DIRECTION_PROP, str)
            )

        @observation_direction.setter
        def observation_direction(self, v: ObservationDirection | None) -> None:
            self._set_property(OBSERVATION_DIRECTION_PROP, map_opt(lambda x: x.value, v))

        @classmethod
        def get_schema_uri(cls) -> str:
            return SCHEMA_URI

        @classmethod
        def ext(cls, obj: T, add_if_missing: bool = False) -> SarExtension[T]:
            """Extends the given STAC Object with properties from the :stac-ext:`SAR
            Extension <sar>`.

            This extension can be applied to instances of :class:`~pystac.Item` or
            :class:`~pystac.Asset`.

            Raises:

                pystac.ExtensionTypeError : If an invalid object type is passed.
            """
            if isinstance(obj, pystac.Item):
                cls.ensure_has_extension(obj, add_if_missing)
                return cast(SarExtension[T], ItemSarExtension(obj))
            elif isinstance(obj, pystac.Asset):
                if obj.owner is not None and not isinstance(obj.owner, pystac.Item):
                    raise pystac.ExtensionTypeError(
                        "SAR extension does not apply to Collection Assets."
                    )
                cls.ensure_owner_has_extension(obj, add_if_missing)
                return cast(SarExtension[T], AssetSarExtension(obj))
            elif isinstance(obj, pystac.ItemAssetDefinition):
                cls.ensure_owner_has_extension(obj, add_if_missing)
                return cast(SarExtension[T], ItemAssetsSarExtension(obj))
            else:
                raise pystac.ExtensionTypeError(cls._ext_error_message(obj))

        @classmethod
        def summaries(
            cls, obj: pystac.Collection, add_if_missing: bool = False
        ) -> SummariesSarExtension:
            """Returns the extended summaries object for the given collection."""
            cls.ensure_has_extension(obj, add_if_missing)
            return SummariesSarExtension(obj)

    class ItemSarExtension(SarExtension[pystac.Item]):
        """A concrete implementation of :class:`SarExtension` on an :class:`~pystac.Item`
        that extends the properties of the Item to include properties defined in the
        :stac-ext:`SAR Extension <sar>`.

        This class should generally not be instantiated directly. Instead, call
        :meth:`SarExtension.ext` on an :class:`~pystac.Item` to extend it.
        """

        item: pystac.Item
        """The :class:`~pystac.Item` being extended."""

        properties: dict[str, Any]
        """The :class:`~pystac.Item` properties, including extension properties."""

        def __init__(self, item: pystac.Item):
            self.item = item
            self.properties = item.properties

        def __repr__(self) -> str:
            return f"<ItemSarExtension Item id={self.item.id}>"

    class AssetSarExtension(SarExtension[pystac.Asset]):
        """A concrete implementation of :class:`SarExtension` on an :class:`~pystac.Asset`
        that extends the Asset fields to include properties defined in the
        :stac-ext:`SAR Extension <sar>`.

        This class should generally not be instantiated directly. Instead, call
        :meth:`SarExtension.ext` on an :class:`~pystac.Asset` to extend it.
        """

        asset_href: str
        """The ``href`` value of the :class:`~pystac.Asset` being extended."""

        properties: dict[str, Any]
        """The :class:`~pystac.Asset` fields, including extension properties."""

        additional_read_properties: Iterable[dict[str, Any]] | None = None
        """If present, this will be a list containing 1 dictionary representing the
        properties of the owning :class:`~pystac.Item`."""

        def __init__(self, asset: pystac.Asset):
            self.asset_href = asset.href
            self.properties = asset.extra_fields
            if asset.owner and isinstance(asset.owner, pystac.Item):
                self.additional_read_properties = [asset.owner.properties]

        def __repr__(self) -> str:
            return f"<AssetSarExtension Asset href={self.asset_href}>"

    class ItemAssetsSarExtension(SarExtension[pystac.ItemAssetDefinition]):
        properties: dict[str, Any]
        asset_defn: pystac.ItemAssetDefinition

        def __init__(self, item_asset: pystac.ItemAssetDefinition):
            self.asset_defn = item_asset
            self.properties = item_asset.properties

    class SummariesSarExtension(SummariesExtension):
        """A concrete implementation of :class:`~pystac.extensions.base.SummariesExtension`
        that extends the ``summaries`` field of a :class:`~pystac.Collection` to include
        properties defined in the :stac-ext:`SAR Extension <sar>`.
        """

        @property
        def instrument_mode(self) -> list[str] | None:
            """Get or sets the summary of :attr:`SarExtension.instrument_mode` values
            for this Collection.
            """

            return self.summaries.get_list(INSTRUMENT_MODE_PROP)

        @instrument_mode.setter
        def instrument_mode(self, v: list[str] | None) -> None:
            self._set_summary(INSTRUMENT_MODE_PROP, v)

        @property
        def frequency_band(self) -> list[FrequencyBand] | None:
            """Get or sets the summary of :attr:`SarExtension.frequency_band` values
            for this Collection.
            """

            return self.summaries.get_list(FREQUENCY_BAND_PROP)

        @frequency_band.setter
        def frequency_band(self, v: list[FrequencyBand] | None) -> None:
            self._set_summary(FREQUENCY_BAND_PROP, v)

        @property
        def center_frequency(self) -> RangeSummary[float] | None:
            """Get or sets the summary of :attr:`SarExtension.center_frequency` values
            for this Collection.
            """

            return self.summaries.get_range(CENTER_FREQUENCY_PROP)

        @center_frequency.setter
        def center_frequency(self, v: RangeSummary[float] | None) -> None:
            self._set_summary(CENTER_FREQUENCY_PROP, v)

        @property
        def polarizations(self) -> list[Polarization] | None:
            """Get or sets the summary of :attr:`SarExtension.polarizations` values
            for this Collection.
            """

            return self.summaries.get_list(POLARIZATIONS_PROP)

        @polarizations.setter
        def polarizations(self, v: list[Polarization] | None) -> None:
            self._set_summary(POLARIZATIONS_PROP, v)

        @property
        def product_type(self) -> list[str] | None:
            """Get or sets the summary of :attr:`SarExtension.product_type` values
            for this Collection.
            """

            return self.summaries.get_list(PRODUCT_TYPE_PROP)

        @product_type.setter
        def product_type(self, v: list[str] | None) -> None:
            self._set_summary(PRODUCT_TYPE_PROP, v)

        @property
        def resolution_range(self) -> RangeSummary[float] | None:
            """Get or sets the summary of :attr:`SarExtension.resolution_range` values
            for this Collection.
            """

            return self.summaries.get_range(RESOLUTION_RANGE_PROP)

        @resolution_range.setter
        def resolution_range(self, v: RangeSummary[float] | None) -> None:
            self._set_summary(RESOLUTION_RANGE_PROP, v)

        @property
        def resolution_azimuth(self) -> RangeSummary[float] | None:
            """Get or sets the summary of :attr:`SarExtension.resolution_azimuth` values
            for this Collection.
            """

            return self.summaries.get_range(RESOLUTION_AZIMUTH_PROP)

        @resolution_azimuth.setter
        def resolution_azimuth(self, v: RangeSummary[float] | None) -> None:
            self._set_summary(RESOLUTION_AZIMUTH_PROP, v)

        @property
        def pixel_spacing_range(self) -> RangeSummary[float] | None:
            """Get or sets the summary of :attr:`SarExtension.pixel_spacing_range` values
            for this Collection.
            """

            return self.summaries.get_range(PIXEL_SPACING_RANGE_PROP)

        @pixel_spacing_range.setter
        def pixel_spacing_range(self, v: RangeSummary[float] | None) -> None:
            self._set_summary(PIXEL_SPACING_RANGE_PROP, v)

        @property
        def pixel_spacing_azimuth(self) -> RangeSummary[float] | None:
            """Get or sets the summary of :attr:`SarExtension.pixel_spacing_azimuth` values
            for this Collection.
            """

            return self.summaries.get_range(PIXEL_SPACING_AZIMUTH_PROP)

        @pixel_spacing_azimuth.setter
        def pixel_spacing_azimuth(self, v: RangeSummary[float] | None) -> None:
            self._set_summary(PIXEL_SPACING_AZIMUTH_PROP, v)

        @property
        def looks_range(self) -> RangeSummary[int] | None:
            """Get or sets the summary of :attr:`SarExtension.looks_range` values
            for this Collection.
            """

            return self.summaries.get_range(LOOKS_RANGE_PROP)

        @looks_range.setter
        def looks_range(self, v: RangeSummary[int] | None) -> None:
            self._set_summary(LOOKS_RANGE_PROP, v)

        @property
        def looks_azimuth(self) -> RangeSummary[int] | None:
            """Get or sets the summary of :attr:`SarExtension.looks_azimuth` values
            for this Collection.
            """

            return self.summaries.get_range(LOOKS_AZIMUTH_PROP)

        @looks_azimuth.setter
        def looks_azimuth(self, v: RangeSummary[int] | None) -> None:
            self._set_summary(LOOKS_AZIMUTH_PROP, v)

        @property
        def looks_equivalent_number(self) -> RangeSummary[float] | None:
            """Get or sets the summary of :attr:`SarExtension.looks_equivalent_number`
            values for this Collection.
            """

            return self.summaries.get_range(LOOKS_EQUIVALENT_NUMBER_PROP)

        @looks_equivalent_number.setter
        def looks_equivalent_number(self, v: RangeSummary[float] | None) -> None:
            self._set_summary(LOOKS_EQUIVALENT_NUMBER_PROP, v)

        @property
        def observation_direction(self) -> list[ObservationDirection] | None:
            """Get or sets the summary of :attr:`SarExtension.observation_direction` values
            for this Collection.
            """

            return self.summaries.get_list(OBSERVATION_DIRECTION_PROP)

        @observation_direction.setter
        def observation_direction(self, v: list[ObservationDirection] | None) -> None:
            self._set_summary(OBSERVATION_DIRECTION_PROP, v)

    class SarExtensionHooks(ExtensionHooks):
        schema_uri = SCHEMA_URI
        prev_extension_ids = {"sar"}
        stac_object_types = {pystac.STACObjectType.ITEM}

        def migrate(
            self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription
        ) -> None:
            if version < "0.9":
                # Some sar fields became common_metadata
                if (
                    PREFIX + "platform" in obj["properties"]
                    and "platform" not in obj["properties"]
                ):
                    obj["properties"]["platform"] = obj["properties"].pop(
                        PREFIX + "platform"
                    )

                if (
                    PREFIX + "instrument" in obj["properties"]
                    and "instruments" not in obj["properties"]
                ):
                    obj["properties"]["instruments"] = [
                        obj["properties"].pop(PREFIX + "instrument")
                    ]

                if (
                    PREFIX + "constellation" in obj["properties"]
                    and "constellation" not in obj["properties"]
                ):
                    obj["properties"]["constellation"] = obj["properties"].pop(
                        PREFIX + "constellation"
                    )

                # Some SAR fields changed property names
                if (
                    PREFIX + "type" in obj["properties"]
                    and PRODUCT_TYPE_PROP not in obj["properties"]
                ):
                    obj["properties"][PRODUCT_TYPE_PROP] = obj["properties"].pop(
                        PREFIX + "type"
                    )

                if (
                    PREFIX + "polarization" in obj["properties"]
                    and POLARIZATIONS_PROP not in obj["properties"]
                ):
                    obj["properties"][POLARIZATIONS_PROP] = [
                        obj["properties"].pop(PREFIX + "polarization")
                    ]

            super().migrate(obj, version, info)

    SAR_EXTENSION_HOOKS: ExtensionHooks = SarExtensionHooks()