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