1   /* Copyright 2002-2026 CS GROUP
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  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import org.orekit.data.DataProvidersManager;
29  import org.orekit.data.DataSource;
30  import org.orekit.errors.OrekitException;
31  import org.orekit.errors.OrekitMessages;
32  import org.orekit.time.AbsoluteDate;
33  import org.orekit.time.DateComponents;
34  import org.orekit.time.TimeComponents;
35  import org.orekit.time.TimeScale;
36  import org.orekit.utils.Constants;
37  import org.orekit.utils.IERSConventions;
38  import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
39  
40  /** Loader for EOP C04 files.
41   * <p>EOP C04 files contain {@link EOPEntry
42   * Earth Orientation Parameters} consistent with ITRF20xx for one year periods, with various
43   * xx (05, 08, 14, 20) depending on the data source.</p>
44   * <p>The EOP C04 files retrieved from the old ftp site
45   * <a href="ftp://ftp.iers.org/products/eop/long-term/">ftp://ftp.iers.org/products/eop/long-term/</a>
46   * were recognized thanks to their base names, which must match one of the patterns
47   * {@code eopc04_##_IAU2000.##} or {@code eopc04_##.##} (or the same ending with <code>.gz</code> for
48   * gzip-compressed files) where # stands for a digit character. As of early 2023, this ftp site
49   * seems not to be accessible anymore.</p>
50   * <p>
51   * The official source for these files is now the web site
52   * <a href="https://hpiers.obspm.fr/eoppc/eop/">https://hpiers.obspm.fr/eoppc/eop/</a>. These
53   * files do <em>not</em> follow the old naming convention that was used in the older ftp site.
54   * They lack the _05, _08 or _14 markers in the file names. The ITRF year appears only in the URL
55   * (with directories eopc04_05, eop04_c08…). The directory for the current data is named eopc04
56   * without any suffix. So before 2023-02-14 the eopc04 directory would contain files compatible with
57   * ITRF2014 and after 2023-02-14 it would contain files compatible with ITRF2020. In each directory,
58   * the files don't have any marker, hence users downloading eopc04.99 file from eopc04_05 would get
59   * a file compatible with ITRF2005 whereas users downloading a file with the exact same name eopc04.99
60   * but from eop04_c08 would get a file compatible with ITRF2008.
61   * </p>
62   * <p>
63   * Starting with Orekit version 12.0, the ITRF year is retrieved by analyzing the file header, it is
64   * not linked to file name anymore, hence it is compatible with any IERS site layout.
65   * </p>
66   * <p>
67   * This class is immutable and hence thread-safe
68   * </p>
69   * @author Luc Maisonobe
70   */
71  class EopC04FilesLoader extends AbstractEopLoader implements EopHistoryLoader {
72  
73      /** Build a loader for IERS EOP C04 files.
74       * @param supportedNames regular expression for supported files names
75       * @param manager provides access to the EOP C04 files.
76       * @param utcSupplier UTC time scale.
77       */
78      EopC04FilesLoader(final String supportedNames,
79                        final DataProvidersManager manager,
80                        final Supplier<TimeScale> utcSupplier) {
81          super(supportedNames, manager, utcSupplier);
82      }
83  
84      /** {@inheritDoc} */
85      public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
86                              final Collection<EOPEntry> history) {
87          final Parser parser = new Parser(converter, getUtc());
88          final EopParserLoader loader = new EopParserLoader(parser);
89          this.feed(loader);
90          history.addAll(loader.getEop());
91      }
92  
93      /** Internal class performing the parsing. */
94      static class Parser extends AbstractEopParser {
95  
96          /** Simple constructor.
97           * @param converter converter to use
98           * @param utc       time scale for parsing dates.
99           */
100         Parser(final NutationCorrectionConverter converter,
101                final TimeScale utc) {
102             super(converter, null, utc);
103         }
104 
105         /** {@inheritDoc} */
106         public List<EOPEntry> parse(final DataSource source)
107             throws IOException, OrekitException {
108 
109             final List<EOPEntry> history = new ArrayList<>();
110 
111             // set up a reader for line-oriented EOP C04 files
112             try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
113                 // reset parse info to start new file (do not clear history!)
114                 int lineNumber   = 0;
115                 boolean inHeader = true;
116                 final LineParser[] tentativeParsers = new LineParser[] {
117                     new LineWithoutRatesParser(source.getName()),
118                     new LineWithRatesParser(source.getName())
119                 };
120                 LineParser selectedParser = null;
121 
122                 // read all file
123                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
124                     ++lineNumber;
125                     boolean parsed = false;
126 
127                     if (inHeader) {
128                         // maybe it's an header line
129                         for (final LineParser parser : tentativeParsers) {
130                             if (parser.parseHeaderLine(line)) {
131                                 // we recognized one EOP C04 format
132                                 selectedParser = parser;
133                                 break;
134                             }
135                         }
136                     }
137 
138                     if (selectedParser != null) {
139                         // maybe it's a data line
140                         final EOPEntry entry = selectedParser.parseDataLine(line, source.getName());
141                         if (entry != null) {
142 
143                             // this is a data line, build an entry from the extracted fields
144                             history.add(entry);
145                             parsed = true;
146 
147                             // we know we have already finished header
148                             inHeader = false;
149 
150                         }
151                     }
152 
153                     if (!(inHeader || parsed)) {
154                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
155                                 lineNumber, source.getName(), line);
156                     }
157                 }
158 
159                 // check if we have read something
160                 if (inHeader) {
161                     throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, source.getName());
162                 }
163             }
164 
165             return history;
166         }
167 
168         /** Base parser for EOP C04 lines.
169          * @since 12.0
170          */
171         private abstract static class LineParser {
172 
173             /** Pattern for ITRF version. */
174             private final Pattern itrfVersionPattern;
175 
176             /** Pattern for columns header. */
177             private final Pattern columnHeaderPattern;
178 
179             /** Pattern for data lines. */
180             private final Pattern dataPattern;
181 
182             /** Year group. */
183             private final int yearGroup;
184 
185             /** Month group. */
186             private final int monthGroup;
187 
188             /** Day group. */
189             private final int dayGroup;
190 
191             /** MJD group. */
192             private final int mjdGroup;
193 
194             /** Name of the stream for error messages. */
195             private final String name;
196 
197             /** ITRF version. */
198             private ITRFVersion itrfVersion;
199 
200             /** Simple constructor.
201              * @param itrfVersionRegexp regular expression for ITRF version
202              * @param columnsHeaderRegexp regular expression for columns header
203              * @param dataRegexp regular expression for data lines
204              * @param yearGroup year group
205              * @param monthGroup month group
206              * @param dayGroup day group
207              * @param mjdGroup MJD group
208              * @param name  of the stream for error messages.
209              */
210             protected LineParser(final String itrfVersionRegexp, final String columnsHeaderRegexp,
211                                  final String dataRegexp,
212                                  final int yearGroup, final int monthGroup, final int dayGroup,
213                                  final int mjdGroup, final String name) {
214                 this.itrfVersionPattern  = Pattern.compile(itrfVersionRegexp);
215                 this.columnHeaderPattern = Pattern.compile(columnsHeaderRegexp);
216                 this.dataPattern         = Pattern.compile(dataRegexp);
217                 this.yearGroup           = yearGroup;
218                 this.monthGroup          = monthGroup;
219                 this.dayGroup            = dayGroup;
220                 this.mjdGroup            = mjdGroup;
221                 this.name                = name;
222             }
223 
224             /** Get the ITRF version for this EOP C04 file.
225              * @return ITRF version
226              */
227             protected ITRFVersion getItrfVersion() {
228                 return itrfVersion;
229             }
230 
231             /** Parse a header line.
232              * @param line line to parse
233              * @return true if line was recognized (either ITRF version or columns header)
234              */
235             public boolean parseHeaderLine(final String line) {
236                 final Matcher itrfVersionMatcher = itrfVersionPattern.matcher(line);
237                 if (itrfVersionMatcher.matches()) {
238                     switch (Integer.parseInt(itrfVersionMatcher.group(1))) {
239                         case 5 :
240                             itrfVersion = ITRFVersion.ITRF_2005;
241                             break;
242                         case 8 :
243                             itrfVersion = ITRFVersion.ITRF_2008;
244                             break;
245                         case 14 :
246                             itrfVersion = ITRFVersion.ITRF_2014;
247                             break;
248                         case 20 :
249                             itrfVersion = ITRFVersion.ITRF_2020;
250                             break;
251                         default :
252                             throw new OrekitException(OrekitMessages.NO_SUCH_ITRF_FRAME, itrfVersionMatcher.group(1));
253                     }
254                     return true;
255                 } else {
256                     final Matcher columnHeaderMatcher = columnHeaderPattern.matcher(line);
257                     if (columnHeaderMatcher.matches()) {
258                         parseColumnsHeaderLine(columnHeaderMatcher);
259                         return true;
260                     }
261                     return false;
262                 }
263             }
264 
265             /** Parse a data line.
266              * @param line line to parse
267              * @param fileName file name
268              * @return EOP entry for the line, or null if line does not match expected regular expression
269              */
270             public EOPEntry parseDataLine(final String line, final String fileName) {
271 
272                 final Matcher matcher = dataPattern.matcher(line);
273                 if (!matcher.matches()) {
274                     // this is not a data line
275                     return null;
276                 }
277 
278                 // check date
279                 final DateComponents dc = new DateComponents(Integer.parseInt(matcher.group(yearGroup)),
280                                                              Integer.parseInt(matcher.group(monthGroup)),
281                                                              Integer.parseInt(matcher.group(dayGroup)));
282                 final int    mjd   = Integer.parseInt(matcher.group(mjdGroup));
283                 if (dc.getMJD() != mjd) {
284                     throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
285                                               name, dc.getYear(), dc.getMonth(), dc.getDay(), mjd);
286                 }
287 
288                 return parseDataLine(matcher, dc, fileName);
289 
290             }
291 
292             /** Parse a columns header line.
293              * @param matcher matcher for line
294              */
295             protected abstract void parseColumnsHeaderLine(Matcher matcher);
296 
297             /** Parse a data line.
298              * @param matcher matcher for line
299              * @param dc date components already extracted from the line
300              * @param fileName file name
301              * @return EOP entry for the line
302              */
303             protected abstract EOPEntry parseDataLine(Matcher matcher, DateComponents dc, String fileName);
304 
305         }
306 
307         /** Parser for data lines without pole rates.
308          * <p>
309          * ITRF markers have either the following form:
310          * </p>
311          * <pre>
312          *                           EOP (IERS) 05 C04
313          * </pre>
314          * <p>
315          * or the following form:
316          * </p>
317          * <pre>
318          *                           EOP (IERS) 14 C04 TIME SERIES
319          * </pre>
320          * <p>
321          * Header have either the following form:
322          * </p>
323          * <pre>
324          *       Date      MJD      x          y        UT1-UTC       LOD         dPsi      dEps       x Err     y Err   UT1-UTC Err  LOD Err    dPsi Err   dEpsilon Err
325          *                          "          "           s           s            "         "        "          "          s           s            "         "
326          *      (0h UTC)
327          * </pre>
328          * <p>
329          * or the following form:
330          * </p>
331          * <pre>
332          *       Date      MJD      x          y        UT1-UTC       LOD         dX        dY        x Err     y Err   UT1-UTC Err  LOD Err     dX Err       dY Err
333          *                          "          "           s           s          "         "           "          "          s         s            "           "
334          *      (0h UTC)
335          * </pre>
336          * <p>
337          * The data lines in the EOP C04 yearly data files have either the following fixed form:
338          * </p>
339          * <pre>
340          * year month day MJD …12 floating values fields in decimal format...
341          * 2000   1   1  51544   0.043242   0.377915   0.3554777   …
342          * 2000   1   2  51545   0.043515   0.377753   0.3546065   …
343          * 2000   1   3  51546   0.043623   0.377452   0.3538444   …
344          * </pre>
345          * @since 12.0
346          */
347         private class LineWithoutRatesParser extends LineParser {
348 
349             /** Nutation header group. */
350             private static final int NUTATION_HEADER_GROUP = 1;
351 
352             /** Year group. */
353             private static final int YEAR_GROUP = 1;
354 
355             /** Month group. */
356             private static final int MONTH_GROUP = 2;
357 
358             /** Day group. */
359             private static final int DAY_GROUP = 3;
360 
361             /** MJD group. */
362             private static final int MJD_GROUP = 4;
363 
364             /** X component of pole motion group. */
365             private static final int POLE_X_GROUP = 5;
366 
367             /** Y component of pole motion group. */
368             private static final int POLE_Y_GROUP = 6;
369 
370             /** UT1-UTC group. */
371             private static final int UT1_UTC_GROUP = 7;
372 
373             /** LoD group. */
374             private static final int LOD_GROUP = 8;
375 
376             /** Correction for nutation first field (either dX or dPsi). */
377             private static final int NUT_0_GROUP = 9;
378 
379             /** Correction for nutation second field (either dY or dEps). */
380             private static final int NUT_1_GROUP = 10;
381 
382             /** Indicator for non-rotating origin. */
383             private boolean isNonRotatingOrigin;
384 
385             /** Simple constructor.
386              * @param name  of the stream for error messages.
387              */
388             LineWithoutRatesParser(final String name) {
389                 super("^ +EOP +\\(IERS\\) +([0-9][0-9]) +C04.*",
390                       "^ *Date +MJD +x +y +UT1-UTC +LOD +((?:dPsi +dEps)|(?:dX +dY)) .*",
391                       "^(\\d+) +(\\d+) +(\\d+) +(\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+)(?: +(-?\\d+\\.\\d+)){6}$",
392                       YEAR_GROUP, MONTH_GROUP, DAY_GROUP, MJD_GROUP,
393                       name);
394             }
395 
396             /** {@inheritDoc} */
397             @Override
398             protected void parseColumnsHeaderLine(final Matcher matcher) {
399                 isNonRotatingOrigin = matcher.group(NUTATION_HEADER_GROUP).startsWith("dX");
400             }
401 
402             /** {@inheritDoc} */
403             @Override
404             protected EOPEntry parseDataLine(final Matcher matcher, final DateComponents dc, final String fileName) {
405 
406                 final AbsoluteDate date = new AbsoluteDate(dc, getUtc());
407 
408                 final double x     = Double.parseDouble(matcher.group(POLE_X_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS;
409                 final double y     = Double.parseDouble(matcher.group(POLE_Y_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS;
410                 final double dtu1  = Double.parseDouble(matcher.group(UT1_UTC_GROUP));
411                 final double lod   = Double.parseDouble(matcher.group(LOD_GROUP));
412                 final double[] equinox;
413                 final double[] nro;
414                 if (isNonRotatingOrigin) {
415                     nro = new double[] {
416                         Double.parseDouble(matcher.group(NUT_0_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS,
417                         Double.parseDouble(matcher.group(NUT_1_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS
418                     };
419                     equinox = getConverter().toEquinox(date, nro[0], nro[1]);
420                 } else {
421                     equinox = new double[] {
422                         Double.parseDouble(matcher.group(NUT_0_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS,
423                         Double.parseDouble(matcher.group(NUT_1_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS
424                     };
425                     nro = getConverter().toNonRotating(date, equinox[0], equinox[1]);
426                 }
427 
428                 final EOPOrigin origin = new EOPOrigin(new DateComponents(dc.getYear(), 12, 31), fileName);
429                 return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, Double.NaN, Double.NaN,
430                                     equinox[0], equinox[1], nro[0], nro[1],
431                                     getItrfVersion(), date, EopDataType.FINAL,
432                                     origin, null, null);
433 
434             }
435         }
436 
437         /** Parser for data lines with pole rates.
438          * <p>
439          * ITRF markers have either the following form:
440          * </p>
441          * <pre>
442          * # EOP (IERS) 20 C04 TIME SERIES  consistent with ITRF 2020 - sampled at 0h UTC
443          * </pre>
444          * <p>
445          * Header have either the following form:
446          * </p>
447          * <pre>
448          * # YR  MM  DD  HH       MJD        x(")        y(")  UT1-UTC(s)       dX(")      dY(")       xrt(")      yrt(")      LOD(s)        x Er        y Er  UT1-UTC Er      dX Er       dY Er       xrt Er      yrt Er      LOD Er
449          * </pre>
450          * <p>
451          * The data lines in the EOP C04 yearly data files have either the following fixed form:
452          * </p>
453          * <pre>
454          * year month day hour MJD (in floating format) …16 floating values fields in decimal format...
455          * 2015   1   1  12  57023.50    0.030148    0.281014   …
456          * 2015   1   2  12  57024.50    0.029219    0.281441   …
457          * 2015   1   3  12  57025.50    0.028777    0.281824   …
458          * </pre>
459          * @since 12.0
460          */
461         private class LineWithRatesParser extends LineParser {
462 
463             /** Year group. */
464             private static final int YEAR_GROUP = 1;
465 
466             /** Month group. */
467             private static final int MONTH_GROUP = 2;
468 
469             /** Day group. */
470             private static final int DAY_GROUP = 3;
471 
472             /** Hour group. */
473             private static final int HOUR_GROUP = 4;
474 
475             /** MJD group. */
476             private static final int MJD_GROUP = 5;
477 
478             /** X component of pole motion group. */
479             private static final int POLE_X_GROUP = 6;
480 
481             /** Y component of pole motion group. */
482             private static final int POLE_Y_GROUP = 7;
483 
484             /** UT1-UTC group. */
485             private static final int UT1_UTC_GROUP = 8;
486 
487             /** Correction for nutation first field. */
488             private static final int NUT_DX_GROUP = 9;
489 
490             /** Correction for nutation second field. */
491             private static final int NUT_DY_GROUP = 10;
492 
493             /** X rate component of pole motion group.
494              * @since 12.0
495              */
496             private static final int POLE_X_RATE_GROUP = 11;
497 
498             /** Y rate component of pole motion group.
499              * @since 12.0
500              */
501             private static final int POLE_Y_RATE_GROUP = 12;
502 
503             /** LoD group. */
504             private static final int LOD_GROUP = 13;
505 
506             /** Simple constructor.
507              * @param name  of the stream for error messages.
508              */
509             LineWithRatesParser(final String name) {
510                 super("^# +EOP +\\(IERS\\) +([0-9][0-9]) +C04.*",
511                       "^# +YR +MM +DD +H +MJD +x\\(\"\\) +y\\(\"\\) +UT1-UTC\\(s\\) +dX\\(\"\\) +dY\\(\"\\) +xrt\\(\"\\) +yrt\\'\"\\) +.*",
512                       "^(\\d+) +(\\d+) +(\\d+) +(\\d+) +(\\d+)\\.\\d+ +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+) +(-?\\d+\\.\\d+)(?: +(-?\\d+\\.\\d+)){8}$", // we intentionally ignore MJD fractional part
513                       YEAR_GROUP, MONTH_GROUP, DAY_GROUP, MJD_GROUP,
514                       name);
515             }
516 
517             /** {@inheritDoc} */
518             @Override
519             protected void parseColumnsHeaderLine(final Matcher matcher) {
520                 // nothing to do here
521             }
522 
523             /** {@inheritDoc} */
524             @Override
525             protected EOPEntry parseDataLine(final Matcher matcher, final DateComponents dc, final String fileName) {
526 
527                 final TimeComponents tc = new TimeComponents(Integer.parseInt(matcher.group(HOUR_GROUP)), 0, 0.0);
528                 final AbsoluteDate date = new AbsoluteDate(dc, tc, getUtc());
529 
530                 final double x     = Double.parseDouble(matcher.group(POLE_X_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS;
531                 final double y     = Double.parseDouble(matcher.group(POLE_Y_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS;
532                 final double xRate = Double.parseDouble(matcher.group(POLE_X_RATE_GROUP)) *
533                                      Constants.ARC_SECONDS_TO_RADIANS / Constants.JULIAN_DAY;
534                 final double yRate = Double.parseDouble(matcher.group(POLE_Y_RATE_GROUP)) *
535                                      Constants.ARC_SECONDS_TO_RADIANS / Constants.JULIAN_DAY;
536                 final double dtu1  = Double.parseDouble(matcher.group(UT1_UTC_GROUP));
537                 final double lod   = Double.parseDouble(matcher.group(LOD_GROUP));
538                 final double[] nro = new double[] {
539                     Double.parseDouble(matcher.group(NUT_DX_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS,
540                     Double.parseDouble(matcher.group(NUT_DY_GROUP)) * Constants.ARC_SECONDS_TO_RADIANS
541                 };
542                 final double[] equinox = getConverter().toEquinox(date, nro[0], nro[1]);
543 
544                 final EOPOrigin origin = new EOPOrigin(new DateComponents(dc.getYear(), 12, 31), fileName);
545                 return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
546                                     equinox[0], equinox[1], nro[0], nro[1],
547                                     getItrfVersion(), date, EopDataType.FINAL,
548                                     origin, null, null);
549 
550             }
551         }
552 
553     }
554 
555 }