YUMAParser.java

  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.gnss;

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.text.ParseException;
  24. import java.util.ArrayList;
  25. import java.util.List;
  26. import java.util.Locale;
  27. import java.util.regex.Pattern;

  28. import org.hipparchus.util.Pair;
  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.GPSAlmanac;
  37. import org.orekit.time.AbsoluteDate;
  38. import org.orekit.time.GNSSDate;
  39. import org.orekit.time.TimeScales;


  40. /**
  41.  * This class reads Yuma almanac files and provides {@link GPSAlmanac GPS almanacs}.
  42.  *
  43.  * <p>The definition of a Yuma almanac comes from the
  44.  * <a href="http://www.navcen.uscg.gov/?pageName=gpsYuma">U.S. COAST GUARD NAVIGATION CENTER</a>.</p>
  45.  *
  46.  * <p>The format of the files holding Yuma almanacs is not precisely specified,
  47.  * so the parsing rules have been deduced from the downloadable files at
  48.  * <a href="http://www.navcen.uscg.gov/?pageName=gpsAlmanacs">NAVCEN</a>
  49.  * and at <a href="https://celestrak.com/GPS/almanac/Yuma/">CelesTrak</a>.</p>
  50.  *
  51.  * @author Pascal Parraud
  52.  * @since 8.0
  53.  *
  54.  */
  55. public class YUMAParser extends AbstractSelfFeedingLoader implements DataLoader {

  56.     // Constants
  57.     /** The source of the almanacs. */
  58.     private static final String SOURCE = "YUMA";

  59.     /** the useful keys in the YUMA file. */
  60.     private static final String[] KEY = {
  61.         "id", // ID
  62.         "health", // Health
  63.         "eccentricity", // Eccentricity
  64.         "time", // Time of Applicability(s)
  65.         "orbital", // Orbital Inclination(rad)
  66.         "rate", // Rate of Right Ascen(r/s)
  67.         "sqrt", // SQRT(A)  (m 1/2)
  68.         "right", // Right Ascen at Week(rad)
  69.         "argument", // Argument of Perigee(rad)
  70.         "mean", // Mean Anom(rad)
  71.         "af0", // Af0(s)
  72.         "af1", // Af1(s/s)
  73.         "week" // week
  74.     };

  75.     /** Default supported files name pattern. */
  76.     private static final String DEFAULT_SUPPORTED_NAMES = ".*\\.alm$";

  77.     /** Pattern for delimiting regular expressions. */
  78.     private static final Pattern SEPARATOR = Pattern.compile(":");

  79.     // Fields
  80.     /** the list of all the almanacs read from the file. */
  81.     private final List<GPSAlmanac> almanacs;

  82.     /** the list of all the PRN numbers of all the almanacs read from the file. */
  83.     private final List<Integer> prnList;

  84.     /** Set of time scales to use. */
  85.     private final TimeScales timeScales;

  86.     /** Simple constructor.
  87.     *
  88.     * <p>This constructor does not load any data by itself. Data must be loaded
  89.     * later on by calling one of the {@link #loadData() loadData()} method or
  90.     * the {@link #loadData(InputStream, String) loadData(inputStream, fileName)}
  91.     * method.</p>
  92.      *
  93.      * <p>The supported files names are used when getting data from the
  94.      * {@link #loadData() loadData()} method that relies on the
  95.      * {@link DataContext#getDefault() default data context}. They are useless when
  96.      * getting data from the {@link #loadData(InputStream, String) loadData(input, name)}
  97.      * method.</p>
  98.      *
  99.      * @param supportedNames regular expression for supported files names
  100.      * (if null, a default pattern matching files with a ".alm" extension will be used)
  101.      * @see #loadData()
  102.      * @see #YUMAParser(String, DataProvidersManager, TimeScales)
  103.     */
  104.     @DefaultDataContext
  105.     public YUMAParser(final String supportedNames) {
  106.         this(supportedNames,
  107.                 DataContext.getDefault().getDataProvidersManager(),
  108.                 DataContext.getDefault().getTimeScales());
  109.     }

  110.     /**
  111.      * Create a YUMA loader/parser with the given source for YUMA auxiliary data files.
  112.      *
  113.      * <p>This constructor does not load any data by itself. Data must be loaded
  114.      * later on by calling one of the {@link #loadData() loadData()} method or
  115.      * the {@link #loadData(InputStream, String) loadData(inputStream, fileName)}
  116.      * method.</p>
  117.      *
  118.      * <p>The supported files names are used when getting data from the
  119.      * {@link #loadData() loadData()} method that relies on the
  120.      * {@code dataProvidersManager}. They are useless when
  121.      * getting data from the {@link #loadData(InputStream, String) loadData(input, name)}
  122.      * method.</p>
  123.      *
  124.      * @param supportedNames regular expression for supported files names
  125.      * (if null, a default pattern matching files with a ".alm" extension will be used)
  126.      * @param dataProvidersManager provides access to auxiliary data.
  127.      * @param timeScales to use when parsing the GPS dates.
  128.      * @see #loadData()
  129.      * @since 10.1
  130.      */
  131.     public YUMAParser(final String supportedNames,
  132.                       final DataProvidersManager dataProvidersManager,
  133.                       final TimeScales timeScales) {
  134.         super((supportedNames == null) ? DEFAULT_SUPPORTED_NAMES : supportedNames,
  135.                 dataProvidersManager);
  136.         this.almanacs = new ArrayList<>();
  137.         this.prnList = new ArrayList<>();
  138.         this.timeScales = timeScales;
  139.     }

  140.     /**
  141.      * Loads almanacs.
  142.      *
  143.      * <p>The almanacs already loaded in the instance will be discarded
  144.      * and replaced by the newly loaded data.</p>
  145.      * <p>This feature is useful when the file selection is already set up by
  146.      * the {@link DataProvidersManager data providers manager} configuration.</p>
  147.      *
  148.      */
  149.     public void loadData() {
  150.         // load the data from the configured data providers
  151.         feed(this);
  152.         if (almanacs.isEmpty()) {
  153.             throw new OrekitException(OrekitMessages.NO_YUMA_ALMANAC_AVAILABLE);
  154.         }
  155.     }

  156.     @Override
  157.     public void loadData(final InputStream input, final String name)
  158.         throws IOException, ParseException, OrekitException {

  159.         // Clears the lists
  160.         almanacs.clear();
  161.         prnList.clear();

  162.         // Creates the reader
  163.         try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
  164.             // Gathers data to create one GPSAlmanac from 13 consecutive lines
  165.             final List<Pair<String, String>> entries =
  166.                     new ArrayList<>(KEY.length);

  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 = SEPARATOR.split(line.trim());
  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 GPSAlmanac from the entries
  179.                     final GPSAlmanac almanac = getAlmanac(entries, name);
  180.                     // Adds the GPSAlmanac to the list
  181.                     almanacs.add(almanac);
  182.                     // Adds the PRN number of the GPSAlmanac 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(ioe, OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
  190.                                       name);
  191.         }
  192.     }

  193.     @Override
  194.     public boolean stillAcceptsData() {
  195.         return almanacs.isEmpty();
  196.     }

  197.     @Override
  198.     public String getSupportedNames() {
  199.         return super.getSupportedNames();
  200.     }

  201.     /**
  202.      * Gets all the {@link GPSAlmanac GPS almanacs} read from the file.
  203.      *
  204.      * @return the list of {@link GPSAlmanac} from the file
  205.      */
  206.     public List<GPSAlmanac> getAlmanacs() {
  207.         return almanacs;
  208.     }

  209.     /**
  210.      * Gets the PRN numbers of all the {@link GPSAlmanac GPS almanacs} read from the file.
  211.      *
  212.      * @return the PRN numbers of all the {@link GPSAlmanac GPS almanacs} read from the file
  213.      */
  214.     public List<Integer> getPRNNumbers() {
  215.         return prnList;
  216.     }

  217.     /**
  218.      * Builds a {@link GPSAlmanac GPS almanac} from data read in the file.
  219.      *
  220.      * @param entries the data read from the file
  221.      * @param name name of the file
  222.      * @return a {@link GPSAlmanac GPS almanac}
  223.      */
  224.     private GPSAlmanac getAlmanac(final List<Pair<String, String>> entries, final String name) {
  225.         try {
  226.             // Initializes almanac and set the source
  227.             final GPSAlmanac almanac = new GPSAlmanac();
  228.             almanac.setSource(SOURCE);

  229.             // Initializes checks
  230.             final boolean[] checks = new boolean[KEY.length];
  231.             // Loop over entries
  232.             for (Pair<String, String> entry: entries) {
  233.                 final String lowerCaseKey = entry.getKey().toLowerCase(Locale.US);
  234.                 if (lowerCaseKey.startsWith(KEY[0])) {
  235.                     // Gets the PRN of the SVN
  236.                     almanac.setPRN(Integer.parseInt(entry.getValue()));
  237.                     checks[0] = true;
  238.                 } else if (lowerCaseKey.startsWith(KEY[1])) {
  239.                     // Gets the Health status
  240.                     almanac.setHealth(Integer.parseInt(entry.getValue()));
  241.                     checks[1] = true;
  242.                 } else if (lowerCaseKey.startsWith(KEY[2])) {
  243.                     // Gets the eccentricity
  244.                     almanac.setE(Double.parseDouble(entry.getValue()));
  245.                     checks[2] = true;
  246.                 } else if (lowerCaseKey.startsWith(KEY[3])) {
  247.                     // Gets the Time of Applicability
  248.                     almanac.setTime(Double.parseDouble(entry.getValue()));
  249.                     checks[3] = true;
  250.                 } else if (lowerCaseKey.startsWith(KEY[4])) {
  251.                     // Gets the Inclination
  252.                     almanac.setI0(Double.parseDouble(entry.getValue()));
  253.                     checks[4] = true;
  254.                 } else if (lowerCaseKey.startsWith(KEY[5])) {
  255.                     // Gets the Rate of Right Ascension
  256.                     almanac.setOmegaDot(Double.parseDouble(entry.getValue()));
  257.                     checks[5] = true;
  258.                 } else if (lowerCaseKey.startsWith(KEY[6])) {
  259.                     // Gets the square root of the semi-major axis
  260.                     almanac.setSqrtA(Double.parseDouble(entry.getValue()));
  261.                     checks[6] = true;
  262.                 } else if (lowerCaseKey.startsWith(KEY[7])) {
  263.                     // Gets the Right Ascension of Ascending Node
  264.                     almanac.setOmega0(Double.parseDouble(entry.getValue()));
  265.                     checks[7] = true;
  266.                 } else if (lowerCaseKey.startsWith(KEY[8])) {
  267.                     // Gets the Argument of Perigee
  268.                     almanac.setPa(Double.parseDouble(entry.getValue()));
  269.                     checks[8] = true;
  270.                 } else if (lowerCaseKey.startsWith(KEY[9])) {
  271.                     // Gets the Mean Anomalie
  272.                     almanac.setM0(Double.parseDouble(entry.getValue()));
  273.                     checks[9] = true;
  274.                 } else if (lowerCaseKey.startsWith(KEY[10])) {
  275.                     // Gets the SV clock bias
  276.                     almanac.setAf0(Double.parseDouble(entry.getValue()));
  277.                     checks[10] = true;
  278.                 } else if (lowerCaseKey.startsWith(KEY[11])) {
  279.                     // Gets the SV clock Drift
  280.                     almanac.setAf1(Double.parseDouble(entry.getValue()));
  281.                     checks[11] = true;
  282.                 } else if (lowerCaseKey.startsWith(KEY[12])) {
  283.                     // Gets the week number
  284.                     almanac.setWeek(Integer.parseInt(entry.getValue()));
  285.                     checks[12] = true;
  286.                 } else {
  287.                     // Unknown entry: the file is not a YUMA file
  288.                     throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
  289.                                               name);
  290.                 }
  291.             }

  292.             // If all expected fields have been read
  293.             if (readOK(checks)) {
  294.                 // Returns a GPSAlmanac built from the entries
  295.                 final AbsoluteDate date =
  296.                         new GNSSDate(almanac.getWeek(), almanac.getTime() * 1000, SatelliteSystem.GPS, timeScales)
  297.                                 .getDate();
  298.                 almanac.setDate(date);

  299.                 // Add default values to missing keys
  300.                 almanac.setSVN(-1);
  301.                 almanac.setURA(-1);
  302.                 almanac.setSatConfiguration(-1);

  303.                 return almanac;
  304.             } else {
  305.                 // The file is not a YUMA file
  306.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
  307.                                           name);
  308.             }
  309.         } catch (NumberFormatException nfe) {
  310.             throw new OrekitException(nfe, OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
  311.                                       name);
  312.         }
  313.     }

  314.     /** Checks if all expected fields have been read.
  315.      * @param checks flags for read fields
  316.      * @return true if all expected fields have been read, false if not
  317.      */
  318.     private boolean readOK(final boolean[] checks) {
  319.         for (boolean check: checks) {
  320.             if (!check) {
  321.                 return false;
  322.             }
  323.         }
  324.         return true;
  325.     }
  326. }