1   /* Copyright 2002-2026 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  
19  import java.util.ArrayList;
20  import java.util.List;
21  import java.util.function.Function;
22  
23  import org.orekit.data.DataContext;
24  import org.orekit.files.ccsds.definitions.CcsdsFrameMapper;
25  import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
26  import org.orekit.files.ccsds.ndm.adm.AdmCommonMetadataKey;
27  import org.orekit.files.ccsds.ndm.adm.AdmHeader;
28  import org.orekit.files.ccsds.ndm.adm.AdmMetadata;
29  import org.orekit.files.ccsds.ndm.adm.AdmMetadataKey;
30  import org.orekit.files.ccsds.ndm.adm.AdmParser;
31  import org.orekit.files.ccsds.section.CommentsContainer;
32  import org.orekit.files.ccsds.section.HeaderProcessingState;
33  import org.orekit.files.ccsds.section.MetadataKey;
34  import org.orekit.files.ccsds.section.Segment;
35  import org.orekit.files.ccsds.section.XmlStructureProcessingState;
36  import org.orekit.files.ccsds.utils.ContextBinding;
37  import org.orekit.files.ccsds.utils.FileFormat;
38  import org.orekit.files.ccsds.utils.lexical.ParseToken;
39  import org.orekit.files.ccsds.utils.lexical.TokenType;
40  import org.orekit.files.ccsds.utils.parsing.ErrorState;
41  import org.orekit.files.ccsds.utils.parsing.ProcessingState;
42  import org.orekit.frames.Frame;
43  import org.orekit.time.AbsoluteDate;
44  import org.orekit.utils.IERSConventions;
45  
46  /**
47   * A parser for the CCSDS APM (Attitude Parameter Message).
48   * @author Bryan Cazabonne * <p>
49   * Note than starting with Orekit 11.0, CCSDS message parsers are
50   * mutable objects that gather the data being parsed, until the
51   * message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
52   * parseMessage} method has returned. This implies that parsers
53   * should <em>not</em> be used in a multi-thread context. The recommended
54   * way to use parsers is to either dedicate one parser for each message
55   * and drop it afterwards, or to use a single-thread loop.
56   * </p>
57  
58   * @since 10.2
59   */
60  public class ApmParser extends AdmParser<Apm, ApmParser> {
61  
62      /** File header. */
63      private AdmHeader header;
64  
65      /** File segments. */
66      private List<Segment<AdmMetadata, ApmData>> segments;
67  
68      /** APM metadata being read. */
69      private AdmMetadata metadata;
70  
71      /** Context binding valid for current metadata. */
72      private ContextBinding context;
73  
74      /** APM epoch.
75       * @since 12.0
76       */
77      private AbsoluteDate epoch;
78  
79      /** APM general comments block being read. */
80      private CommentsContainer commentsBlock;
81  
82      /** APM quaternion logical block being read. */
83      private ApmQuaternion quaternionBlock;
84  
85      /** APM Euler angles logical block being read. */
86      private Euler eulerBlock;
87  
88      /** APM angular velocity logical block being read.
89       * @since 12.0
90       */
91      private AngularVelocity angularVelocityBlock;
92  
93      /** APM spin-stabilized logical block being read. */
94      private SpinStabilized spinStabilizedBlock;
95  
96      /** APM inertia block being read.
97       * @since 12.0
98       */
99      private Inertia inertiaBlock;
100 
101     /** Current maneuver. */
102     private Maneuver currentManeuver;
103 
104     /** All maneuvers. */
105     private List<Maneuver> maneuvers;
106 
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#buildApmParser()
114      * parserBuilder.buildApmParser()}.
115      * </p>
116      * @param conventions IERS Conventions
117      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
118      * @param dataContext used to retrieve frames, time scales, etc.
119      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
120      * (may be null if time system is absolute)
121      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
122      * @param filters filters to apply to parse tokens
123      * @param frameMapper for creating an Orekit {@link Frame}.
124      * @since 13.1.5
125      */
126     public ApmParser(final IERSConventions conventions, final boolean simpleEOP, final DataContext dataContext,
127                      final AbsoluteDate missionReferenceDate, final ParsedUnitsBehavior parsedUnitsBehavior,
128                      final Function<ParseToken, List<ParseToken>>[] filters,
129                      final CcsdsFrameMapper frameMapper) {
130         super(Apm.ROOT, Apm.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext,
131               missionReferenceDate, parsedUnitsBehavior, filters, frameMapper);
132     }
133 
134     /** {@inheritDoc} */
135     @Override
136     public AdmHeader getHeader() {
137         return header;
138     }
139 
140     /** {@inheritDoc} */
141     @Override
142     public void reset(final FileFormat fileFormat) {
143         header              = new AdmHeader();
144         segments            = new ArrayList<>();
145         metadata            = null;
146         context             = null;
147         quaternionBlock     = null;
148         eulerBlock          = null;
149         spinStabilizedBlock = null;
150         inertiaBlock        = null;
151         currentManeuver     = null;
152         maneuvers           = new ArrayList<>();
153         if (fileFormat == FileFormat.XML) {
154             structureProcessor = new XmlStructureProcessingState(Apm.ROOT, this);
155             reset(fileFormat, structureProcessor);
156         } else {
157             structureProcessor = new ErrorState(); // should never be called
158             reset(fileFormat, new HeaderProcessingState(this));
159         }
160     }
161 
162     /** {@inheritDoc} */
163     @Override
164     public boolean prepareHeader() {
165         anticipateNext(new HeaderProcessingState(this));
166         return true;
167     }
168 
169     /** {@inheritDoc} */
170     @Override
171     public boolean inHeader() {
172         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
173         return true;
174     }
175 
176     /** {@inheritDoc} */
177     @Override
178     public boolean finalizeHeader() {
179         header.validate(header.getFormatVersion());
180         return true;
181     }
182 
183     /** {@inheritDoc} */
184     @Override
185     public boolean prepareMetadata() {
186         if (metadata != null) {
187             return false;
188         }
189         metadata  = new AdmMetadata(getFrameMapper());
190         context   = new ContextBinding(this::getConventions, this::isSimpleEOP,
191                                        this::getDataContext, this::getParsedUnitsBehavior,
192                                        this::getMissionReferenceDate,
193                                        metadata::getTimeSystem, () -> 0.0, () -> 1.0);
194         anticipateNext(this::processMetadataToken);
195         return true;
196     }
197 
198     /** {@inheritDoc} */
199     @Override
200     public boolean inMetadata() {
201         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processDataToken);
202         return true;
203     }
204 
205     /** {@inheritDoc} */
206     @Override
207     public boolean finalizeMetadata() {
208         metadata.validate(header.getFormatVersion());
209         return true;
210     }
211 
212     /** {@inheritDoc} */
213     @Override
214     public boolean prepareData() {
215         commentsBlock = new CommentsContainer();
216         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
217                        this::processDataToken : this::processDataSubStructureToken);
218         return true;
219     }
220 
221     /** {@inheritDoc} */
222     @Override
223     public boolean inData() {
224         return true;
225     }
226 
227     /** {@inheritDoc} */
228     @Override
229     public boolean finalizeData() {
230         if (metadata != null) {
231             final ApmData data = new ApmData(commentsBlock, epoch, quaternionBlock, eulerBlock,
232                                              angularVelocityBlock, spinStabilizedBlock, inertiaBlock);
233             if (currentManeuver != null) {
234                 // current maneuver is completed
235                 maneuvers.add(currentManeuver);
236                 currentManeuver = null;
237             }
238             for (final Maneuver maneuver : maneuvers) {
239                 data.addManeuver(maneuver);
240             }
241             data.validate(header.getFormatVersion());
242             segments.add(new Segment<>(metadata, data));
243         }
244         metadata             = null;
245         context              = null;
246         quaternionBlock      = null;
247         eulerBlock           = null;
248         angularVelocityBlock = null;
249         spinStabilizedBlock  = null;
250         inertiaBlock         = null;
251         currentManeuver      = null;
252         return true;
253     }
254 
255     /** {@inheritDoc} */
256     @Override
257     public Apm build() {
258         // APM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
259         // automatically before the end of the file
260         finalizeData();
261         final Apm file = new Apm(header, segments, getConventions(), getDataContext());
262         return file;
263     }
264 
265     /** Add a general comment.
266      * @param comment comment to add
267      * @return always return true
268      */
269     boolean addGeneralComment(final String comment) {
270         return commentsBlock.addComment(comment);
271     }
272 
273     /** Set current epoch.
274      * @param epoch epoch to set
275      * @since 12.0
276      */
277     void setEpoch(final AbsoluteDate epoch) {
278         this.epoch = epoch;
279     }
280 
281     /** Manage quaternion 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 manageQuaternionSection(final boolean starting) {
287         anticipateNext(starting ? this::processQuaternionToken : structureProcessor);
288         return true;
289     }
290 
291     /** Manage Euler elements / three axis stabilized section.
292      * @param starting if true, parser is entering the section
293      * otherwise it is leaving the section
294      * @return always return true
295      */
296     boolean manageEulerElementsSection(final boolean starting) {
297         anticipateNext(starting ? this::processEulerToken : structureProcessor);
298         return true;
299     }
300 
301     /** Manage angular velocity section.
302      * @param starting if true, parser is entering the section
303      * otherwise it is leaving the section
304      * @return always return true
305      * @since 12.0
306      */
307     boolean manageAngularVelocitylementsSection(final boolean starting) {
308         anticipateNext(starting ? this::processAngularVelocityToken : structureProcessor);
309         return true;
310     }
311 
312     /** Manage Euler elements /spin stabilized section.
313      * @param starting if true, parser is entering the section
314      * otherwise it is leaving the section
315      * @return always return true
316      */
317     boolean manageSpinElementsSection(final boolean starting) {
318         anticipateNext(starting ? this::processSpinStabilizedToken : structureProcessor);
319         return true;
320     }
321 
322     /** Manage inertia section.
323      * @param starting if true, parser is entering the section
324      * otherwise it is leaving the section
325      * @return always return true
326      * @since 12.0
327      */
328     boolean manageInertiaSection(final boolean starting) {
329         anticipateNext(starting ? this::processInertiaToken : structureProcessor);
330         return true;
331     }
332 
333     /** Manage maneuver parameters section.
334      * @param starting if true, parser is entering the section
335      * otherwise it is leaving the section
336      * @return always return true
337      */
338     boolean manageManeuverParametersSection(final boolean starting) {
339         anticipateNext(starting ? this::processManeuverToken : structureProcessor);
340         return true;
341     }
342 
343     /** Process one metadata token.
344      * @param token token to process
345      * @return true if token was processed, false otherwise
346      */
347     private boolean processMetadataToken(final ParseToken token) {
348         if (metadata == null) {
349             // APM KVN file lack a META_START keyword, hence we can't call prepareMetadata()
350             // automatically before the first metadata token arrives
351             prepareMetadata();
352         }
353         inMetadata();
354         try {
355             return token.getName() != null &&
356                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
357         } catch (IllegalArgumentException iaeM) {
358             try {
359                 return AdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
360             } catch (IllegalArgumentException iaeD) {
361                 try {
362                     return AdmCommonMetadataKey.valueOf(token.getName()).process(token, context, metadata);
363                 } catch (IllegalArgumentException iaeC) {
364                     // token has not been recognized
365                     return false;
366                 }
367             }
368         }
369     }
370 
371     /** Process one data substructure token.
372      * @param token token to process
373      * @return true if token was processed, false otherwise
374      */
375     private boolean processDataSubStructureToken(final ParseToken token) {
376         try {
377             return token.getName() != null &&
378                    ApmDataSubStructureKey.valueOf(token.getName()).process(token, context, this);
379         } catch (IllegalArgumentException iae) {
380             // token has not been recognized
381             return false;
382         }
383     }
384 
385     /** Process one data token.
386      * @param token token to process
387      * @return true if token was processed, false otherwise
388      */
389     private boolean processDataToken(final ParseToken token) {
390         if (commentsBlock == null) {
391             // APM KVN file lack a META_STOP keyword, hence we can't call finalizeMetadata()
392             // automatically before the first data token arrives
393             finalizeMetadata();
394             // APM KVN file lack a DATA_START keyword, hence we can't call prepareData()
395             // automatically before the first data token arrives
396             prepareData();
397         }
398         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
399                        this::processQuaternionToken : this::processDataSubStructureToken);
400         if ("COMMENT".equals(token.getName())) {
401             if (token.getType() == TokenType.ENTRY) {
402                 commentsBlock.addComment(token.getContentAsNormalizedString());
403             }
404             return true;
405         } else if ("EPOCH".equals(token.getName())) {
406             if (token.getType() == TokenType.ENTRY) {
407                 token.processAsDate(date -> epoch = date, context);
408             }
409             return true;
410         } else {
411             return false;
412         }
413     }
414 
415     /** Process one quaternion data token.
416      * @param token token to process
417      * @return true if token was processed, false otherwise
418      */
419     private boolean processQuaternionToken(final ParseToken token) {
420         commentsBlock.refuseFurtherComments();
421         if (quaternionBlock == null) {
422             quaternionBlock = new ApmQuaternion(getFrameMapper());
423         }
424         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
425                        this::processEulerToken : this::processDataSubStructureToken);
426         try {
427             return token.getName() != null &&
428                    ApmQuaternionKey.valueOf(token.getName()).process(token, context, quaternionBlock, this::setEpoch);
429         } catch (IllegalArgumentException iae) {
430             // token has not been recognized
431             return false;
432         }
433     }
434 
435     /** Process one Euler angles data token.
436      * @param token token to process
437      * @return true if token was processed, false otherwise
438      */
439     private boolean processEulerToken(final ParseToken token) {
440         commentsBlock.refuseFurtherComments();
441         if (eulerBlock == null) {
442             eulerBlock = new Euler(getFrameMapper());
443             if (moveCommentsIfEmpty(quaternionBlock, eulerBlock)) {
444                 // get rid of the empty logical block
445                 quaternionBlock = null;
446             }
447         }
448         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
449                        this::processAngularVelocityToken : this::processDataSubStructureToken);
450         try {
451             return token.getName() != null &&
452                    EulerKey.valueOf(token.getName()).process(token, context, eulerBlock);
453         } catch (IllegalArgumentException iae) {
454             // token has not been recognized
455             return false;
456         }
457     }
458 
459     /** Process one angular velocity data token.
460      * @param token token to process
461      * @return true if token was processed, false otherwise
462      * @since 12.0
463      */
464     private boolean processAngularVelocityToken(final ParseToken token) {
465         commentsBlock.refuseFurtherComments();
466         if (angularVelocityBlock == null) {
467             angularVelocityBlock = new AngularVelocity(getFrameMapper());
468             if (moveCommentsIfEmpty(eulerBlock, angularVelocityBlock)) {
469                 // get rid of the empty logical block
470                 eulerBlock = null;
471             }
472         }
473         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
474                        this::processSpinStabilizedToken : this::processDataSubStructureToken);
475         try {
476             return token.getName() != null &&
477                    AngularVelocityKey.valueOf(token.getName()).process(token, context, angularVelocityBlock);
478         } catch (IllegalArgumentException iae) {
479             // token has not been recognized
480             return false;
481         }
482     }
483 
484     /** Process one spin-stabilized data token.
485      * @param token token to process
486      * @return true if token was processed, false otherwise
487      */
488     private boolean processSpinStabilizedToken(final ParseToken token) {
489         commentsBlock.refuseFurtherComments();
490         if (spinStabilizedBlock == null) {
491             spinStabilizedBlock = new SpinStabilized(getFrameMapper());
492             if (moveCommentsIfEmpty(angularVelocityBlock, spinStabilizedBlock)) {
493                 // get rid of the empty logical block
494                 angularVelocityBlock = null;
495             }
496         }
497         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
498                        this::processInertiaToken : this::processDataSubStructureToken);
499         try {
500             return token.getName() != null &&
501                    SpinStabilizedKey.valueOf(token.getName()).process(token, context, spinStabilizedBlock);
502         } catch (IllegalArgumentException iae) {
503             // token has not been recognized
504             return false;
505         }
506     }
507 
508     /** Process one spacecraft parameters data token.
509      * @param token token to process
510      * @return true if token was processed, false otherwise
511      */
512     private boolean processInertiaToken(final ParseToken token) {
513         commentsBlock.refuseFurtherComments();
514         if (inertiaBlock == null) {
515             inertiaBlock = new Inertia();
516             if (moveCommentsIfEmpty(spinStabilizedBlock, inertiaBlock)) {
517                 // get rid of the empty logical block
518                 spinStabilizedBlock = null;
519             }
520         }
521         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
522                                           this::processManeuverToken : this::processDataSubStructureToken);
523         try {
524             return token.getName() != null &&
525                    InertiaKey.valueOf(token.getName()).process(token, context, inertiaBlock);
526         } catch (IllegalArgumentException iae) {
527             // token has not been recognized
528             return false;
529         }
530     }
531 
532     /** Process one maneuver data token.
533      * @param token token to process
534      * @return true if token was processed, false otherwise
535      */
536     private boolean processManeuverToken(final ParseToken token) {
537         commentsBlock.refuseFurtherComments();
538         if (currentManeuver == null) {
539             currentManeuver = new Maneuver();
540             if (moveCommentsIfEmpty(inertiaBlock, currentManeuver)) {
541                 // get rid of the empty logical block
542                 inertiaBlock = null;
543             }
544         }
545         anticipateNext(getFileFormat() == FileFormat.KVN && header.getFormatVersion() < 2.0 ?
546                        new ErrorState() : this::processDataSubStructureToken);
547         try {
548             return token.getName() != null &&
549                    ManeuverKey.valueOf(token.getName()).process(token, context, currentManeuver);
550         } catch (IllegalArgumentException iae) {
551             // token has not been recognized
552             maneuvers.add(currentManeuver);
553             currentManeuver = null;
554             return false;
555         }
556     }
557 
558     /** Move comments from one empty logical block to another logical block.
559      * @param origin origin block
560      * @param destination destination block
561      * @return true if origin block was empty
562      */
563     private boolean moveCommentsIfEmpty(final CommentsContainer origin, final CommentsContainer destination) {
564         if (origin != null && origin.acceptComments()) {
565             // origin block is empty, move the existing comments
566             for (final String comment : origin.getComments()) {
567                 destination.addComment(comment);
568             }
569             return true;
570         } else {
571             return false;
572         }
573     }
574 
575 }