1   /* Copyright 2002-2026 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;
18  
19  import java.lang.reflect.Array;
20  import java.util.List;
21  import java.util.function.Function;
22  
23  import org.orekit.annotation.DefaultDataContext;
24  import org.orekit.data.DataContext;
25  import org.orekit.files.ccsds.definitions.CcsdsFrameMapper;
26  import org.orekit.files.ccsds.definitions.OrekitCcsdsFrameMapper;
27  import org.orekit.files.ccsds.ndm.adm.acm.AcmParser;
28  import org.orekit.files.ccsds.ndm.adm.aem.AemParser;
29  import org.orekit.files.ccsds.ndm.adm.apm.ApmParser;
30  import org.orekit.files.ccsds.ndm.cdm.CdmParser;
31  import org.orekit.files.ccsds.ndm.odm.ocm.OcmParser;
32  import org.orekit.files.ccsds.ndm.odm.oem.OemParser;
33  import org.orekit.files.ccsds.ndm.odm.omm.OmmParser;
34  import org.orekit.files.ccsds.ndm.odm.opm.OpmParser;
35  import org.orekit.files.ccsds.ndm.tdm.IdentityConverter;
36  import org.orekit.files.ccsds.ndm.tdm.RangeUnits;
37  import org.orekit.files.ccsds.ndm.tdm.RangeUnitsConverter;
38  import org.orekit.files.ccsds.ndm.tdm.TdmParser;
39  import org.orekit.files.ccsds.utils.lexical.ParseToken;
40  import org.orekit.frames.Frame;
41  import org.orekit.time.AbsoluteDate;
42  import org.orekit.utils.IERSConventions;
43  
44  /** Builder for all {@link NdmConstituent CCSDS Message} files parsers.
45   * <p>
46   * This builder can be used for building all CCSDS Messages parsers types.
47   * It is particularly useful in multi-threaded context as parsers cannot
48   * be shared between threads and thus several independent parsers must be
49   * built in this case.
50   * </p>
51   * @author Luc Maisonobe
52   * @since 11.0
53   */
54  public class ParserBuilder extends AbstractBuilder<ParserBuilder> {
55  
56      /** Indicator for simple or accurate EOP interpolation. */
57      private final  boolean simpleEOP;
58  
59      /** Gravitational coefficient. */
60      private final double mu;
61  
62      /** Default mass. */
63      private final double defaultMass;
64  
65      /** Default interpolation degree. */
66      private final int defaultInterpolationDegree;
67  
68      /** Behavior adopted for units that have been parsed from a CCSDS message. */
69      private final ParsedUnitsBehavior parsedUnitsBehavior;
70  
71      /** Filters for parse tokens.
72       * @since 12.0
73       */
74      private final Function<ParseToken, List<ParseToken>>[] filters;
75  
76      /**
77       * Maps CCSDS center and frame to a {@link Frame}.
78       *
79       * @since 13.1.5
80       */
81      private final CcsdsFrameMapper frameMapper;
82  
83      /**
84       * Simple constructor.
85       * <p>
86       * This constructor creates a builder with
87       * <ul>
88       *   <li>{@link #getConventions() IERS conventions} set to {@link IERSConventions#IERS_2010}</li>
89       *   <li>{@link #isSimpleEOP() simple EOP} set to {@code true}</li>
90       *   <li>{@link #getDataContext() data context} set to {@link DataContext#getDefault() default context}</li>
91       *   <li>{@link #getMissionReferenceDate() mission reference date} set to {@code null}</li>
92       *   <li>{@link #getMu() gravitational coefficient} set to {@code Double.NaN}</li>
93       *   <li>{@link #getEquatorialRadius() central body equatorial radius} set to {@code Double.NaN}</li>
94       *   <li>{@link #getFlattening() central body flattening} set to {@code Double.NaN}</li>
95       *   <li>{@link #getDefaultMass() default mass} set to {@code Double.NaN}</li>
96       *   <li>{@link #getDefaultInterpolationDegree() default interpolation degree} set to {@code 1}</li>
97       *   <li>{@link #getParsedUnitsBehavior() parsed unit behavior} set to {@link ParsedUnitsBehavior#CONVERT_COMPATIBLE}</li>
98       *   <li>{@link #getRangeUnitsConverter() converter for range units} set to {@link IdentityConverter}</li>
99       * </ul>
100      */
101     @DefaultDataContext
102     public ParserBuilder() {
103         this(DataContext.getDefault());
104     }
105 
106     /**
107      * Simple constructor.
108      * <p>
109      * This constructor creates a builder with
110      * <ul>
111      *   <li>{@link #getConventions() IERS conventions} set to {@link IERSConventions#IERS_2010}</li>
112      *   <li>{@link #isSimpleEOP() simple EOP} set to {@code true}</li>
113      *   <li>{@link #getMissionReferenceDate() mission reference date} set to {@code null}</li>
114      *   <li>{@link #getMu() gravitational coefficient} set to {@code Double.NaN}</li>
115      *   <li>{@link #getEquatorialRadius() central body equatorial radius} set to {@code Double.NaN}</li>
116      *   <li>{@link #getFlattening() central body flattening} set to {@code Double.NaN}</li>
117      *   <li>{@link #getDefaultMass() default mass} set to {@code Double.NaN}</li>
118      *   <li>{@link #getDefaultInterpolationDegree() default interpolation degree} set to {@code 1}</li>
119      *   <li>{@link #getParsedUnitsBehavior() parsed unit behavior} set to {@link ParsedUnitsBehavior#CONVERT_COMPATIBLE}</li>
120      *   <li>{@link #getRangeUnitsConverter() converter for range units} set to {@link IdentityConverter}</li>
121      * </ul>
122      * @param dataContext data context used to retrieve frames, time scales, etc.
123      */
124     @SuppressWarnings("unchecked")
125     public ParserBuilder(final DataContext dataContext) {
126         this(IERSConventions.IERS_2010, Double.NaN, Double.NaN, dataContext,
127              null, new IdentityConverter(), true, Double.NaN, Double.NaN,
128              1, ParsedUnitsBehavior.CONVERT_COMPATIBLE,
129              (Function<ParseToken, List<ParseToken>>[]) Array.newInstance(Function.class, 0),
130                 new OrekitCcsdsFrameMapper());
131     }
132 
133     /** Complete constructor.
134      * @param conventions IERS Conventions
135      * @param equatorialRadius central body equatorial radius
136      * @param flattening central body flattening
137      * @param dataContext used to retrieve frames, time scales, etc.
138      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
139      * @param rangeUnitsConverter converter for {@link RangeUnits#RU Range Units}
140      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
141      * @param mu gravitational coefficient
142      * @param defaultMass default mass
143      * @param defaultInterpolationDegree default interpolation degree
144      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
145      * @param filters filters to apply to parse tokens
146      * @param frameMapper for creating a {@link Frame}.
147      * @since 13.1.5
148      */
149     private ParserBuilder(final IERSConventions conventions,
150                           final double equatorialRadius, final double flattening,
151                           final DataContext dataContext, final AbsoluteDate missionReferenceDate,
152                           final RangeUnitsConverter rangeUnitsConverter,
153                           final boolean simpleEOP, final double mu,
154                           final double defaultMass, final int defaultInterpolationDegree,
155                           final ParsedUnitsBehavior parsedUnitsBehavior,
156                           final Function<ParseToken, List<ParseToken>>[] filters,
157                           final CcsdsFrameMapper frameMapper) {
158         super(conventions, equatorialRadius, flattening, dataContext, missionReferenceDate, rangeUnitsConverter);
159         this.simpleEOP                  = simpleEOP;
160         this.mu                         = mu;
161         this.defaultMass                = defaultMass;
162         this.defaultInterpolationDegree = defaultInterpolationDegree;
163         this.parsedUnitsBehavior        = parsedUnitsBehavior;
164         this.filters                    = filters.clone();
165         this.frameMapper                = frameMapper;
166     }
167 
168     /** {@inheritDoc} */
169     @Override
170     protected ParserBuilder create(final IERSConventions newConventions,
171                                    final double newEquatorialRadius, final double newFlattening,
172                                    final DataContext newDataContext,
173                                    final AbsoluteDate newMissionReferenceDate, final RangeUnitsConverter newRangeUnitsConverter) {
174         return new ParserBuilder(newConventions, newEquatorialRadius, newFlattening, newDataContext,
175                                  newMissionReferenceDate, newRangeUnitsConverter, simpleEOP, mu,
176                                  defaultMass, defaultInterpolationDegree, parsedUnitsBehavior, filters,
177                                  getFrameMapper());
178     }
179 
180     /** Set up flag for ignoring tidal effects when interpolating EOP.
181      * @param newSimpleEOP true if tidal effects are ignored when interpolating EOP
182      * @return a new builder with updated configuration (the instance is not changed)
183      */
184     public ParserBuilder withSimpleEOP(final boolean newSimpleEOP) {
185         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
186                                  getMissionReferenceDate(), getRangeUnitsConverter(), newSimpleEOP, getMu(), getDefaultMass(),
187                                  getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters(),
188                                  getFrameMapper());
189     }
190 
191     /** Check if tidal effects are ignored when interpolating EOP.
192      * @return true if tidal effects are ignored when interpolating EOP
193      */
194     public boolean isSimpleEOP() {
195         return simpleEOP;
196     }
197 
198     /** Set up the gravitational coefficient.
199      * @param newMu gravitational coefficient
200      * @return a new builder with updated configuration (the instance is not changed)
201      */
202     public ParserBuilder withMu(final double newMu) {
203         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
204                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), newMu, getDefaultMass(),
205                                  getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters(),
206                                  getFrameMapper());
207     }
208 
209     /** Get the gravitational coefficient.
210      * @return gravitational coefficient
211      */
212     public double getMu() {
213         return mu;
214     }
215 
216     /** Set up the default mass.
217      * <p>
218      * The default mass is used only by {@link OpmParser}.
219      * </p>
220      * @param newDefaultMass default mass
221      * @return a new builder with updated configuration (the instance is not changed)
222      */
223     public ParserBuilder withDefaultMass(final double newDefaultMass) {
224         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
225                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), getMu(), newDefaultMass,
226                                  getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters(),
227                                  getFrameMapper());
228     }
229 
230     /** Get the default mass.
231      * @return default mass
232      */
233     public double getDefaultMass() {
234         return defaultMass;
235     }
236 
237     /** Set up the default interpolation degree.
238      * <p>
239      * The default interpolation degree is used only by {@link AemParser}
240      * and {@link OemParser}.
241      * </p>
242      * @param newDefaultInterpolationDegree default interpolation degree
243      * @return a new builder with updated configuration (the instance is not changed)
244      */
245     public ParserBuilder withDefaultInterpolationDegree(final int newDefaultInterpolationDegree) {
246         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
247                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), getMu(), getDefaultMass(),
248                                  newDefaultInterpolationDegree, getParsedUnitsBehavior(), getFilters(),
249                                  getFrameMapper());
250     }
251 
252     /** Get the default interpolation degree.
253      * @return default interpolation degree
254      */
255     public int getDefaultInterpolationDegree() {
256         return defaultInterpolationDegree;
257     }
258 
259     /** Set up the behavior to adopt for handling parsed units.
260      * @param newParsedUnitsBehavior behavior to adopt for handling parsed units
261      * @return a new builder with updated configuration (the instance is not changed)
262      */
263     public ParserBuilder withParsedUnitsBehavior(final ParsedUnitsBehavior newParsedUnitsBehavior) {
264         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
265                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), getMu(), getDefaultMass(),
266                                  getDefaultInterpolationDegree(), newParsedUnitsBehavior, getFilters(),
267                                  getFrameMapper());
268     }
269 
270     /** Get the behavior to adopt for handling parsed units.
271      * @return behavior to adopt for handling parsed units
272      */
273     public ParsedUnitsBehavior getParsedUnitsBehavior() {
274         return parsedUnitsBehavior;
275     }
276 
277     /** Add a filter for parsed tokens.
278      * <p>
279      * This filter allows to change parsed tokens. This method can be called several times,
280      * once for each filter to set up. The filters are always applied in the order they were set.
281      * There are several use cases for this feature.
282      * </p>
283      * <p>
284      * The first use case is to allow parsing malformed CCSDS messages with some known
285      * discrepancies that can be fixed. One real life example (the one that motivated the
286      * development of this feature) is OMM files in XML format that add an empty
287      * OBJECT_ID. This could be fixed by setting a filter as follows:
288      * </p>
289      * <pre>{@code
290      * Omm omm = new ParserBuilder().
291      *           withFilter(token -> {
292      *                          if ("OBJECT_ID".equals(token.getName()) &&
293      *                              (token.getRawContent() == null || token.getRawContent().isEmpty())) {
294      *                              // replace null/empty entries with "unknown"
295      *                              return Collections.singletonList(new ParseToken(token.getType(), token.getName(),
296      *                                                                              "unknown", token.getUnits(),
297      *                                                                              token.getLineNumber(), token.getFileName()));
298      *                          } else {
299      *                              return Collections.singletonList(token);
300      *                          }
301      *                     }).
302      *           buildOmmParser().
303      *           parseMessage(message);
304      * }</pre>
305      * <p>
306      * A second use case is to remove unwanted data. For example in order to remove all user-defined data
307      * one could use:
308      * </p>
309      * <pre>{@code
310      * Omm omm = new ParserBuilder().
311      *           withFilter(token -> {
312      *                          if (token.getName().startsWith("USER_DEFINED")) {
313      *                              return Collections.emptyList();
314      *                          } else {
315      *                              return Collections.singletonList(token);
316      *                          }
317      *                     }).
318      *           buildOmmmParser().
319      *           parseMessage(message);
320      * }</pre>
321      * <p>
322      * A third use case is to add data not originally present in the file. For example in order
323      * to add a generated ODM V3 message id to an ODM V2 message that lacks it, one could do:
324      * </p>
325      * <pre>{@code
326      * final String myMessageId = ...; // this could be computed from a counter, or a SHA256 digest, or some metadata
327      * Omm omm = new ParserBuilder()
328      *           withFilter(token -> {
329      *                          if ("CCSDS_OMM_VERS".equals(token.getName())) {
330      *                              // enforce ODM V3
331      *                              return Collections.singletonList(new ParseToken(token.getType(), token.getName(),
332      *                                                                              "3.0", token.getUnits(),
333      *                                                                              token.getLineNumber(), token.getFileName()));
334      *                          } else {
335      *                              return Collections.singletonList(token);
336      *                          }
337      *                      }).
338      *           withFilter(token -> {
339      *                          if ("ORIGINATOR".equals(token.getName())) {
340      *                              // add generated message ID after ORIGINATOR entry
341      *                              return Arrays.asList(token,
342      *                                                   new ParseToken(TokenType.ENTRY, "MESSAGE_ID",
343      *                                                                  myMessageId, null,
344      *                                                                  -1, token.getFileName()));
345      *                          } else {
346      *                              return Collections.singletonList(token);
347      *                          }
348      *                      }).
349      *           buildOmmmParser().
350      *           parseMessage(message);
351      * }</pre>
352      * @param filter token filter to add
353      * @return a new builder with updated configuration (the instance is not changed)
354      * @since 12.0
355      */
356     public ParserBuilder withFilter(final Function<ParseToken, List<ParseToken>> filter) {
357 
358         // populate new filters array
359         @SuppressWarnings("unchecked")
360         final Function<ParseToken, List<ParseToken>>[] newFilters =
361                         (Function<ParseToken, List<ParseToken>>[]) Array.newInstance(Function.class, filters.length + 1);
362         System.arraycopy(filters, 0, newFilters, 0, filters.length);
363         newFilters[filters.length] = filter;
364 
365         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
366                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), getMu(), getDefaultMass(),
367                                  getDefaultInterpolationDegree(), getParsedUnitsBehavior(),
368                                  newFilters, getFrameMapper());
369 
370     }
371 
372     /** Get the filters to apply to parse tokens.
373      * @return filters to apply to parse tokens
374      * @since 12.0
375      */
376     public Function<ParseToken, List<ParseToken>>[] getFilters() {
377         return filters.clone();
378     }
379 
380     /**
381      * Get the mapping between CCSDS NDM center and frame and a {@link Frame}.
382      *
383      * @return the frame mapper.
384      * @since 13.1.5
385      */
386     public CcsdsFrameMapper getFrameMapper() {
387         return frameMapper;
388     }
389 
390     /**
391      * Set the mapping between CCSDS NDM center and frame and a {@link Frame}.
392      *
393      * @param newFrameMapper the frame mapper.
394      * @return a new builder with updated configuration (the instance is not
395      * changed)
396      * @since 13.1.5
397      */
398     public ParserBuilder withFrameMapper(final CcsdsFrameMapper newFrameMapper) {
399         return new ParserBuilder(getConventions(), getEquatorialRadius(),
400                 getFlattening(), getDataContext(), getMissionReferenceDate(),
401                 getRangeUnitsConverter(), isSimpleEOP(), getMu(),
402                 getDefaultMass(), getDefaultInterpolationDegree(),
403                 getParsedUnitsBehavior(), getFilters(), newFrameMapper);
404     }
405 
406     /** Build a parser for {@link org.orekit.files.ccsds.ndm.Ndm Navigation Data Messages}.
407      * @return a new parser
408      */
409     public NdmParser buildNdmParser() {
410         return new NdmParser(this, getFilters());
411     }
412 
413     /** Build a parser for {@link org.orekit.files.ccsds.ndm.odm.opm.Opm Orbit Parameters Messages}.
414      * @return a new parser
415      */
416     public OpmParser buildOpmParser() {
417         return new OpmParser(getConventions(), isSimpleEOP(), getDataContext(), getMissionReferenceDate(),
418                              getMu(), getDefaultMass(), getParsedUnitsBehavior(), getFilters(),
419                              getFrameMapper());
420     }
421 
422     /** Build a parser for {@link org.orekit.files.ccsds.ndm.odm.omm.Omm Orbit Mean elements Messages}.
423      * @return a new parser
424      */
425     public OmmParser buildOmmParser() {
426         return new OmmParser(getConventions(), isSimpleEOP(), getDataContext(), getMissionReferenceDate(),
427                              getMu(), getDefaultMass(), getParsedUnitsBehavior(), getFilters(),
428                              getFrameMapper());
429     }
430 
431     /** Build a parser for {@link org.orekit.files.ccsds.ndm.odm.oem.Oem Orbit Ephemeris Messages}.
432      * @return a new parser
433      */
434     public OemParser buildOemParser() {
435         return new OemParser(getConventions(), isSimpleEOP(), getDataContext(), getMissionReferenceDate(),
436                              getMu(), getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters(),
437                              getFrameMapper());
438     }
439 
440     /** Build a parser for {@link org.orekit.files.ccsds.ndm.odm.ocm.Ocm Orbit Comprehensive Messages}.
441      * @return a new parser
442      */
443     public OcmParser buildOcmParser() {
444         return new OcmParser(getConventions(), getEquatorialRadius(), getFlattening(),
445                              isSimpleEOP(), getDataContext(), getMu(),
446                              getParsedUnitsBehavior(), getFilters(),
447                              getFrameMapper());
448     }
449 
450     /** Build a parser for {@link org.orekit.files.ccsds.ndm.adm.apm.Apm Attitude Parameters Messages}.
451      * @return a new parser
452      */
453     public ApmParser buildApmParser() {
454         return new ApmParser(getConventions(), isSimpleEOP(), getDataContext(),
455                              getMissionReferenceDate(), getParsedUnitsBehavior(), getFilters(),
456                              getFrameMapper());
457     }
458 
459     /** Build a parser for {@link org.orekit.files.ccsds.ndm.adm.aem.Aem Attitude Ephemeris Messages}.
460      * @return a new parser
461      */
462     public AemParser buildAemParser() {
463         return new AemParser(getConventions(), isSimpleEOP(), getDataContext(), getMissionReferenceDate(),
464                              getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters(),
465                              getFrameMapper());
466     }
467 
468     /** Build a parser for {@link org.orekit.files.ccsds.ndm.adm.acm.Acm Attitude Comprehensive Messages}.
469      * @return a new parser
470      * @since 12.0
471      */
472     public AcmParser buildAcmParser() {
473         return new AcmParser(getConventions(), isSimpleEOP(), getDataContext(),
474                              getParsedUnitsBehavior(), getFilters(),
475                              getFrameMapper());
476     }
477 
478     /** Build a parser for {@link org.orekit.files.ccsds.ndm.tdm.Tdm Tracking Data Messages}.
479      * @return a new parser
480      */
481     public TdmParser buildTdmParser() {
482         return new TdmParser(getConventions(), isSimpleEOP(), getDataContext(),
483                              getParsedUnitsBehavior(), getRangeUnitsConverter(), getFilters(),
484                              getFrameMapper());
485     }
486 
487     /** Build a parser for {@link org.orekit.files.ccsds.ndm.cdm.Cdm Conjunction Data Messages}.
488      * @return a new parser
489      */
490     public CdmParser buildCdmParser() {
491         return new CdmParser(getConventions(), isSimpleEOP(), getDataContext(),
492                              getParsedUnitsBehavior(), getFilters(),
493                              getFrameMapper());
494     }
495 
496 }