From 811d82231cb98ace5d337b47990fd4e225ac32aa Mon Sep 17 00:00:00 2001
From: Impact <pascal.mouquet@ird.fr>
Date: Mon, 12 Oct 2020 10:16:08 +0400
Subject: [PATCH] initiating sen2chain products versioning

---
 sen2chain/__init__.py             |   6 +-
 sen2chain/config.py               |  16 +-
 sen2chain/data/sen2chain_info.xml |   7 +
 sen2chain/products.py             | 297 +++++++++++++++++++++++++++++-
 sen2chain/sen2cor.py              |   8 +-
 sen2chain/tiles.py                | 184 +++++++++++++++---
 sen2chain/time_series.py          |   2 +-
 sen2chain/utils.py                |   8 +-
 sen2chain/xmlparser.py            |  64 ++++++-
 9 files changed, 544 insertions(+), 48 deletions(-)
 create mode 100644 sen2chain/data/sen2chain_info.xml

diff --git a/sen2chain/__init__.py b/sen2chain/__init__.py
index 9d15b50..c2b715c 100644
--- a/sen2chain/__init__.py
+++ b/sen2chain/__init__.py
@@ -21,16 +21,16 @@ This module lists all externally useful classes and functions.
 
 from .config import Config
 from .tiles import Tile
-from .products import L1cProduct, L2aProduct
+from .products import L1cProduct, L2aProduct, OldCloudMaskProduct, NewCloudMaskProduct
 from .library import Library
 from .data_request import DataRequest
 from .indices import IndicesCollection
 from .download_and_process import DownloadAndProcess
 from .time_series import TimeSeries
 from .automatization import Automatization
-from .utils import format_word, grouper, datetime_to_str, str_to_datetime, human_size_decimal, human_size
+from .utils import format_word, grouper, datetime_to_str, str_to_datetime, human_size_decimal, human_size, get_current_Sen2Cor_version
 from .geo_utils import serialise_tiles_index, get_processed_indices_vect, crop_product_by_shp
 from .multi_processing import l2a_multiprocessing, cldidx_multiprocessing, cld_multiprocessing, idx_multiprocessing
 from .tileset import TileSet
-__version__ = "0.1.0"
+__version__ = "0.6.0"
 __author__ = "Jérémy Commins <jebins@openmailbox.org> & Impact <pascal.mouquet@ird.fr>"
diff --git a/sen2chain/config.py b/sen2chain/config.py
index 5047fb5..0404a5c 100644
--- a/sen2chain/config.py
+++ b/sen2chain/config.py
@@ -16,9 +16,10 @@ logging.basicConfig(level=logging.INFO)
 ROOT = Path(os.path.realpath(__file__)).parent.parent
 
 SHARED_DATA = dict(
-    tiles_index=ROOT / "sen2chain" / "data" / "tiles_index.gpkg",
-    tiles_index_dict=ROOT / "sen2chain" / "data" / "tiles_index_dict.p",
-    peps_download=ROOT / "sen2chain" / "peps_download3.py")
+    tiles_index = ROOT / "sen2chain" / "data" / "tiles_index.gpkg",
+    tiles_index_dict = ROOT / "sen2chain" / "data" / "tiles_index_dict.p",
+    peps_download = ROOT / "sen2chain" / "peps_download3.py",
+    sen2chain_meta = ROOT / "sen2chain" / "data" / "sen2chain_info.xml")
 
 
 class Config:
@@ -42,18 +43,21 @@ class Config:
         self._config_params = ConfigParser()
         self._config_params["DATA PATHS"] = {"temp_path": "",
                                              "l1c_path": "",
-                                             "l1c_archive_path": "/ARCHIVE_SENTINEL-PROD/S2_L1C_ARCHIVE",
+                                             "l1c_archive_path": "",
                                              "l2a_path": "",
-                                             "l2a_archive_path": "/ARCHIVE_SENTINEL-PROD/S2_L2A_ARCHIVE",
+                                             "l2a_archive_path": "",
                                              "indices_path": "",
                                              "time_series_path": "",
-                                             "temporal_summaries_path": ""}
+                                             "temporal_summaries_path": "",
+                                             "cloudmasks_path": "",
+                                             }
         self._config_params["SEN2COR PATH"] = {"sen2cor_bashrc_path": ""}
         self._config_params["HUBS LOGINS"] = {"scihub_id": "",
                                               "scihub_pwd": "",
                                               "peps_config_path": ""}
         self._config_params["PROXY SETTINGS"] = {"proxy_http_url": "",
                                                  "proxy_https_url": ""}
+        self._config_params["SEN2CHAIN VERSIONS"] = {"sen2chain_processing_version": "xx.xx"}
         
         if self._CONFIG_FILE.exists():
             self._config_params.read(str(self._CONFIG_FILE))
