1   /* Copyright 2002-2019 CS Systèmes d'Information
2    * Licensed to CS Systèmes d'Information (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.Locale;
23  import java.util.Objects;
24  import java.util.regex.Pattern;
25  
26  import org.hipparchus.util.ArithmeticUtils;
27  import org.hipparchus.util.FastMath;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitInternalError;
30  import org.orekit.errors.OrekitMessages;
31  import org.orekit.time.AbsoluteDate;
32  import org.orekit.time.DateComponents;
33  import org.orekit.time.DateTimeComponents;
34  import org.orekit.time.TimeComponents;
35  import org.orekit.time.TimeScalesFactory;
36  import org.orekit.time.TimeStamped;
37  
38  /** This class is a container for a single set of TLE data.
39   *
40   * <p>TLE sets can be built either by providing directly the two lines, in
41   * which case parsing is performed internally or by providing the already
42   * parsed elements.</p>
43   * <p>TLE are not transparently convertible to {@link org.orekit.orbits.Orbit Orbit}
44   * instances. They are significant only with respect to their dedicated {@link
45   * TLEPropagator propagator}, which also computes position and velocity coordinates.
46   * Any attempt to directly use orbital parameters like {@link #getE() eccentricity},
47   * {@link #getI() inclination}, etc. without any reference to the {@link TLEPropagator
48   * TLE propagator} is prone to errors.</p>
49   * <p>More information on the TLE format can be found on the
50   * <a href="http://www.celestrak.com/">CelesTrak website.</a></p>
51   * @author Fabien Maussion
52   * @author Luc Maisonobe
53   */
54  public class TLE implements TimeStamped, Serializable {
55  
56      /** Identifier for default type of ephemeris (SGP4/SDP4). */
57      public static final int DEFAULT = 0;
58  
59      /** Identifier for SGP type of ephemeris. */
60      public static final int SGP = 1;
61  
62      /** Identifier for SGP4 type of ephemeris. */
63      public static final int SGP4 = 2;
64  
65      /** Identifier for SDP4 type of ephemeris. */
66      public static final int SDP4 = 3;
67  
68      /** Identifier for SGP8 type of ephemeris. */
69      public static final int SGP8 = 4;
70  
71      /** Identifier for SDP8 type of ephemeris. */
72      public static final int SDP8 = 5;
73  
74      /** Pattern for line 1. */
75      private static final Pattern LINE_1_PATTERN =
76          Pattern.compile("1 [ 0-9]{5}[A-Z] [ 0-9]{5}[ A-Z]{3} [ 0-9]{5}[.][ 0-9]{8} (?:(?:[ 0+-][.][ 0-9]{8})|(?: [ +-][.][ 0-9]{7})) " +
77                          "[ +-][ 0-9]{5}[+-][ 0-9] [ +-][ 0-9]{5}[+-][ 0-9] [ 0-9] [ 0-9]{4}[ 0-9]");
78  
79      /** Pattern for line 2. */
80      private static final Pattern LINE_2_PATTERN =
81          Pattern.compile("2 [ 0-9]{5} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{7} " +
82                          "[ 0-9]{3}[.][ 0-9]{4} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{2}[.][ 0-9]{13}[ 0-9]");
83  
84      /** International symbols for parsing. */
85      private static final DecimalFormatSymbols SYMBOLS =
86          new DecimalFormatSymbols(Locale.US);
87  
88      /** Serializable UID. */
89      private static final long serialVersionUID = -1596648022319057689L;
90  
91      /** The satellite number. */
92      private final int satelliteNumber;
93  
94      /** Classification (U for unclassified). */
95      private final char classification;
96  
97      /** Launch year. */
98      private final int launchYear;
99  
100     /** Launch number. */
101     private final int launchNumber;
102 
103     /** Piece of launch (from "A" to "ZZZ"). */
104     private final String launchPiece;
105 
106     /** Type of ephemeris. */
107     private final int ephemerisType;
108 
109     /** Element number. */
110     private final int elementNumber;
111 
112     /** the TLE current date. */
113     private final AbsoluteDate epoch;
114 
115     /** Mean motion (rad/s). */
116     private final double meanMotion;
117 
118     /** Mean motion first derivative (rad/s²). */
119     private final double meanMotionFirstDerivative;
120 
121     /** Mean motion second derivative (rad/s³). */
122     private final double meanMotionSecondDerivative;
123 
124     /** Eccentricity. */
125     private final double eccentricity;
126 
127     /** Inclination (rad). */
128     private final double inclination;
129 
130     /** Argument of perigee (rad). */
131     private final double pa;
132 
133     /** Right Ascension of the Ascending node (rad). */
134     private final double raan;
135 
136     /** Mean anomaly (rad). */
137     private final double meanAnomaly;
138 
139     /** Revolution number at epoch. */
140     private final int revolutionNumberAtEpoch;
141 
142     /** Ballistic coefficient. */
143     private final double bStar;
144 
145     /** First line. */
146     private String line1;
147 
148     /** Second line. */
149     private String line2;
150 
151     /** Simple constructor from unparsed two lines.
152      * <p>The static method {@link #isFormatOK(String, String)} should be called
153      * before trying to build this object.<p>
154      * @param line1 the first element (69 char String)
155      * @param line2 the second element (69 char String)
156      */
157     public TLE(final String line1, final String line2) {
158 
159         // identification
160         satelliteNumber = parseInteger(line1, 2, 5);
161         final int satNum2 = parseInteger(line2, 2, 5);
162         if (satelliteNumber != satNum2) {
163             throw new OrekitException(OrekitMessages.TLE_LINES_DO_NOT_REFER_TO_SAME_OBJECT,
164                                       line1, line2);
165         }
166         classification  = line1.charAt(7);
167         launchYear      = parseYear(line1, 9);
168         launchNumber    = parseInteger(line1, 11, 3);
169         launchPiece     = line1.substring(14, 17).trim();
170         ephemerisType   = parseInteger(line1, 62, 1);
171         elementNumber   = parseInteger(line1, 64, 4);
172 
173         // Date format transform (nota: 27/31250 == 86400/100000000)
174         final int    year      = parseYear(line1, 18);
175         final int    dayInYear = parseInteger(line1, 20, 3);
176         final long   df        = 27l * parseInteger(line1, 24, 8);
177         final int    secondsA  = (int) (df / 31250l);
178         final double secondsB  = (df % 31250l) / 31250.0;
179         epoch = new AbsoluteDate(new DateComponents(year, dayInYear),
180                                  new TimeComponents(secondsA, secondsB),
181                                  TimeScalesFactory.getUTC());
182 
183         // mean motion development
184         // converted from rev/day, 2 * rev/day^2 and 6 * rev/day^3 to rad/s, rad/s^2 and rad/s^3
185         meanMotion                 = parseDouble(line2, 52, 11) * FastMath.PI / 43200.0;
186         meanMotionFirstDerivative  = parseDouble(line1, 33, 10) * FastMath.PI / 1.86624e9;
187         meanMotionSecondDerivative = Double.parseDouble((line1.substring(44, 45) + '.' +
188                                                          line1.substring(45, 50) + 'e' +
189                                                          line1.substring(50, 52)).replace(' ', '0')) *
190                                      FastMath.PI / 5.3747712e13;
191 
192         eccentricity = Double.parseDouble("." + line2.substring(26, 33).replace(' ', '0'));
193         inclination  = FastMath.toRadians(parseDouble(line2, 8, 8));
194         pa           = FastMath.toRadians(parseDouble(line2, 34, 8));
195         raan         = FastMath.toRadians(Double.parseDouble(line2.substring(17, 25).replace(' ', '0')));
196         meanAnomaly  = FastMath.toRadians(parseDouble(line2, 43, 8));
197 
198         revolutionNumberAtEpoch = parseInteger(line2, 63, 5);
199         bStar = Double.parseDouble((line1.substring(53, 54) + '.' +
200                                     line1.substring(54, 59) + 'e' +
201                                     line1.substring(59, 61)).replace(' ', '0'));
202 
203         // save the lines
204         this.line1 = line1;
205         this.line2 = line2;
206 
207     }
208 
209     /** Simple constructor from already parsed elements.
210      * @param satelliteNumber satellite number
211      * @param classification classification (U for unclassified)
212      * @param launchYear launch year (all digits)
213      * @param launchNumber launch number
214      * @param launchPiece launch piece
215      * @param ephemerisType type of ephemeris
216      * @param elementNumber element number
217      * @param epoch elements epoch
218      * @param meanMotion mean motion (rad/s)
219      * @param meanMotionFirstDerivative mean motion first derivative (rad/s²)
220      * @param meanMotionSecondDerivative mean motion second derivative (rad/s³)
221      * @param e eccentricity
222      * @param i inclination (rad)
223      * @param pa argument of perigee (rad)
224      * @param raan right ascension of ascending node (rad)
225      * @param meanAnomaly mean anomaly (rad)
226      * @param revolutionNumberAtEpoch revolution number at epoch
227      * @param bStar ballistic coefficient
228      */
229     public TLE(final int satelliteNumber, final char classification,
230                final int launchYear, final int launchNumber, final String launchPiece,
231                final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
232                final double meanMotion, final double meanMotionFirstDerivative,
233                final double meanMotionSecondDerivative, final double e, final double i,
234                final double pa, final double raan, final double meanAnomaly,
235                final int revolutionNumberAtEpoch, final double bStar) {
236 
237         // identification
238         this.satelliteNumber = satelliteNumber;
239         this.classification  = classification;
240         this.launchYear      = launchYear;
241         this.launchNumber    = launchNumber;
242         this.launchPiece     = launchPiece;
243         this.ephemerisType   = ephemerisType;
244         this.elementNumber   = elementNumber;
245 
246         // orbital parameters
247         this.epoch                      = epoch;
248         this.meanMotion                 = meanMotion;
249         this.meanMotionFirstDerivative  = meanMotionFirstDerivative;
250         this.meanMotionSecondDerivative = meanMotionSecondDerivative;
251         this.inclination                = i;
252         this.raan                       = raan;
253         this.eccentricity               = e;
254         this.pa                         = pa;
255         this.meanAnomaly                = meanAnomaly;
256 
257         this.revolutionNumberAtEpoch = revolutionNumberAtEpoch;
258         this.bStar                   = bStar;
259 
260         // don't build the line until really needed
261         this.line1 = null;
262         this.line2 = null;
263 
264     }
265 
266     /** Get the first line.
267      * @return first line
268      */
269     public String getLine1() {
270         if (line1 == null) {
271             buildLine1();
272         }
273         return line1;
274     }
275 
276     /** Get the second line.
277      * @return second line
278      */
279     public String getLine2() {
280         if (line2 == null) {
281             buildLine2();
282         }
283         return line2;
284     }
285 
286     /** Build the line 1 from the parsed elements.
287      */
288     private void buildLine1() {
289 
290         final StringBuffer buffer = new StringBuffer();
291 
292         buffer.append('1');
293 
294         buffer.append(' ');
295         buffer.append(addPadding("satelliteNumber-1", satelliteNumber, '0', 5, true));
296         buffer.append(classification);
297 
298         buffer.append(' ');
299         buffer.append(addPadding("launchYear",   launchYear % 100, '0', 2, true));
300         buffer.append(addPadding("launchNumber", launchNumber, '0', 3, true));
301         buffer.append(addPadding("launchPiece",  launchPiece, ' ', 3, false));
302 
303         buffer.append(' ');
304         final DateTimeComponents dtc = epoch.getComponents(TimeScalesFactory.getUTC());
305         buffer.append(addPadding("year", dtc.getDate().getYear() % 100, '0', 2, true));
306         buffer.append(addPadding("day",  dtc.getDate().getDayOfYear(),  '0', 3, true));
307         buffer.append('.');
308         // nota: 31250/27 == 100000000/86400
309         final int fraction = (int) FastMath.rint(31250 * dtc.getTime().getSecondsInUTCDay() / 27.0);
310         buffer.append(addPadding("fraction", fraction,  '0', 8, true));
311 
312         buffer.append(' ');
313         final double n1 = meanMotionFirstDerivative * 1.86624e9 / FastMath.PI;
314         final String sn1 = addPadding("meanMotionFirstDerivative",
315                                       new DecimalFormat(".00000000", SYMBOLS).format(n1), ' ', 10, true);
316         buffer.append(sn1);
317 
318         buffer.append(' ');
319         final double n2 = meanMotionSecondDerivative * 5.3747712e13 / FastMath.PI;
320         buffer.append(formatExponentMarkerFree("meanMotionSecondDerivative", n2, 5, ' ', 8, true));
321 
322         buffer.append(' ');
323         buffer.append(formatExponentMarkerFree("B*", bStar, 5, ' ', 8, true));
324 
325         buffer.append(' ');
326         buffer.append(ephemerisType);
327 
328         buffer.append(' ');
329         buffer.append(addPadding("elementNumber", elementNumber, ' ', 4, true));
330 
331         buffer.append(Integer.toString(checksum(buffer)));
332 
333         line1 = buffer.toString();
334 
335     }
336 
337     /** Format a real number without 'e' exponent marker.
338      * @param name parameter name
339      * @param d number to format
340      * @param mantissaSize size of the mantissa (not counting initial '-' or ' ' for sign)
341      * @param c padding character
342      * @param size desired size
343      * @param rightJustified if true, the resulting string is
344      * right justified (i.e. space are added to the left)
345      * @return formatted and padded number
346      */
347     private String formatExponentMarkerFree(final String name, final double d, final int mantissaSize,
348                                             final char c, final int size, final boolean rightJustified) {
349         final double dAbs = FastMath.abs(d);
350         int exponent = (dAbs < 1.0e-9) ? -9 : (int) FastMath.ceil(FastMath.log10(dAbs));
351         long mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
352         if (mantissa == 0) {
353             exponent = 0;
354         } else if (mantissa > (ArithmeticUtils.pow(10, mantissaSize) - 1)) {
355             // rare case: if d has a single digit like d = 1.0e-4 with mantissaSize = 5
356             // the above computation finds exponent = -4 and mantissa = 100000 which
357             // doesn't fit in a 5 digits string
358             exponent++;
359             mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
360         }
361         final String sMantissa = addPadding(name, (int) mantissa, '0', mantissaSize, true);
362         final String sExponent = Integer.toString(FastMath.abs(exponent));
363         final String formatted = (d <  0 ? '-' : ' ') + sMantissa + (exponent <= 0 ? '-' : '+') + sExponent;
364 
365         return addPadding(name, formatted, c, size, rightJustified);
366 
367     }
368 
369     /** Build the line 2 from the parsed elements.
370      */
371     private void buildLine2() {
372 
373         final StringBuffer buffer = new StringBuffer();
374         final DecimalFormat f34   = new DecimalFormat("##0.0000", SYMBOLS);
375         final DecimalFormat f211  = new DecimalFormat("#0.00000000", SYMBOLS);
376 
377         buffer.append('2');
378 
379         buffer.append(' ');
380         buffer.append(addPadding("satelliteNumber-2", satelliteNumber, '0', 5, true));
381 
382         buffer.append(' ');
383         buffer.append(addPadding("inclination", f34.format(FastMath.toDegrees(inclination)), ' ', 8, true));
384         buffer.append(' ');
385         buffer.append(addPadding("raan", f34.format(FastMath.toDegrees(raan)), ' ', 8, true));
386         buffer.append(' ');
387         buffer.append(addPadding("eccentricity", (int) FastMath.rint(eccentricity * 1.0e7), '0', 7, true));
388         buffer.append(' ');
389         buffer.append(addPadding("pa", f34.format(FastMath.toDegrees(pa)), ' ', 8, true));
390         buffer.append(' ');
391         buffer.append(addPadding("meanAnomaly", f34.format(FastMath.toDegrees(meanAnomaly)), ' ', 8, true));
392 
393         buffer.append(' ');
394         buffer.append(addPadding("meanMotion", f211.format(meanMotion * 43200.0 / FastMath.PI), ' ', 11, true));
395         buffer.append(addPadding("revolutionNumberAtEpoch", revolutionNumberAtEpoch, ' ', 5, true));
396 
397         buffer.append(Integer.toString(checksum(buffer)));
398 
399         line2 = buffer.toString();
400 
401     }
402 
403     /** Add padding characters before an integer.
404      * @param name parameter name
405      * @param k integer to pad
406      * @param c padding character
407      * @param size desired size
408      * @param rightJustified if true, the resulting string is
409      * right justified (i.e. space are added to the left)
410      * @return padded string
411      */
412     private String addPadding(final String name, final int k, final char c,
413                               final int size, final boolean rightJustified) {
414         return addPadding(name, Integer.toString(k), c, size, rightJustified);
415     }
416 
417     /** Add padding characters to a string.
418      * @param name parameter name
419      * @param string string to pad
420      * @param c padding character
421      * @param size desired size
422      * @param rightJustified if true, the resulting string is
423      * right justified (i.e. space are added to the left)
424      * @return padded string
425      */
426     private String addPadding(final String name, final String string, final char c,
427                               final int size, final boolean rightJustified) {
428 
429         if (string.length() > size) {
430             throw new OrekitException(OrekitMessages.TLE_INVALID_PARAMETER,
431                                       satelliteNumber, name, string);
432         }
433 
434         final StringBuffer padding = new StringBuffer();
435         for (int i = 0; i < size; ++i) {
436             padding.append(c);
437         }
438 
439         if (rightJustified) {
440             final String concatenated = padding + string;
441             final int l = concatenated.length();
442             return concatenated.substring(l - size, l);
443         }
444 
445         return (string + padding).substring(0, size);
446 
447     }
448 
449     /** Parse a double.
450      * @param line line to parse
451      * @param start start index of the first character
452      * @param length length of the string
453      * @return value of the double
454      */
455     private double parseDouble(final String line, final int start, final int length) {
456         final String field = line.substring(start, start + length).trim();
457         return field.length() > 0 ? Double.parseDouble(field.replace(' ', '0')) : 0;
458     }
459 
460     /** Parse an integer.
461      * @param line line to parse
462      * @param start start index of the first character
463      * @param length length of the string
464      * @return value of the integer
465      */
466     private int parseInteger(final String line, final int start, final int length) {
467         final String field = line.substring(start, start + length).trim();
468         return field.length() > 0 ? Integer.parseInt(field.replace(' ', '0')) : 0;
469     }
470 
471     /** Parse a year written on 2 digits.
472      * @param line line to parse
473      * @param start start index of the first character
474      * @return value of the year
475      */
476     private int parseYear(final String line, final int start) {
477         final int year = 2000 + parseInteger(line, start, 2);
478         return (year > 2056) ? (year - 100) : year;
479     }
480 
481     /** Get the satellite id.
482      * @return the satellite number
483      */
484     public int getSatelliteNumber() {
485         return satelliteNumber;
486     }
487 
488     /** Get the classification.
489      * @return classification
490      */
491     public char getClassification() {
492         return classification;
493     }
494 
495     /** Get the launch year.
496      * @return the launch year
497      */
498     public int getLaunchYear() {
499         return launchYear;
500     }
501 
502     /** Get the launch number.
503      * @return the launch number
504      */
505     public int getLaunchNumber() {
506         return launchNumber;
507     }
508 
509     /** Get the launch piece.
510      * @return the launch piece
511      */
512     public String getLaunchPiece() {
513         return launchPiece;
514     }
515 
516     /** Get the type of ephemeris.
517      * @return the ephemeris type (one of {@link #DEFAULT}, {@link #SGP},
518      * {@link #SGP4}, {@link #SGP8}, {@link #SDP4}, {@link #SDP8})
519      */
520     public int getEphemerisType() {
521         return ephemerisType;
522     }
523 
524     /** Get the element number.
525      * @return the element number
526      */
527     public int getElementNumber() {
528         return elementNumber;
529     }
530 
531     /** Get the TLE current date.
532      * @return the epoch
533      */
534     public AbsoluteDate getDate() {
535         return epoch;
536     }
537 
538     /** Get the mean motion.
539      * @return the mean motion (rad/s)
540      */
541     public double getMeanMotion() {
542         return meanMotion;
543     }
544 
545     /** Get the mean motion first derivative.
546      * @return the mean motion first derivative (rad/s²)
547      */
548     public double getMeanMotionFirstDerivative() {
549         return meanMotionFirstDerivative;
550     }
551 
552     /** Get the mean motion second derivative.
553      * @return the mean motion second derivative (rad/s³)
554      */
555     public double getMeanMotionSecondDerivative() {
556         return meanMotionSecondDerivative;
557     }
558 
559     /** Get the eccentricity.
560      * @return the eccentricity
561      */
562     public double getE() {
563         return eccentricity;
564     }
565 
566     /** Get the inclination.
567      * @return the inclination (rad)
568      */
569     public double getI() {
570         return inclination;
571     }
572 
573     /** Get the argument of perigee.
574      * @return omega (rad)
575      */
576     public double getPerigeeArgument() {
577         return pa;
578     }
579 
580     /** Get Right Ascension of the Ascending node.
581      * @return the raan (rad)
582      */
583     public double getRaan() {
584         return raan;
585     }
586 
587     /** Get the mean anomaly.
588      * @return the mean anomaly (rad)
589      */
590     public double getMeanAnomaly() {
591         return meanAnomaly;
592     }
593 
594     /** Get the revolution number.
595      * @return the revolutionNumberAtEpoch
596      */
597     public int getRevolutionNumberAtEpoch() {
598         return revolutionNumberAtEpoch;
599     }
600 
601     /** Get the ballistic coefficient.
602      * @return bStar
603      */
604     public double getBStar() {
605         return bStar;
606     }
607 
608     /** Get a string representation of this TLE set.
609      * <p>The representation is simply the two lines separated by the
610      * platform line separator.</p>
611      * @return string representation of this TLE set
612      */
613     public String toString() {
614         try {
615             return getLine1() + System.getProperty("line.separator") + getLine2();
616         } catch (OrekitException oe) {
617             throw new OrekitInternalError(oe);
618         }
619     }
620 
621     /** Check the lines format validity.
622      * @param line1 the first element
623      * @param line2 the second element
624      * @return true if format is recognized (non null lines, 69 characters length,
625      * line content), false if not
626      */
627     public static boolean isFormatOK(final String line1, final String line2) {
628 
629         if (line1 == null || line1.length() != 69 ||
630             line2 == null || line2.length() != 69) {
631             return false;
632         }
633 
634         if (!(LINE_1_PATTERN.matcher(line1).matches() &&
635               LINE_2_PATTERN.matcher(line2).matches())) {
636             return false;
637         }
638 
639         // check sums
640         final int checksum1 = checksum(line1);
641         if (Integer.parseInt(line1.substring(68)) != (checksum1 % 10)) {
642             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
643                                       1, Integer.toString(checksum1 % 10), line1.substring(68), line1);
644         }
645 
646         final int checksum2 = checksum(line2);
647         if (Integer.parseInt(line2.substring(68)) != (checksum2 % 10)) {
648             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
649                                       2, Integer.toString(checksum2 % 10), line2.substring(68), line2);
650         }
651 
652         return true;
653 
654     }
655 
656     /** Compute the checksum of the first 68 characters of a line.
657      * @param line line to check
658      * @return checksum
659      */
660     private static int checksum(final CharSequence line) {
661         int sum = 0;
662         for (int j = 0; j < 68; j++) {
663             final char c = line.charAt(j);
664             if (Character.isDigit(c)) {
665                 sum += Character.digit(c, 10);
666             } else if (c == '-') {
667                 ++sum;
668             }
669         }
670         return sum % 10;
671     }
672 
673     /** Check if this tle equals the provided tle.
674      * <p>Due to the difference in precision between object and string
675      * representations of TLE, it is possible for this method to return false
676      * even if string representations returned by {@link #toString()}
677      * are equal.</p>
678      * @param o other tle
679      * @return true if this tle equals the provided tle
680      */
681     @Override
682     public boolean equals(final Object o) {
683         if (o == this) {
684             return true;
685         }
686         if (!(o instanceof TLE)) {
687             return false;
688         }
689         final TLEhref="../../../../../org/orekit/propagation/analytical/tle/TLE.html#TLE">TLE tle = (TLE) o;
690         return satelliteNumber == tle.satelliteNumber &&
691                 classification == tle.classification &&
692                 launchYear == tle.launchYear &&
693                 launchNumber == tle.launchNumber &&
694                 Objects.equals(launchPiece, tle.launchPiece) &&
695                 ephemerisType == tle.ephemerisType &&
696                 elementNumber == tle.elementNumber &&
697                 Objects.equals(epoch, tle.epoch) &&
698                 meanMotion == tle.meanMotion &&
699                 meanMotionFirstDerivative == tle.meanMotionFirstDerivative &&
700                 meanMotionSecondDerivative == tle.meanMotionSecondDerivative &&
701                 eccentricity == tle.eccentricity &&
702                 inclination == tle.inclination &&
703                 pa == tle.pa &&
704                 raan == tle.raan &&
705                 meanAnomaly == tle.meanAnomaly &&
706                 revolutionNumberAtEpoch == tle.revolutionNumberAtEpoch &&
707                 bStar == tle.bStar;
708     }
709 
710     /** Get a hashcode for this tle.
711      * @return hashcode
712      */
713     @Override
714     public int hashCode() {
715         return Objects.hash(satelliteNumber,
716                 classification,
717                 launchYear,
718                 launchNumber,
719                 launchPiece,
720                 ephemerisType,
721                 elementNumber,
722                 epoch,
723                 meanMotion,
724                 meanMotionFirstDerivative,
725                 meanMotionSecondDerivative,
726                 eccentricity,
727                 inclination,
728                 pa,
729                 raan,
730                 meanAnomaly,
731                 revolutionNumberAtEpoch,
732                 bStar);
733     }
734 
735 }