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