diff --git a/sen2chain/data/sen2chain_info.xml b/sen2chain/data/sen2chain_info.xml
new file mode 100644
index 0000000..05bcdbe
--- /dev/null
+++ b/sen2chain/data/sen2chain_info.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Sen2Chain_Report_File>
+    <SEN2CHAIN_VERSION></SEN2CHAIN_VERSION>
+    <SEN2CHAIN_PROCESSING_VERSION></SEN2CHAIN_PROCESSING_VERSION>
+    <SEN2COR_VERSION></SEN2COR_VERSION>
+</Sen2Chain_Report_File>
+
diff --git a/sen2chain/products.py b/sen2chain/products.py
index e59cc13..3a35bc2 100755
--- a/sen2chain/products.py
+++ b/sen2chain/products.py
@@ -13,14 +13,15 @@ from pathlib import Path
 # type annotations
 from typing import List, Tuple, Optional
 
-from .utils import grouper, setPermissions
+from .utils import grouper, setPermissions, get_current_Sen2Cor_version
 from .config import Config, SHARED_DATA
-from .xmlparser import MetadataParser
+from .xmlparser import MetadataParser, Sen2ChainMetadataParser
 from .sen2cor import process_sen2cor
 from .cloud_mask import create_cloud_mask, create_cloud_mask_v2, create_cloud_mask_b11
 from .indices import IndicesCollection
 from .colormap import create_l2a_ql, create_l1c_ql
 
+
 s2_tiles_index = SHARED_DATA.get("tiles_index")
 
 logging.basicConfig(level=logging.INFO)
