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