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 }