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