diff --git a/CHANGES.md b/CHANGES.md
index 9f1f806c..5150948c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -9,7 +9,7 @@
- **ENH: Handle Python 3.12. ([#113](https://github.com/sertit/eoreader/issues/113))**
- **ENH: Guard against S1 COG format, not yet handled by SNAP.**
- **ENH: Calibration step for `Capella` products now exists in ESA SNAP. Add it in pre-processing.**
-- **ENH: Handling of Sentinel-1 ASF RTC products. ([#112](https://github.com/sertit/eoreader/issues/112))**
+- **ENH: Handling of Sentinel-1 [ASF](https://hyp3-docs.asf.alaska.edu/guides/rtc_product_guide/#readme-file) and [MPC](https://planetarycomputer.microsoft.com/dataset/sentinel-1-rtc) RTC products. ([#112](https://github.com/sertit/eoreader/issues/112), [#118](https://github.com/sertit/eoreader/issues/118))**
- **ENH: Handling of Sentinel-1 SM products.**
- FIX: Fix jpg, png... quicklooks management when plotting
- FIX: Fix an `xarray` issue when trying to compute percentiles when stacking bands
diff --git a/docs/sar.md b/docs/sar.md
index 7e04bcdb..d644e575 100644
--- a/docs/sar.md
+++ b/docs/sar.md
@@ -6,18 +6,18 @@ You will find a SAR tutorial [here](https://eoreader.readthedocs.io/en/latest/no

-| Constellations | Class | Use archive |
-|-------------------------------------|---------------------------------------------------------------|---------------------------------------------|
-| `Capella` | {meth}`~eoreader.products.sar.capella_product.CapellaProduct` | ❌ |
-| `COSMO-Skymed 1st Generation` | {meth}`~eoreader.products.sar.csk_product.CskProduct` | ❌ |
-| `COSMO-Skymed 2nd Generation` | {meth}`~eoreader.products.sar.csg_product.CsgProduct` | ❌ |
-| `ICEYE` | {meth}`~eoreader.products.sar.iceye_product.IceyeProduct` | ❌ |
-| `RADARSAT Constellation Mission` | {meth}`~eoreader.products.sar.rcm_product.RcmProduct` | ❌ |
+| Constellations | Class | Use archive |
+|-------------------------------------|---------------------------------------------------------------|-------------------------------------------|
+| `Capella` | {meth}`~eoreader.products.sar.capella_product.CapellaProduct` | ❌ |
+| `COSMO-Skymed 1st Generation` | {meth}`~eoreader.products.sar.csk_product.CskProduct` | ❌ |
+| `COSMO-Skymed 2nd Generation` | {meth}`~eoreader.products.sar.csg_product.CsgProduct` | ❌ |
+| `ICEYE` | {meth}`~eoreader.products.sar.iceye_product.IceyeProduct` | ❌ |
+| `RADARSAT Constellation Mission` | {meth}`~eoreader.products.sar.rcm_product.RcmProduct` | ❌ |
| `RADARSAT-2` | {meth}`~eoreader.products.sar.rs2_product.Rs2Product` | ✅ for ground range data, ❌ for complex data |
-| `Sentinel-1` | {meth}`~eoreader.products.sar.s1_product.S1Product` | ✅ |
-| `Sentinel-1 RTC` | {meth}`~eoreader.products.sar.s1_product.S1RtcProduct` | ✅ |
-| `SAOCOM-1` | {meth}`~eoreader.products.sar.saocom_product.SaocomProduct` | ❌ |
-| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | {meth}`~eoreader.products.sar.tsx_product.TsxProduct` | ❌ |
+| `Sentinel-1` | {meth}`~eoreader.products.sar.s1_product.S1Product` | ✅ |
+| `Sentinel-1 RTC` | {meth}`~eoreader.products.sar.s1_product.S1RtcProduct` | ✅ for ASF |
+| `SAOCOM-1` | {meth}`~eoreader.products.sar.saocom_product.SaocomProduct` | ❌ |
+| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | {meth}`~eoreader.products.sar.tsx_product.TsxProduct` | ❌ |
```{warning}
Satellites products that cannot be used as archived have to be extracted before use,
@@ -26,36 +26,36 @@ mostly because SNAP doesn't handle them.
## Product type handling
-| Constellations | Product Type | Handled |
-|-------------------------------------|--------------------------------------|---------|
-| `Capella` | SLC | ✅ |
-| `Capella` | GEC | ✅ |
-| `Capella` | GEO | ✅ |
-| `Capella` | SICD, SIDD, CPHD | ❌ |
-| `COSMO-Skymed` | SCS | ✅ |
-| `COSMO-SkyMed` 1st Generation | DGM | ✅ |
-| `COSMO-SkyMed` 2nd Generation | DGM | ⚠ |
-| `COSMO-SkyMed` | GEC, GTC | ⚠ |
-| `ICEYE` | SLC | ✅ |
-| `ICEYE` | GRD | ✅ |
-| `ICEYE` | ORTHO | 💤 |
-| `RADARSAT Constellation Mission` | SLC | ⚠ |
-| `RADARSAT Constellation Mission` | GRC, GCC, GCD | ⚠ |
-| `RADARSAT Constellation Mission` | GRD | ✅ |
-| `RADARSAT-2` | SLC | ✅ |
-| `RADARSAT-2` | SGX, SCN, SCW,
SCF, SCS, SSG, SPG | ⚠ |
-| `RADARSAT-2` | SGF | ✅ |
-| `Sentinel-1` | SLC | ✅ |
-| `Sentinel-1` | GRD | ✅ |
-| `Sentinel-1` | RTC (ASF) | ✅ |
-| `SAOCOM-1` | SLC | ✅ |
-| `SAOCOM-1` | ID | ⚠ |
-| `SAOCOM-1` | GEC | ✅ |
-| `SAOCOM-1` | GTC | ✅ |
-| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | SSC | ✅ |
-| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | MGD | ✅ |
-| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | GEC | ⚠ |
-| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | EEC | ✅ |
+| Constellations | Product Type | Handled |
+|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
+| `Capella` | SLC | ✅ |
+| `Capella` | GEC | ✅ |
+| `Capella` | GEO | ✅ |
+| `Capella` | SICD, SIDD, CPHD | ❌ |
+| `COSMO-Skymed` | SCS | ✅ |
+| `COSMO-SkyMed` 1st Generation | DGM | ✅ |
+| `COSMO-SkyMed` 2nd Generation | DGM | ⚠ |
+| `COSMO-SkyMed` | GEC, GTC | ⚠ |
+| `ICEYE` | SLC | ✅ |
+| `ICEYE` | GRD | ✅ |
+| `ICEYE` | ORTHO | 💤 |
+| `RADARSAT Constellation Mission` | SLC | ⚠ |
+| `RADARSAT Constellation Mission` | GRC, GCC, GCD | ⚠ |
+| `RADARSAT Constellation Mission` | GRD | ✅ |
+| `RADARSAT-2` | SLC | ✅ |
+| `RADARSAT-2` | SGX, SCN, SCW,
SCF, SCS, SSG, SPG | ⚠ |
+| `RADARSAT-2` | SGF | ✅ |
+| `Sentinel-1` | SLC | ✅ |
+| `Sentinel-1` | GRD | ✅ |
+| `Sentinel-1` | RTC ([ASF](https://hyp3-docs.asf.alaska.edu/guides/rtc_product_guide/#readme-file) and [MPC](https://planetarycomputer.microsoft.com/dataset/sentinel-1-rtc)) | ✅ |
+| `SAOCOM-1` | SLC | ✅ |
+| `SAOCOM-1` | ID | ⚠ |
+| `SAOCOM-1` | GEC | ✅ |
+| `SAOCOM-1` | GTC | ✅ |
+| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | SSC | ✅ |
+| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | MGD | ✅ |
+| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | GEC | ⚠ |
+| `TerraSAR-X`, `TanDEM-X`, `PAZ SAR` | EEC | ✅ |
✅: Tested
⚠: Never tested, **use it at your own risk!**
diff --git a/eoreader/products/__init__.py b/eoreader/products/__init__.py
index b7f28752..cf9764ff 100644
--- a/eoreader/products/__init__.py
+++ b/eoreader/products/__init__.py
@@ -170,7 +170,7 @@
S2Jp2Masks,
S2StacProduct,
)
-from .optical.s2_e84_product import S2E84Product, S2E84StacProduct, S2MPCStacProduct
+from .optical.s2_e84_product import S2E84Product, S2E84StacProduct, S2MpcStacProduct
from .optical.s2_theia_product import S2TheiaProduct
from .optical.s3_product import S3Product, S3ProductType, S3DataType, S3Instrument
from .optical.s3_olci_product import S3OlciProduct
@@ -205,9 +205,9 @@
"S1Product",
"S1SensorMode",
"S1ProductType",
- "S1RTCProduct",
- "S1RTCProductType",
- "SaocomProduct",
+ "S1RtcProduct",
+ "S1RtcProductType",
+ "S1RtcMpcStacProduct" "SaocomProduct",
"SaocomProductType",
"SaocomPolarization",
"SaocomSensorMode" "TsxProduct",
@@ -227,7 +227,7 @@
from .sar.rcm_product import RcmProduct, RcmProductType, RcmSensorMode
from .sar.rs2_product import Rs2Product, Rs2ProductType, Rs2SensorMode
from .sar.s1_product import S1Product, S1SensorMode, S1ProductType
-from .sar.s1_rtc_product import S1RtcProduct, S1RtcProductType
+from .sar.s1_rtc_product import S1RtcAsfProduct, S1RtcProductType, S1RtcMpcStacProduct
from .sar.saocom_product import (
SaocomProduct,
SaocomProductType,
diff --git a/eoreader/products/optical/s2_e84_product.py b/eoreader/products/optical/s2_e84_product.py
index 85b4abe9..1bcc2c90 100644
--- a/eoreader/products/optical/s2_e84_product.py
+++ b/eoreader/products/optical/s2_e84_product.py
@@ -894,7 +894,7 @@ def get_quicklook_path(self) -> str:
return self._get_path("thumbnail")
-class S2MPCStacProduct(StacProduct, S2E84Product):
+class S2MpcStacProduct(StacProduct, S2E84Product):
def __init__(
self,
product_path: AnyPathStrType = None,
diff --git a/eoreader/products/sar/s1_rtc_product.py b/eoreader/products/sar/s1_rtc_product.py
index ceee3580..e23a4c5d 100644
--- a/eoreader/products/sar/s1_rtc_product.py
+++ b/eoreader/products/sar/s1_rtc_product.py
@@ -19,21 +19,26 @@
Take a look
`here `_.
"""
+import asyncio
import logging
from datetime import datetime
from enum import unique
from typing import Union
import geopandas as gpd
+import planetary_computer
from lxml import etree
+from pystac import Item
from rasterio import crs
-from sertit import path, vectors
+from sertit import AnyPath, path, vectors, xml
from sertit.misc import ListEnum
+from sertit.types import AnyPathStrType
from shapely import box
from eoreader import DATETIME_FMT, EOREADER_NAME, cache
+from eoreader.bands import SarBandNames as sab
from eoreader.exceptions import InvalidProductError
-from eoreader.products import S1SensorMode, SarProduct
+from eoreader.products import S1SensorMode, SarProduct, StacProduct
from eoreader.products.product import OrbitDirection
from eoreader.utils import simplify
@@ -53,9 +58,9 @@ class S1RtcProductType(ListEnum):
"""
-class S1RtcProduct(SarProduct):
+class S1RtcAsfProduct(SarProduct):
"""
- Class for S1 RTC
+ Class for S1 RTC from Asf
Take a look
`here `_.
"""
@@ -317,3 +322,245 @@ def get_orbit_direction(self) -> OrbitDirection:
OrbitDirection: Orbit direction (ASCENDING/DESCENDING)
"""
return OrbitDirection.UNKNOWN
+
+
+class S1RtcMpcStacProduct(StacProduct, SarProduct):
+ """
+ Class for S1 RTC from MPC (via stac)
+ Take a look
+ `here `_.
+ """
+
+ def __init__(
+ self,
+ product_path: AnyPathStrType = None,
+ archive_path: AnyPathStrType = None,
+ output_path: AnyPathStrType = None,
+ remove_tmp: bool = False,
+ **kwargs,
+ ) -> None:
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
+ self.kwargs = kwargs
+ """Custom kwargs"""
+
+ # Copy the kwargs
+ super_kwargs = kwargs.copy()
+
+ # Get STAC Item
+ self.item = None
+ """ STAC Item of the product """
+ self.item = super_kwargs.pop("item", None)
+ if self.item is None:
+ try:
+ self.item = Item.from_file(product_path)
+ except TypeError:
+ raise InvalidProductError(
+ "You should either fill 'product_path' or 'item'."
+ )
+
+ self.default_clients = [planetary_computer]
+ self.clients = super_kwargs.pop("client", self.default_clients)
+
+ if product_path is None:
+ # Canonical link is always the second one
+ # TODO: check if ok
+ product_path = AnyPath(self.item.links[0].target).parent
+
+ # Initialization from the super class
+ super().__init__(product_path, archive_path, output_path, remove_tmp, **kwargs)
+
+ def _set_pixel_size(self) -> None:
+ """
+ Set product default pixel size (in meters)
+ """
+ # Use range, maybe azimuth one day
+ self.pixel_size = self.item.properties["sar:pixel_spacing_range"]
+ self.resolution = self.item.properties["sar:resolution_range"]
+
+ def _set_instrument(self) -> None:
+ """
+ Set instrument
+
+ Sentinel-1: https://sentinels.copernicus.eu/web/sentinel/missions/sentinel-1/instrument-payload
+ """
+ self.instrument = "SAR C-band"
+
+ def _pre_init(self, **kwargs) -> None:
+ """
+ Function used to pre_init the products
+ (setting needs_extraction and so on)
+ """
+ self.stac_mtd = self.item.to_dict()
+
+ self._band_folder = self.path
+
+ # Already ORTHO, so OK
+ self.needs_extraction = False
+
+ # Its original filename is its name
+ self._use_filename = True
+
+ # Pre init done by the super class
+ super()._pre_init(**kwargs)
+
+ def _post_init(self, **kwargs) -> None:
+ """
+ Function used to post_init the products
+ (setting product-type, band names and so on)
+ """
+ # Private attributes
+ self._raw_band_regex = "*_{!l}.rtc.tiff"
+
+ # Post init done by the super class
+ super()._post_init(**kwargs)
+
+ def get_raw_band_paths(self, **kwargs) -> dict:
+ """
+ Return the existing band paths (as they come with the archived products).
+
+ Args:
+ **kwargs: Additional arguments
+
+ Returns:
+ dict: Dictionary containing the path of every band existing in the raw products
+ """
+ band_paths = {}
+ for band in sab.speckle_list():
+ try:
+ band_paths[band] = self.sign_url(
+ self.item.assets[band.name.lower()].href
+ )
+ except KeyError:
+ continue
+
+ return band_paths
+
+ def _set_product_type(self) -> None:
+ """Set products type"""
+ self.product_type = S1RtcProductType.RTC
+
+ def _set_sensor_mode(self) -> None:
+ """Get sensor mode"""
+ mode = self.split_name[1]
+ # Mono swath SM
+ if mode in ["S1", "S2", "S3", "S4", "S5", "S6"]:
+ mode = "SM"
+
+ # Get sensor mode
+ self.sensor_mode = S1SensorMode.from_value(mode)
+
+ if not self.sensor_mode:
+ raise InvalidProductError(
+ f"Invalid {self.constellation.value} name: {self.name}"
+ )
+
+ def get_datetime(self, as_datetime: bool = False) -> Union[str, datetime]:
+ """
+ Get the product's acquisition datetime, with format :code:`YYYYMMDDTHHMMSS` <-> :code:`%Y%m%dT%H%M%S`
+
+ .. code-block:: python
+
+ >>> from eoreader.reader import Reader
+ >>> path = r"1011117-766193"
+ >>> prod = Reader().open(path)
+ >>> prod.get_datetime(as_datetime=True)
+ datetime.datetime(2020, 10, 28, 22, 46, 25)
+ >>> prod.get_datetime(as_datetime=False)
+ '20201028T224625'
+
+ Args:
+ as_datetime (bool): Return the date as a datetime.datetime. If false, returns a string.
+
+ Returns:
+ Union[str, datetime.datetime]: Its acquisition datetime
+ """
+ if self.datetime is None:
+ # Sentinel-2 datetime (in the filename) is the datatake sensing time, not the granule sensing time !
+ sensing_time = self.split_name[4]
+
+ # Convert to datetime
+ date = datetime.strptime(sensing_time, "%Y%m%dT%H%M%S")
+ else:
+ date = self.datetime
+
+ if not as_datetime:
+ date = date.strftime(DATETIME_FMT)
+
+ return date
+
+ def _get_name(self) -> str:
+ """
+ Set product real name.
+
+ Returns:
+ str: True name of the product (from metadata)
+ """
+ return self.stac_mtd["id"]
+
+ def _get_name_constellation_specific(self) -> str:
+ """
+ Set product real name from metadata
+
+ Returns:
+ str: True name of the product (from metadata)
+ """
+ # No need here (_get_name reimplemented)
+ pass
+
+ @cache
+ def _read_mtd(self) -> (etree._Element, dict):
+ """
+ Read metadata and outputs the metadata XML root and its namespaces as a dict
+
+ .. code-block:: python
+
+ >>> from eoreader.reader import Reader
+ >>> path = r"1001513-735093"
+ >>> prod = Reader().open(path)
+ >>> prod.read_mtd()
+ (, {})
+
+ Returns:
+ (etree._Element, dict): Metadata XML root and its namespaces
+ """
+ return xml.dict_to_xml(self.stac_mtd), {}
+
+ def get_quicklook_path(self) -> str:
+ """
+ Get quicklook path if existing.
+
+ Returns:
+ str: Quicklook path
+ """
+ quicklook_path = None
+ try:
+ if self.is_archived:
+ quicklook_path = self.path / path.get_archived_path(
+ self.path, file_regex=r".*preview\.png"
+ )
+ else:
+ quicklook_path = next(self.path.glob("*preview.png"))
+ except (StopIteration, FileNotFoundError):
+ pass
+
+ return str(quicklook_path)
+
+ @cache
+ def get_orbit_direction(self) -> OrbitDirection:
+ """
+ Get cloud cover as given in the metadata
+
+ .. code-block:: python
+
+ >>> from eoreader.reader import Reader
+ >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip"
+ >>> prod = Reader().open(path)
+ >>> prod.get_orbit_direction().value
+ "DESCENDING"
+
+ Returns:
+ OrbitDirection: Orbit direction (ASCENDING/DESCENDING)
+ """
+ return OrbitDirection.from_value(
+ self.item.properties["sat:orbit_state"].upper()
+ )