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.ilrs;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.Reader;
22  import java.util.Arrays;
23  import java.util.Collections;
24  import java.util.regex.Pattern;
25  
26  import org.hipparchus.exception.LocalizedCoreFormats;
27  import org.hipparchus.geometry.euclidean.threed.Vector3D;
28  import org.orekit.annotation.DefaultDataContext;
29  import org.orekit.data.DataContext;
30  import org.orekit.data.DataSource;
31  import org.orekit.errors.OrekitException;
32  import org.orekit.errors.OrekitMessages;
33  import org.orekit.files.general.EphemerisFileParser;
34  import org.orekit.frames.Frame;
35  import org.orekit.frames.Frames;
36  import org.orekit.time.AbsoluteDate;
37  import org.orekit.time.DateComponents;
38  import org.orekit.time.TimeScale;
39  import org.orekit.utils.CartesianDerivativesFilter;
40  import org.orekit.utils.Constants;
41  import org.orekit.utils.IERSConventions;
42  
43  /**
44   * A parser for the CPF orbit file format.
45   * <p>
46   * It supports both 1.0 and 2.0 versions
47   * <p>
48   * <b>Note:</b> Only required header keys are read. Furthermore, only position data are read.
49   * Other keys are simply ignored
50   * Contributions are welcome to support more fields in the format.
51   * </p>
52   * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2006/cpf_1.01.pdf">1.0 file format</a>
53   * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2018/cpf_2.00h-1.pdf">2.0 file format</a>
54   * @author Bryan Cazabonne
55   * @since 10.3
56   */
57  public class CPFParser implements EphemerisFileParser<CPF> {
58  
59      /** Default number of sample for interpolating data (See: reference documents). */
60      public static final int DEFAULT_INTERPOLATION_SAMPLE = 10;
61  
62      /** File format. */
63      private static final String FILE_FORMAT = "CPF";
64  
65      /** Miscroseconds to seconds converter. */
66      private static final double MS_TO_S = 1.0e-6;
67  
68      /** Pattern for delimiting regular expressions. */
69      private static final Pattern SEPARATOR = Pattern.compile("\\s+");
70  
71      /** Standard gravitational parameter in m^3 / s^2. */
72      private final double mu;
73  
74      /** Time scale used to define epochs in CPF file. */
75      private final TimeScale timeScale;
76  
77      /** Set of frames. */
78      private final Frames frames;
79  
80      /** Interpolation sample for data interpolating. */
81      private final int interpolationSample;
82  
83      /** IERS convention for frames. */
84      private final IERSConventions iersConvention;
85  
86      /**
87       * Default constructor.
88       * <p>
89       * This constructor uses the {@link DataContext#getDefault() default data context}.
90       */
91      @DefaultDataContext
92      public CPFParser() {
93          this(Constants.EIGEN5C_EARTH_MU, DEFAULT_INTERPOLATION_SAMPLE,
94               IERSConventions.IERS_2010, DataContext.getDefault().getTimeScales().getUTC(),
95               DataContext.getDefault().getFrames());
96      }
97  
98      /**
99       * Constructor.
100      * @param mu standard gravitational parameter to use for
101      *           creating {@link org.orekit.orbits.Orbit Orbits} from
102      *           the ephemeris data.
103      * @param interpolationSamples number of samples to use when interpolating
104      * @param iersConventions IERS convention for frames definition
105      * @param utc time scale used to define epochs in CPF files (UTC)
106      * @param frames set of frames for satellite coordinates
107      */
108     public CPFParser(final double mu,
109                      final int interpolationSamples,
110                      final IERSConventions iersConventions,
111                      final TimeScale utc,
112                      final Frames frames) {
113         this.mu                  = mu;
114         this.interpolationSample = interpolationSamples;
115         this.iersConvention      = iersConventions;
116         this.timeScale           = utc;
117         this.frames              = frames;
118     }
119 
120     /** {@inheritDoc} */
121     @Override
122     public CPF parse(final DataSource source) {
123 
124         try (Reader reader = source.getOpener().openReaderOnce();
125              BufferedReader br = (reader == null) ? null : new BufferedReader(reader)) {
126 
127             if (br == null) {
128                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
129             }
130 
131             // initialize internal data structures
132             final ParseInfo pi = new ParseInfo();
133 
134             int lineNumber = 0;
135             Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.H1);
136             nextLine:
137                 for (String line = br.readLine(); line != null; line = br.readLine()) {
138                     ++lineNumber;
139                     for (final LineParser candidate : candidateParsers) {
140                         if (candidate.canHandle(line)) {
141                             try {
142 
143                                 candidate.parse(line, pi);
144 
145                                 if (pi.done) {
146                                     pi.file.setFilter(pi.hasVelocityEntries ?
147                                                       CartesianDerivativesFilter.USE_PV :
148                                                       CartesianDerivativesFilter.USE_P);
149                                     // Return file
150                                     return pi.file;
151                                 }
152 
153                                 candidateParsers = candidate.allowedNext();
154                                 continue nextLine;
155 
156                             } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
157                                 throw new OrekitException(e,
158                                                           OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
159                                                           lineNumber, source.getName(), line);
160                             }
161                         }
162                     }
163 
164                 }
165 
166             // We never reached the EOF marker
167             throw new OrekitException(OrekitMessages.CPF_UNEXPECTED_END_OF_FILE, lineNumber);
168 
169         } catch (IOException ioe) {
170             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
171         }
172 
173     }
174 
175     /** Transient data used for parsing a CPF file. The data is kept in a
176      * separate data structure to make the parser thread-safe.
177      * <p><b>Note</b>: The class intentionally does not provide accessor
178      * methods, as it is only used internally for parsing a CPF file.</p>
179      */
180     private class ParseInfo {
181 
182         /** The corresponding CPF file. */
183         private CPF file;
184 
185         /** IERS convention. */
186         private IERSConventions convention;
187 
188         /** Set of frames. */
189         private Frames frames;
190 
191         /** Frame for the ephemeris data. */
192         private Frame frame;
193 
194         /** Time scale. */
195         private TimeScale timeScale;
196 
197         /** Indicates if the SP3 file has velocity entries. */
198         private boolean hasVelocityEntries;
199 
200         /** End Of File reached indicator. */
201         private boolean done;
202 
203         /**
204          * Constructor.
205          */
206         protected ParseInfo() {
207 
208             // Initialise file
209             file = new CPF();
210 
211             // Time scale
212             this.timeScale = CPFParser.this.timeScale;
213 
214             // Initialise fields
215             file.setMu(mu);
216             file.setInterpolationSample(interpolationSample);
217             file.setTimeScale(timeScale);
218 
219             // Default values
220             this.done               = false;
221             this.hasVelocityEntries = false;
222 
223             // Default value for reference frame
224             this.convention = CPFParser.this.iersConvention;
225             this.frames     = CPFParser.this.frames;
226             frame           = frames.getITRF(convention, false);
227 
228         }
229 
230     }
231 
232     /** Parsers for specific lines. */
233     private enum LineParser {
234 
235         /** Header first line. */
236         H1("H1") {
237 
238             /** {@inheritDoc} */
239             @Override
240             public void parse(final String line, final ParseInfo pi) {
241 
242                 // Data contained in the line
243                 final String[] values = SEPARATOR.split(line);
244 
245                 // Index for reading data.
246                 // Allow taking into consideration difference between 1.0 and 2.0 formats
247                 int index = 1;
248 
249                 // Format
250                 final String format = values[index++];
251 
252                 // Throw an exception if format is not equal to "CPF"
253                 if (!FILE_FORMAT.equals(format)) {
254                     throw new OrekitException(OrekitMessages.UNEXPECTED_FORMAT_FOR_ILRS_FILE, FILE_FORMAT, format);
255                 }
256 
257                 // Fill first elements
258                 pi.file.getHeader().setFormat(format);
259                 pi.file.getHeader().setVersion(Integer.parseInt(values[index++]));
260                 pi.file.getHeader().setSource(values[index++]);
261 
262                 // Epoch of ephemeris production
263                 final int year  = Integer.parseInt(values[index++]);
264                 final int month = Integer.parseInt(values[index++]);
265                 final int day   = Integer.parseInt(values[index++]);
266                 pi.file.getHeader().setProductionEpoch(new DateComponents(year, month, day));
267 
268                 // Hour of ephemeris production
269                 pi.file.getHeader().setProductionHour(Integer.parseInt(values[index++]));
270 
271                 // Ephemeris sequence number
272                 pi.file.getHeader().setSequenceNumber(Integer.parseInt(values[index++]));
273 
274                 // Difference between version 1.0 and 2.0: sub-daily ephemeris sequence number
275                 if (pi.file.getHeader().getVersion() == 2) {
276                     pi.file.getHeader().setSubDailySequenceNumber(Integer.parseInt(values[index++]));
277                 }
278 
279                 // Target Name
280                 pi.file.getHeader().setName(values[index]);
281 
282             }
283 
284             /** {@inheritDoc} */
285             @Override
286             public Iterable<LineParser> allowedNext() {
287                 return Arrays.asList(H2, ZERO);
288             }
289 
290         },
291 
292         /** Header second line. */
293         H2("H2") {
294 
295             /** {@inheritDoc} */
296             @Override
297             public void parse(final String line, final ParseInfo pi) {
298 
299                 // Data contained in the line
300                 final String[] values = SEPARATOR.split(line);
301 
302                 // Identifiers
303                 pi.file.getHeader().setIlrsSatelliteId(values[1]);
304                 pi.file.getHeader().setSic(values[2]);
305                 pi.file.getHeader().setNoradId(values[3]);
306 
307                 // Start epoch
308                 final int    yearS   = Integer.parseInt(values[4]);
309                 final int    monthS  = Integer.parseInt(values[5]);
310                 final int    dayS    = Integer.parseInt(values[6]);
311                 final int    hourS   = Integer.parseInt(values[7]);
312                 final int    minuteS = Integer.parseInt(values[8]);
313                 final double secondS = Integer.parseInt(values[9]);
314 
315                 pi.file.getHeader().setStartEpoch(new AbsoluteDate(yearS, monthS, dayS,
316                                                                    hourS, minuteS, secondS,
317                                                                    pi.file.getTimeScale()));
318 
319                 // End epoch
320                 final int    yearE   = Integer.parseInt(values[10]);
321                 final int    monthE  = Integer.parseInt(values[11]);
322                 final int    dayE    = Integer.parseInt(values[12]);
323                 final int    hourE   = Integer.parseInt(values[13]);
324                 final int    minuteE = Integer.parseInt(values[14]);
325                 final double secondE = Integer.parseInt(values[15]);
326 
327                 pi.file.getHeader().setEndEpoch(new AbsoluteDate(yearE, monthE, dayE,
328                                                                  hourE, minuteE, secondE,
329                                                                  pi.file.getTimeScale()));
330 
331                 // Time between table entries
332                 pi.file.getHeader().setStep(Integer.parseInt(values[16]));
333 
334                 // Compatibility with TIVs
335                 pi.file.getHeader().setIsCompatibleWithTIVs(Integer.parseInt(values[17]) == 1);
336 
337                 // Target class
338                 pi.file.getHeader().setTargetClass(Integer.parseInt(values[18]));
339 
340                 // Reference frame
341                 final int frameId = Integer.parseInt(values[19]);
342                 switch (frameId) {
343                     case 0:
344                         pi.frame = pi.frames.getITRF(pi.convention, false);
345                         break;
346                     case 1:
347                         pi.frame = pi.frames.getTOD(true);
348                         break;
349                     case 2:
350                         pi.frame = pi.frames.getMOD(pi.convention);
351                         break;
352                     default:
353                         pi.frame = pi.frames.getITRF(pi.convention, false);
354                         break;
355                 }
356                 pi.file.getHeader().setRefFrame(pi.frame);
357                 pi.file.getHeader().setRefFrameId(frameId);
358 
359                 // Last fields
360                 pi.file.getHeader().setRotationalAngleType(Integer.parseInt(values[20]));
361                 pi.file.getHeader().setIsCenterOfMassCorrectionApplied(Integer.parseInt(values[21]) == 1);
362                 if (pi.file.getHeader().getVersion() == 2) {
363                     pi.file.getHeader().setTargetLocation(Integer.parseInt(values[22]));
364                 }
365 
366             }
367 
368             /** {@inheritDoc} */
369             @Override
370             public Iterable<LineParser> allowedNext() {
371                 return Arrays.asList(H3, H4, H5, H9, ZERO);
372             }
373 
374         },
375 
376         /** Header third line. */
377         H3("H3") {
378 
379             /** {@inheritDoc} */
380             @Override
381             public void parse(final String line, final ParseInfo pi) {
382                 // Not implemented yet
383             }
384 
385             /** {@inheritDoc} */
386             @Override
387             public Iterable<LineParser> allowedNext() {
388                 return Arrays.asList(H4, H5, H9, ZERO);
389             }
390 
391         },
392 
393         /** Header fourth line. */
394         H4("H4") {
395 
396             /** {@inheritDoc} */
397             @Override
398             public void parse(final String line, final ParseInfo pi) {
399 
400                 // Data contained in the line
401                 final String[] values = SEPARATOR.split(line);
402 
403                 // Pulse Repetition Frequency (PRF)
404                 pi.file.getHeader().setPrf(Double.parseDouble(values[1]));
405 
406                 // Transponder information
407                 pi.file.getHeader().setTranspTransmitDelay(Double.parseDouble(values[2]) * MS_TO_S);
408                 pi.file.getHeader().setTranspUtcOffset(Double.parseDouble(values[3]) * MS_TO_S);
409                 pi.file.getHeader().setTranspOscDrift(Double.parseDouble(values[4]));
410                 if (pi.file.getHeader().getVersion() == 2) {
411                     pi.file.getHeader().setTranspClkRef(Double.parseDouble(values[5]));
412                 }
413 
414             }
415 
416             /** {@inheritDoc} */
417             @Override
418             public Iterable<LineParser> allowedNext() {
419                 return Arrays.asList(H5, H9, ZERO);
420             }
421 
422         },
423 
424         /** Header fifth line. */
425         H5("H5") {
426 
427             /** {@inheritDoc} */
428             @Override
429             public void parse(final String line, final ParseInfo pi) {
430 
431                 // Approximate center of mass to reflector offset in meters
432                 final double offset = Double.parseDouble(SEPARATOR.split(line)[1]);
433                 pi.file.getHeader().setCenterOfMassOffset(offset);
434 
435             }
436 
437             /** {@inheritDoc} */
438             @Override
439             public Iterable<LineParser> allowedNext() {
440                 return Arrays.asList(H9, ZERO);
441             }
442 
443         },
444 
445         /** Header last line. */
446         H9("H9") {
447 
448             /** {@inheritDoc} */
449             @Override
450             public void parse(final String line, final ParseInfo pi) {
451                 // End of header. Nothing to do
452             }
453 
454             /** {@inheritDoc} */
455             @Override
456             public Iterable<LineParser> allowedNext() {
457                 return Arrays.asList(TEN, ZERO);
458             }
459 
460         },
461 
462         /** Position values. */
463         TEN("10") {
464 
465             /** {@inheritDoc} */
466             @Override
467             public void parse(final String line, final ParseInfo pi) {
468 
469                 // Data contained in the line
470                 final String[] values = SEPARATOR.split(line);
471 
472                 // Epoch
473                 final int mjd           = Integer.parseInt(values[2]);
474                 final double secInDay   = Double.parseDouble(values[3]);
475                 final AbsoluteDate date = AbsoluteDate.createMJDDate(mjd, secInDay, pi.timeScale);
476 
477                 // Leap second flag
478                 final int leap = Integer.parseInt(values[4]);
479 
480                 // Coordinates
481                 final double x = Double.parseDouble(values[5]);
482                 final double y = Double.parseDouble(values[6]);
483                 final double z = Double.parseDouble(values[7]);
484                 final Vector3D position = new Vector3D(x, y, z);
485 
486                 // CPF coordinate
487                 final CPF.CPFCoordinate coordinate = new CPF.CPFCoordinate(date, position, leap);
488                 pi.file.addSatelliteCoordinate(pi.file.getHeader().getIlrsSatelliteId(), coordinate);
489 
490             }
491 
492             /** {@inheritDoc} */
493             @Override
494             public Iterable<LineParser> allowedNext() {
495                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
496             }
497 
498         },
499 
500         /** Velocity values. */
501         TWENTY("20") {
502 
503             /** {@inheritDoc} */
504             @Override
505             public void parse(final String line, final ParseInfo pi) {
506 
507                 // Data contained in the line
508                 final String[] values = SEPARATOR.split(line);
509 
510                 // Coordinates
511                 final double x = Double.parseDouble(values[2]);
512                 final double y = Double.parseDouble(values[3]);
513                 final double z = Double.parseDouble(values[4]);
514                 final Vector3D velocity = new Vector3D(x, y, z);
515 
516                 // CPF coordinate
517                 pi.file.addSatelliteVelocityToCPFCoordinate(pi.file.getHeader().getIlrsSatelliteId(), velocity);
518             }
519 
520             /** {@inheritDoc} */
521             @Override
522             public Iterable<LineParser> allowedNext() {
523                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
524             }
525 
526         },
527 
528         /** Corrections. */
529         THIRTY("30") {
530 
531             /** {@inheritDoc} */
532             @Override
533             public void parse(final String line, final ParseInfo pi) {
534                 // Not implemented yet
535             }
536 
537             /** {@inheritDoc} */
538             @Override
539             public Iterable<LineParser> allowedNext() {
540                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
541             }
542 
543         },
544 
545         /** Transponder specific. */
546         FORTY("40") {
547 
548             /** {@inheritDoc} */
549             @Override
550             public void parse(final String line, final ParseInfo pi) {
551                 // Not implemented yet
552             }
553 
554             /** {@inheritDoc} */
555             @Override
556             public Iterable<LineParser> allowedNext() {
557                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
558             }
559 
560         },
561 
562         /** Offset from center of main body. */
563         FIFTY("50") {
564 
565             /** {@inheritDoc} */
566             @Override
567             public void parse(final String line, final ParseInfo pi) {
568                 // Not implemented yet
569             }
570 
571             /** {@inheritDoc} */
572             @Override
573             public Iterable<LineParser> allowedNext() {
574                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
575             }
576 
577         },
578 
579         /** Rotation angle of offset. */
580         SIXTY("60") {
581 
582             /** {@inheritDoc} */
583             @Override
584             public void parse(final String line, final ParseInfo pi) {
585                 // Not implemented yet
586             }
587 
588             /** {@inheritDoc} */
589             @Override
590             public Iterable<LineParser> allowedNext() {
591                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
592             }
593 
594         },
595 
596         /** Earth orientation. */
597         SEVENTY("70") {
598 
599             /** {@inheritDoc} */
600             @Override
601             public void parse(final String line, final ParseInfo pi) {
602                 // Not implemented yet
603             }
604 
605             /** {@inheritDoc} */
606             @Override
607             public Iterable<LineParser> allowedNext() {
608                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
609             }
610 
611         },
612 
613         /** Comments. */
614         ZERO("00") {
615 
616             /** {@inheritDoc} */
617             @Override
618             public void parse(final String line, final ParseInfo pi) {
619 
620                 // Comment
621                 final String comment = line.split(getIdentifier())[1].trim();
622                 pi.file.getComments().add(comment);
623 
624             }
625 
626             /** {@inheritDoc} */
627             @Override
628             public Iterable<LineParser> allowedNext() {
629                 return Arrays.asList(H1, H2, H3, H4, H5, H9,
630                                      TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
631             }
632 
633         },
634 
635         /** Last record in ephemeris. */
636         EOF("99") {
637 
638             @Override
639             public void parse(final String line, final ParseInfo pi) {
640                 pi.done = true;
641             }
642 
643             /** {@inheritDoc} */
644             @Override
645             public Iterable<LineParser> allowedNext() {
646                 return Collections.singleton(EOF);
647             }
648 
649         };
650 
651         /** Pattern for identifying line. */
652         private final Pattern pattern;
653 
654         /** Identifier. */
655         private final String identifier;
656 
657         /** Simple constructor.
658          * @param identifier regular expression for identifying line (i.e. first element)
659          */
660         LineParser(final String identifier) {
661             this.identifier = identifier;
662             pattern = Pattern.compile(identifier);
663         }
664 
665         /**
666          * Get the regular expression for identifying line.
667          * @return the regular expression for identifying line
668          */
669         public String getIdentifier() {
670             return identifier;
671         }
672 
673         /** Parse a line.
674          * @param line line to parse
675          * @param pi holder for transient data
676          */
677         public abstract void parse(String line, ParseInfo pi);
678 
679         /** Get the allowed parsers for next line.
680          * @return allowed parsers for next line
681          */
682         public abstract Iterable<LineParser> allowedNext();
683 
684         /** Check if parser can handle line.
685          * @param line line to parse
686          * @return true if parser can handle the specified line
687          */
688         public boolean canHandle(final String line) {
689             return pattern.matcher(SEPARATOR.split(line)[0]).matches();
690         }
691 
692     }
693 
694 }