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