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