File indexing completed on 2024-12-01 12:29:09

0001 # SPDX-License-Identifier: LGPL-2.1-or-later
0002 #
0003 # SPDX-FileCopyrightText: 2012 Rene Kuettner <rene@bitkanal.net>
0004 #
0005 
0006 from __future__ import print_function
0007 
0008 from SpaceObjectCatalog import HorizonsSpaceObject, TASCSpaceObject
0009 
0010 import sys
0011 import telnetlib
0012 import time
0013 import httplib
0014 import urllib
0015 import calendar
0016 import re
0017 
0018 
0019 class HorizonsDownloader(object):
0020 
0021     """A simple telnet client for the horizons system."""
0022 
0023     _body_ids = {
0024         'Mercury'   : 199,
0025         'Venus'     : 299,
0026         'Earth'     : 399,
0027         'Mars'      : 499,
0028         'Jupiter'   : 599,
0029         'Saturn'    : 699,
0030         'Uranus'    : 799,
0031         'Neptune'   : 899,
0032         }
0033 
0034     _commands = { 
0035         re.escape("$$SOE"): 'NoResponse',
0036         re.escape("$$EOE"): 'Data',
0037         re.escape("Horizons> "): 'Done',
0038         re.escape("Continue [ <cr>=yes, n=no, ? ] : "): "n",
0039         re.escape("[E]phemeris, [F]tp, [M]ail, [R]edisplay, ?, <cr>: "): "e",
0040         re.escape("Observe, Elements, Vectors  [o,e,v,?] : "): "v",
0041         re.escape("Coordinate center [ <id>,coord,geo  ] : "): "500@{body_id}",
0042         re.escape("Confirm selected station    [ y/n ] --> "): "y",
0043         re.escape("Reference plane [eclip, frame, body ] : "): "frame",
0044         re.escape("Output interval [ex: 10m, 1h, 1d, ? ] : "): "{interval_days}d",
0045         re.escape("Accept default output [ cr=(y), n, ?] : "): "n",
0046         re.escape("Output reference frame [J2000, B1950] : "): "J2000",
0047         re.escape("Corrections [ 1=NONE, 2=LT, 3=LT+S ]  : "): "1",
0048         re.escape("Output units [1=KM-S, 2=AU-D, 3=KM-D] : "): "1",
0049         re.escape("Spreadsheet CSV format    [ YES, NO ] : "): "YES",
0050         re.escape("Label cartesian output    [ YES, NO ] : "): "NO",
0051         re.escape("Select output table type  [ 1-6, ?  ] : "): "2",
0052         re.escape("[A]gain, [N]ew-case, [F]tp, [K]ermit, [M]ail, [R]edisplay, ? : "): "n",
0053         re.escape("Use previous center  [ cr=(y), n, ? ] : "): "n",
0054         re.compile("Starting CT .* : "): "{datetimestart}",
0055         re.compile("Ending   CT .* : "): "{datetimeend}",
0056         }
0057 
0058     def __init__(self):
0059         super(HorizonsDownloader, self).__init__()
0060         self._host = "horizons.jpl.nasa.gov"
0061         self._port = 6775
0062         self._prompt = "Horizons>"
0063         self._connection = None
0064         self._debug = False
0065         self._last_command_index = None
0066 
0067     def connect(self):
0068         self._connection = telnetlib.Telnet(self._host, self._port)
0069         self._read_until_prompt()
0070 
0071     def disconnect(self):
0072         self._connection.close()
0073 
0074     def download_state_vectors(self, hspace_obj, t_srt=None, t_end=None):
0075         if self._connection is None:
0076             print("Not connected!")
0077             return
0078         print("Requesting data for {0} relative to {1}..." .format(
0079             hspace_obj.name, hspace_obj.related_body))
0080         self._send(hspace_obj.horizons_id)
0081 
0082         if t_srt is None:
0083             t_srt = hspace_obj.data_from
0084         if t_end is None or t_end <= t_srt:
0085             t_end = hspace_obj.data_until
0086 
0087         body_id = self._body_ids[hspace_obj.related_body]
0088         
0089         cmds = self._commands.keys()
0090         resp = [y.format(body=hspace_obj.related_body,
0091                          body_id=body_id,
0092                          datetimestart=self._unixtime_to_str(t_srt),
0093                          datetimeend=self._unixtime_to_str(t_end),
0094                          interval_days=hspace_obj.data_interval_days)
0095                 for y in self._commands.values()]
0096         
0097         done = False
0098         vectors = []
0099 
0100         while(not done):
0101             r, data = self._next_command(cmds, resp)
0102             if r == 'Done':
0103                 done = True
0104             elif r == 'Data':
0105                 print("  Found... Parsing...")
0106                 vectors = self._parse_data(data)
0107                 print("  Success")
0108             elif r == 'NoResponse':
0109                 pass
0110             else:
0111                 self._send(r)
0112 
0113         return vectors
0114 
0115     def _next_command(self, cmds, resp):
0116         idx, match, data = self._connection.expect(cmds, 120)
0117         if self._debug:
0118             print(data)
0119         if self._last_command_index == idx:
0120             print("Horizons repeated the last message:")
0121             print(data)
0122             print("Probably something went wrong. Aborting!")
0123             raise Exception()
0124         else:
0125             self._last_command_index = idx
0126         return (resp[idx], data)
0127 
0128     def _unixtime_to_str(self, timestamp):
0129         return time.strftime('%Y-%m-%d %H:%M', time.gmtime(timestamp))
0130  
0131     def _jdate_to_unixtime(self, jstamp):
0132         return (jstamp - 2440587.5) * 86400
0133 
0134     def _parse_data(self, data):
0135         sdat = data.split(',')
0136         if len(sdat) < 1:
0137             raise Exception()
0138         vectors = []
0139         for i in range(0, len(sdat) - 1, 8):
0140             ldat = sdat[i:i+8]
0141             vectors.append( [ float(ldat[0]),
0142                      float(ldat[2]), float(ldat[3]), float(ldat[4]),
0143                      float(ldat[5]), float(ldat[6]), float(ldat[7]) ] )
0144         return vectors
0145 
0146     def _send(self, data):
0147         self._connection.write("{0}\n".format(data))
0148 
0149     def _read_until_prompt(self):
0150         self._connection.read_until(self._prompt)
0151 
0152 
0153 class TASCDownloader(object):
0154 
0155     """A simple http client for ESA's TASC service at http://tasc.esa.int/."""
0156 
0157     _params = {
0158         'mission'       : "{mission}",
0159         'querytyp'      : "data",
0160         'qtyp'          : "sta",
0161         'staobj'        : "{object_name}",
0162         'starefobj'     : "{related_body}",
0163         'staltc'        : "NO",
0164         'stafrm'        : "mean equatorial J2000",
0165         'tscl'          : "TDB",
0166         'tstart'        : "{t_start}",
0167         'tend'          : "{t_end}",
0168         'tstp'          : "{interval_days:03d} 00:00:00.000",
0169         'orbtyp'        : "ops",
0170     }
0171 
0172     def __init__(self):
0173         super(TASCDownloader, self).__init__()
0174         self._host = "tasc.esa.int"
0175         self._port = 80
0176         self._uri  = '/cgi-bin/query.html'
0177         self._debug = False
0178 
0179     def download_state_vectors(self, space_obj, t_start=None, t_end=None):
0180         print("Requesting data for {0} relative to {1}..." .format(
0181             space_obj.name, space_obj.related_body))
0182 
0183         if t_start is None:
0184             t_start = space_obj.data_from
0185         if t_end is None or t_end <= t_start:
0186             t_end = space_obj.data_until
0187 
0188         start = time.strftime('%Y/%m/%d %H:%M:%S.000', time.gmtime(t_start))
0189         end = time.strftime('%Y/%m/%d %H:%M:%S.000', time.gmtime(t_end))
0190         interval_days = 30
0191 
0192         params = []
0193         for key in self._params.keys():
0194             value = urllib.quote_plus(self._params[key].format(
0195                 mission=space_obj.tasc_mission,
0196                 object_name=space_obj.name,
0197                 related_body=space_obj.related_body,
0198                 t_start=start,
0199                 t_end=end,
0200                 interval_days=interval_days))
0201             params = params + [ key, value ]
0202 
0203         data = self._fetch_from_http(params)
0204         print("  Parsing...")
0205         return self._parse(data)
0206 
0207     def _fetch_from_http(self, params):
0208         con = httplib.HTTPConnection(self._host, self._port)
0209         p = []
0210         for i in range(0, len(params), 2):
0211             p = p + [ params[i] + "=" + params[i+1] ]
0212         uri = self._uri + '?' + '&'.join(p)
0213         if self._debug:
0214             print("Request:", uri)
0215         con.request('GET', uri)
0216         response = con.getresponse()
0217         if response.status != 200:
0218             print("Failed to load data via HTTP:",
0219                     response.status, response.reason)
0220             return ""
0221         return response.read()
0222 
0223     def _parse(self, data):
0224         if self._debug:
0225             print(data)
0226 
0227         dl = [x for x in data.split("\n")
0228             if x != "" and x.find("#") == -1 and x.find("<") == -1]
0229 
0230         if len(dl) < 1:
0231             return []
0232 
0233         vecs = []
0234         for line in dl:
0235             fields = line.split()
0236             if len(fields) != 7:
0237                 print("Invalid data line:", line)
0238                 continue;
0239             vec = [ self._TDB_to_JD(fields[0]),
0240                 float(fields[1]), float(fields[2]), float(fields[3]),
0241                 float(fields[4]), float(fields[5]), float(fields[6])]
0242             vecs.append(vec)
0243         return vecs
0244 
0245     def _TDB_to_JD(self, tdb):
0246         tdb = tdb.split(".")[0] # strip .000
0247         t = calendar.timegm(time.strptime(tdb, '%Y/%m/%dT%H:%M:%S'))
0248         return t / 86400 + 2440587.5
0249 
0250 
0251 class DataDownloader(object):
0252 
0253     def __init__(self):
0254         super(DataDownloader, self).__init__()
0255         self._horizons = HorizonsDownloader()
0256         self._horizons.connect()
0257         self._tasc = TASCDownloader()
0258 
0259     def __del__(self):
0260         self._horizons.disconnect()
0261 
0262     def download_state_vectors(self, space_obj):
0263         if isinstance(space_obj, HorizonsSpaceObject):
0264             return self._horizons.download_state_vectors(space_obj)
0265         elif isinstance(space_obj, TASCSpaceObject):
0266             return self._tasc.download_state_vectors(space_obj)
0267         else:
0268             raise NotImplementedError(space_obj.__class__.__name__)