# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/handlers/helcom.ipynb.

# %% auto 0
__all__ = ['src_dir', 'fname_out_nc', 'zotero_key', 'default_smp_types', 'fixes_nuclide_names', 'lut_nuclides', 'coi_sediment',
           'coi_val', 'coi_units_unc', 'lut_units', 'lut_dl', 'coi_dl', 'fixes_biota_species', 'lut_biota',
           'fixes_biota_tissues', 'lut_tissues', 'lut_biogroup_from_biota', 'fixes_sediments', 'sed_replace_lut',
           'lut_sediments', 'lut_filtered', 'kw', 'read_csv', 'load_data', 'RemapNuclideNameCB', 'ParseTimeCB',
           'SplitSedimentValuesCB', 'SanitizeValueCB', 'unc_rel2stan', 'NormalizeUncCB', 'RemapUnitCB',
           'RemapDetectionLimitCB', 'RemapSedimentCB', 'RemapFiltCB', 'AddSampleIDCB', 'AddDepthCB', 'AddSalinityCB',
           'AddTemperatureCB', 'RemapSedSliceTopBottomCB', 'LookupDryWetPercentWeightCB', 'ParseCoordinates',
           'get_attrs', 'encode']

# %% ../../nbs/handlers/helcom.ipynb 6
import pandas as pd 
import numpy as np
import fastcore.all as fc 
from pathlib import Path 
from typing import List, Dict, Callable, Tuple, Any 
import re
import time

from marisco.utils import (
    Remapper, 
    ddmm_to_dd,
    Match, 
    get_unique_across_dfs,
    ExtractNetcdfContents,
    NA
)

from marisco.callbacks import (
    Callback, 
    Transformer, 
    EncodeTimeCB, 
    LowerStripNameCB, 
    SanitizeLonLatCB, 
    CompareDfsAndTfmCB, 
    RemapCB
)

from marisco.metadata import (
    GlobAttrsFeeder, 
    BboxCB, 
    DepthRangeCB, 
    TimeRangeCB, 
    ZoteroCB, 
    KeyValuePairCB
)

from marisco.configs import (
    nuc_lut_path, 
    cfg, 
    species_lut_path, 
    sediments_lut_path, 
    bodyparts_lut_path, 
    detection_limit_lut_path, 
    filtered_lut_path, 
    get_lut, 
    unit_lut_path,
    cache_path
)

from ..encoders import NetCDFEncoder
from ..netcdf_to_refine import decode

import warnings
warnings.filterwarnings('ignore')

# %% ../../nbs/handlers/helcom.ipynb 10
src_dir = 'https://raw.githubusercontent.com/franckalbinet/maris-crawlers/refs/heads/main/data/processed/HELCOM%20MORS'
fname_out_nc = '../../_data/output/100-HELCOM-MORS-2024.nc'
zotero_key ='26VMZZ2Q' # HELCOM MORS zotero key

# %% ../../nbs/handlers/helcom.ipynb 13
default_smp_types = {  
    'BIO': 'BIOTA', 
    'SEA': 'SEAWATER', 
    'SED': 'SEDIMENT'
}

# %% ../../nbs/handlers/helcom.ipynb 14
def read_csv(file_name, dir=src_dir):
    file_path = f'{dir}/{file_name}'
    return pd.read_csv(file_path)

# %% ../../nbs/handlers/helcom.ipynb 15
def load_data(src_url: str, 
              smp_types: dict = default_smp_types, 
              use_cache: bool = False,
              save_to_cache: bool = False,
              verbose: bool = False) -> Dict[str, pd.DataFrame]:
    "Load HELCOM data and return the data in a dictionary of dataframes with the dictionary key as the sample type."

    
    def load_and_merge(file_prefix: str) -> pd.DataFrame:
        
        if use_cache:
            dir=cache_path()
        else:
            dir = src_url
            
        file_smp_path = f'{dir}/{file_prefix}01.csv'
        file_meas_path = f'{dir}/{file_prefix}02.csv'

        if use_cache:
            if not Path(file_smp_path).exists():
                print(f'{file_smp_path} not found.')            
            if not Path(file_meas_path).exists():
                print(f'{file_meas_path} not found.')
        
        if verbose:
            start_time = time.time()
        df_meas = read_csv(f'{file_prefix}02.csv', dir)
        df_smp = read_csv(f'{file_prefix}01.csv', dir)
        
        df_meas.columns = df_meas.columns.str.lower()
        df_smp.columns = df_smp.columns.str.lower()
        
        merged_df = pd.merge(df_meas, df_smp, on='key', how='left')
        
        if verbose:
            print(f"Downloaded data for {file_prefix}01.csv and {file_prefix}02.csv in {time.time() - start_time:.2f} seconds.")
            
        if save_to_cache:
            dir = cache_path()
            df_smp.to_csv(f'{dir}/{file_prefix}01.csv', index=False)
            df_meas.to_csv(f'{dir}/{file_prefix}02.csv', index=False)
            if verbose:
                print(f"Saved downloaded data to cache at {dir}/{file_prefix}01.csv and {dir}/{file_prefix}02.csv")

        return merged_df
    return {smp_type: load_and_merge(file_prefix) for file_prefix, smp_type in smp_types.items()}

