diff --git a/src/imagery/i.eodag/Makefile b/src/imagery/i.eodag/Makefile new file mode 100644 index 0000000000..a8e27237c6 --- /dev/null +++ b/src/imagery/i.eodag/Makefile @@ -0,0 +1,7 @@ +MODULE_TOPDIR = ../.. + +PGM = i.eodag + +include $(MODULE_TOPDIR)/include/Make/Script.make + +default: script diff --git a/src/imagery/i.eodag/i.eodag.html b/src/imagery/i.eodag/i.eodag.html new file mode 100644 index 0000000000..becbb2f30a --- /dev/null +++ b/src/imagery/i.eodag/i.eodag.html @@ -0,0 +1,105 @@ +

DESCRIPTION

+

WARNING: I.EODAG IS UNDER DEVELOPMENT. THIS IS AN EXPERIMENTAL VERSION.

+ +i.eodag allows to search and download imagery datasets, e.g. Sentinel, +Landsat, and MODIS, from a number of different providers. +The module utilizes the +EODAG API, +as a single interface to search for datasets on the providers that EODAG supports. + + +

+Currently, only products which footprint intersects the current computational +region extent (area of interest, AOI) are retrieved. + +

+To only list available scenes, l flag must be set. If no start +or end dates are provided, the module will search scenes from the +past 60 days. + +

+To download all scenes found within the time frame provided, the user must +remove the l flag and provide an output directory. Otherwise, +files will be downloaded into /tmp directory. +To download only selected scenes, one or more IDs must be provided +through the id option. + +

+To be able to download data through i.eodag, the user will need to register for the +providers of interest. +i.eodag reads the user credentials, which the user should have acquired, +from the EODAG YAML config file. User have to specify the config file path +location through the config option, otherwise i.eodag +will use the credentials found in the default config file ~/.config/eodag/eodag.yml + +

+Following is an example for a config YAML file with Copernicus Dataspace credentials: +

+cop_dataspace:
+    priority: # Lower value means lower priority (Default: 0)
+    search:   # Search parameters configuration
+    download:
+        extract:
+        outputs_prefix:
+    auth:
+        credentials:
+          username: email@email.com
+          password: password
+
+ +

+See + + Providers Registration, +and + Configure EODAG +sections for more details about registration and configuration of the providers' credentials. + +

EXAMPLES

+ +Search and list the available Sentinel 2 scenes in the Copernicus Data Space Ecosystem: + +
+i.eodag -l start=2022-05-25 end=2022-06-01 \
+    dataset=S2_MSI_L2A provider=cop_dataspace
+
+ +Download all available scenes in the tmp directory: + +
+i.eodag start=2022-05-25 end=2022-06-01 \
+    dataset=S2_MSI_L2A provider=cop_dataspace
+
+ +Download and extract only selected scenes into the download_here directory, +using a custom config file: + +
+i.eodag -e provider=cop_dataspace \
+    id="S2B_MSIL2A_20240526T080609_N0510_R078_T37SDD_20240526T094753,
+    S2B_MSIL2A_20240529T081609_N0510_R121_T37SED_20240529T124818" \
+    config=full/path/to/eodag/config.yaml \
+    output=download_here
+
+ +

REQUIREMENTS

+ + + +

SEE ALSO

+ + +i.landsat, +i.sentinel, +i.modis + + +

AUTHOR

+ +Hamed Elgizery, Giza, Egypt.
+

+GSoC 2024 Mentors: Luca Delucchi, Stefan Blumentrath, Veronica Andreo +

