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