pystac-ext-eo 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pystac/extensions/eo.py
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
"""Implements the :stac-ext:`Electro-Optical Extension <eo>`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Generic,
|
|
9
|
+
Literal,
|
|
10
|
+
TypeVar,
|
|
11
|
+
cast,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
import pystac
|
|
15
|
+
from pystac.extensions import projection, view
|
|
16
|
+
from pystac.extensions.base import (
|
|
17
|
+
ExtensionManagementMixin,
|
|
18
|
+
PropertiesExtension,
|
|
19
|
+
SummariesExtension,
|
|
20
|
+
)
|
|
21
|
+
from pystac.extensions.hooks import ExtensionHooks
|
|
22
|
+
from pystac.serialization.identify import STACJSONDescription, STACVersionID
|
|
23
|
+
from pystac.summaries import RangeSummary
|
|
24
|
+
from pystac.utils import get_required, map_opt
|
|
25
|
+
|
|
26
|
+
#: Generalized version of :class:`~pystac.Item`, :class:`~pystac.Asset`,
|
|
27
|
+
#: pr :class:`~pystac.ItemAssetDefinition`
|
|
28
|
+
T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition)
|
|
29
|
+
|
|
30
|
+
SCHEMA_URI: str = "https://stac-extensions.github.io/eo/v1.1.0/schema.json"
|
|
31
|
+
SCHEMA_URIS: list[str] = [
|
|
32
|
+
"https://stac-extensions.github.io/eo/v1.0.0/schema.json",
|
|
33
|
+
SCHEMA_URI,
|
|
34
|
+
]
|
|
35
|
+
PREFIX: str = "eo:"
|
|
36
|
+
|
|
37
|
+
# Field names
|
|
38
|
+
BANDS_PROP: str = PREFIX + "bands"
|
|
39
|
+
CLOUD_COVER_PROP: str = PREFIX + "cloud_cover"
|
|
40
|
+
SNOW_COVER_PROP: str = PREFIX + "snow_cover"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validated_percentage(v: float | None) -> float | None:
|
|
44
|
+
if v is not None and not isinstance(v, (float, int)) or isinstance(v, bool):
|
|
45
|
+
raise ValueError(f"Invalid percentage: {v} must be number")
|
|
46
|
+
if v is not None and not 0 <= v <= 100:
|
|
47
|
+
raise ValueError(f"Invalid percentage: {v} must be between 0 and 100")
|
|
48
|
+
return v
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Band:
|
|
52
|
+
"""Represents Band information attached to an Item that implements the eo extension.
|
|
53
|
+
|
|
54
|
+
Use :meth:`Band.create` to create a new Band.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
properties: dict[str, Any]
|
|
58
|
+
|
|
59
|
+
def __init__(self, properties: dict[str, Any]) -> None:
|
|
60
|
+
self.properties = properties
|
|
61
|
+
|
|
62
|
+
def apply(
|
|
63
|
+
self,
|
|
64
|
+
name: str,
|
|
65
|
+
common_name: str | None = None,
|
|
66
|
+
description: str | None = None,
|
|
67
|
+
center_wavelength: float | None = None,
|
|
68
|
+
full_width_half_max: float | None = None,
|
|
69
|
+
solar_illumination: float | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Sets the properties for this Band.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA").
|
|
76
|
+
common_name : The name commonly used to refer to the band to make it
|
|
77
|
+
easier to search for bands across instruments. See the :stac-ext:`list
|
|
78
|
+
of accepted common names <eo#common-band-names>`.
|
|
79
|
+
description : Description to fully explain the band.
|
|
80
|
+
center_wavelength : The center wavelength of the band, in micrometers (μm).
|
|
81
|
+
full_width_half_max : Full width at half maximum (FWHM). The width of the
|
|
82
|
+
band, as measured at half the maximum transmission, in micrometers (μm).
|
|
83
|
+
solar_illumination: The solar illumination of the band,
|
|
84
|
+
as measured at half the maximum transmission, in W/m2/micrometers.
|
|
85
|
+
"""
|
|
86
|
+
self.name = name
|
|
87
|
+
self.common_name = common_name
|
|
88
|
+
self.description = description
|
|
89
|
+
self.center_wavelength = center_wavelength
|
|
90
|
+
self.full_width_half_max = full_width_half_max
|
|
91
|
+
self.solar_illumination = solar_illumination
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def create(
|
|
95
|
+
cls,
|
|
96
|
+
name: str,
|
|
97
|
+
common_name: str | None = None,
|
|
98
|
+
description: str | None = None,
|
|
99
|
+
center_wavelength: float | None = None,
|
|
100
|
+
full_width_half_max: float | None = None,
|
|
101
|
+
solar_illumination: float | None = None,
|
|
102
|
+
) -> Band:
|
|
103
|
+
"""
|
|
104
|
+
Creates a new band.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA").
|
|
108
|
+
common_name : The name commonly used to refer to the band to make it easier
|
|
109
|
+
to search for bands across instruments. See the :stac-ext:`list of
|
|
110
|
+
accepted common names <eo#common-band-names>`.
|
|
111
|
+
description : Description to fully explain the band.
|
|
112
|
+
center_wavelength : The center wavelength of the band, in micrometers (μm).
|
|
113
|
+
full_width_half_max : Full width at half maximum (FWHM). The width of the
|
|
114
|
+
band, as measured at half the maximum transmission, in micrometers (μm).
|
|
115
|
+
solar_illumination: The solar illumination of the band,
|
|
116
|
+
as measured at half the maximum transmission, in W/m2/micrometers.
|
|
117
|
+
"""
|
|
118
|
+
b = cls({})
|
|
119
|
+
b.apply(
|
|
120
|
+
name=name,
|
|
121
|
+
common_name=common_name,
|
|
122
|
+
description=description,
|
|
123
|
+
center_wavelength=center_wavelength,
|
|
124
|
+
full_width_half_max=full_width_half_max,
|
|
125
|
+
solar_illumination=solar_illumination,
|
|
126
|
+
)
|
|
127
|
+
return b
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def name(self) -> str:
|
|
131
|
+
"""Get or sets the name of the band (e.g., "B01", "B02", "B1", "B5", "QA").
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
str
|
|
135
|
+
"""
|
|
136
|
+
return cast(str, get_required(self.properties.get("name"), self, "name"))
|
|
137
|
+
|
|
138
|
+
@name.setter
|
|
139
|
+
def name(self, v: str) -> None:
|
|
140
|
+
self.properties["name"] = v
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def common_name(self) -> str | None:
|
|
144
|
+
"""Get or sets the name commonly used to refer to the band to make it easier
|
|
145
|
+
to search for bands across instruments. See the :stac-ext:`list of accepted
|
|
146
|
+
common names <eo#common-band-names>`.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Optional[str]
|
|
150
|
+
"""
|
|
151
|
+
return self.properties.get("common_name")
|
|
152
|
+
|
|
153
|
+
@common_name.setter
|
|
154
|
+
def common_name(self, v: str | None) -> None:
|
|
155
|
+
if v is not None:
|
|
156
|
+
self.properties["common_name"] = v
|
|
157
|
+
else:
|
|
158
|
+
self.properties.pop("common_name", None)
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def description(self) -> str | None:
|
|
162
|
+
"""Get or sets the description to fully explain the band. CommonMark 0.29
|
|
163
|
+
syntax MAY be used for rich text representation.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
str
|
|
167
|
+
"""
|
|
168
|
+
return self.properties.get("description")
|
|
169
|
+
|
|
170
|
+
@description.setter
|
|
171
|
+
def description(self, v: str | None) -> None:
|
|
172
|
+
if v is not None:
|
|
173
|
+
self.properties["description"] = v
|
|
174
|
+
else:
|
|
175
|
+
self.properties.pop("description", None)
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def center_wavelength(self) -> float | None:
|
|
179
|
+
"""Get or sets the center wavelength of the band, in micrometers (μm).
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
float
|
|
183
|
+
"""
|
|
184
|
+
return self.properties.get("center_wavelength")
|
|
185
|
+
|
|
186
|
+
@center_wavelength.setter
|
|
187
|
+
def center_wavelength(self, v: float | None) -> None:
|
|
188
|
+
if v is not None:
|
|
189
|
+
self.properties["center_wavelength"] = v
|
|
190
|
+
else:
|
|
191
|
+
self.properties.pop("center_wavelength", None)
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def full_width_half_max(self) -> float | None:
|
|
195
|
+
"""Get or sets the full width at half maximum (FWHM). The width of the band,
|
|
196
|
+
as measured at half the maximum transmission, in micrometers (μm).
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
[float]
|
|
200
|
+
"""
|
|
201
|
+
return self.properties.get("full_width_half_max")
|
|
202
|
+
|
|
203
|
+
@full_width_half_max.setter
|
|
204
|
+
def full_width_half_max(self, v: float | None) -> None:
|
|
205
|
+
if v is not None:
|
|
206
|
+
self.properties["full_width_half_max"] = v
|
|
207
|
+
else:
|
|
208
|
+
self.properties.pop("full_width_half_max", None)
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def solar_illumination(self) -> float | None:
|
|
212
|
+
"""Get or sets the solar illumination of the band,
|
|
213
|
+
as measured at half the maximum transmission, in W/m2/micrometers.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
[float]
|
|
217
|
+
"""
|
|
218
|
+
return self.properties.get("solar_illumination")
|
|
219
|
+
|
|
220
|
+
@solar_illumination.setter
|
|
221
|
+
def solar_illumination(self, v: float | None) -> None:
|
|
222
|
+
if v is not None:
|
|
223
|
+
self.properties["solar_illumination"] = v
|
|
224
|
+
else:
|
|
225
|
+
self.properties.pop("solar_illumination", None)
|
|
226
|
+
|
|
227
|
+
def __repr__(self) -> str:
|
|
228
|
+
return f"<Band name={self.properties.get('name')}>"
|
|
229
|
+
|
|
230
|
+
def to_dict(self) -> dict[str, Any]:
|
|
231
|
+
"""Returns this band as a dictionary.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
dict: The serialization of this Band.
|
|
235
|
+
"""
|
|
236
|
+
return self.properties
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def band_range(common_name: str) -> tuple[float, float] | None:
|
|
240
|
+
"""Gets the band range for a common band name.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
common_name : The common band name. Must be one of the :stac-ext:`list of
|
|
244
|
+
accepted common names <eo#common-band-names>`.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Tuple[float, float] or None: The band range for this name as (min, max), or
|
|
248
|
+
None if this is not a recognized common name.
|
|
249
|
+
"""
|
|
250
|
+
name_to_range = {
|
|
251
|
+
"coastal": (0.40, 0.45),
|
|
252
|
+
"blue": (0.45, 0.50),
|
|
253
|
+
"green": (0.50, 0.60),
|
|
254
|
+
"red": (0.60, 0.70),
|
|
255
|
+
"yellow": (0.58, 0.62),
|
|
256
|
+
"pan": (0.50, 0.70),
|
|
257
|
+
"rededge": (0.70, 0.75),
|
|
258
|
+
"nir": (0.75, 1.00),
|
|
259
|
+
"nir08": (0.75, 0.90),
|
|
260
|
+
"nir09": (0.85, 1.05),
|
|
261
|
+
"cirrus": (1.35, 1.40),
|
|
262
|
+
"swir16": (1.55, 1.75),
|
|
263
|
+
"swir22": (2.10, 2.30),
|
|
264
|
+
"lwir": (10.5, 12.5),
|
|
265
|
+
"lwir11": (10.5, 11.5),
|
|
266
|
+
"lwir12": (11.5, 12.5),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return name_to_range.get(common_name)
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def band_description(common_name: str) -> str | None:
|
|
273
|
+
"""Returns a description of the band for one with a common name.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
common_name : The common band name. Must be one of the :stac-ext:`list of
|
|
277
|
+
accepted common names <eo#common-band-names>`.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
str or None: If a recognized common name, returns a description including
|
|
281
|
+
the band range. Otherwise, returns None.
|
|
282
|
+
"""
|
|
283
|
+
r = Band.band_range(common_name)
|
|
284
|
+
if r is not None:
|
|
285
|
+
return f"Common name: {common_name}, Range: {r[0]} to {r[1]}"
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class EOExtension(
|
|
290
|
+
Generic[T],
|
|
291
|
+
PropertiesExtension,
|
|
292
|
+
ExtensionManagementMixin[pystac.Item | pystac.Collection],
|
|
293
|
+
):
|
|
294
|
+
"""An abstract class that can be used to extend the properties of an
|
|
295
|
+
:class:`~pystac.Item` or :class:`~pystac.Asset` with properties from the
|
|
296
|
+
:stac-ext:`Electro-Optical Extension <eo>`. This class is generic over the type of
|
|
297
|
+
STAC Object to be extended (e.g. :class:`~pystac.Item`,
|
|
298
|
+
:class:`~pystac.Asset`).
|
|
299
|
+
|
|
300
|
+
To create a concrete instance of :class:`EOExtension`, use the
|
|
301
|
+
:meth:`EOExtension.ext` method. For example:
|
|
302
|
+
|
|
303
|
+
.. code-block:: python
|
|
304
|
+
|
|
305
|
+
>>> item: pystac.Item = ...
|
|
306
|
+
>>> eo_ext = EOExtension.ext(item)
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
name: Literal["eo"] = "eo"
|
|
310
|
+
|
|
311
|
+
def apply(
|
|
312
|
+
self,
|
|
313
|
+
bands: list[Band] | None = None,
|
|
314
|
+
cloud_cover: float | None = None,
|
|
315
|
+
snow_cover: float | None = None,
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Applies Electro-Optical Extension properties to the extended
|
|
318
|
+
:class:`~pystac.Item` or :class:`~pystac.Asset`.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
bands : A list of available bands where each item is a :class:`~Band`
|
|
322
|
+
object. If given, requires at least one band.
|
|
323
|
+
cloud_cover : The estimate of cloud cover as a percentage
|
|
324
|
+
(0-100) of the entire scene. If not available the field should not
|
|
325
|
+
be provided.
|
|
326
|
+
snow_cover : The estimate of snow cover as a percentage
|
|
327
|
+
(0-100) of the entire scene. If not available the field should not
|
|
328
|
+
be provided.
|
|
329
|
+
"""
|
|
330
|
+
self.bands = bands
|
|
331
|
+
self.cloud_cover = validated_percentage(cloud_cover)
|
|
332
|
+
self.snow_cover = validated_percentage(snow_cover)
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def bands(self) -> list[Band] | None:
|
|
336
|
+
"""Gets or sets a list of available bands where each item is a :class:`~Band`
|
|
337
|
+
object (or ``None`` if no bands have been set). If not available the field
|
|
338
|
+
should not be provided.
|
|
339
|
+
"""
|
|
340
|
+
return self._get_bands()
|
|
341
|
+
|
|
342
|
+
@bands.setter
|
|
343
|
+
def bands(self, v: list[Band] | None) -> None:
|
|
344
|
+
self._set_property(
|
|
345
|
+
BANDS_PROP, map_opt(lambda bands: [b.to_dict() for b in bands], v)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _get_bands(self) -> list[Band] | None:
|
|
349
|
+
return map_opt(
|
|
350
|
+
lambda bands: [Band(b) for b in bands],
|
|
351
|
+
self._get_property(BANDS_PROP, list[dict[str, Any]]),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def cloud_cover(self) -> float | None:
|
|
356
|
+
"""Get or sets the estimate of cloud cover as a percentage
|
|
357
|
+
(0-100) of the entire scene. If not available the field should not be provided.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
float or None
|
|
361
|
+
"""
|
|
362
|
+
return self._get_property(CLOUD_COVER_PROP, float)
|
|
363
|
+
|
|
364
|
+
@cloud_cover.setter
|
|
365
|
+
def cloud_cover(self, v: float | None) -> None:
|
|
366
|
+
self._set_property(CLOUD_COVER_PROP, validated_percentage(v), pop_if_none=True)
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def snow_cover(self) -> float | None:
|
|
370
|
+
"""Get or sets the estimate of snow cover as a percentage
|
|
371
|
+
(0-100) of the entire scene. If not available the field should not be provided.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
float or None
|
|
375
|
+
"""
|
|
376
|
+
return self._get_property(SNOW_COVER_PROP, float)
|
|
377
|
+
|
|
378
|
+
@snow_cover.setter
|
|
379
|
+
def snow_cover(self, v: float | None) -> None:
|
|
380
|
+
self._set_property(SNOW_COVER_PROP, validated_percentage(v), pop_if_none=True)
|
|
381
|
+
|
|
382
|
+
@classmethod
|
|
383
|
+
def get_schema_uri(cls) -> str:
|
|
384
|
+
return SCHEMA_URI
|
|
385
|
+
|
|
386
|
+
@classmethod
|
|
387
|
+
def get_schema_uris(cls) -> list[str]:
|
|
388
|
+
import warnings
|
|
389
|
+
|
|
390
|
+
warnings.warn(
|
|
391
|
+
"get_schema_uris is deprecated and will be removed in v2",
|
|
392
|
+
DeprecationWarning,
|
|
393
|
+
)
|
|
394
|
+
return SCHEMA_URIS
|
|
395
|
+
|
|
396
|
+
@classmethod
|
|
397
|
+
def ext(cls, obj: T, add_if_missing: bool = False) -> EOExtension[T]:
|
|
398
|
+
"""Extends the given STAC Object with properties from the
|
|
399
|
+
:stac-ext:`Electro-Optical Extension <eo>`.
|
|
400
|
+
|
|
401
|
+
This extension can be applied to instances of :class:`~pystac.Item` or
|
|
402
|
+
:class:`~pystac.Asset`.
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
|
|
406
|
+
pystac.ExtensionTypeError : If an invalid object type is passed.
|
|
407
|
+
"""
|
|
408
|
+
if isinstance(obj, pystac.Item):
|
|
409
|
+
cls.ensure_has_extension(obj, add_if_missing)
|
|
410
|
+
return cast(EOExtension[T], ItemEOExtension(obj))
|
|
411
|
+
elif isinstance(obj, pystac.Asset):
|
|
412
|
+
cls.ensure_owner_has_extension(obj, add_if_missing)
|
|
413
|
+
return cast(EOExtension[T], AssetEOExtension(obj))
|
|
414
|
+
elif isinstance(obj, pystac.ItemAssetDefinition):
|
|
415
|
+
cls.ensure_owner_has_extension(obj, add_if_missing)
|
|
416
|
+
return cast(EOExtension[T], ItemAssetsEOExtension(obj))
|
|
417
|
+
else:
|
|
418
|
+
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
|
|
419
|
+
|
|
420
|
+
@classmethod
|
|
421
|
+
def summaries(
|
|
422
|
+
cls, obj: pystac.Collection, add_if_missing: bool = False
|
|
423
|
+
) -> SummariesEOExtension:
|
|
424
|
+
"""Returns the extended summaries object for the given collection."""
|
|
425
|
+
cls.ensure_has_extension(obj, add_if_missing)
|
|
426
|
+
return SummariesEOExtension(obj)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class ItemEOExtension(EOExtension[pystac.Item]):
|
|
430
|
+
"""A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Item`
|
|
431
|
+
that extends the properties of the Item to include properties defined in the
|
|
432
|
+
:stac-ext:`Electro-Optical Extension <eo>`.
|
|
433
|
+
|
|
434
|
+
This class should generally not be instantiated directly. Instead, call
|
|
435
|
+
:meth:`EOExtension.ext` on an :class:`~pystac.Item` to extend it.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
item: pystac.Item
|
|
439
|
+
"""The :class:`~pystac.Item` being extended."""
|
|
440
|
+
|
|
441
|
+
properties: dict[str, Any]
|
|
442
|
+
"""The :class:`~pystac.Item` properties, including extension properties."""
|
|
443
|
+
|
|
444
|
+
def __init__(self, item: pystac.Item):
|
|
445
|
+
self.item = item
|
|
446
|
+
self.properties = item.properties
|
|
447
|
+
|
|
448
|
+
def _get_bands(self) -> list[Band] | None:
|
|
449
|
+
"""Get or sets a list of :class:`~pystac.Band` objects that represent
|
|
450
|
+
the available bands.
|
|
451
|
+
"""
|
|
452
|
+
bands = self._get_property(BANDS_PROP, list[dict[str, Any]])
|
|
453
|
+
|
|
454
|
+
# get assets with eo:bands even if not in item
|
|
455
|
+
if bands is None:
|
|
456
|
+
asset_bands: list[dict[str, Any]] = []
|
|
457
|
+
for _, value in self.item.get_assets().items():
|
|
458
|
+
if BANDS_PROP in value.extra_fields:
|
|
459
|
+
asset_bands.extend(
|
|
460
|
+
cast(list[dict[str, Any]], value.extra_fields.get(BANDS_PROP))
|
|
461
|
+
)
|
|
462
|
+
if any(asset_bands):
|
|
463
|
+
bands = asset_bands
|
|
464
|
+
|
|
465
|
+
if bands is not None:
|
|
466
|
+
return [Band(b) for b in bands]
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
def get_assets(
|
|
470
|
+
self,
|
|
471
|
+
name: str | None = None,
|
|
472
|
+
common_name: str | None = None,
|
|
473
|
+
) -> dict[str, pystac.Asset]:
|
|
474
|
+
"""Get the item's assets where eo:bands are defined.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
name: If set, filter the assets such that only those with a
|
|
478
|
+
matching ``eo:band.name`` are returned.
|
|
479
|
+
common_name: If set, filter the assets such that only those with a matching
|
|
480
|
+
``eo:band.common_name`` are returned.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Dict[str, Asset]: A dictionary of assets that match ``name``
|
|
484
|
+
and/or ``common_name`` if set or else all of this item's assets were
|
|
485
|
+
eo:bands are defined.
|
|
486
|
+
"""
|
|
487
|
+
kwargs = {"name": name, "common_name": common_name}
|
|
488
|
+
return {
|
|
489
|
+
key: asset
|
|
490
|
+
for key, asset in self.item.get_assets().items()
|
|
491
|
+
if BANDS_PROP in asset.extra_fields
|
|
492
|
+
and all(
|
|
493
|
+
v is None or any(v == b.get(k) for b in asset.extra_fields[BANDS_PROP])
|
|
494
|
+
for k, v in kwargs.items()
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
def __repr__(self) -> str:
|
|
499
|
+
return f"<ItemEOExtension Item id={self.item.id}>"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class AssetEOExtension(EOExtension[pystac.Asset]):
|
|
503
|
+
"""A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Asset`
|
|
504
|
+
that extends the Asset fields to include properties defined in the
|
|
505
|
+
:stac-ext:`Electro-Optical Extension <eo>`.
|
|
506
|
+
|
|
507
|
+
This class should generally not be instantiated directly. Instead, call
|
|
508
|
+
:meth:`EOExtension.ext` on an :class:`~pystac.Asset` to extend it.
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
asset_href: str
|
|
512
|
+
"""The ``href`` value of the :class:`~pystac.Asset` being extended."""
|
|
513
|
+
|
|
514
|
+
properties: dict[str, Any]
|
|
515
|
+
"""The :class:`~pystac.Asset` fields, including extension properties."""
|
|
516
|
+
|
|
517
|
+
additional_read_properties: Iterable[dict[str, Any]] | None = None
|
|
518
|
+
"""If present, this will be a list containing 1 dictionary representing the
|
|
519
|
+
properties of the owning :class:`~pystac.Item`."""
|
|
520
|
+
|
|
521
|
+
def _get_bands(self) -> list[Band] | None:
|
|
522
|
+
if BANDS_PROP not in self.properties:
|
|
523
|
+
return None
|
|
524
|
+
return list(
|
|
525
|
+
map(
|
|
526
|
+
lambda band: Band(band),
|
|
527
|
+
cast(list[dict[str, Any]], self.properties.get(BANDS_PROP)),
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
def __init__(self, asset: pystac.Asset):
|
|
532
|
+
self.asset_href = asset.href
|
|
533
|
+
self.properties = asset.extra_fields
|
|
534
|
+
if asset.owner and isinstance(asset.owner, pystac.Item):
|
|
535
|
+
self.additional_read_properties = [asset.owner.properties]
|
|
536
|
+
|
|
537
|
+
def __repr__(self) -> str:
|
|
538
|
+
return f"<AssetEOExtension Asset href={self.asset_href}>"
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
class ItemAssetsEOExtension(EOExtension[pystac.ItemAssetDefinition]):
|
|
542
|
+
properties: dict[str, Any]
|
|
543
|
+
asset_defn: pystac.ItemAssetDefinition
|
|
544
|
+
|
|
545
|
+
def _get_bands(self) -> list[Band] | None:
|
|
546
|
+
if BANDS_PROP not in self.properties:
|
|
547
|
+
return None
|
|
548
|
+
return list(
|
|
549
|
+
map(
|
|
550
|
+
lambda band: Band(band),
|
|
551
|
+
cast(list[dict[str, Any]], self.properties.get(BANDS_PROP)),
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
def __init__(self, item_asset: pystac.ItemAssetDefinition):
|
|
556
|
+
self.asset_defn = item_asset
|
|
557
|
+
self.properties = item_asset.properties
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class SummariesEOExtension(SummariesExtension):
|
|
561
|
+
"""A concrete implementation of :class:`~pystac.extensions.base.SummariesExtension`
|
|
562
|
+
that extends the ``summaries`` field of a :class:`~pystac.Collection` to include
|
|
563
|
+
properties defined in the :stac-ext:`Electro-Optical Extension <eo>`.
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def bands(self) -> list[Band] | None:
|
|
568
|
+
"""Get or sets the summary of :attr:`EOExtension.bands` values
|
|
569
|
+
for this Collection.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
return map_opt(
|
|
573
|
+
lambda bands: [Band(b) for b in bands],
|
|
574
|
+
self.summaries.get_list(BANDS_PROP),
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
@bands.setter
|
|
578
|
+
def bands(self, v: list[Band] | None) -> None:
|
|
579
|
+
self._set_summary(BANDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v))
|
|
580
|
+
|
|
581
|
+
@property
|
|
582
|
+
def cloud_cover(self) -> RangeSummary[float] | None:
|
|
583
|
+
"""Get or sets the summary of :attr:`EOExtension.cloud_cover` values
|
|
584
|
+
for this Collection.
|
|
585
|
+
"""
|
|
586
|
+
return self.summaries.get_range(CLOUD_COVER_PROP)
|
|
587
|
+
|
|
588
|
+
@cloud_cover.setter
|
|
589
|
+
def cloud_cover(self, v: RangeSummary[float] | None) -> None:
|
|
590
|
+
self._set_summary(CLOUD_COVER_PROP, v)
|
|
591
|
+
|
|
592
|
+
@property
|
|
593
|
+
def snow_cover(self) -> RangeSummary[float] | None:
|
|
594
|
+
"""Get or sets the summary of :attr:`EOExtension.snow_cover` values
|
|
595
|
+
for this Collection.
|
|
596
|
+
"""
|
|
597
|
+
return self.summaries.get_range(SNOW_COVER_PROP)
|
|
598
|
+
|
|
599
|
+
@snow_cover.setter
|
|
600
|
+
def snow_cover(self, v: RangeSummary[float] | None) -> None:
|
|
601
|
+
self._set_summary(SNOW_COVER_PROP, v)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
class EOExtensionHooks(ExtensionHooks):
|
|
605
|
+
schema_uri: str = SCHEMA_URI
|
|
606
|
+
prev_extension_ids = {
|
|
607
|
+
"eo",
|
|
608
|
+
*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI],
|
|
609
|
+
}
|
|
610
|
+
stac_object_types = {pystac.STACObjectType.ITEM}
|
|
611
|
+
|
|
612
|
+
def migrate(
|
|
613
|
+
self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription
|
|
614
|
+
) -> None:
|
|
615
|
+
if version < "0.9":
|
|
616
|
+
# Some eo fields became common_metadata
|
|
617
|
+
if (
|
|
618
|
+
"eo:platform" in obj["properties"]
|
|
619
|
+
and "platform" not in obj["properties"]
|
|
620
|
+
):
|
|
621
|
+
obj["properties"]["platform"] = obj["properties"]["eo:platform"]
|
|
622
|
+
del obj["properties"]["eo:platform"]
|
|
623
|
+
|
|
624
|
+
if (
|
|
625
|
+
"eo:instrument" in obj["properties"]
|
|
626
|
+
and "instruments" not in obj["properties"]
|
|
627
|
+
):
|
|
628
|
+
obj["properties"]["instruments"] = [obj["properties"]["eo:instrument"]]
|
|
629
|
+
del obj["properties"]["eo:instrument"]
|
|
630
|
+
|
|
631
|
+
if (
|
|
632
|
+
"eo:constellation" in obj["properties"]
|
|
633
|
+
and "constellation" not in obj["properties"]
|
|
634
|
+
):
|
|
635
|
+
obj["properties"]["constellation"] = obj["properties"][
|
|
636
|
+
"eo:constellation"
|
|
637
|
+
]
|
|
638
|
+
del obj["properties"]["eo:constellation"]
|
|
639
|
+
|
|
640
|
+
# Some eo fields became view extension fields
|
|
641
|
+
eo_to_view_fields = [
|
|
642
|
+
"off_nadir",
|
|
643
|
+
"azimuth",
|
|
644
|
+
"incidence_angle",
|
|
645
|
+
"sun_azimuth",
|
|
646
|
+
"sun_elevation",
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
for field in eo_to_view_fields:
|
|
650
|
+
if f"eo:{field}" in obj["properties"]:
|
|
651
|
+
if "stac_extensions" not in obj:
|
|
652
|
+
obj["stac_extensions"] = []
|
|
653
|
+
if view.SCHEMA_URI not in obj["stac_extensions"]:
|
|
654
|
+
obj["stac_extensions"].append(view.SCHEMA_URI)
|
|
655
|
+
if f"view:{field}" not in obj["properties"]:
|
|
656
|
+
obj["properties"][f"view:{field}"] = obj["properties"][
|
|
657
|
+
f"eo:{field}"
|
|
658
|
+
]
|
|
659
|
+
del obj["properties"][f"eo:{field}"]
|
|
660
|
+
|
|
661
|
+
# eo:epsg became proj:epsg in Projection Extension <2.0.0 and became
|
|
662
|
+
# proj:code in Projection Extension 2.0.0
|
|
663
|
+
eo_epsg = PREFIX + "epsg"
|
|
664
|
+
proj_epsg = projection.PREFIX + "epsg"
|
|
665
|
+
proj_code = projection.PREFIX + "code"
|
|
666
|
+
if (
|
|
667
|
+
eo_epsg in obj["properties"]
|
|
668
|
+
and proj_epsg not in obj["properties"]
|
|
669
|
+
and proj_code not in obj["properties"]
|
|
670
|
+
):
|
|
671
|
+
obj["stac_extensions"] = obj.get("stac_extensions", [])
|
|
672
|
+
if set(obj["stac_extensions"]).intersection(
|
|
673
|
+
projection.ProjectionExtensionHooks.pre_2
|
|
674
|
+
):
|
|
675
|
+
obj["properties"][proj_epsg] = obj["properties"].pop(eo_epsg)
|
|
676
|
+
else:
|
|
677
|
+
obj["properties"][proj_code] = (
|
|
678
|
+
f"EPSG:{obj['properties'].pop(eo_epsg)}"
|
|
679
|
+
)
|
|
680
|
+
if not projection.ProjectionExtensionHooks().has_extension(obj):
|
|
681
|
+
obj["stac_extensions"].append(
|
|
682
|
+
projection.ProjectionExtension.get_schema_uri()
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
if not any(prop.startswith(PREFIX) for prop in obj["properties"]):
|
|
686
|
+
obj["stac_extensions"].remove(EOExtension.get_schema_uri())
|
|
687
|
+
|
|
688
|
+
if version < "1.0.0-beta.1" and info.object_type == pystac.STACObjectType.ITEM:
|
|
689
|
+
# gsd moved from eo to common metadata
|
|
690
|
+
if "eo:gsd" in obj["properties"]:
|
|
691
|
+
obj["properties"]["gsd"] = obj["properties"]["eo:gsd"]
|
|
692
|
+
del obj["properties"]["eo:gsd"]
|
|
693
|
+
|
|
694
|
+
# The way bands were declared in assets changed.
|
|
695
|
+
# In 1.0.0-beta.1 they are inlined into assets as
|
|
696
|
+
# opposed to having indices back into a property-level array.
|
|
697
|
+
if "eo:bands" in obj["properties"]:
|
|
698
|
+
bands = obj["properties"]["eo:bands"]
|
|
699
|
+
for asset in obj["assets"].values():
|
|
700
|
+
if "eo:bands" in asset:
|
|
701
|
+
new_bands: list[dict[str, Any]] = []
|
|
702
|
+
for band_index in asset["eo:bands"]:
|
|
703
|
+
new_bands.append(bands[band_index])
|
|
704
|
+
asset["eo:bands"] = new_bands
|
|
705
|
+
|
|
706
|
+
super().migrate(obj, version, info)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
EO_EXTENSION_HOOKS: ExtensionHooks = EOExtensionHooks()
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pystac-ext-eo
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Electro-Optical extension for PySTAC
|
|
5
|
+
Project-URL: Documentation, https://pystac.readthedocs.io
|
|
6
|
+
Project-URL: Repository, https://github.com/stac-utils/pystac
|
|
7
|
+
Project-URL: Issues, https://github.com/stac-utils/pystac/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/stac-utils/pystac/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Discussions, https://github.com/radiantearth/stac-spec/discussions/categories/stac-software
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
Keywords: STAC,catalog,eo,imagery,pystac,raster
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: pystac-core
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# pystac-ext-eo
|
|
26
|
+
|
|
27
|
+
[PySTAC](https://pypi.org/project/pystac/) extension package for the [Electro-Optical Extension](https://github.com/stac-extensions/eo).
|
|
28
|
+
This extension provides fields for describing electro-optical sensor data, including spectral band information, cloud cover, and snow cover.
|
|
29
|
+
|
|
30
|
+
## Supported versions
|
|
31
|
+
|
|
32
|
+
- [v1.1.0](https://stac-extensions.github.io/eo/v1.1.0/schema.json)
|
|
33
|
+
- [v1.0.0](https://stac-extensions.github.io/eo/v1.0.0/schema.json)
|
|
34
|
+
|
|
35
|
+
## Versioning
|
|
36
|
+
|
|
37
|
+
This package's version corresponds to the version of the extension specification it targets.
|
|
38
|
+
When we release updates to the package code without changing the target extension version, we use [post releases](https://packaging.python.org/en/latest/discussions/versioning/#post-releases), e.g. `1.1.0.post1`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
pystac/extensions/eo.py,sha256=eUp78damK68KVo0dMeZCtHAA9kelGD_AkYEY441oN1w,25788
|
|
2
|
+
pystac/extensions/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
pystac_ext_eo-1.1.0.dist-info/METADATA,sha256=tJcR04d1yDcc3IluvPl-0JLq5E8_bJuo-v8Ian1VAtI,1869
|
|
4
|
+
pystac_ext_eo-1.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
pystac_ext_eo-1.1.0.dist-info/RECORD,,
|