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