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 }