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