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