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.gnss;
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.text.ParseException;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.regex.Pattern;
29
30 import org.hipparchus.util.Pair;
31 import org.orekit.annotation.DefaultDataContext;
32 import org.orekit.data.AbstractSelfFeedingLoader;
33 import org.orekit.data.DataContext;
34 import org.orekit.data.DataLoader;
35 import org.orekit.data.DataProvidersManager;
36 import org.orekit.errors.OrekitException;
37 import org.orekit.errors.OrekitMessages;
38 import org.orekit.propagation.analytical.gnss.data.GPSAlmanac;
39 import org.orekit.time.TimeScales;
40
41
42 /**
43 * This class reads Yuma almanac files and provides {@link GPSAlmanac GPS almanacs}.
44 *
45 * <p>The definition of a Yuma almanac comes from the
46 * <a href="http://www.navcen.uscg.gov/?pageName=gpsYuma">U.S. COAST GUARD NAVIGATION CENTER</a>.</p>
47 *
48 * <p>The format of the files holding Yuma almanacs is not precisely specified,
49 * so the parsing rules have been deduced from the downloadable files at
50 * <a href="http://www.navcen.uscg.gov/?pageName=gpsAlmanacs">NAVCEN</a>
51 * and at <a href="https://celestrak.com/GPS/almanac/Yuma/">CelesTrak</a>.</p>
52 *
53 * @author Pascal Parraud
54 * @since 8.0
55 *
56 */
57 public class YUMAParser extends AbstractSelfFeedingLoader implements DataLoader {
58
59 // Constants
60 /** The source of the almanacs. */
61 private static final String SOURCE = "YUMA";
62
63 /** the useful keys in the YUMA file. */
64 private static final String[] KEY = {
65 "id", // ID
66 "health", // Health
67 "eccentricity", // Eccentricity
68 "time", // Time of Applicability(s)
69 "orbital", // Orbital Inclination(rad)
70 "rate", // Rate of Right Ascen(r/s)
71 "sqrt", // SQRT(A) (m 1/2)
72 "right", // Right Ascen at Week(rad)
73 "argument", // Argument of Perigee(rad)
74 "mean", // Mean Anom(rad)
75 "af0", // Af0(s)
76 "af1", // Af1(s/s)
77 "week" // week
78 };
79
80 /** Default supported files name pattern. */
81 private static final String DEFAULT_SUPPORTED_NAMES = ".*\\.alm$";
82
83 /** Pattern for delimiting regular expressions. */
84 private static final Pattern SEPARATOR = Pattern.compile(":");
85
86 // Fields
87 /** the list of all the almanacs read from the file. */
88 private final List<GPSAlmanac> almanacs;
89
90 /** the list of all the PRN numbers of all the almanacs read from the file. */
91 private final List<Integer> prnList;
92
93 /** Set of time scales to use. */
94 private final TimeScales timeScales;
95
96 /** Simple constructor.
97 *
98 * <p>This constructor does not load any data by itself. Data must be loaded
99 * later on by calling one of the {@link #loadData() loadData()} method or
100 * the {@link #loadData(InputStream, String) loadData(inputStream, fileName)}
101 * method.</p>
102 *
103 * <p>The supported files names are used when getting data from the
104 * {@link #loadData() loadData()} method that relies on the
105 * {@link DataContext#getDefault() default data context}. They are useless when
106 * getting data from the {@link #loadData(InputStream, String) loadData(input, name)}
107 * method.</p>
108 *
109 * @param supportedNames regular expression for supported files names
110 * (if null, a default pattern matching files with a ".alm" extension will be used)
111 * @see #loadData()
112 * @see #YUMAParser(String, DataProvidersManager, TimeScales)
113 */
114 @DefaultDataContext
115 public YUMAParser(final String supportedNames) {
116 this(supportedNames,
117 DataContext.getDefault().getDataProvidersManager(),
118 DataContext.getDefault().getTimeScales());
119 }
120
121 /**
122 * Create a YUMA loader/parser with the given source for YUMA auxiliary data files.
123 *
124 * <p>This constructor does not load any data by itself. Data must be loaded
125 * later on by calling one of the {@link #loadData() loadData()} method or
126 * the {@link #loadData(InputStream, String) loadData(inputStream, fileName)}
127 * method.</p>
128 *
129 * <p>The supported files names are used when getting data from the
130 * {@link #loadData() loadData()} method that relies on the
131 * {@code dataProvidersManager}. They are useless when
132 * getting data from the {@link #loadData(InputStream, String) loadData(input, name)}
133 * method.</p>
134 *
135 * @param supportedNames regular expression for supported files names
136 * (if null, a default pattern matching files with a ".alm" extension will be used)
137 * @param dataProvidersManager provides access to auxiliary data.
138 * @param timeScales to use when parsing the GPS dates.
139 * @see #loadData()
140 * @since 10.1
141 */
142 public YUMAParser(final String supportedNames,
143 final DataProvidersManager dataProvidersManager,
144 final TimeScales timeScales) {
145 super((supportedNames == null) ? DEFAULT_SUPPORTED_NAMES : supportedNames,
146 dataProvidersManager);
147 this.almanacs = new ArrayList<>();
148 this.prnList = new ArrayList<>();
149 this.timeScales = timeScales;
150 }
151
152 /**
153 * Loads almanacs.
154 *
155 * <p>The almanacs already loaded in the instance will be discarded
156 * and replaced by the newly loaded data.</p>
157 * <p>This feature is useful when the file selection is already set up by
158 * the {@link DataProvidersManager data providers manager} configuration.</p>
159 *
160 */
161 public void loadData() {
162 // load the data from the configured data providers
163 feed(this);
164 if (almanacs.isEmpty()) {
165 throw new OrekitException(OrekitMessages.NO_YUMA_ALMANAC_AVAILABLE);
166 }
167 }
168
169 @Override
170 public void loadData(final InputStream input, final String name)
171 throws IOException, ParseException, OrekitException {
172
173 // Clears the lists
174 almanacs.clear();
175 prnList.clear();
176
177 // Creates the reader
178 try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
179 // Gathers data to create one GPSAlmanac from 13 consecutive lines
180 final List<Pair<String, String>> entries =
181 new ArrayList<>(KEY.length);
182
183 // Reads the data one line at a time
184 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
185 // Try to split the line into 2 tokens as key:value
186 final String[] token = SEPARATOR.split(line.trim());
187 // If the line is made of 2 tokens
188 if (token.length == 2) {
189 // Adds these tokens as an entry to the entries
190 entries.add(new Pair<>(token[0].trim(), token[1].trim()));
191 }
192 // If the number of entries equals the expected number
193 if (entries.size() == KEY.length) {
194 // Gets a GPSAlmanac from the entries
195 final GPSAlmanac almanac = getAlmanac(entries, name);
196 // Adds the GPSAlmanac to the list
197 almanacs.add(almanac);
198 // Adds the PRN number of the GPSAlmanac to the list
199 prnList.add(almanac.getPRN());
200 // Clears the entries
201 entries.clear();
202 }
203 }
204 } catch (IOException ioe) {
205 throw new OrekitException(ioe, OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
206 name);
207 }
208 }
209
210 @Override
211 public boolean stillAcceptsData() {
212 return almanacs.isEmpty();
213 }
214
215 @Override
216 public String getSupportedNames() {
217 return super.getSupportedNames();
218 }
219
220 /**
221 * Gets all the {@link GPSAlmanac GPS almanacs} read from the file.
222 *
223 * @return the list of {@link GPSAlmanac} from the file
224 */
225 public List<GPSAlmanac> getAlmanacs() {
226 return almanacs;
227 }
228
229 /**
230 * Gets the PRN numbers of all the {@link GPSAlmanac GPS almanacs} read from the file.
231 *
232 * @return the PRN numbers of all the {@link GPSAlmanac GPS almanacs} read from the file
233 */
234 public List<Integer> getPRNNumbers() {
235 return prnList;
236 }
237
238 /**
239 * Builds a {@link GPSAlmanac GPS almanac} from data read in the file.
240 *
241 * @param entries the data read from the file
242 * @param name name of the file
243 * @return a {@link GPSAlmanac GPS almanac}
244 */
245 private GPSAlmanac getAlmanac(final List<Pair<String, String>> entries, final String name) {
246 try {
247 // Initializes almanac and set the source
248 final GPSAlmanac almanac = new GPSAlmanac(timeScales, SatelliteSystem.GPS);
249 almanac.setSource(SOURCE);
250
251 // Initializes checks
252 final boolean[] checks = new boolean[KEY.length];
253 // Loop over entries
254 for (Pair<String, String> entry: entries) {
255 final String lowerCaseKey = entry.getKey().toLowerCase(Locale.US);
256 if (lowerCaseKey.startsWith(KEY[0])) {
257 // Gets the PRN of the SVN
258 almanac.setPRN(Integer.parseInt(entry.getValue()));
259 checks[0] = true;
260 } else if (lowerCaseKey.startsWith(KEY[1])) {
261 // Gets the Health status
262 almanac.setHealth(Integer.parseInt(entry.getValue()));
263 checks[1] = true;
264 } else if (lowerCaseKey.startsWith(KEY[2])) {
265 // Gets the eccentricity
266 almanac.setE(Double.parseDouble(entry.getValue()));
267 checks[2] = true;
268 } else if (lowerCaseKey.startsWith(KEY[3])) {
269 // Gets the Time of Applicability
270 almanac.setTime(Double.parseDouble(entry.getValue()));
271 checks[3] = true;
272 } else if (lowerCaseKey.startsWith(KEY[4])) {
273 // Gets the Inclination
274 almanac.setI0(Double.parseDouble(entry.getValue()));
275 checks[4] = true;
276 } else if (lowerCaseKey.startsWith(KEY[5])) {
277 // Gets the Rate of Right Ascension
278 almanac.setOmegaDot(Double.parseDouble(entry.getValue()));
279 checks[5] = true;
280 } else if (lowerCaseKey.startsWith(KEY[6])) {
281 // Gets the square root of the semi-major axis
282 almanac.setSqrtA(Double.parseDouble(entry.getValue()));
283 checks[6] = true;
284 } else if (lowerCaseKey.startsWith(KEY[7])) {
285 // Gets the Right Ascension of Ascending Node
286 almanac.setOmega0(Double.parseDouble(entry.getValue()));
287 checks[7] = true;
288 } else if (lowerCaseKey.startsWith(KEY[8])) {
289 // Gets the Argument of Perigee
290 almanac.setPa(Double.parseDouble(entry.getValue()));
291 checks[8] = true;
292 } else if (lowerCaseKey.startsWith(KEY[9])) {
293 // Gets the Mean Anomalie
294 almanac.setM0(Double.parseDouble(entry.getValue()));
295 checks[9] = true;
296 } else if (lowerCaseKey.startsWith(KEY[10])) {
297 // Gets the SV clock bias
298 almanac.setAf0(Double.parseDouble(entry.getValue()));
299 checks[10] = true;
300 } else if (lowerCaseKey.startsWith(KEY[11])) {
301 // Gets the SV clock Drift
302 almanac.setAf1(Double.parseDouble(entry.getValue()));
303 checks[11] = true;
304 } else if (lowerCaseKey.startsWith(KEY[12])) {
305 // Gets the week number
306 almanac.setWeek(Integer.parseInt(entry.getValue()));
307 checks[12] = true;
308 } else {
309 // Unknown entry: the file is not a YUMA file
310 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
311 name);
312 }
313 }
314
315 // If all expected fields have been read
316 if (readOK(checks)) {
317
318 // Add default values to missing keys
319 almanac.setSVN(-1);
320 almanac.setURA(-1);
321 almanac.setSatConfiguration(-1);
322
323 return almanac;
324 } else {
325 // The file is not a YUMA file
326 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
327 name);
328 }
329 } catch (NumberFormatException nfe) {
330 throw new OrekitException(nfe, OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
331 name);
332 }
333 }
334
335 /** Checks if all expected fields have been read.
336 * @param checks flags for read fields
337 * @return true if all expected fields have been read, false if not
338 */
339 private boolean readOK(final boolean[] checks) {
340 for (boolean check: checks) {
341 if (!check) {
342 return false;
343 }
344 }
345 return true;
346 }
347 }