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