1"""
2Module defining constants and enumerate types used in the package
3"""
4import functools
5import itertools as it
6import mimetypes
7import re
8import warnings
9from enum import Enum, EnumMeta
10
11import utm
12import pyproj
13from aenum import extend_enum
14
15from .exceptions import SHUserWarning
16from ._version import __version__
17
18
19class PackageProps:
20    """ Class for obtaining package properties. Currently it supports obtaining package version."""
21
22    @staticmethod
23    def get_version():
24        """ Returns package version
25
26        :return: package version
27        :rtype: str
28        """
29        return __version__
30
31
32class ServiceUrl:
33    """ Most commonly used Sentinel Hub service URLs
34    """
35    MAIN = 'https://services.sentinel-hub.com'
36    USWEST = 'https://services-uswest2.sentinel-hub.com'
37    CREODIAS = 'https://creodias.sentinel-hub.com'
38    EOCLOUD = 'http://services.eocloud.sentinel-hub.com'
39    MUNDI = 'https://shservices.mundiwebservices.com'
40
41
42class ServiceType(Enum):
43    """ Enum constant class for type of service
44
45    Supported types are WMS, WCS, WFS, AWS, IMAGE
46    """
47    WMS = 'wms'
48    WCS = 'wcs'
49    WFS = 'wfs'
50    AWS = 'aws'
51    IMAGE = 'image'
52    FIS = 'fis'
53    PROCESSING_API = 'processing'
54
55
56class CRSMeta(EnumMeta):
57    """ Metaclass used for building CRS Enum class
58    """
59    _UNSUPPORTED_CRS = pyproj.CRS(4326)
60
61    def __new__(mcs, cls, bases, classdict):
62        """ This is executed at the beginning of runtime when CRS class is created
63        """
64        for direction, direction_value in [('N', '6'), ('S', '7')]:
65            for zone in range(1, 61):
66                classdict[f'UTM_{zone}{direction}'] = f'32{direction_value}{zone:02}'
67
68        return super().__new__(mcs, cls, bases, classdict)
69
70    def __call__(cls, crs_value, *args, **kwargs):
71        """ This is executed whenever CRS('something') is called
72        """
73        # pylint: disable=signature-differs
74        crs_value = cls._parse_crs(crs_value)
75
76        if isinstance(crs_value, str) and not cls.has_value(crs_value) and crs_value.isdigit() and len(crs_value) >= 4:
77            crs_name = f'EPSG_{crs_value}'
78            extend_enum(cls, crs_name, crs_value)
79
80        return super().__call__(crs_value, *args, **kwargs)
81
82    @staticmethod
83    def _parse_crs(value):
84        """ Method for parsing different inputs representing the same CRS enum. Examples:
85
86        - 4326
87        - 'EPSG:3857'
88        - {'init': 32633}
89        - geojson['crs']['properties']['name'] string (urn:ogc:def:crs:...)
90        - pyproj.CRS(32743)
91        """
92        if isinstance(value, dict) and 'init' in value:
93            value = value['init']
94        if isinstance(value, pyproj.CRS):
95            if value == CRSMeta._UNSUPPORTED_CRS:
96                message = 'sentinelhub-py supports only WGS 84 coordinate reference system with ' \
97                          'coordinate order lng-lat. Given pyproj.CRS(4326) has coordinate order lat-lng. Be careful ' \
98                          'to use the correct order of coordinates.'
99                warnings.warn(message, category=SHUserWarning)
100
101            epsg_code = value.to_epsg()
102            if epsg_code is not None:
103                return str(epsg_code)
104
105            error_message = f'Failed to determine an EPSG code of the given CRS:\n{repr(value)}'
106            maybe_epsg = value.to_epsg(min_confidence=0)
107            if maybe_epsg is not None:
108                error_message = f'{error_message}\nIt might be EPSG {maybe_epsg} but pyproj is not confident ' \
109                                'enough.'
110            raise ValueError(error_message)
111
112        if isinstance(value, int):
113            return str(value)
114        if isinstance(value, str):
115            if 'urn:ogc:def:crs' in value.lower():
116                crs_template = re.compile(r'urn:ogc:def:crs:.+::(?P<code>.+)', re.IGNORECASE)
117                value = crs_template.match(value).group("code")
118            if value.upper() == 'CRS84':
119                return '4326'
120            return value.lower().strip('epsg: ')
121        return value
122
123
124class CRS(Enum, metaclass=CRSMeta):
125    """ Coordinate Reference System enumerate class
126
127    Available CRS constants are WGS84, POP_WEB (i.e. Popular Web Mercator) and constants in form UTM_<zone><direction>,
128    where zone is an integer from [1, 60] and direction is either N or S (i.e. northern or southern hemisphere)
129    """
130    WGS84 = '4326'
131    POP_WEB = '3857'
132    #: UTM enum members are defined in CRSMeta.__new__
133
134    def __str__(self):
135        """ Method for casting CRS enum into string
136        """
137        return self.ogc_string()
138
139    def __repr__(self):
140        """ Method for retrieving CRS enum representation
141        """
142        return f"CRS('{self.value}')"
143
144    @classmethod
145    def has_value(cls, value):
146        """ Tests whether CRS contains a constant defined with string `value`.
147
148        :param value: The string representation of the enum constant.
149        :type value: str
150        :return: `True` if there exists a constant with string value `value`, `False` otherwise
151        :rtype: bool
152        """
153        return value in cls._value2member_map_
154
155    @property
156    def epsg(self):
157        """ EPSG code property
158
159        :return: EPSG code of given CRS
160        :rtype: int
161        """
162        return int(self.value)
163
164    def ogc_string(self):
165        """ Returns a string of the form authority:id representing the CRS.
166
167        :param self: An enum constant representing a coordinate reference system.
168        :type self: CRS
169        :return: A string representation of the CRS.
170        :rtype: str
171        """
172        return f'EPSG:{CRS(self).value}'
173
174    @property
175    def opengis_string(self):
176        """ Returns an URL to OGC webpage where the CRS is defined
177
178        :return: An URL with CRS definition
179        :rtype: str
180        """
181        return f'http://www.opengis.net/def/crs/EPSG/0/{self.epsg}'
182
183    def is_utm(self):
184        """ Checks if crs is one of the 64 possible UTM coordinate reference systems.
185
186        :param self: An enum constant representing a coordinate reference system.
187        :type self: CRS
188        :return: `True` if crs is UTM and `False` otherwise
189        :rtype: bool
190        """
191        return self.name.startswith('UTM')
192
193    @functools.lru_cache(maxsize=5)
194    def projection(self):
195        """ Returns a projection in form of pyproj class. For better time performance it will cache results of
196        5 most recently used CRS classes.
197
198        :return: pyproj projection class
199        :rtype: pyproj.Proj
200        """
201        return pyproj.Proj(self._get_pyproj_projection_def(), preserve_units=True)
202
203    @functools.lru_cache(maxsize=5)
204    def pyproj_crs(self):
205        """ Returns a pyproj CRS class. For better time performance it will cache results of
206        5 most recently used CRS classes.
207
208        :return: pyproj CRS class
209        :rtype: pyproj.CRS
210        """
211        return pyproj.CRS(self._get_pyproj_projection_def())
212
213    @functools.lru_cache(maxsize=10)
214    def get_transform_function(self, other, always_xy=True):
215        """ Returns a function for transforming geometrical objects from one CRS to another. The function will support
216        transformations between any objects that pyproj supports.
217        For better time performance this method will cache results of 10 most recently used pairs of CRS classes.
218
219        :param self: Initial CRS
220        :type self: CRS
221        :param other: Target CRS
222        :type other: CRS
223        :param always_xy: Parameter that is passed to `pyproj.Transformer` object and defines axis order for
224            transformation. The default value `True` is in most cases the correct one.
225        :type always_xy: bool
226        :return: A projection function obtained from pyproj package
227        :rtype: function
228        """
229        return pyproj.Transformer.from_proj(self.projection(), other.projection(), always_xy=always_xy).transform
230
231    @staticmethod
232    def get_utm_from_wgs84(lng, lat):
233        """ Convert from WGS84 to UTM coordinate system
234
235        :param lng: Longitude
236        :type lng: float
237        :param lat: Latitude
238        :type lat: float
239        :return: UTM coordinates
240        :rtype: tuple
241        """
242        _, _, zone, _ = utm.from_latlon(lat, lng)
243        direction = 'N' if lat >= 0 else 'S'
244        return CRS[f'UTM_{zone}{direction}']
245
246    def _get_pyproj_projection_def(self):
247        """ Returns a pyproj crs definition
248
249        For WGS 84 it ensures lng-lat order
250        """
251        return '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs' if self is CRS.WGS84 else self.ogc_string()
252
253
254class CustomUrlParam(Enum):
255    """ Enum class to represent supported custom url parameters of OGC services
256
257    Supported parameters are `SHOWLOGO`, `EVALSCRIPT`, `EVALSCRIPTURL`, `PREVIEW`, `QUALITY`, `UPSAMPLING`,
258    `DOWNSAMPLING`, `GEOMETRY` and `WARNINGS`.
259
260    See http://sentinel-hub.com/develop/documentation/api/custom-url-parameters and
261    https://www.sentinel-hub.com/develop/documentation/api/ogc_api/wms-parameters for more information.
262    """
263    SHOWLOGO = 'ShowLogo'
264    EVALSCRIPT = 'EvalScript'
265    EVALSCRIPTURL = 'EvalScriptUrl'
266    PREVIEW = 'Preview'
267    QUALITY = 'Quality'
268    UPSAMPLING = 'Upsampling'
269    DOWNSAMPLING = 'Downsampling'
270    GEOMETRY = 'Geometry'
271    MINQA = 'MinQA'
272
273    @classmethod
274    def has_value(cls, value):
275        """ Tests whether CustomUrlParam contains a constant defined with a string `value`
276
277        :param value: The string representation of the enum constant
278        :type value: str
279        :return: `True` if there exists a constant with a string value `value`, `False` otherwise
280        :rtype: bool
281        """
282        return any(value.lower() == item.value.lower() for item in cls)
283
284    @staticmethod
285    def get_string(param):
286        """ Get custom url parameter name as string
287
288        :param param: CustomUrlParam enum constant
289        :type param: Enum constant
290        :return: String describing the file format
291        :rtype: str
292        """
293        return param.value
294
295
296class HistogramType(Enum):
297    """ Enum class for types of histogram supported by Sentinel Hub FIS service
298
299    Supported histogram types are EQUALFREQUENCY, EQUIDISTANT and STREAMING
300    """
301    EQUALFREQUENCY = 'equalfrequency'
302    EQUIDISTANT = 'equidistant'
303    STREAMING = 'streaming'
304
305
306class MimeType(Enum):
307    """ Enum class to represent supported file formats
308
309    Supported file formats are TIFF 8-bit, TIFF 16-bit, TIFF 32-bit float, PNG, JPEG, JPEG2000, JSON, CSV, ZIP, HDF5,
310    XML, GML, RAW
311    """
312    TIFF = 'tiff'
313    PNG = 'png'
314    JPG = 'jpg'
315    JP2 = 'jp2'
316    JSON = 'json'
317    CSV = 'csv'
318    ZIP = 'zip'
319    HDF = 'hdf'
320    XML = 'xml'
321    GML = 'gml'
322    TXT = 'txt'
323    TAR = 'tar'
324    RAW = 'raw'
325    SAFE = 'safe'
326
327    @property
328    def extension(self):
329        """ Returns file extension of the MimeType object
330
331        :returns: A file extension string
332        :rtype: str
333        """
334        return self.value
335
336    @staticmethod
337    def from_string(mime_type_str):
338        """ Parses mime type from a file extension string
339
340        :param mime_type_str: A file extension string
341        :type mime_type_str: str
342        :return: A mime type enum
343        :rtype: MimeType
344        """
345
346        # These two cases are handled seperately due to issues with python 3.6
347        if mime_type_str == 'image/jpeg':
348            return MimeType.JPG
349        if mime_type_str == 'text/plain':
350            return MimeType.TXT
351
352        guessed_extension = mimetypes.guess_extension(mime_type_str)
353        if guessed_extension:
354            mime_type_str = guessed_extension.strip('.')
355        else:
356            mime_type_str = mime_type_str.split('/')[-1]
357
358        if MimeType.has_value(mime_type_str):
359            return MimeType(mime_type_str)
360
361        try:
362            return {
363                'tif': MimeType.TIFF,
364                'jpeg': MimeType.JPG,
365                'hdf5': MimeType.HDF,
366                'h5': MimeType.HDF
367            }[mime_type_str]
368        except KeyError as exception:
369            raise ValueError(f'Data format {mime_type_str} is not supported') from exception
370
371    def is_image_format(self):
372        """ Checks whether file format is an image format
373
374        Example: ``MimeType.PNG.is_image_format()`` or ``MimeType.is_image_format(MimeType.PNG)``
375
376        :param self: File format
377        :type self: MimeType
378        :return: `True` if file is in image format, `False` otherwise
379        :rtype: bool
380        """
381        return self in frozenset([MimeType.TIFF, MimeType.PNG, MimeType.JP2, MimeType.JPG])
382
383    def is_api_format(self):
384        """ Checks if mime type is supported by Sentinel Hub API
385
386        :return: True if API supports this format and False otherwise
387        :rtype: bool
388        """
389        return self in frozenset([MimeType.JPG, MimeType.PNG, MimeType.TIFF, MimeType.JSON])
390
391    @classmethod
392    def has_value(cls, value):
393        """ Tests whether MimeType contains a constant defined with string ``value``
394
395        :param value: The string representation of the enum constant
396        :type value: str
397        :return: `True` if there exists a constant with string value ``value``, `False` otherwise
398        :rtype: bool
399        """
400        return value in cls._value2member_map_
401
402    def get_string(self):
403        """ Get file format as string
404
405        :return: String describing the file format
406        :rtype: str
407        """
408        if self is MimeType.TAR:
409            return 'application/x-tar'
410        if self is MimeType.JSON:
411            return 'application/json'
412        if self is MimeType.JP2:
413            return 'image/jpeg2000'
414        if self is MimeType.XML:
415            return 'text/xml'
416        if self is MimeType.RAW:
417            return self.value
418        return mimetypes.types_map['.' + self.value]
419
420    def get_expected_max_value(self):
421        """ Returns max value of image `MimeType` format and raises an error if it is not an image format
422
423        :return: A maximum value of specified image format
424        :rtype: int or float
425        :raises: ValueError
426        """
427        try:
428            return {
429                MimeType.TIFF: 65535,
430                MimeType.PNG: 255,
431                MimeType.JPG: 255,
432                MimeType.JP2: 10000
433            }[self]
434        except KeyError as exception:
435            raise ValueError(f'Type {self} is not supported by this method') from exception
436
437
438class RequestType(Enum):
439    """ Enum constant class for GET/POST request type """
440    GET = 'GET'
441    POST = 'POST'
442    DELETE = 'DELETE'
443    PUT = 'PUT'
444    PATCH = 'PATCH'
445
446
447class SHConstants:
448    """ Initialisation of constants used by OGC request.
449
450        Constants are LATEST
451    """
452    LATEST = 'latest'
453    HEADERS = {'User-Agent': f'sentinelhub-py/v{PackageProps.get_version()}'}
454
455
456class AwsConstants:
457    """ Initialisation of every constant used by AWS classes
458
459    For each supported data collection it contains lists of all possible bands and all possible metadata files:
460
461        - S2_L1C_BANDS and S2_L1C_METAFILES
462        - S2_L2A_BANDS and S2_L2A_METAFILES
463
464    It also contains dictionary of all possible files and their formats: AWS_FILES
465    """
466    # General constants:
467    SOURCE_ID_LIST = ['L1C', 'L2A']
468    TILE_INFO = 'tileInfo'
469    PRODUCT_INFO = 'productInfo'
470    METADATA = 'metadata'
471    PREVIEW = 'preview'
472    PREVIEW_JP2 = 'preview*'
473    QI_LIST = ['DEFECT', 'DETFOO', 'NODATA', 'SATURA', 'TECQUA']
474    QI_MSK_CLOUD = 'qi/MSK_CLOUDS_B00'
475    AUX_DATA = 'AUX_DATA'
476    DATASTRIP = 'DATASTRIP'
477    GRANULE = 'GRANULE'
478    HTML = 'HTML'
479    INFO = 'rep_info'
480    QI_DATA = 'QI_DATA'
481    IMG_DATA = 'IMG_DATA'
482    INSPIRE = 'inspire'
483    MANIFEST = 'manifest'
484    TCI = 'TCI'
485    PVI = 'PVI'
486    ECMWFT = 'auxiliary/ECMWFT'
487    AUX_ECMWFT = 'auxiliary/AUX_ECMWFT'
488    DATASTRIP_METADATA = 'datastrip/*/metadata'
489
490    # More constants about L2A
491    AOT = 'AOT'
492    WVP = 'WVP'
493    SCL = 'SCL'
494    VIS = 'VIS'
495    L2A_MANIFEST = 'L2AManifest'
496    REPORT = 'report'
497    GIPP = 'auxiliary/GIP_TL'
498    FORMAT_CORRECTNESS = 'FORMAT_CORRECTNESS'
499    GENERAL_QUALITY = 'GENERAL_QUALITY'
500    GEOMETRIC_QUALITY = 'GEOMETRIC_QUALITY'
501    RADIOMETRIC_QUALITY = 'RADIOMETRIC_QUALITY'
502    SENSOR_QUALITY = 'SENSOR_QUALITY'
503    QUALITY_REPORTS = [FORMAT_CORRECTNESS, GENERAL_QUALITY, GEOMETRIC_QUALITY, RADIOMETRIC_QUALITY, SENSOR_QUALITY]
504    CLASS_MASKS = ['SNW', 'CLD']
505    R10m = 'R10m'
506    R20m = 'R20m'
507    R60m = 'R60m'
508    RESOLUTIONS = [R10m, R20m, R60m]
509    S2_L2A_BAND_MAP = {R10m: ['B02', 'B03', 'B04', 'B08', AOT, TCI, WVP],
510                       R20m: ['B02', 'B03', 'B04', 'B05', 'B06', 'B07', 'B8A', 'B11', 'B12', AOT, SCL, TCI, VIS, WVP],
511                       R60m: ['B01', 'B02', 'B03', 'B04', 'B05', 'B06', 'B07', 'B8A', 'B09', 'B11', 'B12', AOT, SCL,
512                              TCI, WVP]}
513
514    # Order of elements in following lists is important
515    # Sentinel-2 L1C products:
516    S2_L1C_BANDS = ['B01', 'B02', 'B03', 'B04', 'B05', 'B06', 'B07', 'B08', 'B8A', 'B09', 'B10', 'B11', 'B12']
517    S2_L1C_METAFILES = [PRODUCT_INFO, TILE_INFO, METADATA, INSPIRE, MANIFEST, DATASTRIP_METADATA] +\
518                       [PREVIEW, PREVIEW_JP2, TCI] +\
519                       [f'{preview}/{band}' for preview, band in it.zip_longest([], S2_L1C_BANDS, fillvalue=PREVIEW)] +\
520                       [QI_MSK_CLOUD] +\
521                       [f'qi/MSK_{qi}_{band}' for qi, band in it.product(QI_LIST, S2_L1C_BANDS)] + \
522                       [f'qi/{qi_report}' for qi_report in [FORMAT_CORRECTNESS, GENERAL_QUALITY,
523                                                            GEOMETRIC_QUALITY, SENSOR_QUALITY]] +\
524                       [ECMWFT]
525
526    # Sentinel-2 L2A products:
527    S2_L2A_BANDS = [f'{resolution}/{band}' for resolution, band_list in sorted(S2_L2A_BAND_MAP.items())
528                    for band in band_list]
529    S2_L2A_METAFILES = [PRODUCT_INFO, TILE_INFO, METADATA, INSPIRE, MANIFEST, L2A_MANIFEST, REPORT,
530                        DATASTRIP_METADATA] + [f'datastrip/*/qi/{qi_report}' for qi_report in QUALITY_REPORTS] +\
531                       [f'qi/{source_id}_PVI' for source_id in SOURCE_ID_LIST] +\
532                       [f'qi/{mask}_{res.lstrip("R")}' for mask, res in it.product(CLASS_MASKS, [R20m, R60m])] +\
533                       [f'qi/MSK_{qi}_{band}' for qi, band in it.product(QI_LIST, S2_L1C_BANDS)] +\
534                       [QI_MSK_CLOUD] +\
535                       [f'qi/{qi_report}' for qi_report in QUALITY_REPORTS] +\
536                       [ECMWFT, AUX_ECMWFT, GIPP]
537
538    # Product files with formats:
539    PRODUCT_FILES = {**{PRODUCT_INFO: MimeType.JSON,
540                        METADATA: MimeType.XML,
541                        INSPIRE: MimeType.XML,
542                        MANIFEST: MimeType.SAFE,
543                        L2A_MANIFEST: MimeType.XML,
544                        REPORT: MimeType.XML,
545                        DATASTRIP_METADATA: MimeType.XML},
546                     **{f'datastrip/*/qi/{qi_report}': MimeType.XML for qi_report in QUALITY_REPORTS}}
547    # Tile files with formats:
548    TILE_FILES = {**{TILE_INFO: MimeType.JSON,
549                     PRODUCT_INFO: MimeType.JSON,
550                     METADATA: MimeType.XML,
551                     PREVIEW: MimeType.JPG,
552                     PREVIEW_JP2: MimeType.JP2,
553                     TCI: MimeType.JP2,
554                     QI_MSK_CLOUD: MimeType.GML,
555                     ECMWFT: MimeType.RAW,
556                     AUX_ECMWFT: MimeType.RAW,
557                     GIPP: MimeType.XML},
558                  **{f'qi/{qi_report}': MimeType.XML for qi_report in QUALITY_REPORTS},
559                  **{f'{preview}/{band}': MimeType.JP2
560                     for preview, band in it.zip_longest([], S2_L1C_BANDS, fillvalue=PREVIEW)},
561                  **{f'qi/MSK_{qi}_{band}': MimeType.GML for qi, band in it.product(QI_LIST, S2_L1C_BANDS)},
562                  **{band: MimeType.JP2 for band in S2_L1C_BANDS},
563                  **{band: MimeType.JP2 for band in S2_L2A_BANDS},
564                  **{f'qi/{source_id}_PVI': MimeType.JP2 for source_id in SOURCE_ID_LIST},
565                  **{f'qi/{mask}_{res.lstrip("R")}': MimeType.JP2 for mask, res in it.product(CLASS_MASKS,
566                                                                                              [R20m, R60m])}}
567
568    # All files joined together
569    AWS_FILES = {**PRODUCT_FILES,
570                 **{filename.split('/')[-1]: data_format for filename, data_format in PRODUCT_FILES.items()},
571                 **TILE_FILES,
572                 **{filename.split('/')[-1]: data_format for filename, data_format in TILE_FILES.items()}}
573
574
575class EsaSafeType(Enum):
576    """ Enum constants class for ESA .SAFE type.
577
578     Types are OLD_TYPE and COMPACT_TYPE
579    """
580    OLD_TYPE = 'old_type'
581    COMPACT_TYPE = 'compact_type'
582