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