# %% ../../nbs/handlers/helcom.ipynb 36
fixes_nuclide_names = {
    'cs134137': 'cs134_137_tot',
    'cm243244': 'cm243_244_tot',
    'pu239240': 'pu239_240_tot',
    'pu238240': 'pu238_240_tot',
    'cs143': 'cs137',
    'cs145': 'cs137',
    'cs142': 'cs137',
    'cs141': 'cs137',
    'cs144': 'cs137',
    'k-40': 'k40',
    'cs140': 'cs137',
    'cs146': 'cs137',
    'cs139': 'cs137',
    'cs138': 'cs137'
    }

# %% ../../nbs/handlers/helcom.ipynb 40
# Create a lookup table for nuclide names
lut_nuclides = lambda df: Remapper(provider_lut_df=df,
                                   maris_lut_fn=nuc_lut_path,
                                   maris_col_id='nuclide_id',
                                   maris_col_name='nc_name',
                                   provider_col_to_match='value',
                                   provider_col_key='value',
                                   fname_cache='nuclides_helcom.pkl').generate_lookup_table(fixes=fixes_nuclide_names, 
                                                                                            as_df=False, overwrite=False)

# %% ../../nbs/handlers/helcom.ipynb 42
class RemapNuclideNameCB(Callback):
    "Remap data provider nuclide names to standardized MARIS nuclide names."
    def __init__(self, 
                 fn_lut: Callable, # Function that returns the lookup table dictionary
                 col_name: str # Column name to remap
                ):
        fc.store_attr()

    def __call__(self, tfm: Transformer):
        df_uniques = get_unique_across_dfs(tfm.dfs, col_name=self.col_name, as_df=True)
        #lut = {k: v.matched_maris_name for k, v in self.fn_lut(df_uniques).items()}    
        lut = {k: v.matched_id for k, v in self.fn_lut(df_uniques).items()}    
        for k in tfm.dfs.keys():
            tfm.dfs[k]['NUCLIDE'] = tfm.dfs[k][self.col_name].replace(lut)

# %% ../../nbs/handlers/helcom.ipynb 48
class ParseTimeCB(Callback):
    "Standardize time format across all dataframes."
    def __call__(self, tfm: Transformer):
        for df in tfm.dfs.values():
            self._process_dates(df)

    def _process_dates(self, df: pd.DataFrame) -> None:
        "Process and correct date and time information in the DataFrame."
        df['TIME'] = self._parse_date(df)
        self._handle_missing_dates(df)
        self._fill_missing_time(df)

    def _parse_date(self, df: pd.DataFrame) -> pd.Series:
        "Parse the DATE column if present."
        return pd.to_datetime(df['date'], format='%m/%d/%y %H:%M:%S', errors='coerce')

    def _handle_missing_dates(self, df: pd.DataFrame):
        "Handle cases where DAY or MONTH is 0 or missing."
        df.loc[df["day"] == 0, "day"] = 1
        df.loc[df["month"] == 0, "month"] = 1
        
        missing_day_month = (df["day"].isna()) & (df["month"].isna()) & (df["year"].notna())
        df.loc[missing_day_month, ["day", "month"]] = 1

    def _fill_missing_time(self, df: pd.DataFrame) -> None:
        "Fill missing time values using year, month, and day columns."
        missing_time = df['TIME'].isna()
        df.loc[missing_time, 'TIME'] = pd.to_datetime(
            df.loc[missing_time, ['year', 'month', 'day']], 
            format='%Y%m%d', 
            errors='coerce'
        )

