1"""
2Main module for collecting data
3"""
4
5import datetime
6import os
7import logging
8import copy
9from abc import ABC, abstractmethod
10
11from .config import SHConfig
12from .ogc import OgcImageService
13from .fis import FisService
14from .geopedia import GeopediaWmsService, GeopediaImageService
15from .aws import AwsProduct, AwsTile
16from .aws_safe import SafeProduct, SafeTile
17from .data_collections import handle_deprecated_data_source
18from .download import DownloadRequest, DownloadClient, AwsDownloadClient, SentinelHubDownloadClient
19from .os_utils import make_folder
20from .constants import MimeType, CustomUrlParam, ServiceType, CRS, HistogramType
21from .data_collections import DataCollection
22
23LOGGER = logging.getLogger(__name__)
24
25
26class DataRequest(ABC):
27    """ Abstract class for all Sentinel Hub data requests.
28
29    Every data request type can write the fetched data to disk and then read it again (and hence avoid the need to
30    download the same data again).
31    """
32    def __init__(self, download_client_class, *, data_folder=None, config=None):
33        """
34        :param download_client_class: A class implementing a download client
35        :type download_client_class: type
36        :param data_folder: location of the directory where the fetched data will be saved.
37        :type data_folder: str
38        :param config: A custom instance of config class to override parameters from the saved configuration.
39        :type config: SHConfig or None
40        """
41        self.download_client_class = download_client_class
42        self.data_folder = data_folder
43        self.config = config or SHConfig()
44
45        self.download_list = []
46        self.folder_list = []
47        self.create_request()
48
49    @abstractmethod
50    def create_request(self):
51        """ An abstract method for logic of creating download requests
52        """
53        raise NotImplementedError
54
55    def get_download_list(self):
56        """
57        Returns a list of download requests for requested data.
58
59        :return: List of data to be downloaded
60        :rtype: list(sentinelhub.DownloadRequest)
61        """
62        return self.download_list
63
64    def get_filename_list(self):
65        """ Returns a list of file names (or paths relative to `data_folder`) where the requested data will be saved
66        or read from, if it has already been downloaded and saved.
67
68        :return: A list of filenames
69        :rtype: list(str)
70        """
71        return [request.get_relative_paths()[1] for request in self.download_list]
72
73    def get_url_list(self):
74        """
75        Returns a list of urls for requested data.
76
77        :return: List of URLs from where data will be downloaded.
78        :rtype: list(str)
79        """
80        return [request.url for request in self.download_list]
81
82    def is_valid_request(self):
83        """ Checks if initialized class instance successfully prepared a list of items to download
84
85        :return: `True` if request is valid and `False` otherwise
86        :rtype: bool
87        """
88        return isinstance(self.download_list, list) and \
89            all(isinstance(request, DownloadRequest) for request in self.download_list)
90
91    def get_data(self, *, save_data=False, redownload=False, data_filter=None, max_threads=None,
92                 decode_data=True, raise_download_errors=True):
93        """ Get requested data either by downloading it or by reading it from the disk (if it
94        was previously downloaded and saved).
95
96        :param save_data: flag to turn on/off saving of data to disk. Default is `False`.
97        :type save_data: bool
98        :param redownload: if `True`, download again the requested data even though it's already saved to disk.
99                            Default is `False`, do not download if data is already available on disk.
100        :type redownload: bool
101        :param data_filter: Used to specify which items will be returned by the method and in which order. E.g. with
102            ``data_filter=[0, 2, -1]`` the method will return only 1st, 3rd and last item. Default filter is `None`.
103        :type data_filter: list(int) or None
104        :param max_threads: Maximum number of threads to be used for download in parallel. The default is
105            `max_threads=None` which will use the number of processors on the system multiplied by 5.
106        :type max_threads: int or None
107        :param decode_data: If `True` (default) it decodes data (e.g., returns image as an array of numbers);
108            if `False` it returns binary data.
109        :type decode_data: bool
110        :param raise_download_errors: If `True` any error in download process should be raised as
111            ``DownloadFailedException``. If `False` failed downloads will only raise warnings and the method will
112            return list with `None` values in places where the results of failed download requests should be.
113        :type raise_download_errors: bool
114        :return: requested images as numpy arrays, where each array corresponds to a single acquisition and has
115                    shape ``[height, width, channels]``.
116        :rtype: list of numpy arrays
117        """
118        self._preprocess_request(save_data, True)
119        return self._execute_data_download(data_filter, redownload, max_threads, raise_download_errors,
120                                           decode_data=decode_data)
121
122    def save_data(self, *, data_filter=None, redownload=False, max_threads=None, raise_download_errors=False):
123        """ Saves data to disk. If ``redownload=True`` then the data is redownloaded using ``max_threads`` workers.
124
125        :param data_filter: Used to specify which items will be returned by the method and in which order. E.g. with
126            `data_filter=[0, 2, -1]` the method will return only 1st, 3rd and last item. Default filter is `None`.
127        :type data_filter: list(int) or None
128        :param redownload: data is redownloaded if ``redownload=True``. Default is `False`
129        :type redownload: bool
130        :param max_threads: Maximum number of threads to be used for download in parallel. The default is
131            `max_threads=None` which will use the number of processors on the system multiplied by 5.
132        :type max_threads: int or None
133        :param raise_download_errors: If `True` any error in download process should be raised as
134            ``DownloadFailedException``. If `False` failed downloads will only raise warnings.
135        :type raise_download_errors: bool
136        """
137        self._preprocess_request(True, False)
138        self._execute_data_download(data_filter, redownload, max_threads, raise_download_errors)
139
140    def _execute_data_download(self, data_filter, redownload, max_threads, raise_download_errors, decode_data=True):
141        """ Calls download module and executes the download process
142
143        :param data_filter: Used to specify which items will be returned by the method and in which order. E.g. with
144            `data_filter=[0, 2, -1]` the method will return only 1st, 3rd and last item. Default filter is `None`.
145        :type data_filter: list(int) or None
146        :param redownload: data is redownloaded if ``redownload=True``. Default is `False`
147        :type redownload: bool
148        :param max_threads: Maximum number of threads to be used for download in parallel. The default is
149            `max_threads=None` which will use the number of processors on the system multiplied by 5.
150        :type max_threads: int or None
151        :param raise_download_errors: If `True` any error in download process should be raised as
152            ``DownloadFailedException``. If `False` failed downloads will only raise warnings.
153        :type raise_download_errors: bool
154        :param decode_data: If `True` (default) it decodes data (e.g., returns image as an array of numbers);
155            if `False` it returns binary data.
156        :type decode_data: bool
157        :return: List of data obtained from download
158        :rtype: list
159        """
160        is_repeating_filter = False
161        if data_filter is None:
162            filtered_download_list = self.download_list
163        elif isinstance(data_filter, (list, tuple)):
164            try:
165                filtered_download_list = [self.download_list[index] for index in data_filter]
166            except IndexError as exception:
167                raise IndexError('Indices of data_filter are out of range') from exception
168
169            filtered_download_list, mapping_list = self._filter_repeating_items(filtered_download_list)
170            is_repeating_filter = len(filtered_download_list) < len(mapping_list)
171        else:
172            raise ValueError('data_filter parameter must be a list of indices')
173
174        client = self.download_client_class(
175            redownload=redownload,
176            raise_download_errors=raise_download_errors,
177            config=self.config
178        )
179        data_list = client.download(filtered_download_list, max_threads=max_threads, decode_data=decode_data)
180
181        if is_repeating_filter:
182            data_list = [copy.deepcopy(data_list[index]) for index in mapping_list]
183
184        return data_list
185
186    @staticmethod
187    def _filter_repeating_items(download_list):
188        """ Because of data_filter some requests in download list might be the same. In order not to download them again
189        this method will reduce the list of requests. It will also return a mapping list which can be used to
190        reconstruct the previous list of download requests.
191
192        :param download_list: List of download requests
193        :type download_list: list(sentinelhub.DownloadRequest)
194        :return: reduced download list with unique requests and mapping list
195        :rtype: (list(sentinelhub.DownloadRequest), list(int))
196        """
197        unique_requests_map = {}
198        mapping_list = []
199        unique_download_list = []
200        for download_request in download_list:
201            if download_request not in unique_requests_map:
202                unique_requests_map[download_request] = len(unique_download_list)
203                unique_download_list.append(download_request)
204            mapping_list.append(unique_requests_map[download_request])
205        return unique_download_list, mapping_list
206
207    def _preprocess_request(self, save_data, return_data):
208        """ Prepares requests for download and creates empty folders
209
210        :param save_data: Tells whether to save data or not
211        :type save_data: bool
212        :param return_data: Tells whether to return data or not
213        :type return_data: bool
214        """
215        if not self.is_valid_request():
216            raise ValueError('Cannot obtain data because request is invalid')
217
218        if save_data and self.data_folder is None:
219            raise ValueError('Request parameter `data_folder` is not specified. '
220                             'In order to save data please set `data_folder` to location on your disk.')
221
222        for download_request in self.download_list:
223            download_request.save_response = save_data
224            download_request.return_data = return_data
225            download_request.data_folder = self.data_folder
226
227        if save_data:
228            for folder in self.folder_list:
229                make_folder(os.path.join(self.data_folder, folder))
230
231
232class OgcRequest(DataRequest):
233    """ The base class for OGC-type requests (WMS and WCS) where all common parameters are defined
234    """
235    def __init__(self, layer, bbox, *, time='latest', service_type=None, data_collection=None,
236                 size_x=None, size_y=None, maxcc=1.0, image_format=MimeType.PNG, custom_url_params=None,
237                 time_difference=datetime.timedelta(seconds=-1), data_source=None, **kwargs):
238        """
239        :param layer: An ID of a layer configured in Sentinel Hub Dashboard. It has to be configured for the same
240            instance ID which will be used for this request. Also the satellite collection of the layer in Dashboard
241            must match the one given by `data_collection` parameter
242        :type layer: str
243        :param bbox: Bounding box of the requested image. Coordinates must be in the specified coordinate reference
244            system.
245        :type bbox: geometry.BBox
246        :param time: time or time range for which to return the results, in ISO8601 format
247            (year-month-date, for example: ``2016-01-01``, or year-month-dateThours:minutes:seconds format,
248            i.e. ``2016-01-01T16:31:21``). When a single time is specified the request will return data for that
249            specific date, if it exists. If a time range is specified the result is a list of all scenes between the
250            specified dates conforming to the cloud coverage criteria. Most recent acquisition being first in the list.
251            For the latest acquisition use ``latest``. Examples: ``latest``, ``'2016-01-01'``, or
252            ``('2016-01-01', ' 2016-01-31')``
253        :type time: str or (str, str) or datetime.date or (datetime.date, datetime.date) or datetime.datetime or
254            (datetime.datetime, datetime.datetime)
255        :param service_type: type of OGC service (WMS or WCS)
256        :type service_type: constants.ServiceType
257        :param data_collection: A collection of requested satellite data. It has to be the same as defined in
258            Sentinel Hub Dashboard for the given layer.
259        :type data_collection: DataCollection
260        :param size_x: number of pixels in x or resolution in x (i.e. ``512`` or ``10m``)
261        :type size_x: int or str
262        :param size_y: number of pixels in x or resolution in y (i.e. ``512`` or ``10m``)
263        :type size_y: int or str
264        :param maxcc: maximum accepted cloud coverage of an image. Float between 0.0 and 1.0. Default is ``1.0``.
265        :type maxcc: float
266        :param image_format: format of the returned image by the Sentinel Hub's WMS getMap service. Default is PNG, but
267                            in some cases 32-bit TIFF is required, i.e. if requesting unprocessed raw bands.
268                            Default is ``constants.MimeType.PNG``.
269        :type image_format: constants.MimeType
270        :param custom_url_params: A dictionary of CustomUrlParameters and their values supported by Sentinel Hub's WMS
271            and WCS services. All available parameters are described at
272            http://www.sentinel-hub.com/develop/documentation/api/custom-url-parameters. Note: in case of
273            `CustomUrlParam.EVALSCRIPT` the dictionary value must be a string of Javascript code that is not
274            encoded into base64.
275        :type custom_url_params: Dict[CustomUrlParameter, object]
276        :param time_difference: The time difference below which dates are deemed equal. That is, if for the given set
277            of OGC parameters the images are available at datestimes `d1<=d2<=...<=dn` then only those with
278            `dk-dj>time_difference` will be considered. The default time difference is negative (`-1s`), meaning
279            that all dates are considered by default.
280        :type time_difference: datetime.timedelta
281        :param data_folder: location of the directory where the fetched data will be saved.
282        :type data_folder: str
283        :param config: A custom instance of config class to override parameters from the saved configuration.
284        :type config: SHConfig or None
285        :param data_source: A deprecated alternative to data_collection
286        :type data_source: DataCollection
287        """
288        self.layer = layer
289        self.bbox = bbox
290        self.time = time
291        self.data_collection = DataCollection(handle_deprecated_data_source(data_collection, data_source,
292                                                                            default=DataCollection.SENTINEL2_L1C))
293        self.maxcc = maxcc
294        self.image_format = MimeType(image_format)
295        self.service_type = service_type
296        self.size_x = size_x
297        self.size_y = size_y
298        self.custom_url_params = custom_url_params
299        self.time_difference = time_difference
300
301        if self.custom_url_params is not None:
302            self._check_custom_url_parameters()
303
304        self.wfs_iterator = None
305
306        super().__init__(SentinelHubDownloadClient, **kwargs)
307
308    def _check_custom_url_parameters(self):
309        """ Checks if custom url parameters are valid parameters.
310
311        Throws ValueError if the provided parameter is not a valid parameter.
312        """
313        for param in self.custom_url_params:
314            if param not in CustomUrlParam:
315                raise ValueError(f'Parameter {param} is not a valid custom url parameter. Please check and fix.')
316
317        if self.service_type is ServiceType.FIS and CustomUrlParam.GEOMETRY in self.custom_url_params:
318            raise ValueError(f'{CustomUrlParam.GEOMETRY} should not be a custom url parameter of a FIS request')
319
320    def create_request(self, reset_wfs_iterator=False):
321        """ Set download requests
322
323        Create a list of DownloadRequests for all Sentinel-2 acquisitions within request's time interval and
324        acceptable cloud coverage.
325
326        :param reset_wfs_iterator: When re-running the method this flag is used to reset/keep existing ``wfs_iterator``
327            (i.e. instance of ``WebFeatureService`` class). If the iterator is not reset you don't have to repeat a
328            service call but tiles and dates will stay the same.
329        :type reset_wfs_iterator: bool
330        """
331        if reset_wfs_iterator:
332            self.wfs_iterator = None
333
334        ogc_service = OgcImageService(config=self.config)
335        self.download_list = ogc_service.get_request(self)
336        self.wfs_iterator = ogc_service.get_wfs_iterator()
337
338    def get_dates(self):
339        """ Get list of dates
340
341        List of all available Sentinel-2 acquisitions for given bbox with max cloud coverage and the specified
342        time interval. When a single time is specified the request will return that specific date, if it exists.
343        If a time range is specified the result is a list of all scenes between the specified dates conforming to
344        the cloud coverage criteria. Most recent acquisition being first in the list.
345
346        :return: list of all available Sentinel-2 acquisition times within request's time interval and
347                acceptable cloud coverage.
348        :rtype: list(datetime.datetime) or [None]
349        """
350        return OgcImageService(config=self.config).get_dates(self)
351
352    def get_tiles(self):
353        """ Returns iterator over info about all satellite tiles used for the OgcRequest
354
355        :return: Iterator of dictionaries containing info about all satellite tiles used in the request. In case of
356                 DataCollection.DEM it returns None.
357        :rtype: Iterator[dict] or None
358        """
359        return self.wfs_iterator
360
361
362class WmsRequest(OgcRequest):
363    """ Web Map Service request class
364
365    Creates an instance of Sentinel Hub WMS (Web Map Service) GetMap request,
366    which provides access to Sentinel-2's unprocessed bands (B01, B02, ..., B08, B8A, ..., B12)
367    or processed products such as true color imagery, NDVI, etc. The only difference is that in
368    the case of WMS request the user specifies the desired image size instead of its resolution.
369
370    It is required to specify at least one of `width` and `height` parameters. If only one of them is specified the
371    the other one will be calculated to best fit the bounding box ratio. If both of them are specified they will be used
372    no matter the bounding box ratio.
373
374    More info available at:
375    https://www.sentinel-hub.com/develop/documentation/api/ogc_api/wms-parameters
376    """
377    def __init__(self, *, width=None, height=None, **kwargs):
378        """
379        :param width: width (number of columns) of the returned image (array)
380        :type width: int or None
381        :param height: height (number of rows) of the returned image (array)
382        :type height: int or None
383        :param layer: An ID of a layer configured in Sentinel Hub Dashboard. It has to be configured for the same
384            instance ID which will be used for this request. Also the satellite collection of the layer in Dashboard
385            must match the one given by `data_collection` parameter
386        :type layer: str
387        :param bbox: Bounding box of the requested image. Coordinates must be in the specified coordinate reference
388            system.
389        :type bbox: geometry.BBox
390        :param time: time or time range for which to return the results, in ISO8601 format
391            (year-month-date, for example: ``2016-01-01``, or year-month-dateThours:minutes:seconds format,
392            i.e. ``2016-01-01T16:31:21``). When a single time is specified the request will return data for that
393            specific date, if it exists. If a time range is specified the result is a list of all scenes between the
394            specified dates conforming to the cloud coverage criteria. Most recent acquisition being first in the list.
395            For the latest acquisition use ``latest``. Examples: ``latest``, ``'2016-01-01'``, or
396            ``('2016-01-01', ' 2016-01-31')``
397        :type time: str or (str, str) or datetime.date or (datetime.date, datetime.date) or datetime.datetime or
398            (datetime.datetime, datetime.datetime)
399        :param data_collection: A collection of requested satellite data. It has to be the same as defined in
400            Sentinel Hub Dashboard for the given layer. Default is Sentinel-2 L1C.
401        :type data_collection: DataCollection
402        :param size_x: number of pixels in x or resolution in x (i.e. ``512`` or ``10m``)
403        :type size_x: int or str
404        :param size_y: number of pixels in x or resolution in y (i.e. ``512`` or ``10m``)
405        :type size_y: int or str
406        :param maxcc: maximum accepted cloud coverage of an image. Float between 0.0 and 1.0. Default is ``1.0``.
407        :type maxcc: float
408        :param image_format: format of the returned image by the Sentinel Hub's WMS getMap service. Default is PNG, but
409                            in some cases 32-bit TIFF is required, i.e. if requesting unprocessed raw bands.
410                            Default is ``constants.MimeType.PNG``.
411        :type image_format: constants.MimeType
412        :param custom_url_params: A dictionary of CustomUrlParameters and their values supported by Sentinel Hub's WMS
413            and WCS services. All available parameters are described at
414            http://www.sentinel-hub.com/develop/documentation/api/custom-url-parameters. Note: in case of
415            `CustomUrlParam.EVALSCRIPT` the dictionary value must be a string of Javascript code that is not
416            encoded into base64.
417        :type custom_url_params: Dict[CustomUrlParameter, object]
418        :param time_difference: The time difference below which dates are deemed equal. That is, if for the given set
419            of OGC parameters the images are available at datestimes `d1<=d2<=...<=dn` then only those with
420            `dk-dj>time_difference` will be considered. The default time difference is negative (`-1s`), meaning
421            that all dates are considered by default.
422        :type time_difference: datetime.timedelta
423        :param data_folder: location of the directory where the fetched data will be saved.
424        :type data_folder: str
425        :param config: A custom instance of config class to override parameters from the saved configuration.
426        :type config: SHConfig or None
427        :param data_source: A deprecated alternative to data_collection
428        :type data_source: DataCollection
429        """
430        super().__init__(service_type=ServiceType.WMS, size_x=width, size_y=height, **kwargs)
431
432
433class WcsRequest(OgcRequest):
434    """ Web Coverage Service request class
435
436    Creates an instance of Sentinel Hub WCS (Web Coverage Service) GetCoverage request,
437    which provides access to Sentinel-2's unprocessed bands (B01, B02, ..., B08, B8A, ..., B12)
438    or processed products such as true color imagery, NDVI, etc., as the WMS service. The
439    only difference is that in the case of WCS request the user specifies the desired
440    resolution of the image instead of its size.
441
442    More info available at:
443    https://www.sentinel-hub.com/develop/documentation/api/ogc_api/wcs-request
444    """
445    def __init__(self, *, resx='10m', resy='10m', **kwargs):
446        """
447        :param resx: resolution in x (resolution of a column) given in meters in the format (examples ``10m``,
448            ``20m``, ...). Default is ``10m``, which is the best native resolution of some Sentinel-2 bands.
449        :type resx: str
450        :param resy: resolution in y (resolution of a row) given in meters in the format
451            (examples ``10m``, ``20m``, ...). Default is ``10m``, which is the best native resolution of some
452            Sentinel-2 bands.
453        :type resy: str
454        :param layer: An ID of a layer configured in Sentinel Hub Dashboard. It has to be configured for the same
455            instance ID which will be used for this request. Also the satellite collection of the layer in Dashboard
456            must match the one given by `data_collection` parameter
457        :type layer: str
458        :param bbox: Bounding box of the requested image. Coordinates must be in the specified coordinate reference
459            system.
460        :type bbox: geometry.BBox
461        :param time: time or time range for which to return the results, in ISO8601 format
462            (year-month-date, for example: ``2016-01-01``, or year-month-dateThours:minutes:seconds format,
463            i.e. ``2016-01-01T16:31:21``). When a single time is specified the request will return data for that
464            specific date, if it exists. If a time range is specified the result is a list of all scenes between the
465            specified dates conforming to the cloud coverage criteria. Most recent acquisition being first in the list.
466            For the latest acquisition use ``latest``. Examples: ``latest``, ``'2016-01-01'``, or
467            ``('2016-01-01', ' 2016-01-31')``
468        :type time: str or (str, str) or datetime.date or (datetime.date, datetime.date) or datetime.datetime or
469            (datetime.datetime, datetime.datetime)
470        :param data_collection: A collection of requested satellite data. It has to be the same as defined in Sentinel
471            Hub Dashboard for the given layer. Default is Sentinel-2 L1C.
472        :type data_collection: DataCollection
473        :param size_x: number of pixels in x or resolution in x (i.e. ``512`` or ``10m``)
474        :type size_x: int or str
475        :param size_y: number of pixels in x or resolution in y (i.e. ``512`` or ``10m``)
476        :type size_y: int or str
477        :param maxcc: maximum accepted cloud coverage of an image. Float between 0.0 and 1.0. Default is ``1.0``.
478        :type maxcc: float
479        :param image_format: format of the returned image by the Sentinel Hub's WMS getMap service. Default is PNG, but
480                            in some cases 32-bit TIFF is required, i.e. if requesting unprocessed raw bands.
481                            Default is ``constants.MimeType.PNG``.
482        :type image_format: constants.MimeType
483        :param custom_url_params: A dictionary of CustomUrlParameters and their values supported by Sentinel Hub's WMS
484            and WCS services. All available parameters are described at
485            http://www.sentinel-hub.com/develop/documentation/api/custom-url-parameters. Note: in case of
486            `CustomUrlParam.EVALSCRIPT` the dictionary value must be a string of Javascript code that is not
487            encoded into base64.
488        :type custom_url_params: Dict[CustomUrlParameter, object]
489        :param time_difference: The time difference below which dates are deemed equal. That is, if for the given set
490            of OGC parameters the images are available at datestimes `d1<=d2<=...<=dn` then only those with
491            `dk-dj>time_difference` will be considered. The default time difference is negative (`-1s`), meaning
492            that all dates are considered by default.
493        :type time_difference: datetime.timedelta
494        :param data_folder: location of the directory where the fetched data will be saved.
495        :type data_folder: str
496        :param config: A custom instance of config class to override parameters from the saved configuration.
497        :type config: SHConfig or None
498        :param data_source: A deprecated alternative to data_collection
499        :type data_source: DataCollection
500        """
501        super().__init__(service_type=ServiceType.WCS, size_x=resx, size_y=resy, **kwargs)
502
503
504class FisRequest(OgcRequest):
505    """ The Statistical info (or feature info service, abbreviated FIS) request class
506
507    The Statistical info (or feature info service, abbreviated FIS), performs elementary statistical
508    computations---such as mean, standard deviation, and histogram approximating the distribution of reflectance
509    values---on remotely sensed data for a region specified in a given spatial reference system across different
510    bands and time ranges.
511
512    A quintessential usage example would be querying the service for basic statistics and the distribution of NDVI
513    values for a polygon representing an agricultural unit over a time range.
514
515    More info available at:
516    https://www.sentinel-hub.com/develop/documentation/api/ogc_api/wcs-request
517    """
518    def __init__(self, layer, time, geometry_list, *, resolution='10m', bins=None, histogram_type=None, **kwargs):
519        """
520        :param layer: An ID of a layer configured in Sentinel Hub Dashboard. It has to be configured for the same
521            instance ID which will be used for this request. Also the satellite collection of the layer in Dashboard
522            must match the one given by `data_collection` parameter
523        :type layer: str
524        :param time: time or time range for which to return the results, in ISO8601 format
525            (year-month-date, for example: ``2016-01-01``, or year-month-dateThours:minutes:seconds format,
526            i.e. ``2016-01-01T16:31:21``). Examples: ``'2016-01-01'``, or ``('2016-01-01', ' 2016-01-31')``
527        :type time: str or (str, str) or datetime.date or (datetime.date, datetime.date) or datetime.datetime or
528            (datetime.datetime, datetime.datetime)
529        :param geometry_list: A WKT representation of a geometry describing the region of interest.
530            Note that WCS 1.1.1 standard is used here, so for EPSG:4326 coordinates should be in latitude/longitude
531            order.
532        :type geometry_list: list, [geometry.Geometry or geometry.Bbox]
533        :param resolution: Specifies the spatial resolution, in meters per pixel, of the image from which the statistics
534            are to be estimated. When using CRS=EPSG:4326 one has to add the "m" suffix to
535            enforce resolution in meters per pixel (e.g. RESOLUTION=10m).
536        :type resolution: str
537        :param bins: The number of bins (a positive integer) in the histogram. If this parameter is absent no histogram
538            is computed.
539        :type bins: str
540        :param histogram_type: type of histogram
541        :type histogram_type: HistogramType
542        :param data_collection: A collection of requested satellite data. It has to be the same as defined in Sentinel
543            Hub Dashboard for the given layer. Default is Sentinel-2 L1C.
544        :type data_collection: DataCollection
545        :param maxcc: maximum accepted cloud coverage of an image. Float between 0.0 and 1.0. Default is ``1.0``.
546        :type maxcc: float
547        :param custom_url_params: Dictionary of CustomUrlParameters and their values supported by Sentinel Hub's WMS
548            and WCS services. All available parameters are described at
549            http://www.sentinel-hub.com/develop/documentation/api/custom-url-parameters. Note: in
550            case of constants.CustomUrlParam.EVALSCRIPT the dictionary value must be a string
551            of Javascript code that is not encoded into base64.
552        :type custom_url_params: Dict[CustomUrlParameter, object]
553        :param data_folder: location of the directory where the fetched data will be saved.
554        :type data_folder: str
555        :param config: A custom instance of config class to override parameters from the saved configuration.
556        :type config: SHConfig or None
557        :param data_source: A deprecated alternative to data_collection
558        :type data_source: DataCollection
559        """
560        self.geometry_list = geometry_list
561        self.resolution = resolution
562        self.bins = bins
563        self.histogram_type = HistogramType(histogram_type) if histogram_type else None
564
565        super().__init__(bbox=None, layer=layer, time=time, service_type=ServiceType.FIS, **kwargs)
566
567    def create_request(self):
568        """ Set download requests
569
570        Create a list of DownloadRequests for all Sentinel-2 acquisitions within request's time interval and
571        acceptable cloud coverage.
572        """
573        fis_service = FisService(config=self.config)
574        self.download_list = fis_service.get_request(self)
575
576    def get_dates(self):
577        """ This method is not supported for FIS request
578        """
579        raise NotImplementedError
580
581    def get_tiles(self):
582        """ This method is not supported for FIS request
583        """
584        raise NotImplementedError
585
586
587class GeopediaRequest(DataRequest):
588    """ The base class for Geopedia requests where all common parameters are defined.
589    """
590    def __init__(self, layer, service_type, *, bbox=None, theme=None, image_format=MimeType.PNG, **kwargs):
591        """
592        :param layer: Geopedia layer which contains requested data
593        :type layer: str
594        :param service_type: Type of the service, supported are ``ServiceType.WMS`` and ``ServiceType.IMAGE``
595        :type service_type: constants.ServiceType
596        :param bbox: Bounding box of the requested data
597        :type bbox: geometry.BBox
598        :param theme: Geopedia's theme endpoint string for which the layer is defined. Only required by WMS service.
599        :type theme: str
600        :param image_format: Format of the returned image by the Sentinel Hub's WMS getMap service. Default is
601            ``constants.MimeType.PNG``.
602        :type image_format: constants.MimeType
603        :param data_folder: Location of the directory where the fetched data will be saved.
604        :type data_folder: str
605        :param config: A custom instance of config class to override parameters from the saved configuration.
606        :type config: SHConfig or None
607        """
608        self.layer = layer
609        self.service_type = service_type
610
611        self.bbox = bbox
612        if bbox.crs is not CRS.POP_WEB:
613            raise ValueError('Geopedia Request at the moment supports only bounding boxes with coordinates in '
614                             f'{CRS.POP_WEB}')
615
616        self.theme = theme
617        self.image_format = MimeType(image_format)
618
619        super().__init__(DownloadClient, **kwargs)
620
621    @abstractmethod
622    def create_request(self):
623        raise NotImplementedError
624
625
626class GeopediaWmsRequest(GeopediaRequest):
627    """ Web Map Service request class for Geopedia
628
629    Creates an instance of Geopedia's WMS (Web Map Service) GetMap request, which provides access to WMS layers in
630    Geopedia.
631    """
632    def __init__(self, layer, theme, bbox, *, width=None, height=None, **kwargs):
633        """
634        :param layer: Geopedia layer which contains requested data
635        :type layer: str
636        :param theme: Geopedia's theme endpoint string for which the layer is defined.
637        :type theme: str
638        :param bbox: Bounding box of the requested data
639        :type bbox: geometry.BBox
640        :param width: width (number of columns) of the returned image (array)
641        :type width: int or None
642        :param height: height (number of rows) of the returned image (array)
643        :type height: int or None
644        :param image_format: Format of the returned image by the Sentinel Hub's WMS getMap service. Default is
645            ``constants.MimeType.PNG``.
646        :type image_format: constants.MimeType
647        :param data_folder: Location of the directory where the fetched data will be saved.
648        :type data_folder: str
649        :param config: A custom instance of config class to override parameters from the saved configuration.
650        :type config: SHConfig or None
651        """
652        self.size_x = width
653        self.size_y = height
654
655        super().__init__(layer=layer, theme=theme, bbox=bbox, service_type=ServiceType.WMS, **kwargs)
656
657    def create_request(self):
658        """ Set download requests
659
660        Create a list of DownloadRequests for all Sentinel-2 acquisitions within request's time interval and
661        acceptable cloud coverage.
662        """
663        gpd_service = GeopediaWmsService(config=self.config)
664        self.download_list = gpd_service.get_request(self)
665
666
667class GeopediaImageRequest(GeopediaRequest):
668    """ Request to access data in a Geopedia vector / raster layer.
669    """
670    def __init__(self, *, image_field_name, keep_image_names=True, gpd_session=None, **kwargs):
671        """
672        :param image_field_name: Name of the field in the data table which holds images
673        :type image_field_name: str
674        :param keep_image_names: If `True` images will be saved with the same names as in Geopedia otherwise Geopedia
675            hashes will be used as names. If there are multiple images with the same names in the Geopedia layer this
676            parameter should be set to `False` to prevent images being overwritten.
677        :type keep_image_names: bool
678        :param layer: Geopedia layer which contains requested data
679        :type layer: str
680        :param bbox: Bounding box of the requested data
681        :type bbox: geometry.BBox
682        :param image_format: Format of the returned image by the Sentinel Hub's WMS getMap service. Default is
683            ``constants.MimeType.PNG``.
684        :type image_format: constants.MimeType
685        :param gpd_session: Optional parameter for specifying a custom Geopedia session, which can also contain login
686            credentials. This can be used for accessing private Geopedia layers. By default it is set to `None` and a
687            basic Geopedia session without credentials will be created.
688        :type gpd_session: GeopediaSession or None
689        :param data_folder: Location of the directory where the fetched data will be saved.
690        :type data_folder: str
691        :param config: A custom instance of config class to override parameters from the saved configuration.
692        :type config: SHConfig or None
693        """
694        self.image_field_name = image_field_name
695        self.keep_image_names = keep_image_names
696        self.gpd_session = gpd_session
697
698        self.gpd_iterator = None
699
700        super().__init__(service_type=ServiceType.IMAGE, **kwargs)
701
702    def create_request(self, reset_gpd_iterator=False):
703        """ Set a list of download requests
704
705        Set a list of DownloadRequests for all images that are under the
706        given property of the Geopedia's Vector layer.
707
708        :param reset_gpd_iterator: When re-running the method this flag is used to reset/keep existing ``gpd_iterator``
709            (i.e. instance of ``GeopediaFeatureIterator`` class). If the iterator is not reset you don't have to
710            repeat a service call but tiles and dates will stay the same.
711        :type reset_gpd_iterator: bool
712        """
713        if reset_gpd_iterator:
714            self.gpd_iterator = None
715
716        gpd_service = GeopediaImageService(config=self.config)
717        self.download_list = gpd_service.get_request(self)
718        self.gpd_iterator = gpd_service.get_gpd_iterator()
719
720    def get_items(self):
721        """ Returns iterator over info about data used for this request
722
723        :return: Iterator of dictionaries containing info about data used in
724                 this request.
725        :rtype: Iterator[dict] or None
726        """
727        return self.gpd_iterator
728
729
730class AwsRequest(DataRequest):
731    """ The base class for Amazon Web Service request classes. Common parameters are defined here.
732
733    Collects and provides data from AWS.
734
735    AWS database is available at:
736    http://sentinel-s2-l1c.s3-website.eu-central-1.amazonaws.com/
737    """
738    def __init__(self, *, bands=None, metafiles=None, safe_format=False, **kwargs):
739        """
740        :param bands: List of Sentinel-2 bands for request. If `None` all bands will be obtained
741        :type bands: list(str) or None
742        :param metafiles: list of additional metafiles available on AWS
743                          (e.g. ``['metadata', 'tileInfo', 'preview/B01', 'TCI']``)
744        :type metafiles: list(str)
745        :param safe_format: flag that determines the structure of saved data. If `True` it will be saved in .SAFE format
746                            defined by ESA. If `False` it will be saved in the same structure as the structure at AWS.
747        :type safe_format: bool
748        :param data_folder: location of the directory where the fetched data will be saved.
749        :type data_folder: str
750        :param config: A custom instance of config class to override parameters from the saved configuration.
751        :type config: SHConfig or None
752        """
753        self.bands = bands
754        self.metafiles = metafiles
755        self.safe_format = safe_format
756
757        self.aws_service = None
758        super().__init__(AwsDownloadClient, **kwargs)
759
760    @abstractmethod
761    def create_request(self):
762        raise NotImplementedError
763
764    def get_aws_service(self):
765        """
766        :return: initialized AWS service class
767        :rtype: aws.AwsProduct or aws.AwsTile or aws_safe.SafeProduct or aws_safe.SafeTile
768        """
769        return self.aws_service
770
771
772class AwsProductRequest(AwsRequest):
773    """ AWS Service request class for an ESA product
774
775    List of available products:
776    http://sentinel-s2-l1c.s3-website.eu-central-1.amazonaws.com/#products/
777    """
778    def __init__(self, product_id, *, tile_list=None, **kwargs):
779        """
780        :param product_id: original ESA product identification string
781            (e.g. ``'S2A_MSIL1C_20170414T003551_N0204_R016_T54HVH_20170414T003551'``)
782        :type product_id: str
783        :param tile_list: list of tiles inside the product to be downloaded. If parameter is set to `None` all
784            tiles inside the product will be downloaded.
785        :type tile_list: list(str) or None
786        :param bands: List of Sentinel-2 bands for request. If `None` all bands will be obtained
787        :type bands: list(str) or None
788        :param metafiles: list of additional metafiles available on AWS
789            (e.g. ``['metadata', 'tileInfo', 'preview/B01', 'TCI']``)
790        :type metafiles: list(str)
791        :param safe_format: flag that determines the structure of saved data. If `True` it will be saved in .SAFE format
792            defined by ESA. If `False` it will be saved in the same structure as the structure at AWS.
793        :type safe_format: bool
794        :param data_folder: location of the directory where the fetched data will be saved.
795        :type data_folder: str
796        :param config: A custom instance of config class to override parameters from the saved configuration.
797        :type config: SHConfig or None
798        """
799        self.product_id = product_id
800        self.tile_list = tile_list
801
802        super().__init__(**kwargs)
803
804    def create_request(self):
805        if self.safe_format:
806            self.aws_service = SafeProduct(self.product_id, tile_list=self.tile_list, bands=self.bands,
807                                           metafiles=self.metafiles, config=self.config)
808        else:
809            self.aws_service = AwsProduct(self.product_id, tile_list=self.tile_list, bands=self.bands,
810                                          metafiles=self.metafiles, config=self.config)
811
812        self.download_list, self.folder_list = self.aws_service.get_requests()
813
814
815class AwsTileRequest(AwsRequest):
816    """ AWS Service request class for an ESA tile
817
818    List of available products:
819    http://sentinel-s2-l1c.s3-website.eu-central-1.amazonaws.com/#tiles/
820    """
821    def __init__(self, *, tile=None, time=None, aws_index=None, data_collection=None, data_source=None, **kwargs):
822        """
823        :param tile: tile name (e.g. ``'T10UEV'``)
824        :type tile: str
825        :param time: tile sensing time in ISO8601 format
826        :type time: str
827        :param aws_index: there exist Sentinel-2 tiles with the same tile and time parameter. Therefore each tile on
828            AWS also has an index which is visible in their url path. If aws_index is set to `None` the class
829            will try to find the index automatically. If there will be multiple choices it will choose the
830            lowest index and inform the user.
831        :type aws_index: int or None
832        :param data_collection: A collection of requested AWS data. Supported collections are Sentinel-2 L1C and
833            Sentinel-2 L2A.
834        :type data_collection: DataCollection
835        :param bands: List of Sentinel-2 bands for request. If `None` all bands will be obtained
836        :type bands: list(str) or None
837        :param metafiles: list of additional metafiles available on AWS
838            (e.g. ``['metadata', 'tileInfo', 'preview/B01', 'TCI']``)
839        :type metafiles: list(str)
840        :param safe_format: flag that determines the structure of saved data. If `True` it will be saved in .SAFE
841            format defined by ESA. If `False` it will be saved in the same structure as the structure at AWS.
842        :type safe_format: bool
843        :param data_folder: location of the directory where the fetched data will be saved.
844        :type data_folder: str
845        :param config: A custom instance of config class to override parameters from the saved configuration.
846        :type config: SHConfig or None
847        :param data_source: A deprecated alternative to data_collection
848        :type data_source: DataCollection
849        """
850        self.tile = tile
851        self.time = time
852        self.aws_index = aws_index
853        self.data_collection = DataCollection(handle_deprecated_data_source(data_collection, data_source,
854                                                                            default=DataCollection.SENTINEL2_L1C))
855
856        super().__init__(**kwargs)
857
858    def create_request(self):
859        if self.safe_format:
860            self.aws_service = SafeTile(self.tile, self.time, self.aws_index, bands=self.bands,
861                                        metafiles=self.metafiles, data_collection=self.data_collection,
862                                        config=self.config)
863        else:
864            self.aws_service = AwsTile(self.tile, self.time, self.aws_index, bands=self.bands,
865                                       metafiles=self.metafiles, data_collection=self.data_collection,
866                                       config=self.config)
867
868        self.download_list, self.folder_list = self.aws_service.get_requests()
869
870
871def get_safe_format(product_id=None, tile=None, entire_product=False, bands=None,
872                    data_collection=None, data_source=None):
873    """ Returns .SAFE format structure in form of nested dictionaries. Either ``product_id`` or ``tile`` must be
874    specified.
875
876    :param product_id: original ESA product identification string. Default is `None`
877    :type product_id: str
878    :param tile: tuple containing tile name and sensing time/date. Default is `None`
879    :type tile: (str, str)
880    :param entire_product: in case tile is specified this flag determines if it will be place inside a .SAFE structure
881        of the product. Default is `False`
882    :type entire_product: bool
883    :param bands: list of bands to download. If `None` all bands will be downloaded. Default is `None`
884    :type bands: list(str) or None
885    :param data_collection: In case of tile request the collection of satellite data has to be specified.
886    :type data_collection: DataCollection
887    :param data_source: A deprecated alternative to data_collection
888    :type data_source: DataCollection
889    :return: Nested dictionaries representing .SAFE structure.
890    :rtype: dict
891    """
892    data_collection = handle_deprecated_data_source(data_collection, data_source)
893
894    entire_product = entire_product and product_id is None
895    if tile is not None:
896        safe_tile = SafeTile(tile_name=tile[0], time=tile[1], bands=bands, data_collection=data_collection)
897        if not entire_product:
898            return safe_tile.get_safe_struct()
899        product_id = safe_tile.get_product_id()
900    if product_id is None:
901        raise ValueError('Either product_id or tile must be specified')
902    safe_product = SafeProduct(product_id, tile_list=[tile[0]], bands=bands) if entire_product else \
903        SafeProduct(product_id, bands=bands)
904    return safe_product.get_safe_struct()
905
906
907def download_safe_format(product_id=None, tile=None, folder='.', redownload=False, entire_product=False, bands=None,
908                         data_collection=None, data_source=None):
909    """ Downloads .SAFE format structure in form of nested dictionaries. Either ``product_id`` or ``tile`` must
910    be specified.
911
912    :param product_id: original ESA product identification string. Default is `None`
913    :type product_id: str
914    :param tile: tuple containing tile name and sensing time/date. Default is `None`
915    :type tile: (str, str)
916    :param folder: location of the directory where the fetched data will be saved. Default is ``'.'``
917    :type folder: str
918    :param redownload: if `True`, download again the requested data even though it's already saved to disk. If
919        `False`, do not download if data is already available on disk. Default is `False`
920    :type redownload: bool
921    :param entire_product: in case tile is specified this flag determines if it will be place inside a .SAFE structure
922        of the product. Default is `False`
923    :type entire_product: bool
924    :param bands: list of bands to download. If `None` all bands will be downloaded. Default is `None`
925    :type bands: list(str) or None
926    :param data_collection: In case of tile request the collection of satellite data has to be specified.
927    :type data_collection: DataCollection
928    :param data_source: A deprecated alternative to data_collection
929    :type data_source: DataCollection
930    :return: Nested dictionaries representing .SAFE structure.
931    :rtype: dict
932    """
933    data_collection = handle_deprecated_data_source(data_collection, data_source)
934
935    entire_product = entire_product and product_id is None
936    if tile is not None:
937        safe_request = AwsTileRequest(tile=tile[0], time=tile[1], data_folder=folder, bands=bands,
938                                      safe_format=True, data_collection=data_collection)
939        if entire_product:
940            safe_tile = safe_request.get_aws_service()
941            product_id = safe_tile.get_product_id()
942    if product_id is not None:
943        safe_request = AwsProductRequest(product_id, tile_list=[tile[0]], data_folder=folder, bands=bands,
944                                         safe_format=True) if entire_product else \
945            AwsProductRequest(product_id, data_folder=folder, bands=bands, safe_format=True)
946
947    safe_request.save_data(redownload=redownload)
948