izisat 0.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.
izisat/misc/files.py ADDED
@@ -0,0 +1,135 @@
1
+ import os
2
+ from loguru import logger
3
+ from datetime import datetime
4
+
5
+
6
+ class Files:
7
+ def __init__(self):
8
+ pass
9
+
10
+
11
+ def save_path_sentinel2_data(self, dir_out, etc, date, type, tile, band=None):
12
+ """
13
+ Generate a directory path for Sentinel-2 data and create it if it doesn't exist.
14
+
15
+ Parameters:
16
+ ------------
17
+ dir_out: str
18
+ The base output directory.
19
+ date: str
20
+ The date in the format '%Y-%m-%dT%H:%M:%S.%fZ'.
21
+ etc: str
22
+ Additional information for the directory path.
23
+ tile: str
24
+ The tile information for the directory path.
25
+ band: str, optional
26
+ The band information for the directory path.
27
+
28
+ Returns:
29
+ ----------
30
+ str
31
+ The path of the generated or existing directory.
32
+ """
33
+ dt_object = datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%fZ')
34
+ # Format the datetime object as 'YYYY/MM/DD'
35
+ date = dt_object.strftime('%Y/%m/%d')
36
+
37
+ if band is not None:
38
+ directory = os.path.join(dir_out, etc, date, type, tile, band)
39
+ else:
40
+ directory = os.path.join(dir_out, etc, date, type, tile)
41
+
42
+ if os.path.exists(directory):
43
+ logger.warning(f"Directory {directory} already exists.")
44
+ return directory
45
+ else:
46
+ os.makedirs(directory, exist_ok=True)
47
+ logger.success(f"Directory {directory} created!")
48
+ return directory
49
+
50
+
51
+ def create_output_folders(self, output_base_path, products_info, bands_dict):
52
+ """
53
+ Create, if necessary, folders to save the downloaded bands.
54
+
55
+ Parameters:
56
+ -----------
57
+ output_base_path: str
58
+ The base path where the output folders will be created.
59
+ products_info: list
60
+ A list of product information, where each item contains:
61
+ [product_id, product_name, product_path, product_origin_date, product_tile, product_platform_name, product_type]
62
+ bands_dict: dict
63
+ A dictionary containing information about bands for different product types and resolutions.
64
+
65
+ Returns:
66
+ --------
67
+ created_directories: list
68
+ A list of paths to the created output directories.
69
+ """
70
+ logger.info("Creating, if necessary, folders to save the downloaded bands...")
71
+ created_directories = []
72
+ for product_info in products_info:
73
+ product_type = product_info[6]
74
+ product_tile = product_info[4]
75
+ product_date = product_info[3]
76
+ product_plataform = product_info[5]
77
+ if product_type == "L1C":
78
+ directory_path = self.save_path_sentinel2_data(output_base_path, product_plataform, product_date, product_tile, product_type)
79
+ created_directories.append(directory_path)
80
+ elif product_type == "L2A":
81
+ for resolution, _ in bands_dict.items():
82
+ if resolution == "L1C":
83
+ pass
84
+ else:
85
+ l2a_dict = bands_dict.get("L2A", {})
86
+ for resolution_key in l2a_dict:
87
+ directory_path = self.save_path_sentinel2_data(output_base_path, product_plataform, product_date, product_tile, product_type, resolution_key)
88
+ created_directories.append(directory_path)
89
+
90
+ return created_directories
91
+
92
+
93
+ def check_file_exist(self, base_dir, etc, date, tile, type, filename, resolution = None):
94
+ """
95
+ Check if a file exists based on the specified parameters.
96
+
97
+ Parameters:
98
+ -----------
99
+ base_dir: str
100
+ The base directory where the file is expected to exist.
101
+ etc: str
102
+ Additional subdirectory for categorization (e.g., product platform or other metadata).
103
+ date: str
104
+ The date of the product in 'YYYY-MM-DDTHH:MM:SS.sssZ' format.
105
+ tile: str
106
+ The tile identifier associated with the product.
107
+ type: str
108
+ The type of the product (e.g., 'L1C', 'L2A').
109
+ filename: str
110
+ The name of the file to check.
111
+ resolution: str, optional
112
+ The resolution category (e.g., 'resolution10m', 'resolution20m').
113
+ If provided, the file path will include this resolution subdirectory.
114
+
115
+ Returns:
116
+ --------
117
+ exists: bool
118
+ True if the file exists, False otherwise.
119
+ file_path: str
120
+ The full path to the checked file.
121
+
122
+ """
123
+ date_obj = datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%fZ')
124
+ # Format datetime object as 'YYYY/MM/DD'
125
+ date = date_obj.strftime('%Y/%m/%d')
126
+
127
+ if resolution is None:
128
+ file = os.path.join(base_dir, etc, date, tile, type, filename)
129
+ return os.path.exists(file), file
130
+ else:
131
+ file = os.path.join(base_dir, etc, date, tile, type, resolution, filename)
132
+ return os.path.exists(file), file
133
+
134
+
135
+
@@ -0,0 +1,373 @@
1
+ import os
2
+ import rasterio
3
+ import rasterio.mask
4
+ from shapely import wkt
5
+ from shapely.geometry import mapping
6
+ from typing import Union
7
+ from pathlib import Path
8
+ import geopandas as gpd
9
+ import numpy as np
10
+ from rasterio.crs import CRS
11
+ from rasterio.transform import from_origin
12
+ from rasterio.merge import merge
13
+ import numpy as np
14
+ import rasterio
15
+ from rasterio.io import MemoryFile
16
+ from rasterio.warp import reproject, Resampling, calculate_default_transform
17
+
18
+
19
+ class RasterProcessing:
20
+ def __init__(self):
21
+ pass
22
+
23
+ def crop_band_by_footprint_wkt(self,
24
+ jp2_path: str,
25
+ footprint_wkt: str,
26
+ output_path: str = None
27
+ ):
28
+ """
29
+ Crops a georeferenced JP2 image using a WKT footprint (MULTIPOLYGON).
30
+
31
+ Args:
32
+ jp2_path (str): Path to the .jp2 image (satellite band).
33
+ footprint_wkt (str): WKT string of the footprint polygon (e.g. 'MULTIPOLYGON (...)').
34
+ output_path (str, optional): Output .tif file path or directory. If None, nothing is saved.
35
+ suffix (str): Suffix to append to the output file name if saving to a directory.
36
+
37
+ Returns:
38
+ dict: {
39
+ "image": Cropped image as a NumPy array,
40
+ "profile": Raster metadata dictionary,
41
+ "output_name": Full output path if saved, else None
42
+ }
43
+ """
44
+ # Convert WKT string to geometry
45
+ geometry = [mapping(wkt.loads(footprint_wkt))]
46
+
47
+ with rasterio.open(jp2_path) as src:
48
+ cropped_image, transform = rasterio.mask.mask(src, geometry, crop=True)
49
+ cropped_image = cropped_image.squeeze()
50
+ profile = src.profile.copy()
51
+ profile.update({
52
+ "height": cropped_image.shape[0],
53
+ "width": cropped_image.shape[1],
54
+ "transform": transform
55
+ })
56
+ if output_path is not None:
57
+ with rasterio.open(output_path, "w", **profile) as dst:
58
+ dst.write(cropped_image)
59
+ return cropped_image, profile
60
+
61
+ def ensure_array_in_epsg(self, array: np.ndarray, profile: dict, target_epsg: int):
62
+ import numpy as np
63
+ import rasterio
64
+ from rasterio.io import MemoryFile
65
+ from rasterio.warp import reproject, Resampling, calculate_default_transform
66
+ from rasterio.transform import array_bounds
67
+ """
68
+ Garante que o array raster esteja no EPSG desejado. Se necessário, reprojeta.
69
+
70
+ Parâmetros:
71
+ - array: np.ndarray (2D)
72
+ - profile: dict com as chaves: 'transform', 'crs', 'width', 'height'
73
+ - target_epsg: int
74
+
75
+ Retorna:
76
+ - dst_array: np.ndarray reprojetado ou original
77
+ - new_profile: dict atualizado
78
+ """
79
+ src_crs = profile.get("crs")
80
+ dst_crs = rasterio.crs.CRS.from_epsg(target_epsg)
81
+
82
+ if src_crs == dst_crs:
83
+ new_profile = profile.copy()
84
+ new_profile["crs"] = dst_crs
85
+ return array, new_profile
86
+
87
+ # Calcula os bounds a partir do transform
88
+ bounds = array_bounds(profile["height"], profile["width"], profile["transform"])
89
+
90
+ transform, width, height = calculate_default_transform(
91
+ src_crs, dst_crs,
92
+ profile["width"], profile["height"],
93
+ *bounds
94
+ )
95
+
96
+ dst_array = np.empty((height, width), dtype=array.dtype)
97
+
98
+ reproject(
99
+ source=array,
100
+ destination=dst_array,
101
+ src_transform=profile["transform"],
102
+ src_crs=src_crs,
103
+ dst_transform=transform,
104
+ dst_crs=dst_crs,
105
+ resampling=Resampling.nearest
106
+ )
107
+
108
+ new_profile = profile.copy()
109
+ new_profile.update({
110
+ "crs": dst_crs,
111
+ "transform": transform,
112
+ "width": width,
113
+ "height": height
114
+ })
115
+
116
+ return dst_array, new_profile
117
+
118
+ def get_cropped_bands_no_merge(self, gdf:gpd.GeoDataFrame, band_paths):
119
+ utm_zones = {
120
+ 19:31979,
121
+ 20:31980,
122
+ 21:31981,
123
+ 22:31982,
124
+ 23:31983,
125
+ 24:31984,
126
+ 25:31985,
127
+ }
128
+ bands_list = ['B02','B03','B04','B08']
129
+ cropped_bands = []
130
+
131
+ epsg = utm_zones[int(os.path.basename(band_paths['B02']).split('_')[0][1:3])]
132
+ gdf = gdf.to_crs(epsg)
133
+ footprint_wkt = gdf.iloc[0].geometry.wkt
134
+
135
+ for band in bands_list:
136
+ path = band_paths.get(band)
137
+ if path:
138
+ cropped, profile = self.crop_band_by_footprint_wkt(path, footprint_wkt)
139
+ cropped_bands.append(cropped)
140
+
141
+ return (*cropped_bands, profile)
142
+
143
+ def get_cropped_bands_with_merge(self, gdf: gpd.GeoDataFrame, band_dicts: list):
144
+ """
145
+ Recorta e mescla múltiplas bandas de entrada organizadas como uma lista de dicionários.
146
+
147
+ Parâmetros:
148
+ - gdf: GeoDataFrame com a geometria para recorte.
149
+ - band_dicts: lista de dicionários, cada um contendo os caminhos das bandas 'B02', 'B03', 'B04' e 'B08'.
150
+
151
+ Retorna:
152
+ - Tupla com arrays mesclados para cada banda: B02, B03, B04, B08 e o profile do merge.
153
+ """
154
+
155
+ utm_zones = {
156
+ 19: 31979,
157
+ 20: 31980,
158
+ 21: 31981,
159
+ 22: 31982,
160
+ 23: 31983,
161
+ 24: 31984,
162
+ 25: 31985,
163
+ }
164
+ bands_list = ['B02', 'B03', 'B04', 'B08']
165
+ cropped_bands_by_type = {band: [] for band in bands_list}
166
+ final_profile = None
167
+ final_crs = gdf.estimate_utm_crs('SIRGAS 2000').to_epsg()
168
+
169
+ # Recorta todas as bandas e armazena por tipo
170
+ for band_paths in band_dicts:
171
+ try:
172
+ for band in bands_list:
173
+ path = band_paths.get(band)
174
+ if path:
175
+ epsg = utm_zones[int(os.path.basename(path).split('_')[0][1:3])]
176
+ gdf = gdf.to_crs(epsg)
177
+ footprint_wkt = gdf.iloc[0].geometry.wkt
178
+ cropped, profile = self.crop_band_by_footprint_wkt(path, footprint_wkt)
179
+ cropped_bands_by_type[band].append((cropped, profile))
180
+ except:
181
+ pass
182
+
183
+ # Faz merge por banda
184
+ merged_bands = []
185
+ for band in bands_list:
186
+ datasets = []
187
+ for cropped, profile in cropped_bands_by_type[band]:
188
+ cropped_epsg, profile_epsg = self.ensure_array_in_epsg(cropped, profile, final_crs)
189
+ profile_epsg.update({
190
+ 'driver': 'GTiff',
191
+ 'dtype': 'uint16',
192
+ 'compress': None,
193
+ 'tiled': False,
194
+ 'nodata': 0
195
+ })
196
+ memfile = rasterio.io.MemoryFile()
197
+ with memfile.open(**profile_epsg) as tmp:
198
+ tmp.write(cropped_epsg, 1)
199
+ datasets.append(memfile.open())
200
+
201
+ merged, out_transform = merge(datasets, method='first')
202
+ merged_bands.append(merged.squeeze())
203
+
204
+ # Pega profile de um dos arquivos e atualiza
205
+ if final_profile is None:
206
+ final_profile = datasets[0].profile.copy()
207
+ final_profile.update({
208
+ "height": merged.shape[1],
209
+ "width": merged.shape[2],
210
+ "transform": out_transform,
211
+ "count": 1
212
+ })
213
+
214
+ # Fecha arquivos temporários
215
+
216
+ return (*merged_bands, final_profile)
217
+
218
+
219
+ def create_rgb(self, b02_array, b03_array, b04_array, reference_profile=None, output_path=None):
220
+ """
221
+ Cria uma imagem RGB a partir das bandas B02 (Blue), B03 (Green) e B04 (Red) do Sentinel-2.
222
+
223
+ Parâmetros:
224
+ - b02_array (np.ndarray): Banda 2 (azul)
225
+ - b03_array (np.ndarray): Banda 3 (verde)
226
+ - b04_array (np.ndarray): Banda 4 (vermelho)
227
+ - reference_profile (dict): Perfil de metadados de referência de uma das bandas originais
228
+ - output_path (str, opcional): Caminho para salvar o arquivo. Se None, apenas retorna o array.
229
+
230
+ Retorna:
231
+ - rgb_array (np.ndarray): Array RGB com shape (3, height, width)
232
+ """
233
+
234
+ # Verifica se as dimensões batem
235
+ if not (b02_array.shape == b03_array.shape == b04_array.shape):
236
+ raise ValueError("As bandas devem ter as mesmas dimensões")
237
+
238
+ # Cria array RGB no formato (3, altura, largura)
239
+ rgb_array = np.stack([b04_array, b03_array, b02_array], axis=0) # (R, G, B)
240
+
241
+ # Atualiza o perfil para refletir 3 bandas
242
+ profile = reference_profile.copy()
243
+ profile.update({
244
+ 'count': 3,
245
+ 'dtype': rgb_array.dtype,
246
+ 'driver': 'GTiff'
247
+ })
248
+
249
+ # Salva ou retorna
250
+ if output_path:
251
+ with rasterio.open(output_path, 'w', **profile) as dst:
252
+ dst.write(rgb_array)
253
+ return rgb_array
254
+
255
+ def create_ndvi(self, b04_array, b08_array, reference_profile, output_path=None):
256
+ """
257
+ Calcula o NDVI (Normalized Difference Vegetation Index) usando as bandas B04 (Red) e B08 (NIR) do Sentinel-2.
258
+
259
+ Parâmetros:
260
+ - b04_array (np.ndarray): Banda 4 (vermelho)
261
+ - b08_array (np.ndarray): Banda 8 (infravermelho próximo)
262
+ - reference_profile (dict): Perfil de metadados de referência de uma das bandas originais
263
+ - output_path (str, opcional): Caminho para salvar o arquivo. Se None, apenas retorna o array.
264
+
265
+ Retorna:
266
+ - ndvi (np.ndarray): Array 2D com valores de NDVI
267
+ """
268
+
269
+ # Verifica se as dimensões batem
270
+ if b04_array.shape != b08_array.shape:
271
+ raise ValueError("As bandas devem ter as mesmas dimensões")
272
+
273
+ # Converte para float32 para evitar problemas de divisão inteira
274
+ b04 = b04_array.astype('float32')
275
+ b08 = b08_array.astype('float32')
276
+
277
+ # NDVI = (NIR - Red) / (NIR + Red)
278
+ denominator = b08 + b04
279
+ with np.errstate(divide='ignore', invalid='ignore'):
280
+ ndvi = (b08 - b04) / denominator
281
+ ndvi[np.isnan(ndvi)] = -9999 # valor nodata
282
+
283
+ # Atualiza o perfil
284
+ profile = reference_profile.copy()
285
+ profile.update({
286
+ 'count': 1,
287
+ 'dtype': 'float32',
288
+ 'driver': 'GTiff',
289
+ 'nodata': -9999
290
+ })
291
+
292
+ # Salva ou retorna
293
+ if output_path:
294
+ with rasterio.open(output_path, 'w', **profile) as dst:
295
+ dst.write(ndvi, 1)
296
+ return ndvi
297
+
298
+ def create_evi(self, b02_array, b04_array, b08_array, reference_profile, output_path=None):
299
+ """
300
+ Calcula o EVI (Enhanced Vegetation Index) usando as bandas B02 (Blue), B04 (Red) e B08 (NIR) do Sentinel-2.
301
+
302
+ Parâmetros:
303
+ - b02_array (np.ndarray): Banda 2 (azul)
304
+ - b04_array (np.ndarray): Banda 4 (vermelho)
305
+ - b08_array (np.ndarray): Banda 8 (infravermelho próximo)
306
+ - reference_profile (dict): Perfil de metadados de referência de uma das bandas originais
307
+ - output_path (str, opcional): Caminho para salvar o arquivo. Se None, apenas retorna o array.
308
+
309
+ Retorna:
310
+ - evi (np.ndarray): Array 2D com valores de EVI
311
+ """
312
+
313
+ # Verifica se as dimensões batem
314
+ if not (b02_array.shape == b04_array.shape == b08_array.shape):
315
+ raise ValueError("As bandas devem ter as mesmas dimensões")
316
+
317
+ # Converte para float32
318
+ b02 = b02_array.astype('float32') / 10000 # Blue
319
+ b04 = b04_array.astype('float32') / 10000 # Red
320
+ b08 = b08_array.astype('float32') / 10000 # NIR
321
+
322
+ # Parâmetros do EVI
323
+ G = 2.5
324
+ C1 = 6.0
325
+ C2 = 7.5
326
+ L = 1.0
327
+
328
+ # Fórmula do EVI
329
+ denominator = b08 + C1 * b04 - C2 * b02 + L
330
+ with np.errstate(divide='ignore', invalid='ignore'):
331
+ evi = G * ((b08 - b04) / denominator)
332
+ evi[np.isnan(evi)] = -9999 # valor nodata
333
+
334
+ # Atualiza o perfil
335
+ profile = reference_profile.copy()
336
+ profile.update({
337
+ 'count': 1,
338
+ 'dtype': 'float32',
339
+ 'driver': 'GTiff',
340
+ 'nodata': -9999
341
+ })
342
+
343
+ # Salva ou retorna
344
+ if output_path:
345
+ with rasterio.open(output_path, 'w', **profile) as dst:
346
+ dst.write(evi, 1)
347
+ return evi
348
+
349
+ if __name__ == '__main__':
350
+ import rasterio
351
+ b02 = '/home/ppz/Documentos/coding/izisat/auxiliary/Sentinel-2/2025/04/06/T22KGD/L2A/10m/T22KGD_20250406T133211_B02_10m.jp2'
352
+ b03 = '/home/ppz/Documentos/coding/izisat/auxiliary/Sentinel-2/2025/04/06/T22KGD/L2A/10m/T22KGD_20250406T133211_B03_10m.jp2'
353
+ b04 = '/home/ppz/Documentos/coding/izisat/auxiliary/Sentinel-2/2025/04/06/T22KGD/L2A/10m/T22KGD_20250406T133211_B04_10m.jp2'
354
+ b08 = '/home/ppz/Documentos/coding/izisat/auxiliary/Sentinel-2/2025/04/06/T22KGD/L2A/10m/T22KGD_20250406T133211_B08_10m.jp2'
355
+
356
+ # Lê as bandas como arrays e pega o perfil de uma delas
357
+ with rasterio.open(b02) as b2_src:
358
+ b02 = b2_src.read(1)
359
+ profile = b2_src.profile
360
+
361
+ with rasterio.open(b03) as b3_src:
362
+ b03 = b3_src.read(1)
363
+
364
+ with rasterio.open(b04) as b4_src:
365
+ b04 = b4_src.read(1)
366
+
367
+ with rasterio.open(b08) as b8_src:
368
+ b08 = b8_src.read(1)
369
+
370
+ # Chama a função
371
+ instance = RasterProcessing()
372
+ rgb_array = instance.create_rgb(b02, b03, b04, reference_profile=profile, output_path='rgb_composite.tif')
373
+ ndvi_array = instance.create_ndvi(b04, b08, profile, output_path='ndvi_composite.tif')
izisat/misc/utils.py ADDED
@@ -0,0 +1,162 @@
1
+ from loguru import logger
2
+ from pathlib import Path
3
+ from datetime import datetime
4
+
5
+ class Utils:
6
+ def __init__(self):
7
+ pass
8
+
9
+
10
+ def construct_query_for_sentinel2_products(self, footprint: str, start_date: str, end_date: str, cloud_cover_percentage: str, type,
11
+ platform_name: str):
12
+ """
13
+ Create a query for downloading Sentinel data based on specified parameters.
14
+
15
+ Parameters:
16
+ ------------
17
+ footprint: str
18
+ The spatial geometry (POLYGON) of the area of interest.
19
+ start_date: str
20
+ The start date for the time range of interest in the format 'YYYY-MM-DD'.
21
+ end_date: str
22
+ The end date for the time range of interest in the format 'YYYY-MM-DD'.
23
+ cloud_cover_percentage: str
24
+ The maximum allowable cloud cover percentage.
25
+ type: str or list
26
+ Type of MSI to download
27
+ platform_name: str, optional
28
+ The name of the Sentinel platform (default: 'SENTINEL-2').
29
+
30
+ Returns:
31
+ params: str
32
+ The query string for downloading Sentinel data.
33
+ """
34
+
35
+ logger.info("Creating query to download Sentinel data...")
36
+ try:
37
+ if footprint and start_date and end_date and platform_name and cloud_cover_percentage and type:
38
+ footprint = footprint.replace(" ", "%20")
39
+ if isinstance(type, str):
40
+ params = f"?&$filter=(Collection/Name%20eq%20%27{platform_name}%27%20and%20(Attributes/OData.CSC.StringAttribute/any(att:att/Name%20eq%20%27instrumentShortName%27%20and%20att/OData.CSC.StringAttribute/Value%20eq%20%27MSI%27)%20and%20Attributes/OData.CSC.DoubleAttribute/any(att:att/Name%20eq%20%27cloudCover%27%20and%20att/OData.CSC.DoubleAttribute/Value%20le%20{cloud_cover_percentage})%20and%20(contains(Name,%27{type}%27)%20and%20OData.CSC.Intersects(area=geography%27SRID=4326;{footprint}%27)))%20and%20Online%20eq%20true)%20and%20ContentDate/Start%20ge%20{start_date}T00:00:00.000Z%20and%20ContentDate/Start%20lt%20{end_date}T23:59:59.999Z&$orderby=ContentDate/Start%20desc&$expand=Attributes&$count=True&$top=50&$expand=Assets&$skip=0"
41
+ logger.success("Query created successfully")
42
+ return params
43
+ elif isinstance(type, list):
44
+ params = f"?&$filter=(Collection/Name%20eq%20%27{platform_name}%27%20and%20(Attributes/OData.CSC.StringAttribute/any(att:att/Name%20eq%20%27instrumentShortName%27%20and%20att/OData.CSC.StringAttribute/Value%20eq%20%27MSI%27)%20and%20Attributes/OData.CSC.DoubleAttribute/any(att:att/Name%20eq%20%27cloudCover%27%20and%20att/OData.CSC.DoubleAttribute/Value%20le%20{cloud_cover_percentage})%20and%20((contains(Name,%27{type[0]}%27)%20and%20OData.CSC.Intersects(area=geography%27SRID=4326;{footprint}%27))%20or%20(contains(Name,%27{type[1]}%27)%20and%20OData.CSC.Intersects(area=geography%27SRID=4326;{footprint}%27))))%20and%20Online%20eq%20true)%20and%20ContentDate/Start%20ge%20{start_date}T00:00:00.000Z%20and%20ContentDate/Start%20lt%20{end_date}T23:59:59.999Z&$orderby=ContentDate/Start%20desc&$expand=Attributes&$count=True&$top=50&$expand=Assets&$skip=0"
45
+ logger.success("Query created successfully")
46
+ return params
47
+ else:
48
+ raise ValueError("Please provide valid values for type.")
49
+ else:
50
+ raise ValueError("Please provide valid values for all required parameters.")
51
+ except Exception as e:
52
+ logger.error(f"An error occurred: {str(e)}")
53
+ raise
54
+
55
+ def retrieve_products_info(self, products):
56
+ """
57
+ Retrieve information about Sentinel products.
58
+
59
+ Parameters:
60
+ -----------
61
+ products: list
62
+ List of Sentinel products, each represented as a dictionary.
63
+
64
+ Returns:
65
+ ---------
66
+ list:
67
+ A list containing information about each product. Each element in the list is a sublist with the following format:
68
+ [product_id, product_name, product_s3path, product_origin_date, product_tile, product_platform_name, product_type]
69
+ """
70
+ result_list = []
71
+ logger.info(f"Retrieving information about {len(products)} products...")
72
+
73
+ for i, product in enumerate(products):
74
+ logger.info(f"Retrieving information about product {i+1}")
75
+ product_id = product['Id']
76
+ product_name = product['Name']
77
+ product_s3path = product["S3Path"]
78
+ product_origin_date = product['ContentDate']['End']
79
+
80
+ # Creating output directory to save downloaded data
81
+ product_tile = product_name.split("_")[5]
82
+ product_platform_name = product_s3path.split('/')[2]
83
+ product_type = product_name.split('_')[1][-3:]
84
+
85
+ # Append product information to the result list
86
+ result_list.append([product_id, product_name, product_s3path, product_origin_date, product_tile, product_platform_name, product_type])
87
+ logger.success(f"ID, Name, S3Path, Origin Date, Tile, Plataform Name and Type successfully retrieved for product {i+1}")
88
+
89
+ logger.success("Information successfully retrieved for all products. A list with these informations were created!")
90
+ return result_list
91
+
92
+ def modify_string(self, url):
93
+ """
94
+ Modify a URL by replacing the last occurrence of "Nodes" with "$value".
95
+
96
+ Parameters:
97
+ -----------
98
+ url: str
99
+ The input URL to be modified.
100
+
101
+ Returns:
102
+ ---------
103
+ modified_url: str
104
+ The modified URL with the last "Nodes" replaced by "$value".
105
+ ValueError
106
+ If "Nodes" is not found in the input URL.
107
+ """
108
+ last_nodes_index = url.rfind("Nodes")
109
+
110
+ if last_nodes_index != -1:
111
+ # Remove the last "Nodes" and replace it with "/$value"
112
+ modified_url = url[:last_nodes_index] + "$value"
113
+ return modified_url
114
+ else:
115
+ raise ValueError("Error: 'Nodes' not found in the URL")
116
+
117
+ def generate_sentinel2_band_paths_nested_by_tile(self, download_folder: str, products: list[list[str]]) -> dict[str, dict[str, str]]:
118
+ """
119
+ Gera dicionário aninhado com caminhos das bandas Sentinel-2, agrupados por data e tile (fuso).
120
+
121
+ Retorna:
122
+ {
123
+ "2025-04-13_T23": {
124
+ "B02": "/.../T23KKV_20250413T132251_B02_10m.jp2",
125
+ "B03": "/.../T23KKV_..._B03_10m.jp2",
126
+ ...
127
+ },
128
+ ...
129
+ }
130
+ """
131
+ from collections import defaultdict
132
+ result = {}
133
+ grouped = defaultdict(list)
134
+ for variables in products:
135
+ date_obj = datetime.fromisoformat(variables[3].replace("Z", ""))
136
+ date_str = date_obj.strftime("%Y-%m-%d")
137
+ tile = variables[4] # Ex: T23KKV
138
+ tile_id = tile[1:3] # Pega apenas o fuso "23"
139
+ sensor = variables[5]
140
+ product_level = variables[6]
141
+ year = f"{date_obj.year}"
142
+ month = f"{date_obj.month:02}"
143
+ day = f"{date_obj.day:02}"
144
+ timestamp_part = variables[1].split("_")[2] # Ex: 20250413T132251
145
+
146
+ bands = ["B02", "B03", "B04", "B08"]
147
+ base_path = Path(download_folder) / sensor / year / month / day / tile / product_level / "10m"
148
+
149
+ band_paths = {
150
+ band: str(base_path / f"{tile}_{timestamp_part}_{band}_10m.jp2")
151
+ for band in bands
152
+ }
153
+
154
+ key = f"{date_str}_T{tile_id}" # Ex: 2025-04-13_T23
155
+ result[key] = band_paths
156
+
157
+ for key, bands in result.items():
158
+ date = key.split('_')[0]
159
+ grouped[date].append(bands)
160
+
161
+ return grouped
162
+