# %% ../../nbs/handlers/helcom.ipynb 59
coi_sediment = {
    'kg_type': {
        'VALUE': 'value_bq/kg',
        'UNC': 'error%_kg',
        'DL': '< value_bq/kg',
        'UNIT': 3,  # Unit ID for Bq/kg
    },
    'm2_type': {
        'VALUE': 'value_bq/m²',
        'UNC': 'error%_m²',
        'DL': '< value_bq/m²',
        'UNIT': 2,  # Unit ID for Bq/m²
    }
}

# %% ../../nbs/handlers/helcom.ipynb 61
class SplitSedimentValuesCB(Callback):
    "Separate sediment entries into distinct rows for Bq/kg and Bq/m² measurements."
    def __init__(self, 
                 coi: Dict[str, Dict[str, Any]] # Columns of interest with value, uncertainty, DL columns and units
                ):
        fc.store_attr()
        
    def __call__(self, tfm: Transformer):
        if 'SEDIMENT' not in tfm.dfs:
            return
            
        df = tfm.dfs['SEDIMENT']
        dfs_to_concat = []
        
        # For each measurement type (kg and m2)
        for measure_type, cols in self.coi.items():
            # If any of value/uncertainty/DL exists, keep the row
            has_data = (
                df[cols['VALUE']].notna() | 
                df[cols['UNC']].notna() | 
                df[cols['DL']].notna()
            )
            
            if has_data.any():
                df_measure = df[has_data].copy()
                
                # Copy columns to standardized names
                df_measure['_VALUE'] = df_measure[cols['VALUE']]
                df_measure['_UNC'] = df_measure[cols['UNC']]
                df_measure['_DL'] = df_measure[cols['DL']]
                df_measure['_UNIT'] = cols['UNIT']
                
                dfs_to_concat.append(df_measure)
        
        # Combine all measurement type dataframes
        if dfs_to_concat:
            tfm.dfs['SEDIMENT'] = pd.concat(dfs_to_concat, ignore_index=True)

# %% ../../nbs/handlers/helcom.ipynb 66
coi_val = {'SEAWATER' : {'VALUE': 'value_bq/m³'},
           'BIOTA':  {'VALUE': 'value_bq/kg'},
           'SEDIMENT': {'VALUE': '_VALUE'}}

# %% ../../nbs/handlers/helcom.ipynb 67
class SanitizeValueCB(Callback):
    "Sanitize measurement values by removing blanks and standardizing to use the `VALUE` column."
    def __init__(self, 
                 coi: Dict[str, Dict[str, str]], # Columns of interest. Format: {group_name: {'val': 'column_name'}}
                 verbose: bool=False
                 ): 
        fc.store_attr()

    def __call__(self, tfm: Transformer):
        tfm.dfs_dropped = {}
        for grp, df in tfm.dfs.items():
            value_col = self.coi[grp]['VALUE']
            # Count NaN values before dropping
            initial_nan_count = df[value_col].isna().sum()
                        
            # define a dataframe with the rows that were dropped    
            tfm.dfs_dropped[grp] = df[df[value_col].isna()]
            
            df.dropna(subset=[value_col], inplace=True)

            # Count NaN values after dropping
            final_nan_count = df[value_col].isna().sum()
            dropped_nan_count = initial_nan_count - final_nan_count
            
            # Print the number of dropped NaN values
            if dropped_nan_count > 0 and self.verbose:
                print(f"Warning: {dropped_nan_count} missing value(s) in {value_col} for group {grp}.")
            
            
            df['VALUE'] = df[value_col]

# %% ../../nbs/handlers/helcom.ipynb 71
def unc_rel2stan(
    df: pd.DataFrame, # DataFrame containing measurement and uncertainty columns
    meas_col: str, # Name of the column with measurement values
    unc_col: str # Name of the column with relative uncertainty values (percentages)
) -> pd.Series: # Series with calculated absolute uncertainties
    "Convert relative uncertainty to absolute uncertainty."
    return df.apply(lambda row: row[unc_col] * row[meas_col] / 100, axis=1)

