OmmParser.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.omm;

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

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

  53. /** A parser for the CCSDS OMM (Orbiter Mean-Elements 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.  * @since 6.1
  65.  */
  66. public class OmmParser extends OdmParser<Omm, OmmParser> {

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

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

  71.     /** File segments. */
  72.     private List<Segment<OmmMetadata, OmmData>> segments;

  73.     /** OMM metadata being read. */
  74.     private OmmMetadata metadata;

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

  77.     /** Keplerian elements logical block being read. */
  78.     private KeplerianElements keplerianElementsBlock;

  79.     /** Spacecraft parameters logical block being read. */
  80.     private SpacecraftParameters spacecraftParametersBlock;

  81.     /** TLE logical block being read. */
  82.     private OmmTle tleBlock;

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

  85.     /** User defined parameters. */
  86.     private UserDefined userDefinedBlock;

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

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

  113.     /** {@inheritDoc} */
  114.     @Override
  115.     public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {

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

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

  119.         return builders;

  120.     }

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

  126.     /** {@inheritDoc} */
  127.     @Override
  128.     public void reset(final FileFormat fileFormat) {
  129.         header                    = new OdmHeader();
  130.         segments                  = new ArrayList<>();
  131.         metadata                  = null;
  132.         context                   = null;
  133.         keplerianElementsBlock    = null;
  134.         spacecraftParametersBlock = null;
  135.         tleBlock                  = null;
  136.         covarianceBlock           = null;
  137.         userDefinedBlock          = null;
  138.         if (fileFormat == FileFormat.XML) {
  139.             structureProcessor = new XmlStructureProcessingState(Omm.ROOT, this);
  140.             reset(fileFormat, structureProcessor);
  141.         } else {
  142.             structureProcessor = new ErrorState(); // should never be called
  143.             reset(fileFormat, new HeaderProcessingState(this));
  144.         }
  145.     }

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

  152.     /** {@inheritDoc} */
  153.     @Override
  154.     public boolean inHeader() {
  155.         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
  156.         return true;
  157.     }

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

  164.     /** {@inheritDoc} */
  165.     @Override
  166.     public boolean prepareMetadata() {
  167.         if (metadata != null) {
  168.             return false;
  169.         }
  170.         metadata  = new OmmMetadata();
  171.         context   = new ContextBinding(this::getConventions, this::isSimpleEOP,
  172.                                        this::getDataContext, this::getParsedUnitsBehavior,
  173.                                        this::getMissionReferenceDate,
  174.                                        metadata::getTimeSystem, () -> 0.0, () -> 1.0);
  175.         anticipateNext(this::processMetadataToken);
  176.         return true;
  177.     }

  178.     /** {@inheritDoc} */
  179.     @Override
  180.     public boolean inMetadata() {
  181.         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processKeplerianElementsToken);
  182.         return true;
  183.     }

  184.     /** {@inheritDoc} */
  185.     @Override
  186.     public boolean finalizeMetadata() {
  187.         metadata.finalizeMetadata(context);
  188.         metadata.validate(header.getFormatVersion());
  189.         if (metadata.getCenter().getBody() != null) {
  190.             setMuCreated(metadata.getCenter().getBody().getGM());
  191.         }
  192.         return true;
  193.     }

  194.     /** {@inheritDoc} */
  195.     @Override
  196.     public boolean prepareData() {
  197.         keplerianElementsBlock = new KeplerianElements();
  198.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processKeplerianElementsToken);
  199.         return true;
  200.     }

  201.     /** {@inheritDoc} */
  202.     @Override
  203.     public boolean inData() {
  204.         return true;
  205.     }

  206.     /** {@inheritDoc} */
  207.     @Override
  208.     public boolean finalizeData() {
  209.         if (metadata != null) {
  210.             if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
  211.                 userDefinedBlock = null;
  212.             }
  213.             if (tleBlock != null) {
  214.                 if (Double.isNaN(keplerianElementsBlock.getMu())) {
  215.                     keplerianElementsBlock.setMu(TLEPropagator.getMU());
  216.                 }
  217.                 final double mu = keplerianElementsBlock.getMu();
  218.                 final double n  = keplerianElementsBlock.getMeanMotion();
  219.                 keplerianElementsBlock.setA(FastMath.cbrt(mu / (n * n)));
  220.                 setMuParsed(mu);
  221.             }
  222.             final double  mass = spacecraftParametersBlock == null ?
  223.                                  defaultMass : spacecraftParametersBlock.getMass();
  224.             final OmmData data = new OmmData(keplerianElementsBlock, spacecraftParametersBlock,
  225.                                              tleBlock, covarianceBlock, userDefinedBlock, mass);
  226.             data.validate(header.getFormatVersion());
  227.             segments.add(new Segment<>(metadata, data));
  228.         }
  229.         metadata                  = null;
  230.         context                   = null;
  231.         keplerianElementsBlock    = null;
  232.         spacecraftParametersBlock = null;
  233.         tleBlock                  = null;
  234.         covarianceBlock           = null;
  235.         userDefinedBlock          = null;
  236.         return true;
  237.     }

  238.     /** {@inheritDoc} */
  239.     @Override
  240.     public Omm build() {
  241.         // OMM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
  242.         // automatically before the end of the file
  243.         finalizeData();
  244.         return new Omm(header, segments, getConventions(), getDataContext());
  245.     }

  246.     /** Manage Keplerian elements section.
  247.      * @param starting if true, parser is entering the section
  248.      * otherwise it is leaving the section
  249.      * @return always return true
  250.      */
  251.     boolean manageKeplerianElementsSection(final boolean starting) {
  252.         anticipateNext(starting ? this::processKeplerianElementsToken : structureProcessor);
  253.         return true;
  254.     }

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

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

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

  282.     /** Manage user-defined parameters section.
  283.      * @param starting if true, parser is entering the section
  284.      * otherwise it is leaving the section
  285.      * @return always return true
  286.      */
  287.     boolean manageUserDefinedParametersSection(final boolean starting) {
  288.         anticipateNext(starting ? this::processUserDefinedToken : structureProcessor);
  289.         return true;
  290.     }

  291.     /** Process one metadata token.
  292.      * @param token token to process
  293.      * @return true if token was processed, false otherwise
  294.      */
  295.     private boolean processMetadataToken(final ParseToken token) {
  296.         if (metadata == null) {
  297.             // OMM KVN file lack a META_START keyword, hence we can't call prepareMetadata()
  298.             // automatically before the first metadata token arrives
  299.             prepareMetadata();
  300.         }
  301.         inMetadata();
  302.         try {
  303.             return token.getName() != null &&
  304.                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
  305.         } catch (IllegalArgumentException iaeG) {
  306.             try {
  307.                 return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  308.             } catch (IllegalArgumentException iaeD) {
  309.                 try {
  310.                     return CommonMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  311.                 } catch (IllegalArgumentException iaeC) {
  312.                     try {
  313.                         return OmmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  314.                     } catch (IllegalArgumentException iaeM) {
  315.                         // token has not been recognized
  316.                         return false;
  317.                     }
  318.                 }
  319.             }
  320.         }
  321.     }

  322.     /** Process one XML data substructure token.
  323.      * @param token token to process
  324.      * @return true if token was processed, false otherwise
  325.      */
  326.     private boolean processXmlSubStructureToken(final ParseToken token) {
  327.         try {
  328.             return token.getName() != null &&
  329.                    XmlSubStructureKey.valueOf(token.getName()).process(token, this);
  330.         } catch (IllegalArgumentException iae) {
  331.             // token has not been recognized
  332.             return false;
  333.         }
  334.     }

  335.     /** Process one mean Keplerian elements data token.
  336.      * @param token token to process
  337.      * @return true if token was processed, false otherwise
  338.      */
  339.     private boolean processKeplerianElementsToken(final ParseToken token) {
  340.         if (keplerianElementsBlock == null) {
  341.             // OMM KVN file lack a META_STOP keyword, hence we can't call finalizeMetadata()
  342.             // automatically before the first data token arrives
  343.             finalizeMetadata();
  344.             // OMM KVN file lack a DATA_START keyword, hence we can't call prepareData()
  345.             // automatically before the first data token arrives
  346.             prepareData();
  347.         }
  348.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processSpacecraftParametersToken);
  349.         try {
  350.             return token.getName() != null &&
  351.                    KeplerianElementsKey.valueOf(token.getName()).process(token, context, keplerianElementsBlock);
  352.         } catch (IllegalArgumentException iae) {
  353.             // token has not been recognized
  354.             return false;
  355.         }
  356.     }

  357.     /** Process one spacecraft parameters data token.
  358.      * @param token token to process
  359.      * @return true if token was processed, false otherwise
  360.      */
  361.     private boolean processSpacecraftParametersToken(final ParseToken token) {
  362.         if (spacecraftParametersBlock == null) {
  363.             spacecraftParametersBlock = new SpacecraftParameters();
  364.             if (moveCommentsIfEmpty(keplerianElementsBlock, spacecraftParametersBlock)) {
  365.                 // get rid of the empty logical block
  366.                 keplerianElementsBlock = null;
  367.             }
  368.         }
  369.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processTLEToken);
  370.         try {
  371.             return token.getName() != null &&
  372.                    SpacecraftParametersKey.valueOf(token.getName()).process(token, context, spacecraftParametersBlock);
  373.         } catch (IllegalArgumentException iae) {
  374.             // token has not been recognized
  375.             return false;
  376.         }
  377.     }

  378.     /** Process one TLE data token.
  379.      * @param token token to process
  380.      * @return true if token was processed, false otherwise
  381.      */
  382.     private boolean processTLEToken(final ParseToken token) {
  383.         if (tleBlock == null) {
  384.             tleBlock = new OmmTle();
  385.             if (moveCommentsIfEmpty(spacecraftParametersBlock, tleBlock)) {
  386.                 // get rid of the empty logical block
  387.                 spacecraftParametersBlock = null;
  388.             }
  389.         }
  390.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processCovarianceToken);
  391.         try {
  392.             return token.getName() != null &&
  393.                    OmmTleKey.valueOf(token.getName()).process(token, context, tleBlock);
  394.         } catch (IllegalArgumentException iae) {
  395.             // token has not been recognized
  396.             return false;
  397.         }
  398.     }

  399.     /** Process one covariance matrix data token.
  400.      * @param token token to process
  401.      * @return true if token was processed, false otherwise
  402.      */
  403.     private boolean processCovarianceToken(final ParseToken token) {
  404.         if (covarianceBlock == null) {
  405.             // save the current metadata for later retrieval of reference frame
  406.             final OdmCommonMetadata savedMetadata = metadata;
  407.             covarianceBlock = new CartesianCovariance(savedMetadata::getReferenceFrame);
  408.             if (moveCommentsIfEmpty(tleBlock, covarianceBlock)) {
  409.                 // get rid of the empty logical block
  410.                 tleBlock = null;
  411.             }
  412.         }
  413.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processUserDefinedToken);
  414.         try {
  415.             return token.getName() != null &&
  416.                    CartesianCovarianceKey.valueOf(token.getName()).process(token, context, covarianceBlock);
  417.         } catch (IllegalArgumentException iae) {
  418.             // token has not been recognized
  419.             return false;
  420.         }
  421.     }

  422.     /** Process one maneuver data token.
  423.      * @param token token to process
  424.      * @return true if token was processed, false otherwise
  425.      */
  426.     private boolean processUserDefinedToken(final ParseToken token) {
  427.         if (userDefinedBlock == null) {
  428.             userDefinedBlock = new UserDefined();
  429.             if (moveCommentsIfEmpty(covarianceBlock, userDefinedBlock)) {
  430.                 // get rid of the empty logical block
  431.                 covarianceBlock = null;
  432.             }
  433.         }
  434.         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : new ErrorState());
  435.         if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
  436.             if (token.getType() == TokenType.ENTRY) {
  437.                 userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
  438.                                           token.getContentAsNormalizedString());
  439.             }
  440.             return true;
  441.         } else {
  442.             // the token was not processed
  443.             return false;
  444.         }
  445.     }

  446.     /** Move comments from one empty logical block to another logical block.
  447.      * @param origin origin block
  448.      * @param destination destination block
  449.      * @return true if origin block was empty
  450.      */
  451.     private boolean moveCommentsIfEmpty(final CommentsContainer origin, final CommentsContainer destination) {
  452.         if (origin != null && origin.acceptComments()) {
  453.             // origin block is empty, move the existing comments
  454.             for (final String comment : origin.getComments()) {
  455.                 destination.addComment(comment);
  456.             }
  457.             return true;
  458.         } else {
  459.             return false;
  460.         }
  461.     }

  462. }