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