diff --git a/src/imagery/i.eodag/i.eodag.py b/src/imagery/i.eodag/i.eodag.py new file mode 100755 index 0000000000..e9929242eb --- /dev/null +++ b/src/imagery/i.eodag/i.eodag.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 + +############################################################################ +# +# MODULE: i.eodag +# +# AUTHOR(S): Hamed Elgizery +# MENTOR(S): Luca Delucchi, Veronica Andreo, Stefan Blumentrath +# +# PURPOSE: Downloads imagery datasets e.g. Landsat, Sentinel, and MODIS +# using EODAG API. +# COPYRIGHT: (C) 2024-2025 by Hamed Elgizery, and the GRASS development team +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. +# +############################################################################# + + +# %Module +# % description: Eodag interface to install imagery datasets from various providers. +# % keyword: imagery +# % keyword: eodag +# % keyword: sentinel +# % keyword: landsat +# % keyword: modis +# % keyword: datasets +# % keyword: download +# %end + +# %option +# % key: dataset +# % type: string +# % description: Imagery dataset to search for +# % required: yes +# % answer: S1_SAR_GRD +# % guisection: Filter +# %end + +# %option G_OPT_M_DIR +# % key: output +# % description: Name for output directory where to store downloaded data OR search results +# % required: no +# % guisection: Output +# %end + +# %option G_OPT_F_INPUT +# % key: config +# % label: Full path to yaml config file +# % required: no +# %end + +# %option +# % key: id +# % type: string +# % multiple: yes +# % description: List of scenes IDs to download +# % guisection: Filter +# %end + +# %option +# % key: provider +# % type: string +# % description: Available providers: https://eodag.readthedocs.io/en/stable/getting_started_guide/providers.html +# % required: yes +# % guisection: Filter +# %end + +# %option +# % key: start +# % type: string +# % description: Start date (in any ISO 8601 format), by default it is 60 days ago +# % guisection: Filter +# %end + +# %option +# % key: end +# % type: string +# % description: End date (in any ISO 8601 format) +# % guisection: Filter +# %end + +# %flag +# % key: l +# % description: List the search result without downloading +# %end + +# %flag +# % key: e +# % description: Extract the downloaded the datasets +# %end + +# %flag +# % key: d +# % description: Delete the product archieve after downloading +# %end + + +import sys +import os +import getpass +from pathlib import Path +from datetime import datetime, timedelta + +import grass.script as gs +from grass.exceptions import ParameterError + + +def create_dir(directory): + try: + Path(directory).mkdir(parents=True, exist_ok=True) + except: + gs.fatal(_("Could not create directory {}").format(dir)) + + +def get_bb(vector=None): + args = {} + if vector: + args["vector"] = vector + # are we in LatLong location? + kv = gs.parse_command("g.proj", flags="j") + if "+proj" not in kv: + gs.fatal(_("Unable to get bounding box: unprojected location not supported")) + if kv["+proj"] != "longlat": + info = gs.parse_command("g.region", flags="uplg", **args) + return { + "lonmin": info["nw_long"], + "latmin": info["sw_lat"], + "lonmax": info["ne_long"], + "latmax": info["nw_lat"], + } + info = gs.parse_command("g.region", flags="upg", **args) + return { + "lonmin": info["w"], + "latmin": info["s"], + "lonmax": info["e"], + "latmax": info["n"], + } + + +def download_by_id(query_id: str): + gs.verbose( + _( + "Searching for product ending with: {}".format( + query_id[-min(len(query_id), 8) :] + ) + ) + ) + product, count = dag.search(id=query_id) + if count != 1: + raise ParameterError("Product couldn't be uniquely identified") + if not product[0].properties["id"].startswith(query_id): + raise ParameterError("Product wasn't found") + gs.verbose( + _("Poduct ending with: {} is found.".format(query_id[-min(len(query_id), 8) :])) + ) + gs.verbose(_("Downloading...")) + dag.download(product[0]) + + +def download_by_ids(products_ids): + # Search in all recognized providers + for product_id in products_ids: + try: + download_by_id(product_id) + except ParameterError: + gs.error( + _( + "Product ending with: {}, failed to download".format( + product_id[-min(len(id), 8) :] + ) + ) + ) + + +def setup_environment_variables(): + os.environ["EODAG__{}__DOWNLOAD__EXTRACT".format(options["provider"])] = str( + flags["e"] + ) + os.environ["EODAG__{}__DOWNLOAD__DELETE_ARCHIV".format(options["provider"])] = str( + flags["d"] + ) + + if options["output"]: + os.environ[ + "EODAG__{}__DOWNLOAD__OUTPUTS_PREFIX".format(options["provider"]) + ] = options["output"] + + if options["config"]: + os.environ["EODAG_CFG_FILE"] = options["config"] + + +def normalize_time(datetime_str: str): + normalized_datetime = datetime.fromisoformat(datetime_str) + normalized_datetime = normalized_datetime.replace(microsecond=0) + normalized_datetime = normalized_datetime.replace(tzinfo=None) + return normalized_datetime.isoformat() + + +def no_fallback_search(search_parameters, provider): + try: + server_poke = dag.search(**search_parameters, provider=provider) + if server_poke[1] == 0: + gs.verbose(_("No products found")) + return SearchResult([]) + except Exception as e: + gs.verbose(e) + gs.fatal(_("Server error, try again.")) + + # https://eodag.readthedocs.io/en/stable/api_reference/core.html#eodag.api.core.EODataAccessGateway.search_iter_page + # This will use the prefered provider by default + search_result = dag.search_iter_page(**search_parameters) + + # TODO: Would it be useful if user could iterate through + # the pages manually, and look for the product themselves? + try: + return list(search_result)[0] + except Exception as e: + gs.verbose(e) + gs.fatal(_("Server error, try again.")) + + +def main(): + # products: https://github.com/CS-SI/eodag/blob/develop/eodag/resources/product_types.yml + + # setting the envirionmnets variables has to come before the dag initialization + setup_environment_variables() + + global dag + dag = EODataAccessGateway() + + if options["provider"]: + dag.set_preferred_provider(options["provider"]) + else: + gs.fatal(_("Please specify a provider...")) + + # Download by ids... if ids are provided only these ids will be downloaded + if options["id"]: + ids = options["id"].split(",") + download_by_ids(ids) + else: + + items_per_page = 20 + # TODO: Check that the product exists, + # could be handled by catching exceptions when searching... + product_type = options["dataset"] + + # TODO: Allow user to specify a shape file path + geom = ( + # use boudning box of current computational region + get_bb() + # { "lonmin": 1.9, "latmin": 43.9, "lonmax": 2, "latmax": 45, } # hardcoded for testing + ) + + gs.verbose(_("Region used for searching: {}".format(geom))) + + search_parameters = { + "items_per_page": items_per_page, + "productType": product_type, + # TODO: Convert to a shapely object + "geom": geom, + } + + # Assumes that the user enter time in UTC + end_date = options["end"] + if not options["end"]: + end_date = datetime.utcnow().isoformat() + try: + end_date = normalize_time(end_date) + except Exception as e: + gs.debug(e) + gs.fatal(_("Could not parse 'end' time.")) + + start_date = options["start"] + if not options["start"]: + delta_days = timedelta(60) + start_date = (datetime.fromisoformat(end_date) - delta_days).isoformat() + try: + start_date = normalize_time(start_date) + except Exception as e: + gs.debug(e) + gs.fatal(_("Could not parse 'start' time.")) + + if end_date < start_date: + gs.fatal( + _( + "End Date ({}) can not come before Start Date ({})".format( + end_date, start_date + ) + ) + ) + + # TODO: Requires further testing to make sure the isoformat works with all the providers + search_parameters["start"] = start_date + search_parameters["end"] = end_date + + search_results = no_fallback_search(search_parameters, options["provider"]) + + num_results = len(search_results) + gs.verbose( + _("Found {} matching scenes of type {}".format(num_results, product_type)) + ) + + if flags["l"]: + # TODO: Oragnize output format better + idx = 0 + for product in search_results: + print( + _( + "Product #{} - ID:{},provider:{}".format( + idx, product.properties["id"], product.provider + ) + ) + ) + idx += 1 + else: + # TODO: Consider adding a quicklook flag + # TODO: Add timeout and wait parameters for downloading offline products... + # https://eodag.readthedocs.io/en/stable/getting_started_guide/product_storage_status.html + dag.download_all(search_results) + + +if __name__ == "__main__": + gs.warning(_("Experimental Version...")) + gs.warning( + _( + "This module is still under development, and its behaviour is not guaranteed to be reliable" + ) + ) + options, flags = gs.parser() + + try: + from eodag import EODataAccessGateway + from eodag import setup_logging + from eodag.api.search_result import SearchResult + + debug_level = int(gs.read_command("g.gisenv", get="DEBUG")) + if not debug_level: + setup_logging(1) + elif debug_level == 1: + setup_logging(2) + else: + setup_logging(3) + except: + gs.fatal(_("Cannot import eodag. Please intall the library first.")) + + sys.exit(main())