@@ -133,7 +134,10 @@ class Product:
         :param key: tag of the metadata to search.
         """
         self._update_metadata_parser()
-        return self._metadata_parser.get_band_path(key=key, res=res)
+        try:
+            return self._metadata_parser.get_band_path(key=key, res=res)
+        except:
+            return None
 
     @property
     def in_library(self) -> bool:
@@ -207,6 +211,7 @@ class L1cProduct(Product):
             if self._library_product:
                 l2a_prod.archive()
             L2aProduct(l2a_identifier).setPermissions()
+            L2aProduct(l2a_identifier).update_sen2chain_metadata()
         return self
 
     def process_ql(self,
@@ -363,35 +368,44 @@ class L2aProduct(Product):
     _library_path = Path(Config().get("l2a_path"))
     _indices_library_path = Path(Config().get("indices_path"))
     _tiled_metadata = "MTD_MSIL2A.xml"
-
+    
     def __init__(
             self,
             identifier: str = None,
             tile: str = None,
             path: str = None
     ) -> None:
-
+        logger.info("l2a-001")
         super().__init__(identifier=identifier, tile=tile, path=path)
         if not re.match(r".*L2A_.*", identifier):
             raise ValueError("Invalid L2A product name")
 
         self.indices_path = self._indices_library_path
-
+        logger.info("l2a-002")
         # user cloud mask
         self.user_cloud_mask = self.path.parent / (self.identifier + "_CLOUD_MASK.jp2")
         if not self.user_cloud_mask.exists():
             self.user_cloud_mask = None
-
+        logger.info("l2a-003")
         # user cloud mask b11
         self.user_cloud_mask_b11 = self.path.parent / (self.identifier + "_CLOUD_MASK_B11.jp2")
         if not self.user_cloud_mask_b11.exists():
             self.user_cloud_mask_b11 = None
-
+        logger.info("l2a-004")
         # user QL
         self.user_ql = self.path.parent / (self.identifier + "_QL.tif")
         if not self.user_ql.exists():
             self.user_ql = None
-
+        
+        logger.info("l2a-005")
+        # versions
+        self._sen2chain_info_path = self.path / "sen2chain_info.xml"
+        logger.info("l2a-005-00")
+        if self._sen2chain_info_path.parent.exists() and not self._sen2chain_info_path.exists():
+            logger.info("l2a-005-01")
+            Sen2ChainMetadataParser(self._sen2chain_info_path).init_metadata()
+        logger.info("l2a-006")
+        
     def process_ql(self,
                    reprocess: bool = False,
                    out_path: pathlib.PosixPath = None,
@@ -502,6 +516,62 @@ class L2aProduct(Product):
             self.user_cloud_mask_b11 = cloud_mask_b11_path
 
         return self
+        
+        
+    def compute_cloud_mask(self,
+                              version: str = "cm001",
+                              probability: int = 1,
+                              iterations: int = 5,
+                              #~ buffering: bool = True,
+                              reprocess: bool = False,
+                              out_path_mask = None,
+                              out_path_mask_b11 = None
+                              ) -> "L2aProduct":
+        """
+        """
+        logger.info("{}: processing cloud_mask_v3".format(self.identifier))
+
+        cloudmask = NewCloudMaskProduct(l2a_identifier = self.identifier, 
+                                        sen2chain_processing_version = self.sen2chain_processing_version,
+                                        version = version, 
+                                        probability = probability, 
+                                        iterations = iterations)
+        
+        if cloudmask.path and not reprocess:
+                logger.info("{} cloud mask already exists".format(cloudmask.identifier))
+        else:
+            if cloudmask.updated_path:
+                    if version == "cm001":
+                        if cloudmask.path:
+                            cloudmask.path.unlink()    # in version 3.8 will be updated using missing_ok = True
+                        create_cloud_mask_v2(self.cld_20m,
+                                             erosion=1,
+                                             dilatation=5,
+                                             out_path=cloudmask.updated_path)
+                        self.user_cloud_mask = cloudmask.updated_path
+                    elif version == "cm002":
+                        cloudmask_cm001 = NewCloudMaskProduct(l2a_identifier = self.identifier, 
+                                                              sen2chain_processing_version = self.sen2chain_processing_version,
+                                                              version = "cm001")
+                        if cloudmask_cm001.path:
+                            if cloudmask.path:
+                                cloudmask.path.unlink()    # in version 3.8 will be updated using missing_ok = True
+                            create_cloud_mask_b11(cloudmask_cm001.path,
+                                                  self.b11_20m,
+                                                  dilatation=5,
+                                                  out_path=Path(str(cloudmask_cm001.path).replace("CM001", "CM002-B11")))
+                        else:
+                            logger.info("No cloudmask version cm001 found, please compute this one first")
+                    elif version == "cm003":
+                        toto=12
+                    elif version == "cm004":
+                        toto=12
+                    else:
+                        logger.info("Wrong cloudmask version {}".format(version))
+            else:
+                logger.info("Impossible to compute cloudmask (no L2a product)")
+        
+        return self
 
     def process_indices(self,
                         indices_list: List[str] = [],
@@ -565,6 +635,48 @@ class L2aProduct(Product):
     # indice_filename = template.format(product_identifier=self.identifier,
     # ext=ext)
     # return (self.indices_path / indice / self.tile / indice_filename).exists()
+    
+    #~ @property
+    #~ def sen2cor_version(self):
+        #~ """ Used Sen2Cor version"""
+        #~ return Sen2ChainMetadataParser(self.identifier):
+            
+            
+        #~ sen2chain_info_path = self.path / "Sen2Chain_info.xml"
+        #~ if sen2chain_info_path.exists():
+            #~ return "xml present"
+            #~ Sen2ChainMetadataParser(self.identifier)
+            
+        #~ else:
+            #~ return None
+    
+    def update_md(self,
+                  sen2chain_version: str = None,
+                  sen2chain_processing_version: str = None,
+                  sen2cor_version: str = None,
+                  ):
+        """ Set custom sen2chain, sen2chain_processing and sen2cor versions """
+        Sen2ChainMetadataParser(self._sen2chain_info_path).set_metadata(sen2chain_version = sen2chain_version,
+                                                                        sen2chain_processing_version = sen2chain_processing_version,
+                                                                        sen2cor_version = sen2cor_version)
+        
+
+    @property
+    def sen2chain_version(self):
+        return Sen2ChainMetadataParser(self._sen2chain_info_path).get_metadata_value('SEN2CHAIN_VERSION')
+    
+    @property
+    def sen2chain_processing_version(self):
+        logger.info("SPV-001")
+        return Sen2ChainMetadataParser(self._sen2chain_info_path).get_metadata_value('SEN2CHAIN_PROCESSING_VERSION')
+    
+    @property
+    def sen2cor_version(self):
+        return Sen2ChainMetadataParser(self._sen2chain_info_path).get_metadata_value('SEN2COR_VERSION')
+        
+    @property
+    def generation_time(self):
+        return self._get_metadata_value(key="GENERATION_TIME")
 
     # METADATA
     @property
@@ -822,3 +934,170 @@ class L2aProduct(Product):
     @property
     def tci_60m(self):
         return self._get_band_path(key="TCI", res="60m")
+
+
+class OldCloudMaskProduct:
+    """Old cloud mask product class.
+
+    :param identifier: cloudmask filename.
+
+    """
+    _library_path = Path(Config().get("l2a_path"))
+    
+    def __init__(self, identifier: str = None) -> None:
+        if identifier is None:
+            raise ValueError("Product identifier is empty")
+        else:
+            self.identifier = identifier
+        self.tile = self.get_tile(identifier)
+        self.l2a = self.get_l2a(identifier)
+        self.path = self._library_path / self.tile / self.identifier
+        
+    
+    @staticmethod
+    def get_tile(identifier) -> str:
+        """Returns tile name from a string.
+
+        :param string: string from which to extract the tile name.
+        """
+        return re.findall("_T([0-9]{2}[A-Z]{3})_", identifier)[0]
+    
+    @staticmethod
+    def get_l2a(identifier) -> str:
+        """Returns l2a name from a old cloud mask identifier string.
+        :param string: string from which to extract the l2a name.
+        """
+        return re.findall(r"(S2.+)_CLOUD_MASK.+jp2", identifier)[0] 
+    
+    
+class NewCloudMaskProduct:
+    """New cloud mask product class.
+
+    :param identifier: cloudmask filename.
+
+    """
+    _library_path = Path(Config().get("cloudmasks_path"))
+    
+    def __init__(self, 
+                 identifier: str = None,
+                 l2a_identifier: str = None,
+                 sen2chain_processing_version: str = None,
+                 version: str = "cm001",
+                 probability: int = 1,
+                 iterations: int = 5,
+                 ) -> None:
+        if not (identifier or l2a_identifier):
+            raise ValueError("Product or L2a identifier cannot be empty")
+        else:
+            self.tile = self.get_tile(identifier or l2a_identifier)
+            self.l2a = l2a_identifier.replace(".SAFE", "") or self.get_l2a(identifier)
+            self.path = None
+            self.updated_path = None
+            self.spv = None
+            self.uptodate = False
+            self.updatable = False
+            #~ self.theorical_path = None
+            if identifier:
+                self.identifier = identifier
+            else:
+                self.identifier = None
+                if version == "cm001":
+                    suffix = "_CM001"
+                elif version == "cm002":
+                    suffix = "_CM002-B11"
+                elif version == "cm003":
+                    suffix = "_CM003-PRB" + str(probability) + "ITR" + str(iterations)
+                else:
+                    raise ValueError("Cloud Mask version cmxxx is not defined")
+                logger.info("002")
+                
+                self.path = next(iter(list((self._library_path / self.tile / self.l2a).glob(self.l2a.replace(".SAFE", "") + \
+                                                  "_SPV*" + suffix + ".jp2"))), \
+                                       None)
+                if self.path:
+                    self.identifier = self.path.name
+                    self.spv = re.findall("_SPV([0-9]{4})_", str(self.path))[0]
+                    if self.spv == Config().get("sen2chain_processing_version"):
+                        self.uptodate = True
+                    else:
+                        l2a = L2aProduct(self.l2a)
+                        if l2a.path.exists() and int(l2a.sen2chain_processing_version) > int(self.spv):
+                            self.updatable = True
+                
+                l2a = L2aProduct(self.l2a)
+                if l2a.path.exists():
+                    self.updated_path = self._library_path /\
+                                          self.tile /\
+                                          self.l2a /\
+                                          (self.l2a + "_SPV" + l2a.sen2chain_processing_version +\
+                                          suffix + ".jp2")
+                    
+                                    
+                #~ self.uptodate = # vrai ou faux si les deux sont égaux et si spv sup 
+                #~ self.updatable = # vrai si uptidater et l2a exist
+                
+                
+                
+                
+                #~ l2a_product = L2aProduct(l2a_identifier)                
+                #~ if not sen2chain_processing_version:
+                    #~ logger.info("002-01")
+                    #~ path = list((self._library_path / l2a_product.tile / self.l2a).glob(self.l2a.replace(".SAFE", "") + \
+                                  #~ "_SPV*" + \
+                                  #~ suffix + \
+                                  #~ ".jp2"))
+                    #~ logger.info(path)
+                    #~ if not path:
+                        #~ try:
+                            #~ sen2chain_processing_version = l2a_product.sen2chain_processing_version
+                            #~ logger.info("sen2chain_processing_version {}".format(sen2chain_processing_version))
+                            #~ self.identifier = self.l2a.replace(".SAFE", "") + \
+                                              #~ "_SPV" + str(sen2chain_processing_version or Config().get("sen2chain_processing_version")) + \
+                                              #~ suffix + \
+                                              #~ ".jp2"
+                            #~ self.l2a_present = False
+                            
+                        #~ except:
+                            #~ self.l2a_present = True
+                            #~ self.spv=None
+                            #~ self.identifier = self.l2a.replace(".SAFE", "") + \
+                                              #~ "_SPV" + Config().get("sen2chain_processing_version") + \
+                                              #~ suffix + \
+                                              #~ ".jp2"
+                            
+                    #~ else:
+                        #~ self.identifier = path[0].name
+                        #~ self.spv = re.findall("_SPV([0-9]{4})_", self.identifier)[0]
+                #~ logger.info("003")
+
+                                  
+                                  
+        #~ else:
+            #~ self.identifier = identifier
+            #~ self.spv = re.findall("_SPV([0-9]{4})_", identifier)[0]
+            #~ self.l2a = self.get_l2a(self.identifier)
+        #~ logger.info("004")
+
+        #~ logger.info("005")
+        
+        #~ logger.info("006")
+        #~ self.path = self._library_path / self.tile / self.l2a / self.identifier
+        #~ logger.info(self.path)
+        #~ if self.path.exists() and 
+        
+    
+    @staticmethod
+    def get_tile(identifier) -> str:
+        """Returns tile name from a string.
+
+        :param string: string from which to extract the tile name.
+        """
+        return re.findall("_T([0-9]{2}[A-Z]{3})_", identifier)[0]
+    
+    @staticmethod
+    def get_l2a(identifier) -> str:
+        """Returns l2a name from a old cloud mask identifier string.
+        :param string: string from which to extract the l2a name.
+        """
+        return re.findall(r"(S2.+)_SPV.+jp2", identifier)[0] 
+    
diff --git a/sen2chain/sen2cor.py b/sen2chain/sen2cor.py
index 85ac0f1..c88cce6 100644
--- a/sen2chain/sen2cor.py
+++ b/sen2chain/sen2cor.py
@@ -7,10 +7,11 @@ Module for sen2cor processings.
 import logging
 import pathlib
 import subprocess
-import re
+#~ import re
 from typing import Union
 
-from .config import Config
+#~ from .config import Config
+from .utils import get_current_Sen2Cor_version
 
 
 logger = logging.getLogger(__name__)
@@ -36,7 +37,8 @@ def process_sen2cor(
     # TODO: Add 60m resolution.
 
     sen2cor_bashrc_path = Config().get("sen2cor_bashrc_path")
-    s2c_v = next(iter(re.findall('Sen2Cor-(\d{2}\.\d{2}\.\d{2})', str(sen2cor_bashrc_path))), None)
+    #~ s2c_v = next(iter(re.findall('Sen2Cor-(\d{2}\.\d{2}\.\d{2})', str(sen2cor_bashrc_path))), None)
+    s2c_v = get_current_Sen2Cor_version()
     l2a_product_path_tmp = l2a_product_path.parent / (l2a_product_path.stem + '.tmp')
         
     if s2c_v == '02.05.05':
diff --git a/sen2chain/tiles.py b/sen2chain/tiles.py
index 2f5155a..d0ae050 100644
--- a/sen2chain/tiles.py
+++ b/sen2chain/tiles.py
@@ -22,7 +22,7 @@ from itertools import chain
 from .config import Config, SHARED_DATA
 from .utils import str_to_datetime, human_size, getFolderSize
 from .indices import IndicesCollection
-from .products import L1cProduct, L2aProduct
+from .products import L1cProduct, L2aProduct, OldCloudMaskProduct, NewCloudMaskProduct
 from .multi_processing import l2a_multiprocessing, cld_multiprocessing, idx_multiprocessing
 
 logging.basicConfig(level=logging.INFO)
@@ -155,6 +155,47 @@ class IndicesList(ProductsList):
         return filtered
 
 
+class CloudMaskList(ProductsList):
+    """Class for managing mask product list
+
+    """
+    @property
+    def cm001(self) -> "CloudMaskList":
+        filtered = CloudMaskList()
+        for k, v in self._dict.items():
+            if "_CM001" in k:
+                filtered[k] = {"date": v["date"], "cloud_cover": v["cloud_cover"]}
+        return filtered
+
+    @property
+    def cm002(self) -> "CloudMaskList":
+        filtered = CloudMaskList()
+        for k, v in self._dict.items():
+            if "_CM002" in k:
+                filtered[k] = {"date": v["date"], "cloud_cover": v["cloud_cover"]}
+        return filtered
+
+    @property
+    def cm003(self) -> "CloudMaskList":
+        filtered = CloudMaskList()
+        for k, v in self._dict.items():
+            if "_CM003" in k:
+                filtered[k] = {"date": v["date"], "cloud_cover": v["cloud_cover"]}
+        return filtered
+    
+    def params(self,
+               probability: int = 1, 
+               iterations: int = 5,
+               ):
+        filtered = CloudMaskList()
+        for k, v in self._dict.items():
+            if "_CM003" in k:
+                if "-PRB" + str(probability) + "ITER" + str(iterations) in k:
+                    filtered[k] = {"date": v["date"], "cloud_cover": v["cloud_cover"]}
+            else:
+                filtered[k] = {"date": v["date"], "cloud_cover": v["cloud_cover"]}
+        return filtered        
+
 class Tile:
     """Class for managing tiles in the library.
 
