1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.orekit.propagation.analytical.tle;
18
19 import java.text.DecimalFormat;
20 import java.text.DecimalFormatSymbols;
21 import java.util.Locale;
22 import java.util.Objects;
23 import java.util.regex.Pattern;
24
25 import org.hipparchus.util.ArithmeticUtils;
26 import org.hipparchus.util.FastMath;
27 import org.hipparchus.util.MathUtils;
28 import org.orekit.annotation.DefaultDataContext;
29 import org.orekit.data.DataContext;
30 import org.orekit.errors.OrekitException;
31 import org.orekit.errors.OrekitMessages;
32 import org.orekit.orbits.OrbitalState;
33 import org.orekit.propagation.SpacecraftState;
34 import org.orekit.propagation.analytical.tle.generation.TleGenerationAlgorithm;
35 import org.orekit.time.AbsoluteDate;
36 import org.orekit.time.DateComponents;
37 import org.orekit.time.DateTimeComponents;
38 import org.orekit.time.TimeComponents;
39 import org.orekit.time.TimeOffset;
40 import org.orekit.time.TimeScale;
41 import org.orekit.utils.Constants;
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 public class TLE implements OrbitalState {
60
61
62 public static final int SGP = 1;
63
64
65 public static final int SGP4 = 2;
66
67
68 public static final int SDP4 = 3;
69
70
71 public static final int SGP8 = 4;
72
73
74 public static final int SDP8 = 5;
75
76
77 public static final int DEFAULT = 0;
78
79
80 private static final String MEAN_MOTION = "meanMotion";
81
82
83 private static final String INCLINATION = "inclination";
84
85
86 private static final String ECCENTRICITY = "eccentricity";
87
88
89 private static final Pattern LINE_1_PATTERN =
90 Pattern.compile("1 [ 0-9A-Z&&[^IO]][ 0-9]{4}[A-Z] [ 0-9]{5}[ A-Z]{3} [ 0-9]{5}[.][ 0-9]{8} " +
91 "(?:[ 0+-][.][ 0-9]{8}| [ +-][.][ 0-9]{7}) [ +-][ 0-9]{5}[+-][ 0-9] " +
92 "[ +-][ 0-9]{5}[+-][ 0-9] [ 0-9] [ 0-9]{4}[ 0-9]");
93
94
95 private static final Pattern LINE_2_PATTERN =
96 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} " +
97 "[ 0-9]{3}[.][ 0-9]{4} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{2}[.][ 0-9]{13}[ 0-9]");
98
99
100 private static final DecimalFormatSymbols SYMBOLS =
101 new DecimalFormatSymbols(Locale.US);
102
103
104 private final int satelliteNumber;
105
106
107 private final char classification;
108
109
110 private final int launchYear;
111
112
113 private final int launchNumber;
114
115
116 private final String launchPiece;
117
118
119 private final int ephemerisType;
120
121
122 private final int elementNumber;
123
124
125 private final AbsoluteDate epoch;
126
127
128 private final double meanMotion;
129
130
131 private final double meanMotionFirstDerivative;
132
133
134 private final double meanMotionSecondDerivative;
135
136
137 private final double eccentricity;
138
139
140 private final double inclination;
141
142
143 private final double pa;
144
145
146 private final double raan;
147
148
149 private final double meanAnomaly;
150
151
152 private final int revolutionNumberAtEpoch;
153
154
155 private String line1;
156
157
158 private String line2;
159
160
161 private final TimeScale utc;
162
163
164 private final double bStar;
165
166
167
168
169
170
171
172
173
174
175
176 @DefaultDataContext
177 public TLE(final String line1, final String line2) {
178 this(line1, line2, DataContext.getDefault().getTimeScales().getUTC());
179 }
180
181
182
183
184
185
186
187
188
189
190 public TLE(final String line1, final String line2, final TimeScale utc) {
191
192
193 satelliteNumber = ParseUtils.parseSatelliteNumber(line1, 2, 5);
194 final int satNum2 = ParseUtils.parseSatelliteNumber(line2, 2, 5);
195 if (satelliteNumber != satNum2) {
196 throw new OrekitException(OrekitMessages.TLE_LINES_DO_NOT_REFER_TO_SAME_OBJECT,
197 line1, line2);
198 }
199 classification = line1.charAt(7);
200 launchYear = ParseUtils.parseYear(line1, 9);
201 launchNumber = ParseUtils.parseInteger(line1, 11, 3);
202 launchPiece = line1.substring(14, 17).trim();
203 ephemerisType = ParseUtils.parseInteger(line1, 62, 1);
204 elementNumber = ParseUtils.parseInteger(line1, 64, 4);
205
206 final int year = ParseUtils.parseYear(line1, 18);
207 final int dayInYear = ParseUtils.parseInteger(line1, 20, 3);
208 final int dayFractionDigits = ParseUtils.parseInteger(line1, 24, 8);
209 final long nanoSecondsCount = dayFractionDigits * (long) Constants.JULIAN_DAY * 10;
210 final TimeOffset dayFraction = new TimeOffset(nanoSecondsCount, TimeOffset.NANOSECOND);
211 epoch = new AbsoluteDate(new DateComponents(year, dayInYear), new TimeComponents(dayFraction), utc);
212
213
214
215 meanMotion = ParseUtils.parseDouble(line2, 52, 11) * FastMath.PI / 43200.0;
216 meanMotionFirstDerivative = ParseUtils.parseDouble(line1, 33, 10) * FastMath.PI / 1.86624e9;
217 meanMotionSecondDerivative = Double.parseDouble((line1.substring(44, 45) + '.' +
218 line1.substring(45, 50) + 'e' +
219 line1.substring(50, 52)).replace(' ', '0')) *
220 FastMath.PI / 5.3747712e13;
221
222 eccentricity = Double.parseDouble("." + line2.substring(26, 33).replace(' ', '0'));
223 inclination = FastMath.toRadians(ParseUtils.parseDouble(line2, 8, 8));
224 pa = FastMath.toRadians(ParseUtils.parseDouble(line2, 34, 8));
225 raan = FastMath.toRadians(Double.parseDouble(line2.substring(17, 25).replace(' ', '0')));
226 meanAnomaly = FastMath.toRadians(ParseUtils.parseDouble(line2, 43, 8));
227
228 revolutionNumberAtEpoch = ParseUtils.parseInteger(line2, 63, 5);
229 bStar = Double.parseDouble((line1.substring(53, 54) + '.' +
230 line1.substring(54, 59) + 'e' +
231 line1.substring(59, 61)).replace(' ', '0'));
232
233
234 this.line1 = line1;
235 this.line2 = line2;
236 this.utc = utc;
237
238 }
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281 @DefaultDataContext
282 public TLE(final int satelliteNumber, final char classification,
283 final int launchYear, final int launchNumber, final String launchPiece,
284 final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
285 final double meanMotion, final double meanMotionFirstDerivative,
286 final double meanMotionSecondDerivative, final double e, final double i,
287 final double pa, final double raan, final double meanAnomaly,
288 final int revolutionNumberAtEpoch, final double bStar) {
289 this(satelliteNumber, classification, launchYear, launchNumber, launchPiece,
290 ephemerisType, elementNumber, epoch, meanMotion,
291 meanMotionFirstDerivative, meanMotionSecondDerivative, e, i, pa, raan,
292 meanAnomaly, revolutionNumberAtEpoch, bStar,
293 DataContext.getDefault().getTimeScales().getUTC());
294 }
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337 public TLE(final int satelliteNumber, final char classification,
338 final int launchYear, final int launchNumber, final String launchPiece,
339 final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
340 final double meanMotion, final double meanMotionFirstDerivative,
341 final double meanMotionSecondDerivative, final double e, final double i,
342 final double pa, final double raan, final double meanAnomaly,
343 final int revolutionNumberAtEpoch, final double bStar,
344 final TimeScale utc) {
345
346
347 this.satelliteNumber = satelliteNumber;
348 this.classification = classification;
349 this.launchYear = launchYear;
350 this.launchNumber = launchNumber;
351 this.launchPiece = launchPiece;
352 this.ephemerisType = ephemerisType;
353 this.elementNumber = elementNumber;
354
355
356 this.epoch = epoch;
357
358 this.meanMotion = meanMotion;
359 this.meanMotionFirstDerivative = meanMotionFirstDerivative;
360 this.meanMotionSecondDerivative = meanMotionSecondDerivative;
361
362
363 this.inclination = i;
364
365
366 this.raan = MathUtils.normalizeAngle(raan, FastMath.PI);
367
368
369 this.eccentricity = e;
370
371
372 this.pa = MathUtils.normalizeAngle(pa, FastMath.PI);
373
374
375 this.meanAnomaly = MathUtils.normalizeAngle(meanAnomaly, FastMath.PI);
376
377 this.revolutionNumberAtEpoch = revolutionNumberAtEpoch;
378 this.bStar = bStar;
379
380
381
382 this.line1 = null;
383 this.line2 = null;
384 this.utc = utc;
385
386 }
387
388
389
390
391
392
393 public TimeScale getUtc() {
394 return utc;
395 }
396
397
398
399
400 public String getLine1() {
401 if (line1 == null) {
402 buildLine1();
403 }
404 return line1;
405 }
406
407
408
409
410 public String getLine2() {
411 if (line2 == null) {
412 buildLine2();
413 }
414 return line2;
415 }
416
417
418
419 private void buildLine1() {
420
421 final StringBuilder buffer = new StringBuilder();
422
423 buffer.append('1');
424
425 buffer.append(' ');
426 buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-1"));
427 buffer.append(classification);
428
429 buffer.append(' ');
430 buffer.append(ParseUtils.addPadding("launchYear", launchYear % 100, '0', 2, true, satelliteNumber));
431 buffer.append(ParseUtils.addPadding("launchNumber", launchNumber, '0', 3, true, satelliteNumber));
432 buffer.append(ParseUtils.addPadding("launchPiece", launchPiece, ' ', 3, false, satelliteNumber));
433
434 buffer.append(' ');
435 DateTimeComponents dtc = epoch.getComponents(utc);
436 int fraction = (int) FastMath.rint(31250 * dtc.getTime().getSecondsInUTCDay() / 27.0);
437 if (fraction >= 100000000) {
438 dtc = epoch.shiftedBy(Constants.JULIAN_DAY).getComponents(utc);
439 fraction -= 100000000;
440 }
441 buffer.append(ParseUtils.addPadding("year", dtc.getDate().getYear() % 100, '0', 2, true, satelliteNumber));
442 buffer.append(ParseUtils.addPadding("day", dtc.getDate().getDayOfYear(), '0', 3, true, satelliteNumber));
443 buffer.append('.');
444
445
446 buffer.append(ParseUtils.addPadding("fraction", fraction, '0', 8, true, satelliteNumber));
447
448 buffer.append(' ');
449 final double n1 = meanMotionFirstDerivative * 1.86624e9 / FastMath.PI;
450 final String sn1 = ParseUtils.addPadding("meanMotionFirstDerivative",
451 new DecimalFormat(".00000000", SYMBOLS).format(n1),
452 ' ', 10, true, satelliteNumber);
453 buffer.append(sn1);
454
455 buffer.append(' ');
456 final double n2 = meanMotionSecondDerivative * 5.3747712e13 / FastMath.PI;
457 buffer.append(formatExponentMarkerFree("meanMotionSecondDerivative", n2, 5, ' ', 8, true));
458
459 buffer.append(' ');
460 buffer.append(formatExponentMarkerFree("B*", getBStar(), 5, ' ', 8, true));
461
462 buffer.append(' ');
463 buffer.append(ephemerisType);
464
465 buffer.append(' ');
466 buffer.append(ParseUtils.addPadding("elementNumber", elementNumber, ' ', 4, true, satelliteNumber));
467
468 buffer.append(checksum(buffer));
469
470 line1 = buffer.toString();
471
472 }
473
474
475
476
477
478
479
480
481
482
483
484 private String formatExponentMarkerFree(final String name, final double d, final int mantissaSize,
485 final char c, final int size, final boolean rightJustified) {
486 final double dAbs = FastMath.abs(d);
487 int exponent = (dAbs < 1.0e-9) ? -9 : (int) FastMath.ceil(FastMath.log10(dAbs));
488 long mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
489 if (mantissa == 0) {
490 exponent = 0;
491 } else if (mantissa > (ArithmeticUtils.pow(10, mantissaSize) - 1)) {
492
493
494
495 exponent++;
496 mantissa = FastMath.round(dAbs * FastMath.pow(10.0, mantissaSize - exponent));
497 }
498 final String sMantissa = ParseUtils.addPadding(name, (int) mantissa, '0', mantissaSize, true, satelliteNumber);
499 final String sExponent = Integer.toString(FastMath.abs(exponent));
500 final String formatted = (d < 0 ? '-' : ' ') + sMantissa + (exponent <= 0 ? '-' : '+') + sExponent;
501
502 return ParseUtils.addPadding(name, formatted, c, size, rightJustified, satelliteNumber);
503
504 }
505
506
507
508 private void buildLine2() {
509
510 final StringBuilder buffer = new StringBuilder();
511 final DecimalFormat f34 = new DecimalFormat("##0.0000", SYMBOLS);
512 final DecimalFormat f211 = new DecimalFormat("#0.00000000", SYMBOLS);
513
514 buffer.append('2');
515
516 buffer.append(' ');
517 buffer.append(ParseUtils.buildSatelliteNumber(satelliteNumber, "satelliteNumber-2"));
518
519 buffer.append(' ');
520 buffer.append(ParseUtils.addPadding(INCLINATION, f34.format(FastMath.toDegrees(inclination)), ' ', 8, true, satelliteNumber));
521 buffer.append(' ');
522 buffer.append(ParseUtils.addPadding("raan", f34.format(FastMath.toDegrees(raan)), ' ', 8, true, satelliteNumber));
523 buffer.append(' ');
524 buffer.append(ParseUtils.addPadding(ECCENTRICITY, (int) FastMath.rint(eccentricity * 1.0e7), '0', 7, true, satelliteNumber));
525 buffer.append(' ');
526 buffer.append(ParseUtils.addPadding("pa", f34.format(FastMath.toDegrees(pa)), ' ', 8, true, satelliteNumber));
527 buffer.append(' ');
528 buffer.append(ParseUtils.addPadding("meanAnomaly", f34.format(FastMath.toDegrees(meanAnomaly)), ' ', 8, true, satelliteNumber));
529
530 buffer.append(' ');
531 buffer.append(ParseUtils.addPadding(MEAN_MOTION, f211.format(meanMotion * 43200.0 / FastMath.PI), ' ', 11, true, satelliteNumber));
532 buffer.append(ParseUtils.addPadding("revolutionNumberAtEpoch", revolutionNumberAtEpoch, ' ', 5, true, satelliteNumber));
533
534 buffer.append(checksum(buffer));
535
536 line2 = buffer.toString();
537
538 }
539
540
541
542
543 public int getSatelliteNumber() {
544 return satelliteNumber;
545 }
546
547
548
549
550 public char getClassification() {
551 return classification;
552 }
553
554
555
556
557 public int getLaunchYear() {
558 return launchYear;
559 }
560
561
562
563
564 public int getLaunchNumber() {
565 return launchNumber;
566 }
567
568
569
570
571 public String getLaunchPiece() {
572 return launchPiece;
573 }
574
575
576
577
578
579 public int getEphemerisType() {
580 return ephemerisType;
581 }
582
583
584
585
586 public int getElementNumber() {
587 return elementNumber;
588 }
589
590
591
592
593 public AbsoluteDate getDate() {
594 return epoch;
595 }
596
597
598
599
600 public double getMeanMotion() {
601 return meanMotion;
602 }
603
604
605
606
607 public double getMeanMotionFirstDerivative() {
608 return meanMotionFirstDerivative;
609 }
610
611
612
613
614 public double getMeanMotionSecondDerivative() {
615 return meanMotionSecondDerivative;
616 }
617
618
619
620
621 public double getE() {
622 return eccentricity;
623 }
624
625
626
627
628 public double getI() {
629 return inclination;
630 }
631
632
633
634
635 public double getPeriapsisArgument() {
636 return pa;
637 }
638
639
640
641
642 public double getRaan() {
643 return raan;
644 }
645
646
647
648
649 public double getMeanAnomaly() {
650 return meanAnomaly;
651 }
652
653
654
655
656 public int getRevolutionNumberAtEpoch() {
657 return revolutionNumberAtEpoch;
658 }
659
660
661
662
663 public double getBStar() {
664 return bStar;
665 }
666
667
668
669
670 public double computeSemiMajorAxis() {
671 return FastMath.cbrt(TLEConstants.MU / (meanMotion * meanMotion));
672 }
673
674
675
676
677
678
679 public String toString() {
680 return getLine1() + System.getProperty("line.separator") + getLine2();
681 }
682
683
684
685
686
687
688
689
690
691
692
693 public static TLE stateToTLE(final SpacecraftState state,
694 final TleGenerationAlgorithm generationAlgorithm) {
695 return generationAlgorithm.generate(state, generationAlgorithm.getTemplateTLE());
696 }
697
698
699
700
701
702
703
704 public static boolean isFormatOK(final String line1, final String line2) {
705
706 if (line1 == null || line1.length() != 69 ||
707 line2 == null || line2.length() != 69) {
708 return false;
709 }
710
711 if (!(LINE_1_PATTERN.matcher(line1).matches() &&
712 LINE_2_PATTERN.matcher(line2).matches())) {
713 return false;
714 }
715
716
717 final int checksum1 = checksum(line1);
718 if (Integer.parseInt(line1.substring(68)) != (checksum1 % 10)) {
719 throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
720 1, Integer.toString(checksum1 % 10), line1.substring(68), line1);
721 }
722
723 final int checksum2 = checksum(line2);
724 if (Integer.parseInt(line2.substring(68)) != (checksum2 % 10)) {
725 throw new OrekitException(OrekitMessages.TLE_CHECKSUM_ERROR,
726 2, Integer.toString(checksum2 % 10), line2.substring(68), line2);
727 }
728
729 return true;
730
731 }
732
733
734
735
736
737 private static int checksum(final CharSequence line) {
738 int sum = 0;
739 for (int j = 0; j < 68; j++) {
740 final char c = line.charAt(j);
741 if (Character.isDigit(c)) {
742 sum += Character.digit(c, 10);
743 } else if (c == '-') {
744 ++sum;
745 }
746 }
747 return sum % 10;
748 }
749
750
751
752
753
754
755
756
757
758
759 public static int parseSatelliteNumber(final String satNumberString) {
760 return ParseUtils.parseSatelliteNumber(satNumberString, 0, satNumberString.length());
761 }
762
763
764
765
766
767
768
769
770
771 @Override
772 public boolean equals(final Object o) {
773 if (o == this) {
774 return true;
775 }
776 if (!(o instanceof final TLE tle)) {
777 return false;
778 }
779 return satelliteNumber == tle.satelliteNumber &&
780 classification == tle.classification &&
781 launchYear == tle.launchYear &&
782 launchNumber == tle.launchNumber &&
783 Objects.equals(launchPiece, tle.launchPiece) &&
784 ephemerisType == tle.ephemerisType &&
785 elementNumber == tle.elementNumber &&
786 Objects.equals(epoch, tle.epoch) &&
787 meanMotion == tle.meanMotion &&
788 meanMotionFirstDerivative == tle.meanMotionFirstDerivative &&
789 meanMotionSecondDerivative == tle.meanMotionSecondDerivative &&
790 eccentricity == tle.eccentricity &&
791 inclination == tle.inclination &&
792 pa == tle.pa &&
793 raan == tle.raan &&
794 meanAnomaly == tle.meanAnomaly &&
795 revolutionNumberAtEpoch == tle.revolutionNumberAtEpoch &&
796 bStar == tle.bStar;
797 }
798
799
800
801
802 @Override
803 public int hashCode() {
804 return Objects.hash(satelliteNumber,
805 classification,
806 launchYear,
807 launchNumber,
808 launchPiece,
809 ephemerisType,
810 elementNumber,
811 epoch,
812 meanMotion,
813 meanMotionFirstDerivative,
814 meanMotionSecondDerivative,
815 eccentricity,
816 inclination,
817 pa,
818 raan,
819 meanAnomaly,
820 revolutionNumberAtEpoch,
821 bStar);
822 }
823
824 }