ApmParser.java

  1. /* Copyright 2002-2024 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.adm.apm;

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

  21. import org.orekit.data.DataContext;
  22. import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
  23. import org.orekit.files.ccsds.ndm.adm.AdmCommonMetadataKey;
  24. import org.orekit.files.ccsds.ndm.adm.AdmHeader;
  25. import org.orekit.files.ccsds.ndm.adm.AdmMetadata;
  26. import org.orekit.files.ccsds.ndm.adm.AdmMetadataKey;
  27. import org.orekit.files.ccsds.ndm.adm.AdmParser;
  28. import org.orekit.files.ccsds.section.CommentsContainer;
  29. import org.orekit.files.ccsds.section.HeaderProcessingState;
  30. import org.orekit.files.ccsds.section.MetadataKey;
  31. import org.orekit.files.ccsds.section.Segment;
  32. import org.orekit.files.ccsds.section.XmlStructureProcessingState;
  33. import org.orekit.files.ccsds.utils.ContextBinding;
  34. import org.orekit.files.ccsds.utils.FileFormat;
  35. import org.orekit.files.ccsds.utils.lexical.ParseToken;
  36. import org.orekit.files.ccsds.utils.lexical.TokenType;
  37. import org.orekit.files.ccsds.utils.parsing.ErrorState;
  38. import org.orekit.files.ccsds.utils.parsing.ProcessingState;
  39. import org.orekit.time.AbsoluteDate;
  40. import org.orekit.utils.IERSConventions;

  41. /**
  42.  * A parser for the CCSDS APM (Attitude Parameter Message).
  43.  * @author Bryan Cazabonne * <p>
  44.  * Note than starting with Orekit 11.0, CCSDS message parsers are
  45.  * mutable objects that gather the data being parsed, until the
  46.  * message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
  47.  * parseMessage} method has returned. This implies that parsers
  48.  * should <em>not</em> be used in a multi-thread context. The recommended
  49.  * way to use parsers is to either dedicate one parser for each message
  50.  * and drop it afterwards, or to use a single-thread loop.
  51.  * </p>

  52.  * @since 10.2
  53.  */
  54. public class ApmParser extends AdmParser<Apm, ApmParser> {

  55.     /** File header. */
  56.     private AdmHeader header;

  57.     /** File segments. */
  58.     private List<Segment<AdmMetadata, ApmData>> segments;

  59.     /** APM metadata being read. */
  60.     private AdmMetadata metadata;

  61.     /** Context binding valid for current metadata. */
  62.     private ContextBinding context;

  63.     /** APM epoch.
  64.      * @since 12.0
  65.      */
  66.     private AbsoluteDate epoch;

  67.     /** APM general comments block being read. */
  68.     private CommentsContainer commentsBlock;

  69.     /** APM quaternion logical block being read. */
  70.     private ApmQuaternion quaternionBlock;

  71.     /** APM Euler angles logical block being read. */
  72.     private Euler eulerBlock;

  73.     /** APM angular velocity logical block being read.
  74.      * @since 12.0
  75.      */
  76.     private AngularVelocity angularVelocityBlock;

  77.     /** APM spin-stabilized logical block being read. */
  78.     private SpinStabilized spinStabilizedBlock;

  79.     /** APM inertia block being read.
  80.      * @since 12.0
  81.      */
  82.     private Inertia inertiaBlock;

  83.     /** Current maneuver. */
  84.     private Maneuver currentManeuver;

  85.     /** All maneuvers. */
  86.     private List<Maneuver> maneuvers;

  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#buildApmParser()
  93.      * parserBuilder.buildApmParser()}.
  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.      * (may be null if time system is absolute)
  100.      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
  101.      * @param filters filters to apply to parse tokens
  102.      * @since 12.0
  103.      */
  104.     public ApmParser(final IERSConventions conventions, final boolean simpleEOP, final DataContext dataContext,
  105.                      final AbsoluteDate missionReferenceDate, final ParsedUnitsBehavior parsedUnitsBehavior,
  106.                      final Function<ParseToken, List<ParseToken>>[] filters) {
  107.         super(Apm.ROOT, Apm.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext,
  108.               missionReferenceDate, parsedUnitsBehavior, filters);
  109.     }

  110.     /** {@inheritDoc} */
  111.     @Override
  112.     public AdmHeader getHeader() {
  113.         return header;
  114.     }

  115.     /** {@inheritDoc} */
  116.     @Override
  117.     public void reset(final FileFormat fileFormat) {
  118.         header              = new AdmHeader();
  119.         segments            = new ArrayList<>();
  120.         metadata            = null;
  121.         context             = null;
  122.         quaternionBlock     = null;
  123.         eulerBlock          = null;
  124.         spinStabilizedBlock = null;
  125.         inertiaBlock        = null;
  126.         currentManeuver     = null;
  127.         maneuvers           = new ArrayList<>();
  128.         if (fileFormat == FileFormat.XML) {
  129.             structureProcessor = new XmlStructureProcessingState(Apm.ROOT, this);
  130.             reset(fileFormat, structureProcessor);
  131.         } else {
  132.             structureProcessor = new ErrorState(); // should never be called
  133.             reset(fileFormat, new HeaderProcessingState(this));
  134.         }
  135.     }

  136.     /** {@inheritDoc} */
  137.     @Override
  138.     public boolean prepareHeader() {
  139.         anticipateNext(new HeaderProcessingState(this));
  140.         return true;
  141.     }

  142.     /** {@inheritDoc} */
  143.     @Override
  144.     public boolean inHeader() {
  145.         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
  146.         return true;
  147.     }

  148.     /** {@inheritDoc} */
  149.     @Override
  150.     public boolean finalizeHeader() {
  151.         header.validate(header.getFormatVersion());
  152.         return true;
  153.     }

  154.     /** {@inheritDoc} */
  155.     @Override
  156.     public boolean prepareMetadata() {
  157.         if (metadata != null) {
  158.             return false;
  159.         }
  160.         metadata  = new AdmMetadata();
  161.         context   = new ContextBinding(this::getConventions, this::isSimpleEOP,
  162.                                        this::getDataContext, this::getParsedUnitsBehavior,
  163.                                        this::getMissionReferenceDate,
  164.                                        metadata::getTimeSystem, () -> 0.0, () -> 1.0);
  165.         anticipateNext(this::processMetadataToken);
  166.         return true;
  167.     }

  168.     /** {@inheritDoc} */
  169.     @Override
  170.     public boolean inMetadata() {
  171.         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processDataToken);
  172.         return true;
  173.     }

  174.     /** {@inheritDoc} */
  175.     @Override
  176.     public boolean finalizeMetadata() {
  177.         metadata.validate(header.getFormatVersion());
  178.         return true;
  179.     }

  180.     /** {@inheritDoc} */
  181.     @Override
  182.     public boolean prepareData() {
  183.         commentsBlock = new CommentsContainer();
  184.         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
  185.                        this::processDataToken : this::processDataSubStructureToken);
  186.         return true;
  187.     }

  188.     /** {@inheritDoc} */
  189.     @Override
  190.     public boolean inData() {
  191.         return true;
  192.     }

  193.     /** {@inheritDoc} */
  194.     @Override
  195.     public boolean finalizeData() {
  196.         if (metadata != null) {
  197.             final ApmData data = new ApmData(commentsBlock, epoch, quaternionBlock, eulerBlock,
  198.                                              angularVelocityBlock, spinStabilizedBlock, inertiaBlock);
  199.             if (currentManeuver != null) {
  200.                 // current maneuver is completed
  201.                 maneuvers.add(currentManeuver);
  202.                 currentManeuver = null;
  203.             }
  204.             for (final Maneuver maneuver : maneuvers) {
  205.                 data.addManeuver(maneuver);
  206.             }
  207.             data.validate(header.getFormatVersion());
  208.             segments.add(new Segment<>(metadata, data));
  209.         }
  210.         metadata             = null;
  211.         context              = null;
  212.         quaternionBlock      = null;
  213.         eulerBlock           = null;
  214.         angularVelocityBlock = null;
  215.         spinStabilizedBlock  = null;
  216.         inertiaBlock         = null;
  217.         currentManeuver      = null;
  218.         return true;
  219.     }

  220.     /** {@inheritDoc} */
  221.     @Override
  222.     public Apm build() {
  223.         // APM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
  224.         // automatically before the end of the file
  225.         finalizeData();
  226.         final Apm file = new Apm(header, segments, getConventions(), getDataContext());
  227.         return file;
  228.     }

  229.     /** Add a general comment.
  230.      * @param comment comment to add
  231.      * @return always return true
  232.      */
  233.     boolean addGeneralComment(final String comment) {
  234.         return commentsBlock.addComment(comment);
  235.     }

  236.     /** Set current epoch.
  237.      * @param epoch epoch to set
  238.      * @since 12.0
  239.      */
  240.     void setEpoch(final AbsoluteDate epoch) {
  241.         this.epoch = epoch;
  242.     }

  243.     /** Manage quaternion section.
  244.      * @param starting if true, parser is entering the section
  245.      * otherwise it is leaving the section
  246.      * @return always return true
  247.      */
  248.     boolean manageQuaternionSection(final boolean starting) {
  249.         anticipateNext(starting ? this::processQuaternionToken : structureProcessor);
  250.         return true;
  251.     }

  252.     /** Manage Euler elements / three axis stabilized 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 manageEulerElementsSection(final boolean starting) {
  258.         anticipateNext(starting ? this::processEulerToken : structureProcessor);
  259.         return true;
  260.     }

  261.     /** Manage angular velocity section.
  262.      * @param starting if true, parser is entering the section
  263.      * otherwise it is leaving the section
  264.      * @return always return true
  265.      * @since 12.0
  266.      */
  267.     boolean manageAngularVelocitylementsSection(final boolean starting) {
  268.         anticipateNext(starting ? this::processAngularVelocityToken : structureProcessor);
  269.         return true;
  270.     }

  271.     /** Manage Euler elements /spin stabilized section.
  272.      * @param starting if true, parser is entering the section
  273.      * otherwise it is leaving the section
  274.      * @return always return true
  275.      */
  276.     boolean manageSpinElementsSection(final boolean starting) {
  277.         anticipateNext(starting ? this::processSpinStabilizedToken : structureProcessor);
  278.         return true;
  279.     }

  280.     /** Manage inertia section.
  281.      * @param starting if true, parser is entering the section
  282.      * otherwise it is leaving the section
  283.      * @return always return true
  284.      * @since 12.0
  285.      */
  286.     boolean manageInertiaSection(final boolean starting) {
  287.         anticipateNext(starting ? this::processInertiaToken : structureProcessor);
  288.         return true;
  289.     }

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

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

  326.     /** Process one data substructure token.
  327.      * @param token token to process
  328.      * @return true if token was processed, false otherwise
  329.      */
  330.     private boolean processDataSubStructureToken(final ParseToken token) {
  331.         try {
  332.             return token.getName() != null &&
  333.                    ApmDataSubStructureKey.valueOf(token.getName()).process(token, context, this);
  334.         } catch (IllegalArgumentException iae) {
  335.             // token has not been recognized
  336.             return false;
  337.         }
  338.     }

  339.     /** Process one data token.
  340.      * @param token token to process
  341.      * @return true if token was processed, false otherwise
  342.      */
  343.     private boolean processDataToken(final ParseToken token) {
  344.         if (commentsBlock == null) {
  345.             // APM KVN file lack a META_STOP keyword, hence we can't call finalizeMetadata()
  346.             // automatically before the first data token arrives
  347.             finalizeMetadata();
  348.             // APM KVN file lack a DATA_START keyword, hence we can't call prepareData()
  349.             // automatically before the first data token arrives
  350.             prepareData();
  351.         }
  352.         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
  353.                        this::processQuaternionToken : this::processDataSubStructureToken);
  354.         if ("COMMENT".equals(token.getName())) {
  355.             if (token.getType() == TokenType.ENTRY) {
  356.                 commentsBlock.addComment(token.getContentAsNormalizedString());
  357.             }
  358.             return true;
  359.         } else if ("EPOCH".equals(token.getName())) {
  360.             if (token.getType() == TokenType.ENTRY) {
  361.                 token.processAsDate(date -> epoch = date, context);
  362.             }
  363.             return true;
  364.         } else {
  365.             return false;
  366.         }
  367.     }

  368.     /** Process one quaternion data token.
  369.      * @param token token to process
  370.      * @return true if token was processed, false otherwise
  371.      */
  372.     private boolean processQuaternionToken(final ParseToken token) {
  373.         commentsBlock.refuseFurtherComments();
  374.         if (quaternionBlock == null) {
  375.             quaternionBlock = new ApmQuaternion();
  376.         }
  377.         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
  378.                        this::processEulerToken : this::processDataSubStructureToken);
  379.         try {
  380.             return token.getName() != null &&
  381.                    ApmQuaternionKey.valueOf(token.getName()).process(token, context, quaternionBlock, this::setEpoch);
  382.         } catch (IllegalArgumentException iae) {
  383.             // token has not been recognized
  384.             return false;
  385.         }
  386.     }

  387.     /** Process one Euler angles data token.
  388.      * @param token token to process
  389.      * @return true if token was processed, false otherwise
  390.      */
  391.     private boolean processEulerToken(final ParseToken token) {
  392.         commentsBlock.refuseFurtherComments();
  393.         if (eulerBlock == null) {
  394.             eulerBlock = new Euler();
  395.             if (moveCommentsIfEmpty(quaternionBlock, eulerBlock)) {
  396.                 // get rid of the empty logical block
  397.                 quaternionBlock = null;
  398.             }
  399.         }
  400.         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
  401.                        this::processAngularVelocityToken : this::processDataSubStructureToken);
  402.         try {
  403.             return token.getName() != null &&
  404.                    EulerKey.valueOf(token.getName()).process(token, context, eulerBlock);
  405.         } catch (IllegalArgumentException iae) {
  406.             // token has not been recognized
  407.             return false;
  408.         }
  409.     }

  410.     /** Process one angular velocity data token.
  411.      * @param token token to process
  412.      * @return true if token was processed, false otherwise
  413.      * @since 12.0
  414.      */
  415.     private boolean processAngularVelocityToken(final ParseToken token) {
  416.         commentsBlock.refuseFurtherComments();
  417.         if (angularVelocityBlock == null) {
  418.             angularVelocityBlock = new AngularVelocity();
  419.             if (moveCommentsIfEmpty(eulerBlock, angularVelocityBlock)) {
  420.                 // get rid of the empty logical block
  421.                 eulerBlock = null;
  422.             }
  423.         }
  424.         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
  425.                        this::processSpinStabilizedToken : this::processDataSubStructureToken);
  426.         try {
  427.             return token.getName() != null &&
  428.                    AngularVelocityKey.valueOf(token.getName()).process(token, context, angularVelocityBlock);
  429.         } catch (IllegalArgumentException iae) {
  430.             // token has not been recognized
  431.             return false;
  432.         }
  433.     }

  434.     /** Process one spin-stabilized data token.
  435.      * @param token token to process
  436.      * @return true if token was processed, false otherwise
  437.      */
  438.     private boolean processSpinStabilizedToken(final ParseToken token) {
  439.         commentsBlock.refuseFurtherComments();
  440.         if (spinStabilizedBlock == null) {
  441.             spinStabilizedBlock = new SpinStabilized();
  442.             if (moveCommentsIfEmpty(angularVelocityBlock, spinStabilizedBlock)) {
  443.                 // get rid of the empty logical block
  444.                 angularVelocityBlock = null;
  445.             }
  446.         }
  447.         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
  448.                        this::processInertiaToken : this::processDataSubStructureToken);
  449.         try {
  450.             return token.getName() != null &&
  451.                    SpinStabilizedKey.valueOf(token.getName()).process(token, context, spinStabilizedBlock);
  452.         } catch (IllegalArgumentException iae) {
  453.             // token has not been recognized
  454.             return false;
  455.         }
  456.     }

  457.     /** Process one spacecraft parameters data token.
  458.      * @param token token to process
  459.      * @return true if token was processed, false otherwise
  460.      */
  461.     private boolean processInertiaToken(final ParseToken token) {
  462.         commentsBlock.refuseFurtherComments();
  463.         if (inertiaBlock == null) {
  464.             inertiaBlock = new Inertia();
  465.             if (moveCommentsIfEmpty(spinStabilizedBlock, inertiaBlock)) {
  466.                 // get rid of the empty logical block
  467.                 spinStabilizedBlock = null;
  468.             }
  469.         }
  470.         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
  471.                                           this::processManeuverToken : this::processDataSubStructureToken);
  472.         try {
  473.             return token.getName() != null &&
  474.                    InertiaKey.valueOf(token.getName()).process(token, context, inertiaBlock);
  475.         } catch (IllegalArgumentException iae) {
  476.             // token has not been recognized
  477.             return false;
  478.         }
  479.     }

  480.     /** Process one maneuver data token.
  481.      * @param token token to process
  482.      * @return true if token was processed, false otherwise
  483.      */
  484.     private boolean processManeuverToken(final ParseToken token) {
  485.         commentsBlock.refuseFurtherComments();
  486.         if (currentManeuver == null) {
  487.             currentManeuver = new Maneuver();
  488.             if (moveCommentsIfEmpty(inertiaBlock, currentManeuver)) {
  489.                 // get rid of the empty logical block
  490.                 inertiaBlock = null;
  491.             }
  492.         }
  493.         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
  494.                        new ErrorState() : this::processDataSubStructureToken);
  495.         try {
  496.             return token.getName() != null &&
  497.                    ManeuverKey.valueOf(token.getName()).process(token, context, currentManeuver);
  498.         } catch (IllegalArgumentException iae) {
  499.             // token has not been recognized
  500.             maneuvers.add(currentManeuver);
  501.             currentManeuver = null;
  502.             return false;
  503.         }
  504.     }

  505.     /** Move comments from one empty logical block to another logical block.
  506.      * @param origin origin block
  507.      * @param destination destination block
  508.      * @return true if origin block was empty
  509.      */
  510.     private boolean moveCommentsIfEmpty(final CommentsContainer origin, final CommentsContainer destination) {
  511.         if (origin != null && origin.acceptComments()) {
  512.             // origin block is empty, move the existing comments
  513.             for (final String comment : origin.getComments()) {
  514.                 destination.addComment(comment);
  515.             }
  516.             return true;
  517.         } else {
  518.             return false;
  519.         }
  520.     }

  521. }