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