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.regex.Pattern;
28  
29  import org.orekit.annotation.DefaultDataContext;
30  import org.orekit.data.AbstractSelfFeedingLoader;
31  import org.orekit.data.DataContext;
32  import org.orekit.data.DataLoader;
33  import org.orekit.data.DataProvidersManager;
34  import org.orekit.errors.OrekitException;
35  import org.orekit.errors.OrekitMessages;
36  import org.orekit.frames.Frame;
37  import org.orekit.propagation.analytical.gnss.data.GNSSConstants;
38  import org.orekit.propagation.analytical.gnss.data.GNSSOrbitalElementsFactory;
39  import org.orekit.propagation.analytical.gnss.data.GPSAlmanac;
40  import org.orekit.propagation.analytical.gnss.data.GPSAlmanacFactory;
41  import org.orekit.time.GNSSDate;
42  import org.orekit.time.TimeScales;
43  import org.orekit.utils.IERSConventions;
44  import org.orekit.utils.ParameterDriversList;
45  
46  /**
47   * This class reads SEM almanac files and provides {@link GPSAlmanac GPS almanacs}.
48   *
49   * <p>The definition of a SEM almanac comes from the
50   * <a href="http://www.navcen.uscg.gov/?pageName=gpsSem">U.S. COAST GUARD NAVIGATION CENTER</a>.</p>
51   *
52   * <p>The format of the files holding SEM almanacs is not precisely specified,
53   * so the parsing rules have been deduced from the downloadable files at
54   * <a href="http://www.navcen.uscg.gov/?pageName=gpsAlmanacs">NAVCEN</a>
55   * and at <a href="https://celestrak.com/GPS/almanac/SEM/">CelesTrak</a>.</p>
56   *
57   * @author Pascal Parraud
58   * @since 8.0
59   *
60   */
61  public class SEMParser extends AbstractSelfFeedingLoader implements DataLoader {
62  
63      // Constants
64      /** The source of the almanacs. */
65      private static final String SOURCE = "SEM";
66  
67      /** the reference value for the inclination of GPS orbit: 0.30 semicircles. */
68      private static final double INC_REF = 0.30;
69  
70      /** Default supported files name pattern. */
71      private static final String DEFAULT_SUPPORTED_NAMES = ".*\\.al3$";
72  
73      /** Separator for parsing. */
74      private static final Pattern SEPARATOR = Pattern.compile("\\s+");
75  
76      // Fields
77      /** the list of all the almanacs read from the file. */
78      private final List<GPSAlmanac> almanacs;
79  
80      /** the list of all the PRN numbers of all the almanacs read from the file. */
81      private final List<Integer> prnList;
82  
83      /** Set of time scales to use. */
84      private final TimeScales timeScales;
85  
86      /** Reference inertial frame.
87       * @since 14.0
88       */
89      private final Frame inertial;
90  
91      /** Body fixed frame.
92       * @since 14.0
93       */
94      private final Frame bodyFixed;
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}. The frames
106      *      * are set to EME2000 and ITRF with IERS 2010 conventions. They are useless when
107      * getting data from the {@link #loadData(InputStream, String) loadData(input, name)}
108      * method.</p>
109      *
110      * @param supportedNames regular expression for supported files names
111      * (if null, a default pattern matching files with a ".al3" extension will be used)
112      * @see #loadData()
113      * @see #SEMParser(String, DataProvidersManager, TimeScales, Frame, Frame)
114      */
115     @DefaultDataContext
116     public SEMParser(final String supportedNames) {
117         this(supportedNames,
118                 DataContext.getDefault().getDataProvidersManager(),
119                 DataContext.getDefault().getTimeScales(),
120                 DataContext.getDefault().getFrames().getEME2000(),
121                 DataContext.getDefault().getFrames().getITRF(IERSConventions.IERS_2010, false));
122     }
123 
124     /**
125      * Create a SEM loader/parser with the given source of SEM auxiliary data files.
126      *
127      * <p>This constructor does not load any data by itself. Data must be loaded
128      * later on by calling one of the {@link #loadData() loadData()} method or
129      * the {@link #loadData(InputStream, String) loadData(inputStream, fileName)}
130      * method.</p>
131      *
132      * <p>The supported files names are used when getting data from the
133      * {@link #loadData() loadData()} method that relies on the
134      * {@code dataProvidersManager}. They are useless when
135      * getting data from the {@link #loadData(InputStream, String) loadData(input, name)}
136      * method.</p>
137      *
138      * @param supportedNames regular expression for supported files names
139      * (if null, a default pattern matching files with a ".al3" extension will be used)
140      * @param dataProvidersManager provides access to auxiliary data.
141      * @param timeScales to use when parsing the GPS dates.
142      * @param inertial   reference inertial frame
143      * @param bodyFixed  body fixed frame
144      * @since 14.0
145      * @see #loadData()
146      */
147     public SEMParser(final String supportedNames,
148                      final DataProvidersManager dataProvidersManager,
149                      final TimeScales timeScales, final Frame inertial, final Frame bodyFixed) {
150         super((supportedNames == null) ? DEFAULT_SUPPORTED_NAMES : supportedNames,
151                 dataProvidersManager);
152         this.almanacs   = new ArrayList<>();
153         this.prnList    = new ArrayList<>();
154         this.timeScales = timeScales;
155         this.inertial   = inertial;
156         this.bodyFixed  = bodyFixed;
157     }
158 
159     /**
160      * Loads almanacs.
161      *
162      * <p>The almanacs already loaded in the instance will be discarded
163      * and replaced by the newly loaded data.</p>
164      * <p>This feature is useful when the file selection is already set up by
165      * the {@link DataProvidersManager data providers manager} configuration.</p>
166      *
167      */
168     public void loadData() {
169         // load the data from the configured data providers
170         feed(this);
171         if (almanacs.isEmpty()) {
172             throw new OrekitException(OrekitMessages.NO_SEM_ALMANAC_AVAILABLE);
173         }
174     }
175 
176     @Override
177     public void loadData(final InputStream input, final String name)
178         throws IOException, ParseException, OrekitException {
179 
180         // Clears the lists
181         almanacs.clear();
182         prnList.clear();
183 
184         // Creates the reader
185         try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
186             // Reads the number of almanacs in the file from the first line
187             String[] token = getTokens(reader);
188             final int almanacNb = Integer.parseInt(token[0].trim());
189 
190             // Reads the week number and the time of applicability from the second line
191             token = getTokens(reader);
192             final int week = Integer.parseInt(token[0].trim());
193             final double toa = Double.parseDouble(token[1].trim());
194 
195             // Loop over data blocks
196             for (int i = 0; i < almanacNb; i++) {
197                 // Reads the next lines to get one almanac from
198                 readAlmanac(reader, week, toa);
199             }
200         } catch (IndexOutOfBoundsException | IOException e) {
201             throw new OrekitException(e, OrekitMessages.NOT_A_SUPPORTED_SEM_ALMANAC_FILE, name);
202         }
203     }
204 
205     @Override
206     public boolean stillAcceptsData() {
207         return almanacs.isEmpty();
208     }
209 
210     /**
211      * Gets all the {@link GPSAlmanac GPS almanacs} read from the file.
212      *
213      * @return the list of {@link GPSAlmanac} from the file
214      */
215     public List<GPSAlmanac> getAlmanacs() {
216         return almanacs;
217     }
218 
219     /**
220      * Gets the PRN numbers of all the {@link GPSAlmanac GPS almanacs} read from the file.
221      *
222      * @return the PRN numbers of all the {@link GPSAlmanac GPS almanacs} read from the file
223      */
224     public List<Integer> getPRNNumbers() {
225         return prnList;
226     }
227 
228     @Override
229     public String getSupportedNames() {
230         return super.getSupportedNames();
231     }
232 
233     /**
234      * Builds {@link GPSAlmanac GPS almanacs} from data read in the file.
235      *
236      * @param reader the reader
237      * @param week the GPS week
238      * @param toa the Time of Applicability
239      * @throws IOException if GPSAlmanacs can't be built from the file
240      */
241     private void readAlmanac(final BufferedReader reader, final int week, final double toa)
242         throws IOException {
243         // Skips the empty line
244         reader.readLine();
245 
246         // Create an empty GPS almanac and set the source
247         final GPSAlmanacFactory factory = new GPSAlmanacFactory(timeScales, SatelliteSystem.GPS,
248                                                                 inertial, bodyFixed);
249         factory.setSource(SOURCE);
250 
251         final ParameterDriversList orb = factory.getOrbitalParametersDrivers();
252         try {
253             // Reads the PRN number from the first line
254             String[] token = getTokens(reader);
255             factory.setPrn(Integer.parseInt(token[0].trim()));
256 
257             // Reads the SV number from the second line
258             token = getTokens(reader);
259             factory.setSvn(Integer.parseInt(token[0].trim()));
260 
261             // Reads the average URA number from the third line
262             token = getTokens(reader);
263             factory.setUra(Integer.parseInt(token[0].trim()));
264 
265             // Reads the fourth line to get ecc, inc and dom
266             token = getTokens(reader);
267             orb.findByName(GNSSOrbitalElementsFactory.ECCENTRICITY).
268                         setValue(Double.parseDouble(token[0].trim()));
269             orb.findByName(GNSSOrbitalElementsFactory.INCLINATION).
270                         setValue(getInclination(Double.parseDouble(token[1].trim())));
271             factory.getOmegaDotDriver().setValue(toRadians(Double.parseDouble(token[2].trim())));
272 
273             // Reads the fifth line to get sqa, raan and aop
274             token = getTokens(reader);
275             final double sqrtA = Double.parseDouble(token[0].trim());
276             orb.findByName(GNSSOrbitalElementsFactory.SEMI_MAJOR_AXIS).
277                         setValue(sqrtA * sqrtA);
278             orb.findByName(GNSSOrbitalElementsFactory.NODE_LONGITUDE).
279                 setValue(toRadians(Double.parseDouble(token[1].trim())));
280             orb.findByName(GNSSOrbitalElementsFactory.ARGUMENT_OF_PERIAPSIS).
281                 setValue(toRadians(Double.parseDouble(token[2].trim())));
282 
283             // Reads the sixth line to get anom, af0 and af1
284             token = getTokens(reader);
285             orb.findByName(GNSSOrbitalElementsFactory.MEAN_ANOMALY).
286                 setValue(toRadians(Double.parseDouble(token[0].trim())));
287             factory.getAf0Driver().setValue(Double.parseDouble(token[1].trim()));
288             factory.getAf1Driver().setValue(Double.parseDouble(token[2].trim()));
289 
290             // Reads the seventh line to get health
291             token = getTokens(reader);
292             factory.setHealth(Integer.parseInt(token[0].trim()));
293 
294             // Reads the eighth line to get Satellite Configuration
295             token = getTokens(reader);
296             factory.setSatConfiguration(Integer.parseInt(token[0].trim()));
297 
298             // Adds the almanac to the list
299             factory.setTimeOfEphemeris(new GNSSDate(week, toa, factory.getSystem()));
300             almanacs.add(factory.createFromDrivers());
301 
302             // Adds the PRN to the list
303             prnList.add(factory.getPrn());
304         } catch (IndexOutOfBoundsException aioobe) {
305             throw new IOException(aioobe);
306         }
307     }
308 
309     /** Read a line and get tokens from.
310      *  @param reader the reader
311      *  @return the tokens from the read line
312      *  @throws IOException if the line is null
313      */
314     private String[] getTokens(final BufferedReader reader) throws IOException {
315         final String line = reader.readLine();
316         if (line != null) {
317             return SEPARATOR.split(line.trim());
318         } else {
319             throw new IOException();
320         }
321     }
322 
323     /**
324      * Gets the inclination from the inclination offset.
325      *
326      * @param incOffset the inclination offset (semicircles)
327      * @return the inclination (rad)
328      */
329     private double getInclination(final double incOffset) {
330         return toRadians(INC_REF + incOffset);
331     }
332 
333     /**
334      * Converts an angular value from semicircles to radians.
335      *
336      * @param semicircles the angular value in semicircles
337      * @return the angular value in radians
338      */
339     private double toRadians(final double semicircles) {
340         return GNSSConstants.GNSS_PI * semicircles;
341     }
342 
343 }