1   /* Copyright 2002-2026 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.frames;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.List;
24  import java.util.function.Supplier;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import org.orekit.data.DataProvidersManager;
29  import org.orekit.data.DataSource;
30  import org.orekit.errors.OrekitException;
31  import org.orekit.errors.OrekitMessages;
32  import org.orekit.time.AbsoluteDate;
33  import org.orekit.time.DateComponents;
34  import org.orekit.time.TimeScale;
35  import org.orekit.utils.IERSConventions;
36  import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
37  import org.orekit.utils.units.UnitsConverter;
38  
39  /** Loader for IERS rapid data and prediction files in columns format (finals file).
40   * <p>Rapid data and prediction files contain {@link EOPEntry
41   * Earth Orientation Parameters} for several years periods, in one file
42   * only that is updated regularly.</p>
43   * <p>
44   * These files contain both the data from IERS Bulletin A and IERS bulletin B.
45   * This class parses only the part from Bulletin A.
46   * </p>
47   * <p>The rapid data and prediction file is recognized thanks to its base name,
48   * which must match one of the the patterns <code>finals.*</code> or
49   * <code>finals2000A.*</code> (or the same ending with <code>.gz</code>
50   * for gzip-compressed files) where * stands for a word like "all", "daily",
51   * or "data". The file with 2000A in their name correspond to the
52   * IAU-2000 precession-nutation model whereas the files without any identifier
53   * correspond to the IAU-1980 precession-nutation model. The files with the all
54   * suffix start from 1973-01-01, the file with the data suffix start
55   * from 1992-01-01 and the files with the daily suffix.</p>
56   * <p>
57   * This class is immutable and hence thread-safe
58   * </p>
59   * @author Romain Di Costanzo
60   * @see <a href="http://maia.usno.navy.mil/ser7/readme.finals2000A">finals2000A file format description at USNO</a>
61   * @see <a href="http://maia.usno.navy.mil/ser7/readme.finals">finals file format description at USNO</a>
62   */
63  class RapidDataAndPredictionColumnsLoader extends AbstractEopLoader
64          implements EopHistoryLoader {
65  
66      /** Field for year, month and day parsing. */
67      private static final String  INTEGER2_FIELD               = "((?:\\p{Blank}|\\p{Digit})\\p{Digit})";
68  
69      /** Field for modified Julian day parsing. */
70      private static final String  MJD_FIELD                    = "\\p{Blank}+(\\p{Digit}+)\\.00*";
71  
72      /** Field for separator parsing. */
73      private static final String  SEPARATOR                    = "\\p{Blank}*([IP])";
74  
75      /** Field for real parsing. */
76      private static final String  REAL_FIELD                   = "\\p{Blank}*(-?\\p{Digit}*\\.\\p{Digit}*)";
77  
78      /** Start index of the date part of the line. */
79      private static final int DATE_START = 0;
80  
81      /** end index of the date part of the line. */
82      private static final int DATE_END   = 15;
83  
84      /** Pattern to match the date part of the line (always present). */
85      private static final Pattern DATE_PATTERN = Pattern.compile(INTEGER2_FIELD + INTEGER2_FIELD + INTEGER2_FIELD + MJD_FIELD);
86  
87      /** Start index of the pole part of the line (from bulletin A). */
88      private static final int POLE_START_A = 16;
89  
90      /** end index of the pole part of the line (from bulletin A). */
91      private static final int POLE_END_A   = 55;
92  
93      /** Pattern to match the pole part of the line (from bulletin A). */
94      private static final Pattern POLE_PATTERN_A = Pattern.compile(SEPARATOR + REAL_FIELD + REAL_FIELD + REAL_FIELD + REAL_FIELD);
95  
96      /** Start index of the pole part of the line (from bulletin B). */
97      private static final int POLE_START_B = 134;
98  
99      /** end index of the pole part of the line (from bulletin B). */
100     private static final int POLE_END_B   = 154;
101 
102     /** Pattern to match the pole part of the line (from bulletin B). */
103     private static final Pattern POLE_PATTERN_B = Pattern.compile(REAL_FIELD + REAL_FIELD);
104 
105     /** Start index of the UT1-UTC part of the line (from bulletin A). */
106     private static final int UT1_UTC_START_A = 57;
107 
108     /** end index of the UT1-UTC part of the line (from bulletin A). */
109     private static final int UT1_UTC_END_A   = 78;
110 
111     /** Pattern to match the UT1-UTC part of the line (from bulletin A). */
112     private static final Pattern UT1_UTC_PATTERN_A = Pattern.compile(SEPARATOR + REAL_FIELD + REAL_FIELD);
113 
114     /** Start index of the UT1-UTC part of the line (from bulletin B). */
115     private static final int UT1_UTC_START_B = 154;
116 
117     /** end index of the UT1-UTC part of the line (from bulletin B). */
118     private static final int UT1_UTC_END_B   = 165;
119 
120     /** Pattern to match the UT1-UTC part of the line (from bulletin B). */
121     private static final Pattern UT1_UTC_PATTERN_B = Pattern.compile(REAL_FIELD);
122 
123     /** Start index of the LOD part of the line (from bulletin A). */
124     private static final int LOD_START_A = 79;
125 
126     /** end index of the LOD part of the line (from bulletin A). */
127     private static final int LOD_END_A   = 93;
128 
129     /** Pattern to match the LOD part of the line (from bulletin A). */
130     private static final Pattern LOD_PATTERN_A = Pattern.compile(REAL_FIELD + REAL_FIELD);
131 
132     // there are no LOD part from bulletin B
133 
134     /** Start index of the nutation part of the line (from bulletin A). */
135     private static final int NUTATION_START_A = 95;
136 
137     /** end index of the nutation part of the line (from bulletin A). */
138     private static final int NUTATION_END_A   = 134;
139 
140     /** Pattern to match the nutation part of the line (from bulletin A). */
141     private static final Pattern NUTATION_PATTERN_A = Pattern.compile(SEPARATOR + REAL_FIELD + REAL_FIELD + REAL_FIELD + REAL_FIELD);
142 
143     /** Start index of the nutation part of the line (from bulletin B). */
144     private static final int NUTATION_START_B = 165;
145 
146     /** end index of the nutation part of the line (from bulletin B). */
147     private static final int NUTATION_END_B   = 185;
148 
149     /** Pattern to match the nutation part of the line (from bulletin B). */
150     private static final Pattern NUTATION_PATTERN_B = Pattern.compile(REAL_FIELD + REAL_FIELD);
151 
152     /** Type of nutation corrections. */
153     private final boolean isNonRotatingOrigin;
154 
155     /** Build a loader for IERS bulletins B files.
156      * @param isNonRotatingOrigin if true the supported files <em>must</em>
157      * contain δX/δY nutation corrections, otherwise they
158      * <em>must</em> contain δΔψ/δΔε nutation
159      * corrections
160      * @param supportedNames regular expression for supported files names
161      * @param manager provides access to EOP data files.
162      * @param utcSupplier UTC time scale.
163      */
164     RapidDataAndPredictionColumnsLoader(final boolean isNonRotatingOrigin,
165                                         final String supportedNames,
166                                         final DataProvidersManager manager,
167                                         final Supplier<TimeScale> utcSupplier) {
168         super(supportedNames, manager, utcSupplier);
169         this.isNonRotatingOrigin = isNonRotatingOrigin;
170     }
171 
172     /** {@inheritDoc} */
173     public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
174                             final Collection<EOPEntry> history) {
175         final ItrfVersionProvider itrfVersionProvider = new ITRFVersionLoader(
176                 ITRFVersionLoader.SUPPORTED_NAMES,
177                 getDataProvidersManager());
178         final Parser parser =
179                 new Parser(converter, itrfVersionProvider, getUtc(), isNonRotatingOrigin);
180         final EopParserLoader loader = new EopParserLoader(parser);
181         this.feed(loader);
182         history.addAll(loader.getEop());
183     }
184 
185     /** Internal class performing the parsing. */
186     static class Parser extends AbstractEopParser {
187 
188         /** Indicator for Non-Rotating Origin. */
189         private final boolean isNonRotatingOrigin;
190 
191         /** Simple constructor.
192          * @param converter converter to use
193          * @param itrfVersionProvider to use for determining the ITRF version of the EOP.
194          * @param utc time scale for parsing dates.
195          * @param isNonRotatingOrigin type of nutation correction
196          */
197         Parser(final NutationCorrectionConverter converter,
198                final ItrfVersionProvider itrfVersionProvider,
199                final TimeScale utc,
200                final boolean isNonRotatingOrigin) {
201             super(converter, itrfVersionProvider, utc);
202             this.isNonRotatingOrigin = isNonRotatingOrigin;
203         }
204 
205         /** {@inheritDoc} */
206         @Override
207         public Collection<EOPEntry> parse(final DataSource source)
208             throws IOException {
209 
210             final List<EOPEntry> history = new ArrayList<>();
211             ITRFVersionLoader.ITRFVersionConfiguration configuration = null;
212 
213             // reset parse info to start new file (do not clear history!)
214             int lineNumber = 0;
215 
216             // set up a reader for line-oriented bulletin B files
217             try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
218 
219                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
220 
221                     lineNumber++;
222 
223                     // split the lines in its various columns (some of them can be blank)
224                     final String datePart       = getPart(line, DATE_START,       DATE_END);
225                     final String polePartA      = getPart(line, POLE_START_A,     POLE_END_A);
226                     final String ut1utcPartA    = getPart(line, UT1_UTC_START_A,  UT1_UTC_END_A);
227                     final String lodPartA       = getPart(line, LOD_START_A,      LOD_END_A);
228                     final String nutationPartA  = getPart(line, NUTATION_START_A, NUTATION_END_A);
229                     final String polePartB      = getPart(line, POLE_START_B,     POLE_END_B);
230                     final String ut1utcPartB    = getPart(line, UT1_UTC_START_B,  UT1_UTC_END_B);
231                     final String nutationPartB  = getPart(line, NUTATION_START_B, NUTATION_END_B);
232 
233                     // parse the date part
234                     final Matcher dateMatcher = DATE_PATTERN.matcher(datePart);
235                     final int mjd;
236                     if (dateMatcher.matches()) {
237                         final int yy = Integer.parseInt(dateMatcher.group(1).trim());
238                         final int mm = Integer.parseInt(dateMatcher.group(2).trim());
239                         final int dd = Integer.parseInt(dateMatcher.group(3).trim());
240                         mjd = Integer.parseInt(dateMatcher.group(4).trim());
241                         final DateComponents reconstructedDate = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd);
242                         if ((reconstructedDate.getYear() % 100) != yy ||
243                              reconstructedDate.getMonth()       != mm ||
244                              reconstructedDate.getDay()         != dd) {
245                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
246                                                       lineNumber, source.getName(), line);
247                         }
248                     } else {
249                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
250                                                   lineNumber, source.getName(), line);
251                     }
252 
253                     // EOP data type is unknown until data is parsed
254                     EopDataType eopDataType = EopDataType.UNKNOWN;
255 
256                     // parse the pole part
257                     final double x;
258                     final double y;
259                     if (polePartB.trim().isEmpty()) {
260                         // pole part from bulletin B is blank
261                         if (polePartA.trim().isEmpty()) {
262                             // pole part from bulletin A is blank
263                             x = 0;
264                             y = 0;
265                         } else {
266                             final Matcher poleAMatcher = POLE_PATTERN_A.matcher(polePartA);
267                             if (poleAMatcher.matches()) {
268                                 x = UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(poleAMatcher.group(2)));
269                                 y = UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(poleAMatcher.group(4)));
270                                 eopDataType = getEopDataType(poleAMatcher);
271                             } else {
272                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
273                                                           lineNumber, source.getName(), line);
274                             }
275                         }
276                     } else {
277                         final Matcher poleBMatcher = POLE_PATTERN_B.matcher(polePartB);
278                         if (poleBMatcher.matches()) {
279                             x = UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(poleBMatcher.group(1)));
280                             y = UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(poleBMatcher.group(2)));
281                             eopDataType = EopDataType.FINAL;
282                         } else {
283                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
284                                                       lineNumber, source.getName(), line);
285                         }
286                     }
287 
288                     // parse the UT1-UTC part
289                     final double dtu1;
290                     if (ut1utcPartB.trim().isEmpty()) {
291                         // UT1-UTC part from bulletin B is blank
292                         if (ut1utcPartA.trim().isEmpty()) {
293                             // UT1-UTC part from bulletin A is blank
294                             dtu1 = 0;
295                         } else {
296                             final Matcher ut1utcAMatcher = UT1_UTC_PATTERN_A.matcher(ut1utcPartA);
297                             if (ut1utcAMatcher.matches()) {
298                                 dtu1 = Double.parseDouble(ut1utcAMatcher.group(2));
299                                 eopDataType = updateEopDataTypeIfUnknown(eopDataType, () -> getEopDataType(ut1utcAMatcher));
300                             } else {
301                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
302                                                           lineNumber, source.getName(), line);
303                             }
304                         }
305                     } else {
306                         final Matcher ut1utcBMatcher = UT1_UTC_PATTERN_B.matcher(ut1utcPartB);
307                         if (ut1utcBMatcher.matches()) {
308                             dtu1 = Double.parseDouble(ut1utcBMatcher.group(1));
309                             eopDataType = updateEopDataTypeIfUnknown(eopDataType, () -> EopDataType.FINAL);
310                         } else {
311                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
312                                                       lineNumber, source.getName(), line);
313                         }
314                     }
315 
316                     // parse the lod part
317                     final double lod;
318                     if (lodPartA.trim().isEmpty()) {
319                         // lod part from bulletin A is blank
320                         lod = Double.NaN;
321                     } else {
322                         final Matcher lodAMatcher = LOD_PATTERN_A.matcher(lodPartA);
323                         if (lodAMatcher.matches()) {
324                             lod = UnitsConverter.MILLI_SECONDS_TO_SECONDS.convert(Double.parseDouble(lodAMatcher.group(1)));
325                         } else {
326                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
327                                                       lineNumber, source.getName(), line);
328                         }
329                     }
330 
331                     // parse the nutation part
332                     final double[] nro;
333                     final double[] equinox;
334                     final AbsoluteDate mjdDate =
335                             new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
336                                     getUtc());
337                     if (nutationPartB.trim().isEmpty()) {
338                         // nutation part from bulletin B is blank
339                         if (nutationPartA.trim().isEmpty()) {
340                             // nutation part from bulletin A is blank
341                             nro     = new double[2];
342                             equinox = new double[2];
343                         } else {
344                             final Matcher nutationAMatcher = NUTATION_PATTERN_A.matcher(nutationPartA);
345                             if (nutationAMatcher.matches()) {
346                                 if (isNonRotatingOrigin) {
347                                     nro = new double[] {
348                                         UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationAMatcher.group(2))),
349                                         UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationAMatcher.group(4)))
350                                     };
351                                     equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
352                                 } else {
353                                     equinox = new double[] {
354                                         UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationAMatcher.group(2))),
355                                         UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationAMatcher.group(4)))
356                                     };
357                                     nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
358                                 }
359                                 eopDataType = updateEopDataTypeIfUnknown(eopDataType, () -> getEopDataType(nutationAMatcher));
360                             } else {
361                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
362                                                           lineNumber, source.getName(), line);
363                             }
364                         }
365                     } else {
366                         final Matcher nutationBMatcher = NUTATION_PATTERN_B.matcher(nutationPartB);
367                         if (nutationBMatcher.matches()) {
368                             if (isNonRotatingOrigin) {
369                                 nro = new double[] {
370                                     UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationBMatcher.group(1))),
371                                     UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationBMatcher.group(2)))
372                                 };
373                                 equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
374                             } else {
375                                 equinox = new double[] {
376                                     UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationBMatcher.group(1))),
377                                     UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationBMatcher.group(2)))
378                                 };
379                                 nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
380                             }
381                             eopDataType = updateEopDataTypeIfUnknown(eopDataType, () -> EopDataType.FINAL);
382                         } else {
383                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
384                                                       lineNumber, source.getName(), line);
385                         }
386                     }
387 
388                     if (configuration == null || !configuration.isValid(mjd)) {
389                         // get a configuration for current name and date range
390                         configuration = getItrfVersionProvider().getConfiguration(source.getName(), mjd);
391                     }
392                     history.add(new EOPEntry(mjd, dtu1, lod, x, y, Double.NaN, Double.NaN,
393                                              equinox[0], equinox[1], nro[0], nro[1],
394                                              configuration.getVersion(), mjdDate, eopDataType));
395 
396                 }
397 
398             }
399 
400             return history;
401         }
402 
403         /** Get EOP data type depending on SEPARATOR.
404          * @param matcher matcher from String parsing
405          * @return EOP data type
406          * @since 13.1.1
407          */
408         private EopDataType getEopDataType(final Matcher matcher) {
409             if (matcher.group(1).equals("P")) {
410                 return EopDataType.PREDICTED;
411             } else {
412                 return EopDataType.RAPID;
413             }
414         }
415 
416         /** Updates the EOP data type if unknown.
417          * @param data EOP data type
418          * @param supplier supplier for new value
419          * @return the updated EOP data type
420          */
421         private EopDataType updateEopDataTypeIfUnknown(final EopDataType data, final Supplier<EopDataType> supplier) {
422             return data == EopDataType.UNKNOWN ? supplier.get() : data;
423         }
424     }
425 
426     /** Get a part of a line.
427      * @param line line to analyze
428      * @param start start index of the part
429      * @param end end index of the part
430      * @return either the line part if present or an empty string if line is too short
431      * @since 11.1
432      */
433     private static String getPart(final String line, final int start, final int end) {
434         return (line.length() >= end) ? line.substring(start, end) : "";
435     }
436 
437 }