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