simplestac 2.0.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.
simplestac/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+
3
+ try:
4
+ __version__ = version("simplestac")
5
+ except PackageNotFoundError:
6
+ # package is not installed
7
+ pass
simplestac/extents.py ADDED
@@ -0,0 +1,223 @@
1
+ """
2
+ Deal with STAC Extents.
3
+
4
+ # AutoSpatialExtent
5
+
6
+ The vanilla `pystac.SpatialExtent` enables to describe a spatial extent
7
+ from several bounding boxes. While this is useful, sometimes we want to
8
+ merge together bounding boxes that are partially overlapping. For instance,
9
+ we can take the example of the France mainland, covered by multiple remote
10
+ sensing products that are generally partially overlapping, and the Corse
11
+ island that is also covered by a number of RS products, but spatially
12
+ disjoint from the France mainland RS products bounding boxes. In this
13
+ particular exemple, we would like to regroup all RS products bounding
14
+ boxes so that there is one bbox for the France mainland, and another
15
+ bbox for the Corse island. This is particularly useful when a STAC
16
+ collection covers sparsely a broad area (e.g. worldwide), with several
17
+ isolated regions.
18
+
19
+ The `AutoSpatialExtent` is an extension of the `pystac.SpatialExtent`.
20
+
21
+ Instances are initialized with the same arguments as `pystac.SpatialExtent`.
22
+ Internally, bounding boxes lists are processed at initialisation, so all
23
+ partially overlapping bounding boxes are merged and updated as a single one.
24
+
25
+ # AutoTemporalExtent
26
+
27
+ The `AutoTemporalExtent` is an extension of the `pystac.TemporalExtent`.
28
+ It computes the date min and date max of a set of dates or dates ranges.
29
+
30
+ """
31
+ import pystac
32
+ from dataclasses import dataclass
33
+ from datetime import datetime
34
+ from typing import Union, List
35
+ from numbers import Number
36
+ from annotated_types import Predicate
37
+ from typing_extensions import Annotated
38
+
39
+
40
+ def is_bbox(l: List) -> bool:
41
+ """
42
+ Predicate to test is the input list represents a bounding box in WGS84.
43
+
44
+ Args:
45
+ l: a list of `Number`
46
+
47
+ Returns:
48
+
49
+ """
50
+ if len(l) == 4:
51
+ if all(isinstance(i, Number) for i in l):
52
+ if -180 <= l[0] and -90 <= l[1] and l[2] <= 180 and l[3] <= 90:
53
+ if l[0] <= l[2] and l[1] <= l[3]:
54
+ return True
55
+ return False
56
+
57
+
58
+ Bbox = Annotated[list, Predicate(is_bbox)]
59
+
60
+
61
+ @dataclass
62
+ class SmartBbox:
63
+ """
64
+ Small class to work with a single 2D bounding box.
65
+ """
66
+ coords: Bbox = None # [xmin, ymin, xmax, ymax]
67
+
68
+ def touches(self, other: "SmartBbox") -> bool:
69
+ """
70
+ Overlap test.
71
+
72
+ Args:
73
+ other: other bounding box
74
+
75
+ Returns:
76
+ True if the other bounding box touches, else False.
77
+
78
+ """
79
+ xmin, ymin, xmax, ymax = self.coords
80
+ o_xmin, o_ymin, o_xmax, o_ymax = other.coords
81
+
82
+ if xmax < o_xmin or o_xmax < xmin or ymax < o_ymin or o_ymax < ymin:
83
+ return False
84
+ return True
85
+
86
+ def update(self, other: "SmartBbox"):
87
+ """
88
+ Update the coordinates of the Bbox. Modifies itself inplace.
89
+
90
+ Args:
91
+ other: other bounding box
92
+
93
+ """
94
+ if not self.coords:
95
+ self.coords = other.coords
96
+ else:
97
+ self.coords = [
98
+ min(self.coords[0], other.coords[0]),
99
+ min(self.coords[1], other.coords[1]),
100
+ max(self.coords[2], other.coords[2]),
101
+ max(self.coords[3], other.coords[3])
102
+ ]
103
+
104
+
105
+ def clusterize_bboxes(bboxes: list[Bbox]) -> list[Bbox]:
106
+ """
107
+ Computes a list of bounding boxes regrouping all overlapping ones.
108
+
109
+ Args:
110
+ bboxes: 2D bounding boxes (list of int of float)
111
+
112
+ Returns:
113
+ list of 2D bounding boxes (list of int of float)
114
+
115
+ """
116
+ bboxes = [SmartBbox(bbox) for bbox in bboxes]
117
+ clusters = [bboxes.pop()]
118
+
119
+ while bboxes:
120
+ bbox = bboxes.pop()
121
+ inter_clusters = [
122
+ i for i, cluster in enumerate(clusters) if bbox.touches(cluster)
123
+ ]
124
+ if inter_clusters:
125
+ # We merge all intersecting clusters into one
126
+ clusters[inter_clusters[0]].update(bbox)
127
+ for i in inter_clusters[1:]:
128
+ clusters[inter_clusters[0]].update(clusters[i])
129
+ clusters = [
130
+ cluster
131
+ for i, cluster in enumerate(clusters)
132
+ if i not in inter_clusters[1:]
133
+ ]
134
+ else:
135
+ clusters.append(bbox)
136
+
137
+ return [cluster.coords for cluster in clusters]
138
+
139
+
140
+ class AutoSpatialExtent(pystac.SpatialExtent):
141
+ """
142
+ Custom extension of pystac.SpatialExtent that automatically compute bboxes.
143
+ """
144
+
145
+ def __init__(self, *args, **kwargs):
146
+ """
147
+ Initializer. Clusterize boxes after the original initializer.
148
+
149
+ Args:
150
+ *args: args
151
+ **kwargs: keyword args
152
+
153
+ """
154
+ super().__init__(*args, **kwargs)
155
+ self.clusterize_bboxes()
156
+
157
+ def update(self, other: pystac.SpatialExtent | Bbox):
158
+ """
159
+ Updates itself with a new spatial extent or bounding box. Modifies
160
+ inplace `self.bboxes`.
161
+
162
+ Args:
163
+ other: spatial extent or bbox coordinates
164
+
165
+ """
166
+ is_spat_ext = isinstance(other, pystac.SpatialExtent)
167
+ self.bboxes += other.bboxes if is_spat_ext else other
168
+ self.clusterize_bboxes()
169
+
170
+ def clusterize_bboxes(self):
171
+ """
172
+ Regroup the bounding boxes that overlap. Modifies inplace `self.bboxes`.
173
+
174
+ """
175
+ self.bboxes = clusterize_bboxes(self.bboxes)
176
+
177
+
178
+ class AutoTemporalExtent(pystac.TemporalExtent):
179
+ """
180
+ Custom extension of pystac.TemporalExtent that automatically updates itself
181
+ with another date or temporal extent provided.
182
+ """
183
+
184
+ def __init__(self, *args, **kwargs):
185
+ """
186
+ Initializer. Regroup all intervals into a single one.
187
+
188
+ Args:
189
+ *args: args
190
+ **kwargs: keyword args
191
+
192
+ """
193
+ super().__init__(*args, **kwargs)
194
+ self.make_single_interval()
195
+
196
+ def update(self, other: Union[pystac.TemporalExtent, datetime]):
197
+ """
198
+ Updates itself with a new temporal extent of date. Modifies inplace
199
+ `self.intervals`.
200
+
201
+ Args:
202
+ other: temporal extent or datetime
203
+
204
+ """
205
+ is_temp_ext = isinstance(other, pystac.TemporalExtent)
206
+ intervals = other.intervals if is_temp_ext else [[other, other]]
207
+ self.intervals += intervals
208
+ self.make_single_interval()
209
+
210
+ def make_single_interval(self):
211
+ all_dates = []
212
+ for dates_or_intervals in self.intervals:
213
+ # Because base class (`pystac.SpatialExtent`) converts everything
214
+ # into [[...]]
215
+ for date_or_interval in dates_or_intervals:
216
+ if isinstance(date_or_interval, (list, tuple)):
217
+ all_dates += [i for i in date_or_interval if i is not None]
218
+ elif isinstance(date_or_interval, datetime):
219
+ all_dates.append(date_or_interval)
220
+ else:
221
+ TypeError(f"Unsupported date/range of: {date_or_interval}")
222
+ self.intervals = \
223
+ [[min(all_dates), max(all_dates)]] if all_dates else [None, None]
@@ -0,0 +1,60 @@
1
+ {
2
+ "item": {
3
+ "pattern": "(S1[ABCD]_IW_GRD[FHM_]_1S[SDHV][HV]_[0-9]{8}T[0-9]{6}_[0-9]{8}T[0-9]{6}_[0-9]{6}_[0-9A-Z]{6}_[0-9A-Z]{4})",
4
+ "properties": {
5
+ "datetime": {
6
+ "pattern": "S1[ABCD]_IW_GRD[FHM_]_1S[SDHV][HV]_([0-9]{8}T[0-9]{6})_.*",
7
+ "format": "%Y%m%dT%H%M%S"
8
+ },
9
+ "start_datetime": {
10
+ "pattern": "S1[ABCD]_IW_GRD[FHM_]_1S[SDHV][HV]_([0-9]{8}T[0-9]{6})_.*",
11
+ "format": "%Y%m%dT%H%M%S"
12
+ },
13
+ "end_datetime": {
14
+ "pattern": "S1[ABCD]_IW_GRD[FHM_]_1S[SDHV][HV]_[0-9]{8}T[0-9]{6}_([0-9]{8}T[0-9]{6})_.*",
15
+ "format": "%Y%m%dT%H%M%S"
16
+ },
17
+ "title": {
18
+ "pattern": "(S1[ABCD]_IW_GRD[FHM_]_1S[SDHV][HV]_[0-9]{8}T[0-9]{6}_[0-9]{8}T[0-9]{6}_[0-9]{6}_[0-9A-Z]{6}_[0-9A-Z]{4})"
19
+ },
20
+ "platform": {
21
+ "pattern": "(S1[ABCD]).+"
22
+ },
23
+ "sar:instrument_mode": "IW",
24
+ "sar:frequency_band": "C",
25
+ "sar:observation_direction": "right",
26
+ "sar:polarizations": ["VH", "VV"],
27
+ "sar:product_type": "GRD",
28
+ "sat:absolute_orbit": {
29
+ "pattern" : "S1[ABCD]_IW_GRD[FHM_]_1S[SDHV][HV]_[0-9]{8}T[0-9]{6}_[0-9]{8}T[0-9]{6}_([0-9]{6})_[0-9A-Z]{6}_[0-9A-Z]{4}"
30
+ },
31
+ "s1:datatake_id": {
32
+ "pattern" : "S1[ABCD]_IW_GRD[FHM_]_1S[SDHV][HV]_[0-9]{8}T[0-9]{6}_[0-9]{8}T[0-9]{6}_[0-9]{6}_([0-9A-Z]{6})_[0-9A-Z]{4}"
33
+ },
34
+ "s1:product_identifier": {
35
+ "pattern" : "(S1[ABCD]_IW_GRD[FHM_]_1S[SDHV][HV]_[0-9]{8}T[0-9]{6}_[0-9]{8}T[0-9]{6}_[0-9]{6}_[0-9A-Z]{6}_[0-9A-Z]{4})"
36
+ },
37
+ "s1:resolution": {
38
+ "pattern": "S1[ABCD]_IW_GRD([FHM_])_1S[SDHV][HV]_[0-9]{8}T[0-9]{6}_[0-9]{8}T[0-9]{6}_[0-9]{6}_[0-9A-Z]{6}_[0-9A-Z]{4}"
39
+ }
40
+ },
41
+ "stac_extensions": ["sar", "sat"]
42
+ },
43
+ "item_assets": {
44
+ "vv": {
45
+ "pattern": "s1[abcd]-iw-grd-vv.+\\.tiff",
46
+ "roles": ["data"],
47
+ "eo:bands": [{
48
+ "name": "VV",
49
+ "description": "VV band: vertical transmit and vertical receive"
50
+ }]
51
+ },
52
+ "vh": {
53
+ "pattern": "s1[abcd]-iw-grd-vh.+\\.tiff",
54
+ "roles": ["data"],
55
+ "eo:bands": [{
56
+ "name": "VH",
57
+ "description": "VH band: vertical transmit and horizontal receive" }]
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "item": {
3
+ "pattern": "(S1[ABCD]_L1ORT_[0-9]{2}[A-Z]{3}_GAM_[A-Z]{3}_[0-9]{3}_[0-9]{8}T[0-9]{6})",
4
+ "properties": {
5
+ "datetime": {
6
+ "pattern": "S1[ABCD]_L1ORT_[0-9]{2}[A-Z]{3}_GAM_[A-Z]{3}_[0-9]{3}_([0-9]{8}T[0-9]{6})",
7
+ "format": "%Y%m%dT%H%M%S"
8
+ },
9
+ "start_datetime": {
10
+ "pattern": "S1[ABCD]_L1ORT_[0-9]{2}[A-Z]{3}_GAM_[A-Z]{3}_[0-9]{3}_([0-9]{8}T[0-9]{6})",
11
+ "format": "%Y%m%dT%H%M%S"
12
+ },
13
+ "end_datetime": {
14
+ "pattern": "S1[ABCD]_L1ORT_[0-9]{2}[A-Z]{3}_GAM_[A-Z]{3}_[0-9]{3}_([0-9]{8}T[0-9]{6})",
15
+ "format": "%Y%m%dT%H%M%S"
16
+ },
17
+ "title": {
18
+ "pattern": "(S1[ABCD]_L1ORT_[0-9]{2}[A-Z]{3}_GAM_[A-Z]{3}_[0-9]{3}_[0-9]{8}T[0-9]{6})"
19
+ },
20
+
21
+ "sat:orbit_state": {
22
+ "pattern": "S1[ABCD]_L1ORT_[0-9]{2}[A-Z]{3}_GAM_([A-Z]{3})_[0-9]{3}_[0-9]{8}T[0-9]{6}"
23
+ },
24
+ "sar:frequency_band": "C",
25
+ "sar:observation_direction": "right",
26
+ "sar:polarizations": ["VH", "VV"],
27
+ "sar:product_type": "GRD_S1Tiling",
28
+ "sar:instrument_mode": "IW",
29
+ "sat:relative_orbit": {
30
+ "pattern" : "S1[ABCD]_L1ORT_[0-9]{2}[A-Z]{3}_GAM_[A-Z]{3}_([0-9]{3})_[0-9]{8}T[0-9]{6}"
31
+ },
32
+ "grid:code": {
33
+ "pattern" : "S1[ABCD]_L1ORT_([0-9]{2}[A-Z]{3})_GAM_[A-Z]{3}_[0-9]{3}_[0-9]{8}T[0-9]{6}"
34
+ }
35
+ },
36
+ "stac_extensions": ["sar", "sat", "grid"]
37
+ },
38
+ "item_assets": {
39
+ "vv": {
40
+ "pattern": "S1[ABCD]_L1ORT_[0-9]{2}[A-Z]{3}_VV_GAM_[A-Z]{3}_[0-9]{3}_[0-9]{8}T[0-9]{6}\\.tif",
41
+ "roles": ["data"],
42
+ "eo:bands": [{
43
+ "name": "VV",
44
+ "description": "VV band: vertical transmit and vertical receive"
45
+ }]
46
+ },
47
+ "vh": {
48
+ "pattern": "S1[ABCD]_L1ORT_[0-9]{2}[A-Z]{3}_VH_GAM_[A-Z]{3}_[0-9]{3}_[0-9]{8}T[0-9]{6}\\.tif",
49
+ "roles": ["data"],
50
+ "eo:bands": [{
51
+ "name": "VH",
52
+ "description": "VH band: vertical transmit and horizontal receive" }]
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,166 @@
1
+ {
2
+ "item" : {
3
+ "pattern" : "(SENTINEL2[ABC]_[0-9]{8}-[0-9]{6}-[0-9]{3}_L2A_T[0-9A-Z]{5}_[A-Z]_V[0-9]-[0-9])$",
4
+ "properties": {
5
+ "datetime" : {
6
+ "pattern" : "SENTINEL2[ABC]_([0-9]{8}-[0-9]{6})-.*",
7
+ "format" : "%Y%m%d-%H%M%S"
8
+ },
9
+ "title" : {
10
+ "pattern" : "(SENTINEL2[ABC]_[0-9]{8}-[0-9]{6}-[0-9]{3}_L2A_T[0-9A-Z]{5}_[A-Z]_V[0-9]-[0-9])"
11
+ },
12
+ "grid:code" : {
13
+ "pattern" : ".+_T([0-9A-Z]{5})_.+"
14
+ },
15
+ "platform" : {
16
+ "pattern" : "(SENTINEL2[ABC]).+"
17
+ },
18
+ "constellation": "sentinel-2",
19
+ "instruments" : ["msi"],
20
+ "product:type" : "S2MSI2A"
21
+ },
22
+ "stac_extensions": ["grid"]
23
+ },
24
+ "item_assets" : {
25
+ "B02" : {
26
+ "pattern" : ".+_FRE_B2\\.tif",
27
+ "roles" : ["reflectance"],
28
+ "eo:bands" : [{
29
+ "name" : "B02",
30
+ "common_name" : "blue",
31
+ "center_wavelength": 0.4966,
32
+ "full_width_half_max": 0.098
33
+ }]
34
+ },
35
+ "B03" : {
36
+ "pattern" : ".+_FRE_B3\\.tif",
37
+ "roles" : ["reflectance"],
38
+ "eo:bands" : [{
39
+ "name" : "B03",
40
+ "common_name" : "green",
41
+ "center_wavelength": 0.56,
42
+ "full_width_half_max": 0.045
43
+ }]
44
+ },
45
+ "B04" : {
46
+ "pattern" : ".+_FRE_B4\\.tif",
47
+ "roles" : ["reflectance"],
48
+ "eo:bands" : [{
49
+ "name" : "B04",
50
+ "common_name" : "red",
51
+ "center_wavelength": 0.6645,
52
+ "full_width_half_max": 0.038
53
+ }]
54
+ },
55
+ "B05" : {
56
+ "pattern" : ".+_FRE_B5\\.tif",
57
+ "roles" : ["reflectance"],
58
+ "eo:bands" : [{
59
+ "name" : "B05",
60
+ "common_name" : "rededge",
61
+ "center_wavelength": 0.7039,
62
+ "full_width_half_max": 0.019
63
+ }]
64
+ },
65
+ "B06" : {
66
+ "pattern" : ".+_FRE_B6\\.tif",
67
+ "roles" : ["reflectance"],
68
+ "eo:bands" : [{
69
+ "name" : "B06",
70
+ "common_name" : "rededge",
71
+ "center_wavelength": 0.7402,
72
+ "full_width_half_max": 0.018
73
+ }]
74
+ },
75
+ "B07" : {
76
+ "pattern" : ".+_FRE_B7\\.tif",
77
+ "roles" : ["reflectance"],
78
+ "eo:bands" : [{
79
+ "name" : "B07",
80
+ "common_name" : "rededge",
81
+ "center_wavelength": 0.7825,
82
+ "full_width_half_max": 0.028
83
+ }]
84
+ },
85
+ "B08" : {
86
+ "pattern" : ".+_FRE_B8\\.tif",
87
+ "roles" : ["reflectance"],
88
+ "eo:bands" : [{
89
+ "name" : "B08",
90
+ "common_name" : "nir",
91
+ "center_wavelength": 0.8351,
92
+ "full_width_half_max": 0.145
93
+ }]
94
+ },
95
+ "B8A" : {
96
+ "pattern" : ".+_FRE_B8A\\.tif",
97
+ "roles" : ["reflectance"],
98
+ "eo:bands" : [{
99
+ "name" : "B8A",
100
+ "common_name" : "nir08",
101
+ "center_wavelength": 0.8648,
102
+ "full_width_half_max": 0.033
103
+ }]
104
+ },
105
+ "B11" : {
106
+ "pattern" : ".+_FRE_B11\\.tif",
107
+ "roles" : ["reflectance"],
108
+ "eo:bands" : [{
109
+ "name" : "B11",
110
+ "common_name" : "swir16",
111
+ "center_wavelength": 1.6137,
112
+ "full_width_half_max": 0.143
113
+ }]
114
+ },
115
+ "B12" : {
116
+ "pattern" : ".+_FRE_B12\\.tif",
117
+ "roles" : ["reflectance"],
118
+ "eo:bands" : [{
119
+ "name" : "B12",
120
+ "common_name" : "swir22",
121
+ "center_wavelength": 2.22024,
122
+ "full_width_half_max": 0.242
123
+ }]
124
+ },
125
+ "CLM_R1" : {
126
+ "pattern" : ".+_CLM_R1\\.tif",
127
+ "roles" : ["data"]
128
+ },
129
+ "CLM_R2" : {
130
+ "pattern" : ".+_CLM_R2\\.tif",
131
+ "roles" : ["data"]
132
+ },
133
+ "SAT_R1" : {
134
+ "pattern" : ".+_SAT_R1\\.tif",
135
+ "roles" : ["data"]
136
+ },
137
+ "SAT_R2" : {
138
+ "pattern" : ".+_SAT_R2\\.tif",
139
+ "roles" : ["data"]
140
+ },
141
+ "EDG_R1" : {
142
+ "pattern" : ".+_EDG_R1\\.tif",
143
+ "roles" : ["data"]
144
+ },
145
+ "EDG_R2" : {
146
+ "pattern" : ".+_EDG_R2\\.tif",
147
+ "roles" : ["data"]
148
+ },
149
+ "MG2_R1" : {
150
+ "pattern" : ".+_MG2_R1\\.tif",
151
+ "roles" : ["data"]
152
+ },
153
+ "MG2_R2" : {
154
+ "pattern" : ".+_MG2_R2\\.tif",
155
+ "roles" : ["data"]
156
+ },
157
+ "IAB_R1" : {
158
+ "pattern" : ".+_IAB_R1\\.tif",
159
+ "roles" : ["data"]
160
+ },
161
+ "IAB_R2" : {
162
+ "pattern" : ".+_IAB_R2\\.tif",
163
+ "roles" : ["data"]
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,17 @@
1
+ Common Name Band Range (μm) Landsat 5/7 Landsat 8 Sentinel 2 MODIS NAIP
2
+ coastal 0.40 - 0.45 1 1
3
+ blue 0.45 - 0.50 1 2 2 3 3
4
+ green 0.50 - 0.60 2 3 3 4 2
5
+ red 0.60 - 0.70 3 4 4 1 1
6
+ yellow 0.58 - 0.62
7
+ pan 0.50 - 0.70 8 (L7 only) 8
8
+ rededge 0.70 - 0.79 5, 6, 7
9
+ nir 0.75 - 1.00 4 8 2 4
10
+ nir08 0.75 - 0.90 5 8a
11
+ nir09 0.85 - 1.05 9
12
+ cirrus 1.35 - 1.40 9 10 26
13
+ swir16 1.55 - 1.75 5 6 11 6
14
+ swir22 2.10 - 2.30 7 7 12 7
15
+ lwir 10.5 - 12.5 6
16
+ lwir11 10.5 - 11.5 10 31
17
+ lwir12 11.5 - 12.5 11 32