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.propagation.analytical.gnss;
18
19 import org.hipparchus.util.FastMath;
20 import org.hipparchus.util.Pair;
21 import org.junit.jupiter.api.Assertions;
22 import org.junit.jupiter.api.Test;
23 import org.orekit.Utils;
24 import org.orekit.data.DataContext;
25 import org.orekit.data.DataLoader;
26 import org.orekit.data.DataProvidersManager;
27 import org.orekit.errors.OrekitException;
28 import org.orekit.errors.OrekitMessages;
29 import org.orekit.gnss.SatelliteSystem;
30 import org.orekit.gnss.YUMAParser;
31 import org.orekit.propagation.analytical.gnss.data.QZSSAlmanac;
32 import org.orekit.time.GNSSDate;
33
34 import java.io.BufferedReader;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.InputStreamReader;
38 import java.nio.charset.StandardCharsets;
39 import java.util.ArrayList;
40 import java.util.List;
41
42 public class QZSSAlmanacTest {
43
44 @Test
45 public void testLoadData() throws OrekitException {
46 Utils.setDataRoot("regular-data");
47 // the parser for reading Yuma files with a pattern
48 QZSSYUMAParser reader = new QZSSYUMAParser(".*\\.yum$");
49 // the YUMA file to read
50 final String fileName = "/gnss/q2019034.alm";
51 final InputStream in = getClass().getResourceAsStream(fileName);
52 reader.loadData(in, fileName);
53
54 Assertions.assertEquals(".*\\.yum$", reader.getSupportedNames());
55
56 // Checks the whole file read
57 Assertions.assertEquals(4, reader.getAlmanacs().size());
58 Assertions.assertEquals(4, reader.getPRNNumbers().size());
59
60 // Checks the last almanac read
61 final QZSSAlmanac alm = reader.getAlmanacs().get(reader.getAlmanacs().size() - 1);
62 Assertions.assertEquals(199, alm.getPRN());
63 Assertions.assertEquals(1015, alm.getWeek());
64 Assertions.assertEquals(262144.0, alm.getTime(), 0.);
65 Assertions.assertEquals(6493.484863, FastMath.sqrt(alm.getSma()), FastMath.ulp(5.E+03));
66 Assertions.assertEquals(1.387596130E-04, alm.getE(), FastMath.ulp(8E-05));
67 Assertions.assertEquals(0.0007490141, alm.getI0(), 0.);
68 Assertions.assertEquals(0., alm.getIDot(), 0.);
69 Assertions.assertEquals(9.194173760E-01, alm.getOmega0(), 0.);
70 Assertions.assertEquals(9.714690370E-10, alm.getOmegaDot(), FastMath.ulp(-8E-09));
71 Assertions.assertEquals(2.722442515, alm.getPa(), 0.);
72 Assertions.assertEquals(-1.158294811, alm.getM0(), 0.);
73 Assertions.assertEquals(6.351470947E-04, alm.getAf0(), 0.);
74 Assertions.assertEquals(0.0, alm.getAf1(), 0.);
75 Assertions.assertEquals(0, alm.getHealth());
76 Assertions.assertEquals("YUMA", alm.getSource());
77 Assertions.assertEquals(0, alm.getDate().durationFrom(new GNSSDate(1015, 262144.0, SatelliteSystem.QZSS).getDate()));
78 Assertions.assertEquals(0., alm.getCic(), 0.);
79 Assertions.assertEquals(0., alm.getCis(), 0.);
80 Assertions.assertEquals(0., alm.getCrc(), 0.);
81 Assertions.assertEquals(0., alm.getCrs(), 0.);
82 Assertions.assertEquals(0., alm.getCuc(), 0.);
83 Assertions.assertEquals(0., alm.getCus(), 0.);
84 }
85
86 /**
87 * This class reads Yuma almanac files and provides {@link QZSSAlmanac QZSS almanacs}.
88 *
89 * <p>This class is a rewrite of {@link YUMAParser} adapted to QZSS yuma files</p>
90 *
91 * @author Pascal Parraud
92 *
93 */
94 private static class QZSSYUMAParser implements DataLoader {
95
96 // Constants
97 /** The source of the almanacs. */
98 private static final String SOURCE = "YUMA";
99
100 /** the useful keys in the YUMA file. */
101 private final String[] KEY = {
102 "id", // ID
103 "health", // Health
104 "eccentricity", // Eccentricity
105 "time", // Time of Applicability(s)
106 "orbital", // Orbital Inclination(rad)
107 "rate", // Rate of Right Ascen(r/s)
108 "sqrt", // SQRT(A) (m 1/2)
109 "right", // Right Ascen at Week(rad)
110 "argument", // Argument of Perigee(rad)
111 "mean", // Mean Anom(rad)
112 "af0", // Af0(s)
113 "af1", // Af1(s/s)
114 "week" // week
115 };
116
117 /** Default supported files name pattern. */
118 private static final String DEFAULT_SUPPORTED_NAMES = ".*\\.alm$";
119
120 // Fields
121 /** Regular expression for supported files names. */
122 private final String supportedNames;
123
124 /** the list of all the almanacs read from the file. */
125 private final List<QZSSAlmanac> almanacs;
126
127 /** the list of all the PRN numbers of all the almanacs read from the file. */
128 private final List<Integer> prnList;
129
130 /** Simple constructor.
131 *
132 * <p>This constructor does not load any data by itself. Data must be loaded
133 * later on by calling one of the {@link #loadData(InputStream, String)} loadData()} method or
134 * the {@link #loadData(InputStream, String) loadData(inputStream, fileName)}
135 * method.</p>
136 *
137 * <p>The supported files names are used when getting data from the
138 * {@link #loadData(InputStream, String) loadData()} method that relies on the
139 * {@link DataProvidersManager data providers manager}. They are useless when
140 * getting data from the {@link #loadData(InputStream, String) loadData(input, name)}
141 * method.</p>
142 *
143 * @param supportedNames regular expression for supported files names
144 * (if null, a default pattern matching files with a ".alm" extension will be used)
145 * @see #loadData(InputStream, String)
146 */
147 public QZSSYUMAParser(final String supportedNames) {
148 this.supportedNames = (supportedNames == null) ? DEFAULT_SUPPORTED_NAMES : supportedNames;
149 this.almanacs = new ArrayList<>();
150 this.prnList = new ArrayList<>();
151 }
152
153 @Override
154 public void loadData(final InputStream input, final String name) {
155
156 // Clears the lists
157 almanacs.clear();
158 prnList.clear();
159
160 // Creates the reader
161 final BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
162
163 try {
164 // Gathers data to create one QZSSAlmanac from 13 consecutive lines
165 final List<Pair<String, String>> entries = new ArrayList<>(KEY.length);
166
167 // Reads the data one line at a time
168 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
169 // Try to split the line into 2 tokens as key:value
170 final String[] token = line.trim().split(":");
171 // If the line is made of 2 tokens
172 if (token.length == 2) {
173 // Adds these tokens as an entry to the entries
174 entries.add(new Pair<>(token[0].trim(), token[1].trim()));
175 }
176 // If the number of entries equals the expected number
177 if (entries.size() == KEY.length) {
178 // Gets a QZSSAlmanac from the entries
179 final QZSSAlmanac almanac = getAlmanac(entries, name);
180 // Adds the QZSSAlmanac to the list
181 almanacs.add(almanac);
182 // Adds the PRN number of the QZSSAlmanac to the list
183 prnList.add(almanac.getPRN());
184 // Clears the entries
185 entries.clear();
186 }
187 }
188 } catch (IOException ioe) {
189 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
190 name);
191 }
192 }
193
194 @Override
195 public boolean stillAcceptsData() {
196 return almanacs.isEmpty();
197 }
198
199 /** Get the supported names for data files.
200 * @return regular expression for the supported names for data files
201 */
202 public String getSupportedNames() {
203 return supportedNames;
204 }
205
206 /**
207 * Gets all the {@link QZSSAlmanac QZSS Almanacs} read from the file.
208 *
209 * @return the list of {@link QZSSAlmanac} from the file
210 */
211 public List<QZSSAlmanac> getAlmanacs() {
212 return almanacs;
213 }
214
215 /**
216 * Gets the PRN numbers of all the {@link QZSSAlmanac QZSS Almanacs} read from the file.
217 *
218 * @return the PRN numbers of all the {@link QZSSAlmanac QZSS Almanacs} read from the file
219 */
220 public List<Integer> getPRNNumbers() {
221 return prnList;
222 }
223
224 /**
225 * Builds a {@link QZSSAlmanac QZSS Almanac} from data read in the file.
226 *
227 * @param entries the data read from the file
228 * @param name name of the file
229 * @return a {@link QZSSAlmanac QZSS Almanac}
230 */
231 private QZSSAlmanac getAlmanac(final List<Pair<String, String>> entries, final String name) {
232 try {
233 // Initializes almanac
234 final QZSSAlmanac almanac = new QZSSAlmanac(DataContext.getDefault().getTimeScales(),
235 SatelliteSystem.QZSS);
236 almanac.setSource(SOURCE);
237
238 // Initializes checks
239 final boolean[] checks = new boolean[KEY.length];
240 // Loop over entries
241 for (Pair<String, String> entry: entries) {
242 if (entry.getKey().toLowerCase().startsWith(KEY[0])) {
243 // Gets the PRN of the SVN
244 almanac.setPRN(Integer.parseInt(entry.getValue()));
245 checks[0] = true;
246 } else if (entry.getKey().toLowerCase().startsWith(KEY[1])) {
247 // Gets the Health status
248 almanac.setHealth(Integer.parseInt(entry.getValue()));
249 checks[1] = true;
250 } else if (entry.getKey().toLowerCase().startsWith(KEY[2])) {
251 // Gets the eccentricity
252 almanac.setE(Double.parseDouble(entry.getValue()));
253 checks[2] = true;
254 } else if (entry.getKey().toLowerCase().startsWith(KEY[3])) {
255 // Gets the Time of Applicability
256 almanac.setTime(Double.parseDouble(entry.getValue()));
257 checks[3] = true;
258 } else if (entry.getKey().toLowerCase().startsWith(KEY[4])) {
259 // Gets the Inclination
260 almanac.setI0(Double.parseDouble(entry.getValue()));
261 checks[4] = true;
262 } else if (entry.getKey().toLowerCase().startsWith(KEY[5])) {
263 // Gets the Rate of Right Ascension
264 almanac.setOmegaDot(Double.parseDouble(entry.getValue()));
265 checks[5] = true;
266 } else if (entry.getKey().toLowerCase().startsWith(KEY[6])) {
267 // Gets the square root of the semi-major axis
268 almanac.setSqrtA(Double.parseDouble(entry.getValue()));
269 checks[6] = true;
270 } else if (entry.getKey().toLowerCase().startsWith(KEY[7])) {
271 // Gets the Right Ascension of Ascending Node
272 almanac.setOmega0(Double.parseDouble(entry.getValue()));
273 checks[7] = true;
274 } else if (entry.getKey().toLowerCase().startsWith(KEY[8])) {
275 // Gets the Argument of Perigee
276 almanac.setPa(Double.parseDouble(entry.getValue()));
277 checks[8] = true;
278 } else if (entry.getKey().toLowerCase().startsWith(KEY[9])) {
279 // Gets the Mean Anomalie
280 almanac.setM0(Double.parseDouble(entry.getValue()));
281 checks[9] = true;
282 } else if (entry.getKey().toLowerCase().startsWith(KEY[10])) {
283 // Gets the SV clock bias
284 almanac.setAf0(Double.parseDouble(entry.getValue()));
285 checks[10] = true;
286 } else if (entry.getKey().toLowerCase().startsWith(KEY[11])) {
287 // Gets the SV clock Drift
288 almanac.setAf1(Double.parseDouble(entry.getValue()));
289 checks[11] = true;
290 } else if (entry.getKey().toLowerCase().startsWith(KEY[12])) {
291 // Gets the week number
292 almanac.setWeek(Integer.parseInt(entry.getValue()));
293 checks[12] = true;
294 } else {
295 // Unknown entry: the file is not a YUMA file
296 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE, name);
297 }
298 }
299
300 // If all expected fields have been read
301 if (readOK(checks)) {
302 // Returns a QZSSAlmanac built from the entries
303 return almanac;
304 } else {
305 // The file is not a YUMA file
306 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE, name);
307 }
308 } catch (NumberFormatException nfe) {
309 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE, name);
310 }
311 }
312
313 /** Checks if all expected fields have been read.
314 * @param checks flags for read fields
315 * @return true if all expected fields have been read, false if not
316 */
317 private boolean readOK(final boolean[] checks) {
318 for (boolean check: checks) {
319 if (!check) {
320 return false;
321 }
322 }
323 return true;
324 }
325 }
326
327 }