diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd02a7e3..dc922b503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,19 @@ - Migration for `sar:type` -> `sar:product_type` and `sar:polarization` -> `sar:polarizations` for pre-0.9 catalogs ([#556](https://github.com/stac-utils/pystac/pull/556)) +- Collection summaries for Point Cloud Extension ([#558](https://github.com/stac-utils/pystac/pull/558)) +- `PhenomenologyType` enum for recommended values of `pc:type` & `SchemaType` enum for + valid values of `type` in [Point Cloud Schema + Objects](https://github.com/stac-extensions/pointcloud#schema-object) + ([#548](https://github.com/stac-utils/pystac/pull/548)) ### Removed ### Changed - The `from_dict` method on STACObjects will set the object's root link when a `root` parameter is present. An ItemCollection `from_dict` with a root parameter will set the root on each of it's Items. ([#549](https://github.com/stac-utils/pystac/pull/549)) +- `PointcloudSchema` -> `Schema`, `PointcloudStatistic` -> `Statistic` for consistency + with naming convention in other extensions ([#548](https://github.com/stac-utils/pystac/pull/548)) ### Fixed diff --git a/docs/api.rst b/docs/api.rst index 0478210c9..cb46e932c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -423,17 +423,62 @@ LabelExtension Pointcloud Extension -------------------- -Implements the :stac-ext:`Point Cloud Extension `. +These classes are representations of the :stac-ext:`Pointcloud Extension Spec +`. -PointcloudItemExt +PhenomenologyType ~~~~~~~~~~~~~~~~~ -**TEMPORARILY REMOVED** +.. autoclass:: pystac.extensions.pointcloud.PhenomenologyType + :members: + :undoc-members: + :show-inheritance: -.. .. autoclass:: pystac.extensions.pointcloud.PointcloudItemExt -.. :members: -.. :undoc-members: -.. :show-inheritance: +SchemaType +~~~~~~~~~~ + +.. autoclass:: pystac.extensions.pointcloud.SchemaType + :members: + :undoc-members: + :show-inheritance: + +Schema +~~~~~~ + +.. autoclass:: pystac.extensions.pointcloud.Schema + :members: + :inherited-members: + :show-inheritance: + +Statistic +~~~~~~~~~ + +.. autoclass:: pystac.extensions.pointcloud.Statistic + :members: + :inherited-members: + :show-inheritance: + +PointcloudExtension +~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.pointcloud.PointcloudExtension + :members: + :inherited-members: + :show-inheritance: + +ItemPointcloudExtension +~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.pointcloud.ItemPointcloudExtension + :members: + :show-inheritance: + +AssetPointcloudExtension +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pystac.extensions.pointcloud.AssetPointcloudExtension + :members: + :show-inheritance: Projection Extension -------------------- diff --git a/pystac/extensions/pointcloud.py b/pystac/extensions/pointcloud.py index 21d6a060c..b8c86d801 100644 --- a/pystac/extensions/pointcloud.py +++ b/pystac/extensions/pointcloud.py @@ -2,64 +2,84 @@ https://github.com/stac-extensions/pointcloud """ - -from typing import Any, Dict, Generic, List, Optional, TypeVar, cast +from enum import Enum +from typing import Any, Dict, Generic, List, Optional, TypeVar, cast, Union import pystac from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, + SummariesExtension, ) from pystac.extensions.hooks import ExtensionHooks -from pystac.utils import map_opt +from pystac.summaries import RangeSummary +from pystac.utils import map_opt, get_required T = TypeVar("T", pystac.Item, pystac.Asset) -SCHEMA_URI = "https://stac-extensions.github.io/pointcloud/v1.0.0/schema.json" +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" + + +class PhenomenologyType(str, Enum): + """Valid values for the ``pc:type`` field in the :stac-ext:`Pointcloud Item + Properties `.""" + + LIDAR = "lidar" + EOPC = "eopc" + RADAR = "radar" + SONAR = "sonar" + OTHER = "other" + + +class SchemaType(str, Enum): + """Valid values for the ``type`` field in a :stac-ext:`Schema Object + `.""" -COUNT_PROP = "pc:count" -TYPE_PROP = "pc:type" -ENCODING_PROP = "pc:encoding" -SCHEMAS_PROP = "pc:schemas" -DENSITY_PROP = "pc:density" -STATISTICS_PROP = "pc:statistics" + FLOATING = "floating" + UNSIGNED = "unsigned" + SIGNED = "signed" -class PointcloudSchema: +class Schema: """Defines a schema for dimension of a pointcloud (e.g., name, size, type) - Use PointCloudSchema.create to create a new instance of PointCloudSchema from + Use :meth:`Schema.create` to create a new instance of ``Schema`` from properties. """ def __init__(self, properties: Dict[str, Any]) -> None: self.properties = properties - def apply(self, name: str, size: int, type: str) -> None: - """Sets the properties for this PointCloudSchema. + 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` + type : Dimension type. Valid values are ``floating``, ``unsigned``, and + ``signed`` """ self.properties["name"] = name self.properties["size"] = size self.properties["type"] = type @classmethod - def create(cls, name: str, size: int, type: str) -> "PointcloudSchema": - """Creates a new PointCloudSchema. + 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` - - Returns: - PointCloudSchema + type : Dimension type. Valid values are ``floating``, ``unsigned``, and + ``signed`` """ c = cls({}) c.apply(name=name, size=size, type=type) @@ -67,11 +87,7 @@ def create(cls, name: str, size: int, type: str) -> "PointcloudSchema": @property def size(self) -> int: - """Get or sets the size value. - - Returns: - int - """ + """Gets or sets the size value.""" result: Optional[int] = self.properties.get("size") if result is None: raise pystac.STACError( @@ -88,11 +104,7 @@ def size(self, v: int) -> None: @property def name(self) -> str: - """Get or sets the name property for this PointCloudSchema. - - Returns: - str - """ + """Gets or sets the name property for this Schema.""" result: Optional[str] = self.properties.get("name") if result is None: raise pystac.STACError( @@ -105,44 +117,30 @@ def name(self, v: str) -> None: self.properties["name"] = v @property - def type(self) -> str: - """Get or sets the type property. Valid values are `floating`, `unsigned`, and `signed` - - Returns: - str - """ - result: Optional[str] = self.properties.get("type") - if result is None: - raise pystac.STACError( - f"Pointcloud schema has no type property: {self.properties}" - ) - return result + 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: str) -> None: + def type(self, v: SchemaType) -> None: self.properties["type"] = v def __repr__(self) -> str: - return "".format( + return "".format( self.name, self.size, self.type ) def to_dict(self) -> Dict[str, Any]: - """Returns the dictionary representing the JSON of this PointCloudSchema. - - Returns: - dict: The wrapped dict of the PointCloudSchema that can be written out as - JSON. - """ + """Returns a JSON-like dictionary representing this ``Schema``.""" return self.properties -class PointcloudStatistic: +class Statistic: """Defines a single statistic for Pointcloud channel or dimension - Use PointcloudStatistic.create to create a new instance of LabelClasses from - property values. - """ + Use :meth:`Statistic.create` to create a new instance of + ``Statistic`` from property values.""" def __init__(self, properties: Dict[str, Any]) -> None: self.properties = properties @@ -158,17 +156,17 @@ def apply( stddev: Optional[float] = None, variance: Optional[float] = None, ) -> None: - """Sets the properties for this PointcloudStatistic. + """Sets the properties for this Statistic. Args: name : REQUIRED. The name of the channel. - position : Position of the channel in the schema. - average : The average of the channel. - count : The number of elements in the channel. - maximum : The maximum value of the channel. - minimum : The minimum value of the channel. - stddev : The standard deviation of the channel. - variance : The variance 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 @@ -190,21 +188,18 @@ def create( minimum: Optional[float] = None, stddev: Optional[float] = None, variance: Optional[float] = None, - ) -> "PointcloudStatistic": - """Creates a new PointcloudStatistic class. + ) -> "Statistic": + """Creates a new Statistic class. Args: name : REQUIRED. The name of the channel. - position : Position of the channel in the schema. - average (float) The average of the channel. - count : The number of elements in the channel. - maximum : The maximum value of the channel. - minimum : The minimum value of the channel. - stddev : The standard deviation of the channel. - variance : The variance of the channel. - - Returns: - LabelClasses + 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( @@ -221,11 +216,7 @@ def create( @property def name(self) -> str: - """Get or sets the name property - - Returns: - str - """ + """Gets or sets the name property.""" result: Optional[str] = self.properties.get("name") if result is None: raise pystac.STACError( @@ -242,11 +233,7 @@ def name(self, v: str) -> None: @property def position(self) -> Optional[int]: - """Get or sets the position property - - Returns: - int - """ + """Gets or sets the position property.""" return self.properties.get("position") @position.setter @@ -258,11 +245,7 @@ def position(self, v: Optional[int]) -> None: @property def average(self) -> Optional[float]: - """Get or sets the average property - - Returns: - float - """ + """Gets or sets the average property.""" return self.properties.get("average") @average.setter @@ -274,11 +257,7 @@ def average(self, v: Optional[float]) -> None: @property def count(self) -> Optional[int]: - """Get or sets the count property - - Returns: - int - """ + """Gets or sets the count property.""" return self.properties.get("count") @count.setter @@ -290,11 +269,7 @@ def count(self, v: Optional[int]) -> None: @property def maximum(self) -> Optional[float]: - """Get or sets the maximum property - - Returns: - float - """ + """Gets or sets the maximum property.""" return self.properties.get("maximum") @maximum.setter @@ -306,11 +281,7 @@ def maximum(self, v: Optional[float]) -> None: @property def minimum(self) -> Optional[float]: - """Get or sets the minimum property - - Returns: - float - """ + """Gets or sets the minimum property.""" return self.properties.get("minimum") @minimum.setter @@ -322,11 +293,7 @@ def minimum(self, v: Optional[float]) -> None: @property def stddev(self) -> Optional[float]: - """Get or sets the stddev property - - Returns: - float - """ + """Gets or sets the stddev property.""" return self.properties.get("stddev") @stddev.setter @@ -338,11 +305,7 @@ def stddev(self, v: Optional[float]) -> None: @property def variance(self) -> Optional[float]: - """Get or sets the variance property - - Returns: - float - """ + """Gets or sets the variance property.""" return self.properties.get("variance") @variance.setter @@ -353,42 +316,47 @@ def variance(self, v: Optional[float]) -> None: self.properties.pop("variance", None) def __repr__(self) -> str: - return "".format(str(self.properties)) + return "".format(str(self.properties)) def to_dict(self) -> Dict[str, Any]: - """Returns the dictionary representing the JSON of this PointcloudStatistic. - - Returns: - dict: The wrapped dict of the PointcloudStatistic that can be written out - as JSON. - """ + """Returns a JSON-like dictionary representing this ``Statistic``.""" return self.properties + def __eq__(self, o: object) -> bool: + if not isinstance(o, Statistic): + return NotImplemented + return self.to_dict() == o.to_dict() + class PointcloudExtension( - Generic[T], PropertiesExtension, ExtensionManagementMixin[pystac.Item] + Generic[T], + PropertiesExtension, + ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]], ): - """PointcloudItemExt is the extension of an Item in the PointCloud Extension. - The Pointclout extension adds pointcloud information to STAC Items. + """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 `. This class is generic over the type + of STAC Object to be extended (e.g. :class:`~pystac.Item`, + :class:`~pystac.Asset`). - Args: - item : The item to be extended. + To create a concrete instance of :class:`PointcloudExtension`, use the + :meth:`PointcloudExtension.ext` method. For example: - Attributes: - item : The Item that is being extended. + .. code-block:: python + >>> item: pystac.Item = ... + >>> pc_ext = PointcloudExtension.ext(item) """ def apply( self, count: int, - type: str, + type: Union[PhenomenologyType, str], encoding: str, - schemas: List[PointcloudSchema], + schemas: List[Schema], density: Optional[float] = None, - statistics: Optional[List[PointcloudStatistic]] = None, - epsg: Optional[int] = None, - ) -> None: # TODO: Remove epsg per spec + statistics: Optional[List[Statistic]] = None, + ) -> None: """Applies Pointcloud extension properties to the extended Item. Args: @@ -398,12 +366,10 @@ def apply( 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. + 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. - epsg : An EPSG code for the projected coordinates of the pointcloud. """ self.count = count self.type = type @@ -411,15 +377,10 @@ def apply( self.schemas = schemas self.density = density self.statistics = statistics - self.epsg = epsg @property def count(self) -> int: - """Get or sets the count property of the datasource. - - Returns: - int - """ + """Gets or sets the number of points in the Item.""" result = self._get_property(COUNT_PROP, int) if result is None: raise pystac.RequiredPropertyMissing(self, COUNT_PROP) @@ -430,31 +391,21 @@ def count(self, v: int) -> None: self._set_property(COUNT_PROP, v, pop_if_none=False) @property - def type(self) -> str: - """Get or sets the pc:type prop on the Item - - Returns: - str - """ - result = self._get_property(TYPE_PROP, str) - if result is None: - raise pystac.RequiredPropertyMissing(self, TYPE_PROP) - return result + def type(self) -> Union[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: str) -> None: + def type(self, v: Union[PhenomenologyType, str]) -> None: self._set_property(TYPE_PROP, v, pop_if_none=False) @property def encoding(self) -> str: - """Get or sets the content-encoding for the item. - - The content-encoding is the underlying encoding format for the point cloud. - Examples may include: laszip, ascii, binary, etc. - - Returns: - str - """ + """Gets or sets the content encoding or format of the data.""" result = self._get_property(ENCODING_PROP, str) if result is None: raise pystac.RequiredPropertyMissing(self, ENCODING_PROP) @@ -465,34 +416,22 @@ def encoding(self, v: str) -> None: self._set_property(ENCODING_PROP, v, pop_if_none=False) @property - def schemas(self) -> List[PointcloudSchema]: - """Get or sets a - - The schemas represent the structure of the data attributes in the pointcloud, - and is represented as a sequential array of items that define the dimensions - and their types, - - Returns: - List[PointcloudSchema] + def schemas(self) -> List[Schema]: + """Gets or sets the list of :class:`Schema` instances defining + dimensions and types for the data. """ result = self._get_property(SCHEMAS_PROP, List[Dict[str, Any]]) if result is None: raise pystac.RequiredPropertyMissing(self, SCHEMAS_PROP) - return [PointcloudSchema(s) for s in result] + return [Schema(s) for s in result] @schemas.setter - def schemas(self, v: List[PointcloudSchema]) -> None: + 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) -> Optional[float]: - """Get or sets the density for the item. - - Density is defined as the number of points per square unit area. - - Returns: - int - """ + """Gets or sets the number of points per square unit area.""" return self._get_property(DENSITY_PROP, float) @density.setter @@ -500,21 +439,15 @@ def density(self, v: Optional[float]) -> None: self._set_property(DENSITY_PROP, v) @property - def statistics(self) -> Optional[List[PointcloudStatistic]]: - """Get or sets the statistics for each property of the dataset. - - A sequential array of items mapping to pc:schemas defines per-channel - statistics. - - Example:: - - item.ext.pointcloud.statistics = [{ 'name': 'red', 'min': 0, 'max': 255 }] - """ + def statistics(self) -> Optional[List[Statistic]]: + """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: [PointcloudStatistic(s) for s in stats], result) + return map_opt(lambda stats: [Statistic(s) for s in stats], result) @statistics.setter - def statistics(self, v: Optional[List[PointcloudStatistic]]) -> None: + def statistics(self, v: Optional[List[Statistic]]) -> None: set_value = map_opt(lambda stats: [s.to_dict() for s in stats], v) self._set_property(STATISTICS_PROP, set_value) @@ -524,6 +457,16 @@ def get_schema_uri(cls) -> str: @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 `. + + 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): if add_if_missing: cls.add_to(obj) @@ -539,8 +482,26 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> "PointcloudExtension[T]": f"Pointcloud extension does not apply to type '{type(obj).__name__}'" ) + @classmethod + def summaries( + cls, obj: pystac.Collection, add_if_missing: bool = False + ) -> "SummariesPointcloudExtension": + if not add_if_missing: + cls.validate_has_extension(obj) + else: + cls.add_to(obj) + return SummariesPointcloudExtension(obj) + 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 `. + + This class should generally not be instantiated directly. Instead, call + :meth:`PointcloudExtension.ext` on an :class:`~pystac.Item` to extend it. + """ + def __init__(self, item: pystac.Item): self.item = item self.properties = item.properties @@ -550,6 +511,14 @@ def __repr__(self) -> str: 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 `. + + This class should generally not be instantiated directly. Instead, call + :meth:`PointcloudExtension.ext` on an :class:`~pystac.Asset` to extend it. + """ + def __init__(self, asset: pystac.Asset): self.asset_href = asset.href self.properties = asset.extra_fields @@ -563,6 +532,59 @@ def __repr__(self) -> str: return f"" +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 `. + """ + + @property + def count(self) -> Optional[RangeSummary[int]]: + return self.summaries.get_range(COUNT_PROP) + + @count.setter + def count(self, v: Optional[RangeSummary[int]]) -> None: + self._set_summary(COUNT_PROP, v) + + @property + def type(self) -> Optional[List[Union[PhenomenologyType, str]]]: + return self.summaries.get_list(TYPE_PROP) + + @type.setter + def type(self, v: Optional[List[Union[PhenomenologyType, str]]]) -> None: + self._set_summary(TYPE_PROP, v) + + @property + def encoding(self) -> Optional[List[str]]: + return self.summaries.get_list(ENCODING_PROP) + + @encoding.setter + def encoding(self, v: Optional[List[str]]) -> None: + self._set_summary(ENCODING_PROP, v) + + @property + def density(self) -> Optional[RangeSummary[float]]: + return self.summaries.get_range(DENSITY_PROP) + + @density.setter + def density(self, v: Optional[RangeSummary[float]]) -> None: + self._set_summary(DENSITY_PROP, v) + + @property + def statistics(self) -> Optional[List[Statistic]]: + return map_opt( + lambda stats: [Statistic(d) for d in stats], + self.summaries.get_list(STATISTICS_PROP), + ) + + @statistics.setter + def statistics(self, v: Optional[List[Statistic]]) -> None: + self._set_summary( + STATISTICS_PROP, + map_opt(lambda stats: [s.to_dict() for s in stats], v), + ) + + class PointcloudExtensionHooks(ExtensionHooks): schema_uri: str = SCHEMA_URI prev_extension_ids = {"pointcloud"} diff --git a/tests/extensions/test_pointcloud.py b/tests/extensions/test_pointcloud.py index 044eba586..f89cff5eb 100644 --- a/tests/extensions/test_pointcloud.py +++ b/tests/extensions/test_pointcloud.py @@ -9,10 +9,13 @@ from pystac.errors import ExtensionTypeError, STACError from pystac.extensions.pointcloud import ( AssetPointcloudExtension, + PhenomenologyType, PointcloudExtension, - PointcloudSchema, - PointcloudStatistic, + Schema, + SchemaType, + Statistic, ) +from pystac.summaries import RangeSummary from tests.utils import TestCases, assert_to_from_dict @@ -37,9 +40,9 @@ def test_apply(self) -> None: PointcloudExtension.add_to(item) PointcloudExtension.ext(item).apply( 1000, - "lidar", + PhenomenologyType.LIDAR, "laszip", - [PointcloudSchema({"name": "X", "size": 8, "type": "floating"})], + [Schema({"name": "X", "size": 8, "type": "floating"})], ) self.assertTrue(PointcloudExtension.has_extension(item)) @@ -108,7 +111,7 @@ def test_schemas(self) -> None: self.assertEqual(pc_schemas, pc_item.properties["pc:schemas"]) # Set - schema = [PointcloudSchema({"name": "X", "size": 8, "type": "floating"})] + schema = [Schema({"name": "X", "size": 8, "type": "floating"})] PointcloudExtension.ext(pc_item).schemas = schema self.assertEqual( [s.to_dict() for s in schema], pc_item.properties["pc:schemas"] @@ -129,7 +132,7 @@ def test_statistics(self) -> None: # Set stats = [ - PointcloudStatistic( + Statistic( { "average": 1, "count": 1, @@ -169,7 +172,7 @@ def test_pointcloud_schema(self) -> None: "size": 8, "type": "floating", } - schema = PointcloudSchema(props) + schema = Schema(props) self.assertEqual(props, schema.properties) # test all getters and setters @@ -181,7 +184,7 @@ def test_pointcloud_schema(self) -> None: setattr(schema, k, val) self.assertEqual(getattr(schema, k), val) - schema = PointcloudSchema.create("intensity", 16, "unsigned") + schema = Schema.create("intensity", 16, SchemaType.UNSIGNED) self.assertEqual(schema.name, "intensity") self.assertEqual(schema.size, 16) self.assertEqual(schema.type, "unsigned") @@ -189,7 +192,7 @@ def test_pointcloud_schema(self) -> None: with self.assertRaises(STACError): schema.size = 0.5 # type: ignore - empty_schema = PointcloudSchema({}) + empty_schema = Schema({}) with self.assertRaises(STACError): empty_schema.size with self.assertRaises(STACError): @@ -208,7 +211,7 @@ def test_pointcloud_statistics(self) -> None: "stddev": 1, "variance": 1, } - stat = PointcloudStatistic(props) + stat = Statistic(props) self.assertEqual(props, stat.properties) # test all getters and setters @@ -220,7 +223,7 @@ def test_pointcloud_statistics(self) -> None: setattr(stat, k, val) self.assertEqual(getattr(stat, k), val) - stat = PointcloudStatistic.create("foo", 1, 2, 3, 4, 5, 6, 7) + stat = Statistic.create("foo", 1, 2, 3, 4, 5, 6, 7) self.assertEqual(stat.name, "foo") self.assertEqual(stat.position, 1) self.assertEqual(stat.average, 2) @@ -247,7 +250,7 @@ def test_pointcloud_statistics(self) -> None: stat.variance = None self.assertNotIn("variance", stat.properties) - empty_stat = PointcloudStatistic({}) + empty_stat = Statistic({}) with self.assertRaises(STACError): empty_stat.name @@ -334,3 +337,119 @@ def test_asset_ext_add_to(self) -> None: _ = PointcloudExtension.ext(asset, add_if_missing=True) self.assertIn(PointcloudExtension.get_schema_uri(), item.stac_extensions) + + +class PointcloudSummariesTest(unittest.TestCase): + def setUp(self) -> None: + self.maxDiff = None + self.collection = pystac.Collection.from_file( + TestCases.get_path("data-files/collections/multi-extent.json") + ) + + def test_count(self) -> None: + collection = self.collection.clone() + summaries_ext = PointcloudExtension.summaries(collection, True) + count_range = RangeSummary(1000, 10000) + + summaries_ext.count = count_range + + self.assertEqual( + summaries_ext.count, + count_range, + ) + + summaries_dict = collection.to_dict()["summaries"] + + self.assertEqual( + summaries_dict["pc:count"], + count_range.to_dict(), + ) + + def test_type(self) -> None: + collection = self.collection.clone() + summaries_ext = PointcloudExtension.summaries(collection, True) + type_list = [PhenomenologyType.LIDAR, "something"] + + summaries_ext.type = type_list + + self.assertEqual( + summaries_ext.type, + type_list, + ) + + summaries_dict = collection.to_dict()["summaries"] + + self.assertEqual( + summaries_dict["pc:type"], + type_list, + ) + + def test_encoding(self) -> None: + collection = self.collection.clone() + summaries_ext = PointcloudExtension.summaries(collection, True) + encoding_list = ["LASzip"] + + summaries_ext.encoding = encoding_list + + self.assertEqual( + summaries_ext.encoding, + encoding_list, + ) + + summaries_dict = collection.to_dict()["summaries"] + + self.assertEqual( + summaries_dict["pc:encoding"], + encoding_list, + ) + + def test_density(self) -> None: + collection = self.collection.clone() + summaries_ext = PointcloudExtension.summaries(collection, True) + density_range = RangeSummary(500.0, 1000.0) + + summaries_ext.density = density_range + + self.assertEqual( + summaries_ext.density, + density_range, + ) + + summaries_dict = collection.to_dict()["summaries"] + + self.assertEqual( + summaries_dict["pc:density"], + density_range.to_dict(), + ) + + def test_statistics(self) -> None: + collection = self.collection.clone() + summaries_ext = PointcloudExtension.summaries(collection, True) + statistics_list = [ + Statistic( + { + "average": 637294.1783, + "count": 10653336, + "maximum": 639003.73, + "minimum": 635577.79, + "name": "X", + "position": 0, + "stddev": 967.9329805, + "variance": 936894.2548, + } + ) + ] + + summaries_ext.statistics = statistics_list + + self.assertEqual( + summaries_ext.statistics, + statistics_list, + ) + + summaries_dict = collection.to_dict()["summaries"] + + self.assertEqual( + summaries_dict["pc:statistics"], + [s.to_dict() for s in statistics_list], + )