SP3Parser.java
/* Copyright 2002-2012 Space Applications Services
* Licensed to CS Systèmes d'Information (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.sp3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Locale;
import java.util.Scanner;
import java.util.function.Function;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.general.EphemerisFileParser;
import org.orekit.files.sp3.SP3File.SP3Coordinate;
import org.orekit.files.sp3.SP3File.SP3FileType;
import org.orekit.files.sp3.SP3File.SP3OrbitType;
import org.orekit.files.sp3.SP3File.TimeSystem;
import org.orekit.frames.Frame;
import org.orekit.frames.FramesFactory;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScalesFactory;
import org.orekit.utils.CartesianDerivativesFilter;
import org.orekit.utils.Constants;
import org.orekit.utils.IERSConventions;
/** A parser for the SP3 orbit file format. It supports the original format as
* well as the latest SP3-c version.
* <p>
* <b>Note:</b> this parser is thread-safe, so calling {@link #parse} from
* different threads is allowed.
* </p>
* @see <a href="http://igscb.jpl.nasa.gov/igscb/data/format/sp3_docu.txt">SP3-a file format</a>
* @see <a href="http://igscb.jpl.nasa.gov/igscb/data/format/sp3c.txt">SP3-c file format</a>
* @author Thomas Neidhart
*/
public class SP3Parser implements EphemerisFileParser {
/** Standard gravitational parameter in m^3 / s^2. */
private final double mu;
/** Number of data points to use in interpolation. */
private final int interpolationSamples;
/** Mapping from frame identifier in the file to a {@link Frame}. */
private final Function<? super String, ? extends Frame> frameBuilder;
/**
* Create an SP3 parser using default values.
*
* @see #SP3Parser(double, int, Function)
*/
public SP3Parser() {
this(Constants.EIGEN5C_EARTH_MU, 7, SP3Parser::guessFrame);
}
/**
* Create an SP3 parser and specify the extra information needed to create a {@link
* org.orekit.propagation.Propagator Propagator} from the ephemeris data.
*
* @param mu is the standard gravitational parameter to use for
* creating {@link org.orekit.orbits.Orbit Orbits} from
* the ephemeris data. See {@link Constants}.
* @param interpolationSamples is the number of samples to use when interpolating.
* @param frameBuilder is a function that can construct a frame from an SP3
* coordinate system string. The coordinate system can be
* any 5 character string e.g. ITR92, IGb08.
*/
public SP3Parser(final double mu,
final int interpolationSamples,
final Function<? super String, ? extends Frame> frameBuilder) {
this.mu = mu;
this.interpolationSamples = interpolationSamples;
this.frameBuilder = frameBuilder;
}
/**
* Default string to {@link Frame} conversion for {@link #SP3Parser()}.
*
* @param name of the frame.
* @return ITRF based on 2010 conventions.
*/
private static Frame guessFrame(final String name) {
try {
return FramesFactory.getITRF(IERSConventions.IERS_2010, true);
} catch (OrekitException e) {
throw new RuntimeException(e);
}
}
/**
* Parse a SP3 file from an input stream using the UTF-8 charset.
*
* <p> This method creates a {@link BufferedReader} from the stream and as such this
* method may read more data than necessary from {@code stream} and the additional
* data will be lost. The other parse methods do not have this issue.
*
* @param stream to read the SP3 file from.
* @return a parsed SP3 file.
* @throws OrekitException if the ephemeris file cannot be parsed.
* @throws IOException if {@code stream} throws one.
* @see #parse(String)
* @see #parse(BufferedReader, String)
*/
public SP3File parse(final InputStream stream) throws OrekitException, IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
return parse(reader, stream.toString());
}
}
@Override
public SP3File parse(final String fileName) throws IOException, OrekitException {
try (BufferedReader reader = Files.newBufferedReader(Paths.get(fileName),
StandardCharsets.UTF_8)) {
return parse(reader, fileName);
}
}
@Override
public SP3File parse(final BufferedReader reader,
final String fileName) throws OrekitException, IOException {
// initialize internal data structures
final ParseInfo pi = new ParseInfo();
String line = null;
int lineNumber = 1;
try {
while (lineNumber < 23) {
line = reader.readLine();
if (line == null) {
throw new OrekitException(OrekitMessages.SP3_UNEXPECTED_END_OF_FILE, lineNumber - 1);
} else {
parseHeaderLine(lineNumber++, line, pi);
}
}
// now handle the epoch/position/velocity entries
boolean done = false;
do {
line = reader.readLine();
if (line == null || "EOF".equalsIgnoreCase(line.trim())) {
done = true;
} else if (line.length() > 0) {
parseContentLine(line, pi);
}
} while (!done);
} finally {
try {
reader.close();
} catch (IOException e1) {
// ignore
}
}
return pi.file;
}
/** Parses a header line from the SP3 file (line number 1 - 22).
* @param lineNumber the current line number
* @param line the line as read from the SP3 file
* @param pi the current {@link ParseInfo} object
* @throws OrekitException if a non-supported construct is found
*/
private void parseHeaderLine(final int lineNumber, final String line, final ParseInfo pi)
throws OrekitException {
final SP3File file = pi.file;
try (Scanner s1 = new Scanner(line);
Scanner s2 = s1.useDelimiter("\\s+");
Scanner scanner = s2.useLocale(Locale.US)) {
// CHECKSTYLE: stop FallThrough check
switch (lineNumber) {
// version, epoch, data used and agency information
case 1: {
scanner.skip("#");
final String v = scanner.next();
final char version = v.substring(0, 1).toLowerCase().charAt(0);
if (version != 'a' && version != 'b' && version != 'c') {
throw new OrekitException(OrekitMessages.SP3_UNSUPPORTED_VERSION, version);
}
pi.hasVelocityEntries = "V".equals(v.substring(1, 2));
file.setFilter(pi.hasVelocityEntries ?
CartesianDerivativesFilter.USE_PV :
CartesianDerivativesFilter.USE_P);
final int year = Integer.parseInt(v.substring(2));
final int month = scanner.nextInt();
final int day = scanner.nextInt();
final int hour = scanner.nextInt();
final int minute = scanner.nextInt();
final double second = scanner.nextDouble();
final AbsoluteDate epoch = new AbsoluteDate(year, month, day,
hour, minute, second,
TimeScalesFactory.getGPS());
file.setEpoch(epoch);
final int numEpochs = scanner.nextInt();
file.setNumberOfEpochs(numEpochs);
// data used indicator
file.setDataUsed(scanner.next());
file.setCoordinateSystem(scanner.next());
file.setOrbitType(SP3OrbitType.parseType(scanner.next()));
file.setAgency(scanner.next());
break;
}
// additional date/time references in gps/julian day notation
case 2: {
scanner.skip("##");
// gps week
file.setGpsWeek(scanner.nextInt());
// seconds of week
file.setSecondsOfWeek(scanner.nextDouble());
// epoch interval
file.setEpochInterval(scanner.nextDouble());
// julian day
file.setJulianDay(scanner.nextInt());
// day fraction
file.setDayFraction(scanner.nextDouble());
break;
}
// line 3 contains the number of satellites
case 3:
pi.maxSatellites = Integer.parseInt(line.substring(4, 6).trim());
// fall-through intended - the line contains already the first entries
// the following 4 lines contain additional satellite ids
case 4:
case 5:
case 6:
case 7: {
final int lineLength = line.length();
int count = file.getSatelliteCount();
int startIdx = 9;
while (count++ < pi.maxSatellites && (startIdx + 3) <= lineLength) {
final String satId = line.substring(startIdx, startIdx + 3).trim();
file.addSatellite(satId);
startIdx += 3;
}
break;
}
// the following 5 lines contain general accuracy information for each satellite
case 8:
case 9:
case 10:
case 11:
case 12: {
final int lineLength = line.length();
int satIdx = (lineNumber - 8) * 17;
int startIdx = 9;
while (satIdx < pi.maxSatellites && (startIdx + 3) <= lineLength) {
final int exponent = Integer.parseInt(line.substring(startIdx, startIdx + 3).trim());
// the accuracy is calculated as 2**exp (in m) -> can be safely
// converted to an integer as there will be no fraction
file.setAccuracy(satIdx++, FastMath.pow(2d, exponent));
startIdx += 3;
}
break;
}
case 13: {
file.setType(getFileType(line.substring(3, 5).trim()));
// now identify the time system in use
final String tsStr = line.substring(9, 12).trim();
file.setTimeScaleString(tsStr);
final TimeSystem ts;
if (tsStr.equalsIgnoreCase("ccc")) {
ts = TimeSystem.GPS;
} else {
ts = TimeSystem.valueOf(tsStr);
}
file.setTimeSystem(ts);
switch (ts) {
case GPS:
pi.timeScale = TimeScalesFactory.getGPS();
break;
case GAL:
pi.timeScale = TimeScalesFactory.getGST();
break;
case GLO:
pi.timeScale = TimeScalesFactory.getGLONASS();
break;
case QZS:
pi.timeScale = TimeScalesFactory.getQZSS();
case TAI:
pi.timeScale = TimeScalesFactory.getTAI();
break;
case UTC:
pi.timeScale = TimeScalesFactory.getUTC();
break;
default:
pi.timeScale = TimeScalesFactory.getGPS();
break;
}
file.setTimeScale(pi.timeScale);
break;
}
case 14:
// ignore additional custom fields
break;
// load base numbers for the standard deviations of
// position/velocity/clock components
case 15: {
// String base = line.substring(3, 13).trim();
// if (!base.equals("0.0000000")) {
// // (mm or 10**-4 mm/sec)
// pi.posVelBase = Double.valueOf(base);
// }
// base = line.substring(14, 26).trim();
// if (!base.equals("0.000000000")) {
// // (psec or 10**-4 psec/sec)
// pi.clockBase = Double.valueOf(base);
// }
break;
}
case 16:
case 17:
case 18:
// ignore additional custom parameters
break;
case 19:
case 20:
case 21:
case 22:
// ignore comment lines
break;
default:
// ignore -> method should only be called up to line 22
break;
}
// CHECKSTYLE: resume FallThrough check
}
}
/** Parses a single content line as read from the SP3 file.
* @param line a string containing the line
* @param pi the current {@link ParseInfo} object
*/
private void parseContentLine(final String line, final ParseInfo pi) {
// EP and EV lines are ignored so far
final SP3File file = pi.file;
switch (line.charAt(0)) {
case '*': {
final int year = Integer.parseInt(line.substring(3, 7).trim());
final int month = Integer.parseInt(line.substring(8, 10).trim());
final int day = Integer.parseInt(line.substring(11, 13).trim());
final int hour = Integer.parseInt(line.substring(14, 16).trim());
final int minute = Integer.parseInt(line.substring(17, 19).trim());
final double second = Double.parseDouble(line.substring(20, 31).trim());
pi.latestEpoch = new AbsoluteDate(year, month, day,
hour, minute, second,
pi.timeScale);
break;
}
case 'P': {
final String satelliteId = line.substring(1, 4).trim();
if (!file.containsSatellite(satelliteId)) {
pi.latestPosition = null;
} else {
final double x = Double.parseDouble(line.substring(4, 18).trim());
final double y = Double.parseDouble(line.substring(18, 32).trim());
final double z = Double.parseDouble(line.substring(32, 46).trim());
// the position values are in km and have to be converted to m
pi.latestPosition = new Vector3D(x * 1000, y * 1000, z * 1000);
// clock (microsec)
pi.latestClock =
Double.parseDouble(line.substring(46, 60).trim()) * 1e6;
// the additional items are optional and not read yet
// if (line.length() >= 73) {
// // x-sdev (b**n mm)
// int xStdDevExp = Integer.valueOf(line.substring(61,
// 63).trim());
// // y-sdev (b**n mm)
// int yStdDevExp = Integer.valueOf(line.substring(64,
// 66).trim());
// // z-sdev (b**n mm)
// int zStdDevExp = Integer.valueOf(line.substring(67,
// 69).trim());
// // c-sdev (b**n psec)
// int cStdDevExp = Integer.valueOf(line.substring(70,
// 73).trim());
//
// pi.posStdDevRecord =
// new PositionStdDevRecord(FastMath.pow(pi.posVelBase, xStdDevExp),
// FastMath.pow(pi.posVelBase,
// yStdDevExp), FastMath.pow(pi.posVelBase, zStdDevExp),
// FastMath.pow(pi.clockBase, cStdDevExp));
//
// String clockEventFlag = line.substring(74, 75);
// String clockPredFlag = line.substring(75, 76);
// String maneuverFlag = line.substring(78, 79);
// String orbitPredFlag = line.substring(79, 80);
// }
if (!pi.hasVelocityEntries) {
final SP3Coordinate coord =
new SP3Coordinate(pi.latestEpoch,
pi.latestPosition,
pi.latestClock);
file.addSatelliteCoordinate(satelliteId, coord);
}
}
break;
}
case 'V': {
final String satelliteId = line.substring(1, 4).trim();
if (file.containsSatellite(satelliteId)) {
final double xv = Double.parseDouble(line.substring(4, 18).trim());
final double yv = Double.parseDouble(line.substring(18, 32).trim());
final double zv = Double.parseDouble(line.substring(32, 46).trim());
// the velocity values are in dm/s and have to be converted to m/s
final Vector3D velocity = new Vector3D(xv / 10d, yv / 10d, zv / 10d);
// clock rate in file is 1e-4 us / s
final double clockRateChange =
Double.parseDouble(line.substring(46, 60).trim()) * 1e10;
// the additional items are optional and not read yet
// if (line.length() >= 73) {
// // xvel-sdev (b**n 10**-4 mm/sec)
// int xVstdDevExp = Integer.valueOf(line.substring(61,
// 63).trim());
// // yvel-sdev (b**n 10**-4 mm/sec)
// int yVstdDevExp = Integer.valueOf(line.substring(64,
// 66).trim());
// // zvel-sdev (b**n 10**-4 mm/sec)
// int zVstdDevExp = Integer.valueOf(line.substring(67,
// 69).trim());
// // clkrate-sdev (b**n 10**-4 psec/sec)
// int clkStdDevExp = Integer.valueOf(line.substring(70,
// 73).trim());
// }
final SP3Coordinate coord =
new SP3Coordinate(pi.latestEpoch,
pi.latestPosition,
velocity,
pi.latestClock,
clockRateChange);
file.addSatelliteCoordinate(satelliteId, coord);
}
break;
}
default:
// ignore everything else
break;
}
}
/** Returns the {@link SP3FileType} that corresponds to a given string in a SP3 file.
* @param fileType file type as string
* @return file type as enum
*/
private SP3FileType getFileType(final String fileType) {
SP3FileType type = SP3FileType.UNDEFINED;
if ("G".equalsIgnoreCase(fileType)) {
type = SP3FileType.GPS;
} else if ("M".equalsIgnoreCase(fileType)) {
type = SP3FileType.MIXED;
} else if ("R".equalsIgnoreCase(fileType)) {
type = SP3FileType.GLONASS;
} else if ("L".equalsIgnoreCase(fileType)) {
type = SP3FileType.LEO;
} else if ("E".equalsIgnoreCase(fileType)) {
type = SP3FileType.GALILEO;
} else if ("C".equalsIgnoreCase(fileType)) {
type = SP3FileType.COMPASS;
} else if ("J".equalsIgnoreCase(fileType)) {
type = SP3FileType.QZSS;
}
return type;
}
/** Transient data used for parsing a sp3 file. The data is kept in a
* separate data structure to make the parser thread-safe.
* <p><b>Note</b>: The class intentionally does not provide accessor
* methods, as it is only used internally for parsing a SP3 file.</p>
*/
private class ParseInfo {
/** The corresponding SP3File object. */
private SP3File file;
/** The latest epoch as read from the SP3 file. */
private AbsoluteDate latestEpoch;
/** The latest position as read from the SP3 file. */
private Vector3D latestPosition;
/** The latest clock value as read from the SP3 file. */
private double latestClock;
/** Indicates if the SP3 file has velocity entries. */
private boolean hasVelocityEntries;
/** The timescale used in the SP3 file. */
private TimeScale timeScale;
/** The number of satellites as contained in the SP3 file. */
private int maxSatellites;
/** The base for pos/vel. */
//private double posVelBase;
/** The base for clock/rate. */
//private double clockBase;
/** Create a new {@link ParseInfo} object. */
protected ParseInfo() {
file = new SP3File(mu, interpolationSamples, frameBuilder);
latestEpoch = null;
latestPosition = null;
latestClock = 0.0d;
hasVelocityEntries = false;
timeScale = TimeScalesFactory.getGPS();
maxSatellites = 0;
//posVelBase = 2d;
//clockBase = 2d;
}
}
}