@@ -167,19 +208,23 @@ class Tile:
         self.name = name
         self._paths = {"l1c": Path(Config().get("l1c_path")) / name,
                        "l2a": Path(Config().get("l2a_path")) / name,
-                       "indices": {}
+                       "indices": {}, 
+                       "cloudmasks": Path(Config().get("cloudmasks_path")) / name,
                        }
         self._indices_path = Path(Config().get("indices_path"))
-
+        self._cloudmasks_path = Path(Config().get("cloudmasks_path"))
+        
         self._products = {"l1c": ProductsList(),
                           "l2a": ProductsList(),
                           "cloudmasks" : ProductsList(),
+                          "cloudmasks2" : ProductsList(),
                           "indices": dict()}
 
         self._get_indices_paths()
         self._get_l1c_list()
         self._get_l2a_list()
         self._get_cloudmasks()
+        self._get_cloudmasks2()
         self._get_indices_list()
 
     def _get_indices_paths(self) -> None:
@@ -217,6 +262,22 @@ class Tile:
                 cloud_cover = L2aProduct(f.name.replace("_CLOUD_MASK.jp2", ".SAFE"), self.name).cloud_coverage_assessment
             self._products["cloudmasks"][f.name.replace("_CLOUD_MASK.jp2", ".SAFE")] = {"date": date, "cloud_cover": float(cloud_cover)}
 
