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.oem;
18  
19  import java.util.ArrayList;
20  import java.util.List;
21  import java.util.function.Function;
22  import java.util.regex.Pattern;
23  
24  import org.orekit.data.DataContext;
25  import org.orekit.data.DataSource;
26  import org.orekit.errors.OrekitException;
27  import org.orekit.errors.OrekitMessages;
28  import org.orekit.files.ccsds.definitions.CcsdsFrameMapper;
29  import org.orekit.files.ccsds.definitions.Units;
30  import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
31  import org.orekit.files.ccsds.ndm.odm.CartesianCovariance;
32  import org.orekit.files.ccsds.ndm.odm.CartesianCovarianceKey;
33  import org.orekit.files.ccsds.ndm.odm.OdmCommonMetadata;
34  import org.orekit.files.ccsds.ndm.odm.CommonMetadataKey;
35  import org.orekit.files.ccsds.ndm.odm.OdmHeader;
36  import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
37  import org.orekit.files.ccsds.ndm.odm.OdmParser;
38  import org.orekit.files.ccsds.ndm.odm.StateVector;
39  import org.orekit.files.ccsds.ndm.odm.StateVectorKey;
40  import org.orekit.files.ccsds.section.HeaderProcessingState;
41  import org.orekit.files.ccsds.section.KvnStructureProcessingState;
42  import org.orekit.files.ccsds.section.MetadataKey;
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.parsing.ProcessingState;
49  import org.orekit.files.general.EphemerisFileParser;
50  import org.orekit.frames.Frame;
51  import org.orekit.time.AbsoluteDate;
52  import org.orekit.utils.IERSConventions;
53  import org.orekit.utils.units.Unit;
54  
55  /**
56   * A parser for the CCSDS OEM (Orbit Ephemeris 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 OemParser extends OdmParser<Oem, OemParser> implements EphemerisFileParser<Oem> {
70  
71      /** Comment marker. */
72      private static final String COMMENT = "COMMENT";
73  
74                      /** Pattern for splitting strings at blanks. */
75      private static final Pattern SPLIT_AT_BLANKS = Pattern.compile("\\s+");
76  
77      /** File header. */
78      private OdmHeader header;
79  
80      /** File segments. */
81      private List<OemSegment> segments;
82  
83      /** Metadata for current observation block. */
84      private OemMetadata metadata;
85  
86      /** Context binding valid for current metadata. */
87      private ContextBinding context;
88  
89      /** Current Ephemerides block being parsed. */
90      private OemData currentBlock;
91  
92      /** Indicator for covariance parsing. */
93      private boolean inCovariance;
94  
95      /** Current covariance matrix being parsed. */
96      private CartesianCovariance currentCovariance;
97  
98      /** Current row number in covariance matrix. */
99      private int currentRow;
100 
101     /** Default interpolation degree. */
102     private final int defaultInterpolationDegree;
103 
104     /** Processor for global message structure. */
105     private ProcessingState structureProcessor;
106 
107     /** State vector logical block being read. */
108     private StateVector stateVectorBlock;
109 
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#buildOemParser()
115      * parserBuilder.buildOemParser()}.
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      * (may be null if time system is absolute)
122      * @param mu gravitational coefficient
123      * @param defaultInterpolationDegree default interpolation degree
124      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
125      * @param filters filters to apply to parse tokens
126      * @param frameMapper for creating an Orekit {@link Frame}.
127      * @since 13.1.5
128      */
129     public OemParser(final IERSConventions conventions, final boolean simpleEOP,
130                      final DataContext dataContext,
131                      final AbsoluteDate missionReferenceDate, final double mu,
132                      final int defaultInterpolationDegree, final ParsedUnitsBehavior parsedUnitsBehavior,
133                      final Function<ParseToken, List<ParseToken>>[] filters,
134                      final CcsdsFrameMapper frameMapper) {
135         super(Oem.ROOT, Oem.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext,
136                 missionReferenceDate, mu, parsedUnitsBehavior, filters,
137                 frameMapper);
138         this.defaultInterpolationDegree  = defaultInterpolationDegree;
139     }
140 
141     /** {@inheritDoc} */
142     @Override
143     public Oem parse(final DataSource source) {
144         return parseMessage(source);
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         currentBlock      = null;
161         inCovariance      = false;
162         currentCovariance = null;
163         currentRow        = -1;
164         if (fileFormat == FileFormat.XML) {
165             structureProcessor = new XmlStructureProcessingState(Oem.ROOT, this);
166             reset(fileFormat, structureProcessor);
167         } else {
168             structureProcessor = new KvnStructureProcessingState(this);
169             reset(fileFormat, new HeaderProcessingState(this));
170         }
171     }
172 
173     /** {@inheritDoc} */
174     @Override
175     public boolean prepareHeader() {
176         anticipateNext(new HeaderProcessingState(this));
177         return true;
178     }
179 
180     /** {@inheritDoc} */
181     @Override
182     public boolean inHeader() {
183         anticipateNext(structureProcessor);
184         return true;
185     }
186 
187     /** {@inheritDoc} */
188     @Override
189     public boolean finalizeHeader() {
190         header.validate(header.getFormatVersion());
191         return true;
192     }
193 
194     /** {@inheritDoc} */
195     @Override
196     public boolean prepareMetadata() {
197         if (currentBlock != null) {
198             // we have started a new segment, we need to finalize the previous one
199             finalizeData();
200         }
201         metadata = new OemMetadata(defaultInterpolationDegree, 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(structureProcessor);
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         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processKvnDataToken);
226         return true;
227     }
228 
229     /** {@inheritDoc} */
230     @Override
231     public boolean prepareData() {
232         currentBlock = new OemData();
233         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processMetadataToken);
234         return true;
235     }
236 
237     /** {@inheritDoc} */
238     @Override
239     public boolean inData() {
240         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processKvnCovarianceToken);
241         return true;
242     }
243 
244     /** {@inheritDoc} */
245     @Override
246     public boolean finalizeData() {
247         if (metadata != null) {
248             currentBlock.validate(header.getFormatVersion());
249             segments.add(new OemSegment(metadata, currentBlock, getSelectedMu()));
250         }
251         metadata          = null;
252         context           = null;
253         currentBlock      = null;
254         inCovariance      = false;
255         currentCovariance = null;
256         currentRow        = -1;
257         return true;
258     }
259 
260     /** {@inheritDoc} */
261     @Override
262     public Oem build() {
263         // OEM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
264         // automatically before the end of the file
265         finalizeData();
266         final Oem file = new Oem(header, segments, getConventions(), getDataContext(), getSelectedMu());
267         file.checkTimeSystems();
268         return file;
269     }
270 
271     /** Manage state vector section in a XML message.
272      * @param starting if true, parser is entering the section
273      * otherwise it is leaving the section
274      * @return always return true
275      */
276     boolean manageXmlStateVectorSection(final boolean starting) {
277         if (starting) {
278             stateVectorBlock = new StateVector();
279             anticipateNext(this::processXmlStateVectorToken);
280         } else {
281             currentBlock.addData(stateVectorBlock.toTimeStampedPVCoordinates(),
282                                  stateVectorBlock.hasAcceleration());
283             stateVectorBlock = null;
284             anticipateNext(structureProcessor);
285         }
286         return true;
287     }
288 
289     /** Manage covariance matrix 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 manageCovarianceSection(final boolean starting) {
295         if (starting) {
296             // save the current metadata for later retrieval of reference frame
297             final OdmCommonMetadata savedMetadata = metadata;
298             currentCovariance = new CartesianCovariance(
299                     savedMetadata::getReferenceFrame,
300                     savedMetadata.getFrameMapper());
301             anticipateNext(getFileFormat() == FileFormat.XML ?
302                         this::processXmlCovarianceToken :
303                         this::processKvnCovarianceToken);
304         } else {
305             currentBlock.addCovarianceMatrix(currentCovariance);
306             currentCovariance = null;
307             anticipateNext(structureProcessor);
308         }
309         return true;
310     }
311 
312     /** Process one metadata token.
313      * @param token token to process
314      * @return true if token was processed, false otherwise
315      */
316     private boolean processMetadataToken(final ParseToken token) {
317         inMetadata();
318         try {
319             return token.getName() != null &&
320                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
321         } catch (IllegalArgumentException iaeM) {
322             try {
323                 return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
324             } catch (IllegalArgumentException iaeD) {
325                 try {
326                     return CommonMetadataKey.valueOf(token.getName()).process(token, context, metadata);
327                 } catch (IllegalArgumentException iaeC) {
328                     try {
329                         return OemMetadataKey.valueOf(token.getName()).process(token, context, metadata);
330                     } catch (IllegalArgumentException iaeE) {
331                         // token has not been recognized
332                         return false;
333                     }
334                 }
335             }
336         }
337     }
338 
339     /** Process one XML data substructure token.
340      * @param token token to process
341      * @return true if token was processed, false otherwise
342      */
343     private boolean processXmlSubStructureToken(final ParseToken token) {
344         if (COMMENT.equals(token.getName())) {
345             return token.getType() == TokenType.ENTRY ? currentBlock.addComment(token.getContentAsNormalizedString()) : true;
346         } else {
347             try {
348                 return token.getName() != null &&
349                                 OemDataSubStructureKey.valueOf(token.getName()).process(token, this);
350             } catch (IllegalArgumentException iae) {
351                 // token has not been recognized
352                 return false;
353             }
354         }
355     }
356 
357     /** Process one data token in a KVN message.
358      * @param token token to process
359      * @return true if token was processed, false otherwise
360      */
361     private boolean processKvnDataToken(final ParseToken token) {
362         if (currentBlock == null) {
363             // OEM KVN file lack a DATA_START keyword, hence we can't call prepareData()
364             // automatically before the first data token arrives
365             prepareData();
366         }
367         inData();
368         if (COMMENT.equals(token.getName())) {
369             return token.getType() == TokenType.ENTRY ? currentBlock.addComment(token.getContentAsNormalizedString()) : true;
370         } else if (token.getType() == TokenType.RAW_LINE) {
371             try {
372                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
373                 if (fields.length != 7 && fields.length != 10) {
374                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
375                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
376                 }
377                 stateVectorBlock = new StateVector();
378                 stateVectorBlock.setEpoch(context.getTimeSystem().getConverter(context).parse(fields[0]));
379                 stateVectorBlock.setP(0, Unit.KILOMETRE.toSI(Double.parseDouble(fields[1])));
380                 stateVectorBlock.setP(1, Unit.KILOMETRE.toSI(Double.parseDouble(fields[2])));
381                 stateVectorBlock.setP(2, Unit.KILOMETRE.toSI(Double.parseDouble(fields[3])));
382                 stateVectorBlock.setV(0, Units.KM_PER_S.toSI(Double.parseDouble(fields[4])));
383                 stateVectorBlock.setV(1, Units.KM_PER_S.toSI(Double.parseDouble(fields[5])));
384                 stateVectorBlock.setV(2, Units.KM_PER_S.toSI(Double.parseDouble(fields[6])));
385                 if (fields.length == 10) {
386                     stateVectorBlock.setA(0, Units.KM_PER_S2.toSI(Double.parseDouble(fields[7])));
387                     stateVectorBlock.setA(1, Units.KM_PER_S2.toSI(Double.parseDouble(fields[8])));
388                     stateVectorBlock.setA(2, Units.KM_PER_S2.toSI(Double.parseDouble(fields[9])));
389                 }
390                 return currentBlock.addData(stateVectorBlock.toTimeStampedPVCoordinates(),
391                                             stateVectorBlock.hasAcceleration());
392             } catch (NumberFormatException nfe) {
393                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
394                                           token.getLineNumber(), token.getFileName(), token.getRawContent());
395             }
396         } else {
397             // not a raw line, it is most probably either the end of the data section or a covariance section
398             return false;
399         }
400     }
401 
402     /** Process one state vector data token in a XML message.
403      * @param token token to process
404      * @return true if token was processed, false otherwise
405      */
406     private boolean processXmlStateVectorToken(final ParseToken token) {
407         anticipateNext(this::processXmlSubStructureToken);
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 covariance token in a KVN message.
418      * @param token token to process
419      * @return true if token was processed, false otherwise
420      */
421     private boolean processKvnCovarianceToken(final ParseToken token) {
422         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
423         if (token.getName() != null) {
424             if (OemDataSubStructureKey.COVARIANCE.name().equals(token.getName()) ||
425                 OemDataSubStructureKey.covarianceMatrix.name().equals(token.getName())) {
426                 // we are entering/leaving covariance section
427                 inCovariance = token.getType() == TokenType.START;
428                 return true;
429             } else if (!inCovariance) {
430                 // this is not a covariance token
431                 return false;
432             } else {
433                 // named tokens in covariance section must be at the start, before the raw lines
434                 if (currentRow > 0) {
435                     // the previous covariance matrix was not completed
436                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_ELEMENT_IN_FILE,
437                                               token.getName(), token.getLineNumber(), token.getFileName());
438                 }
439 
440                 if (currentCovariance == null) {
441                     // save the current metadata for later retrieval of reference frame
442                     final OdmCommonMetadata savedMetadata = metadata;
443                     currentCovariance = new CartesianCovariance(
444                             savedMetadata::getReferenceFrame,
445                             savedMetadata.getFrameMapper());
446                     currentRow        = 0;
447                 }
448 
449                 // parse the token
450                 try {
451                     return CartesianCovarianceKey.valueOf(token.getName()).
452                            process(token, context, currentCovariance);
453                 } catch (IllegalArgumentException iae) {
454                     // token not recognized
455                     return false;
456                 }
457 
458             }
459         } else {
460             // this is a raw line
461             try {
462                 final String[] fields = SPLIT_AT_BLANKS.split(token.getContentAsNormalizedString().trim());
463                 if (fields.length != currentRow + 1) {
464                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
465                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
466                 }
467                 for (int j = 0; j < fields.length; ++j) {
468                     currentCovariance.setCovarianceMatrixEntry(currentRow, j, 1.0e6 * Double.parseDouble(fields[j]));
469                 }
470                 if (++currentRow == 6) {
471                     // this was the last row
472                     currentBlock.addCovarianceMatrix(currentCovariance);
473                     currentCovariance = null;
474                     currentRow        = -1;
475                 }
476                 return true;
477             } catch (NumberFormatException nfe) {
478                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
479                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
480             }
481         }
482     }
483 
484     /** Process one covariance matrix data token in a XML message.
485      * @param token token to process
486      * @return true if token was processed, false otherwise
487      */
488     private boolean processXmlCovarianceToken(final ParseToken token) {
489         anticipateNext(this::processXmlSubStructureToken);
490         try {
491             return token.getName() != null &&
492                    CartesianCovarianceKey.valueOf(token.getName()).process(token, context, currentCovariance);
493         } catch (IllegalArgumentException iae) {
494             // token has not been recognized
495             return false;
496         }
497     }
498 
499 }