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