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 }