RinexNavigationParser.java
/* Copyright 2002-2025 CS GROUP
* 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.navigation;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;
import java.util.Collections;
import java.util.InputMismatchException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.rinex.navigation.parsers.ephemeris.GlonassFdmaParser;
import org.orekit.files.rinex.navigation.parsers.RecordLineParser;
import org.orekit.files.rinex.navigation.parsers.ParseInfo;
import org.orekit.files.rinex.section.CommonLabel;
import org.orekit.files.rinex.utils.ParsingUtils;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.GNSSDate;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScales;
import org.orekit.utils.units.Unit;
/**
* Parser for RINEX navigation messages files.
* <p>
* This parser handles RINEX version from 2 to 4.02.
* </p>
* @see <a href="https://files.igs.org/pub/data/format/rinex2.txt">rinex 2.0</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex210.txt">rinex 2.10</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex211.pdf">rinex 2.11</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex301.pdf"> 3.01 navigation messages file format</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex302.pdf"> 3.02 navigation messages file format</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex303.pdf"> 3.03 navigation messages file format</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex304.pdf"> 3.04 navigation messages file format</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex305.pdf"> 3.05 navigation messages file format</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex_4.00.pdf"> 4.00 navigation messages file format</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex_4.01.pdf"> 4.01 navigation messages file format</a>
* @see <a href="https://files.igs.org/pub/data/format/rinex_4.02.pdf"> 4.02 navigation messages file format</a>
*
* @author Bryan Cazabonne
* @since 11.0
*
*/
public class RinexNavigationParser {
/** Converter for positions. */
public static final Unit KM = Unit.KILOMETRE;
/** Converter for velocities. */
public static final Unit KM_PER_S = Unit.parse("km/s");
/** Converter for accelerations. */
public static final Unit KM_PER_S2 = Unit.parse("km/s²");
/** Converter for velocities. */
public static final Unit M_PER_S = Unit.parse("m/s");
/** Converter for clock drift. */
public static final Unit S_PER_S = Unit.parse("s/s");
/** Converter for clock drift rate. */
public static final Unit S_PER_S2 = Unit.parse("s/s²");
/** Converter for ΔUT₁ first derivative. */
public static final Unit S_PER_DAY = Unit.parse("s/d");
/** Converter for ΔUT₁ second derivative. */
public static final Unit S_PER_DAY2 = Unit.parse("s/d²");
/** Converter for square root of semi-major axis. */
public static final Unit SQRT_M = Unit.parse("√m");
/** Converter for angular rates. */
public static final Unit RAD_PER_S = Unit.parse("rad/s");
/** Converter for angular accelerations. */
public static final Unit RAD_PER_S2 = Unit.parse("rad/s²");
/** Converter for rates of small angle. */
public static final Unit AS_PER_DAY = Unit.parse("as/d");
/** Converter for accelerations of small angles. */
public static final Unit AS_PER_DAY2 = Unit.parse("as/d²");
/** Total Electron Content. */
public static final Unit TEC = Unit.TOTAL_ELECTRON_CONTENT_UNIT;
/** System initials. */
private static final String INITIALS = "GRECIJS";
/** Set of time scales. */
private final TimeScales timeScales;
/**
* Constructor.
* <p>This constructor uses the {@link DataContext#getDefault() default data context}.</p>
* @see #RinexNavigationParser(TimeScales)
*
*/
@DefaultDataContext
public RinexNavigationParser() {
this(DataContext.getDefault().getTimeScales());
}
/**
* Constructor.
* @param timeScales the set of time scales used for parsing dates.
*/
public RinexNavigationParser(final TimeScales timeScales) {
this.timeScales = timeScales;
}
/**
* Parse RINEX navigation messages.
* @param source source providing the data to parse
* @return a parsed RINEX navigation messages file
* @throws IOException if {@code reader} throws one
*/
public RinexNavigation parse(final DataSource source) throws IOException {
// initialize internal data structures
final ParseInfo parseInfo = new ParseInfo(source.getName(), timeScales);
Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
try (Reader reader = source.getOpener().openReaderOnce();
BufferedReader br = new BufferedReader(reader)) {
nextLine:
for (String line = br.readLine(); line != null; line = br.readLine()) {
parseInfo.setLine(line);
for (final LineParser candidate : candidateParsers) {
if (candidate.canHandle.test(parseInfo)) {
try {
candidate.parsingMethod.accept(parseInfo);
candidateParsers = candidate.allowedNextProvider.apply(parseInfo);
continue nextLine;
} catch (StringIndexOutOfBoundsException | NumberFormatException | InputMismatchException e) {
throw new OrekitException(e,
OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
parseInfo.getLineNumber(), source.getName(), line);
}
}
}
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
parseInfo.getLineNumber(), source.getName(), line);
}
}
return parseInfo.getCompletedFile();
}
/** Parsers for specific lines. */
private enum LineParser {
/** Parser for version, file type and satellite system. */
HEADER_VERSION(pi -> pi.getHeader().matchFound(CommonLabel.VERSION, pi.getLine()),
pi -> {
pi.getHeader().parseVersionFileTypeSatelliteSystem(pi.getLine(), SatelliteSystem.GPS,
pi.getName(),
2.0, 2.01, 2.10, 2.11,
3.01, 3.02, 3.03, 3.04, 3.05,
4.00, 4.01, 4.02);
pi.setInitialSpaces(pi.getHeader().getFormatVersion() < 3.0 ? 3 : 4);
},
LineParser::headerNext),
/** Parser for generating program and emitting agency. */
HEADER_PROGRAM(pi -> pi.getHeader().matchFound(CommonLabel.PROGRAM, pi.getLine()),
pi -> pi.getHeader().parseProgramRunByDate(pi.getLine(), pi.getTimeScales()),
LineParser::headerNext),
/** Parser for comments. */
HEADER_COMMENT(pi -> pi.getHeader().matchFound(CommonLabel.COMMENT, pi.getLine()),
ParseInfo::parseComment,
LineParser::headerNext),
/** Parser for ionospheric correction parameters. */
HEADER_ION_ALPHA(pi -> pi.getHeader().matchFound(NavigationLabel.ION_ALPHA, pi.getLine()),
pi -> {
pi.setIonosphericCorrectionType(IonosphericCorrectionType.GPS);
// Read coefficients
final double[] parameters = new double[4];
parameters[0] = ParsingUtils.parseDouble(pi.getLine(), 2, 12);
parameters[1] = ParsingUtils.parseDouble(pi.getLine(), 14, 12);
parameters[2] = ParsingUtils.parseDouble(pi.getLine(), 26, 12);
parameters[3] = ParsingUtils.parseDouble(pi.getLine(), 38, 12);
pi.setKlobucharAlpha(parameters);
},
LineParser::headerNext),
/** Parser for ionospheric correction parameters. */
HEADER_ION_BETA(pi -> pi.getHeader().matchFound(NavigationLabel.ION_BETA, pi.getLine()),
pi -> {
pi.setIonosphericCorrectionType(IonosphericCorrectionType.GPS);
// Read coefficients
final double[] parameters = new double[4];
parameters[0] = ParsingUtils.parseDouble(pi.getLine(), 2, 12);
parameters[1] = ParsingUtils.parseDouble(pi.getLine(), 14, 12);
parameters[2] = ParsingUtils.parseDouble(pi.getLine(), 26, 12);
parameters[3] = ParsingUtils.parseDouble(pi.getLine(), 38, 12);
pi.setKlobucharBeta(parameters);
},
LineParser::headerNext),
/** Parser for ionospheric correction parameters. */
HEADER_IONOSPHERIC(pi -> pi.getHeader().matchFound(NavigationLabel.IONOSPHERIC_CORR, pi.getLine()),
pi -> {
// ionospheric correction type
final IonosphericCorrectionType ionoType =
IonosphericCorrectionType.valueOf(ParsingUtils.parseString(pi.getLine(), 0, 3));
pi.setIonosphericCorrectionType(ionoType);
pi.setTimeMark(pi.getLine().charAt(54));
if (ionoType == IonosphericCorrectionType.GAL) {
// We are parsing Galileo NeQuick G ionospheric parameters
pi.setNeQuickAlpha(new double[] {
ParsingUtils.parseDouble(pi.getLine(), 5, 12),
ParsingUtils.parseDouble(pi.getLine(), 17, 12),
ParsingUtils.parseDouble(pi.getLine(), 29, 12)
});
} else {
// We are parsing Klobuchar ionospheric parameters
final double[] parameters = new double[] {
ParsingUtils.parseDouble(pi.getLine(), 5, 12),
ParsingUtils.parseDouble(pi.getLine(), 17, 12),
ParsingUtils.parseDouble(pi.getLine(), 29, 12),
ParsingUtils.parseDouble(pi.getLine(), 41, 12)
};
if (pi.getLine().charAt(3) == 'A') {
// Ionospheric α parameters
pi.setKlobucharAlpha(parameters);
} else {
// Ionospheric β parameters
pi.setKlobucharBeta(parameters);
}
}
},
LineParser::headerNext),
/** Parser for corrections to transform the system time to UTC or to other time systems. */
HEADER_DELTA_UTC(pi -> pi.getHeader().matchFound(NavigationLabel.DELTA_UTC, pi.getLine()),
pi -> {
// Read fields
final double a0 = ParsingUtils.parseDouble(pi.getLine(), 3, 19);
final double a1 = ParsingUtils.parseDouble(pi.getLine(), 22, 19);
final int refTime = ParsingUtils.parseInt(pi.getLine(), 41, 9);
final int refWeek = ParsingUtils.parseInt(pi.getLine(), 50, 9);
// convert date
final SatelliteSystem satSystem = pi.getHeader().getSatelliteSystem();
final AbsoluteDate date = new GNSSDate(refWeek, refTime, satSystem, pi.getTimeScales()).getDate();
// Add to the list
final TimeSystemCorrection tsc = new TimeSystemCorrection("GPUT", date, a0, a1, "", 0);
pi.getHeader().addTimeSystemCorrections(tsc);
},
LineParser::headerNext),
/** Parser for corrections to transform the GLONASS system time to UTC or to other time systems. */
HEADER_CORR_SYSTEM_TIME(pi -> pi.getHeader().matchFound(NavigationLabel.CORR_TO_SYSTEM_TIME, pi.getLine()),
pi -> {
// Read fields
final int year = ParsingUtils.parseInt(pi.getLine(), 0, 6);
final int month = ParsingUtils.parseInt(pi.getLine(), 6, 6);
final int day = ParsingUtils.parseInt(pi.getLine(), 12, 6);
final double minusTau = ParsingUtils.parseDouble(pi.getLine(), 21, 19);
// convert date
final SatelliteSystem satSystem = SatelliteSystem.GLONASS;
final TimeScale timeScale = satSystem.getObservationTimeScale().getTimeScale(pi.getTimeScales());
final AbsoluteDate date = new AbsoluteDate(year, month, day, timeScale);
// Add to the list
final TimeSystemCorrection tsc = new TimeSystemCorrection("GLUT", date, minusTau, 0.0, "", 0);
pi.getHeader().addTimeSystemCorrections(tsc);
},
LineParser::headerNext),
/** Parser for corrections to transform the system time to UTC or to other time systems. */
HEADER_TIME(pi -> pi.getHeader().matchFound(NavigationLabel.TIME_SYSTEM_CORR, pi.getLine()),
pi -> {
// Read fields
final String type = ParsingUtils.parseString(pi.getLine(), 0, 4);
final double a0 = ParsingUtils.parseDouble(pi.getLine(), 5, 17);
final double a1 = ParsingUtils.parseDouble(pi.getLine(), 22, 16);
final int refTime = ParsingUtils.parseInt(pi.getLine(), 38, 7);
final int refWeek = ParsingUtils.parseInt(pi.getLine(), 46, 5);
final String satId = ParsingUtils.parseString(pi.getLine(), 51, 5);
final int utcId = ParsingUtils.parseInt(pi.getLine(), 57, 2);
// convert date
final SatelliteSystem satSystem = pi.getHeader().getSatelliteSystem();
final AbsoluteDate date;
if (satSystem == SatelliteSystem.GLONASS) {
date = null;
} else if (satSystem == SatelliteSystem.BEIDOU) {
date = new GNSSDate(refWeek, refTime, satSystem, pi.getTimeScales()).getDate();
} else {
// all other systems are converted to GPS week in Rinex files!
date = new GNSSDate(refWeek, refTime, SatelliteSystem.GPS, pi.getTimeScales()).getDate();
}
// Add to the list
final TimeSystemCorrection tsc = new TimeSystemCorrection(type, date, a0, a1, satId, utcId);
pi.getHeader().addTimeSystemCorrections(tsc);
},
LineParser::headerNext),
/** Parser for leap seconds. */
HEADER_LEAP_SECONDS(pi -> pi.getHeader().matchFound(CommonLabel.LEAP_SECONDS, pi.getLine()),
pi -> {
pi.getHeader().setLeapSecondsGNSS(ParsingUtils.parseInt(pi.getLine(), 0, 6));
pi.getHeader().setLeapSecondsFuture(ParsingUtils.parseInt(pi.getLine(), 6, 6));
pi.getHeader().setLeapSecondsWeekNum(ParsingUtils.parseInt(pi.getLine(), 12, 6));
pi.getHeader().setLeapSecondsDayNum(ParsingUtils.parseInt(pi.getLine(), 18, 6));
},
LineParser::headerNext),
/** Parser for DOI.
* @since 12.0
*/
HEADER_DOI(pi -> pi.getHeader().matchFound(CommonLabel.DOI, pi.getLine()),
pi -> pi.getHeader().
setDoi(ParsingUtils.parseString(pi.getLine(), 0, pi.getHeader().getLabelIndex())),
LineParser::headerNext),
/** Parser for license.
* @since 12.0
*/
HEADER_LICENSE(pi -> pi.getHeader().matchFound(CommonLabel.LICENSE, pi.getLine()),
pi -> pi.getHeader().
setLicense(ParsingUtils.parseString(pi.getLine(), 0, pi.getHeader().getLabelIndex())),
LineParser::headerNext),
/** Parser for stationInformation.
* @since 12.0
*/
HEADER_STATION_INFORMATION(pi -> pi.getHeader().matchFound(CommonLabel.STATION_INFORMATION, pi.getLine()),
pi -> pi.getHeader().
setStationInformation(ParsingUtils.parseString(pi.getLine(), 0, pi.getHeader().getLabelIndex())),
LineParser::headerNext),
/** Parser for merged files.
* @since 12.0
*/
HEADER_MERGED_FILE(pi -> pi.getHeader().matchFound(NavigationLabel.MERGED_FILE, pi.getLine()),
pi -> pi.getHeader().setMergedFiles(ParsingUtils.parseInt(pi.getLine(), 0, 9)),
LineParser::headerNext),
/** Parser for the end of pi.getHeader(). */
HEADER_END(pi -> pi.getHeader().matchFound(CommonLabel.END, pi.getLine()),
pi -> {
// get rinex format version
final RinexNavigationHeader header = pi.getHeader();
final double version = pi.getHeader().getFormatVersion();
// check mandatory header fields
if (header.getRunByName() == null ||
version >= 4 && pi.getHeader().getLeapSecondsGNSS() < 0) {
throw new OrekitException(OrekitMessages.INCOMPLETE_HEADER, pi.getName());
}
pi.setHeaderParsed(true);
},
LineParser::navigationNext),
/** Parser for navigation message space vehicle epoch and clock. */
NAVIGATION_SV_EPOCH_CLOCK_RINEX_2(pi -> true,
pi -> {
pi.setRecordLineParser(RecordType.ORBIT,
pi.getHeader().getSatelliteSystem(),
ParsingUtils.parseInt(pi.getLine(), 0, 2),
null, null);
pi.getRecordLineParser().parseLine00();
},
LineParser::navigationNext),
/** Parser for line 0 of ephemeris records. */
EPH_LINE_00(pi -> INITIALS.indexOf(pi.getLine().charAt(0)) >= 0,
pi -> {
if (pi.getHeader().getFormatVersion() < 4) {
final SatelliteSystem system =
SatelliteSystem.parseSatelliteSystem(ParsingUtils.parseString(pi.getLine(), 0, 1));
final int prn = ParsingUtils.parseInt(pi.getLine(), 2, 2);
pi.setRecordLineParser(RecordType.ORBIT, system, prn, null, null);
}
// Read first line
pi.getRecordLineParser().parseLine00();
},
LineParser::navigationNext),
/** Parser for line 0 of other records. */
RECORD_LINE_00(pi -> pi.getLine().charAt(0) == ' ',
pi -> pi.getRecordLineParser().parseLine00(),
LineParser::navigationNext),
/** Parser for message lines. */
MESSAGE_LINE(pi -> RecordType.ORBIT.matches(pi.getLine()),
ParseInfo::parseRecordLine,
LineParser::navigationNext),
/** Parser for navigation message type. */
EPH_TYPE(pi -> RecordType.EPH.matches(pi.getLine()),
pi -> pi.setRecordLineParser(RecordType.ORBIT),
pi -> Collections.singleton(EPH_LINE_00)),
/** Parser for system time offset message type. */
STO_TYPE(pi -> RecordType.STO.matches(pi.getLine()),
pi -> pi.setRecordLineParser(RecordType.STO),
pi -> Collections.singleton(RECORD_LINE_00)),
/** Parser for Earth orientation parameter message type. */
EOP_TYPE(pi -> RecordType.EOP.matches(pi.getLine()),
pi -> pi.setRecordLineParser(RecordType.EOP),
pi -> Collections.singleton(RECORD_LINE_00)),
/** Parser for ionosphere message type. */
IONO_TYPE(pi -> RecordType.ION.matches(pi.getLine()),
pi -> {
final SatelliteSystem system =
SatelliteSystem.parseSatelliteSystem(ParsingUtils.parseString(pi.getLine(), 6, 1));
final int prn = ParsingUtils.parseInt(pi.getLine(), 7, 2);
final String type = ParsingUtils.parseString(pi.getLine(), 10, 4);
final String subtype = ParsingUtils.parseString(pi.getLine(), 15, 4);
pi.setRecordLineParser(RecordType.ION, system, prn, type, subtype);
},
pi -> Collections.singleton(RECORD_LINE_00));
/** Predicate for identifying lines that can be parsed. */
private final Predicate<ParseInfo> canHandle;
/** Parsing method. */
private final Consumer<ParseInfo> parsingMethod;
/** Provider for next line parsers. */
private final Function<ParseInfo, Iterable<LineParser>> allowedNextProvider;
/** Simple constructor.
* @param canHandle predicate for identifying lines that can be parsed
* @param parsingMethod parsing method
* @param allowedNextProvider supplier for allowed parsers for next line
*/
LineParser(final Predicate<ParseInfo> canHandle,
final Consumer<ParseInfo> parsingMethod,
final Function<ParseInfo, Iterable<LineParser>> allowedNextProvider) {
this.canHandle = canHandle;
this.parsingMethod = parsingMethod;
this.allowedNextProvider = allowedNextProvider;
}
/** Get the allowed parsers for next lines while parsing Rinex pi.getHeader().
* @param parseInfo holder for transient data
* @return allowed parsers for next line
*/
private static Iterable<LineParser> headerNext(final ParseInfo parseInfo) {
if (parseInfo.getHeader().getFormatVersion() < 3) {
// Rinex 2.x header entries
return Arrays.asList(HEADER_COMMENT, HEADER_PROGRAM,
HEADER_ION_ALPHA, HEADER_ION_BETA,
HEADER_DELTA_UTC, HEADER_CORR_SYSTEM_TIME,
HEADER_LEAP_SECONDS, HEADER_END);
} else if (parseInfo.getHeader().getFormatVersion() < 4) {
// Rinex 3.x header entries
return Arrays.asList(HEADER_COMMENT, HEADER_PROGRAM,
HEADER_IONOSPHERIC, HEADER_TIME,
HEADER_LEAP_SECONDS, HEADER_END);
} else {
// Rinex 4.x header entries
return Arrays.asList(HEADER_COMMENT, HEADER_PROGRAM,
HEADER_DOI, HEADER_LICENSE, HEADER_STATION_INFORMATION, HEADER_MERGED_FILE,
HEADER_LEAP_SECONDS, HEADER_END);
}
}
/** Get the allowed parsers for next lines while parsing navigation date.
* @param parseInfo holder for transient data
* @return allowed parsers for next line
*/
private static Iterable<LineParser> navigationNext(final ParseInfo parseInfo) {
final RecordLineParser mlp = parseInfo.getRecordLineParser();
if (mlp != null) {
if (mlp instanceof GlonassFdmaParser) {
// workaround for some invalid files that should nevertheless be parsed
// we have encountered in the wild merged files that claimed to be in 3.05 version
// and hence needed at least 4 broadcast GLONASS orbit lines (the fourth line was
// introduced in 3.05), but in fact only had 3 broadcast lines. We think they were
// merged from files in 3.04 or earlier format. In order to parse these files,
// we accept after the third line either another broadcast orbit line or a new message
if (parseInfo.getRecordLineNumber() < 3) {
return Collections.singleton(MESSAGE_LINE);
} else {
if (parseInfo.getHeader().getFormatVersion() < 4) {
return Arrays.asList(MESSAGE_LINE, EPH_LINE_00);
} else {
return Arrays.asList(MESSAGE_LINE, EPH_TYPE, STO_TYPE, EOP_TYPE, IONO_TYPE);
}
}
} else {
return Collections.singleton(MESSAGE_LINE);
}
} else if (parseInfo.getHeader().getFormatVersion() < 3) {
return Collections.singleton(NAVIGATION_SV_EPOCH_CLOCK_RINEX_2);
} else if (parseInfo.getHeader().getFormatVersion() < 4) {
return Collections.singleton(EPH_LINE_00);
} else {
return Arrays.asList(EPH_TYPE, STO_TYPE, EOP_TYPE, IONO_TYPE);
}
}
}
}