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