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 int DATE_START = 0;
83  
84      /** end index of the date part of the line. */
85      private static 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 int POLE_START_A = 16;
92  
93      /** end index of the pole part of the line (from bulletin A). */
94      private static 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 int POLE_START_B = 134;
101 
102     /** end index of the pole part of the line (from bulletin B). */
103     private static 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 int UT1_UTC_START_A = 57;
110 
111     /** end index of the UT1-UTC part of the line (from bulletin A). */
112     private static 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 int UT1_UTC_START_B = 154;
119 
120     /** end index of the UT1-UTC part of the line (from bulletin B). */
121     private static 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 int LOD_START_A = 79;
128 
129     /** end index of the LOD part of the line (from bulletin A). */
130     private static 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 int NUTATION_START_A = 95;
139 
140     /** end index of the nutation part of the line (from bulletin A). */
141     private static 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 int NUTATION_START_B = 165;
148 
149     /** end index of the nutation part of the line (from bulletin B). */
150     private static 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                     // parse the pole part
257                     final double x;
258                     final double y;
259                     if (polePartB.trim().length() == 0) {
260                         // pole part from bulletin B is blank
261                         if (polePartA.trim().length() == 0) {
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(1)));
269                                 y = UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(poleAMatcher.group(3)));
270                             } else {
271                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
272                                                           lineNumber, name, line);
273                             }
274                         }
275                     } else {
276                         final Matcher poleBMatcher = POLE_PATTERN_B.matcher(polePartB);
277                         if (poleBMatcher.matches()) {
278                             x = UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(poleBMatcher.group(1)));
279                             y = UnitsConverter.ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(poleBMatcher.group(2)));
280                         } else {
281                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
282                                                       lineNumber, name, line);
283                         }
284                     }
285 
286                     // parse the UT1-UTC part
287                     final double dtu1;
288                     if (ut1utcPartB.trim().length() == 0) {
289                         // UT1-UTC part from bulletin B is blank
290                         if (ut1utcPartA.trim().length() == 0) {
291                             // UT1-UTC part from bulletin A is blank
292                             dtu1 = 0;
293                         } else {
294                             final Matcher ut1utcAMatcher = UT1_UTC_PATTERN_A.matcher(ut1utcPartA);
295                             if (ut1utcAMatcher.matches()) {
296                                 dtu1 = Double.parseDouble(ut1utcAMatcher.group(1));
297                             } else {
298                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
299                                                           lineNumber, name, line);
300                             }
301                         }
302                     } else {
303                         final Matcher ut1utcBMatcher = UT1_UTC_PATTERN_B.matcher(ut1utcPartB);
304                         if (ut1utcBMatcher.matches()) {
305                             dtu1 = Double.parseDouble(ut1utcBMatcher.group(1));
306                         } else {
307                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
308                                                       lineNumber, name, line);
309                         }
310                     }
311 
312                     // parse the lod part
313                     final double lod;
314                     if (lodPartA.trim().length() == 0) {
315                         // lod part from bulletin A is blank
316                         lod = Double.NaN;
317                     } else {
318                         final Matcher lodAMatcher = LOD_PATTERN_A.matcher(lodPartA);
319                         if (lodAMatcher.matches()) {
320                             lod = UnitsConverter.MILLI_SECONDS_TO_SECONDS.convert(Double.parseDouble(lodAMatcher.group(1)));
321                         } else {
322                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
323                                                       lineNumber, name, line);
324                         }
325                     }
326 
327                     // parse the nutation part
328                     final double[] nro;
329                     final double[] equinox;
330                     final AbsoluteDate mjdDate =
331                             new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
332                                     getUtc());
333                     if (nutationPartB.trim().length() == 0) {
334                         // nutation part from bulletin B is blank
335                         if (nutationPartA.trim().length() == 0) {
336                             // nutation part from bulletin A is blank
337                             nro     = new double[2];
338                             equinox = new double[2];
339                         } else {
340                             final Matcher nutationAMatcher = NUTATION_PATTERN_A.matcher(nutationPartA);
341                             if (nutationAMatcher.matches()) {
342                                 if (isNonRotatingOrigin) {
343                                     nro = new double[] {
344                                         UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationAMatcher.group(1))),
345                                         UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationAMatcher.group(3)))
346                                     };
347                                     equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
348                                 } else {
349                                     equinox = new double[] {
350                                         UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationAMatcher.group(1))),
351                                         UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationAMatcher.group(3)))
352                                     };
353                                     nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
354                                 }
355                             } else {
356                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
357                                                           lineNumber, name, line);
358                             }
359                         }
360                     } else {
361                         final Matcher nutationBMatcher = NUTATION_PATTERN_B.matcher(nutationPartB);
362                         if (nutationBMatcher.matches()) {
363                             if (isNonRotatingOrigin) {
364                                 nro = new double[] {
365                                     UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationBMatcher.group(1))),
366                                     UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationBMatcher.group(2)))
367                                 };
368                                 equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
369                             } else {
370                                 equinox = new double[] {
371                                     UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationBMatcher.group(1))),
372                                     UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(nutationBMatcher.group(2)))
373                                 };
374                                 nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
375                             }
376                         } else {
377                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
378                                                       lineNumber, name, line);
379                         }
380                     }
381 
382                     if (configuration == null || !configuration.isValid(mjd)) {
383                         // get a configuration for current name and date range
384                         configuration = getItrfVersionProvider().getConfiguration(name, mjd);
385                     }
386                     history.add(new EOPEntry(mjd, dtu1, lod, x, y, Double.NaN, Double.NaN,
387                                              equinox[0], equinox[1], nro[0], nro[1],
388                                              configuration.getVersion(), mjdDate));
389 
390                 }
391 
392             }
393 
394             return history;
395         }
396 
397     }
398 
399     /** Get a part of a line.
400      * @param line line to analyze
401      * @param start start index of the part
402      * @param end end index of the part
403      * @return either the line part if present or an empty string if line is too short
404      * @since 11.1
405      */
406     private static String getPart(final String line, final int start, final int end) {
407         return (line.length() >= end) ? line.substring(start, end) : "";
408     }
409 
410 }