# %% ../../nbs/handlers/helcom.ipynb 73
# Columns of interest
coi_units_unc = [('SEAWATER', 'value_bq/m³', 'error%_m³'),
                 ('BIOTA', 'value_bq/kg', 'error%'),
                 ('SEDIMENT', '_VALUE', '_UNC')]


# %% ../../nbs/handlers/helcom.ipynb 75
class NormalizeUncCB(Callback):
    "Convert from relative error to standard uncertainty."
    def __init__(self, 
                 fn_convert_unc: Callable=unc_rel2stan, # Function converting relative uncertainty to absolute uncertainty
                 coi: List[Tuple[str, str, str]]=coi_units_unc # List of columns of interest
                ):
        fc.store_attr()
    
    def __call__(self, tfm: Transformer):
        for grp, val, unc in self.coi:
            if grp in tfm.dfs:
                df = tfm.dfs[grp]
                df['UNC'] = self.fn_convert_unc(df, val, unc)

# %% ../../nbs/handlers/helcom.ipynb 87
lut_units = {
    'SEAWATER': 1,  # 'Bq/m3'
    'SEDIMENT': '_UNIT', # account for in SplitSedimentValuesCB.
    'BIOTA': {
        'D': 4,  # 'Bq/kgd'
        'W': 5,  # 'Bq/kgw'
        'F': 5   # 'Bq/kgw' (assumed to be 'Fresh', so set to wet)
    }
}

# %% ../../nbs/handlers/helcom.ipynb 89
class RemapUnitCB(Callback):
    "Set the `unit` id column in the DataFrames based on a lookup table."
    def __init__(self, 
                 lut_units: dict=lut_units # Dictionary containing renaming rules for different unit categories
                ):
        fc.store_attr()

    def __call__(self, tfm: Transformer):
        for grp in tfm.dfs.keys():
            if grp == 'SEAWATER':
                tfm.dfs[grp]['UNIT'] = self.lut_units[grp]
            elif grp == 'BIOTA':
                tfm.dfs[grp]['UNIT'] = tfm.dfs[grp]['basis'].apply(lambda x: lut_units[grp].get(x, 0))
            elif grp == 'SEDIMENT':
                tfm.dfs[grp]['UNIT'] = tfm.dfs[grp]['_UNIT']

# %% ../../nbs/handlers/helcom.ipynb 95
lut_dl = lambda: pd.read_excel(detection_limit_lut_path(), usecols=['name','id']).set_index('name').to_dict()['id']

# %% ../../nbs/handlers/helcom.ipynb 97
coi_dl = {'SEAWATER' : {'VALUE' : 'value_bq/m³',
                       'UNC' : 'error%_m³',
                       'DL' : '< value_bq/m³'},
          'BIOTA':  {'VALUE' : 'value_bq/kg',
                     'UNC' : 'error%',
                     'DL' : '< value_bq/kg'},
          'SEDIMENT': {
              'VALUE' : '_VALUE',
              'UNC' : '_UNC',
              'DL' : '_DL'}}

# %% ../../nbs/handlers/helcom.ipynb 99
class RemapDetectionLimitCB(Callback):
    "Remap value type to MARIS format."
    
    def __init__(self, 
                 coi: dict,  # Configuration options for column names
                 fn_lut: Callable  # Function that returns a lookup table
                ):
        fc.store_attr()

    def __call__(self, tfm: Transformer):
        lut = self.fn_lut()
        for grp in tfm.dfs:
            df = tfm.dfs[grp]
            self._update_detection_limit(df, grp, lut)

    def _update_detection_limit(self, df: pd.DataFrame, grp: str, lut: dict) -> None:
        if grp not in coi_dl:
            raise ValueError(f"Group '{grp}' not found in coi_dl configuration.")
        
        value_col, uncertainty_col, detection_col = self._get_column_names(grp)
        df['DL'] = df[detection_col]
        self._set_detection_limits(df, value_col, uncertainty_col, lut)

    def _get_column_names(self, grp: str) -> tuple:
        "Retrieve column names for the group."
        return coi_dl[grp]['VALUE'], coi_dl[grp]['UNC'], coi_dl[grp]['DL']

    def _set_detection_limits(self, df: pd.DataFrame, value_col: str, uncertainty_col: str, lut: dict) -> None:
        self._apply_equal_condition(df, value_col, uncertainty_col, lut)
        self._set_unmatched_to_not_available(df, lut)
        self._map_detection_limits(df, lut)

    def _apply_equal_condition(self, df: pd.DataFrame, value_col: str, uncertainty_col: str, lut: dict) -> None:
        "Apply condition to set detection limits to '='."
        # Set detection limits to '=' if there is a value and uncertainty and 'DL' value is not 
        # in the lookup table.
        condition_eq = (df[value_col].notna() & df[uncertainty_col].notna() & ~df['DL'].isin(lut.keys()))
        df.loc[condition_eq, 'DL'] = '='

    def _set_unmatched_to_not_available(self, df: pd.DataFrame, lut: dict) -> None:
        "Set unmatched detection limits to 'Not Available'."
        # Set detection limits to 'Not Available' if 'DL' value is not in the lookup table.
        df.loc[~df['DL'].isin(lut.keys()), 'DL'] = 'Not Available'

    def _map_detection_limits(self, df: pd.DataFrame, lut: dict) -> None:
        "Map detection limits using the lookup table."
        df['DL'] = df['DL'].map(lut)

