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