OemParser.java

  1. /* Copyright 2002-2025 CS GROUP
  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.ccsds.ndm.odm.oem;

  18. import java.util.ArrayList;
  19. import java.util.List;
  20. import java.util.function.Function;
  21. import java.util.regex.Pattern;

  22. import org.orekit.data.DataContext;
  23. import org.orekit.data.DataSource;
  24. import org.orekit.errors.OrekitException;
  25. import org.orekit.errors.OrekitMessages;
  26. import org.orekit.files.ccsds.definitions.Units;
  27. import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
  28. import org.orekit.files.ccsds.ndm.odm.CartesianCovariance;
  29. import org.orekit.files.ccsds.ndm.odm.CartesianCovarianceKey;
  30. import org.orekit.files.ccsds.ndm.odm.OdmCommonMetadata;
  31. import org.orekit.files.ccsds.ndm.odm.CommonMetadataKey;
  32. import org.orekit.files.ccsds.ndm.odm.OdmHeader;
  33. import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
  34. import org.orekit.files.ccsds.ndm.odm.OdmParser;
  35. import org.orekit.files.ccsds.ndm.odm.StateVector;
  36. import org.orekit.files.ccsds.ndm.odm.StateVectorKey;
  37. import org.orekit.files.ccsds.section.HeaderProcessingState;
  38. import org.orekit.files.ccsds.section.KvnStructureProcessingState;
  39. import org.orekit.files.ccsds.section.MetadataKey;
  40. import org.orekit.files.ccsds.section.XmlStructureProcessingState;
  41. import org.orekit.files.ccsds.utils.ContextBinding;
  42. import org.orekit.files.ccsds.utils.FileFormat;
  43. import org.orekit.files.ccsds.utils.lexical.ParseToken;
  44. import org.orekit.files.ccsds.utils.lexical.TokenType;
  45. import org.orekit.files.ccsds.utils.parsing.ProcessingState;
  46. import org.orekit.files.general.EphemerisFileParser;
  47. import org.orekit.time.AbsoluteDate;
  48. import org.orekit.utils.IERSConventions;
  49. import org.orekit.utils.units.Unit;

  50. /**
  51.  * A parser for the CCSDS OEM (Orbit Ephemeris Message).
  52.  * <p>
  53.  * Note than starting with Orekit 11.0, CCSDS message parsers are
  54.  * mutable objects that gather the data being parsed, until the
  55.  * message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
  56.  * parseMessage} method has returned. This implies that parsers
  57.  * should <em>not</em> be used in a multi-thread context. The recommended
  58.  * way to use parsers is to either dedicate one parser for each message
  59.  * and drop it afterwards, or to use a single-thread loop.
  60.  * </p>
  61.  * @author sports
  62.  * @since 6.1
  63.  */
  64. public class OemParser extends OdmParser<Oem, OemParser> implements EphemerisFileParser<Oem> {

  65.     /** Comment marker. */
  66.     private static final String COMMENT = "COMMENT";

  67.                     /** Pattern for splitting strings at blanks. */
  68.     private static final Pattern SPLIT_AT_BLANKS = Pattern.compile("\\s+");

  69.     /** File header. */
  70.     private OdmHeader header;

  71.     /** File segments. */
  72.     private List<OemSegment> segments;

  73.     /** Metadata for current observation block. */
  74.     private OemMetadata metadata;

  75.     /** Context binding valid for current metadata. */
  76.     private ContextBinding context;

  77.     /** Current Ephemerides block being parsed. */
  78.     private OemData currentBlock;

  79.     /** Indicator for covariance parsing. */
  80.     private boolean inCovariance;

  81.     /** Current covariance matrix being parsed. */
  82.     private CartesianCovariance currentCovariance;

  83.     /** Current row number in covariance matrix. */
  84.     private int currentRow;

  85.     /** Default interpolation degree. */
  86.     private int defaultInterpolationDegree;

  87.     /** Processor for global message structure. */
  88.     private ProcessingState structureProcessor;

  89.     /** State vector logical block being read. */
  90.     private StateVector stateVectorBlock;

  91.     /**
  92.      * Complete constructor.
  93.      * <p>
  94.      * Calling this constructor directly is not recommended. Users should rather use
  95.      * {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildOemParser()
  96.      * parserBuilder.buildOemParser()}.
  97.      * </p>
  98.      * @param conventions IERS Conventions
  99.      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
  100.      * @param dataContext used to retrieve frames, time scales, etc.
  101.      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
  102.      * (may be null if time system is absolute)
  103.      * @param mu gravitational coefficient
  104.      * @param defaultInterpolationDegree default interpolation degree
  105.      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
  106.      * @param filters filters to apply to parse tokens
  107.      * @since 12.0
  108.      */
  109.     public OemParser(final IERSConventions conventions, final boolean simpleEOP,
  110.                      final DataContext dataContext,
  111.                      final AbsoluteDate missionReferenceDate, final double mu,
  112.                      final int defaultInterpolationDegree, final ParsedUnitsBehavior parsedUnitsBehavior,
  113.                      final Function<ParseToken, List<ParseToken>>[] filters) {
  114.         super(Oem.ROOT, Oem.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext,
  115.               missionReferenceDate, mu, parsedUnitsBehavior, filters);
  116.         this.defaultInterpolationDegree  = defaultInterpolationDegree;
  117.     }

  118.     /** {@inheritDoc} */
  119.     @Override
  120.     public Oem parse(final DataSource source) {
  121.         return parseMessage(source);
  122.     }

  123.     /** {@inheritDoc} */
  124.     @Override
  125.     public OdmHeader getHeader() {
  126.         return header;
  127.     }

  128.     /** {@inheritDoc} */
  129.     @Override
  130.     public void reset(final FileFormat fileFormat) {
  131.         header            = new OdmHeader();
  132.         segments          = new ArrayList<>();
  133.         metadata          = null;
  134.         context           = null;
  135.         currentBlock      = null;
  136.         inCovariance      = false;
  137.         currentCovariance = null;
  138.         currentRow        = -1;
  139.         if (fileFormat == FileFormat.XML) {
  140.             structureProcessor = new XmlStructureProcessingState(Oem.ROOT, this);
  141.             reset(fileFormat, structureProcessor);
  142.         } else {
  143.             structureProcessor = new KvnStructureProcessingState(this);
  144.             reset(fileFormat, new HeaderProcessingState(this));
  145.         }
  146.     }

  147.     /** {@inheritDoc} */
  148.     @Override
  149.     public boolean prepareHeader() {
  150.         anticipateNext(new HeaderProcessingState(this));
  151.         return true;
  152.     }

  153.     /** {@inheritDoc} */
  154.     @Override
  155.     public boolean inHeader() {
  156.         anticipateNext(structureProcessor);
  157.         return true;
  158.     }

  159.     /** {@inheritDoc} */
  160.     @Override
  161.     public boolean finalizeHeader() {
  162.         header.validate(header.getFormatVersion());
  163.         return true;
  164.     }

  165.     /** {@inheritDoc} */
  166.     @Override
  167.     public boolean prepareMetadata() {
  168.         if (currentBlock != null) {
  169.             // we have started a new segment, we need to finalize the previous one
  170.             finalizeData();
  171.         }
  172.         metadata = new OemMetadata(defaultInterpolationDegree);
  173.         context  = new ContextBinding(this::getConventions, this::isSimpleEOP,
  174.                                       this::getDataContext, this::getParsedUnitsBehavior,
  175.                                       this::getMissionReferenceDate,
  176.                                       metadata::getTimeSystem, () -> 0.0, () -> 1.0);
  177.         anticipateNext(this::processMetadataToken);
  178.         return true;
  179.     }

  180.     /** {@inheritDoc} */
  181.     @Override
  182.     public boolean inMetadata() {
  183.         anticipateNext(structureProcessor);
  184.         return true;
  185.     }

  186.     /** {@inheritDoc} */
  187.     @Override
  188.     public boolean finalizeMetadata() {
  189.         metadata.finalizeMetadata(context);
  190.         metadata.validate(header.getFormatVersion());
  191.         if (metadata.getCenter().getBody() != null) {
  192.             setMuCreated(metadata.getCenter().getBody().getGM());
  193.         }
  194.         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processKvnDataToken);
  195.         return true;
  196.     }

  197.     /** {@inheritDoc} */
  198.     @Override
  199.     public boolean prepareData() {
  200.         currentBlock = new OemData();
  201.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processMetadataToken);
  202.         return true;
  203.     }

  204.     /** {@inheritDoc} */
  205.     @Override
  206.     public boolean inData() {
  207.         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processKvnCovarianceToken);
  208.         return true;
  209.     }

  210.     /** {@inheritDoc} */
  211.     @Override
  212.     public boolean finalizeData() {
  213.         if (metadata != null) {
  214.             currentBlock.validate(header.getFormatVersion());
  215.             segments.add(new OemSegment(metadata, currentBlock, getSelectedMu()));
  216.         }
  217.         metadata          = null;
  218.         context           = null;
  219.         currentBlock      = null;
  220.         inCovariance      = false;
  221.         currentCovariance = null;
  222.         currentRow        = -1;
  223.         return true;
  224.     }

  225.     /** {@inheritDoc} */
  226.     @Override
  227.     public Oem build() {
  228.         // OEM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
  229.         // automatically before the end of the file
  230.         finalizeData();
  231.         final Oem file = new Oem(header, segments, getConventions(), getDataContext(), getSelectedMu());
  232.         file.checkTimeSystems();
  233.         return file;
  234.     }

  235.     /** Manage state vector section in a XML message.
  236.      * @param starting if true, parser is entering the section
  237.      * otherwise it is leaving the section
  238.      * @return always return true
  239.      */
  240.     boolean manageXmlStateVectorSection(final boolean starting) {
  241.         if (starting) {
  242.             stateVectorBlock = new StateVector();
  243.             anticipateNext(this::processXmlStateVectorToken);
  244.         } else {
  245.             currentBlock.addData(stateVectorBlock.toTimeStampedPVCoordinates(),
  246.                                  stateVectorBlock.hasAcceleration());
  247.             stateVectorBlock = null;
  248.             anticipateNext(structureProcessor);
  249.         }
  250.         return true;
  251.     }

  252.     /** Manage covariance matrix section.
  253.      * @param starting if true, parser is entering the section
  254.      * otherwise it is leaving the section
  255.      * @return always return true
  256.      */
  257.     boolean manageCovarianceSection(final boolean starting) {
  258.         if (starting) {
  259.             // save the current metadata for later retrieval of reference frame
  260.             final OdmCommonMetadata savedMetadata = metadata;
  261.             currentCovariance = new CartesianCovariance(savedMetadata::getReferenceFrame);
  262.             anticipateNext(getFileFormat() == FileFormat.XML ?
  263.                         this::processXmlCovarianceToken :
  264.                         this::processKvnCovarianceToken);
  265.         } else {
  266.             currentBlock.addCovarianceMatrix(currentCovariance);
  267.             currentCovariance = null;
  268.             anticipateNext(structureProcessor);
  269.         }
  270.         return true;
  271.     }

  272.     /** Process one metadata token.
  273.      * @param token token to process
  274.      * @return true if token was processed, false otherwise
  275.      */
  276.     private boolean processMetadataToken(final ParseToken token) {
  277.         inMetadata();
  278.         try {
  279.             return token.getName() != null &&
  280.                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
  281.         } catch (IllegalArgumentException iaeM) {
  282.             try {
  283.                 return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  284.             } catch (IllegalArgumentException iaeD) {
  285.                 try {
  286.                     return CommonMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  287.                 } catch (IllegalArgumentException iaeC) {
  288.                     try {
  289.                         return OemMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  290.                     } catch (IllegalArgumentException iaeE) {
  291.                         // token has not been recognized
  292.                         return false;
  293.                     }
  294.                 }
  295.             }
  296.         }
  297.     }

  298.     /** Process one XML data substructure token.
  299.      * @param token token to process
  300.      * @return true if token was processed, false otherwise
  301.      */
  302.     private boolean processXmlSubStructureToken(final ParseToken token) {
  303.         if (COMMENT.equals(token.getName())) {
  304.             return token.getType() == TokenType.ENTRY ? currentBlock.addComment(token.getContentAsNormalizedString()) : true;
  305.         } else {
  306.             try {
  307.                 return token.getName() != null &&
  308.                                 OemDataSubStructureKey.valueOf(token.getName()).process(token, this);
  309.             } catch (IllegalArgumentException iae) {
  310.                 // token has not been recognized
  311.                 return false;
  312.             }
  313.         }
  314.     }

  315.     /** Process one data token in a KVN message.
  316.      * @param token token to process
  317.      * @return true if token was processed, false otherwise
  318.      */
  319.     private boolean processKvnDataToken(final ParseToken token) {
  320.         if (currentBlock == null) {
  321.             // OEM KVN file lack a DATA_START keyword, hence we can't call prepareData()
  322.             // automatically before the first data token arrives
  323.             prepareData();
  324.         }
  325.         inData();
  326.         if (COMMENT.equals(token.getName())) {
  327.             return token.getType() == TokenType.ENTRY ? currentBlock.addComment(token.getContentAsNormalizedString()) : true;
  328.         } else if (token.getType() == TokenType.RAW_LINE) {
  329.             try {
  330.                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
  331.                 if (fields.length != 7 && fields.length != 10) {
  332.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  333.                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
  334.                 }
  335.                 stateVectorBlock = new StateVector();
  336.                 stateVectorBlock.setEpoch(context.getTimeSystem().getConverter(context).parse(fields[0]));
  337.                 stateVectorBlock.setP(0, Unit.KILOMETRE.toSI(Double.parseDouble(fields[1])));
  338.                 stateVectorBlock.setP(1, Unit.KILOMETRE.toSI(Double.parseDouble(fields[2])));
  339.                 stateVectorBlock.setP(2, Unit.KILOMETRE.toSI(Double.parseDouble(fields[3])));
  340.                 stateVectorBlock.setV(0, Units.KM_PER_S.toSI(Double.parseDouble(fields[4])));
  341.                 stateVectorBlock.setV(1, Units.KM_PER_S.toSI(Double.parseDouble(fields[5])));
  342.                 stateVectorBlock.setV(2, Units.KM_PER_S.toSI(Double.parseDouble(fields[6])));
  343.                 if (fields.length == 10) {
  344.                     stateVectorBlock.setA(0, Units.KM_PER_S2.toSI(Double.parseDouble(fields[7])));
  345.                     stateVectorBlock.setA(1, Units.KM_PER_S2.toSI(Double.parseDouble(fields[8])));
  346.                     stateVectorBlock.setA(2, Units.KM_PER_S2.toSI(Double.parseDouble(fields[9])));
  347.                 }
  348.                 return currentBlock.addData(stateVectorBlock.toTimeStampedPVCoordinates(),
  349.                                             stateVectorBlock.hasAcceleration());
  350.             } catch (NumberFormatException nfe) {
  351.                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  352.                                           token.getLineNumber(), token.getFileName(), token.getRawContent());
  353.             }
  354.         } else {
  355.             // not a raw line, it is most probably either the end of the data section or a covariance section
  356.             return false;
  357.         }
  358.     }

  359.     /** Process one state vector data token in a XML message.
  360.      * @param token token to process
  361.      * @return true if token was processed, false otherwise
  362.      */
  363.     private boolean processXmlStateVectorToken(final ParseToken token) {
  364.         anticipateNext(this::processXmlSubStructureToken);
  365.         try {
  366.             return token.getName() != null &&
  367.                    StateVectorKey.valueOf(token.getName()).process(token, context, stateVectorBlock);
  368.         } catch (IllegalArgumentException iae) {
  369.             // token has not been recognized
  370.             return false;
  371.         }
  372.     }

  373.     /** Process one covariance token in a KVN message.
  374.      * @param token token to process
  375.      * @return true if token was processed, false otherwise
  376.      */
  377.     private boolean processKvnCovarianceToken(final ParseToken token) {
  378.         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
  379.         if (token.getName() != null) {
  380.             if (OemDataSubStructureKey.COVARIANCE.name().equals(token.getName()) ||
  381.                 OemDataSubStructureKey.covarianceMatrix.name().equals(token.getName())) {
  382.                 // we are entering/leaving covariance section
  383.                 inCovariance = token.getType() == TokenType.START;
  384.                 return true;
  385.             } else if (!inCovariance) {
  386.                 // this is not a covariance token
  387.                 return false;
  388.             } else {
  389.                 // named tokens in covariance section must be at the start, before the raw lines
  390.                 if (currentRow > 0) {
  391.                     // the previous covariance matrix was not completed
  392.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_ELEMENT_IN_FILE,
  393.                                               token.getName(), token.getLineNumber(), token.getFileName());
  394.                 }

  395.                 if (currentCovariance == null) {
  396.                     // save the current metadata for later retrieval of reference frame
  397.                     final OdmCommonMetadata savedMetadata = metadata;
  398.                     currentCovariance = new CartesianCovariance(savedMetadata::getReferenceFrame);
  399.                     currentRow        = 0;
  400.                 }

  401.                 // parse the token
  402.                 try {
  403.                     return CartesianCovarianceKey.valueOf(token.getName()).
  404.                            process(token, context, currentCovariance);
  405.                 } catch (IllegalArgumentException iae) {
  406.                     // token not recognized
  407.                     return false;
  408.                 }

  409.             }
  410.         } else {
  411.             // this is a raw line
  412.             try {
  413.                 final String[] fields = SPLIT_AT_BLANKS.split(token.getContentAsNormalizedString().trim());
  414.                 if (fields.length != currentRow + 1) {
  415.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  416.                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
  417.                 }
  418.                 for (int j = 0; j < fields.length; ++j) {
  419.                     currentCovariance.setCovarianceMatrixEntry(currentRow, j, 1.0e6 * Double.parseDouble(fields[j]));
  420.                 }
  421.                 if (++currentRow == 6) {
  422.                     // this was the last row
  423.                     currentBlock.addCovarianceMatrix(currentCovariance);
  424.                     currentCovariance = null;
  425.                     currentRow        = -1;
  426.                 }
  427.                 return true;
  428.             } catch (NumberFormatException nfe) {
  429.                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  430.                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
  431.             }
  432.         }
  433.     }

  434.     /** Process one covariance matrix data token in a XML message.
  435.      * @param token token to process
  436.      * @return true if token was processed, false otherwise
  437.      */
  438.     private boolean processXmlCovarianceToken(final ParseToken token) {
  439.         anticipateNext(this::processXmlSubStructureToken);
  440.         try {
  441.             return token.getName() != null &&
  442.                    CartesianCovarianceKey.valueOf(token.getName()).process(token, context, currentCovariance);
  443.         } catch (IllegalArgumentException iae) {
  444.             // token has not been recognized
  445.             return false;
  446.         }
  447.     }

  448. }