# %% ../../nbs/handlers/helcom.ipynb 111
fixes_biota_species = {
    'STIZOSTEDION LUCIOPERCA': 'Sander luciopercas',
    'LAMINARIA SACCHARINA': 'Saccharina latissima',
    'CARDIUM EDULE': 'Cerastoderma edule',
    'CHARA BALTICA': NA,
    'PSETTA MAXIMA': 'Scophthalmus maximus'
    }

# %% ../../nbs/handlers/helcom.ipynb 115
lut_biota = lambda: Remapper(provider_lut_df=read_csv('RUBIN_NAME.csv'),
                             maris_lut_fn=species_lut_path,
                             maris_col_id='species_id',
                             maris_col_name='species',
                             provider_col_to_match='SCIENTIFIC NAME',
                             provider_col_key='RUBIN',
                             fname_cache='species_helcom.pkl'
                             ).generate_lookup_table(fixes=fixes_biota_species, as_df=False, overwrite=False)

# %% ../../nbs/handlers/helcom.ipynb 121
fixes_biota_tissues = {
    'WHOLE FISH WITHOUT HEAD AND ENTRAILS': 'Whole animal eviscerated without head',
    'WHOLE FISH WITHOUT ENTRAILS': 'Whole animal eviscerated',
    'SKIN/EPIDERMIS': 'Skin',
    'ENTRAILS': 'Viscera'
    }

# %% ../../nbs/handlers/helcom.ipynb 124
lut_tissues = lambda: Remapper(provider_lut_df=read_csv('TISSUE.csv'),
                               maris_lut_fn=bodyparts_lut_path,
                               maris_col_id='bodypar_id',
                               maris_col_name='bodypar',
                               provider_col_to_match='TISSUE_DESCRIPTION',
                               provider_col_key='TISSUE',
                               fname_cache='tissues_helcom.pkl'
                               ).generate_lookup_table(fixes=fixes_biota_tissues, as_df=False, overwrite=False)

# %% ../../nbs/handlers/helcom.ipynb 128
lut_biogroup_from_biota = lambda: get_lut(src_dir=species_lut_path().parent, fname=species_lut_path().name, 
                               key='species_id', value='biogroup_id')

# %% ../../nbs/handlers/helcom.ipynb 138
fixes_sediments = {
    'NO DATA': NA
}

# %% ../../nbs/handlers/helcom.ipynb 142
sed_replace_lut = {
    56: -99,
    73: -99,
    NA: -99
}

# %% ../../nbs/handlers/helcom.ipynb 143
class RemapSedimentCB(Callback):
    "Lookup sediment id using lookup table."
    def __init__(self, 
                 fn_lut: Callable,  # Function that returns the lookup table dictionary
                 sed_grp_name: str = 'SEDIMENT',  # The name of the sediment group
                 sed_col_name: str = 'sedi',  # The name of the sediment column
                 replace_lut: dict = None  # Dictionary for replacing SEDI values
                 ):
        fc.store_attr()

    def __call__(self, tfm: Transformer):
        "Remap sediment types using lookup table."
        df = tfm.dfs[self.sed_grp_name]
        self._fix_inconsistent_values(df)
        self._map_sediment_types(df)

    def _fix_inconsistent_values(self, df: pd.DataFrame) -> None:
        "Fix inconsistent values using the replace lookup table."
        if self.replace_lut:
            df[self.sed_col_name] = df[self.sed_col_name].replace(self.replace_lut)
            if NA in self.replace_lut:
                df[self.sed_col_name] = df[self.sed_col_name].fillna(self.replace_lut[NA])

    def _map_sediment_types(self, df: pd.DataFrame) -> None:
        "Map sediment types using the lookup table."
        lut = self.fn_lut()
        df['SED_TYPE'] = df[self.sed_col_name].map(
            lambda x: lut.get(x, Match(0, None, None, None)).matched_id
        )

