RinexUtils.java
/* Copyright 2023 Luc Maisonobe
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.files.rinex.utils.parsing;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitInternalError;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.rinex.RinexFile;
import org.orekit.files.rinex.section.RinexBaseHeader;
import org.orekit.files.rinex.section.RinexComment;
import org.orekit.files.rinex.utils.RinexFileType;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.gnss.TimeSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.Month;
import org.orekit.time.TimeComponents;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScales;
/** Utilities for RINEX various messages files.
* @author Luc Maisonobe
* @since 12.0
*
*/
public class RinexUtils {
/** Index of label in header lines. */
public static final int LABEL_INDEX = 60;
/** Pattern for splitting date, time and time zone. */
private static final Pattern SPLITTING_PATTERN = Pattern.compile("([0-9A-Za-z/-]+) *([0-9:]+) *([A-Z][A-Z0-9_-]*)?");
/** Pattern for dates with month abbrevation. */
private static final Pattern DATE_DD_MMM_YY_PATTERN = Pattern.compile("([0-9]{2})-([A-Za-z]{3})-([0-9]{2})");
/** Pattern for dates in ISO-8601 complete representation (basic or extended). */
private static final Pattern DATE_ISO_8601_PATTERN = Pattern.compile("([0-9]{4})-?([0-9]{2})-?([0-9]{2})");
/** Pattern for dates in european format. */
private static final Pattern DATE_EUROPEAN_PATTERN = Pattern.compile("([0-9]{2})/([0-9]{2})/([0-9]{2})");
/** Pattern for time. */
private static final Pattern TIME_PATTERN = Pattern.compile("([0-9]{2}):?([0-9]{2})(?::?([0-9]{2}))?");
/** Private constructor.
* <p>This class is a utility class, it should neither have a public
* nor a default constructor. This private constructor prevents
* the compiler from generating one automatically.</p>
*/
private RinexUtils() {
}
/** Get the trimmed label from a header line.
* @param line header line to parse
* @return trimmed label
*/
public static String getLabel(final String line) {
return line.length() < LABEL_INDEX ? "" : line.substring(LABEL_INDEX).trim();
}
/** Check if a header line matches an expected label.
* @param line header line to check
* @param label expected label
* @return true if line matches expected label
*/
public static boolean matchesLabel(final String line, final String label) {
return getLabel(line).equals(label);
}
/** Parse version, file type and satellite system.
* @param line line to parse
* @param name file name (for error message generation)
* @param header header to fill with parsed data
* @param supportedVersions supported versions
*/
public static void parseVersionFileTypeSatelliteSystem(final String line, final String name,
final RinexBaseHeader header,
final double... supportedVersions) {
// Rinex version
final double parsedVersion = parseDouble(line, 0, 9);
boolean found = false;
for (final double supported : supportedVersions) {
if (FastMath.abs(parsedVersion - supported) < 1.0e-4) {
found = true;
break;
}
}
if (!found) {
final StringBuilder builder = new StringBuilder();
for (final double supported : supportedVersions) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(supported);
}
throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT_VERSION,
parsedVersion, name, builder.toString());
}
header.setFormatVersion(parsedVersion);
// File type
if (header.getFileType() != RinexFileType.parseRinexFileType(parseString(line, 20, 1))) {
throw new OrekitException(OrekitMessages.WRONG_PARSING_TYPE, name);
}
// Satellite system
switch (header.getFileType()) {
case OBSERVATION:
// for observation files, the satellite system is in column 40, and empty defaults to GPS
header.setSatelliteSystem(SatelliteSystem.parseSatelliteSystemWithGPSDefault(parseString(line, 40, 1)));
break;
case NAVIGATION: {
if (header.getFormatVersion() < 3.0) {
// the satellite system is hidden within the entry, with GPS as default
// set up default
header.setSatelliteSystem(SatelliteSystem.GPS);
// look if default is overridden somewhere in the entry
final String entry = parseString(line, 0, LABEL_INDEX).toUpperCase();
for (final SatelliteSystem satelliteSystem : SatelliteSystem.values()) {
if (entry.contains(satelliteSystem.name())) {
// we found a satellite system hidden in the middle of the line
header.setSatelliteSystem(satelliteSystem);
break;
}
}
} else {
// the satellite system is in column 40 for 3.X and later
header.setSatelliteSystem(SatelliteSystem.parseSatelliteSystemWithGPSDefault(parseString(line, 40, 1)));
}
break;
}
default:
// this should never happen
throw new OrekitInternalError(null);
}
}
/** Parse program, run/by and date.
* @param line line to parse
* @param lineNumber line number
* @param name file name (for error message generation)
* @param timeScales the set of time scales used for parsing dates.
* @param header header to fill with parsed data
*/
public static void parseProgramRunByDate(final String line, final int lineNumber,
final String name, final TimeScales timeScales,
final RinexBaseHeader header) {
// Name of the generating program
header.setProgramName(parseString(line, 0, 20));
// Name of the run/by name
header.setRunByName(parseString(line, 20, 20));
// there are several variations for date formatting in the PGM / RUN BY / DATE line
// in versions 2.x, the pattern is expected to be:
// XXRINEXO V9.9 AIUB 24-MAR-01 14:43 PGM / RUN BY / DATE
// however, we have also found this:
// teqc 2016Nov7 root 20180130 10:38:06UTCPGM / RUN BY / DATE
// BJFMTLcsr UTCSR 2007-09-30 05:30:06 PGM / RUN BY / DATE
// NEODIS TAS 27/05/22 10:28 PGM / RUN BY / DATE
// in versions 3.x, the pattern is expected to be:
// sbf2rin-11.3.3 20180130 002558 LCL PGM / RUN BY / DATE
// however, we have also found:
// NetR9 5.03 Receiver Operator 11-JAN-16 00:00:00 PGM / RUN BY / DATE
// so we cannot rely on the format version, we have to check several variations
final Matcher splittingMatcher = SPLITTING_PATTERN.matcher(parseString(line, 40, 20));
if (splittingMatcher.matches()) {
// date part
final DateComponents dc;
final Matcher abbrevMatcher = DATE_DD_MMM_YY_PATTERN.matcher(splittingMatcher.group(1));
if (abbrevMatcher.matches()) {
// hoping this obsolete format will not be used past year 2079…
dc = new DateComponents(convert2DigitsYear(Integer.parseInt(abbrevMatcher.group(3))),
Month.parseMonth(abbrevMatcher.group(2)).getNumber(),
Integer.parseInt(abbrevMatcher.group(1)));
} else {
final Matcher isoMatcher = DATE_ISO_8601_PATTERN.matcher(splittingMatcher.group(1));
if (isoMatcher.matches()) {
dc = new DateComponents(Integer.parseInt(isoMatcher.group(1)),
Integer.parseInt(isoMatcher.group(2)),
Integer.parseInt(isoMatcher.group(3)));
} else {
final Matcher europeanMatcher = DATE_EUROPEAN_PATTERN.matcher(splittingMatcher.group(1));
if (europeanMatcher.matches()) {
dc = new DateComponents(convert2DigitsYear(Integer.parseInt(europeanMatcher.group(3))),
Integer.parseInt(europeanMatcher.group(2)),
Integer.parseInt(europeanMatcher.group(1)));
} else {
dc = null;
}
}
}
// time part
final TimeComponents tc;
final Matcher timeMatcher = TIME_PATTERN.matcher(splittingMatcher.group(2));
if (timeMatcher.matches()) {
tc = new TimeComponents(Integer.parseInt(timeMatcher.group(1)),
Integer.parseInt(timeMatcher.group(2)),
timeMatcher.group(3) != null ? Integer.parseInt(timeMatcher.group(3)) : 0);
} else {
tc = null;
}
// zone part
final String zone = splittingMatcher.groupCount() > 2 ? splittingMatcher.group(3) : "";
if (dc != null && tc != null) {
// we successfully parsed everything
final DateTimeComponents dtc = new DateTimeComponents(dc, tc);
header.setCreationDateComponents(dtc);
final TimeScale timeScale = zone == null ?
timeScales.getUTC() :
TimeSystem.parseTimeSystem(zone).getTimeScale(timeScales);
header.setCreationDate(new AbsoluteDate(dtc, timeScale));
header.setCreationTimeZone(zone);
return;
}
}
// we were not able to extract date
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
lineNumber, name, line);
}
/** Parse a comment.
* @param lineNumber line number
* @param line line to parse
* @param rinexFile rinex file
*/
public static void parseComment(final int lineNumber, final String line, final RinexFile<?> rinexFile) {
rinexFile.addComment(new RinexComment(lineNumber, parseString(line, 0, 60)));
}
/**
* Parse a double value.
* @param line line to parse
* @param startIndex start index
* @param size size of the value
* @return the parsed value
*/
public static double parseDouble(final String line, final int startIndex, final int size) {
final String subString = parseString(line, startIndex, size);
if (subString == null || subString.isEmpty()) {
return Double.NaN;
} else {
return Double.parseDouble(subString.replace('D', 'E').trim());
}
}
/**
* Parse an integer value.
* @param line line to parse
* @param startIndex start index
* @param size size of the value
* @return the parsed value
*/
public static int parseInt(final String line, final int startIndex, final int size) {
final String subString = parseString(line, startIndex, size);
if (subString == null || subString.isEmpty()) {
return 0;
} else {
return Integer.parseInt(subString.trim());
}
}
/**
* Parse a string value.
* @param line line to parse
* @param startIndex start index
* @param size size of the value
* @return the parsed value
*/
public static String parseString(final String line, final int startIndex, final int size) {
if (line.length() > startIndex) {
return line.substring(startIndex, FastMath.min(line.length(), startIndex + size)).trim();
} else {
return null;
}
}
/** Convert a 2 digits year to a complete year.
* @param yy year between 0 and 99
* @return complete year
* @since 12.0
*/
public static int convert2DigitsYear(final int yy) {
return yy >= 80 ? (yy + 1900) : (yy + 2000);
}
}