11# Licensed under a 3-clause BSD style license - see LICENSE.rst
22
3-
43# 1. standard library imports
5- from numpy import nan
6- from numpy import isnan
7- from numpy import ndarray
84from collections import OrderedDict
5+ from typing import Mapping
96import warnings
107
118# 2. third party imports
129from requests .exceptions import HTTPError
10+ from numpy import nan
11+ from numpy import isnan
12+ from numpy import ndarray
1313from astropy .table import Table , Column
1414from astropy .io import ascii
1515from astropy .time import Time
@@ -51,8 +51,16 @@ def __init__(self, id=None, *, location=None, epochs=None,
5151 Parameters
5252 ----------
5353
54- id : str, required
55- Name, number, or designation of the object to be queried.
54+ id : str or dict, required
55+ Name, number, or designation of target object. Uses the same codes
56+ as JPL Horizons. Arbitrary topocentric coordinates can be added
57+ in a dict. The dict has to be of the form
58+ {``'lon'``: longitude in deg (East positive, West
59+ negative), ``'lat'``: latitude in deg (North positive, South
60+ negative), ``'elevation'``: elevation in km above the reference
61+ ellipsoid, [``'body'``: Horizons body ID of the central body;
62+ optional; if this value is not provided it is assumed that this
63+ location is on Earth]}.
5664
5765 location : str or dict, optional
5866 Observer's location for ephemerides queries or center body name for
@@ -108,9 +116,16 @@ def __init__(self, id=None, *, location=None, epochs=None,
108116 """
109117
110118 super ().__init__ ()
111- self .id = id
112- self .location = location
113-
119+ # check & format coordinate dictionaries for id and location; simply
120+ # treat other values as given
121+ if isinstance (id , Mapping ):
122+ self .id = self ._prep_loc_dict (dict (id ), "id" )
123+ else :
124+ self .id = id
125+ if isinstance (location , Mapping ):
126+ self .location = self ._prep_loc_dict (dict (location ), "location" )
127+ else :
128+ self .location = location
114129 # check for epochs to be dict or list-like; else: make it a list
115130 if epochs is not None :
116131 if isinstance (epochs , (list , tuple , ndarray )):
@@ -535,16 +550,22 @@ def ephemerides_async(self, *, airmass_lessthan=99,
535550
536551 URL = conf .horizons_server
537552
538- # check for required information
553+ # check for required information and assemble commanddline stub
539554 if self .id is None :
540555 raise ValueError ("'id' parameter not set. Query aborted." )
556+ elif isinstance (self .id , dict ):
557+ commandline = (
558+ f"g:{ self .id ['lon' ]} ,{ self .id ['lat' ]} ,"
559+ f"{ self .id ['elevation' ]} @{ self .id ['body' ]} "
560+ )
561+ else :
562+ commandline = str (self .id )
541563 if self .location is None :
542564 self .location = '500@399'
543565 if self .epochs is None :
544566 self .epochs = Time .now ().jd
567+ # expand commandline based on self.id_type
545568
546- # assemble commandline based on self.id_type
547- commandline = str (self .id )
548569 if self .id_type in ['designation' , 'name' ,
549570 'asteroid_name' , 'comet_name' ]:
550571 commandline = ({'designation' : 'DES=' ,
@@ -580,19 +601,7 @@ def ephemerides_async(self, *, airmass_lessthan=99,
580601 ('EXTRA_PREC' , {True : 'YES' , False : 'NO' }[extra_precision ])])
581602
582603 if isinstance (self .location , dict ):
583- if ('lon' not in self .location or 'lat' not in self .location or
584- 'elevation' not in self .location ):
585- raise ValueError (("'location' must contain lon, lat, "
586- "elevation" ))
587-
588- if 'body' not in self .location :
589- self .location ['body' ] = '399'
590- request_payload ['CENTER' ] = 'coord@{:s}' .format (
591- str (self .location ['body' ]))
592- request_payload ['COORD_TYPE' ] = 'GEODETIC'
593- request_payload ['SITE_COORD' ] = "'{:f},{:f},{:f}'" .format (
594- self .location ['lon' ], self .location ['lat' ],
595- self .location ['elevation' ])
604+ request_payload = dict (** request_payload , ** self ._location_to_params (self .location ))
596605 else :
597606 request_payload ['CENTER' ] = "'" + str (self .location ) + "'"
598607
@@ -1032,17 +1041,18 @@ def vectors_async(self, *, get_query_payload=False,
10321041
10331042 URL = conf .horizons_server
10341043
1035- # check for required information
1044+ # check for required information and assemble commandline stub
10361045 if self .id is None :
10371046 raise ValueError ("'id' parameter not set. Query aborted." )
1047+ elif isinstance (self .id , dict ):
1048+ commandline = "g:{lon},{lat},{elevation}@{body}" .format (** self .id )
1049+ else :
1050+ commandline = str (self .id )
10381051 if self .location is None :
10391052 self .location = '500@10'
10401053 if self .epochs is None :
10411054 self .epochs = Time .now ().jd
1042-
1043- # assemble commandline based on self.id_type
1044- commandline = str (self .id )
1045-
1055+ # expand commandline based on self.id_type
10461056 if self .id_type in ['designation' , 'name' ,
10471057 'asteroid_name' , 'comet_name' ]:
10481058 commandline = ({'designation' : 'DES=' ,
@@ -1060,18 +1070,12 @@ def vectors_async(self, *, get_query_payload=False,
10601070 commandline += ' CAP{:s};' .format (closest_apparition )
10611071 if no_fragments :
10621072 commandline += ' NOFRAG;'
1063-
1064- if isinstance (self .location , dict ):
1065- raise ValueError (('cannot use topographic position in state'
1066- 'vectors query' ))
1067-
1068- # configure request_payload for ephemerides query
1073+ # configure request_payload for vectors query
10691074 request_payload = OrderedDict ([
10701075 ('format' , 'text' ),
10711076 ('EPHEM_TYPE' , 'VECTORS' ),
10721077 ('OUT_UNITS' , 'AU-D' ),
10731078 ('COMMAND' , '"' + commandline + '"' ),
1074- ('CENTER' , ("'" + str (self .location ) + "'" )),
10751079 ('CSV_FORMAT' , ('"YES"' )),
10761080 ('REF_PLANE' , {'ecliptic' : 'ECLIPTIC' ,
10771081 'earth' : 'FRAME' ,
@@ -1086,7 +1090,12 @@ def vectors_async(self, *, get_query_payload=False,
10861090 ('VEC_DELTA_T' , {True : 'YES' , False : 'NO' }[delta_T ]),
10871091 ('OBJ_DATA' , 'YES' )]
10881092 )
1089-
1093+ if isinstance (self .location , dict ):
1094+ request_payload = dict (
1095+ ** request_payload , ** self ._location_to_params (self .location )
1096+ )
1097+ else :
1098+ request_payload ['CENTER' ] = "'" + str (self .location ) + "'"
10901099 # parse self.epochs
10911100 if isinstance (self .epochs , (list , tuple , ndarray )):
10921101 request_payload ['TLIST' ] = "\n " .join ([str (epoch ) for epoch in
@@ -1132,6 +1141,30 @@ def vectors_async(self, *, get_query_payload=False,
11321141 return response
11331142
11341143 # ---------------------------------- parser functions
1144+ @staticmethod
1145+ def _prep_loc_dict (loc_dict , attr_name ):
1146+ """prepare coord specification dict for 'location' or 'id'"""
1147+ if {'lat' , 'lon' , 'elevation' } - loc_dict .keys ():
1148+ raise ValueError (
1149+ f"dict values for '{ attr_name } ' must contain 'lat', 'lon', "
1150+ "'elevation' (and optionally 'body')"
1151+ )
1152+ if 'body' not in loc_dict :
1153+ loc_dict ['body' ] = 399
1154+ return loc_dict
1155+
1156+ @staticmethod
1157+ def _location_to_params (loc_dict ):
1158+ """translate a 'location' dict to a dict of request parameters"""
1159+ loc_dict = {
1160+ "CENTER" : f"coord@{ loc_dict ['body' ]} " ,
1161+ "COORD_TYPE" : "GEODETIC" ,
1162+ "SITE_COORD" : "," .join (
1163+ str (float (loc_dict [k ])) for k in ['lon' , 'lat' , 'elevation' ]
1164+ )
1165+ }
1166+ loc_dict ["SITE_COORD" ] = f"'{ loc_dict ['SITE_COORD' ]} '"
1167+ return loc_dict
11351168
11361169 def _parse_result (self , response , verbose = None ):
11371170 """
@@ -1181,14 +1214,18 @@ def _parse_result(self, response, verbose=None):
11811214 H , G = nan , nan
11821215 M1 , M2 , k1 , k2 , phcof = nan , nan , nan , nan , nan
11831216 headerline = []
1217+ centername = ''
11841218 for idx , line in enumerate (src ):
11851219 # read in ephemerides header line; replace some field names
11861220 if (self .query_type == 'ephemerides' and
11871221 "Date__(UT)__HR:MN" in line ):
11881222 headerline = str (line ).split (',' )
11891223 headerline [2 ] = 'solar_presence'
1190- headerline [3 ] = 'flags'
1224+ headerline [3 ] = "lunar_presence" if "Earth" in centername else "interfering_body"
11911225 headerline [- 1 ] = '_dump'
1226+ if isinstance (self .id , dict ) or str (self .id ).startswith ('g:' ):
1227+ headerline [4 ] = 'nearside_flag'
1228+ headerline [5 ] = 'illumination_flag'
11921229 # read in elements header line
11931230 elif (self .query_type == 'elements' and
11941231 "JDTDB," in line ):
@@ -1208,6 +1245,9 @@ def _parse_result(self, response, verbose=None):
12081245 # read in targetname
12091246 if "Target body name" in line :
12101247 targetname = line [18 :50 ].strip ()
1248+ # read in center body name
1249+ if "Center body name" in line :
1250+ centername = line [18 :50 ].strip ()
12111251 # read in H and G (if available)
12121252 if "rotational period in hours)" in line :
12131253 HGline = src [idx + 2 ].split ('=' )
0 commit comments