# %% ../../nbs/handlers/helcom.ipynb 144
lut_sediments = lambda: Remapper(provider_lut_df=read_csv('SEDIMENT_TYPE.csv'),
                                 maris_lut_fn=sediments_lut_path,
                                 maris_col_id='sedtype_id',
                                 maris_col_name='sedtype',
                                 provider_col_to_match='SEDIMENT TYPE',
                                 provider_col_key='SEDI',
                                 fname_cache='sediments_helcom.pkl'
                                 ).generate_lookup_table(fixes=fixes_sediments, as_df=False, overwrite=False)

# %% ../../nbs/handlers/helcom.ipynb 153
lut_filtered = {
    'N': 2, # No
    'n': 2, # No
    'F': 1 # Yes
}

# %% ../../nbs/handlers/helcom.ipynb 155
class RemapFiltCB(Callback):
    "Lookup filt value in dataframe using the lookup table."
    def __init__(self,
                 lut_filtered: dict=lut_filtered, # Dictionary mapping filt codes to their corresponding names
                ):
        fc.store_attr()

    def __call__(self, tfm):
        for df in tfm.dfs.values():
            if 'filt' in df.columns:
                df['FILT'] = df['filt'].map(lambda x: self.lut_filtered.get(x, 0))

# %% ../../nbs/handlers/helcom.ipynb 161
class AddSampleIDCB(Callback):
    "Generate a SMP_ID from the KEY values in the HELCOM dataset. Each KEY is mapped to a unique integer, with the mapping stored in an enumeration (i.e., smp_id)."
    def __call__(self, tfm: Transformer):
        for grp, df in tfm.dfs.items():
            # Generate and store the SMP_ID map
            smp_id_map = self._generate_sample_id_map(df)
            tfm.custom_maps[grp]['SMP_ID'] = smp_id_map
            # Create SMP_ID column in the DataFrame
            self._create_smp_id(df, smp_id_map)
        
    def _generate_sample_id_map(self, df: pd.DataFrame) -> dict:
        """Enumerate unique 'key' values and map them to integers."""
        return {key: idx for idx, key in enumerate(df['key'].unique())}

    def _create_smp_id(self, df: pd.DataFrame, smp_id_map: dict) -> None:
        """Map 'key' values to 'SMP_ID' using the provided enum."""
        df['SMP_ID'] = df['key'].map(smp_id_map)

# %% ../../nbs/handlers/helcom.ipynb 166
class AddDepthCB(Callback):
    "Ensure depth values are floats and add 'SMP_DEPTH' and 'TOT_DEPTH' columns."
    def __call__(self, tfm: Transformer):
        for df in tfm.dfs.values():
            if 'sdepth' in df.columns:
                df['SMP_DEPTH'] = df['sdepth'].astype(float)
            if 'tdepth' in df.columns:
                df['TOT_DEPTH'] = df['tdepth'].astype(float)

# %% ../../nbs/handlers/helcom.ipynb 172
class AddSalinityCB(Callback):
    def __init__(self, salinity_col: str = 'salin'):
        self.salinity_col = salinity_col
    "Add salinity to the SEAWATER DataFrame."
    def __call__(self, tfm: Transformer):
        for df in tfm.dfs.values():
            if self.salinity_col in df.columns:
                df['SALINITY'] = df[self.salinity_col].astype(float)

# %% ../../nbs/handlers/helcom.ipynb 178
class AddTemperatureCB(Callback):
    def __init__(self, temperature_col: str = 'ttemp'):
        self.temperature_col = temperature_col
    "Add temperature to the SEAWATER DataFrame."
    def __call__(self, tfm: Transformer ):
        for df in tfm.dfs.values():
            if self.temperature_col in df.columns:
                df['TEMPERATURE'] = df[self.temperature_col].astype(float)

