mapchete-eo 2025.7.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapchete_eo/__init__.py +1 -0
- mapchete_eo/archives/__init__.py +0 -0
- mapchete_eo/archives/base.py +65 -0
- mapchete_eo/array/__init__.py +0 -0
- mapchete_eo/array/buffer.py +16 -0
- mapchete_eo/array/color.py +29 -0
- mapchete_eo/array/convert.py +157 -0
- mapchete_eo/base.py +528 -0
- mapchete_eo/blacklist.txt +175 -0
- mapchete_eo/cli/__init__.py +30 -0
- mapchete_eo/cli/bounds.py +22 -0
- mapchete_eo/cli/options_arguments.py +243 -0
- mapchete_eo/cli/s2_brdf.py +77 -0
- mapchete_eo/cli/s2_cat_results.py +146 -0
- mapchete_eo/cli/s2_find_broken_products.py +93 -0
- mapchete_eo/cli/s2_jp2_static_catalog.py +166 -0
- mapchete_eo/cli/s2_mask.py +71 -0
- mapchete_eo/cli/s2_mgrs.py +45 -0
- mapchete_eo/cli/s2_rgb.py +114 -0
- mapchete_eo/cli/s2_verify.py +129 -0
- mapchete_eo/cli/static_catalog.py +123 -0
- mapchete_eo/eostac.py +30 -0
- mapchete_eo/exceptions.py +87 -0
- mapchete_eo/geometry.py +271 -0
- mapchete_eo/image_operations/__init__.py +12 -0
- mapchete_eo/image_operations/color_correction.py +136 -0
- mapchete_eo/image_operations/compositing.py +247 -0
- mapchete_eo/image_operations/dtype_scale.py +43 -0
- mapchete_eo/image_operations/fillnodata.py +130 -0
- mapchete_eo/image_operations/filters.py +319 -0
- mapchete_eo/image_operations/linear_normalization.py +81 -0
- mapchete_eo/image_operations/sigmoidal.py +114 -0
- mapchete_eo/io/__init__.py +37 -0
- mapchete_eo/io/assets.py +492 -0
- mapchete_eo/io/items.py +147 -0
- mapchete_eo/io/levelled_cubes.py +228 -0
- mapchete_eo/io/path.py +144 -0
- mapchete_eo/io/products.py +413 -0
- mapchete_eo/io/profiles.py +45 -0
- mapchete_eo/known_catalogs.py +42 -0
- mapchete_eo/platforms/sentinel2/__init__.py +17 -0
- mapchete_eo/platforms/sentinel2/archives.py +190 -0
- mapchete_eo/platforms/sentinel2/bandpass_adjustment.py +104 -0
- mapchete_eo/platforms/sentinel2/brdf/__init__.py +8 -0
- mapchete_eo/platforms/sentinel2/brdf/config.py +32 -0
- mapchete_eo/platforms/sentinel2/brdf/correction.py +260 -0
- mapchete_eo/platforms/sentinel2/brdf/hls.py +251 -0
- mapchete_eo/platforms/sentinel2/brdf/models.py +44 -0
- mapchete_eo/platforms/sentinel2/brdf/protocols.py +27 -0
- mapchete_eo/platforms/sentinel2/brdf/ross_thick.py +136 -0
- mapchete_eo/platforms/sentinel2/brdf/sun_angle_arrays.py +76 -0
- mapchete_eo/platforms/sentinel2/config.py +181 -0
- mapchete_eo/platforms/sentinel2/driver.py +78 -0
- mapchete_eo/platforms/sentinel2/masks.py +325 -0
- mapchete_eo/platforms/sentinel2/metadata_parser.py +734 -0
- mapchete_eo/platforms/sentinel2/path_mappers/__init__.py +29 -0
- mapchete_eo/platforms/sentinel2/path_mappers/base.py +56 -0
- mapchete_eo/platforms/sentinel2/path_mappers/earthsearch.py +34 -0
- mapchete_eo/platforms/sentinel2/path_mappers/metadata_xml.py +135 -0
- mapchete_eo/platforms/sentinel2/path_mappers/sinergise.py +105 -0
- mapchete_eo/platforms/sentinel2/preprocessing_tasks.py +26 -0
- mapchete_eo/platforms/sentinel2/processing_baseline.py +160 -0
- mapchete_eo/platforms/sentinel2/product.py +669 -0
- mapchete_eo/platforms/sentinel2/types.py +109 -0
- mapchete_eo/processes/__init__.py +0 -0
- mapchete_eo/processes/config.py +51 -0
- mapchete_eo/processes/dtype_scale.py +112 -0
- mapchete_eo/processes/eo_to_xarray.py +19 -0
- mapchete_eo/processes/merge_rasters.py +235 -0
- mapchete_eo/product.py +278 -0
- mapchete_eo/protocols.py +56 -0
- mapchete_eo/search/__init__.py +14 -0
- mapchete_eo/search/base.py +222 -0
- mapchete_eo/search/config.py +42 -0
- mapchete_eo/search/s2_mgrs.py +314 -0
- mapchete_eo/search/stac_search.py +251 -0
- mapchete_eo/search/stac_static.py +236 -0
- mapchete_eo/search/utm_search.py +251 -0
- mapchete_eo/settings.py +24 -0
- mapchete_eo/sort.py +48 -0
- mapchete_eo/time.py +53 -0
- mapchete_eo/types.py +73 -0
- mapchete_eo-2025.7.0.dist-info/METADATA +38 -0
- mapchete_eo-2025.7.0.dist-info/RECORD +87 -0
- mapchete_eo-2025.7.0.dist-info/WHEEL +5 -0
- mapchete_eo-2025.7.0.dist-info/entry_points.txt +11 -0
- mapchete_eo-2025.7.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from mapchete_eo.platforms.sentinel2.path_mappers.base import S2PathMapper
|
|
2
|
+
from mapchete_eo.platforms.sentinel2.path_mappers.earthsearch import (
|
|
3
|
+
EarthSearchPathMapper,
|
|
4
|
+
)
|
|
5
|
+
from mapchete_eo.platforms.sentinel2.path_mappers.metadata_xml import XMLMapper
|
|
6
|
+
from mapchete_eo.platforms.sentinel2.path_mappers.sinergise import SinergisePathMapper
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def default_path_mapper_guesser(
|
|
10
|
+
url: str,
|
|
11
|
+
**kwargs,
|
|
12
|
+
) -> S2PathMapper:
|
|
13
|
+
"""Guess S2PathMapper based on URL.
|
|
14
|
+
|
|
15
|
+
If a new path mapper is added in this module, it should also be added to this function
|
|
16
|
+
in order to be detected.
|
|
17
|
+
"""
|
|
18
|
+
if url.startswith(
|
|
19
|
+
("https://roda.sentinel-hub.com/sentinel-s2-l2a/", "s3://sentinel-s2-l2a/")
|
|
20
|
+
) or url.startswith(
|
|
21
|
+
("https://roda.sentinel-hub.com/sentinel-s2-l1c/", "s3://sentinel-s2-l1c/")
|
|
22
|
+
):
|
|
23
|
+
return SinergisePathMapper(url, **kwargs)
|
|
24
|
+
elif url.startswith(
|
|
25
|
+
"https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/"
|
|
26
|
+
):
|
|
27
|
+
return EarthSearchPathMapper(url, **kwargs)
|
|
28
|
+
else:
|
|
29
|
+
return XMLMapper(url, **kwargs)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from mapchete.path import MPath
|
|
4
|
+
|
|
5
|
+
from mapchete_eo.platforms.sentinel2.processing_baseline import ProcessingBaseline
|
|
6
|
+
from mapchete_eo.platforms.sentinel2.types import (
|
|
7
|
+
BandQI,
|
|
8
|
+
L2ABand,
|
|
9
|
+
ProductQI,
|
|
10
|
+
ProductQIMaskResolution,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class S2PathMapper(ABC):
|
|
15
|
+
"""
|
|
16
|
+
Abstract class to help mapping asset paths from metadata.xml to their
|
|
17
|
+
locations of various data archives.
|
|
18
|
+
|
|
19
|
+
This is mainly used for additional data like QI masks.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# All available bands for Sentinel-2 Level 2A.
|
|
23
|
+
_bands = [band.name for band in L2ABand]
|
|
24
|
+
|
|
25
|
+
processing_baseline: ProcessingBaseline
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def product_qi_mask(
|
|
29
|
+
self,
|
|
30
|
+
qi_mask: ProductQI,
|
|
31
|
+
resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"],
|
|
32
|
+
) -> MPath: ...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def classification_mask(self) -> MPath: ...
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def cloud_probability_mask(
|
|
39
|
+
self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
|
|
40
|
+
) -> MPath: ...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def snow_probability_mask(
|
|
44
|
+
self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
|
|
45
|
+
) -> MPath: ...
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def band_qi_mask(self, qi_mask: BandQI, band: L2ABand) -> MPath: ...
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def technical_quality_mask(self, band: L2ABand) -> MPath: ...
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def detector_footprints(self, band: L2ABand) -> MPath: ...
|
|
55
|
+
|
|
56
|
+
def clear_cached_data(self) -> None: ...
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from mapchete.path import MPath
|
|
2
|
+
|
|
3
|
+
from mapchete_eo.platforms.sentinel2.path_mappers.sinergise import SinergisePathMapper
|
|
4
|
+
from mapchete_eo.platforms.sentinel2.processing_baseline import ProcessingBaseline
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EarthSearchPathMapper(SinergisePathMapper):
|
|
8
|
+
"""
|
|
9
|
+
The COG archive maintained by E84 and covered by EarthSearch does not hold additional data
|
|
10
|
+
such as the GML files. This class maps the metadata masks to the current EarthSearch product.
|
|
11
|
+
|
|
12
|
+
e.g.:
|
|
13
|
+
B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_DETFOO_B01.gml
|
|
14
|
+
Cloud masks: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_CLOUDS_B00.gml
|
|
15
|
+
|
|
16
|
+
newer products however:
|
|
17
|
+
B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2022/6/6/0/qi/DETFOO_B01.jp2
|
|
18
|
+
no vector cloudmasks available anymore
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
metadata_xml: MPath,
|
|
24
|
+
alternative_metadata_baseurl: str = "sentinel-s2-l2a",
|
|
25
|
+
protocol: str = "s3",
|
|
26
|
+
baseline_version: str = "04.00",
|
|
27
|
+
**kwargs,
|
|
28
|
+
):
|
|
29
|
+
basedir = metadata_xml.parent
|
|
30
|
+
self._path = (basedir / "tileinfo_metadata.json").read_json()["path"]
|
|
31
|
+
self._utm_zone, self._latitude_band, self._grid_square = basedir.elements[-6:-3]
|
|
32
|
+
self._baseurl = alternative_metadata_baseurl
|
|
33
|
+
self._protocol = protocol
|
|
34
|
+
self.processing_baseline = ProcessingBaseline.from_version(baseline_version)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A path mapper maps from an metadata XML file to additional metadata
|
|
3
|
+
on a given archive or a local SAFE file.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from xml.etree.ElementTree import Element
|
|
8
|
+
from functools import cached_property
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from mapchete.path import MPath
|
|
12
|
+
|
|
13
|
+
from mapchete_eo.io import open_xml
|
|
14
|
+
from mapchete_eo.platforms.sentinel2.path_mappers.base import S2PathMapper
|
|
15
|
+
from mapchete_eo.platforms.sentinel2.processing_baseline import ProcessingBaseline
|
|
16
|
+
from mapchete_eo.platforms.sentinel2.types import (
|
|
17
|
+
BandQI,
|
|
18
|
+
L2ABand,
|
|
19
|
+
ProductQI,
|
|
20
|
+
ProductQIMaskResolution,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class XMLMapper(S2PathMapper):
|
|
27
|
+
def __init__(
|
|
28
|
+
self, metadata_xml: MPath, xml_root: Optional[Element] = None, **kwargs
|
|
29
|
+
):
|
|
30
|
+
self.metadata_xml = metadata_xml
|
|
31
|
+
self._cached_xml_root = xml_root
|
|
32
|
+
self._metadata_dir = metadata_xml.parent
|
|
33
|
+
|
|
34
|
+
def clear_cached_data(self):
|
|
35
|
+
if self._cached_xml_root is not None:
|
|
36
|
+
logger.debug("clear XMLMapper xml cache")
|
|
37
|
+
self._cached_xml_root.clear()
|
|
38
|
+
self._cached_xml_root = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def xml_root(self) -> Element:
|
|
42
|
+
if self._cached_xml_root is None:
|
|
43
|
+
self._cached_xml_root = open_xml(self.metadata_xml)
|
|
44
|
+
return self._cached_xml_root
|
|
45
|
+
|
|
46
|
+
@cached_property
|
|
47
|
+
def processing_baseline(self):
|
|
48
|
+
# try to guess processing baseline from product id
|
|
49
|
+
def _get_version(tag="TILE_ID"):
|
|
50
|
+
product_id = next(self.xml_root.iter(tag)).text
|
|
51
|
+
appendix = product_id.split("_")[-1]
|
|
52
|
+
if appendix.startswith("N"):
|
|
53
|
+
return appendix.lstrip("N")
|
|
54
|
+
|
|
55
|
+
version = _get_version()
|
|
56
|
+
try:
|
|
57
|
+
return ProcessingBaseline.from_version(version)
|
|
58
|
+
except Exception: # pragma: no cover
|
|
59
|
+
# try use L1C product version as fallback
|
|
60
|
+
# we don't need to test this because HOPEFULLY we won't be confronted
|
|
61
|
+
# with such data
|
|
62
|
+
try:
|
|
63
|
+
l1c_version = _get_version("L1C_TILE_ID")
|
|
64
|
+
except StopIteration:
|
|
65
|
+
l1c_version = "02.06"
|
|
66
|
+
if l1c_version is not None:
|
|
67
|
+
return ProcessingBaseline.from_version(f"{l1c_version}")
|
|
68
|
+
|
|
69
|
+
def product_qi_mask(
|
|
70
|
+
self,
|
|
71
|
+
qi_mask: ProductQI,
|
|
72
|
+
resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"],
|
|
73
|
+
) -> MPath:
|
|
74
|
+
"""Determine product QI mask from metadata.xml."""
|
|
75
|
+
qi_mask_type = dict(self.processing_baseline.product_mask_types)[qi_mask]
|
|
76
|
+
for i in self.xml_root.iter():
|
|
77
|
+
if i.tag == "MASK_FILENAME" and i.get("type") == qi_mask_type:
|
|
78
|
+
path = self._metadata_dir / i.text
|
|
79
|
+
if qi_mask == ProductQI.classification:
|
|
80
|
+
return path
|
|
81
|
+
else:
|
|
82
|
+
if resolution.name in path.name:
|
|
83
|
+
return path
|
|
84
|
+
else:
|
|
85
|
+
raise KeyError(f"no {qi_mask_type} with item found in metadata")
|
|
86
|
+
|
|
87
|
+
def classification_mask(self) -> MPath:
|
|
88
|
+
return self.product_qi_mask(ProductQI.classification)
|
|
89
|
+
|
|
90
|
+
def cloud_probability_mask(
|
|
91
|
+
self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
|
|
92
|
+
) -> MPath:
|
|
93
|
+
return self.product_qi_mask(ProductQI.cloud_probability, resolution=resolution)
|
|
94
|
+
|
|
95
|
+
def snow_probability_mask(
|
|
96
|
+
self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
|
|
97
|
+
) -> MPath:
|
|
98
|
+
return self.product_qi_mask(ProductQI.snow_probability, resolution=resolution)
|
|
99
|
+
|
|
100
|
+
def band_qi_mask(self, qi_mask: BandQI, band: L2ABand) -> MPath:
|
|
101
|
+
"""Determine band QI mask from metadata.xml."""
|
|
102
|
+
if qi_mask.name not in dict(self.processing_baseline.band_mask_types).keys():
|
|
103
|
+
raise DeprecationWarning(
|
|
104
|
+
f"QI mask '{qi_mask}' not available for this product"
|
|
105
|
+
)
|
|
106
|
+
mask_types = set()
|
|
107
|
+
for masks in self.xml_root.iter("Pixel_Level_QI"):
|
|
108
|
+
if masks.get("geometry") == "FULL_RESOLUTION":
|
|
109
|
+
for mask_path in masks:
|
|
110
|
+
qi_mask_type = dict(self.processing_baseline.band_mask_types)[
|
|
111
|
+
qi_mask
|
|
112
|
+
]
|
|
113
|
+
mask_type = mask_path.get("type")
|
|
114
|
+
if mask_type:
|
|
115
|
+
mask_types.add(mask_type)
|
|
116
|
+
if mask_type == qi_mask_type:
|
|
117
|
+
band_id = mask_path.get("bandId")
|
|
118
|
+
if band_id is not None:
|
|
119
|
+
band_idx = int(band_id)
|
|
120
|
+
if band_idx == band.value:
|
|
121
|
+
return self._metadata_dir / mask_path.text
|
|
122
|
+
else: # pragma: no cover
|
|
123
|
+
raise KeyError(
|
|
124
|
+
f"no {qi_mask_type} for band {band.name} not found in metadata: {', '.join(mask_types)}"
|
|
125
|
+
)
|
|
126
|
+
else: # pragma: no cover
|
|
127
|
+
raise KeyError(
|
|
128
|
+
f"no {qi_mask_type} not found in metadata: {', '.join(mask_types)}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def technical_quality_mask(self, band: L2ABand) -> MPath:
|
|
132
|
+
return self.band_qi_mask(BandQI.technical_quality, band)
|
|
133
|
+
|
|
134
|
+
def detector_footprints(self, band: L2ABand) -> MPath:
|
|
135
|
+
return self.band_qi_mask(BandQI.detector_footprints, band)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from mapchete.path import MPath, MPathLike
|
|
2
|
+
|
|
3
|
+
from mapchete_eo.platforms.sentinel2.path_mappers.base import S2PathMapper
|
|
4
|
+
from mapchete_eo.platforms.sentinel2.processing_baseline import ProcessingBaseline
|
|
5
|
+
from mapchete_eo.platforms.sentinel2.types import (
|
|
6
|
+
BandQI,
|
|
7
|
+
L2ABand,
|
|
8
|
+
ProductQI,
|
|
9
|
+
ProductQIMaskResolution,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SinergisePathMapper(S2PathMapper):
|
|
14
|
+
"""
|
|
15
|
+
Return true paths of product quality assets from the Sinergise S2 bucket.
|
|
16
|
+
|
|
17
|
+
e.g.:
|
|
18
|
+
B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_DETFOO_B01.gml
|
|
19
|
+
Cloud masks: s3://sentinel-s2-l2a/tiles/51/K/XR/2020/7/31/0/qi/MSK_CLOUDS_B00.gml
|
|
20
|
+
|
|
21
|
+
newer products however:
|
|
22
|
+
B01 detector footprints: s3://sentinel-s2-l2a/tiles/51/K/XR/2022/6/6/0/qi/DETFOO_B01.jp2
|
|
23
|
+
no vector cloudmasks available anymore
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_PRE_0400_MASK_PATHS = {
|
|
27
|
+
ProductQI.classification: "MSK_CLOUDS_B00.gml",
|
|
28
|
+
ProductQI.cloud_probability: "CLD_{resolution}.jp2", # are they really there?
|
|
29
|
+
ProductQI.snow_probability: "SNW_{resolution}.jp2", # are they really there?
|
|
30
|
+
BandQI.detector_footprints: "MSK_DETFOO_{band_identifier}.gml",
|
|
31
|
+
BandQI.technical_quality: "MSK_TECQUA_{band_identifier}.gml",
|
|
32
|
+
}
|
|
33
|
+
_POST_0400_MASK_PATHS = {
|
|
34
|
+
ProductQI.classification: "CLASSI_B00.jp2",
|
|
35
|
+
ProductQI.cloud_probability: "CLD_{resolution}.jp2",
|
|
36
|
+
ProductQI.snow_probability: "SNW_{resolution}.jp2",
|
|
37
|
+
BandQI.detector_footprints: "DETFOO_{band_identifier}.jp2",
|
|
38
|
+
BandQI.technical_quality: "QUALIT_{band_identifier}.jp2",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
url: MPathLike,
|
|
44
|
+
bucket: str = "sentinel-s2-l2a",
|
|
45
|
+
protocol: str = "s3",
|
|
46
|
+
baseline_version: str = "04.00",
|
|
47
|
+
**kwargs,
|
|
48
|
+
):
|
|
49
|
+
url = MPath.from_inp(url)
|
|
50
|
+
tileinfo_path = url.parent / "tileInfo.json"
|
|
51
|
+
self._path = MPath(
|
|
52
|
+
"/".join(tileinfo_path.elements[-9:-1]), **tileinfo_path._kwargs
|
|
53
|
+
)
|
|
54
|
+
self._utm_zone, self._latitude_band, self._grid_square = self._path.split("/")[
|
|
55
|
+
1:-4
|
|
56
|
+
]
|
|
57
|
+
self._baseurl = bucket
|
|
58
|
+
self._protocol = protocol
|
|
59
|
+
self.processing_baseline = ProcessingBaseline.from_version(baseline_version)
|
|
60
|
+
|
|
61
|
+
def product_qi_mask(
|
|
62
|
+
self,
|
|
63
|
+
qi_mask: ProductQI,
|
|
64
|
+
resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"],
|
|
65
|
+
) -> MPath:
|
|
66
|
+
"""Determine product QI mask according to Sinergise bucket schema."""
|
|
67
|
+
if self.processing_baseline.version < "04.00":
|
|
68
|
+
mask_path = self._PRE_0400_MASK_PATHS[qi_mask]
|
|
69
|
+
else:
|
|
70
|
+
mask_path = self._POST_0400_MASK_PATHS[qi_mask]
|
|
71
|
+
key = f"{self._path}/qi/{mask_path.format(resolution=resolution.name)}"
|
|
72
|
+
return MPath.from_inp(f"{self._protocol}://{self._baseurl}/{key}")
|
|
73
|
+
|
|
74
|
+
def classification_mask(self) -> MPath:
|
|
75
|
+
return self.product_qi_mask(ProductQI.classification)
|
|
76
|
+
|
|
77
|
+
def cloud_probability_mask(
|
|
78
|
+
self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
|
|
79
|
+
) -> MPath:
|
|
80
|
+
return self.product_qi_mask(ProductQI.cloud_probability, resolution=resolution)
|
|
81
|
+
|
|
82
|
+
def snow_probability_mask(
|
|
83
|
+
self, resolution: ProductQIMaskResolution = ProductQIMaskResolution["60m"]
|
|
84
|
+
) -> MPath:
|
|
85
|
+
return self.product_qi_mask(ProductQI.snow_probability, resolution=resolution)
|
|
86
|
+
|
|
87
|
+
def band_qi_mask(self, qi_mask: BandQI, band: L2ABand) -> MPath:
|
|
88
|
+
"""Determine product QI mask according to Sinergise bucket schema."""
|
|
89
|
+
try:
|
|
90
|
+
if self.processing_baseline.version < "04.00":
|
|
91
|
+
mask_path = self._PRE_0400_MASK_PATHS[qi_mask]
|
|
92
|
+
else:
|
|
93
|
+
mask_path = self._POST_0400_MASK_PATHS[qi_mask]
|
|
94
|
+
except KeyError:
|
|
95
|
+
raise DeprecationWarning(
|
|
96
|
+
f"'{qi_mask.name}' quality mask not found in this product"
|
|
97
|
+
)
|
|
98
|
+
key = f"{self._path}/qi/{mask_path.format(band_identifier=band.name)}"
|
|
99
|
+
return MPath.from_inp(f"{self._protocol}://{self._baseurl}/{key}")
|
|
100
|
+
|
|
101
|
+
def technical_quality_mask(self, band: L2ABand) -> MPath:
|
|
102
|
+
return self.band_qi_mask(BandQI.technical_quality, band)
|
|
103
|
+
|
|
104
|
+
def detector_footprints(self, band: L2ABand) -> MPath:
|
|
105
|
+
return self.band_qi_mask(BandQI.detector_footprints, band)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
import pystac
|
|
5
|
+
|
|
6
|
+
from mapchete_eo.exceptions import CorruptedProductMetadata
|
|
7
|
+
from mapchete_eo.platforms.sentinel2.config import CacheConfig
|
|
8
|
+
from mapchete_eo.platforms.sentinel2.product import S2Product
|
|
9
|
+
from mapchete_eo.product import add_to_blacklist
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_s2_product(
|
|
15
|
+
item: pystac.Item,
|
|
16
|
+
cache_config: Optional[CacheConfig] = None,
|
|
17
|
+
cache_all: bool = False,
|
|
18
|
+
) -> Union[S2Product, CorruptedProductMetadata]:
|
|
19
|
+
try:
|
|
20
|
+
s2product = S2Product.from_stac_item(
|
|
21
|
+
item, cache_config=cache_config, cache_all=cache_all
|
|
22
|
+
)
|
|
23
|
+
except CorruptedProductMetadata as exc:
|
|
24
|
+
add_to_blacklist(item.get_self_href())
|
|
25
|
+
return exc
|
|
26
|
+
return s2product
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Optional, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProductMaskTypes(BaseModel):
|
|
8
|
+
"""Mapping between mask type and respective metadata.xml type key."""
|
|
9
|
+
|
|
10
|
+
classification: Optional[str] = None
|
|
11
|
+
cloud_probability: Optional[str] = None
|
|
12
|
+
snow_probability: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BandMaskTypes(BaseModel):
|
|
16
|
+
"""Mapping between band mask type and respective metadata.xml type key."""
|
|
17
|
+
|
|
18
|
+
technical_quality: Optional[str] = None
|
|
19
|
+
detector_footprints: Optional[str] = None
|
|
20
|
+
# deprecated since 04.00
|
|
21
|
+
# nodata: Optional[str] = None
|
|
22
|
+
# defect: Optional[str] = None
|
|
23
|
+
# saturated: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ItemMapping(BaseModel):
|
|
27
|
+
"""Configuration of processing baseline keys in metadata.xml."""
|
|
28
|
+
|
|
29
|
+
product_mask_types: ProductMaskTypes
|
|
30
|
+
band_mask_types: BandMaskTypes
|
|
31
|
+
band_mask_extension: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Available product mask types from PB 00.01 until 03.01.
|
|
35
|
+
# "classification" mask was provided as GML, the other two as JP2.
|
|
36
|
+
# Cloud probability and snow probability are available in two separate files for
|
|
37
|
+
# 20m and 60m.
|
|
38
|
+
pre_0400 = ItemMapping(
|
|
39
|
+
product_mask_types=ProductMaskTypes(
|
|
40
|
+
classification="MSK_CLOUDS",
|
|
41
|
+
cloud_probability="MSK_CLDPRB",
|
|
42
|
+
snow_probability="MSK_SNWPRB",
|
|
43
|
+
),
|
|
44
|
+
band_mask_types=BandMaskTypes(
|
|
45
|
+
technical_quality="MSK_TECQUA",
|
|
46
|
+
detector_footprints="MSK_DETFOO",
|
|
47
|
+
),
|
|
48
|
+
band_mask_extension="gml",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Available product mask types from PB 04.00 onwards.
|
|
53
|
+
# Cloud probability and snow probability are available in two separate files for
|
|
54
|
+
# 20m and 60m.
|
|
55
|
+
post_0400 = ItemMapping(
|
|
56
|
+
product_mask_types=ProductMaskTypes(
|
|
57
|
+
classification="MSK_CLASSI",
|
|
58
|
+
cloud_probability="MSK_CLDPRB",
|
|
59
|
+
snow_probability="MSK_SNWPRB",
|
|
60
|
+
),
|
|
61
|
+
band_mask_types=BandMaskTypes(
|
|
62
|
+
technical_quality="MSK_QUALIT",
|
|
63
|
+
detector_footprints="MSK_DETFOO",
|
|
64
|
+
),
|
|
65
|
+
band_mask_extension="jp2",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class BaselineVersion:
|
|
71
|
+
"""Helper for Processing Baseline versions."""
|
|
72
|
+
|
|
73
|
+
major: int
|
|
74
|
+
minor: int
|
|
75
|
+
level: str
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def from_string(version: str) -> "BaselineVersion":
|
|
79
|
+
major, minor = map(int, version.split("."))
|
|
80
|
+
if major < 2:
|
|
81
|
+
level = "L1C"
|
|
82
|
+
# everything below 02.06 is Level 1C
|
|
83
|
+
elif major == 2 and minor <= 6:
|
|
84
|
+
level = "L1C"
|
|
85
|
+
else:
|
|
86
|
+
level = "L2A"
|
|
87
|
+
return BaselineVersion(major, minor, level)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def from_inp(inp: Union[str, "BaselineVersion"]) -> "BaselineVersion":
|
|
91
|
+
if isinstance(inp, str):
|
|
92
|
+
return BaselineVersion.from_string(inp)
|
|
93
|
+
elif isinstance(inp, BaselineVersion):
|
|
94
|
+
return inp
|
|
95
|
+
else:
|
|
96
|
+
raise TypeError(f"cannot generate BaselineVersion from input {inp}")
|
|
97
|
+
|
|
98
|
+
def __eq__(self, other: Any):
|
|
99
|
+
other = BaselineVersion.from_inp(other)
|
|
100
|
+
return self.major == other.major and self.minor == other.minor
|
|
101
|
+
|
|
102
|
+
def __lt__(self, other: Union[str, "BaselineVersion"]):
|
|
103
|
+
other = BaselineVersion.from_inp(other)
|
|
104
|
+
if self.major == other.major:
|
|
105
|
+
return self.minor < other.minor
|
|
106
|
+
else:
|
|
107
|
+
return self.major < other.major
|
|
108
|
+
|
|
109
|
+
def __le__(self, other: Union[str, "BaselineVersion"]):
|
|
110
|
+
other = BaselineVersion.from_inp(other)
|
|
111
|
+
if self.major == other.major:
|
|
112
|
+
return self.minor <= other.minor
|
|
113
|
+
else:
|
|
114
|
+
return self.major <= other.major
|
|
115
|
+
|
|
116
|
+
def __gt__(self, other: Union[str, "BaselineVersion"]):
|
|
117
|
+
other = BaselineVersion.from_inp(other)
|
|
118
|
+
if self.major == other.major:
|
|
119
|
+
return self.minor > other.minor
|
|
120
|
+
else:
|
|
121
|
+
return self.major > other.major
|
|
122
|
+
|
|
123
|
+
def __ge__(self, other: Union[str, "BaselineVersion"]):
|
|
124
|
+
if isinstance(other, str):
|
|
125
|
+
other = BaselineVersion.from_string(other)
|
|
126
|
+
if self.major == other.major:
|
|
127
|
+
return self.minor >= other.minor
|
|
128
|
+
else:
|
|
129
|
+
return self.major >= other.major
|
|
130
|
+
|
|
131
|
+
def __str__(self):
|
|
132
|
+
return f"{self.major:02}.{self.minor:02}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ProcessingBaseline:
|
|
136
|
+
"""Class which combines PB version and metadata.xml keys for QI masks."""
|
|
137
|
+
|
|
138
|
+
version: BaselineVersion
|
|
139
|
+
item_mapping: ItemMapping
|
|
140
|
+
product_mask_types: ProductMaskTypes
|
|
141
|
+
band_mask_types: BandMaskTypes
|
|
142
|
+
band_mask_extension: str
|
|
143
|
+
|
|
144
|
+
def __init__(self, version: BaselineVersion):
|
|
145
|
+
self.version = version
|
|
146
|
+
if self.version.major < 4:
|
|
147
|
+
self.item_mapping = pre_0400
|
|
148
|
+
else:
|
|
149
|
+
self.item_mapping = post_0400
|
|
150
|
+
|
|
151
|
+
self.product_mask_types = self.item_mapping.product_mask_types
|
|
152
|
+
self.band_mask_types = self.item_mapping.band_mask_types
|
|
153
|
+
self.band_mask_extension = self.item_mapping.band_mask_extension
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def from_version(version: Union[BaselineVersion, str]) -> "ProcessingBaseline":
|
|
157
|
+
if isinstance(version, BaselineVersion):
|
|
158
|
+
return ProcessingBaseline(version=version)
|
|
159
|
+
else:
|
|
160
|
+
return ProcessingBaseline(version=BaselineVersion.from_string(version))
|