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 }