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.ocm;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.function.Function;
24  import java.util.regex.Pattern;
25  
26  import org.orekit.bodies.OneAxisEllipsoid;
27  import org.orekit.data.DataContext;
28  import org.orekit.data.DataSource;
29  import org.orekit.errors.OrekitException;
30  import org.orekit.errors.OrekitIllegalArgumentException;
31  import org.orekit.errors.OrekitMessages;
32  import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
33  import org.orekit.files.ccsds.ndm.odm.OdmHeader;
34  import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
35  import org.orekit.files.ccsds.ndm.odm.OdmParser;
36  import org.orekit.files.ccsds.ndm.odm.UserDefined;
37  import org.orekit.files.ccsds.section.HeaderProcessingState;
38  import org.orekit.files.ccsds.section.KvnStructureProcessingState;
39  import org.orekit.files.ccsds.section.MetadataKey;
40  import org.orekit.files.ccsds.section.Segment;
41  import org.orekit.files.ccsds.section.XmlStructureProcessingState;
42  import org.orekit.files.ccsds.utils.ContextBinding;
43  import org.orekit.files.ccsds.utils.FileFormat;
44  import org.orekit.files.ccsds.utils.lexical.ParseToken;
45  import org.orekit.files.ccsds.utils.lexical.TokenType;
46  import org.orekit.files.ccsds.utils.lexical.UserDefinedXmlTokenBuilder;
47  import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
48  import org.orekit.files.ccsds.utils.parsing.ProcessingState;
49  import org.orekit.files.general.EphemerisFileParser;
50  import org.orekit.time.AbsoluteDate;
51  import org.orekit.utils.IERSConventions;
52  import org.orekit.utils.units.Unit;
53  
54  /** A parser for the CCSDS OCM (Orbit Comprehensive 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 Luc Maisonobe
65   * @since 11.0
66   */
67  public class OcmParser extends OdmParser<Ocm, OcmParser> implements EphemerisFileParser<Ocm> {
68  
69      /** Pattern for splitting strings at blanks. */
70      private static final Pattern SPLIT_AT_BLANKS = Pattern.compile("\\s+");
71  
72      /** File header. */
73      private OdmHeader header;
74  
75      /** Metadata for current observation block. */
76      private OcmMetadata metadata;
77  
78      /** Central body equatorial radius.
79       * @since 12.0
80       */
81      private final double equatorialRadius;
82  
83      /** Central body flattening.
84       * @since 12.0
85       */
86      private final double flattening;
87  
88      /** Context binding valid for current metadata. */
89      private ContextBinding context;
90  
91      /** Trajectory state histories logical blocks. */
92      private List<TrajectoryStateHistory> trajectoryBlocks;
93  
94      /** Current trajectory state metadata. */
95      private TrajectoryStateHistoryMetadata currentTrajectoryStateHistoryMetadata;
96  
97      /** Current trajectory state time history being read. */
98      private List<TrajectoryState> currentTrajectoryStateHistory;
99  
100     /** Physical properties logical block. */
101     private OrbitPhysicalProperties physicBlock;
102 
103     /** Covariance logical blocks. */
104     private List<OrbitCovarianceHistory> covarianceBlocks;
105 
106     /** Current covariance metadata. */
107     private OrbitCovarianceHistoryMetadata currentCovarianceHistoryMetadata;
108 
109     /** Current covariance history being read. */
110     private List<OrbitCovariance> currentCovarianceHistory;
111 
112     /** Maneuver logical blocks. */
113     private List<OrbitManeuverHistory> maneuverBlocks;
114 
115     /** Current maneuver metadata. */
116     private OrbitManeuverHistoryMetadata currentManeuverHistoryMetadata;
117 
118     /** Current maneuver history being read. */
119     private List<OrbitManeuver> currentManeuverHistory;
120 
121     /** Perturbations logical block. */
122     private Perturbations perturbationsBlock;
123 
124     /** Orbit determination logical block. */
125     private OrbitDetermination orbitDeterminationBlock;
126 
127     /** User defined parameters logical block. */
128     private UserDefined userDefinedBlock;
129 
130     /** Processor for global message structure. */
131     private ProcessingState structureProcessor;
132 
133     /**
134      * Complete constructor.
135      * <p>
136      * Calling this constructor directly is not recommended. Users should rather use
137      * {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildOcmParser()
138      * parserBuilder.buildOcmParser()}.
139      * </p>
140      * @param conventions IERS Conventions
141      * @param equatorialRadius central body equatorial radius
142      * @param flattening central body flattening
143      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
144      * @param dataContext used to retrieve frames, time scales, etc.
145      * @param mu gravitational coefficient
146      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
147      * @param filters filters to apply to parse tokens
148      * @since 12.0
149      */
150     public OcmParser(final IERSConventions conventions,
151                      final double equatorialRadius, final double flattening,
152                      final boolean simpleEOP, final DataContext dataContext,
153                      final double mu, final ParsedUnitsBehavior parsedUnitsBehavior,
154                      final Function<ParseToken, List<ParseToken>>[] filters) {
155         super(Ocm.ROOT, Ocm.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext, null,
156               mu, parsedUnitsBehavior, filters);
157         this.equatorialRadius = equatorialRadius;
158         this.flattening       = flattening;
159     }
160 
161     /** {@inheritDoc} */
162     @Override
163     public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {
164 
165         final Map<String, XmlTokenBuilder> builders = super.getSpecialXmlElementsBuilders();
166 
167         // special handling of user-defined parameters
168         builders.put(UserDefined.USER_DEFINED_XML_TAG, new UserDefinedXmlTokenBuilder());
169 
170         return builders;
171 
172     }
173 
174     /** {@inheritDoc} */
175     @Override
176     public Ocm parse(final DataSource source) {
177         return parseMessage(source);
178     }
179 
180     /** {@inheritDoc} */
181     @Override
182     public OdmHeader getHeader() {
183         return header;
184     }
185 
186     /** {@inheritDoc} */
187     @Override
188     public void reset(final FileFormat fileFormat) {
189         header                  = new OdmHeader();
190         metadata                = null;
191         context                 = null;
192         trajectoryBlocks        = null;
193         physicBlock             = null;
194         covarianceBlocks        = null;
195         maneuverBlocks          = null;
196         perturbationsBlock      = null;
197         orbitDeterminationBlock = null;
198         userDefinedBlock        = null;
199         if (fileFormat == FileFormat.XML) {
200             structureProcessor = new XmlStructureProcessingState(Ocm.ROOT, this);
201             reset(fileFormat, structureProcessor);
202         } else {
203             structureProcessor = new KvnStructureProcessingState(this);
204             reset(fileFormat, new HeaderProcessingState(this));
205         }
206     }
207 
208     /** {@inheritDoc} */
209     @Override
210     public boolean prepareHeader() {
211         anticipateNext(new HeaderProcessingState(this));
212         return true;
213     }
214 
215     /** {@inheritDoc} */
216     @Override
217     public boolean inHeader() {
218         anticipateNext(structureProcessor);
219         return true;
220     }
221 
222     /** {@inheritDoc} */
223     @Override
224     public boolean finalizeHeader() {
225         header.validate(header.getFormatVersion());
226         return true;
227     }
228 
229     /** {@inheritDoc} */
230     @Override
231     public boolean prepareMetadata() {
232         if (metadata != null) {
233             return false;
234         }
235         metadata  = new OcmMetadata(getDataContext());
236         context   = new ContextBinding(this::getConventions, this::isSimpleEOP, this::getDataContext,
237                                        this::getParsedUnitsBehavior, metadata::getEpochT0, metadata::getTimeSystem,
238                                        metadata::getSclkOffsetAtEpoch, metadata::getSclkSecPerSISec);
239         anticipateNext(this::processMetadataToken);
240         return true;
241     }
242 
243     /** {@inheritDoc} */
244     @Override
245     public boolean inMetadata() {
246         anticipateNext(structureProcessor);
247         return true;
248     }
249 
250     /** {@inheritDoc} */
251     @Override
252     public boolean finalizeMetadata() {
253         metadata.validate(header.getFormatVersion());
254         anticipateNext(this::processDataSubStructureToken);
255         return true;
256     }
257 
258     /** {@inheritDoc} */
259     @Override
260     public boolean prepareData() {
261         anticipateNext(this::processDataSubStructureToken);
262         return true;
263     }
264 
265     /** {@inheritDoc} */
266     @Override
267     public boolean inData() {
268         return true;
269     }
270 
271     /** {@inheritDoc} */
272     @Override
273     public boolean finalizeData() {
274         final List<TrajectoryStateHistory> old = trajectoryBlocks;
275         if (old != null) {
276             final OneAxisEllipsoid body =
277                             currentTrajectoryStateHistoryMetadata.getTrajType() == OrbitElementsType.GEODETIC ?
278                             new OneAxisEllipsoid(equatorialRadius, flattening,
279                                                  currentTrajectoryStateHistoryMetadata.getTrajReferenceFrame().asFrame()) :
280                             null;
281             trajectoryBlocks = new ArrayList<>(old.size());
282             for (final TrajectoryStateHistory osh : old) {
283                 trajectoryBlocks.add(new TrajectoryStateHistory(osh.getMetadata(), osh.getTrajectoryStates(),
284                                                                 body, getSelectedMu()));
285             }
286         }
287         return true;
288     }
289 
290     /** Manage trajectory state history section.
291      * @param starting if true, parser is entering the section
292      * otherwise it is leaving the section
293      * @return always return true
294      */
295     boolean manageTrajectoryStateSection(final boolean starting) {
296         if (starting) {
297             if (trajectoryBlocks == null) {
298                 // this is the first trajectory block, we need to allocate the container
299                 trajectoryBlocks = new ArrayList<>();
300             }
301             currentTrajectoryStateHistoryMetadata = new TrajectoryStateHistoryMetadata(metadata.getEpochT0(),
302                                                                                        getDataContext());
303             currentTrajectoryStateHistory         = new ArrayList<>();
304             anticipateNext(this::processTrajectoryStateToken);
305         } else {
306             final OneAxisEllipsoid body =
307                             currentTrajectoryStateHistoryMetadata.getTrajType() == OrbitElementsType.GEODETIC ?
308                             new OneAxisEllipsoid(equatorialRadius, flattening,
309                                                  currentTrajectoryStateHistoryMetadata.getTrajReferenceFrame().asFrame()) :
310                             null;
311             anticipateNext(structureProcessor);
312             if (currentTrajectoryStateHistoryMetadata.getCenter().getBody() != null) {
313                 setMuCreated(currentTrajectoryStateHistoryMetadata.getCenter().getBody().getGM());
314             }
315             // we temporarily set gravitational parameter to NaN,
316             // as we may get a proper one in the perturbations section
317             trajectoryBlocks.add(new TrajectoryStateHistory(currentTrajectoryStateHistoryMetadata,
318                                                             currentTrajectoryStateHistory,
319                                                             body, Double.NaN));
320         }
321         return true;
322     }
323 
324     /** Manage physical properties section.
325      * @param starting if true, parser is entering the section
326      * otherwise it is leaving the section
327      * @return always return true
328      */
329     boolean managePhysicalPropertiesSection(final boolean starting) {
330         if (starting) {
331             if (physicBlock == null) {
332                 // this is the first (and unique) physical properties block, we need to allocate the container
333                 physicBlock = new OrbitPhysicalProperties(metadata.getEpochT0());
334             }
335             anticipateNext(this::processPhysicalPropertyToken);
336         } else {
337             anticipateNext(structureProcessor);
338         }
339         return true;
340     }
341 
342     /** Manage covariance history section.
343      * @param starting if true, parser is entering the section
344      * otherwise it is leaving the section
345      * @return always return true
346      */
347     boolean manageCovarianceHistorySection(final boolean starting) {
348         if (starting) {
349             if (covarianceBlocks == null) {
350                 // this is the first covariance block, we need to allocate the container
351                 covarianceBlocks = new ArrayList<>();
352             }
353             currentCovarianceHistoryMetadata = new OrbitCovarianceHistoryMetadata(metadata.getEpochT0());
354             currentCovarianceHistory         = new ArrayList<>();
355             anticipateNext(this::processCovarianceToken);
356         } else {
357             anticipateNext(structureProcessor);
358             covarianceBlocks.add(new OrbitCovarianceHistory(currentCovarianceHistoryMetadata,
359                                                             currentCovarianceHistory));
360             currentCovarianceHistoryMetadata = null;
361             currentCovarianceHistory         = null;
362         }
363         return true;
364     }
365 
366     /** Manage maneuvers section.
367      * @param starting if true, parser is entering the section
368      * otherwise it is leaving the section
369      * @return always return true
370      */
371     boolean manageManeuversSection(final boolean starting) {
372         if (starting) {
373             if (maneuverBlocks == null) {
374                 // this is the first maneuver block, we need to allocate the container
375                 maneuverBlocks = new ArrayList<>();
376             }
377             currentManeuverHistoryMetadata = new OrbitManeuverHistoryMetadata(metadata.getEpochT0());
378             currentManeuverHistory         = new ArrayList<>();
379             anticipateNext(this::processManeuverToken);
380         } else {
381             anticipateNext(structureProcessor);
382             maneuverBlocks.add(new OrbitManeuverHistory(currentManeuverHistoryMetadata,
383                                                         currentManeuverHistory));
384             currentManeuverHistoryMetadata = null;
385             currentManeuverHistory         = null;
386         }
387         return true;
388     }
389 
390     /** Manage perturbation parameters section.
391      * @param starting if true, parser is entering the section
392      * otherwise it is leaving the section
393      * @return always return true
394      */
395     boolean managePerturbationParametersSection(final boolean starting) {
396         if (starting) {
397             if (perturbationsBlock == null) {
398                 // this is the first (and unique) perturbations parameters block, we need to allocate the container
399                 perturbationsBlock = new Perturbations(context.getDataContext().getCelestialBodies());
400             }
401             anticipateNext(this::processPerturbationToken);
402         } else {
403             anticipateNext(structureProcessor);
404         }
405         return true;
406     }
407 
408     /** Manage orbit determination section.
409      * @param starting if true, parser is entering the section
410      * otherwise it is leaving the section
411      * @return always return true
412      */
413     boolean manageOrbitDeterminationSection(final boolean starting) {
414         if (starting) {
415             if (orbitDeterminationBlock == null) {
416                 // this is the first (and unique) orbit determination block, we need to allocate the container
417                 orbitDeterminationBlock = new OrbitDetermination();
418             }
419             anticipateNext(this::processOrbitDeterminationToken);
420         } else {
421             anticipateNext(structureProcessor);
422         }
423         return true;
424     }
425 
426     /** Manage user-defined parameters section.
427      * @param starting if true, parser is entering the section
428      * otherwise it is leaving the section
429      * @return always return true
430      */
431     boolean manageUserDefinedParametersSection(final boolean starting) {
432         if (starting) {
433             if (userDefinedBlock == null) {
434                 // this is the first (and unique) user-defined parameters block, we need to allocate the container
435                 userDefinedBlock = new UserDefined();
436             }
437             anticipateNext(this::processUserDefinedToken);
438         } else {
439             anticipateNext(structureProcessor);
440         }
441         return true;
442     }
443 
444     /** {@inheritDoc} */
445     @Override
446     public Ocm build() {
447 
448         // OCM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
449         // automatically before the end of the file
450         finalizeData();
451         if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
452             userDefinedBlock = null;
453         }
454 
455         // the mu is needed only if there are trajectories
456         final double mu;
457         if (trajectoryBlocks == null) {
458             mu = Double.NaN;
459         } else {
460             if (perturbationsBlock != null) {
461                 // this may be Double.NaN, but it will be handled correctly
462                 setMuParsed(perturbationsBlock.getGm());
463             }
464             mu = getSelectedMu();
465         }
466 
467         final OcmData data = new OcmData(trajectoryBlocks, physicBlock, covarianceBlocks,
468                                          maneuverBlocks, perturbationsBlock,
469                                          orbitDeterminationBlock, userDefinedBlock);
470         data.validate(header.getFormatVersion());
471 
472         return new Ocm(header, Collections.singletonList(new Segment<>(metadata, data)),
473                            getConventions(), getDataContext(), mu);
474 
475     }
476 
477     /** Process one metadata token.
478      * @param token token to process
479      * @return true if token was processed, false otherwise
480      */
481     private boolean processMetadataToken(final ParseToken token) {
482         inMetadata();
483         try {
484             return token.getName() != null &&
485                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
486         } catch (IllegalArgumentException iaeM) {
487             try {
488                 return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
489             } catch (IllegalArgumentException iaeD) {
490                 try {
491                     return OcmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
492                 } catch (IllegalArgumentException iaeC) {
493                     // token has not been recognized
494                     return false;
495                 }
496             }
497         }
498     }
499 
500     /** Process one data substructure token.
501      * @param token token to process
502      * @return true if token was processed, false otherwise
503      */
504     private boolean processDataSubStructureToken(final ParseToken token) {
505         try {
506             return token.getName() != null &&
507                    OcmDataSubStructureKey.valueOf(token.getName()).process(token, this);
508         } catch (IllegalArgumentException iae) {
509             // token has not been recognized
510             return false;
511         }
512     }
513 
514     /** Process one trajectory state history data token.
515      * @param token token to process
516      * @return true if token was processed, false otherwise
517      */
518     private boolean processTrajectoryStateToken(final ParseToken token) {
519         if (token.getName() != null && !token.getName().equals(Ocm.TRAJ_LINE)) {
520             // we are in the section metadata part
521             try {
522                 return TrajectoryStateHistoryMetadataKey.valueOf(token.getName()).
523                        process(token, context, currentTrajectoryStateHistoryMetadata);
524             } catch (IllegalArgumentException iae) {
525                 // token has not been recognized
526                 return false;
527             }
528         } else {
529             // we are in the section data part
530             if (currentTrajectoryStateHistory.isEmpty()) {
531                 // we are starting the real data section, we can now check metadata is complete
532                 currentTrajectoryStateHistoryMetadata.validate(header.getFormatVersion());
533                 anticipateNext(this::processDataSubStructureToken);
534             }
535             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
536                 return true;
537             }
538             try {
539                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
540                 // as TRAJ_UNITS is optional and indeed MUST match type, get them directly from type
541                 final List<Unit> units = currentTrajectoryStateHistoryMetadata.getTrajType().getUnits();
542                 if (fields.length != units.size() + 1) {
543                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
544                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
545                 }
546                 final AbsoluteDate epoch = context.getTimeSystem().getConverter(context).parse(fields[0]);
547                 return currentTrajectoryStateHistory.add(new TrajectoryState(currentTrajectoryStateHistoryMetadata.getTrajType(),
548                                                                              epoch, fields, 1, units));
549             } catch (NumberFormatException | OrekitIllegalArgumentException e) {
550                 throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
551                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
552             }
553         }
554     }
555 
556     /** Process one physical property data token.
557      * @param token token to process
558      * @return true if token was processed, false otherwise
559      */
560     private boolean processPhysicalPropertyToken(final ParseToken token) {
561         if (physicBlock == null) {
562             physicBlock = new OrbitPhysicalProperties(metadata.getEpochT0());
563         }
564         anticipateNext(this::processDataSubStructureToken);
565         try {
566             return token.getName() != null &&
567                    OrbitPhysicalPropertiesKey.valueOf(token.getName()).process(token, context, physicBlock);
568         } catch (IllegalArgumentException iae) {
569             // token has not been recognized
570             return false;
571         }
572     }
573 
574     /** Process one covariance history history data token.
575      * @param token token to process
576      * @return true if token was processed, false otherwise
577      */
578     private boolean processCovarianceToken(final ParseToken token) {
579         if (token.getName() != null && !token.getName().equals(Ocm.COV_LINE)) {
580             // we are in the section metadata part
581             try {
582                 return OrbitCovarianceHistoryMetadataKey.valueOf(token.getName()).
583                        process(token, context, currentCovarianceHistoryMetadata);
584             } catch (IllegalArgumentException iae) {
585                 // token has not been recognized
586                 return false;
587             }
588         } else {
589             // we are in the section data part
590             if (currentCovarianceHistory.isEmpty()) {
591                 // we are starting the real data section, we can now check metadata is complete
592                 currentCovarianceHistoryMetadata.validate(header.getFormatVersion());
593                 anticipateNext(this::processDataSubStructureToken);
594             }
595             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
596                 return true;
597             }
598             try {
599                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
600                 final int n = currentCovarianceHistoryMetadata.getCovType().getUnits().size();
601                 if (fields.length - 1 != currentCovarianceHistoryMetadata.getCovOrdering().nbElements(n)) {
602                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
603                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
604                 }
605                 currentCovarianceHistory.add(new OrbitCovariance(currentCovarianceHistoryMetadata.getCovType(),
606                                                             currentCovarianceHistoryMetadata.getCovOrdering(),
607                                                             context.getTimeSystem().getConverter(context).parse(fields[0]),
608                                                             fields, 1));
609                 return true;
610             } catch (NumberFormatException nfe) {
611                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
612                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
613             }
614         }
615     }
616 
617     /** Process one maneuver data token.
618      * @param token token to process
619      * @return true if token was processed, false otherwise
620      */
621     private boolean processManeuverToken(final ParseToken token) {
622         if (token.getName() != null && !token.getName().equals(Ocm.MAN_LINE)) {
623             // we are in the section metadata part
624             try {
625                 return OrbitManeuverHistoryMetadataKey.valueOf(token.getName()).
626                        process(token, context, currentManeuverHistoryMetadata);
627             } catch (IllegalArgumentException iae) {
628                 // token has not been recognized
629                 return false;
630             }
631         } else {
632             // we are in the section data part
633             if (currentManeuverHistory.isEmpty()) {
634                 // we are starting the real data section, we can now check metadata is complete
635                 currentManeuverHistoryMetadata.validate(header.getFormatVersion());
636                 anticipateNext(this::processDataSubStructureToken);
637             }
638             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
639                 return true;
640             }
641             try {
642                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
643                 final List<ManeuverFieldType> types = currentManeuverHistoryMetadata.getManComposition();
644                 if (fields.length != types.size()) {
645                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
646                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
647                 }
648                 final OrbitManeuver maneuver = new OrbitManeuver();
649                 for (int i = 0; i < fields.length; ++i) {
650                     types.get(i).process(fields[i], context, maneuver, token.getLineNumber(), token.getFileName());
651                 }
652                 currentManeuverHistory.add(maneuver);
653                 return true;
654             } catch (NumberFormatException nfe) {
655                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
656                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
657             }
658         }
659     }
660 
661     /** Process one perturbation parameter data token.
662      * @param token token to process
663      * @return true if token was processed, false otherwise
664      */
665     private boolean processPerturbationToken(final ParseToken token) {
666         anticipateNext(this::processDataSubStructureToken);
667         try {
668             return token.getName() != null &&
669                    PerturbationsKey.valueOf(token.getName()).process(token, context, perturbationsBlock);
670         } catch (IllegalArgumentException iae) {
671             // token has not been recognized
672             return false;
673         }
674     }
675 
676     /** Process one orbit determination data token.
677      * @param token token to process
678      * @return true if token was processed, false otherwise
679      */
680     private boolean processOrbitDeterminationToken(final ParseToken token) {
681         if (orbitDeterminationBlock == null) {
682             orbitDeterminationBlock = new OrbitDetermination();
683         }
684         anticipateNext(this::processDataSubStructureToken);
685         try {
686             return token.getName() != null &&
687                    OrbitDeterminationKey.valueOf(token.getName()).process(token, context, orbitDeterminationBlock);
688         } catch (IllegalArgumentException iae) {
689             // token has not been recognized
690             return false;
691         }
692     }
693 
694     /** Process one user-defined parameter data token.
695      * @param token token to process
696      * @return true if token was processed, false otherwise
697      */
698     private boolean processUserDefinedToken(final ParseToken token) {
699         if (userDefinedBlock == null) {
700             userDefinedBlock = new UserDefined();
701         }
702         anticipateNext(this::processDataSubStructureToken);
703         if ("COMMENT".equals(token.getName())) {
704             return token.getType() == TokenType.ENTRY ? userDefinedBlock.addComment(token.getContentAsNormalizedString()) : true;
705         } else if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
706             if (token.getType() == TokenType.ENTRY) {
707                 userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
708                                           token.getContentAsNormalizedString());
709             }
710             return true;
711         } else {
712             // the token was not processed
713             return false;
714         }
715     }
716 
717 }