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