OcmParser.java
/* Copyright 2002-2024 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.ccsds.ndm.odm.ocm;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.orekit.bodies.OneAxisEllipsoid;
import org.orekit.data.DataContext;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitIllegalArgumentException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
import org.orekit.files.ccsds.ndm.odm.OdmHeader;
import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
import org.orekit.files.ccsds.ndm.odm.OdmParser;
import org.orekit.files.ccsds.ndm.odm.UserDefined;
import org.orekit.files.ccsds.section.HeaderProcessingState;
import org.orekit.files.ccsds.section.KvnStructureProcessingState;
import org.orekit.files.ccsds.section.MetadataKey;
import org.orekit.files.ccsds.section.Segment;
import org.orekit.files.ccsds.section.XmlStructureProcessingState;
import org.orekit.files.ccsds.utils.ContextBinding;
import org.orekit.files.ccsds.utils.FileFormat;
import org.orekit.files.ccsds.utils.lexical.ParseToken;
import org.orekit.files.ccsds.utils.lexical.TokenType;
import org.orekit.files.ccsds.utils.lexical.UserDefinedXmlTokenBuilder;
import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
import org.orekit.files.ccsds.utils.parsing.ProcessingState;
import org.orekit.files.general.EphemerisFileParser;
import org.orekit.time.AbsoluteDate;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.units.Unit;
/** A parser for the CCSDS OCM (Orbit Comprehensive Message).
* <p>
* Note than starting with Orekit 11.0, CCSDS message parsers are
* mutable objects that gather the data being parsed, until the
* message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
* parseMessage} method has returned. This implies that parsers
* should <em>not</em> be used in a multi-thread context. The recommended
* way to use parsers is to either dedicate one parser for each message
* and drop it afterwards, or to use a single-thread loop.
* </p>
* @author Luc Maisonobe
* @since 11.0
*/
public class OcmParser extends OdmParser<Ocm, OcmParser> implements EphemerisFileParser<Ocm> {
/** Pattern for splitting strings at blanks. */
private static final Pattern SPLIT_AT_BLANKS = Pattern.compile("\\s+");
/** File header. */
private OdmHeader header;
/** Metadata for current observation block. */
private OcmMetadata metadata;
/** Central body equatorial radius.
* @since 12.0
*/
private final double equatorialRadius;
/** Central body flattening.
* @since 12.0
*/
private final double flattening;
/** Context binding valid for current metadata. */
private ContextBinding context;
/** Trajectory state histories logical blocks. */
private List<TrajectoryStateHistory> trajectoryBlocks;
/** Current trajectory state metadata. */
private TrajectoryStateHistoryMetadata currentTrajectoryStateHistoryMetadata;
/** Current trajectory state time history being read. */
private List<TrajectoryState> currentTrajectoryStateHistory;
/** Physical properties logical block. */
private OrbitPhysicalProperties physicBlock;
/** Covariance logical blocks. */
private List<OrbitCovarianceHistory> covarianceBlocks;
/** Current covariance metadata. */
private OrbitCovarianceHistoryMetadata currentCovarianceHistoryMetadata;
/** Current covariance history being read. */
private List<OrbitCovariance> currentCovarianceHistory;
/** Maneuver logical blocks. */
private List<OrbitManeuverHistory> maneuverBlocks;
/** Current maneuver metadata. */
private OrbitManeuverHistoryMetadata currentManeuverHistoryMetadata;
/** Current maneuver history being read. */
private List<OrbitManeuver> currentManeuverHistory;
/** Perturbations logical block. */
private Perturbations perturbationsBlock;
/** Orbit determination logical block. */
private OrbitDetermination orbitDeterminationBlock;
/** User defined parameters logical block. */
private UserDefined userDefinedBlock;
/** Processor for global message structure. */
private ProcessingState structureProcessor;
/**
* Complete constructor.
* <p>
* Calling this constructor directly is not recommended. Users should rather use
* {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildOcmParser()
* parserBuilder.buildOcmParser()}.
* </p>
* @param conventions IERS Conventions
* @param equatorialRadius central body equatorial radius
* @param flattening central body flattening
* @param simpleEOP if true, tidal effects are ignored when interpolating EOP
* @param dataContext used to retrieve frames, time scales, etc.
* @param mu gravitational coefficient
* @param parsedUnitsBehavior behavior to adopt for handling parsed units
* @param filters filters to apply to parse tokens
* @since 12.0
*/
public OcmParser(final IERSConventions conventions,
final double equatorialRadius, final double flattening,
final boolean simpleEOP, final DataContext dataContext,
final double mu, final ParsedUnitsBehavior parsedUnitsBehavior,
final Function<ParseToken, List<ParseToken>>[] filters) {
super(Ocm.ROOT, Ocm.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext, null,
mu, parsedUnitsBehavior, filters);
this.equatorialRadius = equatorialRadius;
this.flattening = flattening;
}
/** {@inheritDoc} */
@Override
public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {
final Map<String, XmlTokenBuilder> builders = super.getSpecialXmlElementsBuilders();
// special handling of user-defined parameters
builders.put(UserDefined.USER_DEFINED_XML_TAG, new UserDefinedXmlTokenBuilder());
return builders;
}
/** {@inheritDoc} */
@Override
public Ocm parse(final DataSource source) {
return parseMessage(source);
}
/** {@inheritDoc} */
@Override
public OdmHeader getHeader() {
return header;
}
/** {@inheritDoc} */
@Override
public void reset(final FileFormat fileFormat) {
header = new OdmHeader();
metadata = null;
context = null;
trajectoryBlocks = null;
physicBlock = null;
covarianceBlocks = null;
maneuverBlocks = null;
perturbationsBlock = null;
orbitDeterminationBlock = null;
userDefinedBlock = null;
if (fileFormat == FileFormat.XML) {
structureProcessor = new XmlStructureProcessingState(Ocm.ROOT, this);
reset(fileFormat, structureProcessor);
} else {
structureProcessor = new KvnStructureProcessingState(this);
reset(fileFormat, new HeaderProcessingState(this));
}
}
/** {@inheritDoc} */
@Override
public boolean prepareHeader() {
anticipateNext(new HeaderProcessingState(this));
return true;
}
/** {@inheritDoc} */
@Override
public boolean inHeader() {
anticipateNext(structureProcessor);
return true;
}
/** {@inheritDoc} */
@Override
public boolean finalizeHeader() {
header.validate(header.getFormatVersion());
return true;
}
/** {@inheritDoc} */
@Override
public boolean prepareMetadata() {
if (metadata != null) {
return false;
}
metadata = new OcmMetadata(getDataContext());
context = new ContextBinding(this::getConventions, this::isSimpleEOP, this::getDataContext,
this::getParsedUnitsBehavior, metadata::getEpochT0, metadata::getTimeSystem,
metadata::getSclkOffsetAtEpoch, metadata::getSclkSecPerSISec);
anticipateNext(this::processMetadataToken);
return true;
}
/** {@inheritDoc} */
@Override
public boolean inMetadata() {
anticipateNext(structureProcessor);
return true;
}
/** {@inheritDoc} */
@Override
public boolean finalizeMetadata() {
metadata.validate(header.getFormatVersion());
anticipateNext(this::processDataSubStructureToken);
return true;
}
/** {@inheritDoc} */
@Override
public boolean prepareData() {
anticipateNext(this::processDataSubStructureToken);
return true;
}
/** {@inheritDoc} */
@Override
public boolean inData() {
return true;
}
/** {@inheritDoc} */
@Override
public boolean finalizeData() {
final List<TrajectoryStateHistory> old = trajectoryBlocks;
if (old != null) {
final OneAxisEllipsoid body =
currentTrajectoryStateHistoryMetadata.getTrajType() == OrbitElementsType.GEODETIC ?
new OneAxisEllipsoid(equatorialRadius, flattening,
currentTrajectoryStateHistoryMetadata.getTrajReferenceFrame().asFrame()) :
null;
trajectoryBlocks = new ArrayList<>(old.size());
for (final TrajectoryStateHistory osh : old) {
trajectoryBlocks.add(new TrajectoryStateHistory(osh.getMetadata(), osh.getTrajectoryStates(),
body, getSelectedMu()));
}
}
return true;
}
/** Manage trajectory state history section.
* @param starting if true, parser is entering the section
* otherwise it is leaving the section
* @return always return true
*/
boolean manageTrajectoryStateSection(final boolean starting) {
if (starting) {
if (trajectoryBlocks == null) {
// this is the first trajectory block, we need to allocate the container
trajectoryBlocks = new ArrayList<>();
}
currentTrajectoryStateHistoryMetadata = new TrajectoryStateHistoryMetadata(metadata.getEpochT0(),
getDataContext());
currentTrajectoryStateHistory = new ArrayList<>();
anticipateNext(this::processTrajectoryStateToken);
} else {
final OneAxisEllipsoid body =
currentTrajectoryStateHistoryMetadata.getTrajType() == OrbitElementsType.GEODETIC ?
new OneAxisEllipsoid(equatorialRadius, flattening,
currentTrajectoryStateHistoryMetadata.getTrajReferenceFrame().asFrame()) :
null;
anticipateNext(structureProcessor);
if (currentTrajectoryStateHistoryMetadata.getCenter().getBody() != null) {
setMuCreated(currentTrajectoryStateHistoryMetadata.getCenter().getBody().getGM());
}
// we temporarily set gravitational parameter to NaN,
// as we may get a proper one in the perturbations section
trajectoryBlocks.add(new TrajectoryStateHistory(currentTrajectoryStateHistoryMetadata,
currentTrajectoryStateHistory,
body, Double.NaN));
}
return true;
}
/** Manage physical properties section.
* @param starting if true, parser is entering the section
* otherwise it is leaving the section
* @return always return true
*/
boolean managePhysicalPropertiesSection(final boolean starting) {
if (starting) {
if (physicBlock == null) {
// this is the first (and unique) physical properties block, we need to allocate the container
physicBlock = new OrbitPhysicalProperties(metadata.getEpochT0());
}
anticipateNext(this::processPhysicalPropertyToken);
} else {
anticipateNext(structureProcessor);
}
return true;
}
/** Manage covariance history section.
* @param starting if true, parser is entering the section
* otherwise it is leaving the section
* @return always return true
*/
boolean manageCovarianceHistorySection(final boolean starting) {
if (starting) {
if (covarianceBlocks == null) {
// this is the first covariance block, we need to allocate the container
covarianceBlocks = new ArrayList<>();
}
currentCovarianceHistoryMetadata = new OrbitCovarianceHistoryMetadata(metadata.getEpochT0());
currentCovarianceHistory = new ArrayList<>();
anticipateNext(this::processCovarianceToken);
} else {
anticipateNext(structureProcessor);
covarianceBlocks.add(new OrbitCovarianceHistory(currentCovarianceHistoryMetadata,
currentCovarianceHistory));
currentCovarianceHistoryMetadata = null;
currentCovarianceHistory = null;
}
return true;
}
/** Manage maneuvers section.
* @param starting if true, parser is entering the section
* otherwise it is leaving the section
* @return always return true
*/
boolean manageManeuversSection(final boolean starting) {
if (starting) {
if (maneuverBlocks == null) {
// this is the first maneuver block, we need to allocate the container
maneuverBlocks = new ArrayList<>();
}
currentManeuverHistoryMetadata = new OrbitManeuverHistoryMetadata(metadata.getEpochT0());
currentManeuverHistory = new ArrayList<>();
anticipateNext(this::processManeuverToken);
} else {
anticipateNext(structureProcessor);
maneuverBlocks.add(new OrbitManeuverHistory(currentManeuverHistoryMetadata,
currentManeuverHistory));
currentManeuverHistoryMetadata = null;
currentManeuverHistory = null;
}
return true;
}
/** Manage perturbation parameters section.
* @param starting if true, parser is entering the section
* otherwise it is leaving the section
* @return always return true
*/
boolean managePerturbationParametersSection(final boolean starting) {
if (starting) {
if (perturbationsBlock == null) {
// this is the first (and unique) perturbations parameters block, we need to allocate the container
perturbationsBlock = new Perturbations(context.getDataContext().getCelestialBodies());
}
anticipateNext(this::processPerturbationToken);
} else {
anticipateNext(structureProcessor);
}
return true;
}
/** Manage orbit determination section.
* @param starting if true, parser is entering the section
* otherwise it is leaving the section
* @return always return true
*/
boolean manageOrbitDeterminationSection(final boolean starting) {
if (starting) {
if (orbitDeterminationBlock == null) {
// this is the first (and unique) orbit determination block, we need to allocate the container
orbitDeterminationBlock = new OrbitDetermination();
}
anticipateNext(this::processOrbitDeterminationToken);
} else {
anticipateNext(structureProcessor);
}
return true;
}
/** Manage user-defined parameters section.
* @param starting if true, parser is entering the section
* otherwise it is leaving the section
* @return always return true
*/
boolean manageUserDefinedParametersSection(final boolean starting) {
if (starting) {
if (userDefinedBlock == null) {
// this is the first (and unique) user-defined parameters block, we need to allocate the container
userDefinedBlock = new UserDefined();
}
anticipateNext(this::processUserDefinedToken);
} else {
anticipateNext(structureProcessor);
}
return true;
}
/** {@inheritDoc} */
@Override
public Ocm build() {
// OCM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
// automatically before the end of the file
finalizeData();
if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
userDefinedBlock = null;
}
// the mu is needed only if there are trajectories
final double mu;
if (trajectoryBlocks == null) {
mu = Double.NaN;
} else {
if (perturbationsBlock != null) {
// this may be Double.NaN, but it will be handled correctly
setMuParsed(perturbationsBlock.getGm());
}
mu = getSelectedMu();
}
final OcmData data = new OcmData(trajectoryBlocks, physicBlock, covarianceBlocks,
maneuverBlocks, perturbationsBlock,
orbitDeterminationBlock, userDefinedBlock);
data.validate(header.getFormatVersion());
return new Ocm(header, Collections.singletonList(new Segment<>(metadata, data)),
getConventions(), getDataContext(), mu);
}
/** Process one metadata token.
* @param token token to process
* @return true if token was processed, false otherwise
*/
private boolean processMetadataToken(final ParseToken token) {
inMetadata();
try {
return token.getName() != null &&
MetadataKey.valueOf(token.getName()).process(token, context, metadata);
} catch (IllegalArgumentException iaeM) {
try {
return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
} catch (IllegalArgumentException iaeD) {
try {
return OcmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
} catch (IllegalArgumentException iaeC) {
// token has not been recognized
return false;
}
}
}
}
/** Process one data substructure token.
* @param token token to process
* @return true if token was processed, false otherwise
*/
private boolean processDataSubStructureToken(final ParseToken token) {
try {
return token.getName() != null &&
OcmDataSubStructureKey.valueOf(token.getName()).process(token, this);
} catch (IllegalArgumentException iae) {
// token has not been recognized
return false;
}
}
/** Process one trajectory state history data token.
* @param token token to process
* @return true if token was processed, false otherwise
*/
private boolean processTrajectoryStateToken(final ParseToken token) {
if (token.getName() != null && !token.getName().equals(Ocm.TRAJ_LINE)) {
// we are in the section metadata part
try {
return TrajectoryStateHistoryMetadataKey.valueOf(token.getName()).
process(token, context, currentTrajectoryStateHistoryMetadata);
} catch (IllegalArgumentException iae) {
// token has not been recognized
return false;
}
} else {
// we are in the section data part
if (currentTrajectoryStateHistory.isEmpty()) {
// we are starting the real data section, we can now check metadata is complete
currentTrajectoryStateHistoryMetadata.validate(header.getFormatVersion());
anticipateNext(this::processDataSubStructureToken);
}
if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
return true;
}
try {
final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
// as TRAJ_UNITS is optional and indeed MUST match type, get them directly from type
final List<Unit> units = currentTrajectoryStateHistoryMetadata.getTrajType().getUnits();
if (fields.length != units.size() + 1) {
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
}
final AbsoluteDate epoch = context.getTimeSystem().getConverter(context).parse(fields[0]);
return currentTrajectoryStateHistory.add(new TrajectoryState(currentTrajectoryStateHistoryMetadata.getTrajType(),
epoch, fields, 1, units));
} catch (NumberFormatException | OrekitIllegalArgumentException e) {
throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
}
}
}
/** Process one physical property data token.
* @param token token to process
* @return true if token was processed, false otherwise
*/
private boolean processPhysicalPropertyToken(final ParseToken token) {
if (physicBlock == null) {
physicBlock = new OrbitPhysicalProperties(metadata.getEpochT0());
}
anticipateNext(this::processDataSubStructureToken);
try {
return token.getName() != null &&
OrbitPhysicalPropertiesKey.valueOf(token.getName()).process(token, context, physicBlock);
} catch (IllegalArgumentException iae) {
// token has not been recognized
return false;
}
}
/** Process one covariance history history data token.
* @param token token to process
* @return true if token was processed, false otherwise
*/
private boolean processCovarianceToken(final ParseToken token) {
if (token.getName() != null && !token.getName().equals(Ocm.COV_LINE)) {
// we are in the section metadata part
try {
return OrbitCovarianceHistoryMetadataKey.valueOf(token.getName()).
process(token, context, currentCovarianceHistoryMetadata);
} catch (IllegalArgumentException iae) {
// token has not been recognized
return false;
}
} else {
// we are in the section data part
if (currentCovarianceHistory.isEmpty()) {
// we are starting the real data section, we can now check metadata is complete
currentCovarianceHistoryMetadata.validate(header.getFormatVersion());
anticipateNext(this::processDataSubStructureToken);
}
if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
return true;
}
try {
final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
final int n = currentCovarianceHistoryMetadata.getCovType().getUnits().size();
if (fields.length - 1 != currentCovarianceHistoryMetadata.getCovOrdering().nbElements(n)) {
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
}
currentCovarianceHistory.add(new OrbitCovariance(currentCovarianceHistoryMetadata.getCovType(),
currentCovarianceHistoryMetadata.getCovOrdering(),
context.getTimeSystem().getConverter(context).parse(fields[0]),
fields, 1));
return true;
} catch (NumberFormatException nfe) {
throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
}
}
}
/** Process one maneuver data token.
* @param token token to process
* @return true if token was processed, false otherwise
*/
private boolean processManeuverToken(final ParseToken token) {
if (token.getName() != null && !token.getName().equals(Ocm.MAN_LINE)) {
// we are in the section metadata part
try {
return OrbitManeuverHistoryMetadataKey.valueOf(token.getName()).
process(token, context, currentManeuverHistoryMetadata);
} catch (IllegalArgumentException iae) {
// token has not been recognized
return false;
}
} else {
// we are in the section data part
if (currentManeuverHistory.isEmpty()) {
// we are starting the real data section, we can now check metadata is complete
currentManeuverHistoryMetadata.validate(header.getFormatVersion());
anticipateNext(this::processDataSubStructureToken);
}
if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
return true;
}
try {
final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
final List<ManeuverFieldType> types = currentManeuverHistoryMetadata.getManComposition();
if (fields.length != types.size()) {
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
}
final OrbitManeuver maneuver = new OrbitManeuver();
for (int i = 0; i < fields.length; ++i) {
types.get(i).process(fields[i], context, maneuver, token.getLineNumber(), token.getFileName());
}
currentManeuverHistory.add(maneuver);
return true;
} catch (NumberFormatException nfe) {
throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
}
}
}
/** Process one perturbation parameter data token.
* @param token token to process
* @return true if token was processed, false otherwise
*/
private boolean processPerturbationToken(final ParseToken token) {
anticipateNext(this::processDataSubStructureToken);
try {
return token.getName() != null &&
PerturbationsKey.valueOf(token.getName()).process(token, context, perturbationsBlock);
} catch (IllegalArgumentException iae) {
// token has not been recognized
return false;
}
}
/** Process one orbit determination data token.
* @param token token to process
* @return true if token was processed, false otherwise
*/
private boolean processOrbitDeterminationToken(final ParseToken token) {
if (orbitDeterminationBlock == null) {
orbitDeterminationBlock = new OrbitDetermination();
}
anticipateNext(this::processDataSubStructureToken);
try {
return token.getName() != null &&
OrbitDeterminationKey.valueOf(token.getName()).process(token, context, orbitDeterminationBlock);
} catch (IllegalArgumentException iae) {
// token has not been recognized
return false;
}
}
/** Process one user-defined parameter data token.
* @param token token to process
* @return true if token was processed, false otherwise
*/
private boolean processUserDefinedToken(final ParseToken token) {
if (userDefinedBlock == null) {
userDefinedBlock = new UserDefined();
}
anticipateNext(this::processDataSubStructureToken);
if ("COMMENT".equals(token.getName())) {
return token.getType() == TokenType.ENTRY ? userDefinedBlock.addComment(token.getContentAsNormalizedString()) : true;
} else if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
if (token.getType() == TokenType.ENTRY) {
userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
token.getContentAsNormalizedString());
}
return true;
} else {
// the token was not processed
return false;
}
}
}