RinexUtils.java

  1. /* Copyright 2022-2025 Luc Maisonobe
  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.files.rinex.utils.parsing;

  18. import java.util.regex.Matcher;
  19. import java.util.regex.Pattern;

  20. import org.hipparchus.util.FastMath;
  21. import org.orekit.errors.OrekitException;
  22. import org.orekit.errors.OrekitInternalError;
  23. import org.orekit.errors.OrekitMessages;
  24. import org.orekit.files.rinex.RinexFile;
  25. import org.orekit.files.rinex.section.RinexBaseHeader;
  26. import org.orekit.files.rinex.section.RinexComment;
  27. import org.orekit.files.rinex.utils.RinexFileType;
  28. import org.orekit.gnss.SatelliteSystem;
  29. import org.orekit.gnss.TimeSystem;
  30. import org.orekit.time.AbsoluteDate;
  31. import org.orekit.time.DateComponents;
  32. import org.orekit.time.DateTimeComponents;
  33. import org.orekit.time.Month;
  34. import org.orekit.time.TimeComponents;
  35. import org.orekit.time.TimeScale;
  36. import org.orekit.time.TimeScales;

  37. /** Utilities for RINEX various messages files.
  38.  * @author Luc Maisonobe
  39.  * @since 12.0
  40.  *
  41.  */
  42. public class RinexUtils {

  43.     /** Index of label in header lines. */
  44.     public static final int LABEL_INDEX = 60;

  45.     /** Pattern for splitting date, time and time zone. */
  46.     private static final Pattern SPLITTING_PATTERN = Pattern.compile("([0-9A-Za-z/-]+) *([0-9:]+) *([A-Z][A-Z0-9_-]*)?");

  47.     /** Pattern for dates with month abbrevation. */
  48.     private static final Pattern DATE_DD_MMM_YY_PATTERN = Pattern.compile("([0-9]{2})-([A-Za-z]{3})-([0-9]{2})");

  49.     /** Pattern for dates in ISO-8601 complete representation (basic or extended). */
  50.     private static final Pattern DATE_ISO_8601_PATTERN = Pattern.compile("([0-9]{4})-?([0-9]{2})-?([0-9]{2})");

  51.     /** Pattern for dates in european format. */
  52.     private static final Pattern DATE_EUROPEAN_PATTERN = Pattern.compile("([0-9]{2})/([0-9]{2})/([0-9]{2})");

  53.     /** Pattern for time. */
  54.     private static final Pattern TIME_PATTERN = Pattern.compile("([0-9]{2}):?([0-9]{2})(?::?([0-9]{2}))?");

  55.     /** Private constructor.
  56.      * <p>This class is a utility class, it should neither have a public
  57.      * nor a default constructor. This private constructor prevents
  58.      * the compiler from generating one automatically.</p>
  59.      */
  60.     private RinexUtils() {
  61.     }

  62.     /** Get the trimmed label from a header line.
  63.      * @param line header line to parse
  64.      * @return trimmed label
  65.      */
  66.     public static String getLabel(final String line) {
  67.         return line.length() < LABEL_INDEX ? "" : line.substring(LABEL_INDEX).trim();
  68.     }

  69.     /** Check if a header line matches an expected label.
  70.      * @param line header line to check
  71.      * @param label expected label
  72.      * @return true if line matches expected label
  73.      */
  74.     public static boolean matchesLabel(final String line, final String label) {
  75.         return getLabel(line).equals(label);
  76.     }

  77.     /** Parse version, file type and satellite system.
  78.      * @param line line to parse
  79.      * @param name file name (for error message generation)
  80.      * @param header header to fill with parsed data
  81.      * @param supportedVersions supported versions
  82.      */
  83.     public static void parseVersionFileTypeSatelliteSystem(final String line, final String name,
  84.                                                            final RinexBaseHeader header,
  85.                                                            final double... supportedVersions) {

  86.         // Rinex version
  87.         final double parsedVersion = parseDouble(line, 0, 9);

  88.         boolean found = false;
  89.         for (final double supported : supportedVersions) {
  90.             if (FastMath.abs(parsedVersion - supported) < 1.0e-4) {
  91.                 found = true;
  92.                 break;
  93.             }
  94.         }
  95.         if (!found) {
  96.             final StringBuilder builder = new StringBuilder();
  97.             for (final double supported : supportedVersions) {
  98.                 if (builder.length() > 0) {
  99.                     builder.append(", ");
  100.                 }
  101.                 builder.append(supported);
  102.             }
  103.             throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT_VERSION,
  104.                                       parsedVersion, name, builder.toString());
  105.         }
  106.         header.setFormatVersion(parsedVersion);

  107.         // File type
  108.         if (header.getFileType() != RinexFileType.parseRinexFileType(parseString(line, 20, 1))) {
  109.             throw new OrekitException(OrekitMessages.WRONG_PARSING_TYPE, name);
  110.         }

  111.         // Satellite system
  112.         switch (header.getFileType()) {
  113.             case OBSERVATION:
  114.                 // for observation files, the satellite system is in column 40, and empty defaults to GPS
  115.                 header.setSatelliteSystem(SatelliteSystem.parseSatelliteSystemWithGPSDefault(parseString(line, 40, 1)));
  116.                 break;
  117.             case NAVIGATION: {
  118.                 if (header.getFormatVersion() < 3.0) {
  119.                     // the satellite system is hidden within the entry, with GPS as default

  120.                     // set up default
  121.                     header.setSatelliteSystem(SatelliteSystem.GPS);

  122.                     // look if default is overridden somewhere in the entry
  123.                     final String entry = parseString(line, 0, LABEL_INDEX).toUpperCase();
  124.                     for (final SatelliteSystem satelliteSystem : SatelliteSystem.values()) {
  125.                         if (entry.contains(satelliteSystem.name())) {
  126.                             // we found a satellite system hidden in the middle of the line
  127.                             header.setSatelliteSystem(satelliteSystem);
  128.                             break;
  129.                         }
  130.                     }

  131.                 } else {
  132.                     // the satellite system is in column 40 for 3.X and later
  133.                     header.setSatelliteSystem(SatelliteSystem.parseSatelliteSystemWithGPSDefault(parseString(line, 40, 1)));
  134.                 }
  135.                 break;
  136.             }
  137.             default:
  138.                 //  this should never happen
  139.                 throw new OrekitInternalError(null);
  140.         }

  141.     }

  142.     /** Parse program, run/by and date.
  143.      * @param line line to parse
  144.      * @param lineNumber line number
  145.      * @param name file name (for error message generation)
  146.      * @param timeScales the set of time scales used for parsing dates.
  147.      * @param header header to fill with parsed data
  148.      */
  149.     public static void parseProgramRunByDate(final String line, final int lineNumber,
  150.                                              final String name, final TimeScales timeScales,
  151.                                              final RinexBaseHeader header) {

  152.         // Name of the generating program
  153.         header.setProgramName(parseString(line, 0, 20));

  154.         // Name of the run/by name
  155.         header.setRunByName(parseString(line, 20, 20));

  156.         // there are several variations for date formatting in the PGM / RUN BY / DATE line

  157.         // in versions 2.x, the pattern is expected to be:
  158.         // XXRINEXO V9.9       AIUB                24-MAR-01 14:43     PGM / RUN BY / DATE
  159.         // however, we have also found this:
  160.         // teqc  2016Nov7      root                20180130 10:38:06UTCPGM / RUN BY / DATE
  161.         // BJFMTLcsr           UTCSR               2007-09-30 05:30:06 PGM / RUN BY / DATE
  162.         // NEODIS              TAS                 27/05/22 10:28      PGM / RUN BY / DATE

  163.         // in versions 3.x, the pattern is expected to be:
  164.         // sbf2rin-11.3.3                          20180130 002558 LCL PGM / RUN BY / DATE
  165.         // however, we have also found:
  166.         // NetR9 5.03          Receiver Operator   11-JAN-16 00:00:00  PGM / RUN BY / DATE

  167.         // so we cannot rely on the format version, we have to check several variations
  168.         final Matcher splittingMatcher = SPLITTING_PATTERN.matcher(parseString(line, 40, 20));
  169.         if (splittingMatcher.matches()) {

  170.             // date part
  171.             final DateComponents dc;
  172.             final Matcher abbrevMatcher = DATE_DD_MMM_YY_PATTERN.matcher(splittingMatcher.group(1));
  173.             if (abbrevMatcher.matches()) {
  174.                 // hoping this obsolete format will not be used past year 2079…
  175.                 dc = new DateComponents(convert2DigitsYear(Integer.parseInt(abbrevMatcher.group(3))),
  176.                                         Month.parseMonth(abbrevMatcher.group(2)).getNumber(),
  177.                                         Integer.parseInt(abbrevMatcher.group(1)));
  178.             } else {
  179.                 final Matcher isoMatcher = DATE_ISO_8601_PATTERN.matcher(splittingMatcher.group(1));
  180.                 if (isoMatcher.matches()) {
  181.                     dc = new DateComponents(Integer.parseInt(isoMatcher.group(1)),
  182.                                             Integer.parseInt(isoMatcher.group(2)),
  183.                                             Integer.parseInt(isoMatcher.group(3)));
  184.                 } else {
  185.                     final Matcher europeanMatcher = DATE_EUROPEAN_PATTERN.matcher(splittingMatcher.group(1));
  186.                     if (europeanMatcher.matches()) {
  187.                         dc = new DateComponents(convert2DigitsYear(Integer.parseInt(europeanMatcher.group(3))),
  188.                                                 Integer.parseInt(europeanMatcher.group(2)),
  189.                                                 Integer.parseInt(europeanMatcher.group(1)));
  190.                     } else {
  191.                         dc = null;
  192.                     }
  193.                 }
  194.             }

  195.             // time part
  196.             final TimeComponents tc;
  197.             final Matcher timeMatcher = TIME_PATTERN.matcher(splittingMatcher.group(2));
  198.             if (timeMatcher.matches()) {
  199.                 tc = new TimeComponents(Integer.parseInt(timeMatcher.group(1)),
  200.                                         Integer.parseInt(timeMatcher.group(2)),
  201.                                         timeMatcher.group(3) != null ? Integer.parseInt(timeMatcher.group(3)) : 0);
  202.             } else {
  203.                 tc = null;
  204.             }

  205.             // zone part
  206.             final String zone = splittingMatcher.groupCount() > 2 ? splittingMatcher.group(3) : "";

  207.             if (dc != null && tc != null) {
  208.                 // we successfully parsed everything
  209.                 final DateTimeComponents dtc = new DateTimeComponents(dc, tc);
  210.                 header.setCreationDateComponents(dtc);
  211.                 final TimeScale timeScale = zone == null ?
  212.                                             timeScales.getUTC() :
  213.                                             TimeSystem.parseTimeSystem(zone).getTimeScale(timeScales);
  214.                 header.setCreationDate(new AbsoluteDate(dtc, timeScale));
  215.                 header.setCreationTimeZone(zone);
  216.                 return;
  217.             }

  218.         }

  219.         // we were not able to extract date
  220.         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  221.                                       lineNumber, name, line);

  222.     }

  223.     /** Parse a comment.
  224.      * @param lineNumber line number
  225.      * @param line line to parse
  226.      * @param rinexFile rinex file
  227.      */
  228.     public static void parseComment(final int lineNumber, final String line, final RinexFile<?> rinexFile) {
  229.         rinexFile.addComment(new RinexComment(lineNumber, parseString(line, 0, 60)));
  230.     }

  231.     /**
  232.      * Parse a double value.
  233.      * @param line line to parse
  234.      * @param startIndex start index
  235.      * @param size size of the value
  236.      * @return the parsed value
  237.      */
  238.     public static double parseDouble(final String line, final int startIndex, final int size) {
  239.         final String subString = parseString(line, startIndex, size);
  240.         if (subString == null || subString.isEmpty()) {
  241.             return Double.NaN;
  242.         } else {
  243.             return Double.parseDouble(subString.replace('D', 'E').trim());
  244.         }
  245.     }

  246.     /**
  247.      * Parse an integer value.
  248.      * @param line line to parse
  249.      * @param startIndex start index
  250.      * @param size size of the value
  251.      * @return the parsed value
  252.      */
  253.     public static int parseInt(final String line, final int startIndex, final int size) {
  254.         final String subString = parseString(line, startIndex, size);
  255.         if (subString == null || subString.isEmpty()) {
  256.             return 0;
  257.         } else {
  258.             return Integer.parseInt(subString.trim());
  259.         }
  260.     }

  261.     /**
  262.      * Parse a string value.
  263.      * @param line line to parse
  264.      * @param startIndex start index
  265.      * @param size size of the value
  266.      * @return the parsed value
  267.      */
  268.     public static String parseString(final String line, final int startIndex, final int size) {
  269.         if (line.length() > startIndex) {
  270.             return line.substring(startIndex, FastMath.min(line.length(), startIndex + size)).trim();
  271.         } else {
  272.             return null;
  273.         }
  274.     }

  275.     /** Convert a 2 digits year to a complete year.
  276.      * @param yy year between 0 and 99
  277.      * @return complete year
  278.      * @since 12.0
  279.      */
  280.     public static int convert2DigitsYear(final int yy) {
  281.         return yy >= 80 ? (yy + 1900) : (yy + 2000);
  282.     }

  283. }