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