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.propagation.analytical.tle;
18  
19  import java.text.DecimalFormat;
20  import java.text.DecimalFormatSymbols;
21  import java.util.Locale;
22  import java.util.Objects;
23  import java.util.regex.Pattern;
24  
25  import org.hipparchus.util.ArithmeticUtils;
26  import org.hipparchus.util.FastMath;
27  import org.hipparchus.util.MathUtils;
28  import org.orekit.annotation.DefaultDataContext;
29  import org.orekit.data.DataContext;
30  import org.orekit.errors.OrekitException;
31  import org.orekit.errors.OrekitMessages;
32  import org.orekit.orbits.OrbitalState;
33  import org.orekit.propagation.SpacecraftState;
34  import org.orekit.propagation.analytical.tle.generation.TleGenerationAlgorithm;
35  import org.orekit.time.AbsoluteDate;
36  import org.orekit.time.DateComponents;
37  import org.orekit.time.DateTimeComponents;
38  import org.orekit.time.TimeComponents;
39  import org.orekit.time.TimeOffset;
40  import org.orekit.time.TimeScale;
41  import org.orekit.utils.Constants;
42  
43  /** This class is a container for a single set of TLE data.
44   *
45   * <p>TLE sets can be built either by providing directly the two lines, in
46   * which case parsing is performed internally or by providing the already
47   * parsed elements.</p>
48   * <p>TLE are not transparently convertible to {@link org.orekit.orbits.Orbit Orbit}
49   * instances. They are significant only with respect to their dedicated {@link
50   * TLEPropagator propagator}, which also computes position and velocity coordinates.
51   * Any attempt to directly use orbital parameters like {@link #getE() eccentricity},
52   * {@link #getI() inclination}, etc. without any reference to the {@link TLEPropagator
53   * TLE propagator} is prone to errors.</p>
54   * <p>More information on the TLE format can be found on the
55   * <a href="https://www.celestrak.com/">CelesTrak website.</a></p>
56   * @author Fabien Maussion
57   * @author Luc Maisonobe
58   */
59  public class TLE implements OrbitalState {
60  
61      /** Identifier for SGP type of ephemeris. */
62      public static final int SGP = 1;
63  
64      /** Identifier for SGP4 type of ephemeris. */
65      public static final int SGP4 = 2;
66  
67      /** Identifier for SDP4 type of ephemeris. */
68      public static final int SDP4 = 3;
69  
70      /** Identifier for SGP8 type of ephemeris. */
71      public static final int SGP8 = 4;
72  
73      /** Identifier for SDP8 type of ephemeris. */
74      public static final int SDP8 = 5;
75  
76      /** Identifier for default type of ephemeris (SGP4/SDP4). */
77      public static final int DEFAULT = 0;
78  
79      /** Name of the mean motion parameter. */
80      private static final String MEAN_MOTION = "meanMotion";
81  
82      /** Name of the inclination parameter. */
83      private static final String INCLINATION = "inclination";
84  
85      /** Name of the eccentricity parameter. */
86      private static final String ECCENTRICITY = "eccentricity";
87  
88      /** Pattern for line 1. */
89      private static final Pattern LINE_1_PATTERN =
90          Pattern.compile("1 [ 0-9A-Z&&[^IO]][ 0-9]{4}[A-Z] [ 0-9]{5}[ A-Z]{3} [ 0-9]{5}[.][ 0-9]{8} " +
91                          "(?:[ 0+-][.][ 0-9]{8}| [ +-][.][ 0-9]{7}) [ +-][ 0-9]{5}[+-][ 0-9] " +
92                          "[ +-][ 0-9]{5}[+-][ 0-9] [ 0-9] [ 0-9]{4}[ 0-9]");
93  
94      /** Pattern for line 2. */
95      private static final Pattern LINE_2_PATTERN =
96          Pattern.compile("2 [ 0-9A-Z&&[^IO]][ 0-9]{4} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{7} " +
97                          "[ 0-9]{3}[.][ 0-9]{4} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{2}[.][ 0-9]{13}[ 0-9]");
98  
99      /** International symbols for parsing. */
100     private static final DecimalFormatSymbols SYMBOLS =
101         new DecimalFormatSymbols(Locale.US);
102 
103     /** The satellite number. */
104     private final int satelliteNumber;
105 
106     /** Classification (U for unclassified). */
107     private final char classification;
108 
109     /** Launch year. */
110     private final int launchYear;
111 
112     /** Launch number. */
113     private final int launchNumber;
114 
115     /** Piece of launch (from "A" to "ZZZ"). */
116     private final String launchPiece;
117 
118     /** Type of ephemeris. */
119     private final int ephemerisType;
120 
121     /** Element number. */
122     private final int elementNumber;
123 
124     /** the TLE current date. */
125     private final AbsoluteDate epoch;
126 
127     /** Mean motion (rad/s). */
128     private final double meanMotion;
129 
130     /** Mean motion first derivative (rad/s²). */
131     private final double meanMotionFirstDerivative;
132 
133     /** Mean motion second derivative (rad/s³). */
134     private final double meanMotionSecondDerivative;
135 
136     /** Eccentricity. */
137     private final double eccentricity;
138 
139     /** Inclination (rad). */
140     private final double inclination;
141 
142     /** Argument of periapsis (rad). */
143     private final double pa;
144 
145     /** Right Ascension of the Ascending node (rad). */
146     private final double raan;
147 
148     /** Mean anomaly (rad). */
149     private final double meanAnomaly;
150 
151     /** Revolution number at epoch. */
152     private final int revolutionNumberAtEpoch;
153 
154     /** First line. */
155     private String line1;
156 
157     /** Second line. */
158     private String line2;
159 
160     /** The UTC scale. */
161     private final TimeScale utc;
162 
163     /** Driver for ballistic coefficient parameter. */
164     private final double bStar;
165 
166 
167     /** Simple constructor from unparsed two lines. This constructor uses the {@link
168      * DataContext#getDefault() default data context}.
169      *
170      * <p>The static method {@link #isFormatOK(String, String)} should be called
171      * before trying to build this object.</p>
172      * @param line1 the first element (69 char String)
173      * @param line2 the second element (69 char String)
174      * @see #TLE(String, String, TimeScale)
175      */
176     @DefaultDataContext
177     public TLE(final String line1, final String line2) {
178         this(line1, line2, DataContext.getDefault().getTimeScales().getUTC());
179     }
180 
181     /** Simple constructor from unparsed two lines using the given time scale as UTC.
182      *
183      * <p>The static method {@link #isFormatOK(String, String)} should be called
184      * before trying to build this object.</p>
185      * @param line1 the first element (69 char String)
186      * @param line2 the second element (69 char String)
187      * @param utc the UTC time scale.
188      * @since 10.1
189      */
190     public TLE(final String line1, final String line2, final TimeScale utc) {
191 
192         // identification
193         satelliteNumber = ParseUtils.parseSatelliteNumber(line1, 2, 5);
194         final int satNum2 = ParseUtils.parseSatelliteNumber(line2, 2, 5);
195         if (satelliteNumber != satNum2) {
196             throw new OrekitException(OrekitMessages.TLE_LINES_DO_NOT_REFER_TO_SAME_OBJECT,
197                                       line1, line2);
198         }
199         classification  = line1.charAt(7);
200         launchYear      = ParseUtils.parseYear(line1, 9);
201         launchNumber    = ParseUtils.parseInteger(line1, 11, 3);
202         launchPiece     = line1.substring(14, 17).trim();
203         ephemerisType   = ParseUtils.parseInteger(line1, 62, 1);
204         elementNumber   = ParseUtils.parseInteger(line1, 64, 4);
205 
206         final int    year        = ParseUtils.parseYear(line1, 18);
207         final int    dayInYear   = ParseUtils.parseInteger(line1, 20, 3);
208         final int dayFractionDigits = ParseUtils.parseInteger(line1, 24, 8);
209         final long nanoSecondsCount = dayFractionDigits * (long) Constants.JULIAN_DAY * 10;
210         final TimeOffset dayFraction = new TimeOffset(nanoSecondsCount, TimeOffset.NANOSECOND);
211         epoch = new AbsoluteDate(new DateComponents(year, dayInYear), new TimeComponents(dayFraction), utc);
212 
213         // mean motion development
214         // converted from rev/day, 2 * rev/day^2 and 6 * rev/day^3 to rad/s, rad/s^2 and rad/s^3
215         meanMotion                 = ParseUtils.parseDouble(line2, 52, 11) * FastMath.PI / 43200.0;
216         meanMotionFirstDerivative  = ParseUtils.parseDouble(line1, 33, 10) * FastMath.PI / 1.86624e9;
217         meanMotionSecondDerivative = Double.parseDouble((line1.substring(44, 45) + '.' +
218                                                          line1.substring(45, 50) + 'e' +
219                                                          line1.substring(50, 52)).replace(' ', '0')) *
220                                      FastMath.PI / 5.3747712e13;
221 
222         eccentricity = Double.parseDouble("." + line2.substring(26, 33).replace(' ', '0'));
223         inclination  = FastMath.toRadians(ParseUtils.parseDouble(line2, 8, 8));
224         pa           = FastMath.toRadians(ParseUtils.parseDouble(line2, 34, 8));
225         raan         = FastMath.toRadians(Double.parseDouble(line2.substring(17, 25).replace(' ', '0')));
226         meanAnomaly  = FastMath.toRadians(ParseUtils.parseDouble(line2, 43, 8));
227 
228         revolutionNumberAtEpoch = ParseUtils.parseInteger(line2, 63, 5);
229         bStar = Double.parseDouble((line1.substring(53, 54) + '.' +
230                                     line1.substring(54, 59) + 'e' +
231                                     line1.substring(59, 61)).replace(' ', '0'));
232 
233         // save the lines
234         this.line1 = line1;
235         this.line2 = line2;
236         this.utc = utc;
237 
238     }
239 
240     /**
241      * <p>
242      * Simple constructor from already parsed elements. This constructor uses the
243      * {@link DataContext#getDefault() default data context}.
244      * </p>
245      *
246      * <p>
247      * The mean anomaly, the right ascension of ascending node Ω and the argument of
248      * periapsis ω are normalized into the [0, 2π] interval as they can be negative.
249      * After that, a range check is performed on some of the orbital elements:
250      *
251      * <pre>
252      *     meanMotion &gt;= 0
253      *     0 &lt;= i &lt;= π
254      *     0 &lt;= Ω &lt;= 2π
255      *     0 &lt;= e &lt;= 1
256      *     0 &lt;= ω &lt;= 2π
257      *     0 &lt;= meanAnomaly &lt;= 2π
258      * </pre>
259      *
260      * @param satelliteNumber satellite number
261      * @param classification classification (U for unclassified)
262      * @param launchYear launch year (all digits)
263      * @param launchNumber launch number
264      * @param launchPiece launch piece (3 char String)
265      * @param ephemerisType type of ephemeris
266      * @param elementNumber element number
267      * @param epoch elements epoch
268      * @param meanMotion mean motion (rad/s)
269      * @param meanMotionFirstDerivative mean motion first derivative (rad/s²)
270      * @param meanMotionSecondDerivative mean motion second derivative (rad/s³)
271      * @param e eccentricity
272      * @param i inclination (rad)
273      * @param pa argument of periapsis (rad)
274      * @param raan right ascension of ascending node (rad)
275      * @param meanAnomaly mean anomaly (rad)
276      * @param revolutionNumberAtEpoch revolution number at epoch
277      * @param bStar ballistic coefficient
278      * @see #TLE(int, char, int, int, String, int, int, AbsoluteDate, double, double,
279      * double, double, double, double, double, double, int, double, TimeScale)
280      */
281     @DefaultDataContext
282     public TLE(final int satelliteNumber, final char classification,
283                final int launchYear, final int launchNumber, final String launchPiece,
284                final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
285                final double meanMotion, final double meanMotionFirstDerivative,
286                final double meanMotionSecondDerivative, final double e, final double i,
287                final double pa, final double raan, final double meanAnomaly,
288                final int revolutionNumberAtEpoch, final double bStar) {
289         this(satelliteNumber, classification, launchYear, launchNumber, launchPiece,
290                 ephemerisType, elementNumber, epoch, meanMotion,
291                 meanMotionFirstDerivative, meanMotionSecondDerivative, e, i, pa, raan,
292                 meanAnomaly, revolutionNumberAtEpoch, bStar,
293                 DataContext.getDefault().getTimeScales().getUTC());
294     }
295 
296     /**
297      * <p>
298      * Simple constructor from already parsed elements using the given time scale as
299      * UTC.
300      * </p>
301      *
302      * <p>
303      * The mean anomaly, the right ascension of ascending node Ω and the argument of
304      * periapsis ω are normalized into the [0, 2π] interval as they can be negative.
305      * After that, a range check is performed on some of the orbital elements:
306      *
307      * <pre>
308      *     meanMotion &gt;= 0
309      *     0 &lt;= i &lt;= π
310      *     0 &lt;= Ω &lt;= 2π
311      *     0 &lt;= e &lt;= 1
312      *     0 &lt;= ω &lt;= 2π
313      *     0 &lt;= meanAnomaly &lt;= 2π
314      * </pre>
315      *
316      * @param satelliteNumber satellite number
317      * @param classification classification (U for unclassified)
318      * @param launchYear launch year (all digits)
319      * @param launchNumber launch number
320      * @param launchPiece launch piece (3 char String)
321      * @param ephemerisType type of ephemeris
322      * @param elementNumber element number
323      * @param epoch elements epoch
324      * @param meanMotion mean motion (rad/s)
325      * @param meanMotionFirstDerivative mean motion first derivative (rad/s²)
326      * @param meanMotionSecondDerivative mean motion second derivative (rad/s³)
327      * @param e eccentricity
328      * @param i inclination (rad)
329      * @param pa argument of periapsis (rad)
330      * @param raan right ascension of ascending node (rad)
331      * @param meanAnomaly mean anomaly (rad)
332      * @param revolutionNumberAtEpoch revolution number at epoch
333      * @param bStar ballistic coefficient
334      * @param utc the UTC time scale.
335      * @since 10.1
336      */
337     public TLE(final int satelliteNumber, final char classification,
338                final int launchYear, final int launchNumber, final String launchPiece,
339                final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
340                final double meanMotion, final double meanMotionFirstDerivative,
341                final double meanMotionSecondDerivative, final double e, final double i,
342                final double pa, final double raan, final double meanAnomaly,
343                final int revolutionNumberAtEpoch, final double bStar,
344                final TimeScale utc) {
345 
346         // identification
347         this.satelliteNumber = satelliteNumber;
348         this.classification  = classification;
349         this.launchYear      = launchYear;
350         this.launchNumber    = launchNumber;
351         this.launchPiece     = launchPiece;
352         this.ephemerisType   = ephemerisType;
353         this.elementNumber   = elementNumber;
354 
355         // orbital parameters
356         this.epoch = epoch;
357         // Checking mean motion range
358         this.meanMotion = meanMotion;
359         this.meanMotionFirstDerivative = meanMotionFirstDerivative;
360         this.meanMotionSecondDerivative = meanMotionSecondDerivative;
361 
362         // Checking inclination range
363         this.inclination = i;
364 
365         // Normalizing RAAN in [0,2pi] interval
366         this.raan = MathUtils.normalizeAngle(raan, FastMath.PI);
367 
368         // Checking eccentricity range
369         this.eccentricity = e;
370 
371         // Normalizing PA in [0,2pi] interval
372         this.pa = MathUtils.normalizeAngle(pa, FastMath.PI);
373 
374         // Normalizing mean anomaly in [0,2pi] interval
375         this.meanAnomaly = MathUtils.normalizeAngle(meanAnomaly, FastMath.PI);
376 
377         this.revolutionNumberAtEpoch = revolutionNumberAtEpoch;
378         this.bStar                   = bStar;
379 
380 
381         // don't build the line until really needed
382         this.line1 = null;
383         this.line2 = null;
384         this.utc = utc;
385 
386     }
387 
388     /**
389      * Get the UTC time scale used to create this TLE.
390      *
391      * @return UTC time scale.
392      */
393     public TimeScale getUtc() {
394         return utc;
395     }
396 
397     /** Get the first line.
398      * @return first line
399      */
400     public String getLine1() {
401         if (line1 == null) {
402             buildLine1();
403         }
404         return line1;
405     }
406 
407     /** Get the second line.
408      * @return second line
409      */
410     public String getLine2() {
411         if (line2 == null) {
412             buildLine2();
413         }
414         return line2;
415     }
416 
417     /** Build the line 1 from the parsed elements.
418      */
419     private void buildLine1() {
420 
421         final StringBuilder buffer = new StringBuilder();
422 
423         buffer.append('1');
424 
425         buffer.append(' ');
426         buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-1"));
427         buffer.append(classification);
428 
429         buffer.append(' ');
430         buffer.append(ParseUtils.addPadding("launchYear",   launchYear % 100, '0', 2, true, satelliteNumber));
431         buffer.append(ParseUtils.addPadding("launchNumber", launchNumber, '0', 3, true, satelliteNumber));
432         buffer.append(ParseUtils.addPadding("launchPiece",  launchPiece, ' ', 3, false, satelliteNumber));
433 
434         buffer.append(' ');
435         DateTimeComponents dtc = epoch.getComponents(utc);
436         int fraction = (int) FastMath.rint(31250 * dtc.getTime().getSecondsInUTCDay() / 27.0);
437         if (fraction >= 100000000) {
438             dtc =  epoch.shiftedBy(Constants.JULIAN_DAY).getComponents(utc);
439             fraction -= 100000000;
440         }
441         buffer.append(ParseUtils.addPadding("year", dtc.getDate().getYear() % 100, '0', 2, true, satelliteNumber));
442         buffer.append(ParseUtils.addPadding("day",  dtc.getDate().getDayOfYear(),  '0', 3, true, satelliteNumber));
443         buffer.append('.');
444         // nota: 31250/27 == 100000000/86400
445 
446         buffer.append(ParseUtils.addPadding("fraction", fraction,  '0', 8, true, satelliteNumber));
447 
448         buffer.append(' ');
449         final double n1 = meanMotionFirstDerivative * 1.86624e9 / FastMath.PI;
450         final String sn1 = ParseUtils.addPadding("meanMotionFirstDerivative",
451                                                  new DecimalFormat(".00000000", SYMBOLS).format(n1),
452                                                  ' ', 10, true, satelliteNumber);
453         buffer.append(sn1);
454 
455         buffer.append(' ');
456         final double n2 = meanMotionSecondDerivative * 5.3747712e13 / FastMath.PI;
457         buffer.append(formatExponentMarkerFree("meanMotionSecondDerivative", n2, 5, ' ', 8, true));
458 
459         buffer.append(' ');
460         buffer.append(formatExponentMarkerFree("B*", getBStar(), 5, ' ', 8, true));
461 
462         buffer.append(' ');
463         buffer.append(ephemerisType);
464 
465         buffer.append(' ');
466         buffer.append(ParseUtils.addPadding("elementNumber", elementNumber, ' ', 4, true, satelliteNumber));
467 
468         buffer.append(checksum(buffer));
469 
470         line1 = buffer.toString();
471 
472     }
473 
474     /** Format a real number without 'e' exponent marker.
475      * @param name parameter name
476      * @param d number to format
477      * @param mantissaSize size of the mantissa (not counting initial '-' or ' ' for sign)
478      * @param c padding character
479      * @param size desired size
480      * @param rightJustified if true, the resulting string is
481      * right justified (i.e. space are added to the left)
482      * @return formatted and padded number
483      */
484     private String formatExponentMarkerFree(final String name, final double d, final int mantissaSize,
485                                             final char c, final int size, final boolean rightJustified) {
486         final double dAbs = FastMath.abs(d);
487         int exponent = (dAbs < 1.0e-9) ? -9 : (int) FastMath.ceil(FastMath.log10(dAbs));
488         long mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
489         if (mantissa == 0) {
490             exponent = 0;
491         } else if (mantissa > (ArithmeticUtils.pow(10, mantissaSize) - 1)) {
492             // rare case: if d has a single digit like d = 1.0e-4 with mantissaSize = 5
493             // the above computation finds exponent = -4 and mantissa = 100000 which
494             // doesn't fit in a 5 digits string
495             exponent++;
496             mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
497         }
498         final String sMantissa = ParseUtils.addPadding(name, (int) mantissa, '0', mantissaSize, true, satelliteNumber);
499         final String sExponent = Integer.toString(FastMath.abs(exponent));
500         final String formatted = (d <  0 ? '-' : ' ') + sMantissa + (exponent <= 0 ? '-' : '+') + sExponent;
501 
502         return ParseUtils.addPadding(name, formatted, c, size, rightJustified, satelliteNumber);
503 
504     }
505 
506     /** Build the line 2 from the parsed elements.
507      */
508     private void buildLine2() {
509 
510         final StringBuilder buffer = new StringBuilder();
511         final DecimalFormat f34   = new DecimalFormat("##0.0000", SYMBOLS);
512         final DecimalFormat f211  = new DecimalFormat("#0.00000000", SYMBOLS);
513 
514         buffer.append('2');
515 
516         buffer.append(' ');
517         buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-2"));
518 
519         buffer.append(' ');
520         buffer.append(ParseUtils.addPadding(INCLINATION, f34.format(FastMath.toDegrees(inclination)), ' ', 8, true, satelliteNumber));
521         buffer.append(' ');
522         buffer.append(ParseUtils.addPadding("raan", f34.format(FastMath.toDegrees(raan)), ' ', 8, true, satelliteNumber));
523         buffer.append(' ');
524         buffer.append(ParseUtils.addPadding(ECCENTRICITY, (int) FastMath.rint(eccentricity * 1.0e7), '0', 7, true, satelliteNumber));
525         buffer.append(' ');
526         buffer.append(ParseUtils.addPadding("pa", f34.format(FastMath.toDegrees(pa)), ' ', 8, true, satelliteNumber));
527         buffer.append(' ');
528         buffer.append(ParseUtils.addPadding("meanAnomaly", f34.format(FastMath.toDegrees(meanAnomaly)), ' ', 8, true, satelliteNumber));
529 
530         buffer.append(' ');
531         buffer.append(ParseUtils.addPadding(MEAN_MOTION, f211.format(meanMotion * 43200.0 / FastMath.PI), ' ', 11, true, satelliteNumber));
532         buffer.append(ParseUtils.addPadding("revolutionNumberAtEpoch", revolutionNumberAtEpoch, ' ', 5, true, satelliteNumber));
533 
534         buffer.append(checksum(buffer));
535 
536         line2 = buffer.toString();
537 
538     }
539 
540     /** Get the satellite id.
541      * @return the satellite number
542      */
543     public int getSatelliteNumber() {
544         return satelliteNumber;
545     }
546 
547     /** Get the classification.
548      * @return classification
549      */
550     public char getClassification() {
551         return classification;
552     }
553 
554     /** Get the launch year.
555      * @return the launch year
556      */
557     public int getLaunchYear() {
558         return launchYear;
559     }
560 
561     /** Get the launch number.
562      * @return the launch number
563      */
564     public int getLaunchNumber() {
565         return launchNumber;
566     }
567 
568     /** Get the launch piece.
569      * @return the launch piece
570      */
571     public String getLaunchPiece() {
572         return launchPiece;
573     }
574 
575     /** Get the type of ephemeris.
576      * @return the ephemeris type (one of {@link #DEFAULT}, {@link #SGP},
577      * {@link #SGP4}, {@link #SGP8}, {@link #SDP4}, {@link #SDP8})
578      */
579     public int getEphemerisType() {
580         return ephemerisType;
581     }
582 
583     /** Get the element number.
584      * @return the element number
585      */
586     public int getElementNumber() {
587         return elementNumber;
588     }
589 
590     /** Get the TLE current date.
591      * @return the epoch
592      */
593     public AbsoluteDate getDate() {
594         return epoch;
595     }
596 
597     /** Get the mean motion.
598      * @return the mean motion (rad/s)
599      */
600     public double getMeanMotion() {
601         return meanMotion;
602     }
603 
604     /** Get the mean motion first derivative.
605      * @return the mean motion first derivative (rad/s²)
606      */
607     public double getMeanMotionFirstDerivative() {
608         return meanMotionFirstDerivative;
609     }
610 
611     /** Get the mean motion second derivative.
612      * @return the mean motion second derivative (rad/s³)
613      */
614     public double getMeanMotionSecondDerivative() {
615         return meanMotionSecondDerivative;
616     }
617 
618     /** Get the eccentricity.
619      * @return the eccentricity
620      */
621     public double getE() {
622         return eccentricity;
623     }
624 
625     /** Get the inclination.
626      * @return the inclination (rad)
627      */
628     public double getI() {
629         return inclination;
630     }
631 
632     /** Get the argument of periapsis.
633      * @return omega (rad)
634      */
635     public double getPeriapsisArgument() {
636         return pa;
637     }
638 
639     /** Get Right Ascension of the Ascending node.
640      * @return the raan (rad)
641      */
642     public double getRaan() {
643         return raan;
644     }
645 
646     /** Get the mean anomaly.
647      * @return the mean anomaly (rad)
648      */
649     public double getMeanAnomaly() {
650         return meanAnomaly;
651     }
652 
653     /** Get the revolution number.
654      * @return the revolutionNumberAtEpoch
655      */
656     public int getRevolutionNumberAtEpoch() {
657         return revolutionNumberAtEpoch;
658     }
659 
660     /** Get the ballistic coefficient at tle date.
661      * @return bStar
662      */
663     public double getBStar() {
664         return bStar;
665     }
666 
667     /** Compute the semi-major axis from the mean motion of the TLE and the gravitational parameter from TLEConstants.
668      * @return the semi-major axis computed.
669      */
670     public double computeSemiMajorAxis() {
671         return FastMath.cbrt(TLEConstants.MU / (meanMotion * meanMotion));
672     }
673 
674     /** Get a string representation of this TLE set.
675      * <p>The representation is simply the two lines separated by the
676      * platform line separator.</p>
677      * @return string representation of this TLE set
678      */
679     public String toString() {
680         return getLine1() + System.getProperty("line.separator") + getLine2();
681     }
682 
683     /**
684      * Convert Spacecraft State into TLE.
685      * <p>
686      * The B* is not calculated. Its value is simply copied from the model to the generated TLE.
687      * </p>
688      * @param state       Spacecraft State to convert into TLE
689      * @param generationAlgorithm generator for TLE elements
690      * @return a generated TLE
691      * @since 14.0
692      */
693     public static TLE stateToTLE(final SpacecraftState state,
694                                  final TleGenerationAlgorithm generationAlgorithm) {
695         return generationAlgorithm.generate(state, generationAlgorithm.getTemplateTLE());
696     }
697 
698     /** Check the lines format validity.
699      * @param line1 the first element
700      * @param line2 the second element
701      * @return true if format is recognized (non null lines, 69 characters length,
702      * line content), false if not
703      */
704     public static boolean isFormatOK(final String line1, final String line2) {
705 
706         if (line1 == null || line1.length() != 69 ||
707             line2 == null || line2.length() != 69) {
708             return false;
709         }
710 
711         if (!(LINE_1_PATTERN.matcher(line1).matches() &&
712               LINE_2_PATTERN.matcher(line2).matches())) {
713             return false;
714         }
715 
716         // check sums
717         final int checksum1 = checksum(line1);
718         if (Integer.parseInt(line1.substring(68)) != (checksum1 % 10)) {
719             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
720                                       1, Integer.toString(checksum1 % 10), line1.substring(68), line1);
721         }
722 
723         final int checksum2 = checksum(line2);
724         if (Integer.parseInt(line2.substring(68)) != (checksum2 % 10)) {
725             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
726                                       2, Integer.toString(checksum2 % 10), line2.substring(68), line2);
727         }
728 
729         return true;
730 
731     }
732 
733     /** Compute the checksum of the first 68 characters of a line.
734      * @param line line to check
735      * @return checksum
736      */
737     private static int checksum(final CharSequence line) {
738         int sum = 0;
739         for (int j = 0; j < 68; j++) {
740             final char c = line.charAt(j);
741             if (Character.isDigit(c)) {
742                 sum += Character.digit(c, 10);
743             } else if (c == '-') {
744                 ++sum;
745             }
746         }
747         return sum % 10;
748     }
749 
750     /** Parse a satellite number from a String.
751      * <p>
752      * This method supports both traditional 5-digits satellite numbers
753      * and Alpha-5 TLE satellites IDs.
754      * </p>
755      * @param satNumberString the string to parse (e.g., "25544" or "A0001")
756      * @return the satellite number as an integer
757      * @since 13.1.7
758      */
759     public static int parseSatelliteNumber(final String satNumberString) {
760         return ParseUtils.parseSatelliteNumber(satNumberString, 0, satNumberString.length());
761     }
762 
763     /** Check if this tle equals the provided tle.
764      * <p>Due to the difference in precision between object and string
765      * representations of TLE, it is possible for this method to return false
766      * even if string representations returned by {@link #toString()}
767      * are equal.</p>
768      * @param o other tle
769      * @return true if this tle equals the provided tle
770      */
771     @Override
772     public boolean equals(final Object o) {
773         if (o == this) {
774             return true;
775         }
776         if (!(o instanceof final TLE tle)) {
777             return false;
778         }
779         return satelliteNumber == tle.satelliteNumber &&
780                 classification == tle.classification &&
781                 launchYear == tle.launchYear &&
782                 launchNumber == tle.launchNumber &&
783                 Objects.equals(launchPiece, tle.launchPiece) &&
784                 ephemerisType == tle.ephemerisType &&
785                 elementNumber == tle.elementNumber &&
786                 Objects.equals(epoch, tle.epoch) &&
787                 meanMotion == tle.meanMotion &&
788                 meanMotionFirstDerivative == tle.meanMotionFirstDerivative &&
789                 meanMotionSecondDerivative == tle.meanMotionSecondDerivative &&
790                 eccentricity == tle.eccentricity &&
791                 inclination == tle.inclination &&
792                 pa == tle.pa &&
793                 raan == tle.raan &&
794                 meanAnomaly == tle.meanAnomaly &&
795                 revolutionNumberAtEpoch == tle.revolutionNumberAtEpoch &&
796                 bStar == tle.bStar;
797     }
798 
799     /** Get a hashcode for this tle.
800      * @return hashcode
801      */
802     @Override
803     public int hashCode() {
804         return Objects.hash(satelliteNumber,
805                 classification,
806                 launchYear,
807                 launchNumber,
808                 launchPiece,
809                 ephemerisType,
810                 elementNumber,
811                 epoch,
812                 meanMotion,
813                 meanMotionFirstDerivative,
814                 meanMotionSecondDerivative,
815                 eccentricity,
816                 inclination,
817                 pa,
818                 raan,
819                 meanAnomaly,
820                 revolutionNumberAtEpoch,
821                 bStar);
822     }
823 
824 }