+    def _get_cloudmasks2(self) -> None:
+        """Scans cloudmasks folder for cloud masks and adds corresponding L2A products in a ProductsList."""
+        self._products["cloudmasks2"] = CloudMaskList()
+        for f in self._paths["cloudmasks"].glob("*L2A*/*_CM*.jp2"):
+            l1c_name = f.parent.name.replace("L2A_", "L1C_").replace("_USER_", "_OPER_") + ".SAFE"
+            try:
+                date = self._products["l1c"][l1c_name].date
+                cloud_cover = self._products["l1c"][l1c_name].cloud_cover
+            except KeyError:
+                date = Tile._get_date(f.parent.name)
+                cloud_cover = L2aProduct(f.parent.name, self.name).cloud_coverage_assessment
+            self._products["cloudmasks2"][f.name] = {"date": date, 
+                                                            "cloud_cover": float(cloud_cover), 
+                                                            #~ "version": re.findall(r"_(CM...)", f.name)[0]
+                                                            }
+
     def _get_indices_list(self) -> None:
         """Scans indices folders and adds products in a IndicesList."""
         for indice, path in self._paths["indices"].items():
@@ -269,7 +330,7 @@ class Tile:
 
     def __str__(self) -> str:
         return self.name
-
+    
     @property
     def paths(self) -> Dict[str, pathlib.PosixPath]:
         """Returns all the paths related to a Tile object."""
