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