YUMAParser.java

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


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

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

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

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

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

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

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

  82.     /** Set of time scales to use. */
  83.     private final TimeScales timeScales;

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

  108.     /**
  109.      * Create a YUMA loader/parser with the given source for YUMA 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 ".alm" 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 YUMAParser(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.      * Loads almanacs.
  140.      *
  141.      * <p>The almanacs already loaded in the instance will be discarded
  142.      * and replaced by the newly loaded data.</p>
  143.      * <p>This feature is useful when the file selection is already set up by
  144.      * the {@link DataProvidersManager data providers manager} configuration.</p>
  145.      *
  146.      */
  147.     public void loadData() {
  148.         // load the data from the configured data providers
  149.         feed(this);
  150.         if (almanacs.isEmpty()) {
  151.             throw new OrekitException(OrekitMessages.NO_YUMA_ALMANAC_AVAILABLE);
  152.         }
  153.     }

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

  157.         // Clears the lists
  158.         almanacs.clear();
  159.         prnList.clear();

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

  165.             // Reads the data one line at a time
  166.             for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  167.                 // Try to split the line into 2 tokens as key:value
  168.                 final String[] token = SEPARATOR.split(line.trim());
  169.                 // If the line is made of 2 tokens
  170.                 if (token.length == 2) {
  171.                     // Adds these tokens as an entry to the entries
  172.                     entries.add(new Pair<>(token[0].trim(), token[1].trim()));
  173.                 }
  174.                 // If the number of entries equals the expected number
  175.                 if (entries.size() == KEY.length) {
  176.                     // Gets a GPSAlmanac from the entries
  177.                     final GPSAlmanac almanac = getAlmanac(entries, name);
  178.                     // Adds the GPSAlmanac to the list
  179.                     almanacs.add(almanac);
  180.                     // Adds the PRN number of the GPSAlmanac to the list
  181.                     prnList.add(almanac.getPRN());
  182.                     // Clears the entries
  183.                     entries.clear();
  184.                 }
  185.             }
  186.         } catch (IOException ioe) {
  187.             throw new OrekitException(ioe, OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
  188.                                       name);
  189.         }
  190.     }

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

  195.     @Override
  196.     public String getSupportedNames() {
  197.         return super.getSupportedNames();
  198.     }

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

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

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

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

  290.             // If all expected fields have been read
  291.             if (readOK(checks)) {

  292.                 // Add default values to missing keys
  293.                 almanac.setSVN(-1);
  294.                 almanac.setURA(-1);
  295.                 almanac.setSatConfiguration(-1);

  296.                 return almanac;
  297.             } else {
  298.                 // The file is not a YUMA file
  299.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
  300.                                           name);
  301.             }
  302.         } catch (NumberFormatException nfe) {
  303.             throw new OrekitException(nfe, OrekitMessages.NOT_A_SUPPORTED_YUMA_ALMANAC_FILE,
  304.                                       name);
  305.         }
  306.     }

  307.     /** Checks if all expected fields have been read.
  308.      * @param checks flags for read fields
  309.      * @return true if all expected fields have been read, false if not
  310.      */
  311.     private boolean readOK(final boolean[] checks) {
  312.         for (boolean check: checks) {
  313.             if (!check) {
  314.                 return false;
  315.             }
  316.         }
  317.         return true;
  318.     }
  319. }