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