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