@@ -290,6 +351,11 @@ class Tile:
         """Returns tile's cloud masks products as a ProductsList."""
         return self._products["cloudmasks"]
 
+    @property
+    def cloudmasks2(self) -> "ProductsList":
+        """Returns tile's cloud masks products as a ProductsList."""
+        return self._products["cloudmasks2"]
+
     @property
     def l1c_missings(self) -> "ProductsList":
         """Returns tile's L2A products that don't have a L1C as a ProductsList."""
@@ -321,6 +387,22 @@ class Tile:
             prods_list[prod] = {"date": self._products["l2a"][prod].date,
                                 "cloud_cover": self._products["l2a"][prod].cloud_cover}
         return prods_list
+
+    def cloudmasks_missing(self,
+                           version: str = "cm001",
+                           probability: int = 1,
+                           iterations: int = 5,
+                           ) -> "ProductsList":
+        """Returns tile's L2A products that don't have a cloud mask as a ProductsList."""
+        prods_list = ProductsList()
+        missings_l2a_set = set(self.l2a.products) - {(re.findall(r"(S2.+)_SPV.+.jp2", identifier)[0] + ".SAFE") \
+                                                     for identifier in getattr(self.cloudmasks2, version).\
+                                                                               params(probability = probability, iterations = iterations).\
+                                                                               products}
+        for prod in missings_l2a_set:
+            prods_list[prod] = {"date": self._products["l2a"][prod].date,
+                                "cloud_cover": self._products["l2a"][prod].cloud_cover}
+        return prods_list
     
     @property
     def info(self):
@@ -332,22 +414,24 @@ class Tile:
     
     @property
     def size(self):
