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