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 }