-        #~ logger.info("Product: local / archived / total".format(human_size(getFolderSize(str(self.paths['l1c'])))))
-        local = getFolderSize(str(self.paths["l1c"]))
-        total = getFolderSize(str(self.paths["l1c"]), True)
-        logger.info("l1c: {} (local: {} / archived: {})".format(human_size(total),
-                                                   human_size(local),
-                                                   human_size(total-local),
-                                                   ))
-        
-        #~ logger.info("l2a: {}".format(human_size(getFolderSize(str(self.paths["l2a"]), True))))
-        local = getFolderSize(str(self.paths["l2a"]))
-        total = getFolderSize(str(self.paths["l2a"]), True)
-        logger.info("l2a: {} (local: {} / archived: {})".format(human_size(total),
-                                                   human_size(local),
-                                                   human_size(total-local),
-                                                   ))
-        
+        try:
+            local = getFolderSize(str(self.paths["l1c"]))
+            total = getFolderSize(str(self.paths["l1c"]), True)
+            logger.info("l1c: {} (local: {} / archived: {})".format(human_size(total),
+                                                                    human_size(local),
+                                                                    human_size(total-local),
+                                                                    ))
+        except:
+            pass
+        try:
+            local = getFolderSize(str(self.paths["l2a"]))
+            total = getFolderSize(str(self.paths["l2a"]), True)
+            logger.info("l2a: {} (local: {} / archived: {})".format(human_size(total),
+                                                                    human_size(local),
+                                                                    human_size(total-local),
+                                                                    ))
+        except:
+            pass
         for indice, path in self._paths["indices"].items():
             logger.info("{}: {}".format(indice, human_size(getFolderSize(str(path), True))))
             
@@ -384,9 +468,12 @@ class Tile:
             l2a_res = l2a_multiprocessing(l1c_process_list, nb_proc=nb_proc)
         
     def compute_cloudmasks(self,
-                            date_min: str = None,
-                            date_max: str = None,
-                            nb_proc: int = 4):
+                           version: str = "cm001",
+                           probability: int = 1,
+                           iterations: int = 5,
+                           date_min: str = None,
+                           date_max: str = None,
+                           nb_proc: int = 4):
         """
         Compute all missing cloud masks for l2a products
         """
@@ -402,7 +489,31 @@ class Tile:
         cld_res = False
         if cld_l2a_process_list:
             cld_res = cld_multiprocessing(cld_l2a_process_list, nb_proc=nb_proc)
