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.HashMap;
24  import java.util.List;
25  import java.util.Map;
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.orekit.data.DataProvidersManager;
32  import org.orekit.data.DataSource;
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.Month;
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  import org.orekit.utils.units.UnitsConverter;
43  
44  /** Loader for bulletin B files.
45   * <p>Bulletin B files contain {@link EOPEntry
46   * Earth Orientation Parameters} for a few months periods.
47   * They correspond to finalized data, suitable for long term
48   * a posteriori analysis.</p>
49   * <p>The bulletin B files are recognized thanks to their base names,
50   * which must match one of the patterns <code>bulletinb_IAU2000-###.txt</code>,
51   * <code>bulletinb_IAU2000.###</code>, <code>bulletinb-###.txt</code> or
52   * <code>bulletinb.###</code> (or the same ending with <code>.gz</code>
53   * for gzip-compressed files) where # stands for a digit character.</p>
54   * <p>
55   * Starting with bulletin B 252 published in February 2009, buletins B are
56   * written in a format containing nutation corrections for both the
57   * new IAU2000 nutation model as dx, dy entries in its section 1 and nutation
58   * corrections for the old IAU1976 nutation model as dPsi, dEpsilon entries in
59   * its section 2. These bulletins are available from IERS <a
60   * href="ftp://ftp.iers.org/products/eop/bulletinb/format_2009/">
61   *  FTP site</a>. They are also available with exactly the same content
62   * (but a different naming convention) from <a
63   * href="https://hpiers.obspm.fr/eoppc/bul/bulb_new/">Paris-Meudon
64   * observatory site</a>.
65   * </p>
66   * <p>
67   * Ending with bulletin B 263 published in January 2010, bulletins B were
68   * written in a format containing only one type of nutation corrections in its
69   * section 1, either for new IAU2000 nutation model as dx, dy entries or the old
70   * IAU1976 nutation model as dPsi, dEpsilon entries, depending on the file (a pair of
71   * files with different name was published each month between March 2003 and January 2010).
72   * </p>
73   * <p>
74   * Bulletin B in csv format must be read using {@link EopCsvFilesLoader} rather
75   * than using this loader. Bulletin B in xml format must be read using {@link EopXmlLoader}
76   * rather than using this loader.
77   * </p>
78   * <p>
79   * This class handles both the old and the new format.
80   * </p>
81   * <p>
82   * This class is immutable and hence thread-safe
83   * </p>
84   * @author Luc Maisonobe
85   * @see EopCsvFilesLoader
86   * @see EopXmlLoader
87   */
88  class BulletinBFilesLoader extends AbstractEopLoader implements EopHistoryLoader {
89  
90      /** Section 0 date pattern. */
91      private static final Pattern SECTION_0_DATE;
92  
93      /** Section 1 header pattern. */
94      private static final Pattern SECTION_1_HEADER;
95  
96      /** Section 2 header pattern for old format. */
97      private static final Pattern SECTION_2_HEADER_OLD;
98  
99      /** Section 3 header pattern. */
100     private static final Pattern SECTION_3_HEADER;
101 
102     /** Pattern for line introducing the final bulletin B values. */
103     private static final Pattern FINAL_VALUES_START;
104 
105     /** Pattern for line introducing the bulletin B preliminary extension. */
106     private static final Pattern FINAL_VALUES_END;
107 
108     /** Data line pattern in section 1 (old format). */
109     private static final Pattern SECTION_1_DATA_OLD_FORMAT;
110 
111     /** Data line pattern in section 2. */
112     private static final Pattern SECTION_2_DATA_OLD_FORMAT;
113 
114     /** Data line pattern in section 1 (new format). */
115     private static final Pattern SECTION_1_DATA_NEW_FORMAT;
116 
117     /** Data line pattern in section 3 (new format). */
118     private static final Pattern SECTION_3_DATA_NEW_FORMAT;
119 
120     static {
121 
122         // the section headers lines in the old bulletin B monthly data files have
123         // the following form (the indentation discrepancy for section 6 is really
124         // present in the available files):
125         //                                                        BULLETIN B 220
126         //                                                    1 june 2006
127         // 1 - EARTH ORIENTATION PARAMETERS (IERS evaluation).
128         // either
129         // 2 - SMOOTHED VALUES OF x, y, UT1, D, DPSI, DEPSILON (IERS EVALUATION)
130         // or
131         // 2 - SMOOTHED VALUES OF x, y, UT1, D, dX, dY (IERS EVALUATION)
132         // 3 - NORMAL VALUES OF THE EARTH ORIENTATION PARAMETERS AT FIVE-DAY INTERVALS
133         // 4 - DURATION OF THE DAY AND ANGULAR VELOCITY OF THE EARTH (IERS evaluation).
134         // 5 - INFORMATION ON TIME SCALES
135         //       6 - SUMMARY OF CONTRIBUTED EARTH ORIENTATION PARAMETERS SERIES
136         //
137         // the section headers lines in the new bulletin B monthly data files have
138         // the following form:
139         //                                                      BULLETIN B 216
140         //                                                        (IAU 2000)
141         //                                                      31 january 2006
142         // 1 - DAILY FINAL VALUES OF  x, y, UT1-UTC, dX, dY
143         // 2 - DAILY FINAL VALUES OF CELESTIAL POLE OFFSETS dPsi1980 & dEps1980
144         // 3 - EARTH ANGULAR VELOCITY : DAILY FINAL VALUES OF LOD, OMEGA AT 0hUTC
145         // 4 - INFORMATION ON TIME SCALES
146         // 5 - SUMMARY OF CONTRIBUTED EARTH ORIENTATION PARAMETERS SERIES
147         SECTION_0_DATE       = Pattern.compile("^\\p{Blank}*(\\p{Digit}{1,2}) (\\p{Alpha}{3,9}) (\\p{Digit}{4}) *");
148         SECTION_1_HEADER     = Pattern.compile("^ +1 - (\\p{Upper}+) \\p{Upper}+ \\p{Upper}+.*");
149         SECTION_2_HEADER_OLD = Pattern.compile("^ +2 - SMOOTHED \\p{Upper}+ \\p{Upper}+.*(DPSI, DEPSILON|dX, dY).*");
150         SECTION_3_HEADER     = Pattern.compile("^ +3 - \\p{Upper}+ \\p{Upper}+ \\p{Upper}+.*");
151 
152         // the markers bracketing the final values in section 1 in the old bulletin B
153         // monthly data files have the following form:
154         //
155         //  Final Bulletin B values.
156         //   ...
157         //  Preliminary extension, to be updated weekly in Bulletin A and monthly
158         //  in Bulletin B.
159         //
160         // the markers bracketing the final values in section 1 in the new bulletin B
161         // monthly data files have the following form:
162         //
163         //  Final values
164         //   ...
165         //  Preliminary extension
166         //
167         FINAL_VALUES_START = Pattern.compile("^\\p{Blank}+Final( Bulletin B)? values.*");
168         FINAL_VALUES_END   = Pattern.compile("^\\p{Blank}+Preliminary extension.*");
169 
170         // the data lines in the old bulletin B monthly data files have the following form:
171         // in section 1:
172         // AUG   1  55044  0.22176 0.49302  0.231416  -33.768584   -69.1    -8.9
173         // AUG   6  55049  0.23202 0.48003  0.230263  -33.769737   -69.5    -8.5
174         // in section 2:
175         // AUG   1   55044  0.22176  0.49302  0.230581 -0.835  -0.310  -69.1   -8.9
176         // AUG   2   55045  0.22395  0.49041  0.230928 -0.296  -0.328  -69.5   -8.9
177         //
178         // the data lines in the new bulletin B monthly data files have the following form:
179         // in section 1:
180         // 2009   8   2   55045  223.954  490.410  230.9277    0.214 -0.056    0.008    0.009    0.0641  0.048  0.121
181         // 2009   8   3   55046  225.925  487.700  231.2186    0.300 -0.138    0.010    0.012    0.0466  0.099  0.248
182         // 2009   8   4   55047  227.931  485.078  231.3929    0.347 -0.231    0.019    0.023    0.0360  0.099  0.249
183         // 2009   8   5   55048  230.016  482.445  231.4601    0.321 -0.291    0.025    0.028    0.0441  0.095  0.240
184         // 2009   8   6   55049  232.017  480.026  231.3619    0.267 -0.273    0.025    0.029    0.0477  0.038  0.095
185         // in section 2:
186         // 2009   8   2   55045   -69.474    -8.929     0.199     0.121
187         // 2009   8   3   55046   -69.459    -9.016     0.250     0.248
188         // 2009   8   4   55047   -69.401    -9.039     0.250     0.249
189         // 2009   8   5   55048   -69.425    -8.864     0.247     0.240
190         // 2009   8   6   55049   -69.510    -8.539     0.153     0.095
191         // in section 3:
192         // 2009   8   2   55045 -0.3284  0.0013  15.04106723584    0.00000000023
193         // 2009   8   3   55046 -0.2438  0.0013  15.04106722111    0.00000000023
194         // 2009   8   4   55047 -0.1233  0.0013  15.04106720014    0.00000000023
195         // 2009   8   5   55048  0.0119  0.0013  15.04106717660    0.00000000023
196         // 2009   8   6   55049  0.1914  0.0013  15.04106714535    0.00000000023
197         final StringBuilder builder = new StringBuilder("^\\p{Blank}+(?:");
198         for (final Month month : Month.values()) {
199             builder.append(month.getUpperCaseAbbreviation());
200             builder.append('|');
201         }
202         builder.delete(builder.length() - 1, builder.length());
203         builder.append(")");
204         final String integerPattern      = "[-+]?\\p{Digit}+";
205         final String realPattern         = "[-+]?(?:\\p{Digit}+(?:\\.\\p{Digit}*)?|\\.\\p{Digit}+)(?:[eE][-+]?\\p{Digit}+)?";
206         final String monthNameField      = builder.toString();
207         final String ignoredIntegerField = "\\p{Blank}*" + integerPattern;
208         final String storedIntegerField  = "\\p{Blank}*(" + integerPattern + ")";
209         final String mjdField            = "\\p{Blank}+(\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})";
210         final String storedRealField     = "\\p{Blank}+(" + realPattern + ")";
211         final String ignoredRealField    = "\\p{Blank}+" + realPattern;
212         final String finalBlanks         = "\\p{Blank}*$";
213         SECTION_1_DATA_OLD_FORMAT = Pattern.compile(monthNameField + ignoredIntegerField + mjdField +
214                                                     ignoredRealField + ignoredRealField + ignoredRealField +
215                                                     ignoredRealField + ignoredRealField + ignoredRealField +
216                                                     finalBlanks);
217         SECTION_2_DATA_OLD_FORMAT = Pattern.compile(monthNameField + ignoredIntegerField + mjdField +
218                                                     storedRealField  + storedRealField  + storedRealField +
219                                                     ignoredRealField +
220                                                     storedRealField + storedRealField + storedRealField +
221                                                     finalBlanks);
222         SECTION_1_DATA_NEW_FORMAT = Pattern.compile(storedIntegerField + storedIntegerField + storedIntegerField + mjdField +
223                                                     storedRealField + storedRealField + storedRealField +
224                                                     storedRealField + storedRealField + ignoredRealField + ignoredRealField +
225                                                     ignoredRealField + ignoredRealField + ignoredRealField +
226                                                     finalBlanks);
227         SECTION_3_DATA_NEW_FORMAT = Pattern.compile(ignoredIntegerField + ignoredIntegerField + ignoredIntegerField + mjdField +
228                                                     storedRealField +
229                                                     ignoredRealField + ignoredRealField + ignoredRealField +
230                                                     finalBlanks);
231 
232     }
233 
234     /** Build a loader for IERS bulletins B files.
235      * @param supportedNames regular expression for supported files names
236      * @param manager provides access to the bulletin B files.
237      * @param utcSupplier UTC time scale.
238      */
239     BulletinBFilesLoader(final String supportedNames,
240                          final DataProvidersManager manager,
241                          final Supplier<TimeScale> utcSupplier) {
242         super(supportedNames, manager, utcSupplier);
243     }
244 
245     /** {@inheritDoc} */
246     public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
247                             final Collection<EOPEntry> history) {
248         final ItrfVersionProvider itrfVersionProvider = new ITRFVersionLoader(
249                 ITRFVersionLoader.SUPPORTED_NAMES,
250                 getDataProvidersManager());
251         final Parser parser = new Parser(converter, itrfVersionProvider, getUtc());
252         final EopParserLoader loader = new EopParserLoader(parser);
253         this.feed(loader);
254         history.addAll(loader.getEop());
255     }
256 
257     /** Internal class performing the parsing. */
258     static class Parser extends AbstractEopParser {
259 
260         /** ITRF version configuration. */
261         private ITRFVersionLoader.ITRFVersionConfiguration configuration;
262 
263         /** History entries. */
264         private List<EOPEntry> history;
265 
266         /** Map for fields read in different sections. */
267         private final Map<Integer, double[]> fieldsMap;
268 
269         /** Publication date.
270          * @since 14.0
271          */
272         private DateComponents publicationDate;
273 
274         /** Current line number. */
275         private int lineNumber;
276 
277         /** Current line. */
278         private String line;
279 
280         /** Start of final data. */
281         private int mjdMin;
282 
283         /** End of final data. */
284         private int mjdMax;
285 
286         /**
287          * Simple constructor.
288          *
289          * @param converter           converter to use
290          * @param itrfVersionProvider to use for determining the ITRF version of the EOP.
291          * @param utc                 time scale for parsing dates.
292          */
293         Parser(final NutationCorrectionConverter converter,
294                final ItrfVersionProvider itrfVersionProvider,
295                final TimeScale utc) {
296             super(converter, itrfVersionProvider, utc);
297             this.fieldsMap         = new HashMap<>();
298             this.lineNumber        = 0;
299             this.mjdMin            = Integer.MAX_VALUE;
300             this.mjdMax            = Integer.MIN_VALUE;
301         }
302 
303         /** {@inheritDoc} */
304         @Override
305         public Collection<EOPEntry> parse(final DataSource source)
306             throws IOException {
307 
308             // set up a reader for line-oriented bulletin B files
309             try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
310                 // reset parse info to start new file
311                 fieldsMap.clear();
312                 lineNumber    = 0;
313                 mjdMin        = Integer.MAX_VALUE;
314                 mjdMax        = Integer.MIN_VALUE;
315                 history       = new ArrayList<>();
316                 configuration = null;
317 
318                 // parse publication date
319                 // for a long time, there was a typo in Bulletin B header files.
320                 // files published in September, October, November and December
321                 // had their month misspelled as "septembre", "octobre", "novembre", and "decembre".
322                 // the last file published with the error was bulletin B 370 from 2018-12-01.
323                 // we use only the month truncated to its standard 3-letter abbreviation here
324                 // in order to allow parsing the publication date from these erroneous files and
325                 // identify the proper month despite the spelling error.
326                 final Matcher section0Matcher = seekToLine(SECTION_0_DATE, reader, source.getName());
327                 publicationDate = new DateComponents(Integer.parseInt(section0Matcher.group(3)),
328                                                      Month.parseMonth(section0Matcher.group(2).substring(0, 3)),
329                                                      Integer.parseInt(section0Matcher.group(1)));
330 
331                 // skip header up to section 1 and check if we are parsing an old or new format file
332                 final Matcher section1Matcher = seekToLine(SECTION_1_HEADER, reader, source.getName());
333                 final boolean isOldFormat = "EARTH".equals(section1Matcher.group(1));
334 
335                 if (isOldFormat) {
336 
337                     // extract MJD bounds for final data from section 1
338                     loadMJDBoundsOldFormat(reader, source.getName());
339 
340                     final Matcher section2Matcher = seekToLine(SECTION_2_HEADER_OLD, reader, source.getName());
341                     final boolean isNonRotatingOrigin = section2Matcher.group(1).startsWith("dX");
342                     loadEOPOldFormat(isNonRotatingOrigin, reader, source.getName());
343 
344                 } else {
345 
346                     // extract x, y, UT1-UTC, dx, dy from section 1
347                     loadXYDTDxDyNewFormat(reader, source.getName());
348 
349                     // skip to section 3
350                     seekToLine(SECTION_3_HEADER, reader, source.getName());
351 
352                     // extract LOD data from section 3
353                     loadLODNewFormat(reader, source.getName());
354 
355                     // set up the EOP entries
356                     for (Map.Entry<Integer, double[]> entry : fieldsMap.entrySet()) {
357                         final int mjd = entry.getKey();
358                         final double[] array = entry.getValue();
359                         if (Double.isNaN(array[0] + array[1] + array[2] + array[3] + array[4] + array[5])) {
360                             throw notifyUnexpectedErrorEncountered(source.getName());
361                         }
362                         final AbsoluteDate mjdDate =
363                                 new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
364                                                  getUtc());
365                         final double[] equinox = getConverter().toEquinox(mjdDate, array[4], array[5]);
366                         if (configuration == null || !configuration.isValid(mjd)) {
367                             // get a configuration for current name and date range
368                             configuration = getItrfVersionProvider().getConfiguration(source.getName(), mjd);
369                         }
370                         history.add(new EOPEntry(mjd, array[0], array[1], array[2], array[3],
371                                                  Double.NaN, Double.NaN,
372                                                  equinox[0], equinox[1], array[4], array[5],
373                                                  configuration.getVersion(), mjdDate, EopDataType.FINAL,
374                                                  publicationDate.getMJD(),
375                                                  publicationDate.getMJD(),
376                                                  publicationDate.getMJD()));
377                     }
378 
379                 }
380             }
381 
382             return history;
383 
384         }
385 
386         /** Read until a line matching a pattern is found.
387          * @param pattern pattern to look for
388          * @param reader reader from where file content is obtained
389          * @param name name of the file (or zip entry)
390          * @return the matching matcher for the line
391          * @exception IOException if data can't be read
392          */
393         private Matcher seekToLine(final Pattern pattern, final BufferedReader reader, final String name)
394             throws IOException {
395 
396             for (line = reader.readLine(); line != null; line = reader.readLine()) {
397                 ++lineNumber;
398                 final Matcher matcher = pattern.matcher(line);
399                 if (matcher.matches()) {
400                     return matcher;
401                 }
402             }
403 
404             // we have reached end of file and not found a matching line
405             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
406                                       name, lineNumber);
407 
408         }
409 
410         /** Read MJD bounds of the final data part from section 1 in the old bulletin B format.
411          * @param reader reader from where file content is obtained
412          * @param name name of the file (or zip entry)
413          * @exception IOException if data can't be read
414          */
415         private void loadMJDBoundsOldFormat(final BufferedReader reader, final String name)
416             throws IOException {
417 
418             boolean inFinalValuesPart = false;
419             for (line = reader.readLine(); line != null; line = reader.readLine()) {
420                 lineNumber++;
421                 Matcher matcher = FINAL_VALUES_START.matcher(line);
422                 if (matcher.matches()) {
423                     // we are entering final values part (in section 1)
424                     inFinalValuesPart = true;
425                 } else if (inFinalValuesPart) {
426                     matcher = SECTION_1_DATA_OLD_FORMAT.matcher(line);
427                     if (matcher.matches()) {
428                         // this is a data line, build an entry from the extracted fields
429                         final int mjd = Integer.parseInt(matcher.group(1));
430                         mjdMin = FastMath.min(mjdMin, mjd);
431                         mjdMax = FastMath.max(mjdMax, mjd);
432                     } else {
433                         matcher = FINAL_VALUES_END.matcher(line);
434                         if (matcher.matches()) {
435                             // we leave final values part
436                             return;
437                         }
438                     }
439                 }
440             }
441 
442             throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
443                                       name, lineNumber);
444 
445         }
446 
447         /** Read EOP data from section 2 in the old bulletin B format.
448          * @param isNonRotatingOrigin if true, the file contain Non-Rotating Origin nutation corrections
449          * @param reader reader from where file content is obtained
450          * @param name name of the file (or zip entry)
451          * @exception IOException if data can't be read
452          */
453         private void loadEOPOldFormat(final boolean isNonRotatingOrigin,
454                                       final BufferedReader reader, final String name)
455             throws IOException {
456 
457             // read the data lines in the final values part inside section 2
458             line = reader.readLine();
459             while (line != null) {
460                 lineNumber++;
461                 final Matcher matcher = SECTION_2_DATA_OLD_FORMAT.matcher(line);
462                 if (matcher.matches()) {
463                     // this is a data line, build an entry from the extracted fields
464                     final int    mjd   = Integer.parseInt(matcher.group(1));
465                     final double x     = Double.parseDouble(matcher.group(2)) * Constants.ARC_SECONDS_TO_RADIANS;
466                     final double y     = Double.parseDouble(matcher.group(3)) * Constants.ARC_SECONDS_TO_RADIANS;
467                     final double dtu1  = Double.parseDouble(matcher.group(4));
468                     final double lod   = UnitsConverter.MILLI_SECONDS_TO_SECONDS.convert(Double.parseDouble(matcher.group(5)));
469                     if (mjd >= mjdMin) {
470                         final AbsoluteDate mjdDate =
471                                 new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
472                                                  getUtc());
473                         final double[] equinox;
474                         final double[] nro;
475                         if (isNonRotatingOrigin) {
476                             nro = new double[] {
477                                 UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(6))),
478                                 UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(7)))
479                             };
480                             equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
481                         } else {
482                             equinox = new double[] {
483                                 UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(6))),
484                                 UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(7)))
485                             };
486                             nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
487                         }
488                         if (configuration == null || !configuration.isValid(mjd)) {
489                             // get a configuration for current name and date range
490                             configuration = getItrfVersionProvider().getConfiguration(name, mjd);
491                         }
492                         history.add(new EOPEntry(mjd, dtu1, lod, x, y, Double.NaN, Double.NaN,
493                                                  equinox[0], equinox[1], nro[0], nro[1],
494                                                  configuration.getVersion(), mjdDate, EopDataType.FINAL,
495                                                  publicationDate.getMJD(),
496                                                  publicationDate.getMJD(),
497                                                  publicationDate.getMJD()));
498                         line = mjd < mjdMax ? reader.readLine() : null;
499                     } else {
500                         line = reader.readLine();
501                     }
502                 } else {
503                     line = reader.readLine();
504                 }
505             }
506 
507         }
508 
509         /** Read X, Y, UT1-UTC, dx, dy from section 1 in the new bulletin B format.
510          * @param reader reader from where file content is obtained
511          * @param name name of the file (or zip entry)
512          * @exception IOException if data can't be read
513          */
514         private void loadXYDTDxDyNewFormat(final BufferedReader reader, final String name)
515             throws IOException {
516 
517             boolean inFinalValuesPart = false;
518             line = reader.readLine();
519             while (line != null) {
520                 lineNumber++;
521                 Matcher matcher = FINAL_VALUES_START.matcher(line);
522                 if (matcher.matches()) {
523                     // we are entering final values part (in section 1)
524                     inFinalValuesPart = true;
525                     line = reader.readLine();
526                 } else if (inFinalValuesPart) {
527                     matcher = SECTION_1_DATA_NEW_FORMAT.matcher(line);
528                     if (matcher.matches()) {
529                         // this is a data line, build an entry from the extracted fields
530                         final int year  = Integer.parseInt(matcher.group(1));
531                         final int month = Integer.parseInt(matcher.group(2));
532                         final int day   = Integer.parseInt(matcher.group(3));
533                         final int mjd   = Integer.parseInt(matcher.group(4));
534                         if (new DateComponents(year, month, day).getMJD() != mjd) {
535                             throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
536                                                       name, year, month, day, mjd);
537                         }
538                         mjdMin = FastMath.min(mjdMin, mjd);
539                         mjdMax = FastMath.max(mjdMax, mjd);
540                         final double x    = UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(5)));
541                         final double y    = UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(6)));
542                         final double dtu1 = UnitsConverter.MILLI_SECONDS_TO_SECONDS.convert(Double.parseDouble(matcher.group(7)));
543                         final double dx   = UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(8)));
544                         final double dy   = UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(9)));
545                         fieldsMap.put(mjd,
546                                       new double[] {
547                                           dtu1, Double.NaN, x, y, dx, dy
548                                       });
549                         line = reader.readLine();
550                     } else {
551                         matcher = FINAL_VALUES_END.matcher(line);
552                         line = matcher.matches() ? null : reader.readLine();
553                     }
554                 } else {
555                     line = reader.readLine();
556                 }
557             }
558         }
559 
560         /** Read LOD from section 3 in the new bulletin B format.
561          * @param reader reader from where file content is obtained
562          * @param name name of the file (or zip entry)
563          * @exception IOException if data can't be read
564          */
565         private void loadLODNewFormat(final BufferedReader reader, final String name)
566             throws IOException {
567             line = reader.readLine();
568             while (line != null) {
569                 lineNumber++;
570                 final Matcher matcher = SECTION_3_DATA_NEW_FORMAT.matcher(line);
571                 if (matcher.matches()) {
572                     // this is a data line, build an entry from the extracted fields
573                     final int    mjd = Integer.parseInt(matcher.group(1));
574                     if (mjd >= mjdMin) {
575                         final double lod = UnitsConverter.MILLI_SECONDS_TO_SECONDS.convert(Double.parseDouble(matcher.group(2)));
576                         final double[] array = fieldsMap.get(mjd);
577                         if (array == null) {
578                             throw notifyUnexpectedErrorEncountered(name);
579                         }
580                         array[1] = lod;
581                         line = mjd >= mjdMax ? null : reader.readLine();
582                     } else {
583                         line = reader.readLine();
584                     }
585                 } else {
586                     line = reader.readLine();
587                 }
588             }
589         }
590 
591         /** Create an exception to be thrown.
592          * @param name name of the file (or zip entry)
593          * @return OrekitException always thrown to notify an unexpected error has been
594          * encountered by the caller
595          */
596         private OrekitException notifyUnexpectedErrorEncountered(final String name) {
597             String loaderName = BulletinBFilesLoader.class.getName();
598             loaderName = loaderName.substring(loaderName.lastIndexOf('.') + 1);
599             return new OrekitException(OrekitMessages.UNEXPECTED_FILE_FORMAT_ERROR_FOR_LOADER,
600                                        name, loaderName);
601         }
602 
603     }
604 
605 }