OpmParser.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.opm;

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

  22. import org.orekit.data.DataContext;
  23. import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
  24. import org.orekit.files.ccsds.ndm.odm.CartesianCovariance;
  25. import org.orekit.files.ccsds.ndm.odm.CartesianCovarianceKey;
  26. import org.orekit.files.ccsds.ndm.odm.OdmCommonMetadata;
  27. import org.orekit.files.ccsds.ndm.odm.CommonMetadataKey;
  28. import org.orekit.files.ccsds.ndm.odm.KeplerianElements;
  29. import org.orekit.files.ccsds.ndm.odm.KeplerianElementsKey;
  30. import org.orekit.files.ccsds.ndm.odm.OdmHeader;
  31. import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
  32. import org.orekit.files.ccsds.ndm.odm.OdmParser;
  33. import org.orekit.files.ccsds.ndm.odm.SpacecraftParameters;
  34. import org.orekit.files.ccsds.ndm.odm.SpacecraftParametersKey;
  35. import org.orekit.files.ccsds.ndm.odm.StateVector;
  36. import org.orekit.files.ccsds.ndm.odm.StateVectorKey;
  37. import org.orekit.files.ccsds.ndm.odm.UserDefined;
  38. import org.orekit.files.ccsds.section.CommentsContainer;
  39. import org.orekit.files.ccsds.section.HeaderProcessingState;
  40. import org.orekit.files.ccsds.section.MetadataKey;
  41. import org.orekit.files.ccsds.section.Segment;
  42. import org.orekit.files.ccsds.section.XmlStructureProcessingState;
  43. import org.orekit.files.ccsds.utils.ContextBinding;
  44. import org.orekit.files.ccsds.utils.FileFormat;
  45. import org.orekit.files.ccsds.utils.lexical.ParseToken;
  46. import org.orekit.files.ccsds.utils.lexical.TokenType;
  47. import org.orekit.files.ccsds.utils.lexical.UserDefinedXmlTokenBuilder;
  48. import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
  49. import org.orekit.files.ccsds.utils.parsing.ErrorState;
  50. import org.orekit.files.ccsds.utils.parsing.ProcessingState;
  51. import org.orekit.time.AbsoluteDate;
  52. import org.orekit.utils.IERSConventions;

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

  68.     /** Default mass to use if there are no spacecraft parameters block logical block in the file. */
  69.     private final double defaultMass;

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

  72.     /** File segments. */
  73.     private List<Segment<OdmCommonMetadata, OpmData>> segments;

  74.     /** OPM metadata being read. */
  75.     private OdmCommonMetadata metadata;

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

  78.     /** State vector logical block being read. */
  79.     private StateVector stateVectorBlock;

  80.     /** Keplerian elements logical block being read. */
  81.     private KeplerianElements keplerianElementsBlock;

  82.     /** Spacecraft parameters logical block being read. */
  83.     private SpacecraftParameters spacecraftParametersBlock;

  84.     /** Covariance matrix logical block being read. */
  85.     private CartesianCovariance covarianceBlock;

  86.     /** Current maneuver. */
  87.     private Maneuver currentManeuver;

  88.     /** All maneuvers. */
  89.     private List<Maneuver> maneuverBlocks;

  90.     /** User defined parameters. */
  91.     private UserDefined userDefinedBlock;

  92.     /** Processor for global message structure. */
  93.     private ProcessingState structureProcessor;

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

  119.     /** {@inheritDoc} */
  120.     @Override
  121.     public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {

  122.         final Map<String, XmlTokenBuilder> builders = super.getSpecialXmlElementsBuilders();

  123.         // special handling of user-defined parameters
  124.         builders.put(UserDefined.USER_DEFINED_XML_TAG, new UserDefinedXmlTokenBuilder());

  125.         return builders;

  126.     }

  127.     /** {@inheritDoc} */
  128.     @Override
  129.     public OdmHeader getHeader() {
  130.         return header;
  131.     }

  132.     /** {@inheritDoc} */
  133.     @Override
  134.     public void reset(final FileFormat fileFormat) {
  135.         header                    = new OdmHeader();
  136.         segments                  = new ArrayList<>();
  137.         metadata                  = null;
  138.         context                   = null;
  139.         stateVectorBlock          = null;
  140.         keplerianElementsBlock    = null;
  141.         spacecraftParametersBlock = null;
  142.         covarianceBlock           = null;
  143.         currentManeuver           = null;
  144.         maneuverBlocks            = new ArrayList<>();
  145.         userDefinedBlock          = null;
  146.         if (fileFormat == FileFormat.XML) {
  147.             structureProcessor = new XmlStructureProcessingState(Opm.ROOT, this);
  148.             reset(fileFormat, structureProcessor);
  149.         } else {
  150.             structureProcessor = new ErrorState(); // should never be called
  151.             reset(fileFormat, new HeaderProcessingState(this));
  152.         }
  153.     }

  154.     /** {@inheritDoc} */
  155.     @Override
  156.     public boolean prepareHeader() {
  157.         anticipateNext(new HeaderProcessingState(this));
  158.         return true;
  159.     }

  160.     /** {@inheritDoc} */
  161.     @Override
  162.     public boolean inHeader() {
  163.         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
  164.         return true;
  165.     }

  166.     /** {@inheritDoc} */
  167.     @Override
  168.     public boolean finalizeHeader() {
  169.         header.validate(header.getFormatVersion());
  170.         return true;
  171.     }

  172.     /** {@inheritDoc} */
  173.     @Override
  174.     public boolean prepareMetadata() {
  175.         if (metadata != null) {
  176.             return false;
  177.         }
  178.         metadata  = new OdmCommonMetadata();
  179.         context   = new ContextBinding(this::getConventions, this::isSimpleEOP,
  180.                                        this::getDataContext, this::getParsedUnitsBehavior,
  181.                                        this::getMissionReferenceDate,
  182.                                        metadata::getTimeSystem, () -> 0.0, () -> 1.0);
  183.         anticipateNext(this::processMetadataToken);
  184.         return true;
  185.     }

  186.     /** {@inheritDoc} */
  187.     @Override
  188.     public boolean inMetadata() {
  189.         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processStateVectorToken);
  190.         return true;
  191.     }

  192.     /** {@inheritDoc} */
  193.     @Override
  194.     public boolean finalizeMetadata() {
  195.         metadata.finalizeMetadata(context);
  196.         metadata.validate(header.getFormatVersion());
  197.         if (metadata.getCenter().getBody() != null) {
  198.             setMuCreated(metadata.getCenter().getBody().getGM());
  199.         }
  200.         return true;
  201.     }

  202.     /** {@inheritDoc} */
  203.     @Override
  204.     public boolean prepareData() {
  205.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processStateVectorToken);
  206.         return true;
  207.     }

  208.     /** {@inheritDoc} */
  209.     @Override
  210.     public boolean inData() {
  211.         return true;
  212.     }

  213.     /** {@inheritDoc} */
  214.     @Override
  215.     public boolean finalizeData() {
  216.         if (metadata != null) {
  217.             if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
  218.                 userDefinedBlock = null;
  219.             }
  220.             if (keplerianElementsBlock != null) {
  221.                 keplerianElementsBlock.setEpoch(stateVectorBlock.getEpoch());
  222.                 if (Double.isNaN(keplerianElementsBlock.getMu())) {
  223.                     keplerianElementsBlock.setMu(getSelectedMu());
  224.                 } else {
  225.                     setMuParsed(keplerianElementsBlock.getMu());
  226.                 }
  227.             }
  228.             final double  mass = spacecraftParametersBlock == null ?
  229.                                  defaultMass : spacecraftParametersBlock.getMass();
  230.             final OpmData data = new OpmData(stateVectorBlock, keplerianElementsBlock,
  231.                                              spacecraftParametersBlock, covarianceBlock,
  232.                                              maneuverBlocks, userDefinedBlock,
  233.                                              mass);
  234.             data.validate(header.getFormatVersion());
  235.             segments.add(new Segment<>(metadata, data));
  236.         }
  237.         metadata                  = null;
  238.         context                   = null;
  239.         stateVectorBlock          = null;
  240.         keplerianElementsBlock    = null;
  241.         spacecraftParametersBlock = null;
  242.         covarianceBlock           = null;
  243.         currentManeuver           = null;
  244.         maneuverBlocks            = null;
  245.         userDefinedBlock          = null;
  246.         return true;
  247.     }

  248.     /** {@inheritDoc} */
  249.     @Override
  250.     public Opm build() {
  251.         // OPM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
  252.         // automatically before the end of the file
  253.         finalizeData();
  254.         return new Opm(header, segments, getConventions(), getDataContext(), getSelectedMu());
  255.     }

  256.     /** Manage state vector section.
  257.      * @param starting if true, parser is entering the section
  258.      * otherwise it is leaving the section
  259.      * @return always return true
  260.      */
  261.     boolean manageStateVectorSection(final boolean starting) {
  262.         anticipateNext(starting ? this::processStateVectorToken : structureProcessor);
  263.         return true;
  264.     }

  265.     /** Manage Keplerian elements section.
  266.      * @param starting if true, parser is entering the section
  267.      * otherwise it is leaving the section
  268.      * @return always return true
  269.      */
  270.     boolean manageKeplerianElementsSection(final boolean starting) {
  271.         anticipateNext(starting ? this::processKeplerianElementsToken : structureProcessor);
  272.         return true;
  273.     }

  274.     /** Manage spacecraft parameters section.
  275.      * @param starting if true, parser is entering the section
  276.      * otherwise it is leaving the section
  277.      * @return always return true
  278.      */
  279.     boolean manageSpacecraftParametersSection(final boolean starting) {
  280.         anticipateNext(starting ? this::processSpacecraftParametersToken : structureProcessor);
  281.         return true;
  282.     }

  283.     /** Manage covariance matrix section.
  284.      * @param starting if true, parser is entering the section
  285.      * otherwise it is leaving the section
  286.      * @return always return true
  287.      */
  288.     boolean manageCovarianceSection(final boolean starting) {
  289.         anticipateNext(starting ? this::processCovarianceToken : structureProcessor);
  290.         return true;
  291.     }

  292.     /** Manage maneuvers section.
  293.      * @param starting if true, parser is entering the section
  294.      * otherwise it is leaving the section
  295.      * @return always return true
  296.      */
  297.     boolean manageManeuversSection(final boolean starting) {
  298.         anticipateNext(starting ? this::processManeuverToken : structureProcessor);
  299.         return true;
  300.     }

  301.     /** Manage user-defined parameters section.
  302.      * @param starting if true, parser is entering the section
  303.      * otherwise it is leaving the section
  304.      * @return always return true
  305.      */
  306.     boolean manageUserDefinedParametersSection(final boolean starting) {
  307.         anticipateNext(starting ? this::processUserDefinedToken : structureProcessor);
  308.         return true;
  309.     }

  310.     /** Process one metadata token.
  311.      * @param token token to process
  312.      * @return true if token was processed, false otherwise
  313.      */
  314.     private boolean processMetadataToken(final ParseToken token) {
  315.         if (metadata == null) {
  316.             // OPM KVN file lack a META_START keyword, hence we can't call prepareMetadata()
  317.             // automatically before the first metadata token arrives
  318.             prepareMetadata();
  319.         }
  320.         inMetadata();
  321.         try {
  322.             return token.getName() != null &&
  323.                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
  324.         } catch (IllegalArgumentException iaeM) {
  325.             try {
  326.                 return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  327.             } catch (IllegalArgumentException iaeD) {
  328.                 try {
  329.                     return CommonMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  330.                 } catch (IllegalArgumentException iaeC) {
  331.                     // token has not been recognized
  332.                     return false;
  333.                 }
  334.             }
  335.         }
  336.     }

  337.     /** Process one XML data substructure token.
  338.      * @param token token to process
  339.      * @return true if token was processed, false otherwise
  340.      */
  341.     private boolean processXmlSubStructureToken(final ParseToken token) {
  342.         try {
  343.             return token.getName() != null &&
  344.                    XmlSubStructureKey.valueOf(token.getName()).process(token, this);
  345.         } catch (IllegalArgumentException iae) {
  346.             // token has not been recognized
  347.             return false;
  348.         }
  349.     }

  350.     /** Process one state vector data token.
  351.      * @param token token to process
  352.      * @return true if token was processed, false otherwise
  353.      */
  354.     private boolean processStateVectorToken(final ParseToken token) {
  355.         if (stateVectorBlock == null) {
  356.             // OPM KVN file lack a META_STOP keyword, hence we can't call finalizeMetadata()
  357.             // automatically before the first data token arrives
  358.             finalizeMetadata();
  359.             // OPM KVN file lack a DATA_START keyword, hence we can't call prepareData()
  360.             // automatically before the first data token arrives
  361.             prepareData();
  362.             stateVectorBlock = new StateVector();
  363.         }
  364.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processKeplerianElementsToken);
  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 Keplerian elements data token.
  374.      * @param token token to process
  375.      * @return true if token was processed, false otherwise
  376.      */
  377.     private boolean processKeplerianElementsToken(final ParseToken token) {
  378.         if (keplerianElementsBlock == null) {
  379.             keplerianElementsBlock = new KeplerianElements();
  380.         }
  381.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processSpacecraftParametersToken);
  382.         try {
  383.             return token.getName() != null &&
  384.                    KeplerianElementsKey.valueOf(token.getName()).process(token, context, keplerianElementsBlock);
  385.         } catch (IllegalArgumentException iae) {
  386.             // token has not been recognized
  387.             return false;
  388.         }
  389.     }

  390.     /** Process one spacecraft parameters data token.
  391.      * @param token token to process
  392.      * @return true if token was processed, false otherwise
  393.      */
  394.     private boolean processSpacecraftParametersToken(final ParseToken token) {
  395.         if (spacecraftParametersBlock == null) {
  396.             spacecraftParametersBlock = new SpacecraftParameters();
  397.             if (moveCommentsIfEmpty(keplerianElementsBlock, spacecraftParametersBlock)) {
  398.                 // get rid of the empty logical block
  399.                 keplerianElementsBlock = null;
  400.             }
  401.         }
  402.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processCovarianceToken);
  403.         try {
  404.             return token.getName() != null &&
  405.                    SpacecraftParametersKey.valueOf(token.getName()).process(token, context, spacecraftParametersBlock);
  406.         } catch (IllegalArgumentException iae) {
  407.             // token has not been recognized
  408.             return false;
  409.         }
  410.     }

  411.     /** Process one covariance matrix data token.
  412.      * @param token token to process
  413.      * @return true if token was processed, false otherwise
  414.      */
  415.     private boolean processCovarianceToken(final ParseToken token) {
  416.         if (covarianceBlock == null) {
  417.             // save the current metadata for later retrieval of reference frame
  418.             final OdmCommonMetadata savedMetadata = metadata;
  419.             covarianceBlock = new CartesianCovariance(savedMetadata::getReferenceFrame);
  420.             if (moveCommentsIfEmpty(spacecraftParametersBlock, covarianceBlock)) {
  421.                 // get rid of the empty logical block
  422.                 spacecraftParametersBlock = null;
  423.             }
  424.         }
  425.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processManeuverToken);
  426.         try {
  427.             return token.getName() != null &&
  428.                    CartesianCovarianceKey.valueOf(token.getName()).process(token, context, covarianceBlock);
  429.         } catch (IllegalArgumentException iae) {
  430.             // token has not been recognized
  431.             return false;
  432.         }
  433.     }

  434.     /** Process one maneuver data token.
  435.      * @param token token to process
  436.      * @return true if token was processed, false otherwise
  437.      */
  438.     private boolean processManeuverToken(final ParseToken token) {
  439.         if (currentManeuver == null) {
  440.             currentManeuver = new Maneuver();
  441.             if (covarianceBlock != null && moveCommentsIfEmpty(covarianceBlock, currentManeuver)) {
  442.                 // get rid of the empty logical block
  443.                 covarianceBlock = null;
  444.             }
  445.         }
  446.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processUserDefinedToken);
  447.         try {
  448.             if (token.getName() != null &&
  449.                 ManeuverKey.valueOf(token.getName()).process(token, context, currentManeuver)) {
  450.                 // the token was processed properly
  451.                 if (currentManeuver.completed()) {
  452.                     // current maneuver is completed
  453.                     maneuverBlocks.add(currentManeuver);
  454.                     currentManeuver = null;
  455.                 }
  456.                 return true;
  457.             }
  458.         } catch (IllegalArgumentException iae) {
  459.             // ignored, delegate to next state below
  460.         }
  461.         // the token was not processed
  462.         return false;
  463.     }

  464.     /** Process one maneuver data token.
  465.      * @param token token to process
  466.      * @return true if token was processed, false otherwise
  467.      */
  468.     private boolean processUserDefinedToken(final ParseToken token) {
  469.         if (userDefinedBlock == null) {
  470.             userDefinedBlock = new UserDefined();
  471.             if (moveCommentsIfEmpty(currentManeuver, userDefinedBlock)) {
  472.                 // get rid of the empty logical block
  473.                 currentManeuver = null;
  474.             }
  475.         }
  476.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : new ErrorState());
  477.         if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
  478.             if (token.getType() == TokenType.ENTRY) {
  479.                 userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
  480.                                           token.getContentAsNormalizedString());
  481.             }
  482.             return true;
  483.         } else {
  484.             // the token was not processed
  485.             return false;
  486.         }
  487.     }

  488.     /** Move comments from one empty logical block to another logical block.
  489.      * @param origin origin block
  490.      * @param destination destination block
  491.      * @return true if origin block was empty
  492.      */
  493.     private boolean moveCommentsIfEmpty(final CommentsContainer origin, final CommentsContainer destination) {
  494.         if (origin != null && origin.acceptComments()) {
  495.             // origin block is empty, move the existing comments
  496.             for (final String comment : origin.getComments()) {
  497.                 destination.addComment(comment);
  498.             }
  499.             return true;
  500.         } else {
  501.             return false;
  502.         }
  503.     }

  504. }