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
}
}