1   /* Copyright 2002-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.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.TleGenerationAlgorithm;
38  import org.orekit.propagation.analytical.tle.generation.TleGenerationUtil;
39  import org.orekit.propagation.conversion.osc2mean.OsculatingToMeanConverter;
40  import org.orekit.propagation.conversion.osc2mean.TLETheory;
41  import org.orekit.time.AbsoluteDate;
42  import org.orekit.time.DateComponents;
43  import org.orekit.time.DateTimeComponents;
44  import org.orekit.time.TimeComponents;
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 transient 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         // Date format transform (nota: 27/31250 == 86400/100000000)
225         final int    year      = ParseUtils.parseYear(line1, 18);
226         final int    dayInYear = ParseUtils.parseInteger(line1, 20, 3);
227         final long   df        = 27l * ParseUtils.parseInteger(line1, 24, 8);
228         final int    secondsA  = (int) (df / 31250l);
229         final double secondsB  = (df % 31250l) / 31250.0;
230         epoch = new AbsoluteDate(new DateComponents(year, dayInYear),
231                                  new TimeComponents(secondsA, secondsB),
232                                  utc);
233 
234         // mean motion development
235         // converted from rev/day, 2 * rev/day^2 and 6 * rev/day^3 to rad/s, rad/s^2 and rad/s^3
236         meanMotion                 = ParseUtils.parseDouble(line2, 52, 11) * FastMath.PI / 43200.0;
237         meanMotionFirstDerivative  = ParseUtils.parseDouble(line1, 33, 10) * FastMath.PI / 1.86624e9;
238         meanMotionSecondDerivative = Double.parseDouble((line1.substring(44, 45) + '.' +
239                                                          line1.substring(45, 50) + 'e' +
240                                                          line1.substring(50, 52)).replace(' ', '0')) *
241                                      FastMath.PI / 5.3747712e13;
242 
243         eccentricity = Double.parseDouble("." + line2.substring(26, 33).replace(' ', '0'));
244         inclination  = FastMath.toRadians(ParseUtils.parseDouble(line2, 8, 8));
245         pa           = FastMath.toRadians(ParseUtils.parseDouble(line2, 34, 8));
246         raan         = FastMath.toRadians(Double.parseDouble(line2.substring(17, 25).replace(' ', '0')));
247         meanAnomaly  = FastMath.toRadians(ParseUtils.parseDouble(line2, 43, 8));
248 
249         revolutionNumberAtEpoch = ParseUtils.parseInteger(line2, 63, 5);
250         final double bStarValue = Double.parseDouble((line1.substring(53, 54) + '.' +
251                                     line1.substring(54, 59) + 'e' +
252                                     line1.substring(59, 61)).replace(' ', '0'));
253 
254         // save the lines
255         this.line1 = line1;
256         this.line2 = line2;
257         this.utc = utc;
258 
259         // create model parameter drivers
260         this.bStarParameterDriver = new ParameterDriver(B_STAR, bStarValue, B_STAR_SCALE,
261                                                         Double.NEGATIVE_INFINITY,
262                                                         Double.POSITIVE_INFINITY);
263 
264     }
265 
266     /**
267      * <p>
268      * Simple constructor from already parsed elements. This constructor uses the
269      * {@link DataContext#getDefault() default data context}.
270      * </p>
271      *
272      * <p>
273      * The mean anomaly, the right ascension of ascending node Ω and the argument of
274      * perigee ω are normalized into the [0, 2π] interval as they can be negative.
275      * After that, a range check is performed on some of the orbital elements:
276      *
277      * <pre>
278      *     meanMotion &gt;= 0
279      *     0 &lt;= i &lt;= π
280      *     0 &lt;= Ω &lt;= 2π
281      *     0 &lt;= e &lt;= 1
282      *     0 &lt;= ω &lt;= 2π
283      *     0 &lt;= meanAnomaly &lt;= 2π
284      * </pre>
285      *
286      * @param satelliteNumber satellite number
287      * @param classification classification (U for unclassified)
288      * @param launchYear launch year (all digits)
289      * @param launchNumber launch number
290      * @param launchPiece launch piece (3 char String)
291      * @param ephemerisType type of ephemeris
292      * @param elementNumber element number
293      * @param epoch elements epoch
294      * @param meanMotion mean motion (rad/s)
295      * @param meanMotionFirstDerivative mean motion first derivative (rad/s²)
296      * @param meanMotionSecondDerivative mean motion second derivative (rad/s³)
297      * @param e eccentricity
298      * @param i inclination (rad)
299      * @param pa argument of perigee (rad)
300      * @param raan right ascension of ascending node (rad)
301      * @param meanAnomaly mean anomaly (rad)
302      * @param revolutionNumberAtEpoch revolution number at epoch
303      * @param bStar ballistic coefficient
304      * @see #TLE(int, char, int, int, String, int, int, AbsoluteDate, double, double,
305      * double, double, double, double, double, double, int, double, TimeScale)
306      */
307     @DefaultDataContext
308     public TLE(final int satelliteNumber, final char classification,
309                final int launchYear, final int launchNumber, final String launchPiece,
310                final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
311                final double meanMotion, final double meanMotionFirstDerivative,
312                final double meanMotionSecondDerivative, final double e, final double i,
313                final double pa, final double raan, final double meanAnomaly,
314                final int revolutionNumberAtEpoch, final double bStar) {
315         this(satelliteNumber, classification, launchYear, launchNumber, launchPiece,
316                 ephemerisType, elementNumber, epoch, meanMotion,
317                 meanMotionFirstDerivative, meanMotionSecondDerivative, e, i, pa, raan,
318                 meanAnomaly, revolutionNumberAtEpoch, bStar,
319                 DataContext.getDefault().getTimeScales().getUTC());
320     }
321 
322     /**
323      * <p>
324      * Simple constructor from already parsed elements using the given time scale as
325      * UTC.
326      * </p>
327      *
328      * <p>
329      * The mean anomaly, the right ascension of ascending node Ω and the argument of
330      * perigee ω are normalized into the [0, 2π] interval as they can be negative.
331      * After that, a range check is performed on some of the orbital elements:
332      *
333      * <pre>
334      *     meanMotion &gt;= 0
335      *     0 &lt;= i &lt;= π
336      *     0 &lt;= Ω &lt;= 2π
337      *     0 &lt;= e &lt;= 1
338      *     0 &lt;= ω &lt;= 2π
339      *     0 &lt;= meanAnomaly &lt;= 2π
340      * </pre>
341      *
342      * @param satelliteNumber satellite number
343      * @param classification classification (U for unclassified)
344      * @param launchYear launch year (all digits)
345      * @param launchNumber launch number
346      * @param launchPiece launch piece (3 char String)
347      * @param ephemerisType type of ephemeris
348      * @param elementNumber element number
349      * @param epoch elements epoch
350      * @param meanMotion mean motion (rad/s)
351      * @param meanMotionFirstDerivative mean motion first derivative (rad/s²)
352      * @param meanMotionSecondDerivative mean motion second derivative (rad/s³)
353      * @param e eccentricity
354      * @param i inclination (rad)
355      * @param pa argument of perigee (rad)
356      * @param raan right ascension of ascending node (rad)
357      * @param meanAnomaly mean anomaly (rad)
358      * @param revolutionNumberAtEpoch revolution number at epoch
359      * @param bStar ballistic coefficient
360      * @param utc the UTC time scale.
361      * @since 10.1
362      */
363     public TLE(final int satelliteNumber, final char classification,
364                final int launchYear, final int launchNumber, final String launchPiece,
365                final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
366                final double meanMotion, final double meanMotionFirstDerivative,
367                final double meanMotionSecondDerivative, final double e, final double i,
368                final double pa, final double raan, final double meanAnomaly,
369                final int revolutionNumberAtEpoch, final double bStar,
370                final TimeScale utc) {
371 
372         // identification
373         this.satelliteNumber = satelliteNumber;
374         this.classification  = classification;
375         this.launchYear      = launchYear;
376         this.launchNumber    = launchNumber;
377         this.launchPiece     = launchPiece;
378         this.ephemerisType   = ephemerisType;
379         this.elementNumber   = elementNumber;
380 
381         // orbital parameters
382         this.epoch = epoch;
383         // Checking mean motion range
384         this.meanMotion = meanMotion;
385         this.meanMotionFirstDerivative = meanMotionFirstDerivative;
386         this.meanMotionSecondDerivative = meanMotionSecondDerivative;
387 
388         // Checking inclination range
389         this.inclination = i;
390 
391         // Normalizing RAAN in [0,2pi] interval
392         this.raan = MathUtils.normalizeAngle(raan, FastMath.PI);
393 
394         // Checking eccentricity range
395         this.eccentricity = e;
396 
397         // Normalizing PA in [0,2pi] interval
398         this.pa = MathUtils.normalizeAngle(pa, FastMath.PI);
399 
400         // Normalizing mean anomaly in [0,2pi] interval
401         this.meanAnomaly = MathUtils.normalizeAngle(meanAnomaly, FastMath.PI);
402 
403         this.revolutionNumberAtEpoch = revolutionNumberAtEpoch;
404 
405 
406         // don't build the line until really needed
407         this.line1 = null;
408         this.line2 = null;
409         this.utc = utc;
410 
411         // create model parameter drivers
412         this.bStarParameterDriver = new ParameterDriver(B_STAR, bStar, B_STAR_SCALE,
413                                                         Double.NEGATIVE_INFINITY,
414                                                         Double.POSITIVE_INFINITY);
415 
416     }
417 
418     /**
419      * Get the UTC time scale used to create this TLE.
420      *
421      * @return UTC time scale.
422      */
423     public TimeScale getUtc() {
424         return utc;
425     }
426 
427     /** Get the first line.
428      * @return first line
429      */
430     public String getLine1() {
431         if (line1 == null) {
432             buildLine1();
433         }
434         return line1;
435     }
436 
437     /** Get the second line.
438      * @return second line
439      */
440     public String getLine2() {
441         if (line2 == null) {
442             buildLine2();
443         }
444         return line2;
445     }
446 
447     /** Build the line 1 from the parsed elements.
448      */
449     private void buildLine1() {
450 
451         final StringBuilder buffer = new StringBuilder();
452 
453         buffer.append('1');
454 
455         buffer.append(' ');
456         buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-1"));
457         buffer.append(classification);
458 
459         buffer.append(' ');
460         buffer.append(ParseUtils.addPadding("launchYear",   launchYear % 100, '0', 2, true, satelliteNumber));
461         buffer.append(ParseUtils.addPadding("launchNumber", launchNumber, '0', 3, true, satelliteNumber));
462         buffer.append(ParseUtils.addPadding("launchPiece",  launchPiece, ' ', 3, false, satelliteNumber));
463 
464         buffer.append(' ');
465         DateTimeComponents dtc = epoch.getComponents(utc);
466         int fraction = (int) FastMath.rint(31250 * dtc.getTime().getSecondsInUTCDay() / 27.0);
467         if (fraction >= 100000000) {
468             dtc =  epoch.shiftedBy(Constants.JULIAN_DAY).getComponents(utc);
469             fraction -= 100000000;
470         }
471         buffer.append(ParseUtils.addPadding("year", dtc.getDate().getYear() % 100, '0', 2, true, satelliteNumber));
472         buffer.append(ParseUtils.addPadding("day",  dtc.getDate().getDayOfYear(),  '0', 3, true, satelliteNumber));
473         buffer.append('.');
474         // nota: 31250/27 == 100000000/86400
475 
476         buffer.append(ParseUtils.addPadding("fraction", fraction,  '0', 8, true, satelliteNumber));
477 
478         buffer.append(' ');
479         final double n1 = meanMotionFirstDerivative * 1.86624e9 / FastMath.PI;
480         final String sn1 = ParseUtils.addPadding("meanMotionFirstDerivative",
481                                                  new DecimalFormat(".00000000", SYMBOLS).format(n1),
482                                                  ' ', 10, true, satelliteNumber);
483         buffer.append(sn1);
484 
485         buffer.append(' ');
486         final double n2 = meanMotionSecondDerivative * 5.3747712e13 / FastMath.PI;
487         buffer.append(formatExponentMarkerFree("meanMotionSecondDerivative", n2, 5, ' ', 8, true));
488 
489         buffer.append(' ');
490         buffer.append(formatExponentMarkerFree("B*", getBStar(), 5, ' ', 8, true));
491 
492         buffer.append(' ');
493         buffer.append(ephemerisType);
494 
495         buffer.append(' ');
496         buffer.append(ParseUtils.addPadding("elementNumber", elementNumber, ' ', 4, true, satelliteNumber));
497 
498         buffer.append(checksum(buffer));
499 
500         line1 = buffer.toString();
501 
502     }
503 
504     /** Format a real number without 'e' exponent marker.
505      * @param name parameter name
506      * @param d number to format
507      * @param mantissaSize size of the mantissa (not counting initial '-' or ' ' for sign)
508      * @param c padding character
509      * @param size desired size
510      * @param rightJustified if true, the resulting string is
511      * right justified (i.e. space are added to the left)
512      * @return formatted and padded number
513      */
514     private String formatExponentMarkerFree(final String name, final double d, final int mantissaSize,
515                                             final char c, final int size, final boolean rightJustified) {
516         final double dAbs = FastMath.abs(d);
517         int exponent = (dAbs < 1.0e-9) ? -9 : (int) FastMath.ceil(FastMath.log10(dAbs));
518         long mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
519         if (mantissa == 0) {
520             exponent = 0;
521         } else if (mantissa > (ArithmeticUtils.pow(10, mantissaSize) - 1)) {
522             // rare case: if d has a single digit like d = 1.0e-4 with mantissaSize = 5
523             // the above computation finds exponent = -4 and mantissa = 100000 which
524             // doesn't fit in a 5 digits string
525             exponent++;
526             mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
527         }
528         final String sMantissa = ParseUtils.addPadding(name, (int) mantissa, '0', mantissaSize, true, satelliteNumber);
529         final String sExponent = Integer.toString(FastMath.abs(exponent));
530         final String formatted = (d <  0 ? '-' : ' ') + sMantissa + (exponent <= 0 ? '-' : '+') + sExponent;
531 
532         return ParseUtils.addPadding(name, formatted, c, size, rightJustified, satelliteNumber);
533 
534     }
535 
536     /** Build the line 2 from the parsed elements.
537      */
538     private void buildLine2() {
539 
540         final StringBuilder buffer = new StringBuilder();
541         final DecimalFormat f34   = new DecimalFormat("##0.0000", SYMBOLS);
542         final DecimalFormat f211  = new DecimalFormat("#0.00000000", SYMBOLS);
543 
544         buffer.append('2');
545 
546         buffer.append(' ');
547         buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-2"));
548 
549         buffer.append(' ');
550         buffer.append(ParseUtils.addPadding(INCLINATION, f34.format(FastMath.toDegrees(inclination)), ' ', 8, true, satelliteNumber));
551         buffer.append(' ');
552         buffer.append(ParseUtils.addPadding("raan", f34.format(FastMath.toDegrees(raan)), ' ', 8, true, satelliteNumber));
553         buffer.append(' ');
554         buffer.append(ParseUtils.addPadding(ECCENTRICITY, (int) FastMath.rint(eccentricity * 1.0e7), '0', 7, true, satelliteNumber));
555         buffer.append(' ');
556         buffer.append(ParseUtils.addPadding("pa", f34.format(FastMath.toDegrees(pa)), ' ', 8, true, satelliteNumber));
557         buffer.append(' ');
558         buffer.append(ParseUtils.addPadding("meanAnomaly", f34.format(FastMath.toDegrees(meanAnomaly)), ' ', 8, true, satelliteNumber));
559 
560         buffer.append(' ');
561         buffer.append(ParseUtils.addPadding(MEAN_MOTION, f211.format(meanMotion * 43200.0 / FastMath.PI), ' ', 11, true, satelliteNumber));
562         buffer.append(ParseUtils.addPadding("revolutionNumberAtEpoch", revolutionNumberAtEpoch, ' ', 5, true, satelliteNumber));
563 
564         buffer.append(checksum(buffer));
565 
566         line2 = buffer.toString();
567 
568     }
569 
570     /** Get the satellite id.
571      * @return the satellite number
572      */
573     public int getSatelliteNumber() {
574         return satelliteNumber;
575     }
576 
577     /** Get the classification.
578      * @return classification
579      */
580     public char getClassification() {
581         return classification;
582     }
583 
584     /** Get the launch year.
585      * @return the launch year
586      */
587     public int getLaunchYear() {
588         return launchYear;
589     }
590 
591     /** Get the launch number.
592      * @return the launch number
593      */
594     public int getLaunchNumber() {
595         return launchNumber;
596     }
597 
598     /** Get the launch piece.
599      * @return the launch piece
600      */
601     public String getLaunchPiece() {
602         return launchPiece;
603     }
604 
605     /** Get the type of ephemeris.
606      * @return the ephemeris type (one of {@link #DEFAULT}, {@link #SGP},
607      * {@link #SGP4}, {@link #SGP8}, {@link #SDP4}, {@link #SDP8})
608      */
609     public int getEphemerisType() {
610         return ephemerisType;
611     }
612 
613     /** Get the element number.
614      * @return the element number
615      */
616     public int getElementNumber() {
617         return elementNumber;
618     }
619 
620     /** Get the TLE current date.
621      * @return the epoch
622      */
623     public AbsoluteDate getDate() {
624         return epoch;
625     }
626 
627     /** Get the mean motion.
628      * @return the mean motion (rad/s)
629      */
630     public double getMeanMotion() {
631         return meanMotion;
632     }
633 
634     /** Get the mean motion first derivative.
635      * @return the mean motion first derivative (rad/s²)
636      */
637     public double getMeanMotionFirstDerivative() {
638         return meanMotionFirstDerivative;
639     }
640 
641     /** Get the mean motion second derivative.
642      * @return the mean motion second derivative (rad/s³)
643      */
644     public double getMeanMotionSecondDerivative() {
645         return meanMotionSecondDerivative;
646     }
647 
648     /** Get the eccentricity.
649      * @return the eccentricity
650      */
651     public double getE() {
652         return eccentricity;
653     }
654 
655     /** Get the inclination.
656      * @return the inclination (rad)
657      */
658     public double getI() {
659         return inclination;
660     }
661 
662     /** Get the argument of perigee.
663      * @return omega (rad)
664      */
665     public double getPerigeeArgument() {
666         return pa;
667     }
668 
669     /** Get Right Ascension of the Ascending node.
670      * @return the raan (rad)
671      */
672     public double getRaan() {
673         return raan;
674     }
675 
676     /** Get the mean anomaly.
677      * @return the mean anomaly (rad)
678      */
679     public double getMeanAnomaly() {
680         return meanAnomaly;
681     }
682 
683     /** Get the revolution number.
684      * @return the revolutionNumberAtEpoch
685      */
686     public int getRevolutionNumberAtEpoch() {
687         return revolutionNumberAtEpoch;
688     }
689 
690     /** Get the ballistic coefficient at tle date.
691      * @return bStar
692      */
693     public double getBStar() {
694         return bStarParameterDriver.getValue(getDate());
695     }
696 
697     /** Get the ballistic coefficient at a specific date.
698      * @param date at which the ballistic coefficient wants to be known.
699      * @return bStar
700      */
701     public double getBStar(final AbsoluteDate date) {
702         return bStarParameterDriver.getValue(date);
703     }
704 
705     /** Compute the semi-major axis from the mean motion of the TLE and the gravitational parameter from TLEConstants.
706      * @return the semi-major axis computed.
707      */
708     public double computeSemiMajorAxis() {
709         return FastMath.cbrt(TLEConstants.MU / (meanMotion * meanMotion));
710     }
711 
712     /** Get a string representation of this TLE set.
713      * <p>The representation is simply the two lines separated by the
714      * platform line separator.</p>
715      * @return string representation of this TLE set
716      */
717     public String toString() {
718         return getLine1() + System.getProperty("line.separator") + getLine2();
719     }
720 
721     /**
722      * Convert Spacecraft State into TLE.
723      *
724      * @param state Spacecraft State to convert into TLE
725      * @param templateTLE only used to get identifiers like satellite number, launch year, etc. In other words, the keplerian elements contained in the generated TLE are based on the provided state and not the template TLE.
726      * @param generationAlgorithm TLE generation algorithm
727      * @return a generated TLE
728      * @since 12.0
729      * @deprecated As of release 13.0, use {@link #stateToTLE(SpacecraftState, TLE, OsculatingToMeanConverter)} instead.
730      */
731     @Deprecated
732     public static TLE stateToTLE(final SpacecraftState state, final TLE templateTLE,
733                                  final TleGenerationAlgorithm generationAlgorithm) {
734         return generationAlgorithm.generate(state, templateTLE);
735     }
736 
737     /**
738      * Convert Spacecraft State into TLE.
739      * <p>
740      * Uses the {@link DataContext#getDefault() default data context}.
741      * </p>
742      * <p>
743      * The B* is not calculated. Its value is simply copied from the model to the generated TLE.
744      * </p>
745      * @param state       Spacecraft State to convert into TLE
746      * @param templateTLE only used to get identifiers like satellite number, launch year, etc.
747      *                    In other words, the keplerian elements contained in the generated TLE
748      *                    are based on the provided state and not the template TLE.
749      * @param converter   osculating to mean orbit converter
750      * @return a generated TLE
751      * @since 13.0
752      */
753     @DefaultDataContext
754     public static TLE stateToTLE(final SpacecraftState state,
755                                  final TLE templateTLE,
756                                  final OsculatingToMeanConverter converter) {
757         return stateToTLE(state, templateTLE, converter, DataContext.getDefault());
758     }
759 
760     /**
761      * Convert Spacecraft State into TLE.
762      * <p>
763      * The B* is not calculated. Its value is simply copied from the model to the generated TLE.
764      * </p>
765      * @param state       Spacecraft State to convert into TLE
766      * @param templateTLE only used to get identifiers like satellite number, launch year, etc.
767      *                    In other words, the keplerian elements contained in the generated TLE
768      *                    are based on the provided state and not the template TLE.
769      * @param converter   osculating to mean orbit converter
770      * @param dataContext data context
771      * @return a generated TLE
772      * @since 13.0
773      */
774     public static TLE stateToTLE(final SpacecraftState state,
775                                  final TLE templateTLE,
776                                  final OsculatingToMeanConverter converter,
777                                  final DataContext dataContext) {
778         converter.setMeanTheory(new TLETheory(templateTLE, dataContext));
779         final KeplerianOrbit mean = (KeplerianOrbit) OrbitType.KEPLERIAN.convertType(converter.convertToMean(state.getOrbit()));
780         final TLE tle = TleGenerationUtil.newTLE(mean, templateTLE, templateTLE.getBStar(mean.getDate()),
781                                                  dataContext.getTimeScales().getUTC());
782         // reset estimated parameters from template to generated tle
783         for (final ParameterDriver templateDrivers : templateTLE.getParametersDrivers()) {
784             if (templateDrivers.isSelected()) {
785                 // set to selected for the new TLE
786                 tle.getParameterDriver(templateDrivers.getName()).setSelected(true);
787             }
788         }
789         return tle;
790     }
791 
792     /** Check the lines format validity.
793      * @param line1 the first element
794      * @param line2 the second element
795      * @return true if format is recognized (non null lines, 69 characters length,
796      * line content), false if not
797      */
798     public static boolean isFormatOK(final String line1, final String line2) {
799 
800         if (line1 == null || line1.length() != 69 ||
801             line2 == null || line2.length() != 69) {
802             return false;
803         }
804 
805         if (!(LINE_1_PATTERN.matcher(line1).matches() &&
806               LINE_2_PATTERN.matcher(line2).matches())) {
807             return false;
808         }
809 
810         // check sums
811         final int checksum1 = checksum(line1);
812         if (Integer.parseInt(line1.substring(68)) != (checksum1 % 10)) {
813             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
814                                       1, Integer.toString(checksum1 % 10), line1.substring(68), line1);
815         }
816 
817         final int checksum2 = checksum(line2);
818         if (Integer.parseInt(line2.substring(68)) != (checksum2 % 10)) {
819             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
820                                       2, Integer.toString(checksum2 % 10), line2.substring(68), line2);
821         }
822 
823         return true;
824 
825     }
826 
827     /** Compute the checksum of the first 68 characters of a line.
828      * @param line line to check
829      * @return checksum
830      */
831     private static int checksum(final CharSequence line) {
832         int sum = 0;
833         for (int j = 0; j < 68; j++) {
834             final char c = line.charAt(j);
835             if (Character.isDigit(c)) {
836                 sum += Character.digit(c, 10);
837             } else if (c == '-') {
838                 ++sum;
839             }
840         }
841         return sum % 10;
842     }
843 
844     /** Check if this tle equals the provided tle.
845      * <p>Due to the difference in precision between object and string
846      * representations of TLE, it is possible for this method to return false
847      * even if string representations returned by {@link #toString()}
848      * are equal.</p>
849      * @param o other tle
850      * @return true if this tle equals the provided tle
851      */
852     @Override
853     public boolean equals(final Object o) {
854         if (o == this) {
855             return true;
856         }
857         if (!(o instanceof TLE)) {
858             return false;
859         }
860         final TLE tle = (TLE) o;
861         return satelliteNumber == tle.satelliteNumber &&
862                 classification == tle.classification &&
863                 launchYear == tle.launchYear &&
864                 launchNumber == tle.launchNumber &&
865                 Objects.equals(launchPiece, tle.launchPiece) &&
866                 ephemerisType == tle.ephemerisType &&
867                 elementNumber == tle.elementNumber &&
868                 Objects.equals(epoch, tle.epoch) &&
869                 meanMotion == tle.meanMotion &&
870                 meanMotionFirstDerivative == tle.meanMotionFirstDerivative &&
871                 meanMotionSecondDerivative == tle.meanMotionSecondDerivative &&
872                 eccentricity == tle.eccentricity &&
873                 inclination == tle.inclination &&
874                 pa == tle.pa &&
875                 raan == tle.raan &&
876                 meanAnomaly == tle.meanAnomaly &&
877                 revolutionNumberAtEpoch == tle.revolutionNumberAtEpoch &&
878                 getBStar() == tle.getBStar();
879     }
880 
881     /** Get a hashcode for this tle.
882      * @return hashcode
883      */
884     @Override
885     public int hashCode() {
886         return Objects.hash(satelliteNumber,
887                 classification,
888                 launchYear,
889                 launchNumber,
890                 launchPiece,
891                 ephemerisType,
892                 elementNumber,
893                 epoch,
894                 meanMotion,
895                 meanMotionFirstDerivative,
896                 meanMotionSecondDerivative,
897                 eccentricity,
898                 inclination,
899                 pa,
900                 raan,
901                 meanAnomaly,
902                 revolutionNumberAtEpoch,
903                 getBStar());
904     }
905 
906     /** Get the drivers for TLE propagation SGP4 and SDP4.
907      * @return drivers for SGP4 and SDP4 model parameters
908      */
909     public List<ParameterDriver> getParametersDrivers() {
910         return Collections.singletonList(bStarParameterDriver);
911     }
912 
913 }