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 }