1"""Read the TLE earth satellite file format.
2
3This is a minimally-edited copy of "sgp4io.cpp".
4
5"""
6import re
7from datetime import datetime
8from math import pi, pow
9from sgp4.ext import days2mdhms, invjday, jday
10from sgp4.propagation import sgp4init
11
12INT_RE = re.compile(r'[+-]?\d*')
13FLOAT_RE = re.compile(r'[+-]?\d*(\.\d*)?')
14
15LINE1 = '1 NNNNNC NNNNNAAA NNNNN.NNNNNNNN +.NNNNNNNN +NNNNN-N +NNNNN-N N NNNNN'
16LINE2 = '2 NNNNN NNN.NNNN NNN.NNNN NNNNNNN NNN.NNNN NNN.NNNN NN.NNNNNNNNNNNNNN'
17
18error_message = """TLE format error
19
20The Two-Line Element (TLE) format was designed for punch cards, and so
21is very strict about the position of every period, space, and digit.
22Your line does not quite match.  Here is the official format for line {0}
23with an N where each digit should go, followed by the line you provided:
24
25{1}
26{2}"""
27
28"""
29/*     ----------------------------------------------------------------
30*
31*                               sgp4io.cpp
32*
33*    this file contains a function to read two line element sets. while
34*    not formerly part of the sgp4 mathematical theory, it is
35*    required for practical implemenation.
36*
37*                            companion code for
38*               fundamentals of astrodynamics and applications
39*                                    2007
40*                              by david vallado
41*
42*       (w) 719-573-2600, email dvallado@agi.com
43*
44*    current :
45*              27 Aug 10  david vallado
46*                           fix input format and delete unused variables in twoline2rv
47*    changes :
48*               3 sep 08  david vallado
49*                           add operationmode for afspc (a) or improved (i)
50*               9 may 07  david vallado
51*                           fix year correction to 57
52*              27 mar 07  david vallado
53*                           misc fixes to manual inputs
54*              14 aug 06  david vallado
55*                           original baseline
56*       ----------------------------------------------------------------      */
57"""
58
59"""
60/* -----------------------------------------------------------------------------
61*
62*                           function twoline2rv
63*
64*  this function converts the two line element set character string data to
65*    variables and initializes the sgp4 variables. several intermediate varaibles
66*    and quantities are determined. note that the result is a structure so multiple
67*    satellites can be processed simultaneously without having to reinitialize. the
68*    verification mode is an important option that permits quick checks of any
69*    changes to the underlying technical theory. this option works using a
70*    modified tle file in which the start, stop, and delta time values are
71*    included at the end of the second line of data. this only works with the
72*    verification mode. the catalog mode simply propagates from -1440 to 1440 min
73*    from epoch and is useful when performing entire catalog runs.
74*
75*  author        : david vallado                  719-573-2600    1 mar 2001
76*
77*  inputs        :
78*    longstr1    - first line of the tle
79*    longstr2    - second line of the tle
80*    typerun     - type of run                    verification 'v', catalog 'c',
81*                                                 manual 'm'
82*    typeinput   - type of manual input           mfe 'm', epoch 'e', dayofyr 'd'
83*    opsmode     - mode of operation afspc or improved 'a', 'i'
84*    whichconst  - which set of constants to use  72, 84
85*
86*  outputs       :
87*    satrec      - structure containing all the sgp4 satellite information
88*
89*  coupling      :
90*    getgravconst-
91*    days2mdhms  - conversion of days to month, day, hour, minute, second
92*    jday        - convert day month year hour minute second into julian date
93*    sgp4init    - initialize the sgp4 variables
94*
95*  references    :
96*    norad spacetrack report #3
97*    vallado, crawford, hujsak, kelso  2006
98  --------------------------------------------------------------------------- */
99"""
100
101def twoline2rv(longstr1, longstr2, whichconst, opsmode='i', satrec=None):
102    """Return a Satellite imported from two lines of TLE data.
103
104    Provide the two TLE lines as strings `longstr1` and `longstr2`,
105    and select which standard set of gravitational constants you want
106    by providing `gravity_constants`:
107
108    `sgp4.earth_gravity.wgs72` - Standard WGS 72 model
109    `sgp4.earth_gravity.wgs84` - More recent WGS 84 model
110    `sgp4.earth_gravity.wgs72old` - Legacy support for old SGP4 behavior
111
112    Normally, computations are made using various recent improvements
113    to the algorithm.  If you want to turn some of these off and go
114    back into "opsmode" mode, then set `opsmode` to `a`.
115
116    """
117
118    deg2rad  =   pi / 180.0;         #    0.0174532925199433
119    xpdotp   =  1440.0 / (2.0 *pi);  #  229.1831180523293
120
121    # For compatibility with our 1.x API, build an old Satellite object
122    # if the caller fails to supply a satrec.  In that case we perform
123    # the necessary import here to avoid an import loop.
124    if satrec is None:
125        from sgp4.model import Satellite
126        satrec = Satellite()
127
128    satrec.error = 0;
129    satrec.whichconst = whichconst  # Python extension: remembers its consts
130
131    line = longstr1.rstrip()
132
133    if (len(line) >= 64 and
134        line.startswith('1 ') and
135        line[8] == ' ' and
136        line[23] == '.' and
137        line[32] == ' ' and
138        line[34] == '.' and
139        line[43] == ' ' and
140        line[52] == ' ' and
141        line[61] == ' ' and
142        line[63] == ' '):
143
144        _saved_satnum = satrec.satnum = _alpha5(line[2:7])
145        satrec.classification = line[7] or 'U'
146        satrec.intldesg = line[9:17].rstrip()
147        two_digit_year = int(line[18:20])
148        satrec.epochdays = float(line[20:32])
149        satrec.ndot = float(line[33:43])
150        satrec.nddot = float(line[44] + '.' + line[45:50])
151        nexp = int(line[50:52])
152        satrec.bstar = float(line[53] + '.' + line[54:59])
153        ibexp = int(line[59:61])
154        satrec.ephtype = line[62]
155        satrec.elnum = int(line[64:68])
156    else:
157        raise ValueError(error_message.format(1, LINE1, line))
158
159    line = longstr2.rstrip()
160
161    if (len(line) >= 69 and
162        line.startswith('2 ') and
163        line[7] == ' ' and
164        line[11] == '.' and
165        line[16] == ' ' and
166        line[20] == '.' and
167        line[25] == ' ' and
168        line[33] == ' ' and
169        line[37] == '.' and
170        line[42] == ' ' and
171        line[46] == '.' and
172        line[51] == ' '):
173
174        satrec.satnum = _alpha5(line[2:7])
175        if _saved_satnum != satrec.satnum:
176            raise ValueError('Object numbers in lines 1 and 2 do not match')
177
178        satrec.inclo = float(line[8:16])
179        satrec.nodeo = float(line[17:25])
180        satrec.ecco = float('0.' + line[26:33].replace(' ', '0'))
181        satrec.argpo = float(line[34:42])
182        satrec.mo = float(line[43:51])
183        satrec.no_kozai = float(line[52:63])
184        satrec.revnum = line[63:68]
185    #except (AssertionError, IndexError, ValueError):
186    else:
187        raise ValueError(error_message.format(2, LINE2, line))
188
189    #  ---- find no, ndot, nddot ----
190    satrec.no_kozai = satrec.no_kozai / xpdotp; #   rad/min
191    satrec.nddot= satrec.nddot * pow(10.0, nexp);
192    satrec.bstar= satrec.bstar * pow(10.0, ibexp);
193
194    #  ---- convert to sgp4 units ----
195    satrec.ndot = satrec.ndot  / (xpdotp*1440.0);  #   ? * minperday
196    satrec.nddot= satrec.nddot / (xpdotp*1440.0*1440);
197
198    #  ---- find standard orbital elements ----
199    satrec.inclo = satrec.inclo  * deg2rad;
200    satrec.nodeo = satrec.nodeo  * deg2rad;
201    satrec.argpo = satrec.argpo  * deg2rad;
202    satrec.mo    = satrec.mo     * deg2rad;
203
204
205    """
206    // ----------------------------------------------------------------
207    // find sgp4epoch time of element set
208    // remember that sgp4 uses units of days from 0 jan 1950 (sgp4epoch)
209    // and minutes from the epoch (time)
210    // ----------------------------------------------------------------
211
212    // ---------------- temp fix for years from 1957-2056 -------------------
213    // --------- correct fix will occur when year is 4-digit in tle ---------
214    """
215    if two_digit_year < 57:
216        year = two_digit_year + 2000;
217    else:
218        year = two_digit_year + 1900;
219
220    mon,day,hr,minute,sec = days2mdhms(year, satrec.epochdays);
221    sec_whole, sec_fraction = divmod(sec, 1.0)
222
223    satrec.epochyr = year
224    satrec.jdsatepoch = jday(year,mon,day,hr,minute,sec);
225    try:
226        satrec.epoch = datetime(year, mon, day, hr, minute, int(sec_whole),
227                                int(sec_fraction * 1000000.0 // 1.0))
228    except ValueError:
229        # Sometimes a TLE says something like "2019 + 366.82137887 days"
230        # which would be December 32nd which causes a ValueError.
231        year, mon, day, hr, minute, sec = invjday(satrec.jdsatepoch)
232        satrec.epoch = datetime(year, mon, day, hr, minute, int(sec_whole),
233                                int(sec_fraction * 1000000.0 // 1.0))
234
235    #  ---------------- initialize the orbit at sgp4epoch -------------------
236    sgp4init(whichconst, opsmode, satrec.satnum, satrec.jdsatepoch-2433281.5, satrec.bstar,
237             satrec.ndot, satrec.nddot, satrec.ecco, satrec.argpo, satrec.inclo, satrec.mo,
238             satrec.no_kozai, satrec.nodeo, satrec)
239
240    return satrec
241
242def verify_checksum(*lines):
243    """Verify the checksum of one or more TLE lines.
244
245    Raises `ValueError` if any of the lines fails its checksum, and
246    includes the failing line in the error message.
247
248    """
249    for line in lines:
250        checksum = line[68:69]
251        if not checksum.isdigit():
252            continue
253        checksum = int(checksum)
254        computed = compute_checksum(line)
255        if checksum != computed:
256            complaint = ('TLE line gives its checksum as {}'
257                         ' but in fact tallies to {}:\n{}')
258            raise ValueError(complaint.format(checksum, computed, line))
259
260def fix_checksum(line):
261    """Return a new copy of the TLE `line`, with the correct checksum appended.
262
263    This discards any existing checksum at the end of the line, if a
264    checksum is already present.
265
266    """
267    return line[:68].ljust(68) + str(compute_checksum(line))
268
269def compute_checksum(line):
270    """Compute the TLE checksum for the given line."""
271    return sum((int(c) if c.isdigit() else c == '-') for c in line[0:68]) % 10
272
273def _alpha5(s):
274    if not s[0].isalpha():
275        return int(s)
276    c = s[0]
277    n = ord(c) - ord('A') + 10
278    n -= c > 'I'
279    n -= c > 'O'
280    return n * 10000 + int(s[1:])
281