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.aem;
18  
19  import java.util.ArrayList;
20  import java.util.List;
21  import java.util.function.Function;
22  import java.util.regex.Pattern;
23  
24  import org.orekit.data.DataContext;
25  import org.orekit.data.DataSource;
26  import org.orekit.errors.OrekitException;
27  import org.orekit.errors.OrekitMessages;
28  import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
29  import org.orekit.files.ccsds.ndm.adm.AdmCommonMetadataKey;
30  import org.orekit.files.ccsds.ndm.adm.AdmHeader;
31  import org.orekit.files.ccsds.ndm.adm.AdmMetadataKey;
32  import org.orekit.files.ccsds.ndm.adm.AdmParser;
33  import org.orekit.files.ccsds.section.HeaderProcessingState;
34  import org.orekit.files.ccsds.section.KvnStructureProcessingState;
35  import org.orekit.files.ccsds.section.MetadataKey;
36  import org.orekit.files.ccsds.section.XmlStructureProcessingState;
37  import org.orekit.files.ccsds.utils.ContextBinding;
38  import org.orekit.files.ccsds.utils.FileFormat;
39  import org.orekit.files.ccsds.utils.lexical.ParseToken;
40  import org.orekit.files.ccsds.utils.lexical.TokenType;
41  import org.orekit.files.ccsds.utils.parsing.ProcessingState;
42  import org.orekit.files.general.AttitudeEphemerisFileParser;
43  import org.orekit.time.AbsoluteDate;
44  import org.orekit.utils.IERSConventions;
45  
46  /**
47   * A parser for the CCSDS AEM (Attitude Ephemeris Message).
48   * <p>
49   * Note than starting with Orekit 11.0, CCSDS message parsers are
50   * mutable objects that gather the data being parsed, until the
51   * message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
52   * parseMessage} method has returned. This implies that parsers
53   * should <em>not</em> be used in a multi-thread context. The recommended
54   * way to use parsers is to either dedicate one parser for each message
55   * and drop it afterwards, or to use a single-thread loop.
56   * </p>
57   * @author Bryan Cazabonne
58   * @since 10.2
59   */
60  public class AemParser extends AdmParser<Aem, AemParser> implements AttitudeEphemerisFileParser<Aem> {
61  
62      /** Pattern for splitting strings at blanks. */
63      private static final Pattern SPLIT_AT_BLANKS = Pattern.compile("\\s+");
64  
65      /** File header. */
66      private AdmHeader header;
67  
68      /** File segments. */
69      private List<AemSegment> segments;
70  
71      /** Metadata for current observation block. */
72      private AemMetadata metadata;
73  
74      /** Context binding valid for current metadata. */
75      private ContextBinding context;
76  
77      /** Current Ephemerides block being parsed. */
78      private AemData currentBlock;
79  
80      /** Default interpolation degree. */
81      private int defaultInterpolationDegree;
82  
83      /** Processor for global message structure. */
84      private ProcessingState structureProcessor;
85  
86      /** Current attitude entry. */
87      private AttitudeEntry currentEntry;
88  
89      /**Complete constructor.
90       * <p>
91       * Calling this constructor directly is not recommended. Users should rather use
92       * {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildAemParser()
93       * parserBuilder.buildAemParser()}.
94       * </p>
95       * @param conventions IERS Conventions
96       * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
97       * @param dataContext used to retrieve frames, time scales, etc.
98       * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
99       * (may be null if time system is absolute)
100      * @param defaultInterpolationDegree default interpolation degree
101      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
102      * @param filters filters to apply to parse tokens
103      * @since 12.0
104      */
105     public AemParser(final IERSConventions conventions, final boolean simpleEOP,
106                      final DataContext dataContext, final AbsoluteDate missionReferenceDate,
107                      final int defaultInterpolationDegree, final ParsedUnitsBehavior parsedUnitsBehavior,
108                      final Function<ParseToken, List<ParseToken>>[] filters) {
109         super(Aem.ROOT, Aem.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext,
110               missionReferenceDate, parsedUnitsBehavior, filters);
111         this.defaultInterpolationDegree  = defaultInterpolationDegree;
112     }
113 
114     /** {@inheritDoc} */
115     @Override
116     public Aem parse(final DataSource source) {
117         return parseMessage(source);
118     }
119 
120     /** {@inheritDoc} */
121     @Override
122     public AdmHeader getHeader() {
123         return header;
124     }
125 
126     /** {@inheritDoc} */
127     @Override
128     public void reset(final FileFormat fileFormat) {
129         header   = new AdmHeader();
130         segments = new ArrayList<>();
131         metadata = null;
132         context  = null;
133         if (fileFormat == FileFormat.XML) {
134             structureProcessor = new XmlStructureProcessingState(Aem.ROOT, this);
135             reset(fileFormat, structureProcessor);
136         } else {
137             structureProcessor = new KvnStructureProcessingState(this);
138             reset(fileFormat, new HeaderProcessingState(this));
139         }
140     }
141 
142     /** {@inheritDoc} */
143     @Override
144     public boolean prepareHeader() {
145         anticipateNext(new HeaderProcessingState(this));
146         return true;
147     }
148 
149     /** {@inheritDoc} */
150     @Override
151     public boolean inHeader() {
152         anticipateNext(structureProcessor);
153         return true;
154     }
155 
156     /** {@inheritDoc} */
157     @Override
158     public boolean finalizeHeader() {
159         header.validate(header.getFormatVersion());
160         return true;
161     }
162 
163     /** {@inheritDoc} */
164     @Override
165     public boolean prepareMetadata() {
166         if (metadata != null) {
167             return false;
168         }
169         metadata  = new AemMetadata(defaultInterpolationDegree);
170         context   = new ContextBinding(this::getConventions, this::isSimpleEOP,
171                                        this::getDataContext, this::getParsedUnitsBehavior,
172                                        this::getMissionReferenceDate,
173                                        metadata::getTimeSystem, () -> 0.0, () -> 1.0);
174         anticipateNext(this::processMetadataToken);
175         return true;
176     }
177 
178     /** {@inheritDoc} */
179     @Override
180     public boolean inMetadata() {
181         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processKvnDataToken);
182         return true;
183     }
184 
185     /** {@inheritDoc} */
186     @Override
187     public boolean finalizeMetadata() {
188         metadata.validate(header.getFormatVersion());
189         return true;
190     }
191 
192     /** {@inheritDoc} */
193     @Override
194     public boolean prepareData() {
195         currentBlock = new AemData();
196         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processMetadataToken);
197         return true;
198     }
199 
200     /** {@inheritDoc} */
201     @Override
202     public boolean inData() {
203         anticipateNext(structureProcessor);
204         return true;
205     }
206 
207     /** {@inheritDoc} */
208     @Override
209     public boolean finalizeData() {
210         if (metadata != null) {
211             currentBlock.validate(header.getFormatVersion());
212             segments.add(new AemSegment(metadata, currentBlock));
213         }
214         metadata = null;
215         context  = null;
216         return true;
217     }
218 
219     /** {@inheritDoc} */
220     @Override
221     public Aem build() {
222         return new Aem(header, segments, getConventions(), getDataContext());
223     }
224 
225     /** Manage attitude state section in a XML message.
226      * @param starting if true, parser is entering the section
227      * otherwise it is leaving the section
228      * @return always return true
229      */
230     boolean manageXmlAttitudeStateSection(final boolean starting) {
231         if (starting) {
232             currentEntry = new AttitudeEntry(metadata);
233             anticipateNext(this::processXmlDataToken);
234         } else {
235             currentBlock.addData(currentEntry.getCoordinates());
236             currentEntry = null;
237             anticipateNext(structureProcessor);
238         }
239         return true;
240     }
241 
242     /** Add a comment to the data section.
243      * @param comment comment to add
244      * @return always return true
245      */
246     boolean addDataComment(final String comment) {
247         currentBlock.addComment(comment);
248         return true;
249     }
250 
251     /** Process one metadata token.
252      * @param token token to process
253      * @return true if token was processed, false otherwise
254      */
255     private boolean processMetadataToken(final ParseToken token) {
256         inMetadata();
257         try {
258             return token.getName() != null &&
259                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
260         } catch (IllegalArgumentException iaeM) {
261             try {
262                 return AdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
263             } catch (IllegalArgumentException iaeD) {
264                 try {
265                     return AdmCommonMetadataKey.valueOf(token.getName()).process(token, context, metadata);
266                 } catch (IllegalArgumentException iaeC) {
267                     try {
268                         return AemMetadataKey.valueOf(token.getName()).process(token, context, metadata);
269                     } catch (IllegalArgumentException iaeE) {
270                         // token has not been recognized
271                         return false;
272                     }
273                 }
274             }
275         }
276     }
277 
278     /** Process one XML data substructure token.
279      * @param token token to process
280      * @return true if token was processed, false otherwise
281      */
282     private boolean processXmlSubStructureToken(final ParseToken token) {
283         try {
284             return token.getName() != null &&
285                    XmlSubStructureKey.valueOf(token.getName()).process(token, this);
286         } catch (IllegalArgumentException iae) {
287             // token has not been recognized
288             return false;
289         }
290     }
291 
292     /** Process one data token in a KVN message.
293      * @param token token to process
294      * @return true if token was processed, false otherwise
295      */
296     private boolean processKvnDataToken(final ParseToken token) {
297         inData();
298         if ("COMMENT".equals(token.getName())) {
299             return token.getType() == TokenType.ENTRY ? currentBlock.addComment(token.getContentAsNormalizedString()) : true;
300         } else if (token.getType() == TokenType.RAW_LINE) {
301             try {
302                 if (metadata.getAttitudeType() == null) {
303                     throw new OrekitException(OrekitMessages.CCSDS_MISSING_KEYWORD,
304                                               AemMetadataKey.ATTITUDE_TYPE.name(), token.getFileName());
305                 }
306                 return currentBlock.addData(metadata.getAttitudeType().parse(metadata.isFirst(),
307                                                                              metadata.getEndpoints().isExternal2SpacecraftBody(),
308                                                                              metadata.getEulerRotSeq(),
309                                                                              metadata.isSpacecraftBodyRate(),
310                                                                              context, SPLIT_AT_BLANKS.split(token.getRawContent().trim())));
311             } catch (NumberFormatException nfe) {
312                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
313                                           token.getLineNumber(), token.getFileName(), token.getRawContent());
314             }
315         } else {
316             // not a raw line, it is most probably the end of the data section
317             return false;
318         }
319     }
320 
321     /** Process one data token in a XML message.
322      * @param token token to process
323      * @return true if token was processed, false otherwise
324      */
325     private boolean processXmlDataToken(final ParseToken token) {
326         anticipateNext(this::processXmlSubStructureToken);
327         try {
328             return token.getName() != null &&
329                    AttitudeEntryKey.valueOf(token.getName()).process(token, context, currentEntry);
330         } catch (IllegalArgumentException iae) {
331             // token has not been recognized
332             return false;
333         }
334     }
335 
336 }