OcmParser.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.ocm;

  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.function.Function;
  23. import java.util.regex.Pattern;

  24. import org.orekit.bodies.OneAxisEllipsoid;
  25. import org.orekit.data.DataContext;
  26. import org.orekit.data.DataSource;
  27. import org.orekit.errors.OrekitException;
  28. import org.orekit.errors.OrekitIllegalArgumentException;
  29. import org.orekit.errors.OrekitMessages;
  30. import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
  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.UserDefined;
  35. import org.orekit.files.ccsds.section.HeaderProcessingState;
  36. import org.orekit.files.ccsds.section.KvnStructureProcessingState;
  37. import org.orekit.files.ccsds.section.MetadataKey;
  38. import org.orekit.files.ccsds.section.Segment;
  39. import org.orekit.files.ccsds.section.XmlStructureProcessingState;
  40. import org.orekit.files.ccsds.utils.ContextBinding;
  41. import org.orekit.files.ccsds.utils.FileFormat;
  42. import org.orekit.files.ccsds.utils.lexical.ParseToken;
  43. import org.orekit.files.ccsds.utils.lexical.TokenType;
  44. import org.orekit.files.ccsds.utils.lexical.UserDefinedXmlTokenBuilder;
  45. import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
  46. import org.orekit.files.ccsds.utils.parsing.ProcessingState;
  47. import org.orekit.files.general.EphemerisFileParser;
  48. import org.orekit.time.AbsoluteDate;
  49. import org.orekit.utils.IERSConventions;
  50. import org.orekit.utils.units.Unit;

  51. /** A parser for the CCSDS OCM (Orbit Comprehensive 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 Luc Maisonobe
  62.  * @since 11.0
  63.  */
  64. public class OcmParser extends OdmParser<Ocm, OcmParser> implements EphemerisFileParser<Ocm> {

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

  67.     /** File header. */
  68.     private OdmHeader header;

  69.     /** Metadata for current observation block. */
  70.     private OcmMetadata metadata;

  71.     /** Central body equatorial radius.
  72.      * @since 12.0
  73.      */
  74.     private final double equatorialRadius;

  75.     /** Central body flattening.
  76.      * @since 12.0
  77.      */
  78.     private final double flattening;

  79.     /** Context binding valid for current metadata. */
  80.     private ContextBinding context;

  81.     /** Trajectory state histories logical blocks. */
  82.     private List<TrajectoryStateHistory> trajectoryBlocks;

  83.     /** Current trajectory state metadata. */
  84.     private TrajectoryStateHistoryMetadata currentTrajectoryStateHistoryMetadata;

  85.     /** Current trajectory state time history being read. */
  86.     private List<TrajectoryState> currentTrajectoryStateHistory;

  87.     /** Physical properties logical block. */
  88.     private OrbitPhysicalProperties physicBlock;

  89.     /** Covariance logical blocks. */
  90.     private List<OrbitCovarianceHistory> covarianceBlocks;

  91.     /** Current covariance metadata. */
  92.     private OrbitCovarianceHistoryMetadata currentCovarianceHistoryMetadata;

  93.     /** Current covariance history being read. */
  94.     private List<OrbitCovariance> currentCovarianceHistory;

  95.     /** Maneuver logical blocks. */
  96.     private List<OrbitManeuverHistory> maneuverBlocks;

  97.     /** Current maneuver metadata. */
  98.     private OrbitManeuverHistoryMetadata currentManeuverHistoryMetadata;

  99.     /** Current maneuver history being read. */
  100.     private List<OrbitManeuver> currentManeuverHistory;

  101.     /** Perturbations logical block. */
  102.     private Perturbations perturbationsBlock;

  103.     /** Orbit determination logical block. */
  104.     private OrbitDetermination orbitDeterminationBlock;

  105.     /** User defined parameters logical block. */
  106.     private UserDefined userDefinedBlock;

  107.     /** Processor for global message structure. */
  108.     private ProcessingState structureProcessor;

  109.     /**
  110.      * Complete constructor.
  111.      * <p>
  112.      * Calling this constructor directly is not recommended. Users should rather use
  113.      * {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildOcmParser()
  114.      * parserBuilder.buildOcmParser()}.
  115.      * </p>
  116.      * @param conventions IERS Conventions
  117.      * @param equatorialRadius central body equatorial radius
  118.      * @param flattening central body flattening
  119.      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
  120.      * @param dataContext used to retrieve frames, time scales, etc.
  121.      * @param mu gravitational coefficient
  122.      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
  123.      * @param filters filters to apply to parse tokens
  124.      * @since 12.0
  125.      */
  126.     public OcmParser(final IERSConventions conventions,
  127.                      final double equatorialRadius, final double flattening,
  128.                      final boolean simpleEOP, final DataContext dataContext,
  129.                      final double mu, final ParsedUnitsBehavior parsedUnitsBehavior,
  130.                      final Function<ParseToken, List<ParseToken>>[] filters) {
  131.         super(Ocm.ROOT, Ocm.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext, null,
  132.               mu, parsedUnitsBehavior, filters);
  133.         this.equatorialRadius = equatorialRadius;
  134.         this.flattening       = flattening;
  135.     }

  136.     /** {@inheritDoc} */
  137.     @Override
  138.     public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {

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

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

  142.         return builders;

  143.     }

  144.     /** {@inheritDoc} */
  145.     @Override
  146.     public Ocm parse(final DataSource source) {
  147.         return parseMessage(source);
  148.     }

  149.     /** {@inheritDoc} */
  150.     @Override
  151.     public OdmHeader getHeader() {
  152.         return header;
  153.     }

  154.     /** {@inheritDoc} */
  155.     @Override
  156.     public void reset(final FileFormat fileFormat) {
  157.         header                  = new OdmHeader();
  158.         metadata                = null;
  159.         context                 = null;
  160.         trajectoryBlocks        = null;
  161.         physicBlock             = null;
  162.         covarianceBlocks        = null;
  163.         maneuverBlocks          = null;
  164.         perturbationsBlock      = null;
  165.         orbitDeterminationBlock = null;
  166.         userDefinedBlock        = null;
  167.         if (fileFormat == FileFormat.XML) {
  168.             structureProcessor = new XmlStructureProcessingState(Ocm.ROOT, this);
  169.             reset(fileFormat, structureProcessor);
  170.         } else {
  171.             structureProcessor = new KvnStructureProcessingState(this);
  172.             reset(fileFormat, new HeaderProcessingState(this));
  173.         }
  174.     }

  175.     /** {@inheritDoc} */
  176.     @Override
  177.     public boolean prepareHeader() {
  178.         anticipateNext(new HeaderProcessingState(this));
  179.         return true;
  180.     }

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

  187.     /** {@inheritDoc} */
  188.     @Override
  189.     public boolean finalizeHeader() {
  190.         header.validate(header.getFormatVersion());
  191.         return true;
  192.     }

  193.     /** {@inheritDoc} */
  194.     @Override
  195.     public boolean prepareMetadata() {
  196.         if (metadata != null) {
  197.             return false;
  198.         }
  199.         metadata  = new OcmMetadata(getDataContext());
  200.         context   = new ContextBinding(this::getConventions, this::isSimpleEOP, this::getDataContext,
  201.                                        this::getParsedUnitsBehavior, metadata::getEpochT0, metadata::getTimeSystem,
  202.                                        metadata::getSclkOffsetAtEpoch, metadata::getSclkSecPerSISec);
  203.         anticipateNext(this::processMetadataToken);
  204.         return true;
  205.     }

  206.     /** {@inheritDoc} */
  207.     @Override
  208.     public boolean inMetadata() {
  209.         anticipateNext(structureProcessor);
  210.         return true;
  211.     }

  212.     /** {@inheritDoc} */
  213.     @Override
  214.     public boolean finalizeMetadata() {
  215.         metadata.validate(header.getFormatVersion());
  216.         anticipateNext(this::processDataSubStructureToken);
  217.         return true;
  218.     }

  219.     /** {@inheritDoc} */
  220.     @Override
  221.     public boolean prepareData() {
  222.         anticipateNext(this::processDataSubStructureToken);
  223.         return true;
  224.     }

  225.     /** {@inheritDoc} */
  226.     @Override
  227.     public boolean inData() {
  228.         return true;
  229.     }

  230.     /** {@inheritDoc} */
  231.     @Override
  232.     public boolean finalizeData() {
  233.         final List<TrajectoryStateHistory> old = trajectoryBlocks;
  234.         if (old != null) {
  235.             final OneAxisEllipsoid body =
  236.                             currentTrajectoryStateHistoryMetadata.getTrajType() == OrbitElementsType.GEODETIC ?
  237.                             new OneAxisEllipsoid(equatorialRadius, flattening,
  238.                                                  currentTrajectoryStateHistoryMetadata.getTrajReferenceFrame().asFrame()) :
  239.                             null;
  240.             trajectoryBlocks = new ArrayList<>(old.size());
  241.             for (final TrajectoryStateHistory osh : old) {
  242.                 trajectoryBlocks.add(new TrajectoryStateHistory(osh.getMetadata(), osh.getTrajectoryStates(),
  243.                                                                 body, getSelectedMu()));
  244.             }
  245.         }
  246.         return true;
  247.     }

  248.     /** Manage trajectory state history section.
  249.      * @param starting if true, parser is entering the section
  250.      * otherwise it is leaving the section
  251.      * @return always return true
  252.      */
  253.     boolean manageTrajectoryStateSection(final boolean starting) {
  254.         if (starting) {
  255.             if (trajectoryBlocks == null) {
  256.                 // this is the first trajectory block, we need to allocate the container
  257.                 trajectoryBlocks = new ArrayList<>();
  258.             }
  259.             currentTrajectoryStateHistoryMetadata = new TrajectoryStateHistoryMetadata(metadata.getEpochT0(),
  260.                                                                                        getDataContext());
  261.             currentTrajectoryStateHistory         = new ArrayList<>();
  262.             anticipateNext(this::processTrajectoryStateToken);
  263.         } else {
  264.             final OneAxisEllipsoid body =
  265.                             currentTrajectoryStateHistoryMetadata.getTrajType() == OrbitElementsType.GEODETIC ?
  266.                             new OneAxisEllipsoid(equatorialRadius, flattening,
  267.                                                  currentTrajectoryStateHistoryMetadata.getTrajReferenceFrame().asFrame()) :
  268.                             null;
  269.             anticipateNext(structureProcessor);
  270.             if (currentTrajectoryStateHistoryMetadata.getCenter().getBody() != null) {
  271.                 setMuCreated(currentTrajectoryStateHistoryMetadata.getCenter().getBody().getGM());
  272.             }
  273.             // we temporarily set gravitational parameter to NaN,
  274.             // as we may get a proper one in the perturbations section
  275.             trajectoryBlocks.add(new TrajectoryStateHistory(currentTrajectoryStateHistoryMetadata,
  276.                                                             currentTrajectoryStateHistory,
  277.                                                             body, Double.NaN));
  278.         }
  279.         return true;
  280.     }

  281.     /** Manage physical properties section.
  282.      * @param starting if true, parser is entering the section
  283.      * otherwise it is leaving the section
  284.      * @return always return true
  285.      */
  286.     boolean managePhysicalPropertiesSection(final boolean starting) {
  287.         if (starting) {
  288.             if (physicBlock == null) {
  289.                 // this is the first (and unique) physical properties block, we need to allocate the container
  290.                 physicBlock = new OrbitPhysicalProperties(metadata.getEpochT0());
  291.             }
  292.             anticipateNext(this::processPhysicalPropertyToken);
  293.         } else {
  294.             anticipateNext(structureProcessor);
  295.         }
  296.         return true;
  297.     }

  298.     /** Manage covariance history section.
  299.      * @param starting if true, parser is entering the section
  300.      * otherwise it is leaving the section
  301.      * @return always return true
  302.      */
  303.     boolean manageCovarianceHistorySection(final boolean starting) {
  304.         if (starting) {
  305.             if (covarianceBlocks == null) {
  306.                 // this is the first covariance block, we need to allocate the container
  307.                 covarianceBlocks = new ArrayList<>();
  308.             }
  309.             currentCovarianceHistoryMetadata = new OrbitCovarianceHistoryMetadata(metadata.getEpochT0());
  310.             currentCovarianceHistory         = new ArrayList<>();
  311.             anticipateNext(this::processCovarianceToken);
  312.         } else {
  313.             anticipateNext(structureProcessor);
  314.             covarianceBlocks.add(new OrbitCovarianceHistory(currentCovarianceHistoryMetadata,
  315.                                                             currentCovarianceHistory));
  316.             currentCovarianceHistoryMetadata = null;
  317.             currentCovarianceHistory         = null;
  318.         }
  319.         return true;
  320.     }

  321.     /** Manage maneuvers section.
  322.      * @param starting if true, parser is entering the section
  323.      * otherwise it is leaving the section
  324.      * @return always return true
  325.      */
  326.     boolean manageManeuversSection(final boolean starting) {
  327.         if (starting) {
  328.             if (maneuverBlocks == null) {
  329.                 // this is the first maneuver block, we need to allocate the container
  330.                 maneuverBlocks = new ArrayList<>();
  331.             }
  332.             currentManeuverHistoryMetadata = new OrbitManeuverHistoryMetadata(metadata.getEpochT0());
  333.             currentManeuverHistory         = new ArrayList<>();
  334.             anticipateNext(this::processManeuverToken);
  335.         } else {
  336.             anticipateNext(structureProcessor);
  337.             maneuverBlocks.add(new OrbitManeuverHistory(currentManeuverHistoryMetadata,
  338.                                                         currentManeuverHistory));
  339.             currentManeuverHistoryMetadata = null;
  340.             currentManeuverHistory         = null;
  341.         }
  342.         return true;
  343.     }

  344.     /** Manage perturbation parameters section.
  345.      * @param starting if true, parser is entering the section
  346.      * otherwise it is leaving the section
  347.      * @return always return true
  348.      */
  349.     boolean managePerturbationParametersSection(final boolean starting) {
  350.         if (starting) {
  351.             if (perturbationsBlock == null) {
  352.                 // this is the first (and unique) perturbations parameters block, we need to allocate the container
  353.                 perturbationsBlock = new Perturbations(context.getDataContext().getCelestialBodies());
  354.             }
  355.             anticipateNext(this::processPerturbationToken);
  356.         } else {
  357.             anticipateNext(structureProcessor);
  358.         }
  359.         return true;
  360.     }

  361.     /** Manage orbit determination section.
  362.      * @param starting if true, parser is entering the section
  363.      * otherwise it is leaving the section
  364.      * @return always return true
  365.      */
  366.     boolean manageOrbitDeterminationSection(final boolean starting) {
  367.         if (starting) {
  368.             if (orbitDeterminationBlock == null) {
  369.                 // this is the first (and unique) orbit determination block, we need to allocate the container
  370.                 orbitDeterminationBlock = new OrbitDetermination();
  371.             }
  372.             anticipateNext(this::processOrbitDeterminationToken);
  373.         } else {
  374.             anticipateNext(structureProcessor);
  375.         }
  376.         return true;
  377.     }

  378.     /** Manage user-defined parameters section.
  379.      * @param starting if true, parser is entering the section
  380.      * otherwise it is leaving the section
  381.      * @return always return true
  382.      */
  383.     boolean manageUserDefinedParametersSection(final boolean starting) {
  384.         if (starting) {
  385.             if (userDefinedBlock == null) {
  386.                 // this is the first (and unique) user-defined parameters block, we need to allocate the container
  387.                 userDefinedBlock = new UserDefined();
  388.             }
  389.             anticipateNext(this::processUserDefinedToken);
  390.         } else {
  391.             anticipateNext(structureProcessor);
  392.         }
  393.         return true;
  394.     }

  395.     /** {@inheritDoc} */
  396.     @Override
  397.     public Ocm build() {

  398.         // OCM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
  399.         // automatically before the end of the file
  400.         finalizeData();
  401.         if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
  402.             userDefinedBlock = null;
  403.         }

  404.         // the mu is needed only if there are trajectories
  405.         final double mu;
  406.         if (trajectoryBlocks == null) {
  407.             mu = Double.NaN;
  408.         } else {
  409.             if (perturbationsBlock != null) {
  410.                 // this may be Double.NaN, but it will be handled correctly
  411.                 setMuParsed(perturbationsBlock.getGm());
  412.             }
  413.             mu = getSelectedMu();
  414.         }

  415.         final OcmData data = new OcmData(trajectoryBlocks, physicBlock, covarianceBlocks,
  416.                                          maneuverBlocks, perturbationsBlock,
  417.                                          orbitDeterminationBlock, userDefinedBlock);
  418.         data.validate(header.getFormatVersion());

  419.         return new Ocm(header, Collections.singletonList(new Segment<>(metadata, data)),
  420.                            getConventions(), getDataContext(), mu);

  421.     }

  422.     /** Process one metadata token.
  423.      * @param token token to process
  424.      * @return true if token was processed, false otherwise
  425.      */
  426.     private boolean processMetadataToken(final ParseToken token) {
  427.         inMetadata();
  428.         try {
  429.             return token.getName() != null &&
  430.                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
  431.         } catch (IllegalArgumentException iaeM) {
  432.             try {
  433.                 return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  434.             } catch (IllegalArgumentException iaeD) {
  435.                 try {
  436.                     return OcmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
  437.                 } catch (IllegalArgumentException iaeC) {
  438.                     // token has not been recognized
  439.                     return false;
  440.                 }
  441.             }
  442.         }
  443.     }

  444.     /** Process one data substructure token.
  445.      * @param token token to process
  446.      * @return true if token was processed, false otherwise
  447.      */
  448.     private boolean processDataSubStructureToken(final ParseToken token) {
  449.         try {
  450.             return token.getName() != null &&
  451.                    OcmDataSubStructureKey.valueOf(token.getName()).process(token, this);
  452.         } catch (IllegalArgumentException iae) {
  453.             // token has not been recognized
  454.             return false;
  455.         }
  456.     }

  457.     /** Process one trajectory state history data token.
  458.      * @param token token to process
  459.      * @return true if token was processed, false otherwise
  460.      */
  461.     private boolean processTrajectoryStateToken(final ParseToken token) {
  462.         if (token.getName() != null && !token.getName().equals(Ocm.TRAJ_LINE)) {
  463.             // we are in the section metadata part
  464.             try {
  465.                 return TrajectoryStateHistoryMetadataKey.valueOf(token.getName()).
  466.                        process(token, context, currentTrajectoryStateHistoryMetadata);
  467.             } catch (IllegalArgumentException iae) {
  468.                 // token has not been recognized
  469.                 return false;
  470.             }
  471.         } else {
  472.             // we are in the section data part
  473.             if (currentTrajectoryStateHistory.isEmpty()) {
  474.                 // we are starting the real data section, we can now check metadata is complete
  475.                 currentTrajectoryStateHistoryMetadata.validate(header.getFormatVersion());
  476.                 anticipateNext(this::processDataSubStructureToken);
  477.             }
  478.             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
  479.                 return true;
  480.             }
  481.             try {
  482.                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
  483.                 // as TRAJ_UNITS is optional and indeed MUST match type, get them directly from type
  484.                 final List<Unit> units = currentTrajectoryStateHistoryMetadata.getTrajType().getUnits();
  485.                 if (fields.length != units.size() + 1) {
  486.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  487.                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
  488.                 }
  489.                 final AbsoluteDate epoch = context.getTimeSystem().getConverter(context).parse(fields[0]);
  490.                 return currentTrajectoryStateHistory.add(new TrajectoryState(currentTrajectoryStateHistoryMetadata.getTrajType(),
  491.                                                                              epoch, fields, 1, units));
  492.             } catch (NumberFormatException | OrekitIllegalArgumentException e) {
  493.                 throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  494.                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
  495.             }
  496.         }
  497.     }

  498.     /** Process one physical property data token.
  499.      * @param token token to process
  500.      * @return true if token was processed, false otherwise
  501.      */
  502.     private boolean processPhysicalPropertyToken(final ParseToken token) {
  503.         if (physicBlock == null) {
  504.             physicBlock = new OrbitPhysicalProperties(metadata.getEpochT0());
  505.         }
  506.         anticipateNext(this::processDataSubStructureToken);
  507.         try {
  508.             return token.getName() != null &&
  509.                    OrbitPhysicalPropertiesKey.valueOf(token.getName()).process(token, context, physicBlock);
  510.         } catch (IllegalArgumentException iae) {
  511.             // token has not been recognized
  512.             return false;
  513.         }
  514.     }

  515.     /** Process one covariance history history data token.
  516.      * @param token token to process
  517.      * @return true if token was processed, false otherwise
  518.      */
  519.     private boolean processCovarianceToken(final ParseToken token) {
  520.         if (token.getName() != null && !token.getName().equals(Ocm.COV_LINE)) {
  521.             // we are in the section metadata part
  522.             try {
  523.                 return OrbitCovarianceHistoryMetadataKey.valueOf(token.getName()).
  524.                        process(token, context, currentCovarianceHistoryMetadata);
  525.             } catch (IllegalArgumentException iae) {
  526.                 // token has not been recognized
  527.                 return false;
  528.             }
  529.         } else {
  530.             // we are in the section data part
  531.             if (currentCovarianceHistory.isEmpty()) {
  532.                 // we are starting the real data section, we can now check metadata is complete
  533.                 currentCovarianceHistoryMetadata.validate(header.getFormatVersion());
  534.                 anticipateNext(this::processDataSubStructureToken);
  535.             }
  536.             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
  537.                 return true;
  538.             }
  539.             try {
  540.                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
  541.                 final int n = currentCovarianceHistoryMetadata.getCovType().getUnits().size();
  542.                 if (fields.length - 1 != currentCovarianceHistoryMetadata.getCovOrdering().nbElements(n)) {
  543.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  544.                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
  545.                 }
  546.                 currentCovarianceHistory.add(new OrbitCovariance(currentCovarianceHistoryMetadata.getCovType(),
  547.                                                             currentCovarianceHistoryMetadata.getCovOrdering(),
  548.                                                             context.getTimeSystem().getConverter(context).parse(fields[0]),
  549.                                                             fields, 1));
  550.                 return true;
  551.             } catch (NumberFormatException nfe) {
  552.                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  553.                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
  554.             }
  555.         }
  556.     }

  557.     /** Process one maneuver data token.
  558.      * @param token token to process
  559.      * @return true if token was processed, false otherwise
  560.      */
  561.     private boolean processManeuverToken(final ParseToken token) {
  562.         if (token.getName() != null && !token.getName().equals(Ocm.MAN_LINE)) {
  563.             // we are in the section metadata part
  564.             try {
  565.                 return OrbitManeuverHistoryMetadataKey.valueOf(token.getName()).
  566.                        process(token, context, currentManeuverHistoryMetadata);
  567.             } catch (IllegalArgumentException iae) {
  568.                 // token has not been recognized
  569.                 return false;
  570.             }
  571.         } else {
  572.             // we are in the section data part
  573.             if (currentManeuverHistory.isEmpty()) {
  574.                 // we are starting the real data section, we can now check metadata is complete
  575.                 currentManeuverHistoryMetadata.validate(header.getFormatVersion());
  576.                 anticipateNext(this::processDataSubStructureToken);
  577.             }
  578.             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
  579.                 return true;
  580.             }
  581.             try {
  582.                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
  583.                 final List<ManeuverFieldType> types = currentManeuverHistoryMetadata.getManComposition();
  584.                 if (fields.length != types.size()) {
  585.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  586.                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
  587.                 }
  588.                 final OrbitManeuver maneuver = new OrbitManeuver();
  589.                 for (int i = 0; i < fields.length; ++i) {
  590.                     types.get(i).process(fields[i], context, maneuver, token.getLineNumber(), token.getFileName());
  591.                 }
  592.                 currentManeuverHistory.add(maneuver);
  593.                 return true;
  594.             } catch (NumberFormatException nfe) {
  595.                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  596.                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
  597.             }
  598.         }
  599.     }

  600.     /** Process one perturbation parameter data token.
  601.      * @param token token to process
  602.      * @return true if token was processed, false otherwise
  603.      */
  604.     private boolean processPerturbationToken(final ParseToken token) {
  605.         anticipateNext(this::processDataSubStructureToken);
  606.         try {
  607.             return token.getName() != null &&
  608.                    PerturbationsKey.valueOf(token.getName()).process(token, context, perturbationsBlock);
  609.         } catch (IllegalArgumentException iae) {
  610.             // token has not been recognized
  611.             return false;
  612.         }
  613.     }

  614.     /** Process one orbit determination data token.
  615.      * @param token token to process
  616.      * @return true if token was processed, false otherwise
  617.      */
  618.     private boolean processOrbitDeterminationToken(final ParseToken token) {
  619.         if (orbitDeterminationBlock == null) {
  620.             orbitDeterminationBlock = new OrbitDetermination();
  621.         }
  622.         anticipateNext(this::processDataSubStructureToken);
  623.         try {
  624.             return token.getName() != null &&
  625.                    OrbitDeterminationKey.valueOf(token.getName()).process(token, context, orbitDeterminationBlock);
  626.         } catch (IllegalArgumentException iae) {
  627.             // token has not been recognized
  628.             return false;
  629.         }
  630.     }

  631.     /** Process one user-defined parameter data token.
  632.      * @param token token to process
  633.      * @return true if token was processed, false otherwise
  634.      */
  635.     private boolean processUserDefinedToken(final ParseToken token) {
  636.         if (userDefinedBlock == null) {
  637.             userDefinedBlock = new UserDefined();
  638.         }
  639.         anticipateNext(this::processDataSubStructureToken);
  640.         if ("COMMENT".equals(token.getName())) {
  641.             return token.getType() == TokenType.ENTRY ? userDefinedBlock.addComment(token.getContentAsNormalizedString()) : true;
  642.         } else if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
  643.             if (token.getType() == TokenType.ENTRY) {
  644.                 userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
  645.                                           token.getContentAsNormalizedString());
  646.             }
  647.             return true;
  648.         } else {
  649.             // the token was not processed
  650.             return false;
  651.         }
  652.     }

  653. }