"""Implements the :stac-ext:`Point Cloud Extension <pointcloud>`."""
from __future__ import annotations
from collections.abc import Iterable
from typing import (
Any,
Generic,
Literal,
TypeVar,
Union,
cast,
)
import pystac
from pystac.extensions import item_assets
from pystac.extensions.base import (
ExtensionManagementMixin,
PropertiesExtension,
SummariesExtension,
)
from pystac.extensions.hooks import ExtensionHooks
from pystac.summaries import RangeSummary
from pystac.utils import StringEnum, get_required, map_opt
T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition)
SCHEMA_URI: str = "https://stac-extensions.github.io/pointcloud/v1.0.0/schema.json"
PREFIX: str = "pc:"
COUNT_PROP = PREFIX + "count"
TYPE_PROP = PREFIX + "type"
ENCODING_PROP = PREFIX + "encoding"
SCHEMAS_PROP = PREFIX + "schemas"
DENSITY_PROP = PREFIX + "density"
STATISTICS_PROP = PREFIX + "statistics"
[docs]
class PhenomenologyType(StringEnum):
"""Valid values for the ``pc:type`` field in the :stac-ext:`Pointcloud Item
Properties <pointcloud#item-properties>`."""
LIDAR = "lidar"
EOPC = "eopc"
RADAR = "radar"
SONAR = "sonar"
OTHER = "other"
[docs]
class SchemaType(StringEnum):
"""Valid values for the ``type`` field in a :stac-ext:`Schema Object
<pointcloud#schema-object>`."""
FLOATING = "floating"
UNSIGNED = "unsigned"
SIGNED = "signed"
[docs]
class Schema:
"""Defines a schema for dimension of a pointcloud (e.g., name, size, type)
Use :meth:`Schema.create` to create a new instance of ``Schema`` from
properties.
"""
properties: dict[str, Any]
def __init__(self, properties: dict[str, Any]) -> None:
self.properties = properties
[docs]
def apply(self, name: str, size: int, type: SchemaType) -> None:
"""Sets the properties for this Schema.
Args:
name : The name of dimension.
size : The size of the dimension in bytes. Whole bytes are supported.
type : Dimension type. Valid values are ``floating``, ``unsigned``, and
``signed``
"""
self.properties["name"] = name
self.properties["size"] = size
self.properties["type"] = type
[docs]
@classmethod
def create(cls, name: str, size: int, type: SchemaType) -> Schema:
"""Creates a new Schema.
Args:
name : The name of dimension.
size : The size of the dimension in bytes. Whole bytes are supported.
type : Dimension type. Valid values are ``floating``, ``unsigned``, and
``signed``
"""
c = cls({})
c.apply(name=name, size=size, type=type)
return c
@property
def size(self) -> int:
"""Gets or sets the size value."""
return get_required(self.properties.get("size"), self, "size")
@size.setter
def size(self, v: int) -> None:
if not isinstance(v, int):
raise pystac.STACError(f"size must be an int! Invalid input: {v}")
self.properties["size"] = v
@property
def name(self) -> str:
"""Gets or sets the name property for this Schema."""
return get_required(self.properties.get("name"), self, "name")
@name.setter
def name(self, v: str) -> None:
self.properties["name"] = v
@property
def type(self) -> SchemaType:
"""Gets or sets the type property. Valid values are ``floating``, ``unsigned``,
and ``signed``."""
return get_required(self.properties.get("type"), self, "type")
@type.setter
def type(self, v: SchemaType) -> None:
self.properties["type"] = v
def __repr__(self) -> str:
return "<Schema name={} size={} type={}>".format(
self.properties.get("name"),
self.properties.get("size"),
self.properties.get("type"),
)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Returns this schema as a dictionary."""
return self.properties
[docs]
class Statistic:
"""Defines a single statistic for Pointcloud channel or dimension
Use :meth:`Statistic.create` to create a new instance of
``Statistic`` from property values."""
properties: dict[str, Any]
def __init__(self, properties: dict[str, Any]) -> None:
self.properties = properties
[docs]
def apply(
self,
name: str,
position: int | None = None,
average: float | None = None,
count: int | None = None,
maximum: float | None = None,
minimum: float | None = None,
stddev: float | None = None,
variance: float | None = None,
) -> None:
"""Sets the properties for this Statistic.
Args:
name : REQUIRED. The name of the channel.
position : Optional position of the channel in the schema.
average : Optional average of the channel.
count : Optional number of elements in the channel.
maximum : Optional maximum value of the channel.
minimum : Optional minimum value of the channel.
stddev : Optional standard deviation of the channel.
variance : Optional variance of the channel.
"""
self.properties["name"] = name
self.properties["position"] = position
self.properties["average"] = average
self.properties["count"] = count
self.properties["maximum"] = maximum
self.properties["minimum"] = minimum
self.properties["stddev"] = stddev
self.properties["variance"] = variance
[docs]
@classmethod
def create(
cls,
name: str,
position: int | None = None,
average: float | None = None,
count: int | None = None,
maximum: float | None = None,
minimum: float | None = None,
stddev: float | None = None,
variance: float | None = None,
) -> Statistic:
"""Creates a new Statistic class.
Args:
name : REQUIRED. The name of the channel.
position : Optional position of the channel in the schema.
average : Optional average of the channel.
count : Optional number of elements in the channel.
maximum : Optional maximum value of the channel.
minimum : Optional minimum value of the channel.
stddev : Optional standard deviation of the channel.
variance : Optional variance of the channel.
"""
c = cls({})
c.apply(
name=name,
position=position,
average=average,
count=count,
maximum=maximum,
minimum=minimum,
stddev=stddev,
variance=variance,
)
return c
@property
def name(self) -> str:
"""Gets or sets the name property."""
return get_required(self.properties.get("name"), self, "name")
@name.setter
def name(self, v: str) -> None:
if v is not None:
self.properties["name"] = v
else:
self.properties.pop("name", None)
@property
def position(self) -> int | None:
"""Gets or sets the position property."""
return self.properties.get("position")
@position.setter
def position(self, v: int | None) -> None:
if v is not None:
self.properties["position"] = v
else:
self.properties.pop("position", None)
@property
def average(self) -> float | None:
"""Gets or sets the average property."""
return self.properties.get("average")
@average.setter
def average(self, v: float | None) -> None:
if v is not None:
self.properties["average"] = v
else:
self.properties.pop("average", None)
@property
def count(self) -> int | None:
"""Gets or sets the count property."""
return self.properties.get("count")
@count.setter
def count(self, v: int | None) -> None:
if v is not None:
self.properties["count"] = v
else:
self.properties.pop("count", None)
@property
def maximum(self) -> float | None:
"""Gets or sets the maximum property."""
return self.properties.get("maximum")
@maximum.setter
def maximum(self, v: float | None) -> None:
if v is not None:
self.properties["maximum"] = v
else:
self.properties.pop("maximum", None)
@property
def minimum(self) -> float | None:
"""Gets or sets the minimum property."""
return self.properties.get("minimum")
@minimum.setter
def minimum(self, v: float | None) -> None:
if v is not None:
self.properties["minimum"] = v
else:
self.properties.pop("minimum", None)
@property
def stddev(self) -> float | None:
"""Gets or sets the stddev property."""
return self.properties.get("stddev")
@stddev.setter
def stddev(self, v: float | None) -> None:
if v is not None:
self.properties["stddev"] = v
else:
self.properties.pop("stddev", None)
@property
def variance(self) -> float | None:
"""Gets or sets the variance property."""
return self.properties.get("variance")
@variance.setter
def variance(self, v: float | None) -> None:
if v is not None:
self.properties["variance"] = v
else:
self.properties.pop("variance", None)
def __repr__(self) -> str:
return f"<Statistic statistics={str(self.properties)}>"
[docs]
def to_dict(self) -> dict[str, Any]:
"""Returns this statistic as a dictionary."""
return self.properties
def __eq__(self, o: object) -> bool:
if not isinstance(o, Statistic):
return NotImplemented
return self.to_dict() == o.to_dict()
[docs]
class PointcloudExtension(
Generic[T],
PropertiesExtension,
ExtensionManagementMixin[Union[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:`Point Cloud Extension <pointcloud>`. 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:`PointcloudExtension`, use the
:meth:`PointcloudExtension.ext` method. For example:
.. code-block:: python
>>> item: pystac.Item = ...
>>> pc_ext = PointcloudExtension.ext(item)
"""
name: Literal["pc"] = "pc"
[docs]
def apply(
self,
count: int,
type: PhenomenologyType | str,
encoding: str,
schemas: list[Schema],
density: float | None = None,
statistics: list[Statistic] | None = None,
) -> None:
"""Applies Pointcloud extension properties to the extended Item.
Args:
count : REQUIRED. The number of points in the cloud.
type : REQUIRED. Phenomenology type for the point cloud. Possible valid
values might include lidar, eopc, radar, sonar, or otherThe type of file
or data format of the cloud.
encoding : REQUIRED. Content encoding or format of the data.
schemas : REQUIRED. A sequential array of items
that define the dimensions and their types.
density : Number of points per square unit area.
statistics : A sequential array of items mapping to
pc:schemas defines per-channel statistics.
"""
self.count = count
self.type = type
self.encoding = encoding
self.schemas = schemas
self.density = density
self.statistics = statistics
@property
def count(self) -> int:
"""Gets or sets the number of points in the Item."""
return get_required(self._get_property(COUNT_PROP, int), self, COUNT_PROP)
@count.setter
def count(self, v: int) -> None:
self._set_property(COUNT_PROP, v, pop_if_none=False)
@property
def type(self) -> PhenomenologyType | str:
"""Gets or sets the phenomenology type for the point cloud."""
return get_required(self._get_property(TYPE_PROP, str), self, TYPE_PROP)
@type.setter
def type(self, v: PhenomenologyType | str) -> None:
self._set_property(TYPE_PROP, v, pop_if_none=False)
@property
def encoding(self) -> str:
"""Gets or sets the content encoding or format of the data."""
return get_required(self._get_property(ENCODING_PROP, str), self, ENCODING_PROP)
@encoding.setter
def encoding(self, v: str) -> None:
self._set_property(ENCODING_PROP, v, pop_if_none=False)
@property
def schemas(self) -> list[Schema]:
"""Gets or sets the list of :class:`Schema` instances defining
dimensions and types for the data.
"""
result = get_required(
self._get_property(SCHEMAS_PROP, list[dict[str, Any]]), self, SCHEMAS_PROP
)
return [Schema(s) for s in result]
@schemas.setter
def schemas(self, v: list[Schema]) -> None:
self._set_property(SCHEMAS_PROP, [x.to_dict() for x in v], pop_if_none=False)
@property
def density(self) -> float | None:
"""Gets or sets the number of points per square unit area."""
return self._get_property(DENSITY_PROP, float)
@density.setter
def density(self, v: float | None) -> None:
self._set_property(DENSITY_PROP, v)
@property
def statistics(self) -> list[Statistic] | None:
"""Gets or sets the list of :class:`Statistic` instances describing
the pre-channel statistics. Elements in this list map to elements in the
:attr:`PointcloudExtension.schemas` list."""
result = self._get_property(STATISTICS_PROP, list[dict[str, Any]])
return map_opt(lambda stats: [Statistic(s) for s in stats], result)
@statistics.setter
def statistics(self, v: list[Statistic] | None) -> None:
set_value = map_opt(lambda stats: [s.to_dict() for s in stats], v)
self._set_property(STATISTICS_PROP, set_value)
[docs]
@classmethod
def get_schema_uri(cls) -> str:
return SCHEMA_URI
[docs]
@classmethod
def ext(cls, obj: T, add_if_missing: bool = False) -> PointcloudExtension[T]:
"""Extends the given STAC Object with properties from the :stac-ext:`Point Cloud
Extension <pointcloud>`.
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(PointcloudExtension[T], ItemPointcloudExtension(obj))
elif isinstance(obj, pystac.Asset):
if obj.owner is not None and not isinstance(obj.owner, pystac.Item):
raise pystac.ExtensionTypeError(
"Pointcloud extension does not apply to Collection Assets."
)
cls.ensure_owner_has_extension(obj, add_if_missing)
return cast(PointcloudExtension[T], AssetPointcloudExtension(obj))
elif isinstance(obj, item_assets.AssetDefinition):
cls.ensure_owner_has_extension(obj, add_if_missing)
return cast(PointcloudExtension[T], ItemAssetsPointcloudExtension(obj))
else:
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
[docs]
@classmethod
def summaries(
cls, obj: pystac.Collection, add_if_missing: bool = False
) -> SummariesPointcloudExtension:
cls.ensure_has_extension(obj, add_if_missing)
return SummariesPointcloudExtension(obj)
[docs]
class ItemPointcloudExtension(PointcloudExtension[pystac.Item]):
"""A concrete implementation of :class:`PointcloudExtension` on an
:class:`~pystac.Item` that extends the properties of the Item to include
properties defined in the :stac-ext:`Point Cloud Extension <pointcloud>`.
This class should generally not be instantiated directly. Instead, call
:meth:`PointcloudExtension.ext` on an :class:`~pystac.Item` to extend it.
"""
item: pystac.Item
properties: dict[str, Any]
def __init__(self, item: pystac.Item):
self.item = item
self.properties = item.properties
def __repr__(self) -> str:
return f"<ItemPointcloudExtension Item id={self.item.id}>"
[docs]
class AssetPointcloudExtension(PointcloudExtension[pystac.Asset]):
"""A concrete implementation of :class:`PointcloudExtension` on an
:class:`~pystac.Asset` that extends the Asset fields to include properties defined
in the :stac-ext:`Point Cloud Extension <pointcloud>`.
This class should generally not be instantiated directly. Instead, call
:meth:`PointcloudExtension.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]
self.repr_id = f"href={asset.href} item.id={asset.owner.id}"
else:
self.repr_id = f"href={asset.href}"
def __repr__(self) -> str:
return f"<AssetPointcloudExtension Asset {self.repr_id}>"
[docs]
class ItemAssetsPointcloudExtension(PointcloudExtension[item_assets.AssetDefinition]):
properties: dict[str, Any]
asset_defn: item_assets.AssetDefinition
def __init__(self, item_asset: item_assets.AssetDefinition):
self.asset_defn = item_asset
self.properties = item_asset.properties
[docs]
class SummariesPointcloudExtension(SummariesExtension):
"""A concrete implementation of :class:`~SummariesExtension` that extends
the ``summaries`` field of a :class:`~pystac.Collection` to include properties
defined in the :stac-ext:`Point Cloud Extension <pointcloud>`.
"""
@property
def count(self) -> RangeSummary[int] | None:
return self.summaries.get_range(COUNT_PROP)
@count.setter
def count(self, v: RangeSummary[int] | None) -> None:
self._set_summary(COUNT_PROP, v)
@property
def type(self) -> list[PhenomenologyType | str] | None:
return self.summaries.get_list(TYPE_PROP)
@type.setter
def type(self, v: list[PhenomenologyType | str] | None) -> None:
self._set_summary(TYPE_PROP, v)
@property
def encoding(self) -> list[str] | None:
return self.summaries.get_list(ENCODING_PROP)
@encoding.setter
def encoding(self, v: list[str] | None) -> None:
self._set_summary(ENCODING_PROP, v)
@property
def density(self) -> RangeSummary[float] | None:
return self.summaries.get_range(DENSITY_PROP)
@density.setter
def density(self, v: RangeSummary[float] | None) -> None:
self._set_summary(DENSITY_PROP, v)
@property
def statistics(self) -> list[Statistic] | None:
return map_opt(
lambda stats: [Statistic(d) for d in stats],
self.summaries.get_list(STATISTICS_PROP),
)
@statistics.setter
def statistics(self, v: list[Statistic] | None) -> None:
self._set_summary(
STATISTICS_PROP,
map_opt(lambda stats: [s.to_dict() for s in stats], v),
)
[docs]
class PointcloudExtensionHooks(ExtensionHooks):
schema_uri: str = SCHEMA_URI
prev_extension_ids = {"pointcloud"}
stac_object_types = {pystac.STACObjectType.ITEM}
POINTCLOUD_EXTENSION_HOOKS: ExtensionHooks = PointcloudExtensionHooks()