# %% ../../nbs/handlers/helcom.ipynb 181
class RemapSedSliceTopBottomCB(Callback):
    "Remap Sediment slice top and bottom to MARIS format."
    def __call__(self, tfm: Transformer):
        "Iterate through all DataFrames in the transformer object and remap sediment slice top and bottom."
        tfm.dfs['SEDIMENT']['TOP'] = tfm.dfs['SEDIMENT']['uppsli']
        tfm.dfs['SEDIMENT']['BOTTOM'] = tfm.dfs['SEDIMENT']['lowsli']

# %% ../../nbs/handlers/helcom.ipynb 205
class LookupDryWetPercentWeightCB(Callback):
    "Lookup dry-wet ratio and format for MARIS."
    def __call__(self, tfm: Transformer):
        "Iterate through all DataFrames in the transformer object and apply the dry-wet ratio lookup."
        for grp in tfm.dfs.keys():
            if 'dw%' in tfm.dfs[grp].columns:
                self._apply_dry_wet_ratio(tfm.dfs[grp])
            if 'weight' in tfm.dfs[grp].columns and 'basis' in tfm.dfs[grp].columns:
                self._correct_basis(tfm.dfs[grp])
                self._apply_weight(tfm.dfs[grp])

    def _apply_dry_wet_ratio(self, df: pd.DataFrame) -> None:
        "Apply dry-wet ratio conversion and formatting to the given DataFrame."
        df['PERCENTWT'] = df['dw%'] / 100  # Convert percentage to fraction
        df.loc[df['PERCENTWT'] == 0, 'PERCENTWT'] = np.NaN  # Convert 0% to NaN

    def _correct_basis(self, df: pd.DataFrame) -> None:
        "Correct BASIS values. Assuming F = Fresh weight, so F = W"
        df.loc[df['basis'] == 'F', 'basis'] = 'W'

    def _apply_weight(self, df: pd.DataFrame) -> None:
        "Apply weight conversion and formatting to the given DataFrame."
        dry_condition = df['basis'] == 'D'
        wet_condition = df['basis'] == 'W'
        
        df.loc[dry_condition, 'DRYWT'] = df['weight']
        df.loc[dry_condition & df['PERCENTWT'].notna(), 'WETWT'] = df['weight'] / df['PERCENTWT']
        
        df.loc[wet_condition, 'WETWT'] = df['weight']
        df.loc[wet_condition & df['PERCENTWT'].notna(), 'DRYWT'] = df['weight'] * df['PERCENTWT']

# %% ../../nbs/handlers/helcom.ipynb 214
class ParseCoordinates(Callback):
    "Get geographical coordinates from columns expressed in degrees decimal format or from columns in degrees/minutes decimal format where degrees decimal format is missing or zero."
    def __init__(self, 
                 fn_convert_cor: Callable # Function that converts coordinates from degree-minute to decimal degree format
                 ):
        self.fn_convert_cor = fn_convert_cor

    def __call__(self, tfm:Transformer):
        for df in tfm.dfs.values():
            self._format_coordinates(df)

    def _format_coordinates(self, df:pd.DataFrame) -> None:
        coord_cols = self._get_coord_columns(df.columns)
        
        
        for coord in ['lat', 'lon']:
            decimal_col, minute_col = coord_cols[f'{coord}_d'], coord_cols[f'{coord}_m']
            # Attempt to convert columns to numeric, coercing errors to NaN.
            df[decimal_col] = pd.to_numeric(df[decimal_col], errors='coerce')
            df[minute_col] = pd.to_numeric(df[minute_col], errors='coerce')
            condition = df[decimal_col].isna() | (df[decimal_col] == 0)
            df[coord.upper()] = np.where(condition,
                                 df[minute_col].apply(self._safe_convert),
                                 df[decimal_col])
        
        df.dropna(subset=['LAT', 'LON'], inplace=True)

    def _get_coord_columns(self, columns) -> dict:
        return {
            'lon_d': self._find_coord_column(columns, 'lon', 'dddddd'),
            'lat_d': self._find_coord_column(columns, 'lat', 'dddddd'),
            'lon_m': self._find_coord_column(columns, 'lon', 'ddmmmm'),
            'lat_m': self._find_coord_column(columns, 'lat', 'ddmmmm')
        }

    def _find_coord_column(self, columns, coord_type, coord_format) -> str:
        pattern = re.compile(f'{coord_type}.*{coord_format}', re.IGNORECASE)
        matching_columns = [col for col in columns if pattern.search(col)]
        return matching_columns[0] if matching_columns else None

    def _safe_convert(self, value) -> str:
        if pd.isna(value):
            return value
        try:
            return self.fn_convert_cor(value)
        except Exception as e:
            print(f"Error converting value {value}: {e}")
            return value

