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.opm;
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.orekit.data.DataContext;
25  import org.orekit.files.ccsds.definitions.CcsdsFrameMapper;
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.StateVector;
39  import org.orekit.files.ccsds.ndm.odm.StateVectorKey;
40  import org.orekit.files.ccsds.ndm.odm.UserDefined;
41  import org.orekit.files.ccsds.section.CommentsContainer;
42  import org.orekit.files.ccsds.section.HeaderProcessingState;
43  import org.orekit.files.ccsds.section.MetadataKey;
44  import org.orekit.files.ccsds.section.Segment;
45  import org.orekit.files.ccsds.section.XmlStructureProcessingState;
46  import org.orekit.files.ccsds.utils.ContextBinding;
47  import org.orekit.files.ccsds.utils.FileFormat;
48  import org.orekit.files.ccsds.utils.lexical.ParseToken;
49  import org.orekit.files.ccsds.utils.lexical.TokenType;
50  import org.orekit.files.ccsds.utils.lexical.UserDefinedXmlTokenBuilder;
51  import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
52  import org.orekit.files.ccsds.utils.parsing.ErrorState;
53  import org.orekit.files.ccsds.utils.parsing.ProcessingState;
54  import org.orekit.frames.Frame;
55  import org.orekit.time.AbsoluteDate;
56  import org.orekit.utils.IERSConventions;
57  
58  /** A parser for the CCSDS OPM (Orbit Parameter 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   * @author Luc Maisonobe
70   * @since 6.1
71   */
72  public class OpmParser extends OdmParser<Opm, OpmParser> {
73  
74      /** Default mass to use if there are no spacecraft parameters block logical block in the file. */
75      private final double defaultMass;
76  
77      /** File header. */
78      private OdmHeader header;
79  
80      /** File segments. */
81      private List<Segment<OdmCommonMetadata, OpmData>> segments;
82  
83      /** OPM metadata being read. */
84      private OdmCommonMetadata metadata;
85  
86      /** Context binding valid for current metadata. */
87      private ContextBinding context;
88  
89      /** State vector logical block being read. */
90      private StateVector stateVectorBlock;
91  
92      /** Keplerian elements logical block being read. */
93      private KeplerianElements keplerianElementsBlock;
94  
95      /** Spacecraft parameters logical block being read. */
96      private SpacecraftParameters spacecraftParametersBlock;
97  
98      /** Covariance matrix logical block being read. */
99      private CartesianCovariance covarianceBlock;
100 
101     /** Current maneuver. */
102     private Maneuver currentManeuver;
103 
104     /** All maneuvers. */
105     private List<Maneuver> maneuverBlocks;
106 
107     /** User defined parameters. */
108     private UserDefined userDefinedBlock;
109 
110     /** Processor for global message structure. */
111     private ProcessingState structureProcessor;
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#buildOpmParser()
117      * parserBuilder.buildOpmParser()}.
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 missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
123      * @param mu gravitational coefficient
124      * @param defaultMass default mass to use if there are no spacecraft parameters block logical block in the file
125      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
126      * @param filters filters to apply to parse tokens
127      * @param frameMapper for creating an Orekit {@link Frame}.
128      * @since 13.1.5
129      */
130     public OpmParser(final IERSConventions conventions, final boolean simpleEOP,
131                      final DataContext dataContext,
132                      final AbsoluteDate missionReferenceDate, final double mu,
133                      final double defaultMass, final ParsedUnitsBehavior parsedUnitsBehavior,
134                      final Function<ParseToken, List<ParseToken>>[] filters,
135                      final CcsdsFrameMapper frameMapper) {
136         super(Opm.ROOT, Opm.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext,
137                 missionReferenceDate, mu, parsedUnitsBehavior, filters,
138                 frameMapper);
139         this.defaultMass = defaultMass;
140     }
141 
142     /** {@inheritDoc} */
143     @Override
144     public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {
145 
146         final Map<String, XmlTokenBuilder> builders = super.getSpecialXmlElementsBuilders();
147 
148         // special handling of user-defined parameters
149         builders.put(UserDefined.USER_DEFINED_XML_TAG, new UserDefinedXmlTokenBuilder());
150 
151         return builders;
152 
153     }
154 
155     /** {@inheritDoc} */
156     @Override
157     public OdmHeader getHeader() {
158         return header;
159     }
160 
161     /** {@inheritDoc} */
162     @Override
163     public void reset(final FileFormat fileFormat) {
164         header                    = new OdmHeader();
165         segments                  = new ArrayList<>();
166         metadata                  = null;
167         context                   = null;
168         stateVectorBlock          = null;
169         keplerianElementsBlock    = null;
170         spacecraftParametersBlock = null;
171         covarianceBlock           = null;
172         currentManeuver           = null;
173         maneuverBlocks            = new ArrayList<>();
174         userDefinedBlock          = null;
175         if (fileFormat == FileFormat.XML) {
176             structureProcessor = new XmlStructureProcessingState(Opm.ROOT, this);
177             reset(fileFormat, structureProcessor);
178         } else {
179             structureProcessor = new ErrorState(); // should never be called
180             reset(fileFormat, new HeaderProcessingState(this));
181         }
182     }
183 
184     /** {@inheritDoc} */
185     @Override
186     public boolean prepareHeader() {
187         anticipateNext(new HeaderProcessingState(this));
188         return true;
189     }
190 
191     /** {@inheritDoc} */
192     @Override
193     public boolean inHeader() {
194         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
195         return true;
196     }
197 
198     /** {@inheritDoc} */
199     @Override
200     public boolean finalizeHeader() {
201         header.validate(header.getFormatVersion());
202         return true;
203     }
204 
205     /** {@inheritDoc} */
206     @Override
207     public boolean prepareMetadata() {
208         if (metadata != null) {
209             return false;
210         }
211         metadata  = new OdmCommonMetadata(getFrameMapper());
212         context   = new ContextBinding(this::getConventions, this::isSimpleEOP,
213                                        this::getDataContext, this::getParsedUnitsBehavior,
214                                        this::getMissionReferenceDate,
215                                        metadata::getTimeSystem, () -> 0.0, () -> 1.0);
216         anticipateNext(this::processMetadataToken);
217         return true;
218     }
219 
220     /** {@inheritDoc} */
221     @Override
222     public boolean inMetadata() {
223         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processStateVectorToken);
224         return true;
225     }
226 
227     /** {@inheritDoc} */
228     @Override
229     public boolean finalizeMetadata() {
230         metadata.finalizeMetadata(context);
231         metadata.validate(header.getFormatVersion());
232         if (metadata.getCenter().getBody() != null) {
233             setMuCreated(metadata.getCenter().getBody().getGM());
234         }
235         return true;
236     }
237 
238     /** {@inheritDoc} */
239     @Override
240     public boolean prepareData() {
241         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processStateVectorToken);
242         return true;
243     }
244 
245     /** {@inheritDoc} */
246     @Override
247     public boolean inData() {
248         return true;
249     }
250 
251     /** {@inheritDoc} */
252     @Override
253     public boolean finalizeData() {
254         if (metadata != null) {
255             if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
256                 userDefinedBlock = null;
257             }
258             if (keplerianElementsBlock != null) {
259                 keplerianElementsBlock.setEpoch(stateVectorBlock.getEpoch());
260                 if (Double.isNaN(keplerianElementsBlock.getMu())) {
261                     keplerianElementsBlock.setMu(getSelectedMu());
262                 } else {
263                     setMuParsed(keplerianElementsBlock.getMu());
264                 }
265             }
266             final double  mass = spacecraftParametersBlock == null ?
267                                  defaultMass : spacecraftParametersBlock.getMass();
268             final OpmData data = new OpmData(stateVectorBlock, keplerianElementsBlock,
269                                              spacecraftParametersBlock, covarianceBlock,
270                                              maneuverBlocks, userDefinedBlock,
271                                              mass);
272             data.validate(header.getFormatVersion());
273             segments.add(new Segment<>(metadata, data));
274         }
275         metadata                  = null;
276         context                   = null;
277         stateVectorBlock          = null;
278         keplerianElementsBlock    = null;
279         spacecraftParametersBlock = null;
280         covarianceBlock           = null;
281         currentManeuver           = null;
282         maneuverBlocks            = null;
283         userDefinedBlock          = null;
284         return true;
285     }
286 
287     /** {@inheritDoc} */
288     @Override
289     public Opm build() {
290         // OPM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
291         // automatically before the end of the file
292         finalizeData();
293         return new Opm(header, segments, getConventions(), getDataContext(), getSelectedMu());
294     }
295 
296     /** Manage state vector section.
297      * @param starting if true, parser is entering the section
298      * otherwise it is leaving the section
299      * @return always return true
300      */
301     boolean manageStateVectorSection(final boolean starting) {
302         anticipateNext(starting ? this::processStateVectorToken : structureProcessor);
303         return true;
304     }
305 
306     /** Manage Keplerian elements section.
307      * @param starting if true, parser is entering the section
308      * otherwise it is leaving the section
309      * @return always return true
310      */
311     boolean manageKeplerianElementsSection(final boolean starting) {
312         anticipateNext(starting ? this::processKeplerianElementsToken : structureProcessor);
313         return true;
314     }
315 
316     /** Manage spacecraft parameters section.
317      * @param starting if true, parser is entering the section
318      * otherwise it is leaving the section
319      * @return always return true
320      */
321     boolean manageSpacecraftParametersSection(final boolean starting) {
322         anticipateNext(starting ? this::processSpacecraftParametersToken : structureProcessor);
323         return true;
324     }
325 
326     /** Manage covariance matrix section.
327      * @param starting if true, parser is entering the section
328      * otherwise it is leaving the section
329      * @return always return true
330      */
331     boolean manageCovarianceSection(final boolean starting) {
332         anticipateNext(starting ? this::processCovarianceToken : structureProcessor);
333         return true;
334     }
335 
336     /** Manage maneuvers section.
337      * @param starting if true, parser is entering the section
338      * otherwise it is leaving the section
339      * @return always return true
340      */
341     boolean manageManeuversSection(final boolean starting) {
342         anticipateNext(starting ? this::processManeuverToken : structureProcessor);
343         return true;
344     }
345 
346     /** Manage user-defined parameters section.
347      * @param starting if true, parser is entering the section
348      * otherwise it is leaving the section
349      * @return always return true
350      */
351     boolean manageUserDefinedParametersSection(final boolean starting) {
352         anticipateNext(starting ? this::processUserDefinedToken : structureProcessor);
353         return true;
354     }
355 
356     /** Process one metadata token.
357      * @param token token to process
358      * @return true if token was processed, false otherwise
359      */
360     private boolean processMetadataToken(final ParseToken token) {
361         if (metadata == null) {
362             // OPM KVN file lack a META_START keyword, hence we can't call prepareMetadata()
363             // automatically before the first metadata token arrives
364             prepareMetadata();
365         }
366         inMetadata();
367         try {
368             return token.getName() != null &&
369                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
370         } catch (IllegalArgumentException iaeM) {
371             try {
372                 return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
373             } catch (IllegalArgumentException iaeD) {
374                 try {
375                     return CommonMetadataKey.valueOf(token.getName()).process(token, context, metadata);
376                 } catch (IllegalArgumentException iaeC) {
377                     // token has not been recognized
378                     return false;
379                 }
380             }
381         }
382     }
383 
384     /** Process one XML data substructure token.
385      * @param token token to process
386      * @return true if token was processed, false otherwise
387      */
388     private boolean processXmlSubStructureToken(final ParseToken token) {
389         try {
390             return token.getName() != null &&
391                    XmlSubStructureKey.valueOf(token.getName()).process(token, this);
392         } catch (IllegalArgumentException iae) {
393             // token has not been recognized
394             return false;
395         }
396     }
397 
398     /** Process one state vector data token.
399      * @param token token to process
400      * @return true if token was processed, false otherwise
401      */
402     private boolean processStateVectorToken(final ParseToken token) {
403         if (stateVectorBlock == null) {
404             // OPM KVN file lack a META_STOP keyword, hence we can't call finalizeMetadata()
405             // automatically before the first data token arrives
406             finalizeMetadata();
407             // OPM KVN file lack a DATA_START keyword, hence we can't call prepareData()
408             // automatically before the first data token arrives
409             prepareData();
410             stateVectorBlock = new StateVector();
411         }
412         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processKeplerianElementsToken);
413         try {
414             return token.getName() != null &&
415                    StateVectorKey.valueOf(token.getName()).process(token, context, stateVectorBlock);
416         } catch (IllegalArgumentException iae) {
417             // token has not been recognized
418             return false;
419         }
420     }
421 
422     /** Process one Keplerian elements data token.
423      * @param token token to process
424      * @return true if token was processed, false otherwise
425      */
426     private boolean processKeplerianElementsToken(final ParseToken token) {
427         if (keplerianElementsBlock == null) {
428             keplerianElementsBlock = new KeplerianElements();
429         }
430         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processSpacecraftParametersToken);
431         try {
432             return token.getName() != null &&
433                    KeplerianElementsKey.valueOf(token.getName()).process(token, context, keplerianElementsBlock);
434         } catch (IllegalArgumentException iae) {
435             // token has not been recognized
436             return false;
437         }
438     }
439 
440     /** Process one spacecraft parameters data token.
441      * @param token token to process
442      * @return true if token was processed, false otherwise
443      */
444     private boolean processSpacecraftParametersToken(final ParseToken token) {
445         if (spacecraftParametersBlock == null) {
446             spacecraftParametersBlock = new SpacecraftParameters();
447             if (moveCommentsIfEmpty(keplerianElementsBlock, spacecraftParametersBlock)) {
448                 // get rid of the empty logical block
449                 keplerianElementsBlock = null;
450             }
451         }
452         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processCovarianceToken);
453         try {
454             return token.getName() != null &&
455                    SpacecraftParametersKey.valueOf(token.getName()).process(token, context, spacecraftParametersBlock);
456         } catch (IllegalArgumentException iae) {
457             // token has not been recognized
458             return false;
459         }
460     }
461 
462     /** Process one covariance matrix data token.
463      * @param token token to process
464      * @return true if token was processed, false otherwise
465      */
466     private boolean processCovarianceToken(final ParseToken token) {
467         if (covarianceBlock == null) {
468             // save the current metadata for later retrieval of reference frame
469             final OdmCommonMetadata savedMetadata = metadata;
470             covarianceBlock = new CartesianCovariance(
471                     savedMetadata::getReferenceFrame,
472                     savedMetadata.getFrameMapper());
473             if (moveCommentsIfEmpty(spacecraftParametersBlock, covarianceBlock)) {
474                 // get rid of the empty logical block
475                 spacecraftParametersBlock = null;
476             }
477         }
478         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processManeuverToken);
479         try {
480             return token.getName() != null &&
481                    CartesianCovarianceKey.valueOf(token.getName()).process(token, context, covarianceBlock);
482         } catch (IllegalArgumentException iae) {
483             // token has not been recognized
484             return false;
485         }
486     }
487 
488     /** Process one maneuver data token.
489      * @param token token to process
490      * @return true if token was processed, false otherwise
491      */
492     private boolean processManeuverToken(final ParseToken token) {
493         if (currentManeuver == null) {
494             currentManeuver = new Maneuver();
495             if (covarianceBlock != null && moveCommentsIfEmpty(covarianceBlock, currentManeuver)) {
496                 // get rid of the empty logical block
497                 covarianceBlock = null;
498             }
499         }
500         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processUserDefinedToken);
501         try {
502             if (token.getName() != null &&
503                 ManeuverKey.valueOf(token.getName()).process(token, context, currentManeuver)) {
504                 // the token was processed properly
505                 if (currentManeuver.completed()) {
506                     // current maneuver is completed
507                     maneuverBlocks.add(currentManeuver);
508                     currentManeuver = null;
509                 }
510                 return true;
511             }
512         } catch (IllegalArgumentException iae) {
513             // ignored, delegate to next state below
514         }
515         // the token was not processed
516         return false;
517     }
518 
519     /** Process one maneuver data token.
520      * @param token token to process
521      * @return true if token was processed, false otherwise
522      */
523     private boolean processUserDefinedToken(final ParseToken token) {
524         if (userDefinedBlock == null) {
525             userDefinedBlock = new UserDefined();
526             if (moveCommentsIfEmpty(currentManeuver, userDefinedBlock)) {
527                 // get rid of the empty logical block
528                 currentManeuver = null;
529             }
530         }
531         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : new ErrorState());
532         if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
533             if (token.getType() == TokenType.ENTRY) {
534                 userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
535                                           token.getContentAsNormalizedString());
536             }
537             return true;
538         } else {
539             // the token was not processed
540             return false;
541         }
542     }
543 
544     /** Move comments from one empty logical block to another logical block.
545      * @param origin origin block
546      * @param destination destination block
547      * @return true if origin block was empty
548      */
549     private boolean moveCommentsIfEmpty(final CommentsContainer origin, final CommentsContainer destination) {
550         if (origin != null && origin.acceptComments()) {
551             // origin block is empty, move the existing comments
552             for (final String comment : origin.getComments()) {
553                 destination.addComment(comment);
554             }
555             return true;
556         } else {
557             return false;
558         }
559     }
560 
561 }