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