Skip to content

Commit 3ce331a

Browse files
committed
[wip]
1 parent 2c65a1c commit 3ce331a

File tree

4 files changed

+238
-106
lines changed

4 files changed

+238
-106
lines changed

pystac/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
import pystac.validation
8585

8686
import pystac.extensions.hooks
87+
import pystac.extensions.classification
8788
import pystac.extensions.datacube
8889
import pystac.extensions.eo
8990
import pystac.extensions.file
@@ -105,6 +106,7 @@
105106

106107
EXTENSION_HOOKS = pystac.extensions.hooks.RegisteredExtensionHooks(
107108
[
109+
pystac.extensions.classification.CLASSIFICATION_EXTENSION_HOOKS,
108110
pystac.extensions.datacube.DATACUBE_EXTENSION_HOOKS,
109111
pystac.extensions.eo.EO_EXTENSION_HOOKS,
110112
pystac.extensions.file.FILE_EXTENSION_HOOKS,

pystac/extensions/classification.py

Lines changed: 103 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import re
56
from typing import (
67
Any,
78
Dict,
@@ -32,7 +33,7 @@
3233
# Field names
3334
BITFIELDS_PROP: str = PREFIX + "bitfields"
3435
CLASSES_PROP: str = PREFIX + "classes"
35-
36+
RASTER_BANDS_PROP: str = "raster:bands"
3637

3738
class 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+
262291
class 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

364395
class 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+
389464
class SummariesClassificationExtension(SummariesExtension):
390465
@property
391466
def classes(self) -> Optional[List[Classification]]:

tests/data-files/classification/classification-landsat-example.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,6 @@
12161216
"https://stac-extensions.github.io/projection/v1.0.0/schema.json",
12171217
"https://landsat.usgs.gov/stac/landsat-extension/v1.1.1/schema.json",
12181218
"https://stac-extensions.github.io/scientific/v1.0.0/schema.json",
1219-
"https://stac-extensions.github.io/classification/v1.1.0/schema.json"
1219+
"https://stac-extensions.github.io/classification/v1.0.0/schema.json"
12201220
]
12211221
}

0 commit comments

Comments
 (0)