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.odm.omm;
18  
19  import java.util.ArrayList;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.function.Function;
23  
24  import org.hipparchus.util.FastMath;
25  import org.orekit.data.DataContext;
26  import org.orekit.files.ccsds.definitions.CcsdsFrameMapper;
27  import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
28  import org.orekit.files.ccsds.ndm.odm.CartesianCovariance;
29  import org.orekit.files.ccsds.ndm.odm.CartesianCovarianceKey;
30  import org.orekit.files.ccsds.ndm.odm.OdmCommonMetadata;
31  import org.orekit.files.ccsds.ndm.odm.CommonMetadataKey;
32  import org.orekit.files.ccsds.ndm.odm.KeplerianElements;
33  import org.orekit.files.ccsds.ndm.odm.KeplerianElementsKey;
34  import org.orekit.files.ccsds.ndm.odm.OdmHeader;
35  import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
36  import org.orekit.files.ccsds.ndm.odm.OdmParser;
37  import org.orekit.files.ccsds.ndm.odm.SpacecraftParameters;
38  import org.orekit.files.ccsds.ndm.odm.SpacecraftParametersKey;
39  import org.orekit.files.ccsds.ndm.odm.UserDefined;
40  import org.orekit.files.ccsds.section.CommentsContainer;
41  import org.orekit.files.ccsds.section.HeaderProcessingState;
42  import org.orekit.files.ccsds.section.MetadataKey;
43  import org.orekit.files.ccsds.section.Segment;
44  import org.orekit.files.ccsds.section.XmlStructureProcessingState;
45  import org.orekit.files.ccsds.utils.ContextBinding;
46  import org.orekit.files.ccsds.utils.FileFormat;
47  import org.orekit.files.ccsds.utils.lexical.ParseToken;
48  import org.orekit.files.ccsds.utils.lexical.TokenType;
49  import org.orekit.files.ccsds.utils.lexical.UserDefinedXmlTokenBuilder;
50  import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
51  import org.orekit.files.ccsds.utils.parsing.ErrorState;
52  import org.orekit.files.ccsds.utils.parsing.ProcessingState;
53  import org.orekit.frames.Frame;
54  import org.orekit.propagation.analytical.tle.TLEPropagator;
55  import org.orekit.time.AbsoluteDate;
56  import org.orekit.utils.IERSConventions;
57  
58  /** A parser for the CCSDS OMM (Orbiter Mean-Elements Message).
59   * <p>
60   * Note than starting with Orekit 11.0, CCSDS message parsers are
61   * mutable objects that gather the data being parsed, until the
62   * message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
63   * parseMessage} method has returned. This implies that parsers
64   * should <em>not</em> be used in a multi-thread context. The recommended
65   * way to use parsers is to either dedicate one parser for each message
66   * and drop it afterwards, or to use a single-thread loop.
67   * </p>
68   * @author sports
69   * @since 6.1
70   */
71  public class OmmParser extends OdmParser<Omm, OmmParser> {
72  
73      /** Default mass to use if there are no spacecraft parameters block logical block in the file. */
74      private final double defaultMass;
75  
76      /** File header. */
77      private OdmHeader header;
78  
79      /** File segments. */
80      private List<Segment<OmmMetadata, OmmData>> segments;
81  
82      /** OMM metadata being read. */
83      private OmmMetadata metadata;
84  
85      /** Context binding valid for current metadata. */
86      private ContextBinding context;
87  
88      /** Keplerian elements logical block being read. */
89      private KeplerianElements keplerianElementsBlock;
90  
91      /** Spacecraft parameters logical block being read. */
92      private SpacecraftParameters spacecraftParametersBlock;
93  
94      /** TLE logical block being read. */
95      private OmmTle tleBlock;
96  
97      /** Covariance matrix logical block being read. */
98      private CartesianCovariance covarianceBlock;
99  
100     /** User defined parameters. */
101     private UserDefined userDefinedBlock;
102 
103     /** Processor for global message structure. */
104     private ProcessingState structureProcessor;
105 
106     /** Complete constructor.
107      * <p>
108      * Calling this constructor directly is not recommended. Users should rather use
109      * {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildOmmParser()
110      * parserBuilder.buildOmmParser()}.
111      * </p>
112      * @param conventions IERS Conventions
113      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
114      * @param dataContext used to retrieve frames, time scales, etc.
115      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
116      * @param mu gravitational coefficient
117      * @param defaultMass default mass to use if there are no spacecraft parameters block logical block in the file
118      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
119      * @param filters filters to apply to parse tokens
120      * @param frameMapper for creating an Orekit {@link Frame}.
121      * @since 13.1.5
122      */
123     public OmmParser(final IERSConventions conventions, final boolean simpleEOP,
124                      final DataContext dataContext, final AbsoluteDate missionReferenceDate,
125                      final double mu, final double defaultMass, final ParsedUnitsBehavior parsedUnitsBehavior,
126                      final Function<ParseToken, List<ParseToken>>[] filters,
127                      final CcsdsFrameMapper frameMapper) {
128         super(Omm.ROOT, Omm.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext,
129               missionReferenceDate, mu, parsedUnitsBehavior, filters,
130                 frameMapper);
131         this.defaultMass = defaultMass;
132     }
133 
134     /** {@inheritDoc} */
135     @Override
136     public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {
137 
138         final Map<String, XmlTokenBuilder> builders = super.getSpecialXmlElementsBuilders();
139 
140         // special handling of user-defined parameters
141         builders.put(UserDefined.USER_DEFINED_XML_TAG, new UserDefinedXmlTokenBuilder());
142 
143         return builders;
144 
145     }
146 
147     /** {@inheritDoc} */
148     @Override
149     public OdmHeader getHeader() {
150         return header;
151     }
152 
153     /** {@inheritDoc} */
154     @Override
155     public void reset(final FileFormat fileFormat) {
156         header                    = new OdmHeader();
157         segments                  = new ArrayList<>();
158         metadata                  = null;
159         context                   = null;
160         keplerianElementsBlock    = null;
161         spacecraftParametersBlock = null;
162         tleBlock                  = null;
163         covarianceBlock           = null;
164         userDefinedBlock          = null;
165         if (fileFormat == FileFormat.XML) {
166             structureProcessor = new XmlStructureProcessingState(Omm.ROOT, this);
167             reset(fileFormat, structureProcessor);
168         } else {
169             structureProcessor = new ErrorState(); // should never be called
170             reset(fileFormat, new HeaderProcessingState(this));
171         }
172     }
173 
174     /** {@inheritDoc} */
175     @Override
176     public boolean prepareHeader() {
177         anticipateNext(new HeaderProcessingState(this));
178         return true;
179     }
180 
181     /** {@inheritDoc} */
182     @Override
183     public boolean inHeader() {
184         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
185         return true;
186     }
187 
188     /** {@inheritDoc} */
189     @Override
190     public boolean finalizeHeader() {
191         header.validate(header.getFormatVersion());
192         return true;
193     }
194 
195     /** {@inheritDoc} */
196     @Override
197     public boolean prepareMetadata() {
198         if (metadata != null) {
199             return false;
200         }
201         metadata  = new OmmMetadata(getFrameMapper());
202         context   = new ContextBinding(this::getConventions, this::isSimpleEOP,
203                                        this::getDataContext, this::getParsedUnitsBehavior,
204                                        this::getMissionReferenceDate,
205                                        metadata::getTimeSystem, () -> 0.0, () -> 1.0);
206         anticipateNext(this::processMetadataToken);
207         return true;
208     }
209 
210     /** {@inheritDoc} */
211     @Override
212     public boolean inMetadata() {
213         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processKeplerianElementsToken);
214         return true;
215     }
216 
217     /** {@inheritDoc} */
218     @Override
219     public boolean finalizeMetadata() {
220         metadata.finalizeMetadata(context);
221         metadata.validate(header.getFormatVersion());
222         if (metadata.getCenter().getBody() != null) {
223             setMuCreated(metadata.getCenter().getBody().getGM());
224         }
225         return true;
226     }
227 
228     /** {@inheritDoc} */
229     @Override
230     public boolean prepareData() {
231         keplerianElementsBlock = new KeplerianElements();
232         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processKeplerianElementsToken);
233         return true;
234     }
235 
236     /** {@inheritDoc} */
237     @Override
238     public boolean inData() {
239         return true;
240     }
241 
242     /** {@inheritDoc} */
243     @Override
244     public boolean finalizeData() {
245         if (metadata != null) {
246             if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
247                 userDefinedBlock = null;
248             }
249             if (tleBlock != null) {
250                 if (Double.isNaN(keplerianElementsBlock.getMu())) {
251                     keplerianElementsBlock.setMu(TLEPropagator.getMU());
252                 }
253                 final double mu = keplerianElementsBlock.getMu();
254                 final double n  = keplerianElementsBlock.getMeanMotion();
255                 keplerianElementsBlock.setA(FastMath.cbrt(mu / (n * n)));
256                 setMuParsed(mu);
257             }
258             final double  mass = spacecraftParametersBlock == null ?
259                                  defaultMass : spacecraftParametersBlock.getMass();
260             final OmmData data = new OmmData(keplerianElementsBlock, spacecraftParametersBlock,
261                                              tleBlock, covarianceBlock, userDefinedBlock, mass);
262             data.validate(header.getFormatVersion());
263             segments.add(new Segment<>(metadata, data));
264         }
265         metadata                  = null;
266         context                   = null;
267         keplerianElementsBlock    = null;
268         spacecraftParametersBlock = null;
269         tleBlock                  = null;
270         covarianceBlock           = null;
271         userDefinedBlock          = null;
272         return true;
273     }
274 
275     /** {@inheritDoc} */
276     @Override
277     public Omm build() {
278         // OMM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
279         // automatically before the end of the file
280         finalizeData();
281         return new Omm(header, segments, getConventions(), getDataContext());
282     }
283 
284     /** Manage Keplerian elements section.
285      * @param starting if true, parser is entering the section
286      * otherwise it is leaving the section
287      * @return always return true
288      */
289     boolean manageKeplerianElementsSection(final boolean starting) {
290         anticipateNext(starting ? this::processKeplerianElementsToken : structureProcessor);
291         return true;
292     }
293 
294     /** Manage spacecraft parameters section.
295      * @param starting if true, parser is entering the section
296      * otherwise it is leaving the section
297      * @return always return true
298      */
299     boolean manageSpacecraftParametersSection(final boolean starting) {
300         anticipateNext(starting ? this::processSpacecraftParametersToken : structureProcessor);
301         return true;
302     }
303 
304     /** Manage TLE parameters section.
305      * @param starting if true, parser is entering the section
306      * otherwise it is leaving the section
307      * @return always return true
308      */
309     boolean manageTleParametersSection(final boolean starting) {
310         anticipateNext(starting ? this::processTLEToken : structureProcessor);
311         return true;
312     }
313 
314         /** Manage covariance matrix section.
315      * @param starting if true, parser is entering the section
316      * otherwise it is leaving the section
317      * @return always return true
318      */
319     boolean manageCovarianceSection(final boolean starting) {
320         anticipateNext(starting ? this::processCovarianceToken : structureProcessor);
321         return true;
322     }
323 
324     /** Manage user-defined parameters section.
325      * @param starting if true, parser is entering the section
326      * otherwise it is leaving the section
327      * @return always return true
328      */
329     boolean manageUserDefinedParametersSection(final boolean starting) {
330         anticipateNext(starting ? this::processUserDefinedToken : structureProcessor);
331         return true;
332     }
333 
334     /** Process one metadata token.
335      * @param token token to process
336      * @return true if token was processed, false otherwise
337      */
338     private boolean processMetadataToken(final ParseToken token) {
339         if (metadata == null) {
340             // OMM KVN file lack a META_START keyword, hence we can't call prepareMetadata()
341             // automatically before the first metadata token arrives
342             prepareMetadata();
343         }
344         inMetadata();
345         try {
346             return token.getName() != null &&
347                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
348         } catch (IllegalArgumentException iaeG) {
349             try {
350                 return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
351             } catch (IllegalArgumentException iaeD) {
352                 try {
353                     return CommonMetadataKey.valueOf(token.getName()).process(token, context, metadata);
354                 } catch (IllegalArgumentException iaeC) {
355                     try {
356                         return OmmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
357                     } catch (IllegalArgumentException iaeM) {
358                         // token has not been recognized
359                         return false;
360                     }
361                 }
362             }
363         }
364     }
365 
366     /** Process one XML data substructure token.
367      * @param token token to process
368      * @return true if token was processed, false otherwise
369      */
370     private boolean processXmlSubStructureToken(final ParseToken token) {
371         try {
372             return token.getName() != null &&
373                    XmlSubStructureKey.valueOf(token.getName()).process(token, this);
374         } catch (IllegalArgumentException iae) {
375             // token has not been recognized
376             return false;
377         }
378     }
379 
380     /** Process one mean Keplerian elements data token.
381      * @param token token to process
382      * @return true if token was processed, false otherwise
383      */
384     private boolean processKeplerianElementsToken(final ParseToken token) {
385         if (keplerianElementsBlock == null) {
386             // OMM KVN file lack a META_STOP keyword, hence we can't call finalizeMetadata()
387             // automatically before the first data token arrives
388             finalizeMetadata();
389             // OMM KVN file lack a DATA_START keyword, hence we can't call prepareData()
390             // automatically before the first data token arrives
391             prepareData();
392         }
393         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processSpacecraftParametersToken);
394         try {
395             return token.getName() != null &&
396                    KeplerianElementsKey.valueOf(token.getName()).process(token, context, keplerianElementsBlock);
397         } catch (IllegalArgumentException iae) {
398             // token has not been recognized
399             return false;
400         }
401     }
402 
403     /** Process one spacecraft parameters data token.
404      * @param token token to process
405      * @return true if token was processed, false otherwise
406      */
407     private boolean processSpacecraftParametersToken(final ParseToken token) {
408         if (spacecraftParametersBlock == null) {
409             spacecraftParametersBlock = new SpacecraftParameters();
410             if (moveCommentsIfEmpty(keplerianElementsBlock, spacecraftParametersBlock)) {
411                 // get rid of the empty logical block
412                 keplerianElementsBlock = null;
413             }
414         }
415         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processTLEToken);
416         try {
417             return token.getName() != null &&
418                    SpacecraftParametersKey.valueOf(token.getName()).process(token, context, spacecraftParametersBlock);
419         } catch (IllegalArgumentException iae) {
420             // token has not been recognized
421             return false;
422         }
423     }
424 
425     /** Process one TLE data token.
426      * @param token token to process
427      * @return true if token was processed, false otherwise
428      */
429     private boolean processTLEToken(final ParseToken token) {
430         if (tleBlock == null) {
431             tleBlock = new OmmTle();
432             if (moveCommentsIfEmpty(spacecraftParametersBlock, tleBlock)) {
433                 // get rid of the empty logical block
434                 spacecraftParametersBlock = null;
435             }
436         }
437         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processCovarianceToken);
438         try {
439             return token.getName() != null &&
440                    OmmTleKey.valueOf(token.getName()).process(token, context, tleBlock);
441         } catch (IllegalArgumentException iae) {
442             // token has not been recognized
443             return false;
444         }
445     }
446 
447     /** Process one covariance matrix data token.
448      * @param token token to process
449      * @return true if token was processed, false otherwise
450      */
451     private boolean processCovarianceToken(final ParseToken token) {
452         if (covarianceBlock == null) {
453             // save the current metadata for later retrieval of reference frame
454             final OdmCommonMetadata savedMetadata = metadata;
455             covarianceBlock = new CartesianCovariance(
456                     savedMetadata::getReferenceFrame,
457                     savedMetadata.getFrameMapper());
458             if (moveCommentsIfEmpty(tleBlock, covarianceBlock)) {
459                 // get rid of the empty logical block
460                 tleBlock = null;
461             }
462         }
463         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processUserDefinedToken);
464         try {
465             return token.getName() != null &&
466                    CartesianCovarianceKey.valueOf(token.getName()).process(token, context, covarianceBlock);
467         } catch (IllegalArgumentException iae) {
468             // token has not been recognized
469             return false;
470         }
471     }
472 
473     /** Process one maneuver data token.
474      * @param token token to process
475      * @return true if token was processed, false otherwise
476      */
477     private boolean processUserDefinedToken(final ParseToken token) {
478         if (userDefinedBlock == null) {
479             userDefinedBlock = new UserDefined();
480             if (moveCommentsIfEmpty(covarianceBlock, userDefinedBlock)) {
481                 // get rid of the empty logical block
482                 covarianceBlock = null;
483             }
484         }
485         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : new ErrorState());
486         if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
487             if (token.getType() == TokenType.ENTRY) {
488                 userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
489                                           token.getContentAsNormalizedString());
490             }
491             return true;
492         } else {
493             // the token was not processed
494             return false;
495         }
496     }
497 
498     /** Move comments from one empty logical block to another logical block.
499      * @param origin origin block
500      * @param destination destination block
501      * @return true if origin block was empty
502      */
503     private boolean moveCommentsIfEmpty(final CommentsContainer origin, final CommentsContainer destination) {
504         if (origin != null && origin.acceptComments()) {
505             // origin block is empty, move the existing comments
506             for (final String comment : origin.getComments()) {
507                 destination.addComment(comment);
508             }
509             return true;
510         } else {
511             return false;
512         }
513     }
514 
515 }