22
33from __future__ import annotations
44
5+ import re
56from typing import (
67 Any ,
78 Dict ,
3233# Field names
3334BITFIELDS_PROP : str = PREFIX + "bitfields"
3435CLASSES_PROP : str = PREFIX + "classes"
35-
36+ RASTER_BANDS_PROP : str = "raster:bands"
3637
3738class Classification :
3839 properties : Dict [str , Any ]
@@ -44,12 +45,12 @@ def apply(
4445 self ,
4546 value : int ,
4647 name : str ,
47- description : Optional [str ],
48- title : Optional [str ],
48+ description : Optional [str ] = None ,
49+ title : Optional [str ] = None ,
4950 color_hint : Optional [
5051 str
51- ], # TODO: convert to an RGB string with syntax checking
52- nodata : Optional [bool ],
52+ ] = None ,
53+ nodata : Optional [bool ] = None ,
5354 ) -> None :
5455 self .value = value
5556 self .name = name
@@ -58,6 +59,10 @@ def apply(
5859 self .color_hint = color_hint
5960 self .nodata = nodata
6061
62+ color_hint_pattern = re .compile ("^([0-9A-F]{6})$" )
63+ match = color_hint_pattern .match (color_hint )
64+ assert color_hint is None or match is not None and match .group () == color_hint , "Must format color hints as '^([0-9A-F]{6})$'"
65+
6166 @classmethod
6267 def create (
6368 cls ,
@@ -170,9 +175,12 @@ def apply(
170175
171176 assert offset >= 0 , "Non-negative offsets only"
172177 assert length >= 1 , "Positive field lengths only"
178+ assert len (classes ) > 0 , "Must specify at least one class"
179+ assert roles is None or len (roles ) > 0 , "When set, roles must contain at least one item"
173180
174- class_coverage = set ([c .value for c in classes ])
175- assert set (range (0 , 2 ** length )) - class_coverage == set (), "Classes must cover the complete range of values"
181+ ## Ensure complete coverage of bit fields in classes
182+ # class_coverage = set([c.value for c in classes])
183+ # assert set(range(0, 2**length)) - class_coverage == set(), "Classes must cover the complete range of values"
176184
177185 @classmethod
178186 def create (
@@ -213,11 +221,11 @@ def length(self, v: int) -> None:
213221
214222 @property
215223 def classes (self ) -> List [Classification ]:
216- return get_required (self .properties .get ("classes" ), self , "classes" )
224+ return [ Classification ( d ) for d in get_required (self .properties .get ("classes" ), self , "classes" )]
217225
218226 @classes .setter
219227 def classes (self , v : List [Classification ]) -> None :
220- self .properties ["classes" ] = v
228+ self .properties ["classes" ] = [ c . to_dict () for c in v ]
221229
222230 @property
223231 def roles (self ) -> Optional [List [str ]]:
@@ -259,6 +267,27 @@ def to_dict(self) -> Dict[str, Any]:
259267 return self .properties
260268
261269
270+ def __fetch_from_dict (d : Dict [str , Any ]) -> Optional [Union [Classification , Bitfield ]]:
271+ assert not (CLASSES_PROP in d and BITFIELDS_PROP in d ), f"Cannot define both { CLASSES_PROP } and { BITFIELDS :PROP} in the same entity"
272+
273+ if CLASSES_PROP in d :
274+ return list (
275+ map (
276+ lambda item : Classification (item ),
277+ d [CLASSES_PROP ]
278+ )
279+ )
280+ elif BITFIELDS_PROP in d :
281+ return list (
282+ map (
283+ lambda item : Bitfield (item ),
284+ d [BITFIELDS_PROP ]
285+ )
286+ )
287+ else :
288+ return None
289+
290+
262291class ClassificationExtension (
263292 Generic [T ],
264293 PropertiesExtension ,
@@ -269,11 +298,12 @@ def apply(
269298 classes : Optional [List [Classification ]] = None ,
270299 bitfields : Optional [List [Bitfield ]] = None ,
271300 ) -> None :
301+ # assert classes is None and bitfields is not None or bitfields is None and classes is not None, "Must set exactly one of `classes` or `bitfields`"
272302 self .classes = classes
273303 self .bitfields = bitfields
274304
275305 @property
276- def classes (self ) -> Optional [List [Classification ]]:
306+ def classes (self ) -> Optional [List [Optional [ Classification ] ]]:
277307 return self ._get_classes ()
278308
279309 @classes .setter
@@ -282,7 +312,7 @@ def classes(self, v: Optional[List[Classification]]) -> None:
282312 CLASSES_PROP , map_opt (lambda classes : [c .to_dict () for c in classes ], v )
283313 )
284314
285- def _get_classes (self ) -> Optional [List [Classification ]]:
315+ def _get_classes (self ) -> Optional [List [Optional [ Classification ] ]]:
286316 return map_opt (
287317 lambda classes : [Classification (c ) for c in classes ],
288318 self ._get_property (CLASSES_PROP , List [Dict [str , Any ]]),
@@ -317,6 +347,9 @@ def ext(cls, obj: T, add_if_missing: bool = False) -> ClassificationExtension[T]
317347 elif isinstance (obj , pystac .Asset ):
318348 cls .validate_owner_has_extension (obj , add_if_missing )
319349 return cast (ClassificationExtension [T ], AssetClassificationExtension (obj ))
350+ elif isinstance (obj , pystac .Collection ):
351+ cls .validate_has_extension (obj , add_if_missing )
352+ return cast (ClassificationExtension [T ], CollectionClassificationExtension (obj ))
320353 else :
321354 raise pystac .ExtensionTypeError (
322355 f"Classification extension does not apply to type '{ type (obj ).__name__ } '"
@@ -342,41 +375,61 @@ def __init__(self, item: pystac.Item):
342375 def _get_classes (self ) -> Optional [List [Classification ]]:
343376 classes = self ._get_property (CLASSES_PROP , List [Dict [str , Any ]])
344377
345- if classes is None :
346- asset_classes : List [Dict [str , Any ]] = []
347- for _ , value in self .item .get_assets ().items ():
348- if CLASSES_PROP in value .extra_fields :
349- asset_classes .extend (
350- cast (List [Dict [str , Any ]], value .extra_fields .get (CLASSES_PROP ))
351- )
352- if any (asset_classes ):
353- classes = asset_classes
354-
355378 if classes is not None :
356379 return [Classification (c ) for c in classes ]
357380
358381 return None
359382
383+ def _get_bitfields (self ) -> Optional [List [Bitfields ]]:
384+ bitfields = self ._get_property (BITFIELDS_PROP , List [Dict [str , Any ]])
385+
386+ if bitfields is not None :
387+ return [Bitfields (b ) for b in bitfields ]
388+
389+ return None
390+
360391 def __repr__ (self ) -> str :
361392 return f"<ItemClassificationExtension Item id={ self .item .id } >"
362393
363394
364395class AssetClassificationExtension (ClassificationExtension [pystac .Asset ]):
396+ asset : pystac .Asset
365397 asset_href : str
366398 properties : Dict [str , Any ]
367399 additional_red_properties : Optional [Iterable [Dict [str , Any ]]] = None
368400
369401 def _get_classes (self ) -> Optional [List [Classification ]]:
370402 if CLASSES_PROP not in self .properties :
371- return None
372- return list (
373- map (
374- lambda cls : Classification (cls ),
375- cast (List [Dict [str , Any ]], self .properties .get (CLASSES_PROP )),
376- )
377- )
403+ if (self .asset .owner and RasterExtension .has_extension (self .asset .owner )):
404+ bnds = RasterExtension .ext (self .asset ).bands
405+ return list (
406+ map (
407+ lambda b : Classification (b .properties .get (CLASSES_PROP )),
408+ bnds
409+ )
410+ )
411+ else :
412+ return None
413+ else :
414+ return [Classification (self .properties .get (CLASSES_PROP ))]
415+
416+ def _get_bitfields (self ) -> Optional [List [Bitfield ]]:
417+ if BITFIELDS_PROP not in self .properties :
418+ if (self .asset .owner and RasterExtension .has_extension (self .asset .owner )):
419+ bnds = RasterExtension .ext (self .asset ).bands
420+ return list (
421+ map (
422+ lambda b : Bitfield (b .properties .get (BITFIELDS_PROP )),
423+ bnds
424+ )
425+ )
426+ else :
427+ return None
428+ else :
429+ return [Classification (self .properties .get (CLASSES_PROP ))]
378430
379431 def __init__ (self , asset : pystac .Asset ):
432+ self .asset = asset
380433 self .asset_href = asset .href
381434 self .properties = asset .extra_fields
382435 if asset .owner and isinstance (asset .owner , pystac .Item ):
@@ -386,6 +439,28 @@ def __repr__(self) -> str:
386439 return f"<AssetClassificationExtension Asset href={ self .asset_href } >"
387440
388441
442+ class CollectionClassificationExtension (ClassificationExtension [pystac .Collection ]):
443+ """A concrete implementation of :class:`ClassificationExtension` on a
444+ :class:`~pystac.Collection` that extends the properties of a Collection to
445+ include properties defined in the :stac_ext:`Classification Extension
446+ <classification>`.
447+ """
448+ collection : pystac .Collection
449+ properties : Dict [str , Any ]
450+
451+ def __init__ (self , collection : pystac .Collection ):
452+ self .collection = collection
453+ self .properties = collection .assets
454+
455+ def _get_classes (self ) -> Optional [List [Optional [Classification ]]]:
456+ classes = __fetch_from_dict (self .properties )
457+ if classes is None and ItemAssetsExtension .has_extension (self .collection ):
458+ classes = __fetch_from_dict (ItemAssetsExtension .ext (self .collection ).item_assets )
459+ return list (filter (lambda x : isinstance (x , Classification ), classes ))
460+
461+ def _get_bitfields (self ) -> Optional [List [Bitfield ]]:
462+ pass
463+
389464class SummariesClassificationExtension (SummariesExtension ):
390465 @property
391466 def classes (self ) -> Optional [List [Classification ]]:
0 commit comments