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