1   /* Copyright 2002-2021 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.geometry.euclidean.threed.Rotation;
29  import org.hipparchus.util.ArithmeticUtils;
30  import org.hipparchus.util.FastMath;
31  import org.hipparchus.util.MathUtils;
32  import org.orekit.annotation.DefaultDataContext;
33  import org.orekit.attitudes.InertialProvider;
34  import org.orekit.data.DataContext;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.errors.OrekitMessages;
37  import org.orekit.frames.Frame;
38  import org.orekit.orbits.EquinoctialOrbit;
39  import org.orekit.orbits.KeplerianOrbit;
40  import org.orekit.orbits.Orbit;
41  import org.orekit.orbits.OrbitType;
42  import org.orekit.orbits.PositionAngle;
43  import org.orekit.propagation.SpacecraftState;
44  import org.orekit.time.AbsoluteDate;
45  import org.orekit.time.DateComponents;
46  import org.orekit.time.DateTimeComponents;
47  import org.orekit.time.TimeComponents;
48  import org.orekit.time.TimeScale;
49  import org.orekit.time.TimeStamped;
50  import org.orekit.utils.ParameterDriver;
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, Serializable {
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      /** Default value for epsilon. */
92      private static final double EPSILON_DEFAULT = 1.0e-10;
93  
94      /** Default value for maxIterations. */
95      private static final int MAX_ITERATIONS_DEFAULT = 100;
96  
97      /** B* scaling factor.
98       * <p>
99       * We use a power of 2 to avoid numeric noise introduction
100      * in the multiplications/divisions sequences.
101      * </p>
102      */
103     private static final double B_STAR_SCALE = FastMath.scalb(1.0, -20);
104 
105     /** Name of the mean motion parameter. */
106     private static final String MEAN_MOTION = "meanMotion";
107 
108     /** Name of the inclination parameter. */
109     private static final String INCLINATION = "inclination";
110 
111     /** Name of the eccentricity parameter. */
112     private static final String ECCENTRICITY = "eccentricity";
113 
114     /** Pattern for line 1. */
115     private static final Pattern LINE_1_PATTERN =
116         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})) " +
117                         "[ +-][ 0-9]{5}[+-][ 0-9] [ +-][ 0-9]{5}[+-][ 0-9] [ 0-9] [ 0-9]{4}[ 0-9]");
118 
119     /** Pattern for line 2. */
120     private static final Pattern LINE_2_PATTERN =
121         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} " +
122                         "[ 0-9]{3}[.][ 0-9]{4} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{2}[.][ 0-9]{13}[ 0-9]");
123 
124     /** International symbols for parsing. */
125     private static final DecimalFormatSymbols SYMBOLS =
126         new DecimalFormatSymbols(Locale.US);
127 
128     /** Serializable UID. */
129     private static final long serialVersionUID = -1596648022319057689L;
130 
131     /** The satellite number. */
132     private final int satelliteNumber;
133 
134     /** Classification (U for unclassified). */
135     private final char classification;
136 
137     /** Launch year. */
138     private final int launchYear;
139 
140     /** Launch number. */
141     private final int launchNumber;
142 
143     /** Piece of launch (from "A" to "ZZZ"). */
144     private final String launchPiece;
145 
146     /** Type of ephemeris. */
147     private final int ephemerisType;
148 
149     /** Element number. */
150     private final int elementNumber;
151 
152     /** the TLE current date. */
153     private final AbsoluteDate epoch;
154 
155     /** Mean motion (rad/s). */
156     private final double meanMotion;
157 
158     /** Mean motion first derivative (rad/s²). */
159     private final double meanMotionFirstDerivative;
160 
161     /** Mean motion second derivative (rad/s³). */
162     private final double meanMotionSecondDerivative;
163 
164     /** Eccentricity. */
165     private final double eccentricity;
166 
167     /** Inclination (rad). */
168     private final double inclination;
169 
170     /** Argument of perigee (rad). */
171     private final double pa;
172 
173     /** Right Ascension of the Ascending node (rad). */
174     private final double raan;
175 
176     /** Mean anomaly (rad). */
177     private final double meanAnomaly;
178 
179     /** Revolution number at epoch. */
180     private final int revolutionNumberAtEpoch;
181 
182     /** First line. */
183     private String line1;
184 
185     /** Second line. */
186     private String line2;
187 
188     /** The UTC scale. */
189     private final TimeScale utc;
190 
191     /** Driver for ballistic coefficient parameter. */
192     private final transient ParameterDriver bStarParameterDriver;
193 
194 
195     /** Simple constructor from unparsed two lines. This constructor uses the {@link
196      * DataContext#getDefault() default data context}.
197      *
198      * <p>The static method {@link #isFormatOK(String, String)} should be called
199      * before trying to build this object.<p>
200      * @param line1 the first element (69 char String)
201      * @param line2 the second element (69 char String)
202      * @see #TLE(String, String, TimeScale)
203      */
204     @DefaultDataContext
205     public TLE(final String line1, final String line2) {
206         this(line1, line2, DataContext.getDefault().getTimeScales().getUTC());
207     }
208 
209     /** Simple constructor from unparsed two lines using the given time scale as UTC.
210      *
211      * <p>The static method {@link #isFormatOK(String, String)} should be called
212      * before trying to build this object.<p>
213      * @param line1 the first element (69 char String)
214      * @param line2 the second element (69 char String)
215      * @param utc the UTC time scale.
216      * @since 10.1
217      */
218     public TLE(final String line1, final String line2, final TimeScale utc) {
219 
220         // identification
221         satelliteNumber = ParseUtils.parseSatelliteNumber(line1, 2, 5);
222         final int satNum2 = ParseUtils.parseSatelliteNumber(line2, 2, 5);
223         if (satelliteNumber != satNum2) {
224             throw new OrekitException(OrekitMessages.TLE_LINES_DO_NOT_REFER_TO_SAME_OBJECT,
225                                       line1, line2);
226         }
227         classification  = line1.charAt(7);
228         launchYear      = ParseUtils.parseYear(line1, 9);
229         launchNumber    = ParseUtils.parseInteger(line1, 11, 3);
230         launchPiece     = line1.substring(14, 17).trim();
231         ephemerisType   = ParseUtils.parseInteger(line1, 62, 1);
232         elementNumber   = ParseUtils.parseInteger(line1, 64, 4);
233 
234         // Date format transform (nota: 27/31250 == 86400/100000000)
235         final int    year      = ParseUtils.parseYear(line1, 18);
236         final int    dayInYear = ParseUtils.parseInteger(line1, 20, 3);
237         final long   df        = 27l * ParseUtils.parseInteger(line1, 24, 8);
238         final int    secondsA  = (int) (df / 31250l);
239         final double secondsB  = (df % 31250l) / 31250.0;
240         epoch = new AbsoluteDate(new DateComponents(year, dayInYear),
241                                  new TimeComponents(secondsA, secondsB),
242                                  utc);
243 
244         // mean motion development
245         // converted from rev/day, 2 * rev/day^2 and 6 * rev/day^3 to rad/s, rad/s^2 and rad/s^3
246         meanMotion                 = ParseUtils.parseDouble(line2, 52, 11) * FastMath.PI / 43200.0;
247         meanMotionFirstDerivative  = ParseUtils.parseDouble(line1, 33, 10) * FastMath.PI / 1.86624e9;
248         meanMotionSecondDerivative = Double.parseDouble((line1.substring(44, 45) + '.' +
249                                                          line1.substring(45, 50) + 'e' +
250                                                          line1.substring(50, 52)).replace(' ', '0')) *
251                                      FastMath.PI / 5.3747712e13;
252 
253         eccentricity = Double.parseDouble("." + line2.substring(26, 33).replace(' ', '0'));
254         inclination  = FastMath.toRadians(ParseUtils.parseDouble(line2, 8, 8));
255         pa           = FastMath.toRadians(ParseUtils.parseDouble(line2, 34, 8));
256         raan         = FastMath.toRadians(Double.parseDouble(line2.substring(17, 25).replace(' ', '0')));
257         meanAnomaly  = FastMath.toRadians(ParseUtils.parseDouble(line2, 43, 8));
258 
259         revolutionNumberAtEpoch = ParseUtils.parseInteger(line2, 63, 5);
260         final double bStarValue = Double.parseDouble((line1.substring(53, 54) + '.' +
261                                     line1.substring(54, 59) + 'e' +
262                                     line1.substring(59, 61)).replace(' ', '0'));
263 
264         // save the lines
265         this.line1 = line1;
266         this.line2 = line2;
267         this.utc = utc;
268 
269         // create model parameter drivers
270         this.bStarParameterDriver = new ParameterDriver(B_STAR, bStarValue, B_STAR_SCALE,
271                                                         Double.NEGATIVE_INFINITY,
272                                                         Double.POSITIVE_INFINITY);
273 
274     }
275 
276     /**
277      * <p>
278      * Simple constructor from already parsed elements. This constructor uses the
279      * {@link DataContext#getDefault() default data context}.
280      * </p>
281      *
282      * <p>
283      * The mean anomaly, the right ascension of ascending node Ω and the argument of
284      * perigee ω are normalized into the [0, 2π] interval as they can be negative.
285      * After that, a range check is performed on some of the orbital elements:
286      *
287      * <pre>
288      *     meanMotion &gt;= 0
289      *     0 &lt;= i &lt;= π
290      *     0 &lt;= Ω &lt;= 2π
291      *     0 &lt;= e &lt;= 1
292      *     0 &lt;= ω &lt;= 2π
293      *     0 &lt;= meanAnomaly &lt;= 2π
294      * </pre>
295      *
296      * @param satelliteNumber satellite number
297      * @param classification classification (U for unclassified)
298      * @param launchYear launch year (all digits)
299      * @param launchNumber launch number
300      * @param launchPiece launch piece (3 char String)
301      * @param ephemerisType type of ephemeris
302      * @param elementNumber element number
303      * @param epoch elements epoch
304      * @param meanMotion mean motion (rad/s)
305      * @param meanMotionFirstDerivative mean motion first derivative (rad/s²)
306      * @param meanMotionSecondDerivative mean motion second derivative (rad/s³)
307      * @param e eccentricity
308      * @param i inclination (rad)
309      * @param pa argument of perigee (rad)
310      * @param raan right ascension of ascending node (rad)
311      * @param meanAnomaly mean anomaly (rad)
312      * @param revolutionNumberAtEpoch revolution number at epoch
313      * @param bStar ballistic coefficient
314      * @see #TLE(int, char, int, int, String, int, int, AbsoluteDate, double, double,
315      * double, double, double, double, double, double, int, double, TimeScale)
316      */
317     @DefaultDataContext
318     public TLE(final int satelliteNumber, final char classification,
319                final int launchYear, final int launchNumber, final String launchPiece,
320                final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
321                final double meanMotion, final double meanMotionFirstDerivative,
322                final double meanMotionSecondDerivative, final double e, final double i,
323                final double pa, final double raan, final double meanAnomaly,
324                final int revolutionNumberAtEpoch, final double bStar) {
325         this(satelliteNumber, classification, launchYear, launchNumber, launchPiece,
326                 ephemerisType, elementNumber, epoch, meanMotion,
327                 meanMotionFirstDerivative, meanMotionSecondDerivative, e, i, pa, raan,
328                 meanAnomaly, revolutionNumberAtEpoch, bStar,
329                 DataContext.getDefault().getTimeScales().getUTC());
330     }
331 
332     /**
333      * <p>
334      * Simple constructor from already parsed elements using the given time scale as
335      * UTC.
336      * </p>
337      *
338      * <p>
339      * The mean anomaly, the right ascension of ascending node Ω and the argument of
340      * perigee ω are normalized into the [0, 2π] interval as they can be negative.
341      * After that, a range check is performed on some of the orbital elements:
342      *
343      * <pre>
344      *     meanMotion &gt;= 0
345      *     0 &lt;= i &lt;= π
346      *     0 &lt;= Ω &lt;= 2π
347      *     0 &lt;= e &lt;= 1
348      *     0 &lt;= ω &lt;= 2π
349      *     0 &lt;= meanAnomaly &lt;= 2π
350      * </pre>
351      *
352      * @param satelliteNumber satellite number
353      * @param classification classification (U for unclassified)
354      * @param launchYear launch year (all digits)
355      * @param launchNumber launch number
356      * @param launchPiece launch piece (3 char String)
357      * @param ephemerisType type of ephemeris
358      * @param elementNumber element number
359      * @param epoch elements epoch
360      * @param meanMotion mean motion (rad/s)
361      * @param meanMotionFirstDerivative mean motion first derivative (rad/s²)
362      * @param meanMotionSecondDerivative mean motion second derivative (rad/s³)
363      * @param e eccentricity
364      * @param i inclination (rad)
365      * @param pa argument of perigee (rad)
366      * @param raan right ascension of ascending node (rad)
367      * @param meanAnomaly mean anomaly (rad)
368      * @param revolutionNumberAtEpoch revolution number at epoch
369      * @param bStar ballistic coefficient
370      * @param utc the UTC time scale.
371      * @since 10.1
372      */
373     public TLE(final int satelliteNumber, final char classification,
374                final int launchYear, final int launchNumber, final String launchPiece,
375                final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
376                final double meanMotion, final double meanMotionFirstDerivative,
377                final double meanMotionSecondDerivative, final double e, final double i,
378                final double pa, final double raan, final double meanAnomaly,
379                final int revolutionNumberAtEpoch, final double bStar,
380                final TimeScale utc) {
381 
382         // identification
383         this.satelliteNumber = satelliteNumber;
384         this.classification  = classification;
385         this.launchYear      = launchYear;
386         this.launchNumber    = launchNumber;
387         this.launchPiece     = launchPiece;
388         this.ephemerisType   = ephemerisType;
389         this.elementNumber   = elementNumber;
390 
391         // orbital parameters
392         this.epoch = epoch;
393         // Checking mean motion range
394         this.meanMotion = meanMotion;
395         this.meanMotionFirstDerivative = meanMotionFirstDerivative;
396         this.meanMotionSecondDerivative = meanMotionSecondDerivative;
397 
398         // Checking inclination range
399         this.inclination = i;
400 
401         // Normalizing RAAN in [0,2pi] interval
402         this.raan = MathUtils.normalizeAngle(raan, FastMath.PI);
403 
404         // Checking eccentricity range
405         this.eccentricity = e;
406 
407         // Normalizing PA in [0,2pi] interval
408         this.pa = MathUtils.normalizeAngle(pa, FastMath.PI);
409 
410         // Normalizing mean anomaly in [0,2pi] interval
411         this.meanAnomaly = MathUtils.normalizeAngle(meanAnomaly, FastMath.PI);
412 
413         this.revolutionNumberAtEpoch = revolutionNumberAtEpoch;
414 
415 
416         // don't build the line until really needed
417         this.line1 = null;
418         this.line2 = null;
419         this.utc = utc;
420 
421         // create model parameter drivers
422         this.bStarParameterDriver = new ParameterDriver(B_STAR, bStar, B_STAR_SCALE,
423                                                         Double.NEGATIVE_INFINITY,
424                                                         Double.POSITIVE_INFINITY);
425 
426     }
427 
428     /**
429      * Get the UTC time scale used to create this TLE.
430      *
431      * @return UTC time scale.
432      */
433     public TimeScale getUtc() {
434         return utc;
435     }
436 
437     /** Get the first line.
438      * @return first line
439      */
440     public String getLine1() {
441         if (line1 == null) {
442             buildLine1();
443         }
444         return line1;
445     }
446 
447     /** Get the second line.
448      * @return second line
449      */
450     public String getLine2() {
451         if (line2 == null) {
452             buildLine2();
453         }
454         return line2;
455     }
456 
457     /** Build the line 1 from the parsed elements.
458      */
459     private void buildLine1() {
460 
461         final StringBuffer buffer = new StringBuffer();
462 
463         buffer.append('1');
464 
465         buffer.append(' ');
466         buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-1"));
467         buffer.append(classification);
468 
469         buffer.append(' ');
470         buffer.append(ParseUtils.addPadding("launchYear",   launchYear % 100, '0', 2, true, satelliteNumber));
471         buffer.append(ParseUtils.addPadding("launchNumber", launchNumber, '0', 3, true, satelliteNumber));
472         buffer.append(ParseUtils.addPadding("launchPiece",  launchPiece, ' ', 3, false, satelliteNumber));
473 
474         buffer.append(' ');
475         final DateTimeComponents dtc = epoch.getComponents(utc);
476         buffer.append(ParseUtils.addPadding("year", dtc.getDate().getYear() % 100, '0', 2, true, satelliteNumber));
477         buffer.append(ParseUtils.addPadding("day",  dtc.getDate().getDayOfYear(),  '0', 3, true, satelliteNumber));
478         buffer.append('.');
479         // nota: 31250/27 == 100000000/86400
480         final int fraction = (int) FastMath.rint(31250 * dtc.getTime().getSecondsInUTCDay() / 27.0);
481         buffer.append(ParseUtils.addPadding("fraction", fraction,  '0', 8, true, satelliteNumber));
482 
483         buffer.append(' ');
484         final double n1 = meanMotionFirstDerivative * 1.86624e9 / FastMath.PI;
485         final String sn1 = ParseUtils.addPadding("meanMotionFirstDerivative",
486                                                  new DecimalFormat(".00000000", SYMBOLS).format(n1),
487                                                  ' ', 10, true, satelliteNumber);
488         buffer.append(sn1);
489 
490         buffer.append(' ');
491         final double n2 = meanMotionSecondDerivative * 5.3747712e13 / FastMath.PI;
492         buffer.append(formatExponentMarkerFree("meanMotionSecondDerivative", n2, 5, ' ', 8, true));
493 
494         buffer.append(' ');
495         buffer.append(formatExponentMarkerFree("B*", getBStar(), 5, ' ', 8, true));
496 
497         buffer.append(' ');
498         buffer.append(ephemerisType);
499 
500         buffer.append(' ');
501         buffer.append(ParseUtils.addPadding("elementNumber", elementNumber, ' ', 4, true, satelliteNumber));
502 
503         buffer.append(Integer.toString(checksum(buffer)));
504 
505         line1 = buffer.toString();
506 
507     }
508 
509     /** Format a real number without 'e' exponent marker.
510      * @param name parameter name
511      * @param d number to format
512      * @param mantissaSize size of the mantissa (not counting initial '-' or ' ' for sign)
513      * @param c padding character
514      * @param size desired size
515      * @param rightJustified if true, the resulting string is
516      * right justified (i.e. space are added to the left)
517      * @return formatted and padded number
518      */
519     private String formatExponentMarkerFree(final String name, final double d, final int mantissaSize,
520                                             final char c, final int size, final boolean rightJustified) {
521         final double dAbs = FastMath.abs(d);
522         int exponent = (dAbs < 1.0e-9) ? -9 : (int) FastMath.ceil(FastMath.log10(dAbs));
523         long mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
524         if (mantissa == 0) {
525             exponent = 0;
526         } else if (mantissa > (ArithmeticUtils.pow(10, mantissaSize) - 1)) {
527             // rare case: if d has a single digit like d = 1.0e-4 with mantissaSize = 5
528             // the above computation finds exponent = -4 and mantissa = 100000 which
529             // doesn't fit in a 5 digits string
530             exponent++;
531             mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
532         }
533         final String sMantissa = ParseUtils.addPadding(name, (int) mantissa, '0', mantissaSize, true, satelliteNumber);
534         final String sExponent = Integer.toString(FastMath.abs(exponent));
535         final String formatted = (d <  0 ? '-' : ' ') + sMantissa + (exponent <= 0 ? '-' : '+') + sExponent;
536 
537         return ParseUtils.addPadding(name, formatted, c, size, rightJustified, satelliteNumber);
538 
539     }
540 
541     /** Build the line 2 from the parsed elements.
542      */
543     private void buildLine2() {
544 
545         final StringBuffer buffer = new StringBuffer();
546         final DecimalFormat f34   = new DecimalFormat("##0.0000", SYMBOLS);
547         final DecimalFormat f211  = new DecimalFormat("#0.00000000", SYMBOLS);
548 
549         buffer.append('2');
550 
551         buffer.append(' ');
552         buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-2"));
553 
554         buffer.append(' ');
555         buffer.append(ParseUtils.addPadding(INCLINATION, f34.format(FastMath.toDegrees(inclination)), ' ', 8, true, satelliteNumber));
556         buffer.append(' ');
557         buffer.append(ParseUtils.addPadding("raan", f34.format(FastMath.toDegrees(raan)), ' ', 8, true, satelliteNumber));
558         buffer.append(' ');
559         buffer.append(ParseUtils.addPadding(ECCENTRICITY, (int) FastMath.rint(eccentricity * 1.0e7), '0', 7, true, satelliteNumber));
560         buffer.append(' ');
561         buffer.append(ParseUtils.addPadding("pa", f34.format(FastMath.toDegrees(pa)), ' ', 8, true, satelliteNumber));
562         buffer.append(' ');
563         buffer.append(ParseUtils.addPadding("meanAnomaly", f34.format(FastMath.toDegrees(meanAnomaly)), ' ', 8, true, satelliteNumber));
564 
565         buffer.append(' ');
566         buffer.append(ParseUtils.addPadding(MEAN_MOTION, f211.format(meanMotion * 43200.0 / FastMath.PI), ' ', 11, true, satelliteNumber));
567         buffer.append(ParseUtils.addPadding("revolutionNumberAtEpoch", revolutionNumberAtEpoch, ' ', 5, true, satelliteNumber));
568 
569         buffer.append(Integer.toString(checksum(buffer)));
570 
571         line2 = buffer.toString();
572 
573     }
574 
575     /** Get the satellite id.
576      * @return the satellite number
577      */
578     public int getSatelliteNumber() {
579         return satelliteNumber;
580     }
581 
582     /** Get the classification.
583      * @return classification
584      */
585     public char getClassification() {
586         return classification;
587     }
588 
589     /** Get the launch year.
590      * @return the launch year
591      */
592     public int getLaunchYear() {
593         return launchYear;
594     }
595 
596     /** Get the launch number.
597      * @return the launch number
598      */
599     public int getLaunchNumber() {
600         return launchNumber;
601     }
602 
603     /** Get the launch piece.
604      * @return the launch piece
605      */
606     public String getLaunchPiece() {
607         return launchPiece;
608     }
609 
610     /** Get the type of ephemeris.
611      * @return the ephemeris type (one of {@link #DEFAULT}, {@link #SGP},
612      * {@link #SGP4}, {@link #SGP8}, {@link #SDP4}, {@link #SDP8})
613      */
614     public int getEphemerisType() {
615         return ephemerisType;
616     }
617 
618     /** Get the element number.
619      * @return the element number
620      */
621     public int getElementNumber() {
622         return elementNumber;
623     }
624 
625     /** Get the TLE current date.
626      * @return the epoch
627      */
628     public AbsoluteDate getDate() {
629         return epoch;
630     }
631 
632     /** Get the mean motion.
633      * @return the mean motion (rad/s)
634      */
635     public double getMeanMotion() {
636         return meanMotion;
637     }
638 
639     /** Get the mean motion first derivative.
640      * @return the mean motion first derivative (rad/s²)
641      */
642     public double getMeanMotionFirstDerivative() {
643         return meanMotionFirstDerivative;
644     }
645 
646     /** Get the mean motion second derivative.
647      * @return the mean motion second derivative (rad/s³)
648      */
649     public double getMeanMotionSecondDerivative() {
650         return meanMotionSecondDerivative;
651     }
652 
653     /** Get the eccentricity.
654      * @return the eccentricity
655      */
656     public double getE() {
657         return eccentricity;
658     }
659 
660     /** Get the inclination.
661      * @return the inclination (rad)
662      */
663     public double getI() {
664         return inclination;
665     }
666 
667     /** Get the argument of perigee.
668      * @return omega (rad)
669      */
670     public double getPerigeeArgument() {
671         return pa;
672     }
673 
674     /** Get Right Ascension of the Ascending node.
675      * @return the raan (rad)
676      */
677     public double getRaan() {
678         return raan;
679     }
680 
681     /** Get the mean anomaly.
682      * @return the mean anomaly (rad)
683      */
684     public double getMeanAnomaly() {
685         return meanAnomaly;
686     }
687 
688     /** Get the revolution number.
689      * @return the revolutionNumberAtEpoch
690      */
691     public int getRevolutionNumberAtEpoch() {
692         return revolutionNumberAtEpoch;
693     }
694 
695     /** Get the ballistic coefficient.
696      * @return bStar
697      */
698     public double getBStar() {
699         return bStarParameterDriver.getValue();
700     }
701 
702     /** Get a string representation of this TLE set.
703      * <p>The representation is simply the two lines separated by the
704      * platform line separator.</p>
705      * @return string representation of this TLE set
706      */
707     public String toString() {
708         return getLine1() + System.getProperty("line.separator") + getLine2();
709     }
710 
711     /**
712      * Convert Spacecraft State into TLE.
713      * This converter uses Fixed Point method to reverse SGP4 and SDP4 propagation algorithm
714      * and generates a usable TLE version of a state.
715      * Equinocital orbital parameters are used in order to get a stiff method.
716      * New TLE epoch is state epoch.
717      *
718      * <p>
719      * This method uses the {@link DataContext#getDefault() default data context},
720      * as well as {@link #EPSILON_DEFAULT} and {@link #MAX_ITERATIONS_DEFAULT} for method convergence.
721      *
722      * @param state Spacecraft State to convert into TLE
723      * @param templateTLE first guess used to get identification and estimate new TLE
724      * @return TLE matching with Spacecraft State and template identification
725      * @see #stateToTLE(SpacecraftState, TLE, TimeScale, Frame)
726      * @see #stateToTLE(SpacecraftState, TLE, TimeScale, Frame, double, int)
727      * @since 11.0
728      */
729     @DefaultDataContext
730     public static TLE stateToTLE(final SpacecraftState state, final TLE templateTLE) {
731         return stateToTLE(state, templateTLE,
732                           DataContext.getDefault().getTimeScales().getUTC(),
733                           DataContext.getDefault().getFrames().getTEME());
734     }
735 
736     /**
737      * Convert Spacecraft State into TLE.
738      * This converter uses Fixed Point method to reverse SGP4 and SDP4 propagation algorithm
739      * and generates a usable TLE version of a state.
740      * Equinocital orbital parameters are used in order to get a stiff method.
741      * New TLE epoch is state epoch.
742      *
743      * <p>
744      * This method uses {@link #EPSILON_DEFAULT} and {@link #MAX_ITERATIONS_DEFAULT}
745      * for method convergence.
746      *
747      * @param state Spacecraft State to convert into TLE
748      * @param templateTLE first guess used to get identification and estimate new TLE
749      * @param utc the UTC time scale
750      * @param teme the TEME frame to use for propagation
751      * @return TLE matching with Spacecraft State and template identification
752      * @see #stateToTLE(SpacecraftState, TLE, TimeScale, Frame, double, int)
753      * @since 11.0
754      */
755     public static TLE stateToTLE(final SpacecraftState state, final TLE templateTLE,
756                                  final TimeScale utc, final Frame teme) {
757         return stateToTLE(state, templateTLE, utc, teme, EPSILON_DEFAULT, MAX_ITERATIONS_DEFAULT);
758     }
759 
760     /**
761      * Convert Spacecraft State into TLE.
762      * This converter uses Newton method to reverse SGP4 and SDP4 propagation algorithm
763      * and generates a usable TLE version of a state.
764      * New TLE epoch is state epoch.
765      *
766      * @param state Spacecraft State to convert into TLE
767      * @param templateTLE first guess used to get identification and estimate new TLE
768      * @param utc the UTC time scale
769      * @param teme the TEME frame to use for propagation
770      * @param epsilon used to compute threshold for convergence check
771      * @param maxIterations maximum number of iterations for convergence
772      * @return TLE matching with Spacecraft State and template identification
773      * @since 11.0
774      */
775     public static TLE stateToTLE(final SpacecraftState state, final TLE templateTLE,
776                                  final TimeScale utc, final Frame teme,
777                                  final double epsilon, final int maxIterations) {
778 
779         // Gets equinoctial parameters in TEME frame from state
780         final EquinoctialOrbit equiOrbit = convert(state.getOrbit(), teme);
781         double sma = equiOrbit.getA();
782         double ex  = equiOrbit.getEquinoctialEx();
783         double ey  = equiOrbit.getEquinoctialEy();
784         double hx  = equiOrbit.getHx();
785         double hy  = equiOrbit.getHy();
786         double lv  = equiOrbit.getLv();
787 
788         // Rough initialization of the TLE
789         final KeplerianOrbit keplerianOrbit = (KeplerianOrbit) OrbitType.KEPLERIAN.convertType(equiOrbit);
790         TLE current = newTLE(keplerianOrbit, templateTLE, utc);
791 
792         // threshold for each parameter
793         final double thrA = epsilon * (1 + sma);
794         final double thrE = epsilon * (1 + FastMath.hypot(ex, ey));
795         final double thrH = epsilon * (1 + FastMath.hypot(hx, hy));
796         final double thrV = epsilon * FastMath.PI;
797 
798         int k = 0;
799         while (k++ < maxIterations) {
800 
801             // recompute the state from the current TLE
802             final TLEPropagator propagator = TLEPropagator.selectExtrapolator(current, new InertialProvider(Rotation.IDENTITY, teme), state.getMass(), teme);
803             final Orbit recovOrbit = propagator.getInitialState().getOrbit();
804             final EquinoctialOrbit recovEquiOrbit = (EquinoctialOrbit) OrbitType.EQUINOCTIAL.convertType(recovOrbit);
805 
806             // adapted parameters residuals
807             final double deltaSma = equiOrbit.getA() - recovEquiOrbit.getA();
808             final double deltaEx  = equiOrbit.getEquinoctialEx() - recovEquiOrbit.getEquinoctialEx();
809             final double deltaEy  = equiOrbit.getEquinoctialEy() - recovEquiOrbit.getEquinoctialEy();
810             final double deltaHx  = equiOrbit.getHx() - recovEquiOrbit.getHx();
811             final double deltaHy  = equiOrbit.getHy() - recovEquiOrbit.getHy();
812             final double deltaLv  = MathUtils.normalizeAngle(equiOrbit.getLv() - recovEquiOrbit.getLv(), 0.0);
813 
814             // check convergence
815             if (FastMath.abs(deltaSma) < thrA &&
816                 FastMath.abs(deltaEx)  < thrE &&
817                 FastMath.abs(deltaEy)  < thrE &&
818                 FastMath.abs(deltaHx)  < thrH &&
819                 FastMath.abs(deltaHy)  < thrH &&
820                 FastMath.abs(deltaLv)  < thrV) {
821 
822                 return current;
823             }
824 
825             // update state
826             sma += deltaSma;
827             ex  += deltaEx;
828             ey  += deltaEy;
829             hx  += deltaHx;
830             hy  += deltaHy;
831             lv  += deltaLv;
832             final EquinoctialOrbit newEquiOrbit =
833                                     new EquinoctialOrbit(sma, ex, ey, hx, hy, lv, PositionAngle.TRUE,
834                                     equiOrbit.getFrame(), equiOrbit.getDate(), equiOrbit.getMu());
835             final KeplerianOrbit newKeplOrbit = (KeplerianOrbit) OrbitType.KEPLERIAN.convertType(newEquiOrbit);
836 
837             // update TLE
838             current = newTLE(newKeplOrbit, templateTLE, utc);
839         }
840 
841         throw new OrekitException(OrekitMessages.UNABLE_TO_COMPUTE_TLE, k);
842     }
843 
844     /**
845      * Converts an orbit into an equinoctial orbit expressed in TEME frame.
846      *
847      * @param orbitIn the orbit to convert
848      * @param teme the TEME frame to use for propagation
849      * @return the converted orbit, i.e. equinoctial in TEME frame
850      */
851     private static EquinoctialOrbit convert(final Orbit orbitIn, final Frame teme) {
852         return new EquinoctialOrbit(orbitIn.getPVCoordinates(teme), teme, orbitIn.getMu());
853     }
854 
855     /**
856      * Builds a new TLE from Keplerian parameters and a template for TLE data.
857      * @param keplerianOrbit the Keplerian parameters to build the TLE from
858      * @param templateTLE TLE used to get object identification
859      * @param utc the UTC time scale
860      * @return TLE with template identification and new orbital parameters
861      */
862     private static TLE newTLE(final KeplerianOrbit keplerianOrbit, final TLE templateTLE,
863                               final TimeScale utc) {
864         // Keplerian parameters
865         final double meanMotion  = keplerianOrbit.getKeplerianMeanMotion();
866         final double e           = keplerianOrbit.getE();
867         final double i           = keplerianOrbit.getI();
868         final double raan        = keplerianOrbit.getRightAscensionOfAscendingNode();
869         final double pa          = keplerianOrbit.getPerigeeArgument();
870         final double meanAnomaly = keplerianOrbit.getMeanAnomaly();
871         // TLE epoch is state epoch
872         final AbsoluteDate epoch = keplerianOrbit.getDate();
873         // Identification
874         final int satelliteNumber = templateTLE.getSatelliteNumber();
875         final char classification = templateTLE.getClassification();
876         final int launchYear = templateTLE.getLaunchYear();
877         final int launchNumber = templateTLE.getLaunchNumber();
878         final String launchPiece = templateTLE.getLaunchPiece();
879         final int ephemerisType = templateTLE.getEphemerisType();
880         final int elementNumber = templateTLE.getElementNumber();
881         // Updates revolutionNumberAtEpoch
882         final int revolutionNumberAtEpoch = templateTLE.getRevolutionNumberAtEpoch();
883         final double dt = epoch.durationFrom(templateTLE.getDate());
884         final int newRevolutionNumberAtEpoch = (int) ((int) revolutionNumberAtEpoch + FastMath.floor((MathUtils.normalizeAngle(meanAnomaly, FastMath.PI) + dt * meanMotion) / (2 * FastMath.PI)));
885         // Gets B*
886         final double bStar = templateTLE.getBStar();
887         // Gets Mean Motion derivatives
888         final double meanMotionFirstDerivative = templateTLE.getMeanMotionFirstDerivative();
889         final double meanMotionSecondDerivative = templateTLE.getMeanMotionSecondDerivative();
890         // Returns the new TLE
891         return new TLE(satelliteNumber, classification, launchYear, launchNumber, launchPiece, ephemerisType,
892                        elementNumber, epoch, meanMotion, meanMotionFirstDerivative, meanMotionSecondDerivative,
893                        e, i, pa, raan, meanAnomaly, newRevolutionNumberAtEpoch, bStar, utc);
894     }
895 
896     /** Check the lines format validity.
897      * @param line1 the first element
898      * @param line2 the second element
899      * @return true if format is recognized (non null lines, 69 characters length,
900      * line content), false if not
901      */
902     public static boolean isFormatOK(final String line1, final String line2) {
903 
904         if (line1 == null || line1.length() != 69 ||
905             line2 == null || line2.length() != 69) {
906             return false;
907         }
908 
909         if (!(LINE_1_PATTERN.matcher(line1).matches() &&
910               LINE_2_PATTERN.matcher(line2).matches())) {
911             return false;
912         }
913 
914         // check sums
915         final int checksum1 = checksum(line1);
916         if (Integer.parseInt(line1.substring(68)) != (checksum1 % 10)) {
917             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
918                                       1, Integer.toString(checksum1 % 10), line1.substring(68), line1);
919         }
920 
921         final int checksum2 = checksum(line2);
922         if (Integer.parseInt(line2.substring(68)) != (checksum2 % 10)) {
923             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
924                                       2, Integer.toString(checksum2 % 10), line2.substring(68), line2);
925         }
926 
927         return true;
928 
929     }
930 
931     /** Compute the checksum of the first 68 characters of a line.
932      * @param line line to check
933      * @return checksum
934      */
935     private static int checksum(final CharSequence line) {
936         int sum = 0;
937         for (int j = 0; j < 68; j++) {
938             final char c = line.charAt(j);
939             if (Character.isDigit(c)) {
940                 sum += Character.digit(c, 10);
941             } else if (c == '-') {
942                 ++sum;
943             }
944         }
945         return sum % 10;
946     }
947 
948     /** Check if this tle equals the provided tle.
949      * <p>Due to the difference in precision between object and string
950      * representations of TLE, it is possible for this method to return false
951      * even if string representations returned by {@link #toString()}
952      * are equal.</p>
953      * @param o other tle
954      * @return true if this tle equals the provided tle
955      */
956     @Override
957     public boolean equals(final Object o) {
958         if (o == this) {
959             return true;
960         }
961         if (!(o instanceof TLE)) {
962             return false;
963         }
964         final TLE tle = (TLE) o;
965         return satelliteNumber == tle.satelliteNumber &&
966                 classification == tle.classification &&
967                 launchYear == tle.launchYear &&
968                 launchNumber == tle.launchNumber &&
969                 Objects.equals(launchPiece, tle.launchPiece) &&
970                 ephemerisType == tle.ephemerisType &&
971                 elementNumber == tle.elementNumber &&
972                 Objects.equals(epoch, tle.epoch) &&
973                 meanMotion == tle.meanMotion &&
974                 meanMotionFirstDerivative == tle.meanMotionFirstDerivative &&
975                 meanMotionSecondDerivative == tle.meanMotionSecondDerivative &&
976                 eccentricity == tle.eccentricity &&
977                 inclination == tle.inclination &&
978                 pa == tle.pa &&
979                 raan == tle.raan &&
980                 meanAnomaly == tle.meanAnomaly &&
981                 revolutionNumberAtEpoch == tle.revolutionNumberAtEpoch &&
982                 getBStar() == tle.getBStar();
983     }
984 
985     /** Get a hashcode for this tle.
986      * @return hashcode
987      */
988     @Override
989     public int hashCode() {
990         return Objects.hash(satelliteNumber,
991                 classification,
992                 launchYear,
993                 launchNumber,
994                 launchPiece,
995                 ephemerisType,
996                 elementNumber,
997                 epoch,
998                 meanMotion,
999                 meanMotionFirstDerivative,
1000                 meanMotionSecondDerivative,
1001                 eccentricity,
1002                 inclination,
1003                 pa,
1004                 raan,
1005                 meanAnomaly,
1006                 revolutionNumberAtEpoch,
1007                 getBStar());
1008     }
1009 
1010     /** Get the drivers for TLE propagation SGP4 and SDP4.
1011      * @return drivers for SGP4 and SDP4 model parameters
1012      */
1013     public List<ParameterDriver> getParametersDrivers() {
1014         return Collections.singletonList(bStarParameterDriver);
1015     }
1016 
1017     /** Replace the instance with a data transfer object for serialization.
1018      * @return data transfer object that will be serialized
1019      */
1020     private Object writeReplace() {
1021         return new DataTransferObject(line1, line2, utc);
1022     }
1023 
1024     /** Internal class used only for serialization. */
1025     private static class DataTransferObject implements Serializable {
1026 
1027         /** Serializable UID. */
1028         private static final long serialVersionUID = -1596648022319057689L;
1029 
1030         /** First line. */
1031         private String line1;
1032 
1033         /** Second line. */
1034         private String line2;
1035 
1036         /** The UTC scale. */
1037         private final TimeScale utc;
1038 
1039         /** Simple constructor.
1040          * @param line1 the first element (69 char String)
1041          * @param line2 the second element (69 char String)
1042          * @param utc the UTC time scale
1043          */
1044         DataTransferObject(final String line1, final String line2, final TimeScale utc) {
1045             this.line1 = line1;
1046             this.line2 = line2;
1047             this.utc   = utc;
1048         }
1049 
1050         /** Replace the deserialized data transfer object with a {@link TLE}.
1051          * @return replacement {@link TLE}
1052          */
1053         private Object readResolve() {
1054             return new TLE(line1, line2, utc);
1055         }
1056 
1057     }
1058 
1059 }