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.time;
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.List;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.hipparchus.util.FastMath;
30  import org.orekit.annotation.DefaultDataContext;
31  import org.orekit.data.AbstractSelfFeedingLoader;
32  import org.orekit.data.DataContext;
33  import org.orekit.data.DataProvidersManager;
34  import org.orekit.errors.OrekitException;
35  import org.orekit.errors.OrekitMessages;
36  
37  /** Loader for UTC-TAI extracted from LeapSecond file from AGI.
38   * <p>
39   * This class is immutable and hence thread-safe
40   * </p>
41   * @see <a href="ftp://ftp.agi.com/pub/STKData/Astro/LeapSecond.dat">LeapSecond.dat</a>
42   * @author Luc Maisonobe
43   * @since 10.3
44   */
45  public class AGILeapSecondFilesLoader extends AbstractSelfFeedingLoader
46          implements UTCTAIOffsetsLoader {
47  
48      /** Default supported files name pattern. */
49      public static final String DEFAULT_SUPPORTED_NAMES = "^LeapSecond\\.dat$";
50  
51      /** Number of seconds in one day. */
52      private static final long SEC_PER_DAY = 86400L;
53  
54      /** Number of attoseconds in one second. */
55      private static final long ATTOS_PER_NANO = 1000000000L;
56  
57      /** Slope conversion factor from seconds per day to nanoseconds per second. */
58      private static final long SLOPE_FACTOR = SEC_PER_DAY * ATTOS_PER_NANO;
59  
60      /**
61       * Build a loader for LeapSecond.dat file from AGI. This constructor uses the {@link
62       * DataContext#getDefault() default data context}.
63       *
64       * @param supportedNames regular expression for supported files names
65       * @see #AGILeapSecondFilesLoader(String, DataProvidersManager)
66       */
67      @DefaultDataContext
68      public AGILeapSecondFilesLoader(final String supportedNames) {
69          this(supportedNames, DataContext.getDefault().getDataProvidersManager());
70      }
71  
72      /**
73       * Build a loader for LeapSecond.dat file from AGI.
74       *
75       * @param supportedNames regular expression for supported files names
76       * @param manager        provides access to the {@code tai-utc.dat} file.
77       */
78      public AGILeapSecondFilesLoader(final String supportedNames,
79                                      final DataProvidersManager manager) {
80          super(supportedNames, manager);
81      }
82  
83      /** {@inheritDoc} */
84      @Override
85      public List<OffsetModel> loadOffsets() {
86          final UtcTaiOffsetLoader parser = new UtcTaiOffsetLoader(new Parser());
87          this.feed(parser);
88          return parser.getOffsets();
89      }
90  
91      /** Internal class performing the parsing. */
92      public static class Parser implements UTCTAIOffsetsLoader.Parser {
93  
94          /** Regular expression for optional blanks. */
95          private static final String BLANKS               = "\\p{Blank}*";
96  
97          /** Regular expression for storage start. */
98          private static final String STORAGE_START        = "(";
99  
100         /** Regular expression for storage end. */
101         private static final String STORAGE_END          = ")";
102 
103         /** Regular expression for alternative. */
104         private static final String ALTERNATIVE          = "|";
105 
106         /** Regular expression matching blanks at start of line. */
107         private static final String LINE_START_REGEXP     = "^" + BLANKS;
108 
109         /** Regular expression matching blanks at end of line. */
110         private static final String LINE_END_REGEXP       = BLANKS + "$";
111 
112         /** Regular expression matching integers. */
113         private static final String INTEGER_REGEXP        = "[-+]?\\p{Digit}+";
114 
115         /** Regular expression matching real numbers. */
116         private static final String REAL_REGEXP           = "[-+]?(?:\\p{Digit}+(?:\\.\\p{Digit}*)?|\\.\\p{Digit}+)(?:[eE][-+]?\\p{Digit}+)?";
117 
118         /** Regular expression matching an integer field to store. */
119         private static final String STORED_INTEGER_FIELD  = BLANKS + STORAGE_START + INTEGER_REGEXP + STORAGE_END;
120 
121         /** Regular expression matching a real field to store. */
122         private static final String STORED_REAL_FIELD     = BLANKS + STORAGE_START + REAL_REGEXP + STORAGE_END;
123 
124         /** Data lines pattern. */
125         private final Pattern dataPattern;
126 
127         /** Simple constructor.
128          */
129         public Parser() {
130 
131             // data lines read:
132             // 28
133             // 1972 JAN  1   2441317.5     10.0        41317.  0.0
134             // 1972 JUL  1   2441499.5     11.0        41317.  0.0
135             // 1973 JAN  1   2441683.5     12.0        41317.  0.0
136             // 1974 JAN  1   2442048.5     13.0        41317.  0.0
137             // 1975 JAN  1   2442413.5     14.0        41317.  0.0
138             // 1976 JAN  1   2442778.5     15.0        41317.  0.0
139             // 1977 JAN  1   2443144.5     16.0        41317.  0.0
140             // 1978 JAN  1   2443509.5     17.0        41317.  0.0
141 
142             // month as a three letters upper case abbreviation
143             final StringBuilder builder = new StringBuilder(BLANKS + STORAGE_START);
144             for (final Month month : Month.values()) {
145                 builder.append(month.getUpperCaseAbbreviation());
146                 builder.append(ALTERNATIVE);
147             }
148             builder.delete(builder.length() - 1, builder.length());
149             builder.append(STORAGE_END);
150             final String monthField = builder.toString();
151 
152             dataPattern = Pattern.compile(LINE_START_REGEXP +
153                                           STORED_INTEGER_FIELD + monthField + STORED_INTEGER_FIELD +
154                                           BLANKS + STORED_REAL_FIELD +
155                                           BLANKS + STORED_REAL_FIELD +
156                                           BLANKS + STORED_REAL_FIELD +
157                                           BLANKS + STORED_REAL_FIELD +
158                                           LINE_END_REGEXP);
159 
160 
161         }
162 
163         /** Load UTC-TAI offsets entries read from some file.
164          * <p>The time steps are extracted from some {@code LeapSecond.dat} file.
165          * Since entries are stored in a {@link java.util.SortedMap SortedMap},
166          * they are chronologically sorted and only one entry remains for a given date.</p>
167          * @param input data input stream
168          * @param name name of the file (or zip entry)
169          * @exception IOException if data can't be read
170          */
171         @Override
172         public List<OffsetModel> parse(final InputStream input, final String name)
173             throws IOException {
174 
175             final List<OffsetModel> offsets = new ArrayList<>();
176 
177             int lineNumber = 0;
178             DateComponents lastDate = null;
179             String line = null;
180             // set up a reader for line-oriented file
181             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
182 
183                 // read all file, ignoring not recognized lines
184                 for (line = reader.readLine(); line != null; line = reader.readLine()) {
185                     ++lineNumber;
186 
187                     // check matching for data lines
188                     final Matcher matcher = dataPattern.matcher(line);
189                     if (matcher.matches()) {
190 
191                         // build an entry from the extracted fields
192                         final DateComponents dc1 = new DateComponents(Integer.parseInt(matcher.group(1)),
193                                                                       Month.parseMonth(matcher.group(2)),
194                                                                       Integer.parseInt(matcher.group(3)));
195                         final DateComponents dc2 = new DateComponents(DateComponents.JULIAN_EPOCH,
196                                                                       (int) FastMath.ceil(Double.parseDouble(matcher.group(4))));
197                         if (!dc1.equals(dc2)) {
198                             throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
199                                                       name, dc1.getYear(), dc1.getMonth(), dc1.getDay(), dc2.getMJD());
200                         }
201 
202                         if (lastDate != null && dc1.compareTo(lastDate) <= 0) {
203                             throw new OrekitException(OrekitMessages.NON_CHRONOLOGICAL_DATES_IN_FILE,
204                                                       name, lineNumber);
205                         }
206                         lastDate = dc1;
207 
208                         final double mjdRef = Double.parseDouble(matcher.group(6));
209                         offsets.add(new OffsetModel(dc1, (int) FastMath.rint(mjdRef),
210                                                     TimeOffset.parse(matcher.group(5)),
211                                                     (int) (TimeOffset.parse(matcher.group(7)).getAttoSeconds() / SLOPE_FACTOR)));
212 
213                     }
214                 }
215 
216             } catch (NumberFormatException nfe) {
217                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
218                                           lineNumber, name, line);
219             }
220 
221             if (offsets.isEmpty()) {
222                 throw new OrekitException(OrekitMessages.NO_ENTRIES_IN_IERS_UTC_TAI_HISTORY_FILE, name);
223             }
224 
225             return offsets;
226 
227         }
228 
229     }
230 
231 }