# %% ../../nbs/handlers/helcom.ipynb 227
kw = ['oceanography', 'Earth Science > Oceans > Ocean Chemistry> Radionuclides',
      'Earth Science > Human Dimensions > Environmental Impacts > Nuclear Radiation Exposure',
      'Earth Science > Oceans > Ocean Chemistry > Ocean Tracers, Earth Science > Oceans > Marine Sediments',
      'Earth Science > Oceans > Ocean Chemistry, Earth Science > Oceans > Sea Ice > Isotopes',
      'Earth Science > Oceans > Water Quality > Ocean Contaminants',
      'Earth Science > Biological Classification > Animals/Vertebrates > Fish',
      'Earth Science > Biosphere > Ecosystems > Marine Ecosystems',
      'Earth Science > Biological Classification > Animals/Invertebrates > Mollusks',
      'Earth Science > Biological Classification > Animals/Invertebrates > Arthropods > Crustaceans',
      'Earth Science > Biological Classification > Plants > Macroalgae (Seaweeds)']

# %% ../../nbs/handlers/helcom.ipynb 228
def get_attrs(
    tfm: Transformer, # Transformer object
    zotero_key: str, # Zotero dataset record key
    kw: list = kw # List of keywords
    ) -> dict: # Global attributes
    "Retrieve all global attributes."
    return GlobAttrsFeeder(tfm.dfs, cbs=[
        BboxCB(),
        DepthRangeCB(),
        TimeRangeCB(),
        ZoteroCB(zotero_key, cfg=cfg()),
        KeyValuePairCB('keywords', ', '.join(kw)),
        KeyValuePairCB('publisher_postprocess_logs', ', '.join(tfm.logs))
        ])()

# %% ../../nbs/handlers/helcom.ipynb 231
def encode(
    src_dir: str, # Input file name
    fname_out_nc: str, # Output file name
    **kwargs # Additional arguments
    ) -> None:
    "Encode data to NetCDF."
    dfs = load_data(src_dir)
    tfm = Transformer(dfs, cbs=[
                            LowerStripNameCB(col_src='nuclide', col_dst='NUCLIDE'),
                            RemapNuclideNameCB(lut_nuclides, col_name='NUCLIDE'),
                            ParseTimeCB(),
                            EncodeTimeCB(),
                            SplitSedimentValuesCB(coi_sediment),
                            SanitizeValueCB(coi_val),       
                            NormalizeUncCB(),
                            RemapUnitCB(),
                            RemapDetectionLimitCB(coi_dl, lut_dl),                           
                            RemapCB(fn_lut=lut_biota, col_remap='SPECIES', col_src='rubin', dest_grps='BIOTA'),
                            RemapCB(fn_lut=lut_tissues, col_remap='BODY_PART', col_src='tissue', dest_grps='BIOTA'),
                            RemapCB(fn_lut=lut_biogroup_from_biota, col_remap='BIO_GROUP', col_src='SPECIES', dest_grps='BIOTA'),
                            RemapSedimentCB(fn_lut=lut_sediments, replace_lut=sed_replace_lut),
                            RemapFiltCB(lut_filtered),
                            AddSampleIDCB(),
                            AddDepthCB(),
                            AddSalinityCB(),
                            AddTemperatureCB(),
                            RemapSedSliceTopBottomCB(),
                            LookupDryWetPercentWeightCB(),
                            ParseCoordinates(ddmm_to_dd),
                            SanitizeLonLatCB(),
                            ])
    tfm()
    encoder = NetCDFEncoder(tfm.dfs, 
                            dest_fname=fname_out_nc, 
                            global_attrs=get_attrs(tfm, zotero_key=zotero_key, kw=kw),
                            # custom_maps=tfm.custom_maps,
                            verbose=kwargs.get('verbose', False),
                           )
    encoder.encode()
