PhenologyFileLoader.java

/*
 * This file is part of Indicators.
 *
 * Indicators is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Indicators is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
 */
package fr.inrae.agroclim.indicators.model.data.phenology;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import fr.inrae.agroclim.indicators.model.TimeScale;
import fr.inrae.agroclim.indicators.model.data.DataLoadingListener;
import fr.inrae.agroclim.indicators.model.data.FileLoader;
import fr.inrae.agroclim.indicators.model.data.Resource;
import fr.inrae.agroclim.indicators.model.data.ResourcesLoader;
import fr.inrae.agroclim.indicators.model.data.Variable;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlTransient;
import jakarta.xml.bind.annotation.XmlType;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import tools.jackson.databind.MappingIterator;
import tools.jackson.dataformat.csv.CsvMapper;
import tools.jackson.dataformat.csv.CsvReadFeature;
import tools.jackson.dataformat.csv.CsvSchema;

/**
 * Load Phenology data from file.
 *
 * Last changed : $Date$
 *
 * @author $Author$
 * @version $Revision$
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = {"separator", "headers"})
@EqualsAndHashCode(
        callSuper = true,
        of = {"headers", "separator"}
        )
@Log4j2
public final class PhenologyFileLoader extends FileLoader implements ResourcesLoader<List<AnnualStageData>> {

    /**
     * UUID for Serializable.
     */
    private static final long serialVersionUID = -8613079345988870000L;
    /**
     * Fixed name for CSV header.
     */
    public static final String YEAR_COLUMN = "year";

    /**
     * Headers of CSV file.
     */
    @Getter
    @XmlElement(name = "header")
    private String[] headers;

    /**
     * CSV separator.
     */
    @Getter
    @Setter
    @XmlElement
    private String separator = Resource.DEFAULT_SEP;

    /**
     * The column number of "year" column.
     */
    @XmlTransient
    private int yearHeader = 0;

    /**
     * Constructor.
     */
    public PhenologyFileLoader() {
        setDataFile(DataLoadingListener.DataFile.PHENOLOGICAL);
    }

    /**
     * Constructor.
     *
     * @param csvFile
     *            CSV file
     * @param csvHeaders
     *            CSV headers
     * @param csvSeparator
     *            CSV separator
     */
    public PhenologyFileLoader(final String csvFile, final String[] csvHeaders,
            final String csvSeparator) {
        this();
        setPath(csvFile);
        setHeaders(csvHeaders);
        this.separator = csvSeparator;
    }

    @Override
    public PhenologyFileLoader clone() {
        final PhenologyFileLoader clone = new PhenologyFileLoader();
        clone.setPath(getPath());
        clone.setHeaders(headers);
        clone.separator = separator;
        return clone;
    }

    /**
     * @return The absolute pathname string denoting the same file or directory
     *         as this abstract pathname.
     */
    public String getAbsolutePath() {
        if (getFile() == null) {
            throw new RuntimeException("PhenologyFileLoader.file is null!");
        }
        return getFile().getAbsolutePath();
    }

    @Override
    public Map<String, String> getConfigurationErrors() {
        final Map<String, String> errors = new HashMap<>();
        if (getFile() == null) {
            errors.put("phenology.file", "error.evaluation.phenology.file.missing");
        } else if (!getFile().exists()) {
            errors.put("phenology.file", "error.evaluation.phenology.file.doesnotexist");
        } else if (getFile().length() == 0) {
            errors.put("phenology.file", "error.evaluation.phenology.file.empty");
        }
        if (separator == null) {
            errors.put("phenology.separator", "error.evaluation.phenology.separator.missing");
        } else if (separator.isEmpty()) {
            errors.put("phenology.separator", "error.evaluation.phenology.separator.empty");
        }
        if (headers == null) {
            errors.put("phenology.header", "error.evaluation.phenology.header.missing");
        }
        if (errors.isEmpty()) {
            return null;
        }
        return errors;
    }

    @Override
    public Collection<String> getMissingVariables() {
        throw new RuntimeException("Not implemented for phenology!");
    }

    @Override
    public Set<Variable> getVariables() {
        return new HashSet<>();
    }

    @Override
    public List<AnnualStageData> load() {
        final List<AnnualStageData> data = new ArrayList<>();
        final CsvSchema schema = CsvSchema.emptySchema()
                .withSkipFirstDataRow(true)//
                .withColumnSeparator(separator.charAt(0));
        final CsvMapper mapper = CsvMapper.builder() //
                .configure(CsvReadFeature.WRAP_AS_ARRAY, true) //
                .build();
        final File csvFile = getFile();
        try (MappingIterator<Integer[]> it = mapper.readerFor(Integer[].class)
                    .with(schema).readValues(csvFile)) {
            while (it.hasNext()) {
                final Integer[] row = it.next();
                final int lineNumber = it.currentLocation().getLineNr();
                final Integer year = row[yearHeader];
                final AnnualStageData annualStageData = new AnnualStageData();
                annualStageData.setYear(year);
                for (int i = 0; i < row.length; i++) {
                    if (i != yearHeader && i < headers.length) {
                        annualStageData.add(headers[i], row[i]);
                    }
                }
                annualStageData.check(lineNumber, csvFile.getName());
                fireDataLoadingAddEvent(annualStageData);
                data.add(annualStageData);
            }
        }
        return data;
    }

    /**
     * @param csvHeaders CSV header
     */
    public void setHeaders(final String[] csvHeaders) {
        this.headers = csvHeaders;
        for (int i = 0; i != headers.length - 1; i++) {
            if (headers[i].equals("year")) {
                this.yearHeader = i;
            }
        }
    }

    @Override
    public void setTimeScale(final TimeScale timeScale) {
        // do nothing
    }
}