View Javadoc
1   /**
2    * This file is part of Indicators.
3    *
4    * Indicators is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * Indicators is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
16   */
17  package fr.inrae.agroclim.indicators.model.data.phenology;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import org.apache.logging.log4j.Level;
30  
31  import com.fasterxml.jackson.databind.MappingIterator;
32  import com.fasterxml.jackson.dataformat.csv.CsvMapper;
33  import com.fasterxml.jackson.dataformat.csv.CsvParser;
34  import com.fasterxml.jackson.dataformat.csv.CsvSchema;
35  
36  import fr.inrae.agroclim.indicators.model.TimeScale;
37  import fr.inrae.agroclim.indicators.model.data.DataLoadingListener;
38  import fr.inrae.agroclim.indicators.model.data.FileLoader;
39  import fr.inrae.agroclim.indicators.model.data.Resource;
40  import fr.inrae.agroclim.indicators.model.data.ResourcesLoader;
41  import fr.inrae.agroclim.indicators.model.data.Variable;
42  import jakarta.xml.bind.annotation.XmlAccessType;
43  import jakarta.xml.bind.annotation.XmlAccessorType;
44  import jakarta.xml.bind.annotation.XmlElement;
45  import jakarta.xml.bind.annotation.XmlTransient;
46  import jakarta.xml.bind.annotation.XmlType;
47  import lombok.EqualsAndHashCode;
48  import lombok.Getter;
49  import lombok.Setter;
50  import lombok.extern.log4j.Log4j2;
51  
52  /**
53   * Load Phenology data from file.
54   *
55   * Last changed : $Date$
56   *
57   * @author $Author$
58   * @version $Revision$
59   */
60  @XmlAccessorType(XmlAccessType.FIELD)
61  @XmlType(propOrder = {"separator", "headers"})
62  @EqualsAndHashCode(
63          callSuper = true,
64          of = {"headers", "separator"}
65          )
66  @Log4j2
67  public final class PhenologyFileLoader extends FileLoader implements ResourcesLoader<List<AnnualStageData>> {
68  
69      /**
70       * UUID for Serializable.
71       */
72      private static final long serialVersionUID = -8613079345988870000L;
73      /**
74       * Fixed name for CSV header.
75       */
76      public static final String YEAR_COLUMN = "year";
77  
78      /**
79       * Headers of CSV file.
80       */
81      @Getter
82      @XmlElement(name = "header")
83      private String[] headers;
84  
85      /**
86       * CSV separator.
87       */
88      @Getter
89      @Setter
90      @XmlElement
91      private String separator = Resource.DEFAULT_SEP;
92  
93      /**
94       * The column number of "year" column.
95       */
96      @XmlTransient
97      private int yearHeader = 0;
98  
99      /**
100      * Constructor.
101      */
102     public PhenologyFileLoader() {
103         setDataFile(DataLoadingListener.DataFile.PHENOLOGICAL);
104     }
105 
106     /**
107      * Constructor.
108      *
109      * @param csvFile
110      *            CSV file
111      * @param csvHeaders
112      *            CSV headers
113      * @param csvSeparator
114      *            CSV separator
115      */
116     public PhenologyFileLoader(final String csvFile, final String[] csvHeaders,
117             final String csvSeparator) {
118         this();
119         setPath(csvFile);
120         setHeaders(csvHeaders);
121         this.separator = csvSeparator;
122     }
123 
124     @Override
125     public PhenologyFileLoader clone() {
126         final PhenologyFileLoader clone = new PhenologyFileLoader();
127         clone.setPath(getPath());
128         clone.setHeaders(headers);
129         clone.separator = separator;
130         return clone;
131     }
132 
133     /**
134      * @return The absolute pathname string denoting the same file or directory
135      *         as this abstract pathname.
136      */
137     public String getAbsolutePath() {
138         if (getFile() == null) {
139             throw new RuntimeException("PhenologyFileLoader.file is null!");
140         }
141         return getFile().getAbsolutePath();
142     }
143 
144     @Override
145     public Map<String, String> getConfigurationErrors() {
146         final Map<String, String> errors = new HashMap<>();
147         if (getFile() == null) {
148             errors.put("phenology.file", "error.evaluation.phenology.file.missing");
149         } else if (!getFile().exists()) {
150             errors.put("phenology.file", "error.evaluation.phenology.file.doesnotexist");
151         } else if (getFile().length() == 0) {
152             errors.put("phenology.file", "error.evaluation.phenology.file.empty");
153         }
154         if (separator == null) {
155             errors.put("phenology.separator", "error.evaluation.phenology.separator.missing");
156         } else if (separator.isEmpty()) {
157             errors.put("phenology.separator", "error.evaluation.phenology.separator.empty");
158         }
159         if (headers == null) {
160             errors.put("phenology.header", "error.evaluation.phenology.header.missing");
161         }
162         if (errors.isEmpty()) {
163             return null;
164         }
165         return errors;
166     }
167 
168     @Override
169     public Collection<String> getMissingVariables() {
170         throw new RuntimeException("Not implemented for phenology!");
171     }
172 
173     @Override
174     public Set<Variable> getVariables() {
175         return new HashSet<>();
176     }
177 
178     @Override
179     public List<AnnualStageData> load() {
180         final List<AnnualStageData> data = new ArrayList<>();
181         try {
182             final CsvSchema schema = CsvSchema.emptySchema()
183                     .withSkipFirstDataRow(true)//
184                     .withColumnSeparator(separator.charAt(0));
185             final CsvMapper mapper = new CsvMapper();
186             // important: we need "array wrapping" (see next section) here:
187             mapper.enable(CsvParser.Feature.WRAP_AS_ARRAY);
188             final File csvFile = getFile();
189             final MappingIterator<Integer[]> it = mapper.readerFor(Integer[].class)
190                     .with(schema).readValues(csvFile);
191             while (it.hasNext()) {
192                 final Integer[] row = it.next();
193                 final int lineNumber = it.getCurrentLocation().getLineNr();
194                 final Integer year = row[yearHeader];
195                 final AnnualStageData annualStageData = new AnnualStageData();
196                 annualStageData.setYear(year);
197                 for (int i = 0; i < row.length; i++) {
198                     if (i != yearHeader && i < headers.length) {
199                         annualStageData.add(headers[i], row[i]);
200                     }
201                 }
202                 annualStageData.check(lineNumber, csvFile.getName());
203                 fireDataLoadingAddEvent(annualStageData);
204                 data.add(annualStageData);
205             }
206         } catch (final IOException e) {
207             LOGGER.catching(Level.ERROR, e);
208         }
209         return data;
210     }
211 
212     /**
213      * @param csvHeaders CSV header
214      */
215     public void setHeaders(final String[] csvHeaders) {
216         this.headers = csvHeaders;
217         for (int i = 0; i != headers.length - 1; i++) {
218             if (headers[i].equals("year")) {
219                 this.yearHeader = i;
220             }
221         }
222     }
223 
224     @Override
225     public void setTimeScale(final TimeScale timeScale) {
226         // do nothing
227     }
228 }