FieldTLE.java

  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. import java.text.DecimalFormat;
  19. import java.text.DecimalFormatSymbols;
  20. import java.util.Collections;
  21. import java.util.List;
  22. import java.util.Locale;
  23. import java.util.Objects;

  24. import org.hipparchus.CalculusFieldElement;
  25. import org.hipparchus.Field;
  26. import org.hipparchus.util.ArithmeticUtils;
  27. import org.hipparchus.util.FastMath;
  28. import org.hipparchus.util.MathUtils;
  29. import org.orekit.annotation.DefaultDataContext;
  30. import org.orekit.data.DataContext;
  31. import org.orekit.errors.OrekitException;
  32. import org.orekit.errors.OrekitInternalError;
  33. import org.orekit.errors.OrekitMessages;
  34. import org.orekit.orbits.FieldKeplerianOrbit;
  35. import org.orekit.orbits.OrbitType;
  36. import org.orekit.propagation.FieldSpacecraftState;
  37. import org.orekit.propagation.analytical.tle.generation.TleGenerationAlgorithm;
  38. import org.orekit.propagation.analytical.tle.generation.TleGenerationUtil;
  39. import org.orekit.propagation.conversion.osc2mean.OsculatingToMeanConverter;
  40. import org.orekit.propagation.conversion.osc2mean.TLETheory;
  41. import org.orekit.time.DateComponents;
  42. import org.orekit.time.DateTimeComponents;
  43. import org.orekit.time.FieldAbsoluteDate;
  44. import org.orekit.time.FieldTimeStamped;
  45. import org.orekit.time.TimeComponents;
  46. import org.orekit.time.TimeScale;
  47. import org.orekit.utils.Constants;
  48. import org.orekit.utils.ParameterDriver;
  49. import org.orekit.utils.ParameterDriversProvider;

  50. /** This class is a container for a single set of TLE data.
  51.  *
  52.  * <p>TLE sets can be built either by providing directly the two lines, in
  53.  * which case parsing is performed internally or by providing the already
  54.  * parsed elements.</p>
  55.  * <p>TLE are not transparently convertible to {@link org.orekit.orbits.Orbit Orbit}
  56.  * instances. They are significant only with respect to their dedicated {@link
  57.  * TLEPropagator propagator}, which also computes position and velocity coordinates.
  58.  * Any attempt to directly use orbital parameters like {@link #getE() eccentricity},
  59.  * {@link #getI() inclination}, etc. without any reference to the {@link TLEPropagator
  60.  * TLE propagator} is prone to errors.</p>
  61.  * <p>More information on the TLE format can be found on the
  62.  * <a href="https://www.celestrak.com/">CelesTrak website.</a></p>
  63.  * @author Fabien Maussion
  64.  * @author Luc Maisonobe
  65.  * @author Thomas Paulet (field translation)
  66.  * @since 11.0
  67.  * @param <T> type of the field elements
  68.  */
  69. public class FieldTLE<T extends CalculusFieldElement<T>> implements FieldTimeStamped<T>, ParameterDriversProvider {

  70.     /** Identifier for default type of ephemeris (SGP4/SDP4). */
  71.     public static final int DEFAULT = 0;

  72.     /** Identifier for SGP type of ephemeris. */
  73.     public static final int SGP = 1;

  74.     /** Identifier for SGP4 type of ephemeris. */
  75.     public static final int SGP4 = 2;

  76.     /** Identifier for SDP4 type of ephemeris. */
  77.     public static final int SDP4 = 3;

  78.     /** Identifier for SGP8 type of ephemeris. */
  79.     public static final int SGP8 = 4;

  80.     /** Identifier for SDP8 type of ephemeris. */
  81.     public static final int SDP8 = 5;

  82.     /** Parameter name for B* coefficient. */
  83.     public static final String B_STAR = "BSTAR";

  84.     /** B* scaling factor.
  85.      * <p>
  86.      * We use a power of 2 to avoid numeric noise introduction
  87.      * in the multiplications/divisions sequences.
  88.      * </p>
  89.      */
  90.     private static final double B_STAR_SCALE = FastMath.scalb(1.0, -20);

  91.     /** Name of the mean motion parameter. */
  92.     private static final String MEAN_MOTION = "meanMotion";

  93.     /** Name of the inclination parameter. */
  94.     private static final String INCLINATION = "inclination";

  95.     /** Name of the eccentricity parameter. */
  96.     private static final String ECCENTRICITY = "eccentricity";

  97.     /** International symbols for parsing. */
  98.     private static final DecimalFormatSymbols SYMBOLS =
  99.         new DecimalFormatSymbols(Locale.US);

  100.     /** The satellite number. */
  101.     private final int satelliteNumber;

  102.     /** Classification (U for unclassified). */
  103.     private final char classification;

  104.     /** Launch year. */
  105.     private final int launchYear;

  106.     /** Launch number. */
  107.     private final int launchNumber;

  108.     /** Piece of launch (from "A" to "ZZZ"). */
  109.     private final String launchPiece;

  110.     /** Type of ephemeris. */
  111.     private final int ephemerisType;

  112.     /** Element number. */
  113.     private final int elementNumber;

  114.     /** the TLE current date. */
  115.     private final FieldAbsoluteDate<T> epoch;

  116.     /** Mean motion (rad/s). */
  117.     private final T meanMotion;

  118.     /** Mean motion first derivative (rad/s²). */
  119.     private final T meanMotionFirstDerivative;

  120.     /** Mean motion second derivative (rad/s³). */
  121.     private final T meanMotionSecondDerivative;

  122.     /** Eccentricity. */
  123.     private final T eccentricity;

  124.     /** Inclination (rad). */
  125.     private final T inclination;

  126.     /** Argument of perigee (rad). */
  127.     private final T pa;

  128.     /** Right Ascension of the Ascending node (rad). */
  129.     private final T raan;

  130.     /** Mean anomaly (rad). */
  131.     private final T meanAnomaly;

  132.     /** Revolution number at epoch. */
  133.     private final int revolutionNumberAtEpoch;

  134.     /** First line. */
  135.     private String line1;

  136.     /** Second line. */
  137.     private String line2;

  138.     /** The UTC scale. */
  139.     private final TimeScale utc;

  140.     /** Driver for ballistic coefficient parameter. */
  141.     private final ParameterDriver bStarParameterDriver;

  142.     /** Simple constructor from unparsed two lines. This constructor uses the {@link
  143.      * DataContext#getDefault() default data context}.
  144.      *
  145.      * <p>The static method {@link #isFormatOK(String, String)} should be called
  146.      * before trying to build this object.</p>
  147.      * @param field field utilized by default
  148.      * @param line1 the first element (69 char String)
  149.      * @param line2 the second element (69 char String)
  150.      * @see #FieldTLE(Field, String, String, TimeScale)
  151.      */
  152.     @DefaultDataContext
  153.     public FieldTLE(final Field<T> field, final String line1, final String line2) {
  154.         this(field, line1, line2, DataContext.getDefault().getTimeScales().getUTC());
  155.     }

  156.     /** Simple constructor from unparsed two lines using the given time scale as UTC.
  157.      *
  158.      *<p>This method uses the {@link DataContext#getDefault() default data context}.
  159.      *
  160.      * <p>The static method {@link #isFormatOK(String, String)} should be called
  161.      * before trying to build this object.</p>
  162.      * @param field field utilized by default
  163.      * @param line1 the first element (69 char String)
  164.      * @param line2 the second element (69 char String)
  165.      * @param utc the UTC time scale.
  166.      */
  167.     public FieldTLE(final Field<T> field, final String line1, final String line2, final TimeScale utc) {

  168.         // zero and pi for fields
  169.         final T zero = field.getZero();
  170.         final T pi   = zero.getPi();

  171.         // identification
  172.         satelliteNumber = ParseUtils.parseSatelliteNumber(line1, 2, 5);
  173.         final int satNum2 = ParseUtils.parseSatelliteNumber(line2, 2, 5);
  174.         if (satelliteNumber != satNum2) {
  175.             throw new OrekitException(OrekitMessages.TLE_LINES_DO_NOT_REFER_TO_SAME_OBJECT,
  176.                                       line1, line2);
  177.         }
  178.         classification  = line1.charAt(7);
  179.         launchYear      = ParseUtils.parseYear(line1, 9);
  180.         launchNumber    = ParseUtils.parseInteger(line1, 11, 3);
  181.         launchPiece     = line1.substring(14, 17).trim();
  182.         ephemerisType   = ParseUtils.parseInteger(line1, 62, 1);
  183.         elementNumber   = ParseUtils.parseInteger(line1, 64, 4);

  184.         // Date format transform (nota: 27/31250 == 86400/100000000)
  185.         final int    year      = ParseUtils.parseYear(line1, 18);
  186.         final int    dayInYear = ParseUtils.parseInteger(line1, 20, 3);
  187.         final long   df        = 27l * ParseUtils.parseInteger(line1, 24, 8);
  188.         final int    secondsA  = (int) (df / 31250l);
  189.         final double secondsB  = (df % 31250l) / 31250.0;
  190.         epoch = new FieldAbsoluteDate<>(field, new DateComponents(year, dayInYear),
  191.                                  new TimeComponents(secondsA, secondsB),
  192.                                  utc);

  193.         // mean motion development
  194.         // converted from rev/day, 2 * rev/day^2 and 6 * rev/day^3 to rad/s, rad/s^2 and rad/s^3
  195.         meanMotion                 = pi.multiply(ParseUtils.parseDouble(line2, 52, 11)).divide(43200.0);
  196.         meanMotionFirstDerivative  = pi.multiply(ParseUtils.parseDouble(line1, 33, 10)).divide(1.86624e9);
  197.         meanMotionSecondDerivative = pi.multiply(Double.parseDouble((line1.substring(44, 45) + '.' +
  198.                                                                      line1.substring(45, 50) + 'e' +
  199.                                                                      line1.substring(50, 52)).replace(' ', '0'))).divide(5.3747712e13);

  200.         eccentricity = zero.newInstance(Double.parseDouble("." + line2.substring(26, 33).replace(' ', '0')));
  201.         inclination  = zero.newInstance(FastMath.toRadians(ParseUtils.parseDouble(line2, 8, 8)));
  202.         pa           = zero.newInstance(FastMath.toRadians(ParseUtils.parseDouble(line2, 34, 8)));
  203.         raan         = zero.newInstance(FastMath.toRadians(Double.parseDouble(line2.substring(17, 25).replace(' ', '0'))));
  204.         meanAnomaly  = zero.newInstance(FastMath.toRadians(ParseUtils.parseDouble(line2, 43, 8)));

  205.         revolutionNumberAtEpoch = ParseUtils.parseInteger(line2, 63, 5);
  206.         final double bStarValue = Double.parseDouble((line1.substring(53, 54) + '.' +
  207.                         line1.substring(54, 59) + 'e' +
  208.                         line1.substring(59, 61)).replace(' ', '0'));

  209.         // save the lines
  210.         this.line1 = line1;
  211.         this.line2 = line2;
  212.         this.utc = utc;

  213.         this.bStarParameterDriver = new ParameterDriver(B_STAR, bStarValue, B_STAR_SCALE,
  214.                                                         Double.NEGATIVE_INFINITY,
  215.                                                         Double.POSITIVE_INFINITY);

  216.     }

  217.     /**
  218.      * <p>
  219.      * Simple constructor from already parsed elements. This constructor uses the
  220.      * {@link DataContext#getDefault() default data context}.
  221.      * </p>
  222.      *
  223.      * <p>
  224.      * The mean anomaly, the right ascension of ascending node Ω and the argument of
  225.      * perigee ω are normalized into the [0, 2π] interval as they can be negative.
  226.      * After that, a range check is performed on some of the orbital elements:
  227.      *
  228.      * <pre>
  229.      *     meanMotion &gt;= 0
  230.      *     0 &lt;= i &lt;= π
  231.      *     0 &lt;= Ω &lt;= 2π
  232.      *     0 &lt;= e &lt;= 1
  233.      *     0 &lt;= ω &lt;= 2π
  234.      *     0 &lt;= meanAnomaly &lt;= 2π
  235.      * </pre>
  236.      *
  237.      *
  238.      * @param satelliteNumber satellite number
  239.      * @param classification classification (U for unclassified)
  240.      * @param launchYear launch year (all digits)
  241.      * @param launchNumber launch number
  242.      * @param launchPiece launch piece (3 char String)
  243.      * @param ephemerisType type of ephemeris
  244.      * @param elementNumber element number
  245.      * @param epoch elements epoch
  246.      * @param meanMotion mean motion (rad/s)
  247.      * @param meanMotionFirstDerivative mean motion first derivative (rad/s²)
  248.      * @param meanMotionSecondDerivative mean motion second derivative (rad/s³)
  249.      * @param e eccentricity
  250.      * @param i inclination (rad)
  251.      * @param pa argument of perigee (rad)
  252.      * @param raan right ascension of ascending node (rad)
  253.      * @param meanAnomaly mean anomaly (rad)
  254.      * @param revolutionNumberAtEpoch revolution number at epoch
  255.      * @param bStar ballistic coefficient
  256.      * @see #FieldTLE(int, char, int, int, String, int, int, FieldAbsoluteDate, CalculusFieldElement, CalculusFieldElement,
  257.      * CalculusFieldElement, CalculusFieldElement, CalculusFieldElement, CalculusFieldElement, CalculusFieldElement, CalculusFieldElement, int, double, TimeScale)
  258.      */
  259.     @DefaultDataContext
  260.     public FieldTLE(final int satelliteNumber, final char classification,
  261.                final int launchYear, final int launchNumber, final String launchPiece,
  262.                final int ephemerisType, final int elementNumber, final FieldAbsoluteDate<T> epoch,
  263.                final T meanMotion, final T meanMotionFirstDerivative,
  264.                final T meanMotionSecondDerivative, final T e, final T i,
  265.                final T pa, final T raan, final T meanAnomaly,
  266.                final int revolutionNumberAtEpoch, final double bStar) {
  267.         this(satelliteNumber, classification, launchYear, launchNumber, launchPiece,
  268.                 ephemerisType, elementNumber, epoch, meanMotion,
  269.                 meanMotionFirstDerivative, meanMotionSecondDerivative, e, i, pa, raan,
  270.                 meanAnomaly, revolutionNumberAtEpoch, bStar,
  271.                 DataContext.getDefault().getTimeScales().getUTC());
  272.     }

  273.     /**
  274.      * <p>
  275.      * Simple constructor from already parsed elements using the given time scale as
  276.      * UTC.
  277.      * </p>
  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.      * @param utc the UTC time scale.
  312.      */
  313.     public FieldTLE(final int satelliteNumber, final char classification,
  314.                final int launchYear, final int launchNumber, final String launchPiece,
  315.                final int ephemerisType, final int elementNumber, final FieldAbsoluteDate<T> epoch,
  316.                final T meanMotion, final T meanMotionFirstDerivative,
  317.                final T meanMotionSecondDerivative, final T e, final T i,
  318.                final T pa, final T raan, final T meanAnomaly,
  319.                final int revolutionNumberAtEpoch, final double bStar,
  320.                final TimeScale utc) {

  321.         // pi for fields
  322.         final T pi = e.getPi();

  323.         // identification
  324.         this.satelliteNumber = satelliteNumber;
  325.         this.classification  = classification;
  326.         this.launchYear      = launchYear;
  327.         this.launchNumber    = launchNumber;
  328.         this.launchPiece     = launchPiece;
  329.         this.ephemerisType   = ephemerisType;
  330.         this.elementNumber   = elementNumber;

  331.         // orbital parameters
  332.         this.epoch = epoch;
  333.         // Checking mean motion range
  334.         this.meanMotion = meanMotion;
  335.         this.meanMotionFirstDerivative = meanMotionFirstDerivative;
  336.         this.meanMotionSecondDerivative = meanMotionSecondDerivative;

  337.         // Checking inclination range
  338.         this.inclination = i;

  339.         // Normalizing RAAN in [0,2pi] interval
  340.         this.raan = MathUtils.normalizeAngle(raan, pi);

  341.         // Checking eccentricity range
  342.         this.eccentricity = e;

  343.         // Normalizing PA in [0,2pi] interval
  344.         this.pa = MathUtils.normalizeAngle(pa, pi);

  345.         // Normalizing mean anomaly in [0,2pi] interval
  346.         this.meanAnomaly = MathUtils.normalizeAngle(meanAnomaly, pi);

  347.         this.revolutionNumberAtEpoch = revolutionNumberAtEpoch;
  348.         this.bStarParameterDriver = new ParameterDriver(B_STAR, bStar, B_STAR_SCALE,
  349.                                                        Double.NEGATIVE_INFINITY,
  350.                                                        Double.POSITIVE_INFINITY);

  351.         // don't build the line until really needed
  352.         this.line1 = null;
  353.         this.line2 = null;
  354.         this.utc = utc;

  355.     }

  356.     /**
  357.      * Get the UTC time scale used to create this TLE.
  358.      *
  359.      * @return UTC time scale.
  360.      */
  361.     TimeScale getUtc() {
  362.         return utc;
  363.     }

  364.     /** Get the first line.
  365.      * @return first line
  366.      */
  367.     public String getLine1() {
  368.         if (line1 == null) {
  369.             buildLine1();
  370.         }
  371.         return line1;
  372.     }

  373.     /** Get the second line.
  374.      * @return second line
  375.      */
  376.     public String getLine2() {
  377.         if (line2 == null) {
  378.             buildLine2();
  379.         }
  380.         return line2;
  381.     }

  382.     /** Build the line 1 from the parsed elements.
  383.      */
  384.     private void buildLine1() {

  385.         final StringBuilder buffer = new StringBuilder();

  386.         buffer.append('1');

  387.         buffer.append(' ');
  388.         buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-1"));
  389.         buffer.append(classification);

  390.         buffer.append(' ');
  391.         buffer.append(ParseUtils.addPadding("launchYear",   launchYear % 100, '0', 2, true, satelliteNumber));
  392.         buffer.append(ParseUtils.addPadding("launchNumber", launchNumber, '0', 3, true, satelliteNumber));
  393.         buffer.append(ParseUtils.addPadding("launchPiece",  launchPiece, ' ', 3, false, satelliteNumber));

  394.         buffer.append(' ');
  395.         DateTimeComponents dtc = epoch.getComponents(utc);
  396.         int fraction = (int) FastMath.rint(31250 * dtc.getTime().getSecondsInUTCDay() / 27.0);
  397.         if (fraction >= 100000000) {
  398.             dtc =  epoch.shiftedBy(Constants.JULIAN_DAY).getComponents(utc);
  399.             fraction -= 100000000;
  400.         }
  401.         buffer.append(ParseUtils.addPadding("year", dtc.getDate().getYear() % 100, '0', 2, true, satelliteNumber));
  402.         buffer.append(ParseUtils.addPadding("day",  dtc.getDate().getDayOfYear(),  '0', 3, true, satelliteNumber));
  403.         buffer.append('.');
  404.         // nota: 31250/27 == 100000000/86400

  405.         buffer.append(ParseUtils.addPadding("fraction", fraction,  '0', 8, true, satelliteNumber));

  406.         buffer.append(' ');
  407.         final double n1 = meanMotionFirstDerivative.divide(pa.getPi()).multiply(1.86624e9).getReal();
  408.         final String sn1 = ParseUtils.addPadding("meanMotionFirstDerivative",
  409.                                                  new DecimalFormat(".00000000", SYMBOLS).format(n1),
  410.                                                  ' ', 10, true, satelliteNumber);
  411.         buffer.append(sn1);

  412.         buffer.append(' ');
  413.         final double n2 = meanMotionSecondDerivative.divide(pa.getPi()).multiply(5.3747712e13).getReal();
  414.         buffer.append(formatExponentMarkerFree("meanMotionSecondDerivative", n2, 5, ' ', 8, true));

  415.         buffer.append(' ');
  416.         buffer.append(formatExponentMarkerFree("B*", getBStar(), 5, ' ', 8, true));

  417.         buffer.append(' ');
  418.         buffer.append(ephemerisType);

  419.         buffer.append(' ');
  420.         buffer.append(ParseUtils.addPadding("elementNumber", elementNumber, ' ', 4, true, satelliteNumber));

  421.         buffer.append(Integer.toString(checksum(buffer)));

  422.         line1 = buffer.toString();

  423.     }

  424.     /** Format a real number without 'e' exponent marker.
  425.      * @param name parameter name
  426.      * @param d number to format
  427.      * @param mantissaSize size of the mantissa (not counting initial '-' or ' ' for sign)
  428.      * @param c padding character
  429.      * @param size desired size
  430.      * @param rightJustified if true, the resulting string is
  431.      * right justified (i.e. space are added to the left)
  432.      * @return formatted and padded number
  433.      */
  434.     private String formatExponentMarkerFree(final String name, final double d, final int mantissaSize,
  435.                                             final char c, final int size, final boolean rightJustified) {
  436.         final double dAbs = FastMath.abs(d);
  437.         int exponent = (dAbs < 1.0e-9) ? -9 : (int) FastMath.ceil(FastMath.log10(dAbs));
  438.         long mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
  439.         if (mantissa == 0) {
  440.             exponent = 0;
  441.         } else if (mantissa > (ArithmeticUtils.pow(10, mantissaSize) - 1)) {
  442.             // rare case: if d has a single digit like d = 1.0e-4 with mantissaSize = 5
  443.             // the above computation finds exponent = -4 and mantissa = 100000 which
  444.             // doesn't fit in a 5 digits string
  445.             exponent++;
  446.             mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
  447.         }
  448.         final String sMantissa = ParseUtils.addPadding(name, (int) mantissa,
  449.                                                        '0', mantissaSize, true, satelliteNumber);
  450.         final String sExponent = Integer.toString(FastMath.abs(exponent));
  451.         final String formatted = (d <  0 ? '-' : ' ') + sMantissa + (exponent <= 0 ? '-' : '+') + sExponent;

  452.         return ParseUtils.addPadding(name, formatted, c, size, rightJustified, satelliteNumber);

  453.     }

  454.     /** Build the line 2 from the parsed elements.
  455.      */
  456.     private void buildLine2() {

  457.         final StringBuilder buffer = new StringBuilder();
  458.         final DecimalFormat f34   = new DecimalFormat("##0.0000", SYMBOLS);
  459.         final DecimalFormat f211  = new DecimalFormat("#0.00000000", SYMBOLS);

  460.         buffer.append('2');

  461.         buffer.append(' ');
  462.         buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-2"));

  463.         buffer.append(' ');
  464.         buffer.append(ParseUtils.addPadding(INCLINATION, f34.format(FastMath.toDegrees(inclination).getReal()), ' ', 8, true, satelliteNumber));
  465.         buffer.append(' ');
  466.         buffer.append(ParseUtils.addPadding("raan", f34.format(FastMath.toDegrees(raan).getReal()), ' ', 8, true, satelliteNumber));
  467.         buffer.append(' ');
  468.         buffer.append(ParseUtils.addPadding(ECCENTRICITY, (int) FastMath.rint(eccentricity.getReal() * 1.0e7), '0', 7, true, satelliteNumber));
  469.         buffer.append(' ');
  470.         buffer.append(ParseUtils.addPadding("pa", f34.format(FastMath.toDegrees(pa).getReal()), ' ', 8, true, satelliteNumber));
  471.         buffer.append(' ');
  472.         buffer.append(ParseUtils.addPadding("meanAnomaly", f34.format(FastMath.toDegrees(meanAnomaly).getReal()), ' ', 8, true, satelliteNumber));

  473.         buffer.append(' ');
  474.         buffer.append(ParseUtils.addPadding(MEAN_MOTION, f211.format(meanMotion.divide(pa.getPi()).multiply(43200.0).getReal()), ' ', 11, true, satelliteNumber));
  475.         buffer.append(ParseUtils.addPadding("revolutionNumberAtEpoch", revolutionNumberAtEpoch,
  476.                                             ' ', 5, true, satelliteNumber));

  477.         buffer.append(Integer.toString(checksum(buffer)));

  478.         line2 = buffer.toString();

  479.     }

  480.     /** {@inheritDoc}.
  481.      * <p>Get the drivers for TLE propagation SGP4 and SDP4.
  482.      * @return drivers for SGP4 and SDP4 model parameters
  483.      */
  484.     @Override
  485.     public List<ParameterDriver> getParametersDrivers() {
  486.         return Collections.singletonList(bStarParameterDriver);
  487.     }

  488.     /** Get the satellite id.
  489.      * @return the satellite number
  490.      */
  491.     public int getSatelliteNumber() {
  492.         return satelliteNumber;
  493.     }

  494.     /** Get the classification.
  495.      * @return classification
  496.      */
  497.     public char getClassification() {
  498.         return classification;
  499.     }

  500.     /** Get the launch year.
  501.      * @return the launch year
  502.      */
  503.     public int getLaunchYear() {
  504.         return launchYear;
  505.     }

  506.     /** Get the launch number.
  507.      * @return the launch number
  508.      */
  509.     public int getLaunchNumber() {
  510.         return launchNumber;
  511.     }

  512.     /** Get the launch piece.
  513.      * @return the launch piece
  514.      */
  515.     public String getLaunchPiece() {
  516.         return launchPiece;
  517.     }

  518.     /** Get the type of ephemeris.
  519.      * @return the ephemeris type (one of {@link #DEFAULT}, {@link #SGP},
  520.      * {@link #SGP4}, {@link #SGP8}, {@link #SDP4}, {@link #SDP8})
  521.      */
  522.     public int getEphemerisType() {
  523.         return ephemerisType;
  524.     }

  525.     /** Get the element number.
  526.      * @return the element number
  527.      */
  528.     public int getElementNumber() {
  529.         return elementNumber;
  530.     }

  531.     /** Get the TLE current date.
  532.      * @return the epoch
  533.      */
  534.     public FieldAbsoluteDate<T> getDate() {
  535.         return epoch;
  536.     }

  537.     /** Get the mean motion.
  538.      * @return the mean motion (rad/s)
  539.      */
  540.     public T getMeanMotion() {
  541.         return meanMotion;
  542.     }

  543.     /** Get the mean motion first derivative.
  544.      * @return the mean motion first derivative (rad/s²)
  545.      */
  546.     public T getMeanMotionFirstDerivative() {
  547.         return meanMotionFirstDerivative;
  548.     }

  549.     /** Get the mean motion second derivative.
  550.      * @return the mean motion second derivative (rad/s³)
  551.      */
  552.     public T getMeanMotionSecondDerivative() {
  553.         return meanMotionSecondDerivative;
  554.     }

  555.     /** Get the eccentricity.
  556.      * @return the eccentricity
  557.      */
  558.     public T getE() {
  559.         return eccentricity;
  560.     }

  561.     /** Get the inclination.
  562.      * @return the inclination (rad)
  563.      */
  564.     public T getI() {
  565.         return inclination;
  566.     }

  567.     /** Get the argument of perigee.
  568.      * @return omega (rad)
  569.      */
  570.     public T getPerigeeArgument() {
  571.         return pa;
  572.     }

  573.     /** Get Right Ascension of the Ascending node.
  574.      * @return the raan (rad)
  575.      */
  576.     public T getRaan() {
  577.         return raan;
  578.     }

  579.     /** Get the mean anomaly.
  580.      * @return the mean anomaly (rad)
  581.      */
  582.     public T getMeanAnomaly() {
  583.         return meanAnomaly;
  584.     }

  585.     /** Get the revolution number.
  586.      * @return the revolutionNumberAtEpoch
  587.      */
  588.     public int getRevolutionNumberAtEpoch() {
  589.         return revolutionNumberAtEpoch;
  590.     }

  591.     /** Get the ballistic coefficient.
  592.      * @return bStar
  593.      */
  594.     public double getBStar() {
  595.         return bStarParameterDriver.getValue(getDate().toAbsoluteDate());
  596.     }

  597.     /**
  598.      * Compute the semi-major axis from the mean motion of the TLE and the gravitational parameter from TLEConstants.
  599.      * @return the semi-major axis computed.
  600.      */
  601.     public T computeSemiMajorAxis() {
  602.         return FastMath.cbrt(meanMotion.square().reciprocal().multiply(TLEConstants.MU));
  603.     }

  604.     /** Get a string representation of this TLE set.
  605.      * <p>The representation is simply the two lines separated by the
  606.      * platform line separator.</p>
  607.      * @return string representation of this TLE set
  608.      */
  609.     public String toString() {
  610.         try {
  611.             return getLine1() + System.getProperty("line.separator") + getLine2();
  612.         } catch (OrekitException oe) {
  613.             throw new OrekitInternalError(oe);
  614.         }
  615.     }

  616.     /**
  617.      * Convert Spacecraft State into TLE.
  618.      *
  619.      * @param state Spacecraft State to convert into TLE
  620.      * @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.
  621.      * @param generationAlgorithm TLE generation algorithm
  622.      * @param <T> type of the element
  623.      * @return a generated TLE
  624.      * @since 12.0
  625.      * @deprecated As of release 13.0, use {@link #stateToTLE(FieldSpacecraftState, FieldTLE, OsculatingToMeanConverter)} instead.
  626.      */
  627.     @Deprecated
  628.     public static <T extends CalculusFieldElement<T>> FieldTLE<T> stateToTLE(final FieldSpacecraftState<T> state, final FieldTLE<T> templateTLE,
  629.                                                                              final TleGenerationAlgorithm generationAlgorithm) {
  630.         return generationAlgorithm.generate(state, templateTLE);
  631.     }

  632.     /**
  633.      * Convert Spacecraft State into TLE.
  634.      * <p>
  635.      * Uses the {@link DataContext#getDefault() default data context}.
  636.      * </p>
  637.      * <p>
  638.      * The B* is not calculated. Its value is simply copied from the template to the generated TLE.
  639.      * </p>
  640.      * @param <T>         type of the elements
  641.      * @param state       Spacecraft State to convert into TLE
  642.      * @param templateTLE only used to get identifiers like satellite number, launch year, etc.
  643.      *                    In other words, the keplerian elements contained in the generated TLE
  644.      *                    are based on the provided state and not the template TLE.
  645.      * @param converter   osculating to mean orbit converter
  646.      * @return a generated TLE
  647.      * @since 13.0
  648.      */
  649.     @DefaultDataContext
  650.     public static <T extends CalculusFieldElement<T>> FieldTLE<T> stateToTLE(final FieldSpacecraftState<T> state, final FieldTLE<T> templateTLE,
  651.                                                                              final OsculatingToMeanConverter converter) {
  652.         return stateToTLE(state, templateTLE, converter, DataContext.getDefault());
  653.     }

  654.     /**
  655.      * Convert Spacecraft State into TLE.
  656.      * <p>
  657.      * The B* is not calculated. Its value is simply copied from the template to the generated TLE.
  658.      * </p>
  659.      * @param <T>         type of the elements
  660.      * @param state       Spacecraft State to convert into TLE
  661.      * @param templateTLE only used to get identifiers like satellite number, launch year, etc.
  662.      *                    In other words, the keplerian elements contained in the generated TLE
  663.      *                    are based on the provided state and not the template TLE.
  664.      * @param converter   osculating to mean orbit converter
  665.      * @param dataContext data context
  666.      * @return a generated TLE
  667.      * @since 13.0
  668.      */
  669.     public static <T extends CalculusFieldElement<T>> FieldTLE<T> stateToTLE(final FieldSpacecraftState<T> state, final FieldTLE<T> templateTLE,
  670.                                                                              final OsculatingToMeanConverter converter,
  671.                                                                              final DataContext dataContext) {
  672.         converter.setMeanTheory(new TLETheory(templateTLE.toTLE(), dataContext));
  673.         final T bStar = state.getMass().getField().getZero().newInstance(templateTLE.getBStar());
  674.         final FieldKeplerianOrbit<T> mean = (FieldKeplerianOrbit<T>) OrbitType.KEPLERIAN.convertType(converter.convertToMean(state.getOrbit()));
  675.         final FieldTLE<T> tle =  TleGenerationUtil.newTLE(mean, templateTLE, bStar, dataContext.getTimeScales().getUTC());
  676.         // reset estimated parameters from template to generated tle
  677.         for (final ParameterDriver templateDrivers : templateTLE.getParametersDrivers()) {
  678.             if (templateDrivers.isSelected()) {
  679.                 // set to selected for the new TLE
  680.                 tle.getParameterDriver(templateDrivers.getName()).setSelected(true);
  681.             }
  682.         }
  683.         return tle;
  684.     }

  685.     /** Check the lines format validity.
  686.      * @param line1 the first element
  687.      * @param line2 the second element
  688.      * @return true if format is recognized (non null lines, 69 characters length,
  689.      * line content), false if not
  690.      */
  691.     public static boolean isFormatOK(final String line1, final String line2) {
  692.         return TLE.isFormatOK(line1, line2);
  693.     }

  694.     /** Compute the checksum of the first 68 characters of a line.
  695.      * @param line line to check
  696.      * @return checksum
  697.      */
  698.     private static int checksum(final CharSequence line) {
  699.         int sum = 0;
  700.         for (int j = 0; j < 68; j++) {
  701.             final char c = line.charAt(j);
  702.             if (Character.isDigit(c)) {
  703.                 sum += Character.digit(c, 10);
  704.             } else if (c == '-') {
  705.                 ++sum;
  706.             }
  707.         }
  708.         return sum % 10;
  709.     }

  710.     /**
  711.      * Convert FieldTLE into TLE.
  712.      * @return TLE
  713.      */
  714.     public TLE toTLE() {
  715.         final TLE regularTLE = new TLE(getSatelliteNumber(), getClassification(), getLaunchYear(), getLaunchNumber(), getLaunchPiece(), getEphemerisType(),
  716.                                        getElementNumber(), getDate().toAbsoluteDate(), getMeanMotion().getReal(), getMeanMotionFirstDerivative().getReal(),
  717.                                        getMeanMotionSecondDerivative().getReal(), getE().getReal(), getI().getReal(), getPerigeeArgument().getReal(),
  718.                                        getRaan().getReal(), getMeanAnomaly().getReal(), getRevolutionNumberAtEpoch(), getBStar(), getUtc());

  719.         for (int k = 0; k < regularTLE.getParametersDrivers().size(); ++k) {
  720.             regularTLE.getParametersDrivers().get(k).setSelected(getParametersDrivers().get(k).isSelected());
  721.         }

  722.         return regularTLE;

  723.     }

  724.     /** Check if this tle equals the provided tle.
  725.      * <p>Due to the difference in precision between object and string
  726.      * representations of TLE, it is possible for this method to return false
  727.      * even if string representations returned by {@link #toString()}
  728.      * are equal.</p>
  729.      * @param o other tle
  730.      * @return true if this tle equals the provided tle
  731.      */
  732.     @Override
  733.     public boolean equals(final Object o) {
  734.         if (o == this) {
  735.             return true;
  736.         }
  737.         if (!(o instanceof FieldTLE)) {
  738.             return false;
  739.         }
  740.         @SuppressWarnings("unchecked")
  741.         final FieldTLE<T> tle = (FieldTLE<T>) o;
  742.         return satelliteNumber == tle.satelliteNumber &&
  743.                 classification == tle.classification &&
  744.                 launchYear == tle.launchYear &&
  745.                 launchNumber == tle.launchNumber &&
  746.                 Objects.equals(launchPiece, tle.launchPiece) &&
  747.                 ephemerisType == tle.ephemerisType &&
  748.                 elementNumber == tle.elementNumber &&
  749.                 Objects.equals(epoch, tle.epoch) &&
  750.                 meanMotion.getReal() == tle.meanMotion.getReal() &&
  751.                 meanMotionFirstDerivative.getReal() == tle.meanMotionFirstDerivative.getReal() &&
  752.                 meanMotionSecondDerivative.getReal() == tle.meanMotionSecondDerivative.getReal() &&
  753.                 eccentricity.getReal() == tle.eccentricity.getReal() &&
  754.                 inclination.getReal() == tle.inclination.getReal() &&
  755.                 pa.getReal() == tle.pa.getReal() &&
  756.                 raan.getReal() == tle.raan.getReal() &&
  757.                 meanAnomaly.getReal() == tle.meanAnomaly.getReal() &&
  758.                 revolutionNumberAtEpoch == tle.revolutionNumberAtEpoch &&
  759.                 getBStar() == tle.getBStar();
  760.     }

  761.     /** Get a hashcode for this tle.
  762.      * @return hashcode
  763.      */
  764.     @Override
  765.     public int hashCode() {
  766.         return Objects.hash(satelliteNumber,
  767.                 classification,
  768.                 launchYear,
  769.                 launchNumber,
  770.                 launchPiece,
  771.                 ephemerisType,
  772.                 elementNumber,
  773.                 epoch,
  774.                 meanMotion,
  775.                 meanMotionFirstDerivative,
  776.                 meanMotionSecondDerivative,
  777.                 eccentricity,
  778.                 inclination,
  779.                 pa,
  780.                 raan,
  781.                 meanAnomaly,
  782.                 revolutionNumberAtEpoch,
  783.                 getBStar());
  784.     }

  785. }