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 }