1   /* Copyright 2022-2026 Luc Maisonobe
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.frames;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.List;
24  import java.util.function.Supplier;
25  
26  import org.orekit.data.DataProvidersManager;
27  import org.orekit.data.DataSource;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitMessages;
30  import org.orekit.time.AbsoluteDate;
31  import org.orekit.time.DateComponents;
32  import org.orekit.time.TimeScale;
33  import org.orekit.utils.IERSConventions;
34  import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
35  import org.orekit.utils.units.Unit;
36  
37  /** Loader for EOP csv files (can be bulletin A, bulletin B, EOP C04…).
38   * <p>
39   * This class is immutable and hence thread-safe
40   * </p>
41   * @author Luc Maisonobe
42   * @since 12.0
43   */
44  class EopCsvFilesLoader extends AbstractEopLoader implements EopHistoryLoader {
45  
46      /** Separator. */
47      private static final String SEPARATOR = ";";
48  
49      /** Header for MJD. */
50      private static final String MJD = "MJD";
51  
52      /** Header for Year. */
53      private static final String YEAR = "Year";
54  
55      /** Header for Month. */
56      private static final String MONTH = "Month";
57  
58      /** Header for Day. */
59      private static final String DAY = "Day";
60  
61      /** Header for x_pole. */
62      private static final String X_POLE = "x_pole";
63  
64      /** Header for y_pole. */
65      private static final String Y_POLE = "y_pole";
66  
67      /** Header for x_rate. */
68      private static final String X_RATE = "x_rate";
69  
70      /** Header for y_rate. */
71      private static final String Y_RATE = "y_rate";
72  
73      /** Header for UT1-UTC. */
74      private static final String UT1_UTC = "UT1-UTC";
75  
76      /** Header for LOD. */
77      private static final String LOD = "LOD";
78  
79      /** Header for dPsi. */
80      private static final String DPSI = "dPsi";
81  
82      /** Header for dEpsilon. */
83      private static final String DEPSILON = "dEpsilon";
84  
85      /** Header for dX. */
86      private static final String DX = "dX";
87  
88      /** Header for dY. */
89      private static final String DY = "dY";
90  
91      /** Converter for arcseconds. */
92      private static final Unit AS = Unit.parse("as");
93  
94      /** Converter for arcseconds per day. */
95      private static final Unit AS_D = Unit.parse("as/day");
96  
97      /** Converter for seconds. */
98      private static final Unit S = Unit.parse("s");
99  
100     /** Build a loader for IERS EOP csv files.
101      * @param supportedNames regular expression for supported files names
102      * @param manager provides access to the EOP C04 files.
103      * @param utcSupplier UTC time scale.
104      */
105     EopCsvFilesLoader(final String supportedNames,
106                       final DataProvidersManager manager,
107                       final Supplier<TimeScale> utcSupplier) {
108         super(supportedNames, manager, utcSupplier);
109     }
110 
111     /** {@inheritDoc} */
112     public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
113                             final Collection<EOPEntry> history) {
114         final Parser parser = new Parser(converter, getUtc());
115         final EopParserLoader loader = new EopParserLoader(parser);
116         this.feed(loader);
117         history.addAll(loader.getEop());
118     }
119 
120     /** Internal class performing the parsing. */
121     class Parser extends AbstractEopParser {
122 
123         /** Configuration for ITRF versions. */
124         private final ItrfVersionProvider itrfVersionProvider;
125 
126         /** Column number for MJD field. */
127         private int mjdColumn;
128 
129         /** Column number for year field. */
130         private int yearColumn;
131 
132         /** Column number for month field. */
133         private int monthColumn;
134 
135         /** Column number for day field. */
136         private int dayColumn;
137 
138         /** Column number for X pole field. */
139         private int xPoleColumn;
140 
141         /** Column number for Y pole field. */
142         private int yPoleColumn;
143 
144         /** Column number for X rate pole field. */
145         private int xRatePoleColumn;
146 
147         /** Column number for Y rate pole field. */
148         private int yRatePoleColumn;
149 
150         /** Column number for UT1-UTC field. */
151         private int ut1Column;
152 
153         /** Column number for LOD field. */
154         private int lodColumn;
155 
156         /** Column number for dX field. */
157         private int dxColumn;
158 
159         /** Column number for dY field. */
160         private int dyColumn;
161 
162         /** Column number for dPsi field. */
163         private int dPsiColumn;
164 
165         /** Column number for dEpsilon field. */
166         private int dEpsilonColumn;
167 
168         /** ITRF version configuration. */
169         private ITRFVersionLoader.ITRFVersionConfiguration configuration;
170 
171         /** Simple constructor.
172          * @param converter converter to use
173          * @param utc       time scale for parsing dates.
174          */
175         Parser(final NutationCorrectionConverter converter,
176                final TimeScale utc) {
177             super(converter, null, utc);
178             this.itrfVersionProvider = new ITRFVersionLoader(ITRFVersionLoader.SUPPORTED_NAMES,
179                                                              getDataProvidersManager());
180         }
181 
182         /** {@inheritDoc} */
183         public Collection<EOPEntry> parse(final DataSource source)
184             throws IOException, OrekitException {
185 
186             final List<EOPEntry> history = new ArrayList<>();
187 
188             // set up a reader for line-oriented csv files
189             try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
190                 // reset parse info to start new file (do not clear history!)
191                 int lineNumber = 0;
192                 configuration  = null;
193 
194                 // read all file
195                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
196                     ++lineNumber;
197 
198                     final boolean parsed;
199                     if (lineNumber == 1) {
200                         parsed = parseHeaderLine(line);
201                     } else {
202                         history.add(parseDataLine(line, source.getName()));
203                         parsed = true;
204                     }
205 
206                     if (!parsed) {
207                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
208                                 lineNumber, source.getName(), line);
209                     }
210                 }
211 
212                 // check if we have read something
213                 if (lineNumber < 2) {
214                     throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, source.getName());
215                 }
216             }
217 
218             return history;
219         }
220 
221         /** Parse the header line.
222          * @param headerLine header line
223          * @return true if line was parsed correctly
224          */
225         private boolean parseHeaderLine(final String headerLine) {
226 
227             // reset columns numbers
228             mjdColumn       = -1;
229             yearColumn      = -1;
230             monthColumn     = -1;
231             dayColumn       = -1;
232             xPoleColumn     = -1;
233             yPoleColumn     = -1;
234             xRatePoleColumn = -1;
235             yRatePoleColumn = -1;
236             ut1Column       = -1;
237             lodColumn       = -1;
238             dxColumn        = -1;
239             dyColumn        = -1;
240             dPsiColumn      = -1;
241             dEpsilonColumn  = -1;
242 
243             // split header fields
244             final String[] fields = headerLine.split(SEPARATOR);
245 
246             // affect column numbers according to header fields
247             for (int column = 0; column < fields.length; ++column) {
248                 switch (fields[column]) {
249                     case MJD :
250                         mjdColumn = column;
251                         break;
252                     case YEAR :
253                         yearColumn = column;
254                         break;
255                     case MONTH :
256                         monthColumn = column;
257                         break;
258                     case DAY :
259                         dayColumn = column;
260                         break;
261                     case X_POLE :
262                         xPoleColumn = column;
263                         break;
264                     case Y_POLE :
265                         yPoleColumn = column;
266                         break;
267                     case X_RATE :
268                         xRatePoleColumn = column;
269                         break;
270                     case Y_RATE :
271                         yRatePoleColumn = column;
272                         break;
273                     case UT1_UTC :
274                         ut1Column = column;
275                         break;
276                     case LOD :
277                         lodColumn = column;
278                         break;
279                     case DX :
280                         dxColumn = column;
281                         break;
282                     case DY :
283                         dyColumn = column;
284                         break;
285                     case DPSI :
286                         dPsiColumn = column;
287                         break;
288                     case DEPSILON :
289                         dEpsilonColumn = column;
290                         break;
291                     default :
292                         // ignored column
293                 }
294             }
295 
296             // check all required files are present (we just allow pole rates to be missing)
297             return mjdColumn >= 0 && yearColumn >= 0 && monthColumn >= 0 && dayColumn >= 0 &&
298                    xPoleColumn >= 0 && yPoleColumn >= 0 && ut1Column >= 0 && lodColumn >= 0 &&
299                    (dxColumn >= 0 && dyColumn >= 0 || dPsiColumn >= 0 && dEpsilonColumn >= 0);
300 
301         }
302 
303         /** Parse a data line.
304          * @param line line to parse
305          * @param name file name (for error messages)
306          * @return parsed entry
307          */
308         private EOPEntry parseDataLine(final String line, final String name) {
309 
310             final String[] fields = line.split(SEPARATOR);
311 
312             // check date
313             final DateComponents dc = new DateComponents(Integer.parseInt(fields[yearColumn]),
314                                                          Integer.parseInt(fields[monthColumn]),
315                                                          Integer.parseInt(fields[dayColumn]));
316             final int    mjd   = Integer.parseInt(fields[mjdColumn]);
317             if (dc.getMJD() != mjd) {
318                 throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
319                                           name, dc.getYear(), dc.getMonth(), dc.getDay(), mjd);
320             }
321             final AbsoluteDate date = new AbsoluteDate(dc, getUtc());
322 
323             if (configuration == null || !configuration.isValid(mjd)) {
324                 // get a configuration for current name and date range
325                 configuration = itrfVersionProvider.getConfiguration(name, mjd);
326             }
327 
328             final double x     = parseField(fields, xPoleColumn,     AS);
329             final double y     = parseField(fields, yPoleColumn,     AS);
330             final double xRate = parseField(fields, xRatePoleColumn, AS_D);
331             final double yRate = parseField(fields, yRatePoleColumn, AS_D);
332             final double dtu1  = parseField(fields, ut1Column,       S);
333             final double lod   = parseField(fields, lodColumn,       S);
334 
335             if (dxColumn >= 0) {
336                 // non-rotatin origin paradigm
337                 final double dx = parseField(fields, dxColumn, AS);
338                 final double dy = parseField(fields, dyColumn, AS);
339                 final double[] equinox = getConverter().toEquinox(date, dx, dy);
340                 return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
341                                     equinox[0], equinox[1], dx, dy,
342                                     configuration.getVersion(), date, EopDataType.UNKNOWN);
343             } else {
344                 // equinox paradigm
345                 final double ddPsi      = parseField(fields, dPsiColumn,     AS);
346                 final double dddEpsilon = parseField(fields, dEpsilonColumn, AS);
347                 final double[] nro = getConverter().toNonRotating(date, ddPsi, dddEpsilon);
348                 return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
349                                     ddPsi, dddEpsilon, nro[0], nro[1],
350                                     configuration.getVersion(), date, EopDataType.UNKNOWN);
351             }
352 
353 
354         }
355 
356         /** Parse one field.
357          * @param fields fields array to parse
358          * @param index index in the field array (negative for ignored fields)
359          * @param unit field unit
360          * @return parsed and converted field
361          */
362         private double parseField(final String[] fields, final int index, final Unit unit) {
363             return (index < 0 || index >= fields.length || fields[index].isEmpty()) ?
364                    Double.NaN :
365                    unit.toSI(Double.parseDouble(fields[index]));
366         }
367 
368     }
369 
370 }