-    
+
+    #~ def compute_cloudmasks2(self,
+                           #~ version: str = "cm001",
+                           #~ probability: int = 1,
+                           #~ iterations: int = 5,
+                           #~ date_min: str = None,
+                           #~ date_max: str = None,
+                           #~ nb_proc: int = 4):
+        #~ """
+        #~ Compute all missing cloud masks for l2a products
+        #~ """
+        
+        #~ cld_l2a_process_list =  []
+        #~ cld_l2a_process_list.append(list(p.identifier for p in self.cloudmasks_missings.filter_dates(date_min = date_min, date_max = date_max)))
+        #~ cld_l2a_process_list = list(chain.from_iterable(cld_l2a_process_list))
+        #~ if cld_l2a_process_list:
+            #~ logger.info("{} l2a products to process:".format(len(cld_l2a_process_list)))
+            #~ logger.info("{}".format(cld_l2a_process_list))
+        #~ else:
+            #~ logger.info("All cloud masks already computed")
+        #~ cld_res = False
+        #~ if cld_l2a_process_list:
+            #~ cld_res = cld_multiprocessing(cld_l2a_process_list, nb_proc=nb_proc)
+
+
     def compute_indices(self,
                         indices: list = [],
                         date_min: str = None,
@@ -674,3 +785,28 @@ class Tile:
         else:
             l2a.process_ql(out_path = outfullpath, out_resolution=(750,750), jpg = True)
         
+    def update_cloudmasks(self):
+        """
+        Move and rename old cloudmasks to new cloudmask folder
+        cloudmask xmls are removed
+        """
+        #Move and rename old  masks / B11
+        logger.info("Moving and renaming old masks")
+        for f in self._paths["l2a"].glob("*L2A*_CLOUD_MASK*.jp2"):
+            p = OldCloudMaskProduct(f.name)
+            f_renamed = f.name.replace("CLOUD_MASK_B11", 
+                                  "SPV" + \
+                                  L2aProduct(p.l2a).sen2chain_processing_version.replace(".", "") +\
+                                  "_CM002-B11")\
+                              .replace("CLOUD_MASK", 
+                                  "SPV" + \
+                                  L2aProduct(p.l2a).sen2chain_processing_version.replace(".", "") +\
+                                  "_CM001")\
+            p_new = NewCloudMaskProduct(f_renamed)
+            p_new.path.parent.mkdir(exist_ok=True)
+            p.path.replace(p_new.path)
+        
+        #Remove xml
+        logger.info("Removing xmls")
+        for f in self._paths["l2a"].glob("*L2A*_CLOUD_MASK*.jp2.aux.xml"):
+            f.unlink()
diff --git a/sen2chain/time_series.py b/sen2chain/time_series.py
index a7911c4..c29bc67 100644
--- a/sen2chain/time_series.py
+++ b/sen2chain/time_series.py
@@ -41,7 +41,7 @@ from .products import L2aProduct
 logger = logging.getLogger(__name__)
 logging.basicConfig(level=logging.INFO)
 #~ logging.basicConfig(format='%(process)s %(asctime)s %(levelname)s:%(module)s:%(message)s', level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S')
-logging.getLogger().handlers[0].setFormatter(logging.Formatter('%(process)s %(asctime)s %(levelname)s:%(name)s:%(message)s', 
+logging.getLogger().handlers[0].setFormatter(logging.Formatter('%(process)s:%(asctime)s:%(levelname)s:%(name)s:%(message)s', 
                                                                 '%Y-%m-%d %H:%M:%S'))
 
 class TimeSeries:
diff --git a/sen2chain/utils.py b/sen2chain/utils.py
index bdd95fb..4b235a2 100644
--- a/sen2chain/utils.py
+++ b/sen2chain/utils.py
@@ -9,7 +9,9 @@ import logging
 from itertools import zip_longest
 from datetime import datetime
 from typing import Iterable
+import re
 
+from .config import Config
 
 logger = logging.getLogger(__name__)
 logging.basicConfig(level=logging.INFO)
@@ -110,7 +112,11 @@ def setPermissions(path):
             os.chmod(name, os.stat(name).st_mode | 0o064)
             os.chmod(name, os.stat(name).st_mode & ~0o003)
             
+def get_current_Sen2Cor_version():
+    """ Returns your current Sen2Cor version """
+    sen2cor_bashrc_path = Config().get("sen2cor_bashrc_path")
+    return next(iter(re.findall('Sen2Cor-(\d{2}\.\d{2}\.\d{2})', str(sen2cor_bashrc_path))), None)
+
 
-    
     
 
diff --git a/sen2chain/xmlparser.py b/sen2chain/xmlparser.py
index 6f0e918..b3f20d6 100644
--- a/sen2chain/xmlparser.py
+++ b/sen2chain/xmlparser.py
@@ -9,11 +9,15 @@ import pathlib
 import re
 import xml.etree.ElementTree as et
 
+import sen2chain
+from .utils import get_current_Sen2Cor_version
+from .config import SHARED_DATA, Config
+
+
 
 logger = logging.getLogger(__name__)
 logging.basicConfig(level=logging.INFO)
 
-
 class MetadataParser:
     """Class for getting metadata values from a L1C or L2A product's XML metadata file.
 
@@ -126,3 +130,61 @@ class MetadataParser:
         except IndexError:
             logger.error("Band not found: {}".format(pattern))
             raise
+
+class Sen2ChainMetadataParser:
+    """
+    
+    """
+    def __init__(self, 
+                 xml_path,
+                 ):
+        SEN2CHAIN_META = SHARED_DATA.get("sen2chain_meta")
+        logger.info("Sen2ChainMetadataParser-001")
+        self.xml_path = xml_path
+        
+        if xml_path.exists():
+            logger.info("Sen2ChainMetadataParser-002")
+            self._tree = et.parse(str(xml_path))
+            logger.info("Sen2ChainMetadataParser-003")
+
+        else:
+            logger.info("Sen2ChainMetadataParser-004")
+            self._tree = et.parse(str(SEN2CHAIN_META))
+            logger.info("Sen2ChainMetadataParser-005")
+
+        self._root = self._tree.getroot()
+        
+    def get_default_values(self):
+        keys = ["SEN2CHAIN_VERSION", "SEN2CHAIN_PROCESSING_VERSION", "SEN2COR_VERSION"]
+        self.default_values = []
+        for key in keys:
+            self.default_values.append([v.text for v in self._root.findall(".//{0}".format(key))][0])
+            
+    def get_metadata_value(self, key: str = None) -> str:
+        """
+        Returns metadata.
+
+        :param key: metadata tag name.
+        """
+        try:
+            logger.info("get_metadata_value-001")
+            return [v.text for v in self._root.findall(".//{0}".format(key))][0]
+        except IndexError:
+            logger.error("Metadata value not found: {}".format(key))
+    
+    def init_metadata(self):
+        self._root.find("SEN2CHAIN_VERSION").text = sen2chain.__version__
+        self._root.find("SEN2CHAIN_PROCESSING_VERSION").text = Config().get("sen2chain_processing_version")
+        self._root.find("SEN2COR_VERSION").text = get_current_Sen2Cor_version()
+        self._tree.write(str(self.xml_path), encoding="UTF-8", xml_declaration=True)
+    
+    def set_metadata(self,
+                     sen2chain_version: str = None, 
+                     sen2chain_processing_version: str = None,
+                     sen2cor_version: str = None,
+                     ):
+        self._root.find("SEN2CHAIN_VERSION").text = sen2chain_version or sen2chain.__version__
+        self._root.find("SEN2CHAIN_PROCESSING_VERSION").text = sen2chain_processing_version or Config().get("sen2chain_processing_version")
+        self._root.find("SEN2COR_VERSION").text = sen2cor_version or get_current_Sen2Cor_version()
+        self._tree.write(str(self.xml_path), encoding="UTF-8", xml_declaration=True)
+
-- 
GitLab