1   /* Copyright 2002-2018 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      * @exception OrekitException if some format error occurs or lines are inconsistent
157      */
158     public TLE(final String line1, final String line2) throws OrekitException {
159 
160         // identification
161         satelliteNumber = parseInteger(line1, 2, 5);
162         final int satNum2 = parseInteger(line2, 2, 5);
163         if (satelliteNumber != satNum2) {
164             throw new OrekitException(OrekitMessages.TLE_LINES_DO_NOT_REFER_TO_SAME_OBJECT,
165                                       line1, line2);
166         }
167         classification  = line1.charAt(7);
168         launchYear      = parseYear(line1, 9);
169         launchNumber    = parseInteger(line1, 11, 3);
170         launchPiece     = line1.substring(14, 17).trim();
171         ephemerisType   = parseInteger(line1, 62, 1);
172         elementNumber   = parseInteger(line1, 64, 4);
173 
174         // Date format transform (nota: 27/31250 == 86400/100000000)
175         final int    year      = parseYear(line1, 18);
176         final int    dayInYear = parseInteger(line1, 20, 3);
177         final long   df        = 27l * parseInteger(line1, 24, 8);
178         final int    secondsA  = (int) (df / 31250l);
179         final double secondsB  = (df % 31250l) / 31250.0;
180         epoch = new AbsoluteDate(new DateComponents(year, dayInYear),
181                                  new TimeComponents(secondsA, secondsB),
182                                  TimeScalesFactory.getUTC());
183 
184         // mean motion development
185         // converted from rev/day, 2 * rev/day^2 and 6 * rev/day^3 to rad/s, rad/s^2 and rad/s^3
186         meanMotion                 = parseDouble(line2, 52, 11) * FastMath.PI / 43200.0;
187         meanMotionFirstDerivative  = parseDouble(line1, 33, 10) * FastMath.PI / 1.86624e9;
188         meanMotionSecondDerivative = Double.parseDouble((line1.substring(44, 45) + '.' +
189                                                          line1.substring(45, 50) + 'e' +
190                                                          line1.substring(50, 52)).replace(' ', '0')) *
191                                      FastMath.PI / 5.3747712e13;
192 
193         eccentricity = Double.parseDouble("." + line2.substring(26, 33).replace(' ', '0'));
194         inclination  = FastMath.toRadians(parseDouble(line2, 8, 8));
195         pa           = FastMath.toRadians(parseDouble(line2, 34, 8));
196         raan         = FastMath.toRadians(Double.parseDouble(line2.substring(17, 25).replace(' ', '0')));
197         meanAnomaly  = FastMath.toRadians(parseDouble(line2, 43, 8));
198 
199         revolutionNumberAtEpoch = parseInteger(line2, 63, 5);
200         bStar = Double.parseDouble((line1.substring(53, 54) + '.' +
201                                     line1.substring(54, 59) + 'e' +
202                                     line1.substring(59, 61)).replace(' ', '0'));
203 
204         // save the lines
205         this.line1 = line1;
206         this.line2 = line2;
207 
208     }
209 
210     /** Simple constructor from already parsed elements.
211      * @param satelliteNumber satellite number
212      * @param classification classification (U for unclassified)
213      * @param launchYear launch year (all digits)
214      * @param launchNumber launch number
215      * @param launchPiece launch piece
216      * @param ephemerisType type of ephemeris
217      * @param elementNumber element number
218      * @param epoch elements epoch
219      * @param meanMotion mean motion (rad/s)
220      * @param meanMotionFirstDerivative mean motion first derivative (rad/s²)
221      * @param meanMotionSecondDerivative mean motion second derivative (rad/s³)
222      * @param e eccentricity
223      * @param i inclination (rad)
224      * @param pa argument of perigee (rad)
225      * @param raan right ascension of ascending node (rad)
226      * @param meanAnomaly mean anomaly (rad)
227      * @param revolutionNumberAtEpoch revolution number at epoch
228      * @param bStar ballistic coefficient
229      */
230     public TLE(final int satelliteNumber, final char classification,
231                final int launchYear, final int launchNumber, final String launchPiece,
232                final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
233                final double meanMotion, final double meanMotionFirstDerivative,
234                final double meanMotionSecondDerivative, final double e, final double i,
235                final double pa, final double raan, final double meanAnomaly,
236                final int revolutionNumberAtEpoch, final double bStar) {
237 
238         // identification
239         this.satelliteNumber = satelliteNumber;
240         this.classification  = classification;
241         this.launchYear      = launchYear;
242         this.launchNumber    = launchNumber;
243         this.launchPiece     = launchPiece;
244         this.ephemerisType   = ephemerisType;
245         this.elementNumber   = elementNumber;
246 
247         // orbital parameters
248         this.epoch                      = epoch;
249         this.meanMotion                 = meanMotion;
250         this.meanMotionFirstDerivative  = meanMotionFirstDerivative;
251         this.meanMotionSecondDerivative = meanMotionSecondDerivative;
252         this.inclination                = i;
253         this.raan                       = raan;
254         this.eccentricity               = e;
255         this.pa                         = pa;
256         this.meanAnomaly                = meanAnomaly;
257 
258         this.revolutionNumberAtEpoch = revolutionNumberAtEpoch;
259         this.bStar                   = bStar;
260 
261         // don't build the line until really needed
262         this.line1 = null;
263         this.line2 = null;
264 
265     }
266 
267     /** Get the first line.
268      * @return first line
269      * @exception OrekitException if UTC conversion cannot be done or
270      * some parameter is too large to fit format
271      */
272     public String getLine1()
273         throws OrekitException {
274         if (line1 == null) {
275             buildLine1();
276         }
277         return line1;
278     }
279 
280     /** Get the second line.
281      * @return second line
282      * @exception OrekitException if some parameter is too large to fit format
283      */
284     public String getLine2()
285         throws OrekitException {
286         if (line2 == null) {
287             buildLine2();
288         }
289         return line2;
290     }
291 
292     /** Build the line 1 from the parsed elements.
293      * @exception OrekitException if UTC conversion cannot be done or
294      * some parameter is too large to fit format
295      */
296     private void buildLine1()
297         throws OrekitException {
298 
299         final StringBuffer buffer = new StringBuffer();
300 
301         buffer.append('1');
302 
303         buffer.append(' ');
304         buffer.append(addPadding("satelliteNumber-1", satelliteNumber, '0', 5, true));
305         buffer.append(classification);
306 
307         buffer.append(' ');
308         buffer.append(addPadding("launchYear",   launchYear % 100, '0', 2, true));
309         buffer.append(addPadding("launchNumber", launchNumber, '0', 3, true));
310         buffer.append(addPadding("launchPiece",  launchPiece, ' ', 3, false));
311 
312         buffer.append(' ');
313         final DateTimeComponents dtc = epoch.getComponents(TimeScalesFactory.getUTC());
314         buffer.append(addPadding("year", dtc.getDate().getYear() % 100, '0', 2, true));
315         buffer.append(addPadding("day",  dtc.getDate().getDayOfYear(),  '0', 3, true));
316         buffer.append('.');
317         // nota: 31250/27 == 100000000/86400
318         final int fraction = (int) FastMath.rint(31250 * dtc.getTime().getSecondsInUTCDay() / 27.0);
319         buffer.append(addPadding("fraction", fraction,  '0', 8, true));
320 
321         buffer.append(' ');
322         final double n1 = meanMotionFirstDerivative * 1.86624e9 / FastMath.PI;
323         final String sn1 = addPadding("meanMotionFirstDerivative",
324                                       new DecimalFormat(".00000000", SYMBOLS).format(n1), ' ', 10, true);
325         buffer.append(sn1);
326 
327         buffer.append(' ');
328         final double n2 = meanMotionSecondDerivative * 5.3747712e13 / FastMath.PI;
329         buffer.append(formatExponentMarkerFree("meanMotionSecondDerivative", n2, 5, ' ', 8, true));
330 
331         buffer.append(' ');
332         buffer.append(formatExponentMarkerFree("B*", bStar, 5, ' ', 8, true));
333 
334         buffer.append(' ');
335         buffer.append(ephemerisType);
336 
337         buffer.append(' ');
338         buffer.append(addPadding("elementNumber", elementNumber, ' ', 4, true));
339 
340         buffer.append(Integer.toString(checksum(buffer)));
341 
342         line1 = buffer.toString();
343 
344     }
345 
346     /** Format a real number without 'e' exponent marker.
347      * @param name parameter name
348      * @param d number to format
349      * @param mantissaSize size of the mantissa (not counting initial '-' or ' ' for sign)
350      * @param c padding character
351      * @param size desired size
352      * @param rightJustified if true, the resulting string is
353      * right justified (i.e. space are added to the left)
354      * @return formatted and padded number
355      * @exception OrekitException if parameter is too large to fit format
356      */
357     private String formatExponentMarkerFree(final String name, final double d, final int mantissaSize,
358                                             final char c, final int size, final boolean rightJustified)
359         throws OrekitException {
360         final double dAbs = FastMath.abs(d);
361         int exponent = (dAbs < 1.0e-9) ? -9 : (int) FastMath.ceil(FastMath.log10(dAbs));
362         long mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
363         if (mantissa == 0) {
364             exponent = 0;
365         } else if (mantissa > (ArithmeticUtils.pow(10, mantissaSize) - 1)) {
366             // rare case: if d has a single digit like d = 1.0e-4 with mantissaSize = 5
367             // the above computation finds exponent = -4 and mantissa = 100000 which
368             // doesn't fit in a 5 digits string
369             exponent++;
370             mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
371         }
372         final String sMantissa = addPadding(name, (int) mantissa, '0', mantissaSize, true);
373         final String sExponent = Integer.toString(FastMath.abs(exponent));
374         final String formatted = (d <  0 ? '-' : ' ') + sMantissa + (exponent <= 0 ? '-' : '+') + sExponent;
375 
376         return addPadding(name, formatted, c, size, rightJustified);
377 
378     }
379 
380     /** Build the line 2 from the parsed elements.
381      * @exception OrekitException if some parameter is too large to fit format
382      */
383     private void buildLine2() throws OrekitException {
384 
385         final StringBuffer buffer = new StringBuffer();
386         final DecimalFormat f34   = new DecimalFormat("##0.0000", SYMBOLS);
387         final DecimalFormat f211  = new DecimalFormat("#0.00000000", SYMBOLS);
388 
389         buffer.append('2');
390 
391         buffer.append(' ');
392         buffer.append(addPadding("satelliteNumber-2", satelliteNumber, '0', 5, true));
393 
394         buffer.append(' ');
395         buffer.append(addPadding("inclination", f34.format(FastMath.toDegrees(inclination)), ' ', 8, true));
396         buffer.append(' ');
397         buffer.append(addPadding("raan", f34.format(FastMath.toDegrees(raan)), ' ', 8, true));
398         buffer.append(' ');
399         buffer.append(addPadding("eccentricity", (int) FastMath.rint(eccentricity * 1.0e7), '0', 7, true));
400         buffer.append(' ');
401         buffer.append(addPadding("pa", f34.format(FastMath.toDegrees(pa)), ' ', 8, true));
402         buffer.append(' ');
403         buffer.append(addPadding("meanAnomaly", f34.format(FastMath.toDegrees(meanAnomaly)), ' ', 8, true));
404 
405         buffer.append(' ');
406         buffer.append(addPadding("meanMotion", f211.format(meanMotion * 43200.0 / FastMath.PI), ' ', 11, true));
407         buffer.append(addPadding("revolutionNumberAtEpoch", revolutionNumberAtEpoch, ' ', 5, true));
408 
409         buffer.append(Integer.toString(checksum(buffer)));
410 
411         line2 = buffer.toString();
412 
413     }
414 
415     /** Add padding characters before an integer.
416      * @param name parameter name
417      * @param k integer to pad
418      * @param c padding character
419      * @param size desired size
420      * @param rightJustified if true, the resulting string is
421      * right justified (i.e. space are added to the left)
422      * @return padded string
423      * @exception OrekitException if parameter is too large to fit format
424      */
425     private String addPadding(final String name, final int k, final char c,
426                               final int size, final boolean rightJustified)
427         throws OrekitException {
428         return addPadding(name, Integer.toString(k), c, size, rightJustified);
429     }
430 
431     /** Add padding characters to a string.
432      * @param name parameter name
433      * @param string string to pad
434      * @param c padding character
435      * @param size desired size
436      * @param rightJustified if true, the resulting string is
437      * right justified (i.e. space are added to the left)
438      * @return padded string
439      * @exception OrekitException if parameter is too large to fit format
440      */
441     private String addPadding(final String name, final String string, final char c,
442                               final int size, final boolean rightJustified)
443         throws OrekitException {
444 
445         if (string.length() > size) {
446             throw new OrekitException(OrekitMessages.TLE_INVALID_PARAMETER,
447                                       satelliteNumber, name, string);
448         }
449 
450         final StringBuffer padding = new StringBuffer();
451         for (int i = 0; i < size; ++i) {
452             padding.append(c);
453         }
454 
455         if (rightJustified) {
456             final String concatenated = padding + string;
457             final int l = concatenated.length();
458             return concatenated.substring(l - size, l);
459         }
460 
461         return (string + padding).substring(0, size);
462 
463     }
464 
465     /** Parse a double.
466      * @param line line to parse
467      * @param start start index of the first character
468      * @param length length of the string
469      * @return value of the double
470      */
471     private double parseDouble(final String line, final int start, final int length) {
472         final String field = line.substring(start, start + length).trim();
473         return field.length() > 0 ? Double.parseDouble(field.replace(' ', '0')) : 0;
474     }
475 
476     /** Parse an integer.
477      * @param line line to parse
478      * @param start start index of the first character
479      * @param length length of the string
480      * @return value of the integer
481      */
482     private int parseInteger(final String line, final int start, final int length) {
483         final String field = line.substring(start, start + length).trim();
484         return field.length() > 0 ? Integer.parseInt(field.replace(' ', '0')) : 0;
485     }
486 
487     /** Parse a year written on 2 digits.
488      * @param line line to parse
489      * @param start start index of the first character
490      * @return value of the year
491      */
492     private int parseYear(final String line, final int start) {
493         final int year = 2000 + parseInteger(line, start, 2);
494         return (year > 2056) ? (year - 100) : year;
495     }
496 
497     /** Get the satellite id.
498      * @return the satellite number
499      */
500     public int getSatelliteNumber() {
501         return satelliteNumber;
502     }
503 
504     /** Get the classification.
505      * @return classification
506      */
507     public char getClassification() {
508         return classification;
509     }
510 
511     /** Get the launch year.
512      * @return the launch year
513      */
514     public int getLaunchYear() {
515         return launchYear;
516     }
517 
518     /** Get the launch number.
519      * @return the launch number
520      */
521     public int getLaunchNumber() {
522         return launchNumber;
523     }
524 
525     /** Get the launch piece.
526      * @return the launch piece
527      */
528     public String getLaunchPiece() {
529         return launchPiece;
530     }
531 
532     /** Get the type of ephemeris.
533      * @return the ephemeris type (one of {@link #DEFAULT}, {@link #SGP},
534      * {@link #SGP4}, {@link #SGP8}, {@link #SDP4}, {@link #SDP8})
535      */
536     public int getEphemerisType() {
537         return ephemerisType;
538     }
539 
540     /** Get the element number.
541      * @return the element number
542      */
543     public int getElementNumber() {
544         return elementNumber;
545     }
546 
547     /** Get the TLE current date.
548      * @return the epoch
549      */
550     public AbsoluteDate getDate() {
551         return epoch;
552     }
553 
554     /** Get the mean motion.
555      * @return the mean motion (rad/s)
556      */
557     public double getMeanMotion() {
558         return meanMotion;
559     }
560 
561     /** Get the mean motion first derivative.
562      * @return the mean motion first derivative (rad/s²)
563      */
564     public double getMeanMotionFirstDerivative() {
565         return meanMotionFirstDerivative;
566     }
567 
568     /** Get the mean motion second derivative.
569      * @return the mean motion second derivative (rad/s³)
570      */
571     public double getMeanMotionSecondDerivative() {
572         return meanMotionSecondDerivative;
573     }
574 
575     /** Get the eccentricity.
576      * @return the eccentricity
577      */
578     public double getE() {
579         return eccentricity;
580     }
581 
582     /** Get the inclination.
583      * @return the inclination (rad)
584      */
585     public double getI() {
586         return inclination;
587     }
588 
589     /** Get the argument of perigee.
590      * @return omega (rad)
591      */
592     public double getPerigeeArgument() {
593         return pa;
594     }
595 
596     /** Get Right Ascension of the Ascending node.
597      * @return the raan (rad)
598      */
599     public double getRaan() {
600         return raan;
601     }
602 
603     /** Get the mean anomaly.
604      * @return the mean anomaly (rad)
605      */
606     public double getMeanAnomaly() {
607         return meanAnomaly;
608     }
609 
610     /** Get the revolution number.
611      * @return the revolutionNumberAtEpoch
612      */
613     public int getRevolutionNumberAtEpoch() {
614         return revolutionNumberAtEpoch;
615     }
616 
617     /** Get the ballistic coefficient.
618      * @return bStar
619      */
620     public double getBStar() {
621         return bStar;
622     }
623 
624     /** Get a string representation of this TLE set.
625      * <p>The representation is simply the two lines separated by the
626      * platform line separator.</p>
627      * @return string representation of this TLE set
628      */
629     public String toString() {
630         try {
631             return getLine1() + System.getProperty("line.separator") + getLine2();
632         } catch (OrekitException oe) {
633             throw new OrekitInternalError(oe);
634         }
635     }
636 
637     /** Check the lines format validity.
638      * @param line1 the first element
639      * @param line2 the second element
640      * @return true if format is recognized (non null lines, 69 characters length,
641      * line content), false if not
642      * @exception OrekitException if checksum is not valid
643      */
644     public static boolean isFormatOK(final String line1, final String line2)
645         throws OrekitException {
646 
647         if (line1 == null || line1.length() != 69 ||
648             line2 == null || line2.length() != 69) {
649             return false;
650         }
651 
652         if (!(LINE_1_PATTERN.matcher(line1).matches() &&
653               LINE_2_PATTERN.matcher(line2).matches())) {
654             return false;
655         }
656 
657         // check sums
658         final int checksum1 = checksum(line1);
659         if (Integer.parseInt(line1.substring(68)) != (checksum1 % 10)) {
660             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
661                                       1, line1.substring(68), checksum1 % 10, line1);
662         }
663 
664         final int checksum2 = checksum(line2);
665         if (Integer.parseInt(line2.substring(68)) != (checksum2 % 10)) {
666             throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
667                                       2, line2.substring(68), checksum2 % 10, line2);
668         }
669 
670         return true;
671 
672     }
673 
674     /** Compute the checksum of the first 68 characters of a line.
675      * @param line line to check
676      * @return checksum
677      */
678     private static int checksum(final CharSequence line) {
679         int sum = 0;
680         for (int j = 0; j < 68; j++) {
681             final char c = line.charAt(j);
682             if (Character.isDigit(c)) {
683                 sum += Character.digit(c, 10);
684             } else if (c == '-') {
685                 ++sum;
686             }
687         }
688         return sum % 10;
689     }
690 
691     /** Check if this tle equals the provided tle.
692      * <p>Due to the difference in precision between object and string
693      * representations of TLE, it is possible for this method to return false
694      * even if string representations returned by {@link #toString()}
695      * are equal.</p>
696      * @param o other tle
697      * @return true if this tle equals the provided tle
698      */
699     @Override
700     public boolean equals(final Object o) {
701         if (o == this) {
702             return true;
703         }
704         if (!(o instanceof TLE)) {
705             return false;
706         }
707         final TLE tle = (TLE) o;
708         return satelliteNumber == tle.satelliteNumber &&
709                 classification == tle.classification &&
710                 launchYear == tle.launchYear &&
711                 launchNumber == tle.launchNumber &&
712                 Objects.equals(launchPiece, tle.launchPiece) &&
713                 ephemerisType == tle.ephemerisType &&
714                 elementNumber == tle.elementNumber &&
715                 Objects.equals(epoch, tle.epoch) &&
716                 meanMotion == tle.meanMotion &&
717                 meanMotionFirstDerivative == tle.meanMotionFirstDerivative &&
718                 meanMotionSecondDerivative == tle.meanMotionSecondDerivative &&
719                 eccentricity == tle.eccentricity &&
720                 inclination == tle.inclination &&
721                 pa == tle.pa &&
722                 raan == tle.raan &&
723                 meanAnomaly == tle.meanAnomaly &&
724                 revolutionNumberAtEpoch == tle.revolutionNumberAtEpoch &&
725                 bStar == tle.bStar;
726     }
727 
728     /** Get a hashcode for this tle.
729      * @return hashcode
730      */
731     @Override
732     public int hashCode() {
733         return Objects.hash(satelliteNumber,
734                 classification,
735                 launchYear,
736                 launchNumber,
737                 launchPiece,
738                 ephemerisType,
739                 elementNumber,
740                 epoch,
741                 meanMotion,
742                 meanMotionFirstDerivative,
743                 meanMotionSecondDerivative,
744                 eccentricity,
745                 inclination,
746                 pa,
747                 raan,
748                 meanAnomaly,
749                 revolutionNumberAtEpoch,
750                 bStar);
751     }
752 
753 }