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