class SentinelDownloader(object): def __init__(self, user, password, api_url='https://scihub.copernicus.eu/apihub'): self._apiname = api_url self._user = user self._password = password # init logger root = logging.getLogger() root.addHandler(logging.StreamHandler( sys.stderr )) if self._apiname == 'https://scihub.copernicus.eu/apihub': try: from sentinelsat import SentinelAPI except ImportError as e: gs.fatal(_("Module requires sentinelsat library: {}").format(e)) # connect SciHub via API self._api = SentinelAPI(self._user, self._password, api_url=self._apiname ) elif self._apiname == 'USGS_EE': try: import landsatxplore.api from landsatxplore.errors import EarthExplorerError except ImportError as e: gs.fatal(_("Module requires landsatxplore library: {}").format(e)) api_login = False while api_login is False: # avoid login conflict in possible parallel execution try: self._api = landsatxplore.api.API(self._user, self._password) api_login = True except EarthExplorerError as e: time.sleep(1) self._products_df_sorted = None def filter(self, area, area_relation, clouds=None, producttype=None, limit=None, query={}, start=None, end=None, sortby=[], asc=True, relativeorbitnumber=None): args = {} if clouds: args['cloudcoverpercentage'] = (0, int(clouds)) if relativeorbitnumber: args['relativeorbitnumber'] = relativeorbitnumber if producttype.startswith('S2') and int(relativeorbitnumber) > 143: gs.warning("This relative orbit number is out of range") elif int(relativeorbitnumber) > 175: gs.warning(_("This relative orbit number is out of range")) if producttype: args['producttype'] = producttype if producttype.startswith('S2'): args['platformname'] = 'Sentinel-2' else: args['platformname'] = 'Sentinel-1' if not start: start = 'NOW-60DAYS' else: start = start.replace('-', '') if not end: end = 'NOW' else: end = end.replace('-', '') if query: redefined = [value for value in args.keys() if value in query.keys()] if redefined: gs.warning(_("Query overrides already defined options ({})").format( ','.join(redefined) )) args.update(query) gs.verbose(_("Query: area={} area_relation={} date=({}, {}) args={}").format( area, area_relation, start, end, args )) products = self._api.query( area=area, area_relation=area_relation, date=(start, end), **args ) products_df = self._api.to_dataframe(products) if len(products_df) < 1: gs.message(_("No product found")) return # sort and limit to first sorted product if sortby: self._products_df_sorted = products_df.sort_values( sortby, ascending=[asc] * len(sortby) ) else: self._products_df_sorted = products_df if limit: self._products_df_sorted = self._products_df_sorted.head(int(limit)) gs.message(_("{} Sentinel product(s) found").format(len(self._products_df_sorted))) def list(self): if self._products_df_sorted is None: return id_kw = ('uuid', 'entity_id') identifier_kw = ('identifier', 'display_id') cloud_kw = ('cloudcoverpercentage', 'cloud_cover') time_kw = ('beginposition', 'acquisition_date') kw_idx = 1 if self._apiname == 'USGS_EE' else 0 for idx in range(len(self._products_df_sorted[id_kw[kw_idx]])): if cloud_kw[kw_idx] in self._products_df_sorted: ccp = '{0:2.0f}%'.format( float(self._products_df_sorted[cloud_kw[kw_idx]][idx])) else: ccp = 'cloudcover_NA' print_str = '{0} {1}'.format( self._products_df_sorted[id_kw[kw_idx]][idx], self._products_df_sorted[identifier_kw[kw_idx]][idx]) if kw_idx == 1: time_string = self._products_df_sorted[time_kw[kw_idx]][idx] else: time_string = self._products_df_sorted[ time_kw[kw_idx]][idx].strftime("%Y-%m-%dT%H:%M:%SZ") print_str += ' {0} {1}'.format(time_string, ccp) if kw_idx == 0: print_str += ' {0}'.format( self._products_df_sorted['producttype'][idx]) print(print_str) def download(self, output, sleep=False, maxretry=False, datasource='ESA_COAH'): if self._products_df_sorted is None: return create_dir(output) gs.message(_("Downloading data into <{}>...").format(output)) if datasource == 'USGS_EE': from landsatxplore.earthexplorer import EarthExplorer from landsatxplore.errors import EarthExplorerError from zipfile import ZipFile ee_login = False while ee_login is False: # avoid login conflict in possible parallel execution try: ee = EarthExplorer(self._user, self._password) ee_login = True except EarthExplorerError as e: time.sleep(1) for idx in range(len(self._products_df_sorted['entity_id'])): scene = self._products_df_sorted['entity_id'][idx] identifier = self._products_df_sorted['display_id'][idx] zip_file = os.path.join(output, '{}.zip'.format(identifier)) gs.message(_("Downloading {}...").format(identifier)) try: ee.download(identifier=identifier, output_dir=output, timeout=600) except EarthExplorerError as e: gs.fatal(_(e)) ee.logout() # extract .zip to get "usual" .SAFE with ZipFile(zip_file, 'r') as zip: safe_name = zip.namelist()[0].split('/')[0] outpath = os.path.join(output, safe_name) zip.extractall(path=output) gs.message(_("Downloaded to <{}>").format(outpath)) try: os.remove(zip_file) except Exception as e: gs.warning(_("Unable to remove {0}:{1}").format( zip_file, e)) elif datasource == "ESA_COAH": for idx in range(len(self._products_df_sorted['uuid'])): gs.message('{} -> {}.SAFE'.format( self._products_df_sorted['uuid'][idx], os.path.join(output, self._products_df_sorted['identifier'][idx]) )) # download out = self._api.download(self._products_df_sorted['uuid'][idx], output) if sleep: x = 1 online = out['Online'] while not online: # sleep is in minutes so multiply by 60 time.sleep(int(sleep) * 60) out = self._api.download(self._products_df_sorted['uuid'][idx], output) x += 1 if x > maxretry: online = True elif datasource == 'GCS': for scene_id in self._products_df_sorted['identifier']: gs.message(_("Downloading {}...").format(scene_id)) dl_code = download_gcs(scene_id, output) if dl_code == 0: gs.message(_("Downloaded to {}").format( os.path.join(output, '{}.SAFE'.format(scene_id)))) else: # remove incomplete file del_folder = os.path.join(output, '{}.SAFE'.format(scene_id)) try: shutil.rmtree(del_folder) except Exception as e: gs.warning(_("Unable to removed unfinished " "download {}".format(del_folder))) def save_footprints(self, map_name): if self._products_df_sorted is None: return if self._apiname == 'USGS_EE': gs.fatal(_( "USGS Earth Explorer does not support footprint download.")) try: from osgeo import ogr, osr except ImportError as e: gs.fatal(_("Option <footprints> requires GDAL library: {}").format(e)) gs.message(_("Writing footprints into <{}>...").format(map_name)) driver = ogr.GetDriverByName("GPKG") tmp_name = gs.tempfile() + '.gpkg' data_source = driver.CreateDataSource(tmp_name) srs = osr.SpatialReference() srs.ImportFromEPSG(4326) # features can be polygons or multi-polygons layer = data_source.CreateLayer(str(map_name), srs, ogr.wkbMultiPolygon) # attributes attrs = OrderedDict([ ("uuid", ogr.OFTString), ("ingestiondate", ogr.OFTString), ("cloudcoverpercentage", ogr.OFTInteger), ("producttype", ogr.OFTString), ("identifier", ogr.OFTString) ]) # Sentinel-1 data does not have cloudcoverpercentage prod_types = [type for type in self._products_df_sorted["producttype"]] s1_types = ["SLC", "GRD"] if any(type in prod_types for type in s1_types): del attrs["cloudcoverpercentage"] for key in attrs.keys(): field = ogr.FieldDefn(key, attrs[key]) layer.CreateField(field) # features for idx in range(len(self._products_df_sorted['uuid'])): wkt = self._products_df_sorted['footprint'][idx] feature = ogr.Feature(layer.GetLayerDefn()) newgeom = ogr.CreateGeometryFromWkt(wkt) # convert polygons to multi-polygons newgeomtype = ogr.GT_Flatten(newgeom.GetGeometryType()) if newgeomtype == ogr.wkbPolygon: multigeom = ogr.Geometry(ogr.wkbMultiPolygon) multigeom.AddGeometryDirectly(newgeom) feature.SetGeometry(multigeom) else: feature.SetGeometry(newgeom) for key in attrs.keys(): if key == 'ingestiondate': value = self._products_df_sorted[key][idx].strftime("%Y-%m-%dT%H:%M:%SZ") else: value = self._products_df_sorted[key][idx] feature.SetField(key, value) layer.CreateFeature(feature) feature = None data_source = None # coordinates of footprints are in WKT -> fp precision issues # -> snap gs.run_command('v.import', input=tmp_name, output=map_name, layer=map_name, snap=1e-10, quiet=True ) def get_products_from_uuid_usgs(self, uuid_list): scenes = [] for uuid in uuid_list: metadata = self._api.metadata(uuid, 'SENTINEL_2A') scenes.append(metadata) scenes_df = pandas.DataFrame.from_dict(scenes) self._products_df_sorted = scenes_df gs.message(_("{} Sentinel product(s) found").format( len(self._products_df_sorted))) def set_uuid(self, uuid_list): """Set products by uuid. TODO: Find better implementation :param uuid: uuid to download """ if self._apiname == 'USGS_EE': self.get_products_from_uuid_usgs(uuid_list) else: from sentinelsat.sentinel import SentinelAPIError self._products_df_sorted = {'uuid': []} for uuid in uuid_list: try: odata = self._api.get_product_odata(uuid, full=True) except SentinelAPIError as e: gs.error(_("{0}. UUID {1} skipped".format(e, uuid))) continue for k, v in odata.items(): if k == 'id': k = 'uuid' elif k == 'Sensing start': k = 'beginposition' elif k == 'Product type': k = 'producttype' elif k == 'Cloud cover percentage': k = 'cloudcoverpercentage' elif k == 'Identifier': k = 'identifier' elif k == 'Ingestion Date': k = 'ingestiondate' elif k == 'footprint': pass else: continue if k not in self._products_df_sorted: self._products_df_sorted[k] = [] self._products_df_sorted[k].append(v) def filter_USGS(self, area, area_relation, clouds=None, producttype=None, limit=None, query={}, start=None, end=None, sortby=[], asc=True, relativeorbitnumber=None): if area_relation != 'Intersects': gs.fatal(_( "USGS Earth Explorer only supports area_relation" " 'Intersects'")) if relativeorbitnumber: gs.fatal(_( "USGS Earth Explorer does not support 'relativeorbitnumber'" " option.")) if producttype and producttype != 'S2MSI1C': gs.fatal(_( "USGS Earth Explorer only supports producttype S2MSI1C")) if query: if not any(key in query for key in ['identifier', 'filename', 'usgs_identifier']): gs.fatal(_( "USGS Earth Explorer only supports query options" " 'filename', 'identifier' or 'usgs_identifier'.")) if 'usgs_identifier' in query: # get entityId from usgs identifier and directly save results usgs_id = query['usgs_identifier'] check_s2l1c_identifier(usgs_id, source='usgs') # entity_id = self._api.lookup('SENTINEL_2A', [usgs_id], # inverse=True) entity_id = self._api.get_entity_id([usgs_id], 'SENTINEL_2A') self.get_products_from_uuid_usgs(entity_id) return else: if "filename" in query: esa_id = query['filename'].replace('.SAFE', '') else: esa_id = query['identifier'] check_s2l1c_identifier(esa_id, source='esa') esa_prod_id = esa_id.split('_')[-1] utm_tile = esa_id.split('_')[-2] acq_date = esa_id.split('_')[2].split('T')[0] acq_date_string = '{0}-{1}-{2}'.format( acq_date[:4], acq_date[4:6], acq_date[6:]) start_date = end_date = acq_date_string # build the USGS style S2-identifier if utm_tile.startswith('T'): utm_tile_base = utm_tile[1:] bbox = get_bbox_from_S2_UTMtile(utm_tile_base) else: # get coordinate pairs from wkt string str_1 = 'POLYGON((' str_2 = '))' coords = area[area.find(str_1)+len(str_1):area.rfind(str_2)].split(',') # add one space to first pair for consistency coords[0] = ' ' + coords[0] lons = [float(pair.split(' ')[1]) for pair in coords] lats = [float(pair.split(' ')[2]) for pair in coords] bbox = (min(lons), min(lats), max(lons), max(lats)) start_date = start end_date = end usgs_args = { 'dataset': 'SENTINEL_2A', 'bbox': bbox, 'start_date': start_date, 'end_date': end_date } if clouds: usgs_args['max_cloud_cover'] = clouds if limit: usgs_args['max_results'] = limit scenes = self._api.search(**usgs_args) self._api.logout() if query: # check if the UTM-Tile is correct, remove otherwise for scene in scenes: if scene['display_id'].split('_')[1] != utm_tile: scenes.remove(scene) # remove redundant scene if len(scenes) == 2: for scene in scenes: prod_id = scene['display_id'].split('_')[-1] if prod_id != esa_prod_id: scenes.remove(scene) if len(scenes) < 1: gs.message(_("No product found")) return scenes_df = pandas.DataFrame.from_dict(scenes) if sortby: # replace sortby keywords with USGS keywords for idx, keyword in enumerate(sortby): if keyword == 'cloudcoverpercentage': sortby[idx] = 'cloud_cover' # turn cloudcover to float to make it sortable scenes_df['cloud_cover'] = pandas.to_numeric( scenes_df['cloud_cover']) elif keyword == 'ingestiondate': sortby[idx] = 'acquisition_date' # what does sorting by footprint mean elif keyword == 'footprint': sortby[idx] = 'display_id' self._products_df_sorted = scenes_df.sort_values( sortby, ascending=[asc] * len(sortby), ignore_index=True ) else: self._products_df_sorted = scenes_df gs.message(_("{} Sentinel product(s) found").format( len(self._products_df_sorted)))
class SentinelDownloader(object): def __init__( self, user, password, api_url="https://apihub.copernicus.eu/apihub", cred_req=True, ): self._apiname = api_url self._user = user self._password = password self._cred_req = cred_req # init logger root = logging.getLogger() root.addHandler(logging.StreamHandler(sys.stderr)) if self._apiname not in ["USGS_EE", "GCS"]: try: from sentinelsat import SentinelAPI except ImportError as e: gs.fatal( _("Module requires sentinelsat library: {}").format(e)) # connect SciHub via API self._api = SentinelAPI(self._user, self._password, api_url=self._apiname) elif self._apiname == "USGS_EE": api_login = False while api_login is False: # avoid login conflict in possible parallel execution try: self._api = landsatxplore.api.API(self._user, self._password) api_login = True except EarthExplorerError as e: time.sleep(1) self._products_df_sorted = None def filter( self, area, area_relation, clouds=None, producttype=None, limit=None, query={}, start=None, end=None, sortby=[], asc=True, relativeorbitnumber=None, ): # Dict to identify plaforms from requested product platforms = { "SL": "Sentinel-1", "GR": "Sentinel-1", "OC": "Sentinel-1", "S2": "Sentinel-2", "S3": "Sentinel-3", } args = {} if clouds: args["cloudcoverpercentage"] = (0, int(clouds)) if relativeorbitnumber: args["relativeorbitnumber"] = relativeorbitnumber if producttype.startswith("S2") and int(relativeorbitnumber) > 143: gs.warning("This relative orbit number is out of range") elif int(relativeorbitnumber) > 175: gs.warning(_("This relative orbit number is out of range")) if producttype: if producttype.startswith("S3"): # Using custom product names for Sentinel-3 products that look less cryptic split = [0, 2, 4, 5, 8] args["producttype"] = "_".join([ producttype[i:j] for i, j in zip(split, split[1:] + [None]) ][1:]).ljust(11, "_") else: args["producttype"] = producttype args["platformname"] = platforms[producttype[0:2]] if not start: start = "NOW-60DAYS" else: start = start.replace("-", "") if not end: end = "NOW" else: end = end.replace("-", "") if query: redefined = [ value for value in args.keys() if value in query.keys() ] if redefined: gs.warning( _("Query overrides already defined options ({})").format( ",".join(redefined))) args.update(query) gs.verbose( _("Query: area={} area_relation={} date=({}, {}) args={}").format( area, area_relation, start, end, args)) if self._cred_req is False: # in the main function it is ensured that there is an "identifier" query self._products_df_sorted = pandas.DataFrame( {"identifier": [query["identifier"]]}) return products = self._api.query(area=area, area_relation=area_relation, date=(start, end), **args) products_df = self._api.to_dataframe(products) if len(products_df) < 1: gs.message(_("No product found")) return # sort and limit to first sorted product if sortby: self._products_df_sorted = products_df.sort_values( sortby, ascending=[asc] * len(sortby)) else: self._products_df_sorted = products_df if limit: self._products_df_sorted = self._products_df_sorted.head( int(limit)) gs.message( _("{} Sentinel product(s) found").format( len(self._products_df_sorted))) def list(self): if self._products_df_sorted is None: return id_kw = ("uuid", "entity_id") identifier_kw = ("identifier", "display_id") cloud_kw = ("cloudcoverpercentage", "cloud_cover") time_kw = ("beginposition", "acquisition_date") kw_idx = 1 if self._apiname == "USGS_EE" else 0 for idx in range(len(self._products_df_sorted[id_kw[kw_idx]])): if cloud_kw[kw_idx] in self._products_df_sorted: ccp = "{0:2.0f}%".format( float(self._products_df_sorted[cloud_kw[kw_idx]][idx])) else: ccp = "cloudcover_NA" print_str = "{0} {1}".format( self._products_df_sorted[id_kw[kw_idx]][idx], self._products_df_sorted[identifier_kw[kw_idx]][idx], ) if kw_idx == 1: time_string = self._products_df_sorted[time_kw[kw_idx]][idx] else: time_string = self._products_df_sorted[ time_kw[kw_idx]][idx].strftime("%Y-%m-%dT%H:%M:%SZ") print_str += " {0} {1}".format(time_string, ccp) if kw_idx == 0: print_str += " {0}".format( self._products_df_sorted["producttype"][idx]) print_str += " {0}".format( self._products_df_sorted["size"][idx]) print(print_str) def skip_existing(self, output, pattern_file): prod_df_type = type(self._products_df_sorted) # Check i skipping is possible/required if prod_df_type != dict: if self._products_df_sorted.empty: return elif not self._products_df_sorted or os.path.exists(output) == False: return # Check if ingestion date is returned by API if "ingestiondate" not in self._products_df_sorted: gs.warning( _("Ingestiondate not returned. Cannot filter previously downloaded scenes" )) return # Check for previously downloaded scenes existing_files = [ f for f in os.listdir(output) if re.search(r".zip$|.safe$|.ZIP$|.SAFE$", f) ] if len(existing_files) <= 1: return # Filter by ingestion date skiprows = [] for idx, display_id in enumerate( self._products_df_sorted["identifier"]): existing_file = [ sfile for sfile in existing_files if display_id in sfile ] if existing_file: creation_time = datetime.fromtimestamp( os.path.getctime(existing_file[0])) if self._products_df_sorted["ingestiondate"][ idx] <= creation_time: gs.message( _("Skipping scene: {} which is already downloaded.". format(self._products_df_sorted["identifier"][idx]))) skiprows.append(display_id) if prod_df_type == dict: for scene in skiprows: idx = self._products_df_sorted["identifier"].index(scene) for key in self._products_df_sorted: self._products_df_sorted[key].pop(idx) else: self._products_df_sorted = self._products_df_sorted[ ~self._products_df_sorted["identifier"].isin(skiprows)] def download(self, output, sleep=False, maxretry=False, datasource="ESA_COAH"): if self._products_df_sorted is None: return create_dir(output) gs.message(_("Downloading data into <{}>...").format(output)) if datasource == "USGS_EE": from landsatxplore.earthexplorer import EarthExplorer from landsatxplore.errors import EarthExplorerError from zipfile import ZipFile ee_login = False while ee_login is False: # avoid login conflict in possible parallel execution try: ee = EarthExplorer(self._user, self._password) ee_login = True except EarthExplorerError as e: time.sleep(1) for idx in range(len(self._products_df_sorted["entity_id"])): scene = self._products_df_sorted["entity_id"][idx] identifier = self._products_df_sorted["display_id"][idx] zip_file = os.path.join(output, "{}.zip".format(identifier)) gs.message(_("Downloading {}...").format(identifier)) try: ee.download(identifier=identifier, output_dir=output, timeout=600) except EarthExplorerError as e: gs.fatal(_(e)) ee.logout() # extract .zip to get "usual" .SAFE with ZipFile(zip_file, "r") as zip: safe_name = zip.namelist()[0].split("/")[0] outpath = os.path.join(output, safe_name) zip.extractall(path=output) gs.message(_("Downloaded to <{}>").format(outpath)) try: os.remove(zip_file) except Exception as e: gs.warning( _("Unable to remove {0}:{1}").format(zip_file, e)) elif datasource == "ESA_COAH": for idx in range(len(self._products_df_sorted["uuid"])): gs.message("{} -> {}.SAFE".format( self._products_df_sorted["uuid"][idx], os.path.join(output, self._products_df_sorted["identifier"][idx]), )) # download out = self._api.download(self._products_df_sorted["uuid"][idx], output) if sleep: x = 1 online = out["Online"] while not online: # sleep is in minutes so multiply by 60 time.sleep(int(sleep) * 60) out = self._api.download( self._products_df_sorted["uuid"][idx], output) x += 1 if x > maxretry: online = True elif datasource == "GCS": for scene_id in self._products_df_sorted["identifier"]: gs.message(_("Downloading {}...").format(scene_id)) dl_code = download_gcs(scene_id, output) if dl_code == 0: gs.message( _("Downloaded to {}").format( os.path.join(output, "{}.SAFE".format(scene_id)))) else: # remove incomplete file del_folder = os.path.join(output, "{}.SAFE".format(scene_id)) try: shutil.rmtree(del_folder) except Exception as e: gs.warning( _("Unable to removed unfinished " "download {}".format(del_folder))) def save_footprints(self, map_name): if self._products_df_sorted is None: return if self._apiname == "USGS_EE": gs.fatal( _("USGS Earth Explorer does not support footprint download.")) try: from osgeo import ogr, osr except ImportError as e: gs.fatal( _("Option <footprints> requires GDAL library: {}").format(e)) gs.message(_("Writing footprints into <{}>...").format(map_name)) driver = ogr.GetDriverByName("GPKG") tmp_name = gs.tempfile() + ".gpkg" data_source = driver.CreateDataSource(tmp_name) srs = osr.SpatialReference() srs.ImportFromEPSG(4326) # features can be polygons or multi-polygons layer = data_source.CreateLayer(str(map_name), srs, ogr.wkbMultiPolygon) # attributes attrs = OrderedDict([ ("uuid", ogr.OFTString), ("ingestiondate", ogr.OFTString), ("cloudcoverpercentage", ogr.OFTInteger), ("producttype", ogr.OFTString), ("identifier", ogr.OFTString), ]) # Sentinel-1 data does not have cloudcoverpercentage prod_types = [type for type in self._products_df_sorted["producttype"]] if not any(type in prod_types for type in cloudcover_products): del attrs["cloudcoverpercentage"] for key in attrs.keys(): field = ogr.FieldDefn(key, attrs[key]) layer.CreateField(field) # features for idx in range(len(self._products_df_sorted["uuid"])): wkt = self._products_df_sorted["footprint"][idx] feature = ogr.Feature(layer.GetLayerDefn()) newgeom = ogr.CreateGeometryFromWkt(wkt) # convert polygons to multi-polygons newgeomtype = ogr.GT_Flatten(newgeom.GetGeometryType()) if newgeomtype == ogr.wkbPolygon: multigeom = ogr.Geometry(ogr.wkbMultiPolygon) multigeom.AddGeometryDirectly(newgeom) feature.SetGeometry(multigeom) else: feature.SetGeometry(newgeom) for key in attrs.keys(): if key == "ingestiondate": value = self._products_df_sorted[key][idx].strftime( "%Y-%m-%dT%H:%M:%SZ") else: value = self._products_df_sorted[key][idx] feature.SetField(key, value) layer.CreateFeature(feature) feature = None data_source = None # coordinates of footprints are in WKT -> fp precision issues # -> snap gs.run_command( "v.import", input=tmp_name, output=map_name, layer=map_name, snap=1e-10, quiet=True, ) def get_products_from_uuid_usgs(self, uuid_list): scenes = [] for uuid in uuid_list: metadata = self._api.metadata(uuid, "SENTINEL_2A") scenes.append(metadata) scenes_df = pandas.DataFrame.from_dict(scenes) self._products_df_sorted = scenes_df gs.message( _("{} Sentinel product(s) found").format( len(self._products_df_sorted))) def set_uuid(self, uuid_list): """Set products by uuid. TODO: Find better implementation :param uuid: uuid to download """ if self._apiname == "USGS_EE": self.get_products_from_uuid_usgs(uuid_list) else: from sentinelsat.sentinel import SentinelAPIError self._products_df_sorted = {"uuid": []} for uuid in uuid_list: try: odata = self._api.get_product_odata(uuid, full=True) except SentinelAPIError as e: gs.error(_("{0}. UUID {1} skipped".format(e, uuid))) continue for k, v in odata.items(): if k == "id": k = "uuid" elif k == "Sensing start": k = "beginposition" elif k == "Product type": k = "producttype" elif k == "Cloud cover percentage": k = "cloudcoverpercentage" elif k == "Identifier": k = "identifier" elif k == "Ingestion Date": k = "ingestiondate" elif k == "footprint": pass else: continue if k not in self._products_df_sorted: self._products_df_sorted[k] = [] self._products_df_sorted[k].append(v) def filter_USGS( self, area, area_relation, clouds=None, producttype=None, limit=None, query={}, start=None, end=None, sortby=[], asc=True, relativeorbitnumber=None, ): if area_relation != "Intersects": gs.fatal( _("USGS Earth Explorer only supports area_relation" " 'Intersects'")) if relativeorbitnumber: gs.fatal( _("USGS Earth Explorer does not support 'relativeorbitnumber'" " option.")) if producttype and producttype != "S2MSI1C": gs.fatal( _("USGS Earth Explorer only supports producttype S2MSI1C")) if query: if not any( key in query for key in ["identifier", "filename", "usgs_identifier"]): gs.fatal( _("USGS Earth Explorer only supports query options" " 'filename', 'identifier' or 'usgs_identifier'.")) if "usgs_identifier" in query: # get entityId from usgs identifier and directly save results usgs_id = query["usgs_identifier"] check_s2l1c_identifier(usgs_id, source="usgs") # entity_id = self._api.lookup('SENTINEL_2A', [usgs_id], # inverse=True) entity_id = self._api.get_entity_id([usgs_id], "SENTINEL_2A") self.get_products_from_uuid_usgs(entity_id) return else: if "filename" in query: esa_id = query["filename"].replace(".SAFE", "") else: esa_id = query["identifier"] check_s2l1c_identifier(esa_id, source="esa") esa_prod_id = esa_id.split("_")[-1] utm_tile = esa_id.split("_")[-2] acq_date = esa_id.split("_")[2].split("T")[0] acq_date_string = "{0}-{1}-{2}".format(acq_date[:4], acq_date[4:6], acq_date[6:]) start_date = end_date = acq_date_string # build the USGS style S2-identifier if utm_tile.startswith("T"): utm_tile_base = utm_tile[1:] bbox = get_bbox_from_S2_UTMtile(utm_tile_base) else: # get coordinate pairs from wkt string str_1 = "POLYGON((" str_2 = "))" coords = area[area.find(str_1) + len(str_1):area.rfind(str_2)].split(",") # add one space to first pair for consistency coords[0] = " " + coords[0] lons = [float(pair.split(" ")[1]) for pair in coords] lats = [float(pair.split(" ")[2]) for pair in coords] bbox = (min(lons), min(lats), max(lons), max(lats)) start_date = start end_date = end usgs_args = { "dataset": "SENTINEL_2A", "bbox": bbox, "start_date": start_date, "end_date": end_date, } if clouds: usgs_args["max_cloud_cover"] = clouds if limit: usgs_args["max_results"] = limit scenes = self._api.search(**usgs_args) self._api.logout() if query: # check if the UTM-Tile is correct, remove otherwise for scene in scenes: if scene["display_id"].split("_")[1] != utm_tile: scenes.remove(scene) # remove redundant scene if len(scenes) == 2: for scene in scenes: prod_id = scene["display_id"].split("_")[-1] if prod_id != esa_prod_id: scenes.remove(scene) if len(scenes) < 1: gs.message(_("No product found")) return scenes_df = pandas.DataFrame.from_dict(scenes) if sortby: # replace sortby keywords with USGS keywords for idx, keyword in enumerate(sortby): if keyword == "cloudcoverpercentage": sortby[idx] = "cloud_cover" # turn cloudcover to float to make it sortable scenes_df["cloud_cover"] = pandas.to_numeric( scenes_df["cloud_cover"]) elif keyword == "ingestiondate": sortby[idx] = "acquisition_date" # what does sorting by footprint mean elif keyword == "footprint": sortby[idx] = "display_id" self._products_df_sorted = scenes_df.sort_values(sortby, ascending=[asc] * len(sortby), ignore_index=True) else: self._products_df_sorted = scenes_df gs.message( _("{} Sentinel product(s) found").format( len(self._products_df_sorted)))