1   /* Copyright 2022-2025 Luc Maisonobe
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.acm;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.function.Function;
24  import java.util.regex.Pattern;
25  
26  import org.orekit.data.DataContext;
27  import org.orekit.data.DataSource;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitIllegalArgumentException;
30  import org.orekit.errors.OrekitMessages;
31  import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
32  import org.orekit.files.ccsds.ndm.adm.AdmHeader;
33  import org.orekit.files.ccsds.ndm.adm.AdmMetadataKey;
34  import org.orekit.files.ccsds.ndm.adm.AdmParser;
35  import org.orekit.files.ccsds.ndm.odm.UserDefined;
36  import org.orekit.files.ccsds.ndm.odm.ocm.Ocm;
37  import org.orekit.files.ccsds.section.HeaderProcessingState;
38  import org.orekit.files.ccsds.section.KvnStructureProcessingState;
39  import org.orekit.files.ccsds.section.MetadataKey;
40  import org.orekit.files.ccsds.section.Segment;
41  import org.orekit.files.ccsds.section.XmlStructureProcessingState;
42  import org.orekit.files.ccsds.utils.ContextBinding;
43  import org.orekit.files.ccsds.utils.FileFormat;
44  import org.orekit.files.ccsds.utils.lexical.ParseToken;
45  import org.orekit.files.ccsds.utils.lexical.TokenType;
46  import org.orekit.files.ccsds.utils.lexical.UserDefinedXmlTokenBuilder;
47  import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
48  import org.orekit.files.ccsds.utils.parsing.ProcessingState;
49  import org.orekit.files.general.AttitudeEphemerisFileParser;
50  import org.orekit.time.AbsoluteDate;
51  import org.orekit.utils.IERSConventions;
52  
53  /** A parser for the CCSDS ACM (Attitude Comprehensive Message).
54   * @author Luc Maisonobe
55   * @since 12.0
56   */
57  public class AcmParser extends AdmParser<Acm, AcmParser> implements AttitudeEphemerisFileParser<Acm> {
58  
59      /** Pattern for splitting strings at blanks. */
60      private static final Pattern SPLIT_AT_BLANKS = Pattern.compile("\\s+");
61  
62      /** File header. */
63      private AdmHeader header;
64  
65      /** Metadata for current observation block. */
66      private AcmMetadata metadata;
67  
68      /** Context binding valid for current metadata. */
69      private ContextBinding context;
70  
71      /** Attitude state histories logical blocks. */
72      private List<AttitudeStateHistory> attitudeBlocks;
73  
74      /** Current attitude state metadata. */
75      private AttitudeStateHistoryMetadata currentAttitudeStateHistoryMetadata;
76  
77      /** Current attitude state time history being read. */
78      private List<AttitudeState> currentAttitudeStateHistory;
79  
80      /** Physical properties logical block. */
81      private AttitudePhysicalProperties physicBlock;
82  
83      /** Covariance logical blocks. */
84      private List<AttitudeCovarianceHistory> covarianceBlocks;
85  
86      /** Current covariance metadata. */
87      private AttitudeCovarianceHistoryMetadata currentCovarianceHistoryMetadata;
88  
89      /** Current covariance history being read. */
90      private List<AttitudeCovariance> currentCovarianceHistory;
91  
92      /** Maneuver logical blocks. */
93      private List<AttitudeManeuver> maneuverBlocks;
94  
95      /** Current maneuver history being read. */
96      private AttitudeManeuver currentManeuver;
97  
98      /** Attitude determination logical block. */
99      private AttitudeDetermination attitudeDeterminationBlock;
100 
101     /** Attitude determination sensor logical block. */
102     private AttitudeDeterminationSensor attitudeDeterminationSensorBlock;
103 
104     /** User defined parameters logical block. */
105     private UserDefined userDefinedBlock;
106 
107     /** Processor for global message structure. */
108     private ProcessingState structureProcessor;
109 
110     /**
111      * Complete constructor.
112      * <p>
113      * Calling this constructor directly is not recommended. Users should rather use
114      * {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildAcmParser()
115      * parserBuilder.buildAcmParser()}.
116      * </p>
117      * @param conventions IERS Conventions
118      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
119      * @param dataContext used to retrieve frames, time scales, etc.
120      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
121      * @param filters filters to apply to parse tokens
122      * @since 12.0
123      */
124     public AcmParser(final IERSConventions conventions, final boolean simpleEOP, final DataContext dataContext,
125                      final ParsedUnitsBehavior parsedUnitsBehavior,
126                      final Function<ParseToken, List<ParseToken>>[] filters) {
127         super(Acm.ROOT, Acm.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext, null,
128               parsedUnitsBehavior, filters);
129     }
130 
131     /** {@inheritDoc} */
132     @Override
133     public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {
134 
135         final Map<String, XmlTokenBuilder> builders = super.getSpecialXmlElementsBuilders();
136 
137         // special handling of user-defined parameters
138         builders.put(UserDefined.USER_DEFINED_XML_TAG, new UserDefinedXmlTokenBuilder());
139 
140         return builders;
141 
142     }
143 
144     /** {@inheritDoc} */
145     @Override
146     public Acm parse(final DataSource source) {
147         return parseMessage(source);
148     }
149 
150     /** {@inheritDoc} */
151     @Override
152     public AdmHeader getHeader() {
153         return header;
154     }
155 
156     /** {@inheritDoc} */
157     @Override
158     public void reset(final FileFormat fileFormat) {
159         header                     = new AdmHeader();
160         metadata                   = null;
161         context                    = null;
162         attitudeBlocks             = null;
163         physicBlock                = null;
164         covarianceBlocks           = null;
165         maneuverBlocks             = null;
166         attitudeDeterminationBlock = null;
167         userDefinedBlock           = null;
168         if (fileFormat == FileFormat.XML) {
169             structureProcessor = new XmlStructureProcessingState(Acm.ROOT, this);
170             reset(fileFormat, structureProcessor);
171         } else {
172             structureProcessor = new KvnStructureProcessingState(this);
173             reset(fileFormat, new HeaderProcessingState(this));
174         }
175     }
176 
177     /** {@inheritDoc} */
178     @Override
179     public boolean prepareHeader() {
180         anticipateNext(new HeaderProcessingState(this));
181         return true;
182     }
183 
184     /** {@inheritDoc} */
185     @Override
186     public boolean inHeader() {
187         anticipateNext(structureProcessor);
188         return true;
189     }
190 
191     /** {@inheritDoc} */
192     @Override
193     public boolean finalizeHeader() {
194         header.validate(header.getFormatVersion());
195         return true;
196     }
197 
198     /** {@inheritDoc} */
199     @Override
200     public boolean prepareMetadata() {
201         if (metadata != null) {
202             return false;
203         }
204         metadata  = new AcmMetadata(getDataContext());
205         context   = new ContextBinding(this::getConventions, this::isSimpleEOP, this::getDataContext,
206                                        this::getParsedUnitsBehavior, metadata::getEpochT0, metadata::getTimeSystem,
207                                        () -> 0.0, () -> 1.0);
208         anticipateNext(this::processMetadataToken);
209         return true;
210     }
211 
212     /** {@inheritDoc} */
213     @Override
214     public boolean inMetadata() {
215         anticipateNext(structureProcessor);
216         return true;
217     }
218 
219     /** {@inheritDoc} */
220     @Override
221     public boolean finalizeMetadata() {
222         metadata.validate(header.getFormatVersion());
223         anticipateNext(this::processDataSubStructureToken);
224         return true;
225     }
226 
227     /** {@inheritDoc} */
228     @Override
229     public boolean prepareData() {
230         anticipateNext(this::processDataSubStructureToken);
231         return true;
232     }
233 
234     /** {@inheritDoc} */
235     @Override
236     public boolean inData() {
237         return true;
238     }
239 
240     /** {@inheritDoc} */
241     @Override
242     public boolean finalizeData() {
243         return true;
244     }
245 
246     /** Manage attitude state history section.
247      * @param starting if true, parser is entering the section
248      * otherwise it is leaving the section
249      * @return always return true
250      */
251     boolean manageAttitudeStateSection(final boolean starting) {
252         if (starting) {
253             if (attitudeBlocks == null) {
254                 // this is the first attitude block, we need to allocate the container
255                 attitudeBlocks = new ArrayList<>();
256             }
257             currentAttitudeStateHistoryMetadata = new AttitudeStateHistoryMetadata();
258             currentAttitudeStateHistory         = new ArrayList<>();
259             anticipateNext(this::processAttitudeStateToken);
260         } else {
261             anticipateNext(structureProcessor);
262             attitudeBlocks.add(new AttitudeStateHistory(currentAttitudeStateHistoryMetadata,
263                                                         currentAttitudeStateHistory));
264         }
265         return true;
266     }
267 
268     /** Manage physical properties section.
269      * @param starting if true, parser is entering the section
270      * otherwise it is leaving the section
271      * @return always return true
272      */
273     boolean managePhysicalPropertiesSection(final boolean starting) {
274         if (starting) {
275             physicBlock = new AttitudePhysicalProperties();
276             anticipateNext(this::processPhysicalPropertyToken);
277         } else {
278             anticipateNext(structureProcessor);
279         }
280         return true;
281     }
282 
283     /** Manage covariance history section.
284      * @param starting if true, parser is entering the section
285      * otherwise it is leaving the section
286      * @return always return true
287      */
288     boolean manageCovarianceHistorySection(final boolean starting) {
289         if (starting) {
290             if (covarianceBlocks == null) {
291                 // this is the first covariance block, we need to allocate the container
292                 covarianceBlocks = new ArrayList<>();
293             }
294             currentCovarianceHistoryMetadata = new AttitudeCovarianceHistoryMetadata();
295             currentCovarianceHistory         = new ArrayList<>();
296             anticipateNext(this::processCovarianceToken);
297         } else {
298             anticipateNext(structureProcessor);
299             covarianceBlocks.add(new AttitudeCovarianceHistory(currentCovarianceHistoryMetadata,
300                                                                currentCovarianceHistory));
301             currentCovarianceHistoryMetadata = null;
302             currentCovarianceHistory         = null;
303         }
304         return true;
305     }
306 
307     /** Manage maneuvers section.
308      * @param starting if true, parser is entering the section
309      * otherwise it is leaving the section
310      * @return always return true
311      */
312     boolean manageManeuversSection(final boolean starting) {
313         if (starting) {
314             if (maneuverBlocks == null) {
315                 // this is the first maneuver block, we need to allocate the container
316                 maneuverBlocks = new ArrayList<>();
317             }
318             currentManeuver = new AttitudeManeuver();
319             anticipateNext(this::processManeuverToken);
320         } else {
321             anticipateNext(structureProcessor);
322             maneuverBlocks.add(currentManeuver);
323             currentManeuver = null;
324         }
325         return true;
326     }
327 
328     /** Manage attitude determination section.
329      * @param starting if true, parser is entering the section
330      * otherwise it is leaving the section
331      * @return always return true
332      */
333     boolean manageAttitudeDeterminationSection(final boolean starting) {
334         if (starting) {
335             attitudeDeterminationBlock = new AttitudeDetermination();
336             anticipateNext(this::processAttitudeDeterminationToken);
337         } else {
338             anticipateNext(structureProcessor);
339         }
340         return true;
341     }
342 
343     /** Manage attitude determination sensor section.
344      * @param starting if true, parser is entering the section
345      * otherwise it is leaving the section
346      * @return always return true
347      */
348     boolean manageAttitudeDeterminationSensorSection(final boolean starting) {
349         if (starting) {
350             attitudeDeterminationSensorBlock = new AttitudeDeterminationSensor();
351             anticipateNext(this::processAttitudeDeterminationSensorToken);
352         } else {
353             anticipateNext(this::processDataSubStructureToken);
354         }
355         return true;
356     }
357 
358     /** Manage user-defined parameters section.
359      * @param starting if true, parser is entering the section
360      * otherwise it is leaving the section
361      * @return always return true
362      */
363     boolean manageUserDefinedParametersSection(final boolean starting) {
364         if (starting) {
365             userDefinedBlock = new UserDefined();
366             anticipateNext(this::processUserDefinedToken);
367         } else {
368             anticipateNext(structureProcessor);
369         }
370         return true;
371     }
372 
373     /** {@inheritDoc} */
374     @Override
375     public Acm build() {
376 
377         if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
378             userDefinedBlock = null;
379         }
380 
381         final AcmData data = new AcmData(attitudeBlocks, physicBlock, covarianceBlocks,
382                                          maneuverBlocks, attitudeDeterminationBlock, userDefinedBlock);
383         data.validate(header.getFormatVersion());
384 
385         return new Acm(header, Collections.singletonList(new Segment<>(metadata, data)),
386                            getConventions(), getDataContext());
387 
388     }
389 
390     /** Process one metadata token.
391      * @param token token to process
392      * @return true if token was processed, false otherwise
393      */
394     private boolean processMetadataToken(final ParseToken token) {
395         inMetadata();
396         try {
397             return token.getName() != null &&
398                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
399         } catch (IllegalArgumentException iaeM) {
400             try {
401                 return AdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
402             } catch (IllegalArgumentException iaeD) {
403                 try {
404                     return AcmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
405                 } catch (IllegalArgumentException iaeC) {
406                     // token has not been recognized
407                     return false;
408                 }
409             }
410         }
411     }
412 
413     /** Process one data substructure token.
414      * @param token token to process
415      * @return true if token was processed, false otherwise
416      */
417     private boolean processDataSubStructureToken(final ParseToken token) {
418         try {
419             return token.getName() != null &&
420                    AcmDataSubStructureKey.valueOf(token.getName()).process(token, this);
421         } catch (IllegalArgumentException iae) {
422             // token has not been recognized
423             return false;
424         }
425     }
426 
427     /** Process one attitude state history data token.
428      * @param token token to process
429      * @return true if token was processed, false otherwise
430      */
431     private boolean processAttitudeStateToken(final ParseToken token) {
432         if (token.getName() != null && !token.getName().equals(Acm.ATT_LINE)) {
433             // we are in the section metadata part
434             try {
435                 return AttitudeStateHistoryMetadataKey.valueOf(token.getName()).
436                        process(token, context, currentAttitudeStateHistoryMetadata);
437             } catch (IllegalArgumentException iae) {
438                 // token has not been recognized
439                 return false;
440             }
441         } else {
442             // we are in the section data part
443             if (currentAttitudeStateHistory.isEmpty()) {
444                 // we are starting the real data section, we can now check metadata is complete
445                 currentAttitudeStateHistoryMetadata.validate(header.getFormatVersion());
446                 anticipateNext(this::processDataSubStructureToken);
447             }
448             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
449                 return true;
450             }
451             try {
452                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
453                 final AbsoluteDate epoch = context.getTimeSystem().getConverter(context).parse(fields[0]);
454                 return currentAttitudeStateHistory.add(new AttitudeState(currentAttitudeStateHistoryMetadata.getAttitudeType(),
455                                                                          currentAttitudeStateHistoryMetadata.getRateType(),
456                                                                          epoch, fields, 1));
457             } catch (NumberFormatException | OrekitIllegalArgumentException e) {
458                 throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
459                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
460             }
461         }
462     }
463 
464     /** Process one physical property data token.
465      * @param token token to process
466      * @return true if token was processed, false otherwise
467      */
468     private boolean processPhysicalPropertyToken(final ParseToken token) {
469         anticipateNext(this::processDataSubStructureToken);
470         try {
471             return token.getName() != null &&
472                    AttitudePhysicalPropertiesKey.valueOf(token.getName()).process(token, context, physicBlock);
473         } catch (IllegalArgumentException iae) {
474             // token has not been recognized
475             return false;
476         }
477     }
478 
479     /** Process one covariance history history data token.
480      * @param token token to process
481      * @return true if token was processed, false otherwise
482      */
483     private boolean processCovarianceToken(final ParseToken token) {
484         if (token.getName() != null && !token.getName().equals(Ocm.COV_LINE)) {
485             // we are in the section metadata part
486             try {
487                 return AttitudeCovarianceHistoryMetadataKey.valueOf(token.getName()).
488                        process(token, context, currentCovarianceHistoryMetadata);
489             } catch (IllegalArgumentException iae) {
490                 // token has not been recognized
491                 return false;
492             }
493         } else {
494             // we are in the section data part
495             if (currentCovarianceHistory.isEmpty()) {
496                 // we are starting the real data section, we can now check metadata is complete
497                 currentCovarianceHistoryMetadata.validate(header.getFormatVersion());
498                 anticipateNext(this::processDataSubStructureToken);
499             }
500             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
501                 return true;
502             }
503             try {
504                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
505                 currentCovarianceHistory.add(new AttitudeCovariance(currentCovarianceHistoryMetadata.getCovType(),
506                                                                     context.getTimeSystem().getConverter(context).parse(fields[0]),
507                                                                     fields, 1));
508                 return true;
509             } catch (NumberFormatException nfe) {
510                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
511                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
512             }
513         }
514     }
515 
516     /** Process one maneuver data token.
517      * @param token token to process
518      * @return true if token was processed, false otherwise
519      */
520     private boolean processManeuverToken(final ParseToken token) {
521         anticipateNext(this::processDataSubStructureToken);
522         try {
523             return token.getName() != null &&
524                    AttitudeManeuverKey.valueOf(token.getName()).process(token, context, currentManeuver);
525         } catch (IllegalArgumentException iae) {
526             // token has not been recognized
527             return false;
528         }
529     }
530 
531     /** Process one attitude determination data token.
532      * @param token token to process
533      * @return true if token was processed, false otherwise
534      */
535     private boolean processAttitudeDeterminationToken(final ParseToken token) {
536         anticipateNext(attitudeDeterminationSensorBlock != null ?
537                        this::processAttitudeDeterminationSensorToken :
538                        this::processDataSubStructureToken);
539         if (token.getName() == null) {
540             return false;
541         }
542         try {
543             return AttitudeDeterminationKey.valueOf(token.getName()).process(token, this, context, attitudeDeterminationBlock);
544         } catch (IllegalArgumentException iae1) {
545             // token has not been recognized
546             return false;
547         }
548     }
549 
550     /** Process one attitude determination sensor data token.
551      * @param token token to process
552      * @return true if token was processed, false otherwise
553      */
554     private boolean processAttitudeDeterminationSensorToken(final ParseToken token) {
555         anticipateNext(this::processAttitudeDeterminationToken);
556         if (token.getName() == null) {
557             return false;
558         }
559         try {
560             return AttitudeDeterminationSensorKey.valueOf(token.getName()).process(token, context, attitudeDeterminationSensorBlock);
561         } catch (IllegalArgumentException iae1) {
562             // token has not been recognized
563             attitudeDeterminationBlock.addSensor(attitudeDeterminationSensorBlock);
564             attitudeDeterminationSensorBlock = null;
565             return false;
566         }
567     }
568 
569     /** Process one user-defined parameter data token.
570      * @param token token to process
571      * @return true if token was processed, false otherwise
572      */
573     private boolean processUserDefinedToken(final ParseToken token) {
574         anticipateNext(this::processDataSubStructureToken);
575         if ("COMMENT".equals(token.getName())) {
576             return token.getType() == TokenType.ENTRY ? userDefinedBlock.addComment(token.getContentAsNormalizedString()) : true;
577         } else if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
578             if (token.getType() == TokenType.ENTRY) {
579                 userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
580                                           token.getContentAsNormalizedString());
581             }
582             return true;
583         } else {
584             // the token was not processed
585             return false;
586         }
587     }
588 
589 }