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.Arrays;
23  import java.util.Collection;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.function.Supplier;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import org.hipparchus.util.FastMath;
31  import org.hipparchus.util.RomanNumeral;
32  import org.orekit.data.DataProvidersManager;
33  import org.orekit.data.DataSource;
34  import org.orekit.errors.OrekitException;
35  import org.orekit.errors.OrekitInternalError;
36  import org.orekit.errors.OrekitMessages;
37  import org.orekit.time.AbsoluteDate;
38  import org.orekit.time.DateComponents;
39  import org.orekit.time.Month;
40  import org.orekit.time.TimeScale;
41  import org.orekit.utils.IERSConventions;
42  import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
43  import org.orekit.utils.units.UnitsConverter;
44  
45  /** Loader for bulletin A files.
46   * <p>Bulletin A files contain {@link EOPEntry
47   * Earth Orientation Parameters} for a few days periods, they
48   * correspond to rapid data estimations, suitable for near-real time
49   * and prediction purposes. Prediction series are only available for
50   * pole motion xp, yp and UT1-UTC, they are not available for
51   * pole offsets (Δδψ/Δδε and x/y).</p>
52   * <p>A bulletin A published on Modified Julian Day mjd (nominally a
53   * Thursday) will generally contain:
54   * </p>
55   * <ul>
56   *   <li>rapid service xp, yp and UT1-UTC data from mjd-6 to mjd</li>
57   *   <li>prediction xp, yp and UT1-UTC data from mjd+1 to mjd+365</li>
58   *   <li>if it is first bulletin of month m, final values xp, yp and
59   *       UT1-UTC data from day 2 of month m-2 to day 1 of month m-1</li>
60   *   <li>rapid service pole offsets Δδψ/Δδε and x/y if available, for some
61   *       varying period somewhere from mjd-30 to mjd-10 (see below)</li>
62   *   <li>if it is first bulletin of month m, final values pole offsets
63   *       Δδψ/Δδε and x/y data from day 2 of month m-2 to day 1 of month
64   *       m-1</li>
65   * </ul>
66   * <p>
67   * There are some discrepancies in the rapid service time range above,
68   * mainly when the nominal publication Thursday corresponds to holidays.
69   * In this case a bulletin may be published the day before and have a 6
70   * days span only for rapid data, and a later bulletin will have an 8 days
71   * span to recover the normal schedule. This occurred for bulletin A Vol.
72   * XVIII No. 047, bulletin A Vol. XVIII No. 048, bulletin A Vol. XXI No.
73   * 052 and bulletin A Vol. XXII No. 001.
74   * </p>
75   * <p>Rapid service for pole offsets appears irregular. As extreme examples
76   * bulletin A Vol. XXVI No. 037 from 2013-09-12 contained 15 entries
77   * for pole offsets, from mjd-22 to mjd-8, bulletin A Vol. XXVI No. 039
78   * from 2013-09-26 contained only 3 entries for pole offsets, from mjd-15
79   * to mjd-13, and bulletin A Vol. XXVI No. 040 from 2013-10-03 contained no
80   * rapid service pole offsets at all, it contained only final values. Despite
81   * this irregularity, rapid service data is continuous over consecutive files,
82   * so the average number of entries is 7 as the files are published on a weekly
83   * basis.
84   * </p>
85   * <p>
86   * There are no prediction data for pole offsets.
87   * </p>
88   * <p>
89   * This loader reads both the rapid service, the prediction and the final
90   * values parts. As successive files have overlaps between all these sections,
91   * values extracted from latest files (according to the publication dates from header)
92   * override values extracted from earlier files, regardless of the files
93   * reading order. If numerous bulletins A covering more than one year are read,
94   * one particular date will typically appear:
95   * </p>
96   * <ul>
97   *     <li>in the prediction section of 52 or 53 files,</li>
98   *     <li>then in the rapid data section of one file</li>
99   *     <li>then it will be missing in a few files,</li>
100  *     <li>and finally it will appear in the final values sections of a last file.</li>
101  * </ul>
102  * <p>
103  * In this case, the value retained will be the one extracted from the
104  * final values section in the last published file.
105  * </p>
106  * <p>
107  * If only one bulletin A file is read and if it corresponds to the first bulletin
108  * of a month, it will have a roughly one month wide hole between the
109  * final data and the rapid data. This hole will trigger an error as EOP
110  * continuity is checked by default for at most 5 days holes. In this case,
111  * users should call something like {@link FramesFactory#setEOPContinuityThreshold(double)
112  * FramesFactory.setEOPContinuityThreshold(Constants.JULIAN_YEAR)} to prevent
113  * the error to be triggered.
114  * </p>
115  * <p>
116  * The bulletin A files are recognized thanks to their base names, which match the pattern
117  * {@code bulletina-xxx-###.txt}, (or the same ending with {@code .gz} for gzip-compressed
118  * files). In this pattern, xxx is a roman numeral corresponding to the year minus 1987,
119  * and ### is a decimal number corresponding to the number of the week.
120  * </p>
121  * <p>
122  * The week number is generally the ISO week number (i.e. week 1 is the week that contains the
123  * first Thursday of the year). This was not the case in 2009, as the bulletin A for week 1
124  * was published on Thursday, January 8th (probably because Thursday, January 1st was holidays).
125  * So for whole 2009 year, the week number was ISO week minus one. This discrepancy was not
126  * reproduced for years 2015 and 2026, for both years the bulletin A for week 1 was published
127  * on Thursday, January 1st.
128  * </p>
129  * <p>
130  * Bulletin A in csv format must be read using {@link EopCsvFilesLoader} rather
131  * than using this loader. Bulletin A in xml format must be read using {@link EopXmlLoader}
132  * rather than using this loader.
133  * </p>
134  * <p>
135  * This class is immutable and hence thread-safe
136  * </p>
137  * @author Luc Maisonobe
138  * @since 7.0
139  * @see EopCsvFilesLoader
140  * @see EopXmlLoader
141  */
142 class BulletinAFilesLoader extends AbstractEopLoader implements EopHistoryLoader {
143 
144     /** Regular expression matching blanks at start of line. */
145     private static final String LINE_START_REGEXP     = "^\\p{Blank}+";
146 
147     /** Regular expression matching blanks at end of line. */
148     private static final String LINE_END_REGEXP       = "\\p{Blank}*$";
149 
150     /** Regular expression matching integers. */
151     private static final String INTEGER_REGEXP        = "[-+]?\\p{Digit}+";
152 
153     /** Regular expression matching real numbers. */
154     private static final String REAL_REGEXP           = "[-+]?(?:(?:\\p{Digit}+(?:\\.\\p{Digit}*)?)|(?:\\.\\p{Digit}+))(?:[eE][-+]?\\p{Digit}+)?";
155 
156     /** Regular expression matching an integer field to store. */
157     private static final String STORED_INTEGER_FIELD  = "\\p{Blank}*(" + INTEGER_REGEXP + ")";
158 
159     /** regular expression matching a Modified Julian Day field to store. */
160     private static final String STORED_MJD_FIELD      = "\\p{Blank}+(\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})";
161 
162     /** Regular expression matching a real field to store. */
163     private static final String STORED_REAL_FIELD     = "\\p{Blank}+(" + REAL_REGEXP + ")";
164 
165     /** Regular expression matching a real field to ignore. */
166     private static final String IGNORED_REAL_FIELD    = "\\p{Blank}+" + REAL_REGEXP;
167 
168     /** Regular expression matching a string field to store. */
169     private static final String STORED_STRING_FIELD   = "\\p{Blank}+(\\p{Alpha}+)";
170 
171     /** Enum for files sections, in expected order.
172      * <p>The bulletin A weekly data files contain several sections,
173      * each introduced with some fixed header text and followed by tabular data.
174      * </p>
175      */
176     private enum Section {
177 
178         /** Publication metadata. */
179         //      **********************************************************************
180         //      *                                                                    *
181         //      *                   I E R S   B U L L E T I N - A                    *
182         //      *                                                                    *
183         //      *           Rapid Service/Prediction of Earth Orientation            *
184         //      **********************************************************************
185         //      5 January 2006                                        Vol. XIX No. 001
186         //      ______________________________________________________________________
187         PUBLICATION_METADATA("\\* *I E R S   B U L L E T I N - A *\\*",
188                              LINE_START_REGEXP +
189                              STORED_INTEGER_FIELD + STORED_STRING_FIELD + STORED_INTEGER_FIELD +
190                              "\\p{Blank}*Vol\\." + STORED_STRING_FIELD +
191                              "\\p{Blank}*No\\." + STORED_INTEGER_FIELD +
192                              LINE_END_REGEXP),
193 
194         /** Earth Orientation Parameters rapid service. */
195         // section 2 always contain rapid service data including error fields
196         //      COMBINED EARTH ORIENTATION PARAMETERS:
197         //
198         //                              IERS Rapid Service
199         //              MJD      x    error     y    error   UT1-UTC   error
200         //                       "      "       "      "        s        s
201         //   13  8 30  56534 0.16762 .00009 0.32705 .00009  0.038697 0.000019
202         //   13  8 31  56535 0.16669 .00010 0.32564 .00010  0.038471 0.000019
203         //   13  9  1  56536 0.16592 .00009 0.32410 .00010  0.038206 0.000024
204         //   13  9  2  56537 0.16557 .00009 0.32270 .00009  0.037834 0.000024
205         //   13  9  3  56538 0.16532 .00009 0.32147 .00010  0.037351 0.000024
206         //   13  9  4  56539 0.16488 .00009 0.32044 .00010  0.036756 0.000023
207         //   13  9  5  56540 0.16435 .00009 0.31948 .00009  0.036036 0.000024
208         EOP_RAPID_SERVICE("COMBINED EARTH ORIENTATION PARAMETERS:",
209                           LINE_START_REGEXP +
210                           STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
211                           STORED_MJD_FIELD +
212                           STORED_REAL_FIELD + IGNORED_REAL_FIELD +
213                           STORED_REAL_FIELD + IGNORED_REAL_FIELD +
214                           STORED_REAL_FIELD + IGNORED_REAL_FIELD +
215                           LINE_END_REGEXP),
216 
217         /** Earth Orientation Parameters final values. */
218         // the first bulletin A of each month also includes final values for the
219         // period covering from day 2 of month m-2 to day 1 of month m-1.
220         //                                IERS Final Values
221         //                                 MJD        x        y      UT1-UTC
222         //                                            "        "         s
223         //             13  7  2           56475    0.1441   0.3901   0.05717
224         //             13  7  3           56476    0.1457   0.3895   0.05716
225         //             13  7  4           56477    0.1467   0.3887   0.05728
226         //             13  7  5           56478    0.1477   0.3875   0.05755
227         //             13  7  6           56479    0.1490   0.3862   0.05793
228         //             13  7  7           56480    0.1504   0.3849   0.05832
229         //             13  7  8           56481    0.1516   0.3835   0.05858
230         //             13  7  9           56482    0.1530   0.3822   0.05877
231         EOP_FINAL_VALUES("IERS Final Values",
232                          LINE_START_REGEXP +
233                          STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
234                          STORED_MJD_FIELD +
235                          STORED_REAL_FIELD +
236                          STORED_REAL_FIELD +
237                          STORED_REAL_FIELD +
238                          LINE_END_REGEXP),
239 
240         /** Earth Orientation Parameters prediction. */
241         // section 3 always contain prediction data without error fields
242         //
243         //         PREDICTIONS:
244         //         The following formulas will not reproduce the predictions given below,
245         //         but may be used to extend the predictions beyond the end of this table.
246         //
247         //         x =  0.0969 + 0.1110 cos A - 0.0103 sin A - 0.0435 cos C - 0.0171 sin C
248         //         y =  0.3457 - 0.0061 cos A - 0.1001 sin A - 0.0171 cos C + 0.0435 sin C
249         //            UT1-UTC = -0.0052 - 0.00104 (MJD - 56548) - (UT2-UT1)
250         //
251         //         where A = 2*pi*(MJD-56540)/365.25 and C = 2*pi*(MJD-56540)/435.
252         //
253         //            TAI-UTC(MJD 56541) = 35.0
254         //         The accuracy may be estimated from the expressions:
255         //         S x,y = 0.00068 (MJD-56540)**0.80   S t = 0.00025 (MJD-56540)**0.75
256         //         Estimated accuracies are:  Predictions     10 d   20 d   30 d   40 d
257         //                                    Polar coord's  0.004  0.007  0.010  0.013
258         //                                    UT1-UTC        0.0014 0.0024 0.0032 0.0040
259         //
260         //                       MJD      x(arcsec)   y(arcsec)   UT1-UTC(sec)
261         //          2013  9  6  56541       0.1638      0.3185      0.03517
262         //          2013  9  7  56542       0.1633      0.3175      0.03420
263         //          2013  9  8  56543       0.1628      0.3164      0.03322
264         //          2013  9  9  56544       0.1623      0.3153      0.03229
265         //          2013  9 10  56545       0.1618      0.3142      0.03144
266         //          2013  9 11  56546       0.1612      0.3131      0.03071
267         //          2013  9 12  56547       0.1607      0.3119      0.03008
268         EOP_PREDICTION("PREDICTIONS:",
269                        LINE_START_REGEXP +
270                        STORED_INTEGER_FIELD + STORED_INTEGER_FIELD + STORED_INTEGER_FIELD +
271                        STORED_MJD_FIELD +
272                        STORED_REAL_FIELD +
273                        STORED_REAL_FIELD +
274                        STORED_REAL_FIELD +
275                        LINE_END_REGEXP),
276 
277         /** Pole offsets, IAU-1980. */
278         // section 4 may contain rapid service pole offset series including error fields
279         //        CELESTIAL POLE OFFSET SERIES:
280         //                             NEOS Celestial Pole Offset Series
281         //                         MJD      dpsi    error     deps    error
282         //                                          (msec. of arc)
283         //                        56519   -87.47     0.13   -12.96     0.08
284         //                        56520   -87.72     0.13   -13.20     0.08
285         //                        56521   -87.79     0.19   -13.56     0.11
286         POLE_OFFSETS_IAU_1980_RAPID_SERVICE("NEOS Celestial Pole Offset Series",
287                                             LINE_START_REGEXP +
288                                             STORED_MJD_FIELD +
289                                             STORED_REAL_FIELD + IGNORED_REAL_FIELD +
290                                             STORED_REAL_FIELD + IGNORED_REAL_FIELD +
291                                             LINE_END_REGEXP),
292 
293         /** Pole offsets, IAU-1980 final values. */
294         // the first bulletin A of each month also includes final values for the
295         // period covering from day 2 of month m-2 to day 1 of month m-1.
296         //                    IERS Celestial Pole Offset Final Series
297         //                          MJD          dpsi      deps
298         //                                       (msec. of arc)
299         //                         56475       -81.0     -13.3
300         //                         56476       -81.2     -13.4
301         //                         56477       -81.6     -13.4
302         //                         56478       -82.2     -13.5
303         //                         56479       -82.5     -13.6
304         //                         56480       -82.5     -13.7
305         POLE_OFFSETS_IAU_1980_FINAL_VALUES("IERS Celestial Pole Offset Final Series",
306                                            LINE_START_REGEXP +
307                                            STORED_MJD_FIELD +
308                                            STORED_REAL_FIELD +
309                                            STORED_REAL_FIELD +
310                                            LINE_END_REGEXP),
311 
312         /** Pole offsets, IAU-2000. */
313         // the format for the IAU-2000 series is similar, but the meanings of the fields
314         // are different
315         //                       IAU2000A Celestial Pole Offset Series
316         //                        MJD      dX     error     dY     error
317         //                                      (msec. of arc)
318         //                       56519   -0.246   0.052   -0.223   0.080
319         //                       56520   -0.239   0.052   -0.248   0.080
320         //                       56521   -0.224   0.076   -0.277   0.110
321         POLE_OFFSETS_IAU_2000_RAPID_SERVICE("IAU2000A Celestial Pole Offset Series",
322                                             LINE_START_REGEXP +
323                                             STORED_MJD_FIELD +
324                                             STORED_REAL_FIELD + IGNORED_REAL_FIELD +
325                                             STORED_REAL_FIELD + IGNORED_REAL_FIELD +
326                                             LINE_END_REGEXP),
327 
328         /** Pole offsets, IAU-2000 final values. */
329         // the format for the IAU-2000 series is similar, but the meanings of the fields
330         // are different
331         //                   IAU2000A Celestial Pole Offset Final Series
332         //                            MJD     dX         dY
333         //                            (msec. of arc)
334         //                          56475     0.00      -0.28
335         //                          56476    -0.06      -0.29
336         //                          56477    -0.07      -0.27
337         //                          56478    -0.12      -0.33
338         //                          56479    -0.12      -0.33
339         //                          56480    -0.13      -0.36
340         POLE_OFFSETS_IAU_2000_FINAL_VALUES("IAU2000A Celestial Pole Offset Final Series",
341                                            LINE_START_REGEXP +
342                                            STORED_MJD_FIELD +
343                                            STORED_REAL_FIELD +
344                                            STORED_REAL_FIELD +
345                                            LINE_END_REGEXP);
346 
347         /** Header pattern. */
348         private final Pattern header;
349 
350         /** Data pattern. */
351         private final Pattern data;
352 
353         /** Simple constructor.
354          * @param headerRegExp regular expression for header
355          * @param dataRegExp regular expression for data
356          */
357         Section(final String headerRegExp, final String dataRegExp) {
358             this.header = Pattern.compile(LINE_START_REGEXP + headerRegExp + LINE_END_REGEXP);
359             this.data   = Pattern.compile(dataRegExp);
360         }
361 
362         /** Check if a line matches the section header.
363          * @param line line to check
364          * @return true if the line matches the header
365          */
366         public boolean matchesHeader(final String line) {
367             return header.matcher(line).matches();
368         }
369 
370         /** Get the data fields from a line.
371          * @param line line to parse
372          * @return extracted fields, or null if line does not match data format
373          */
374         public String[] getFields(final String line) {
375             final Matcher matcher = data.matcher(line);
376             if (matcher.matches()) {
377                 final String[] fields = new String[matcher.groupCount()];
378                 for (int i = 0; i < fields.length; ++i) {
379                     fields[i] = matcher.group(i + 1);
380                 }
381                 return fields;
382             } else {
383                 return null;
384             }
385         }
386 
387     }
388 
389     /** Build a loader for IERS bulletins A files.
390      * @param supportedNames regular expression for supported files names
391      * @param manager provides access to the bulletin A files.
392      * @param utcSupplier UTC time scale.
393      */
394     BulletinAFilesLoader(final String supportedNames,
395                          final DataProvidersManager manager,
396                          final Supplier<TimeScale> utcSupplier) {
397         super(supportedNames, manager, utcSupplier);
398     }
399 
400     /** {@inheritDoc} */
401     public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
402                             final Collection<EOPEntry> history) {
403         final ItrfVersionProvider itrfVersionProvider =
404             new ITRFVersionLoader(ITRFVersionLoader.SUPPORTED_NAMES, getDataProvidersManager());
405         final Parser parser = new Parser(converter, itrfVersionProvider, getUtc());
406         final EopParserLoader loader = new EopParserLoader(parser);
407         this.feed(loader);
408 
409         history.addAll(loader.getEop());
410 
411     }
412 
413     /** Internal class performing the parsing. */
414     static class Parser extends AbstractEopParser {
415 
416         /** File name. */
417         private String fileName;
418 
419         /** Current line number. */
420         private int lineNumber;
421 
422         /** Current line. */
423         private String line;
424 
425         /** Publication date.
426          * @since 14.0
427          */
428         private DateComponents publicationDate;
429 
430         /** Earliest parsed data. */
431         private int mjdMin;
432 
433         /** Latest parsed data. */
434         private int mjdMax;
435 
436         /** Simple constructor.
437          * @param converter           converter to use
438          * @param itrfVersionProvider to use for determining the ITRF version of the EOP.
439          * @param utc                 time scale for parsing dates.
440          * @since 14.0
441          */
442         Parser(final NutationCorrectionConverter converter,
443                final ItrfVersionProvider itrfVersionProvider,
444                final TimeScale utc) {
445             super(converter, itrfVersionProvider, utc);
446             this.lineNumber      = 0;
447             this.publicationDate = DateComponents.MODIFIED_JULIAN_EPOCH;
448             this.mjdMin          = Integer.MAX_VALUE;
449             this.mjdMax          = Integer.MIN_VALUE;
450         }
451 
452         /** {@inheritDoc} */
453         @Override
454         public Collection<EOPEntry> parse(final DataSource source)
455             throws IOException {
456 
457             // create a new list for entries parsed from this file
458             final List<EOPEntry> eop = new ArrayList<>();
459 
460             this.fileName      = source.getName();
461 
462             // set up a reader for line-oriented bulletin A files
463             try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
464                 lineNumber =  0;
465 
466                 // loop over sections
467                 final List<Section> remaining = new ArrayList<>(Arrays.asList(Section.values()));
468                 for (Section section = nextSection(remaining, reader);
469                      section != null;
470                      section = nextSection(remaining, reader)) {
471 
472                     final EopDataType eopDataType = switch (section) {
473                         case PUBLICATION_METADATA -> null;
474                         case EOP_FINAL_VALUES, POLE_OFFSETS_IAU_1980_FINAL_VALUES, POLE_OFFSETS_IAU_2000_FINAL_VALUES -> EopDataType.FINAL;
475                         case EOP_RAPID_SERVICE, POLE_OFFSETS_IAU_1980_RAPID_SERVICE,
476                              POLE_OFFSETS_IAU_2000_RAPID_SERVICE -> EopDataType.RAPID;
477                         case EOP_PREDICTION -> EopDataType.PREDICTED;
478                     };
479 
480                     switch (section) {
481                         case PUBLICATION_METADATA:
482                             loadMetadata(section, reader);
483                             break;
484                         case EOP_RAPID_SERVICE :
485                         case EOP_FINAL_VALUES  :
486                         case EOP_PREDICTION    :
487                             eop.addAll(loadXYDT(section, reader, eopDataType));
488                             break;
489                         case POLE_OFFSETS_IAU_1980_RAPID_SERVICE :
490                         case POLE_OFFSETS_IAU_1980_FINAL_VALUES  :
491                             eop.addAll(loadPoleOffsets(section, false, reader, eopDataType));
492                             break;
493                         case POLE_OFFSETS_IAU_2000_RAPID_SERVICE :
494                         case POLE_OFFSETS_IAU_2000_FINAL_VALUES  :
495                             eop.addAll(loadPoleOffsets(section, true, reader, eopDataType));
496                             break;
497                         default :
498                             // this should never happen
499                             throw new OrekitInternalError(null);
500                     }
501 
502                     // remove the already parsed section from the list
503                     remaining.remove(section);
504 
505                 }
506 
507                 // check that the mandatory sections have been parsed
508                 if (remaining.contains(Section.EOP_RAPID_SERVICE) ||
509                     remaining.contains(Section.EOP_PREDICTION) ||
510                     (remaining.contains(Section.POLE_OFFSETS_IAU_1980_RAPID_SERVICE) ^
511                      remaining.contains(Section.POLE_OFFSETS_IAU_2000_RAPID_SERVICE)) ||
512                     (remaining.contains(Section.POLE_OFFSETS_IAU_1980_FINAL_VALUES) ^
513                      remaining.contains(Section.POLE_OFFSETS_IAU_2000_FINAL_VALUES))) {
514                     throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, source.getName());
515                 }
516 
517             }
518 
519             // return the parsed eop
520             return eop;
521 
522         }
523 
524         /** Skip to next section header.
525          * @param sections sections to check for
526          * @param reader reader from where file content is obtained
527          * @return the next section or null if no section is found until end of file
528          * @exception IOException if data can't be read
529          */
530         private Section nextSection(final List<Section> sections,
531                                     final BufferedReader reader)
532             throws IOException {
533 
534             for (line = reader.readLine(); line != null; line = reader.readLine()) {
535                 ++lineNumber;
536                 for (Section section : sections) {
537                     if (section.matchesHeader(line)) {
538                         return section;
539                     }
540                 }
541             }
542 
543             // we have reached end of file and not found a matching section header
544             return null;
545 
546         }
547 
548         /** Read publication metadata.
549          * @param section section to parse
550          * @param reader reader from where file content is obtained
551          * @exception IOException if data can't be read
552          */
553         private void loadMetadata(final Section section, final BufferedReader reader)
554             throws IOException {
555             for (line = reader.readLine(); line != null; line = reader.readLine()) {
556                 lineNumber++;
557                 final String[] fields = section.getFields(line);
558                 if (fields != null) {
559 
560                     // parse the date
561                     publicationDate = new DateComponents(Integer.parseInt(fields[2]),
562                                                          Month.parseMonth(fields[1]),
563                                                          Integer.parseInt(fields[0]));
564 
565                     // check volume and week number
566                     checkVolume(RomanNumeral.parse(fields[3].toUpperCase(Locale.ROOT)));
567                     checkWeek(Integer.parseInt(fields[4]));
568 
569                     return;
570 
571                 }
572             }
573         }
574 
575         /** Check the volume number against publication date.
576          * @param parsed parsed volume
577          */
578         private void checkVolume(final int parsed) {
579             final int computed = publicationDate.getYear() - 1987;
580             if (computed != parsed) {
581                 throw new OrekitException(OrekitMessages.UNEXPECTED_DATA_AT_LINE_IN_FILE,
582                                           lineNumber, fileName);
583             }
584         }
585 
586         /** Check the week number against publication date.
587          * @param parsed parsed week
588          */
589         private void checkWeek(final int parsed) {
590 
591             final int computed = publicationDate.getCalendarWeek();
592 
593             if (computed != parsed) {
594 
595                 // in 2009, January 1st was a Thursday, and the bulletin A numbered as week 1
596                 // was published on January 8th. Week numbers in bulletin A were therefore ISO
597                 // week number minus 1 throughout the 2009 year. This did not happen in 2015
598                 // or 2026 despite both years also started on a Thursday
599                 // The previous file published on 2008-12-31 was also numbered as week 53 in
600                 // 2008, despite its ISO number was really 1 in 2009.
601                 // we allow these special cases here
602                 final boolean januaryFirstIsThrusday =
603                     new DateComponents(publicationDate.getYear(), 1, 1).getDayOfWeek() == 4;
604                 if (januaryFirstIsThrusday && parsed == computed - 1 ||
605                     computed == 1 && parsed == 53) {
606                     // we accept this sloppy week number
607                     return;
608                 }
609 
610                 throw new OrekitException(OrekitMessages.UNEXPECTED_DATA_AT_LINE_IN_FILE,
611                                           lineNumber, fileName);
612 
613             }
614 
615         }
616 
617         /** Read X, Y, UT1-UTC.
618          * @param section section to parse
619          * @param reader reader from where file content is obtained
620          * @param eopDataType EOP data type
621          * @return parsed incomplete EOP
622          * @exception IOException if data can't be read
623          */
624         private List<EOPEntry> loadXYDT(final Section section, final BufferedReader reader,
625                                         final EopDataType eopDataType)
626             throws IOException {
627 
628             final List<EOPEntry> eop = new ArrayList<>();
629             for (line = reader.readLine(); line != null; line = reader.readLine()) {
630 
631                 lineNumber++;
632                 final String[] fields = section.getFields(line);
633                 if (fields == null) {
634                     if (eop.isEmpty()) {
635                         // we are still waiting for the first data line
636                         continue;
637                     } else {
638                         // we have reach a separation line with next section
639                         break;
640                     }
641                 }
642 
643                 // this is a data line, build an entry from the extracted fields
644                 final int year  = Integer.parseInt(fields[0]);
645                 final int month = Integer.parseInt(fields[1]);
646                 final int day   = Integer.parseInt(fields[2]);
647                 final int mjd   = Integer.parseInt(fields[3]);
648                 final DateComponents dc = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd);
649                 if ((dc.getYear() % 100) != (year % 100) ||
650                     dc.getMonth() != month ||
651                     dc.getDay() != day) {
652                     throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
653                                               fileName, year, month, day, mjd);
654                 }
655                 mjdMin = FastMath.min(mjdMin, mjd);
656                 mjdMax = FastMath.max(mjdMax, mjd);
657 
658                 eop.add(new EOPEntry(mjd,
659                                      Double.parseDouble(fields[6]), Double.NaN,
660                                      UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[4])),
661                                      UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[5])),
662                                      Double.NaN, Double.NaN,
663                                      0.0, 0.0, 0.0, 0.0,
664                                      getItrfVersionProvider().getConfiguration(fileName, mjd).getVersion(),
665                                      AbsoluteDate.createMJDDate(mjd, 0, getUtc()), eopDataType,
666                                      publicationDate.getMJD(), 0, 0));
667 
668             }
669 
670             if (!eop.isEmpty()) {
671                 // we have read something
672                 return eop;
673             }
674 
675             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
676                                       fileName, lineNumber);
677 
678         }
679 
680         /** Read EOP data.
681          * @param section section to parse
682          * @param isNonRotatingOrigin if true, the file contain Non-Rotating Origin nutation corrections
683          * @param reader reader from where file content is obtained
684          * @param eopDataType EOP data type
685          * @return parsed incomplete EOP
686          * @exception IOException if data can't be read
687          */
688         private List<EOPEntry> loadPoleOffsets(final Section section, final boolean isNonRotatingOrigin,
689                                                final BufferedReader reader, final EopDataType eopDataType)
690             throws IOException {
691 
692             final List<EOPEntry> eop = new ArrayList<>();
693             for (line = reader.readLine(); line != null; line = reader.readLine()) {
694 
695                 lineNumber++;
696                 final String[] fields = section.getFields(line);
697                 if (fields == null) {
698                     if (eop.isEmpty()) {
699                         // we are still waiting for the first data line
700                         continue;
701                     } else {
702                         // we have reach a separation line with next section
703                         break;
704                     }
705                 }
706 
707                 final int mjd = Integer.parseInt(fields[0]);
708                 mjdMin = FastMath.min(mjdMin, mjd);
709                 mjdMax = FastMath.max(mjdMax, mjd);
710 
711                 if (isNonRotatingOrigin) {
712                     eop.add(new EOPEntry(mjd,
713                                          Double.NaN, Double.NaN,
714                                          Double.NaN, Double.NaN,
715                                          Double.NaN, Double.NaN,
716                                          Double.NaN, Double.NaN,
717                                          UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[1])),
718                                          UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[2])),
719                                          getItrfVersionProvider().getConfiguration(fileName, mjd).getVersion(),
720                                          AbsoluteDate.createMJDDate(mjd, 0, getUtc()), eopDataType,
721                                          0, 0, publicationDate.getMJD()));
722                 } else {
723                     eop.add(new EOPEntry(mjd,
724                                          Double.NaN, Double.NaN,
725                                          Double.NaN, Double.NaN,
726                                          Double.NaN, Double.NaN,
727                                          UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[1])),
728                                          UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(fields[2])),
729                                          Double.NaN, Double.NaN,
730                                          getItrfVersionProvider().getConfiguration(fileName, mjd).getVersion(),
731                                          AbsoluteDate.createMJDDate(mjd, 0, getUtc()), eopDataType,
732                                          0, publicationDate.getMJD(), 0));
733                 }
734             }
735 
736             if (!eop.isEmpty()) {
737                 // we have read something
738                 return eop;
739             }
740 
741             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
742                                       fileName, lineNumber);
743 
744         }
745 
746     }
747 
748 }