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