1   /* Copyright 2002-2022 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 StringBuilder buffer = new StringBuilder();
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(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 StringBuilder buffer = new StringBuilder();
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(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                 // Verify if parameters are estimated
823                 for (final ParameterDriver templateDrivers : templateTLE.getParametersDrivers()) {
824                     if (templateDrivers.isSelected()) {
825                         // Set to selected for the new TLE
826                         current.getParameterDriver(templateDrivers.getName()).setSelected(true);
827                     }
828                 }
829 
830                 // Return
831                 return current;
832             }
833 
834             // update state
835             sma += deltaSma;
836             ex  += deltaEx;
837             ey  += deltaEy;
838             hx  += deltaHx;
839             hy  += deltaHy;
840             lv  += deltaLv;
841             final EquinoctialOrbit newEquiOrbit =
842                                     new EquinoctialOrbit(sma, ex, ey, hx, hy, lv, PositionAngle.TRUE,
843                                     equiOrbit.getFrame(), equiOrbit.getDate(), equiOrbit.getMu());
844             final KeplerianOrbit newKeplOrbit = (KeplerianOrbit) OrbitType.KEPLERIAN.convertType(newEquiOrbit);
845 
846             // update TLE
847             current = newTLE(newKeplOrbit, templateTLE, utc);
848         }
849 
850         throw new OrekitException(OrekitMessages.UNABLE_TO_COMPUTE_TLE, k);
851     }
852 
853     /**
854      * Converts an orbit into an equinoctial orbit expressed in TEME frame.
855      *
856      * @param orbitIn the orbit to convert
857      * @param teme the TEME frame to use for propagation
858      * @return the converted orbit, i.e. equinoctial in TEME frame
859      */
860     private static EquinoctialOrbit convert(final Orbit orbitIn, final Frame teme) {
861         return new EquinoctialOrbit(orbitIn.getPVCoordinates(teme), teme, orbitIn.getMu());
862     }
863 
864     /**
865      * Builds a new TLE from Keplerian parameters and a template for TLE data.
866      * @param keplerianOrbit the Keplerian parameters to build the TLE from
867      * @param templateTLE TLE used to get object identification
868      * @param utc the UTC time scale
869      * @return TLE with template identification and new orbital parameters
870      */
871     private static TLE newTLE(final KeplerianOrbit keplerianOrbit, final TLE templateTLE,
872                               final TimeScale utc) {
873         // Keplerian parameters
874         final double meanMotion  = keplerianOrbit.getKeplerianMeanMotion();
875         final double e           = keplerianOrbit.getE();
876         final double i           = keplerianOrbit.getI();
877         final double raan        = keplerianOrbit.getRightAscensionOfAscendingNode();
878         final double pa          = keplerianOrbit.getPerigeeArgument();
879         final double meanAnomaly = keplerianOrbit.getMeanAnomaly();
880         // TLE epoch is state epoch
881         final AbsoluteDate epoch = keplerianOrbit.getDate();
882         // Identification
883         final int satelliteNumber = templateTLE.getSatelliteNumber();
884         final char classification = templateTLE.getClassification();
885         final int launchYear = templateTLE.getLaunchYear();
886         final int launchNumber = templateTLE.getLaunchNumber();
887         final String launchPiece = templateTLE.getLaunchPiece();
888         final int ephemerisType = templateTLE.getEphemerisType();
889         final int elementNumber = templateTLE.getElementNumber();
890         // Updates revolutionNumberAtEpoch
891         final int revolutionNumberAtEpoch = templateTLE.getRevolutionNumberAtEpoch();
892         final double dt = epoch.durationFrom(templateTLE.getDate());
893         final int newRevolutionNumberAtEpoch = (int) (revolutionNumberAtEpoch + FastMath.floor((MathUtils.normalizeAngle(meanAnomaly, FastMath.PI) + dt * meanMotion) / (2 * FastMath.PI)));
894         // Gets B*
895         final double bStar = templateTLE.getBStar();
896         // Gets Mean Motion derivatives
897         final double meanMotionFirstDerivative = templateTLE.getMeanMotionFirstDerivative();
898         final double meanMotionSecondDerivative = templateTLE.getMeanMotionSecondDerivative();
899         // Returns the new TLE
900         return new TLE(satelliteNumber, classification, launchYear, launchNumber, launchPiece, ephemerisType,
901                        elementNumber, epoch, meanMotion, meanMotionFirstDerivative, meanMotionSecondDerivative,
902                        e, i, pa, raan, meanAnomaly, newRevolutionNumberAtEpoch, bStar, utc);
903     }
904 
905     /** Check the lines format validity.
906      * @param line1 the first element
907      * @param line2 the second element
908      * @return true if format is recognized (non null lines, 69 characters length,
909      * line content), false if not
910      */
911     public static boolean isFormatOK(final String line1, final String line2) {
912 
913         if (line1 == null || line1.length() != 69 ||
914             line2 == null || line2.length() != 69) {
915             return false;
916         }
917 
918         if (!(LINE_1_PATTERN.matcher(line1).matches() &&
919               LINE_2_PATTERN.matcher(line2).matches())) {
920             return false;
921         }
922 
923         // check sums
924         final int checksum1 = checksum(line1);
925         if (Integer.parseInt(line1.substring(68)) != (checksum1 % 10)) {
926             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
927                                       1, Integer.toString(checksum1 % 10), line1.substring(68), line1);
928         }
929 
930         final int checksum2 = checksum(line2);
931         if (Integer.parseInt(line2.substring(68)) != (checksum2 % 10)) {
932             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
933                                       2, Integer.toString(checksum2 % 10), line2.substring(68), line2);
934         }
935 
936         return true;
937 
938     }
939 
940     /** Compute the checksum of the first 68 characters of a line.
941      * @param line line to check
942      * @return checksum
943      */
944     private static int checksum(final CharSequence line) {
945         int sum = 0;
946         for (int j = 0; j < 68; j++) {
947             final char c = line.charAt(j);
948             if (Character.isDigit(c)) {
949                 sum += Character.digit(c, 10);
950             } else if (c == '-') {
951                 ++sum;
952             }
953         }
954         return sum % 10;
955     }
956 
957     /** Check if this tle equals the provided tle.
958      * <p>Due to the difference in precision between object and string
959      * representations of TLE, it is possible for this method to return false
960      * even if string representations returned by {@link #toString()}
961      * are equal.</p>
962      * @param o other tle
963      * @return true if this tle equals the provided tle
964      */
965     @Override
966     public boolean equals(final Object o) {
967         if (o == this) {
968             return true;
969         }
970         if (!(o instanceof TLE)) {
971             return false;
972         }
973         final TLE tle = (TLE) o;
974         return satelliteNumber == tle.satelliteNumber &&
975                 classification == tle.classification &&
976                 launchYear == tle.launchYear &&
977                 launchNumber == tle.launchNumber &&
978                 Objects.equals(launchPiece, tle.launchPiece) &&
979                 ephemerisType == tle.ephemerisType &&
980                 elementNumber == tle.elementNumber &&
981                 Objects.equals(epoch, tle.epoch) &&
982                 meanMotion == tle.meanMotion &&
983                 meanMotionFirstDerivative == tle.meanMotionFirstDerivative &&
984                 meanMotionSecondDerivative == tle.meanMotionSecondDerivative &&
985                 eccentricity == tle.eccentricity &&
986                 inclination == tle.inclination &&
987                 pa == tle.pa &&
988                 raan == tle.raan &&
989                 meanAnomaly == tle.meanAnomaly &&
990                 revolutionNumberAtEpoch == tle.revolutionNumberAtEpoch &&
991                 getBStar() == tle.getBStar();
992     }
993 
994     /** Get a hashcode for this tle.
995      * @return hashcode
996      */
997     @Override
998     public int hashCode() {
999         return Objects.hash(satelliteNumber,
1000                 classification,
1001                 launchYear,
1002                 launchNumber,
1003                 launchPiece,
1004                 ephemerisType,
1005                 elementNumber,
1006                 epoch,
1007                 meanMotion,
1008                 meanMotionFirstDerivative,
1009                 meanMotionSecondDerivative,
1010                 eccentricity,
1011                 inclination,
1012                 pa,
1013                 raan,
1014                 meanAnomaly,
1015                 revolutionNumberAtEpoch,
1016                 getBStar());
1017     }
1018 
1019     /** Get the drivers for TLE propagation SGP4 and SDP4.
1020      * @return drivers for SGP4 and SDP4 model parameters
1021      */
1022     public List<ParameterDriver> getParametersDrivers() {
1023         return Collections.singletonList(bStarParameterDriver);
1024     }
1025 
1026     /** Get parameter driver from its name.
1027      * @param name parameter name
1028      * @return parameter driver
1029      * @since 11.1
1030      */
1031     public ParameterDriver getParameterDriver(final String name) {
1032         // Loop on known drivers
1033         for (final ParameterDriver driver : getParametersDrivers()) {
1034             if (name.equals(driver.getName())) {
1035                 // we have found a parameter with that name
1036                 return driver;
1037             }
1038         }
1039 
1040         // build the list of supported parameters
1041         final StringBuilder sBuilder = new StringBuilder();
1042         for (final ParameterDriver driver : getParametersDrivers()) {
1043             if (sBuilder.length() > 0) {
1044                 sBuilder.append(", ");
1045             }
1046             sBuilder.append(driver.getName());
1047         }
1048         throw new OrekitException(OrekitMessages.UNSUPPORTED_PARAMETER_NAME,
1049                                   name, sBuilder.toString());
1050 
1051     }
1052 
1053     /** Replace the instance with a data transfer object for serialization.
1054      * @return data transfer object that will be serialized
1055      */
1056     private Object writeReplace() {
1057         return new DataTransferObject(line1, line2, utc);
1058     }
1059 
1060     /** Internal class used only for serialization. */
1061     private static class DataTransferObject implements Serializable {
1062 
1063         /** Serializable UID. */
1064         private static final long serialVersionUID = -1596648022319057689L;
1065 
1066         /** First line. */
1067         private String line1;
1068 
1069         /** Second line. */
1070         private String line2;
1071 
1072         /** The UTC scale. */
1073         private final TimeScale utc;
1074 
1075         /** Simple constructor.
1076          * @param line1 the first element (69 char String)
1077          * @param line2 the second element (69 char String)
1078          * @param utc the UTC time scale
1079          */
1080         DataTransferObject(final String line1, final String line2, final TimeScale utc) {
1081             this.line1 = line1;
1082             this.line2 = line2;
1083             this.utc   = utc;
1084         }
1085 
1086         /** Replace the deserialized data transfer object with a {@link TLE}.
1087          * @return replacement {@link TLE}
1088          */
1089         private Object readResolve() {
1090             return new TLE(line1, line2, utc);
1091         }
1092 
1093     }
1094 
1095 }