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