import sqlite3
class DatabaseAccess:
- """
- TODO class comments here
- """
-
- def __init__ (self, dbname):
- self.dbname = dbname
-
- def connect(self):
- self.conn = sqlite3.connect(self.dbname)
- self.cursor = self.conn.cursor()
-
- def disconnect(self):
- self.cursor.close()
- self.conn.close()
-
- def get_cursor(self):
- return self.cursor
-
- def get_connection(self):
- return self.conn
-
- def get_status(self):
- tables = ['swarms', 'btclients', 'client_session', 'status_messages', 'verbose_messages']
- for t in tables:
- try:
- self.cursor.execute("select * from '%s'" %t)
- for row in self.cursor:
- print row
- except sqlite3.Error, e:
- print "[select] error: ", e.args[0]
-
- def insert_swarms_row(self, row):
- try:
- self.cursor.execute("insert into swarms(torrent, filesize, purpose, source) values (?,?,?,?)", row)
- self.conn.commit()
- except sqlite3.Error, e:
- print ("[swarms]An error ocurred: ", e.args[0])
-
- def insert_swarms(self, torrent_file, filesize, purpose, source):
- insert_swarms_row([torrent_file, filesize, purpose, source])
-
- def insert_btclients_row(self, row):
- try:
- self.cursor.execute("insert into btclients(name, language, dht, streaming) values (?,?,?,?)", row)
- self.conn.commit()
- except sqlite3.Error, e:
- print ("[btclients]An error ocurred: ", e.args[0])
-
- def insert_btclients(self, client_name, language, dht, streaming):
- btclients([client_name, language, dht, streaming])
-
- def insert_client_session_row(self, row):
- try:
- self.cursor.execute("insert into client_session(swarm_id, client_id, system_os, system_os_version, system_ram, system_cpu, public_ip, public_port, ds_limit, us_limit, start_time) values (?,?,?,?,?,?,?,?,?,?,?)", row)
- self.conn.commit()
- except sqlite3.Error, e:
- print ("[client_session]An error ocurred: ", e.args[0])
-
- def insert_client_session(self, swarm_id, client_id, system_os, system_os_version, system_ram, system_cpu, public_ip, public_port, ds_limit, us_limit, start_time):
- insert_client_session_row([swarm_id, client_id, system_os, system_os_version, system_ram, system_cpu, public_ip, public_port, ds_limit, us_limit, start_time]);
-
- def insert_status_messages_row(self, row):
- try:
- self.cursor.execute("insert into status_messages values (?,?,?,?,?,?,?,?,?)", row)
- self.conn.commit()
- except sqlite3.Error, e:
- print ("[status_messages]An error ocurred: ", e.args[0])
-
- def insert_status_message(self, cs_id, timestamp, peer_num, dht, download_speed, upload_speed, download_size, upload_size, eta):
- insert_status_message_row([cs_id, timestamp, peer_num, dht, download_speed, upload_speed, download_size, upload_size, eta])
-
- def insert_verbose_messages_row(self, row):
- try:
- self.cursor.execute("insert into verbose_messages values (?,?,?,?,?,?,?,?,?)", row)
- self.conn.commit()
- except sqlite3.Error, e:
- print ("[verbose_messages]An error ocurred: ", e.args[0])
-
- def insert_verbose_messages(self, cs_id, timestamp, peer_ip, peer_port, message_type, _index, begin, length, listen_port):
- insert_verbose_messages_row([cs_id, timestamp, peer_ip, peer_port, message_type, _index, begin, length, listen_port])
+ """
+ Low-level class for database access: insert, update, delete,
+ select operations
+ """
+
+ def __init__ (self, dbname):
+ self.dbname = dbname
+
+ def connect(self):
+ self.conn = sqlite3.connect(self.dbname)
+ self.cursor = self.conn.cursor()
+
+ def disconnect(self):
+ self.cursor.close()
+ self.conn.close()
+
+ def get_cursor(self):
+ return self.cursor
+
+ def get_connection(self):
+ return self.conn
+
+ def get_status(self):
+ tables = ['swarms', 'btclients', 'client_sessions', 'status_messages', 'verbose_messages']
+ for t in tables:
+ try:
+ self.cursor.execute("select * from '%s'" %t)
+ for row in self.cursor:
+ print row
+ except sqlite3.Error, e:
+ print "[select] error: ", e.args[0]
+
+ def insert_swarms_row(self, row):
+ try:
+ self.cursor.execute("insert into swarms(torrent, filesize, purpose, source) values (?,?,?,?)", row)
+ self.conn.commit()
+ except sqlite3.Error, e:
+ print ("[swarms]An error ocurred: ", e.args[0])
+
+ def insert_swarms(self, torrent_file, filesize, purpose, source):
+ self.insert_swarms_row([torrent_file, filesize, purpose, source])
+
+ def select_swarms(self, swarm_id = -1):
+ try:
+ if swarm_id == -1:
+ self.cursor.execute("select * from swarms")
+ else:
+ self.cursor.execute("select * from swarms where id='%s'" %swarm_id)
+ for row in self.cursor:
+ print row
+ except sqlite3.Error, e:
+ print("[swarms]An error ocurred: ", e.args[0])
+
+ def delete_swarms(self, swarm_id = -1):
+ try:
+ if swarm_id == -1:
+ self.cursor.execute("delete from swarms")
+ else:
+ self.cursor.execute("delete from swarms where id=?", (swarm_id,))
+ self.conn.commit()
+ except sqlite3.Error, e:
+ print("[swarms]An error ocurred: ", e.args[0])
+
+ def insert_btclients_row(self, row):
+ try:
+ self.cursor.execute("insert into btclients(name, language, dht, streaming) values (?,?,?,?)", row)
+ self.conn.commit()
+ except sqlite3.Error, e:
+ print ("[btclients]An error ocurred: ", e.args[0])
+
+ def insert_btclients(self, client_name, language, dht, streaming):
+ btclients([client_name, language, dht, streaming])
+
+ def select_btclient(self, client_name):
+ try:
+ self.cursor.execute("""select * from btclients where name='%s'""" %client_name)
+ for row in self.cursor:
+ print row
+ except sqlite3.Error, e:
+ print ("[btclients]An error ocurred: ", e.args[0])
+
+ def select_btclient_id_by_name(self, client_name):
+ try:
+ self.cursor.execute("""select * from btclients where name='%s'""" %client_name)
+ for row in self.cursor:
+ return row[0]
+ except sqlite3.Error, e:
+ print ("[btclients]An error ocurred: ", e.args[0])
+
+ def insert_client_sessions_row(self, row):
+ try:
+ self.cursor.execute("insert into client_sessions(swarm_id, client_id, system_os, system_os_version, system_ram, system_cpu, public_ip, public_port, ds_limit, us_limit, start_time) values (?,?,?,?,?,?,?,?,?,?,?)", row)
+ self.conn.commit()
+ except sqlite3.Error, e:
+ print ("[client_sessions]An error ocurred: ", e.args[0])
+
+ def insert_client_sessions(self, swarm_id, client_id, system_os, system_os_version, system_ram, system_cpu, public_ip, public_port, ds_limit, us_limit, start_time):
+ self.insert_client_sessions_row([swarm_id, client_id, system_os, system_os_version, system_ram, system_cpu, public_ip, public_port, ds_limit, us_limit, start_time]);
+
+ def select_client_sessions_by_id(self, cs_id = -1):
+ try:
+ if cs_id == -1:
+ self.cursor.execute("""select * from client_sessions""")
+ else:
+ self.cursor.execute("""select * from client_sessions where id='%s'""" %cs_id)
+ for row in self.cursor:
+ print row
+ except sqlite3.Error, e:
+ print ("[client_sessions]An error ocurred: ", e.args[0])
+
+ def select_client_sessions_by_swarm(self, swarm_id = -1, client_id = None):
+ try:
+ if client_id == None:
+ if swarm_id == -1:
+ self.cursor.execute("""select * from client_sessions""")
+ else:
+ self.cursor.execute("""select * from client_sessions where swarm_id=?""", (swarm_id, ))
+ for row in self.cursor:
+ print row
+ else:
+ if swarm_id == -1:
+ self.cursor.execute("""select * from client_sessions where client_id=?""", (client_id, ))
+ else:
+ self.cursor.execute("""select * from client_sessions where swarm_id=? and client_id=?""", (swarm_id, client_id))
+ for row in self.cursor:
+ print row
+ except sqlite3.Error, e:
+ print ("[client_sessions]An error ocurred: ", e.args[0])
+
+ def delete_client_sessions_by_id(self, cs_id = -1):
+ try:
+ if cs_id == -1:
+ self.cursor.execute("""delete from client_sessions""")
+ else:
+ self.cursor.execute("""delete from client_sessions where id=?""", (cs_id, ))
+ self.conn.commit()
+ except sqlite3.Error, e:
+ print ("[client_sessions]An error ocurred: ", e.args[0])
+
+ def delete_client_sessions_by_swarm(self, swarm_id = -1, client_id = None):
+ try:
+ if client_id == None:
+ if swarm_id == -1:
+ self.cursor.execute("""delete from client_sessions""")
+ else:
+ self.cursor.execute("""delete from client_sessions where swarm_id=?""", (swarm_id, ))
+ else:
+ if swarm_id == -1:
+ self.cursor.execute("""delete from client_sessions where client_id=?""", (client_id, ))
+ else:
+ self.cursor.execute("""delete from client_sessions where swarm_id=? and client_id=?""", (swarm_id, client_id))
+ self.conn.commit()
+ except sqlite3.Error, e:
+ print ("[client_sessions]An error ocurred: ", e.args[0])
+
+ def insert_status_messages_row(self, row):
+ try:
+ self.cursor.execute("insert into status_messages values (?,?,?,?,?,?,?,?,?)", row)
+ self.conn.commit()
+ except sqlite3.Error, e:
+ print ("[status_messages]An error ocurred: ", e.args[0])
+
+ def insert_status_message(self, cs_id, timestamp, peer_num, dht, download_speed, upload_speed, download_size, upload_size, eta):
+ self.insert_status_message_row([cs_id, timestamp, peer_num, dht, download_speed, upload_speed, download_size, upload_size, eta])
+
+ def insert_verbose_messages_row(self, row):
+ try:
+ self.cursor.execute("insert into verbose_messages values (?,?,?,?,?,?,?,?,?)", row)
+ self.conn.commit()
+ except sqlite3.Error, e:
+ print ("[verbose_messages]An error ocurred: ", e.args[0])
+
+ def insert_verbose_messages(self, cs_id, timestamp, peer_ip, peer_port, message_type, _index, begin, length, listen_port):
+ self.insert_verbose_messages_row([cs_id, timestamp, peer_ip, peer_port, message_type, _index, begin, length, listen_port])
"""
Test case
"""
def main():
- dba = DatabaseAccess("p2p-next.db")
-
- dba.connect()
-
- for t in [('DarkKnight', '123000', 'experiment', 'TVTorrents'),
- ('Fedora', '1024', 'experiment', 'local'),
- ('Pulp Fiction', '102400', 'streaming', 'isohunt'),
- ('Karaoke', 'anaaremere', 'streaming', 'local'),
- ]:
- dba.insert_swarms_row(t)
-
- for t in [('Tribler', 'Python', '1', '1'),
- ('libtorrent', 'C++', '1', '0'),
- ('Vuze', 'Java', '1', '0'),
- ('Transmission', 'C', 'asa', '0'),
- ]:
- dba.insert_btclients_row(t)
-
- for t in [('1', '2', 'Linux', '2.6.30', '256', '1833', '0.0.0.0', '6969', '256', '96', '123131.1231'),
- ('3', '4', 'Linux', '2.6.30', '256', '1833', '0.0.0.0', '6969', '256', '96', '123131.1231'),
- ]:
- dba.insert_client_session_row(t)
-
- for t in [('1', '2455128.10', '222', '0', '213', '56', '200', '300', '121.324'),
- ('6', '2455128.10', '222', '0', '213', '56', '200', '300', '121.324'),
- ]:
- dba.insert_status_messages_row(t)
-
- for t in [('1', '2455128.121295811', '127.0.0.1', '1345', '0', '3', '4', '13', '777'),
- ('4', '2455128.121295811', '127.0.0.1', '1345', '0', '3', '4', '13', '777'),
- ]:
- dba.insert_verbose_messages_row(t)
-
- dba.get_status()
-
- dba.disconnect()
-
- """
- for t in [('1', 'mumu', '1024', 'ceva', 'URL'),
- ('2', 'gugu', '1024', 'ceva', 'URL'),
- ('3', 'gaga', '1024', 'ceva', 'URL'),
- ]:
- insert_swarms(t)
-
- for t in [('1', 'tribler', 'python', 1, 0),
- ]:
- insert_btclients(t)
-
- for t in [('1', '1', '2', 'Linux', '2.6.30', '256', '1833', '0.0.0.0', '6969', '256', '96', '123131.1231')
- ]:
- insert_client_session(t)
-
- tables = ['swarms', 'btclients', 'swarms', 'client_session', 'status_messages', 'verbose_messages']
- for t in tables:
- curs.execute("select * from '%s'" %t)
- for row in curs:
- print row
- """
+ dba = DatabaseAccess("p2p-next.db")
+
+ dba.connect()
+
+ for t in [('DarkKnight', '123000', 'experiment', 'TVTorrents'),
+ ('Fedora', '1024', 'experiment', 'local'),
+ ('Pulp Fiction', '102400', 'streaming', 'isohunt'),
+ ('Karaoke', 'anaaremere', 'streaming', 'local'),
+ ]:
+ dba.insert_swarms_row(t)
+
+ for t in [('Tribler', 'Python', '1', '1'),
+ ('libtorrent', 'C++', '1', '0'),
+ ('Vuze', 'Java', '1', '0'),
+ ('Transmission', 'C', 'asa', '0'),
+ ]:
+ dba.insert_btclients_row(t)
+
+ for t in [('1', '2', 'Linux', '2.6.30', '256', '1833', '0.0.0.0', '6969', '256', '96', '123131.1231'),
+ ('3', '4', 'Linux', '2.6.30', '256', '1833', '0.0.0.0', '6969', '256', '96', '123131.1231'),
+ ]:
+ dba.insert_client_sessions_row(t)
+
+ for t in [('1', '2455128.10', '222', '0', '213', '56', '200', '300', '121.324'),
+ ('6', '2455128.10', '222', '0', '213', '56', '200', '300', '121.324'),
+ ]:
+ dba.insert_status_messages_row(t)
+
+ for t in [('1', '2455128.121295811', '127.0.0.1', '1345', '0', '3', '4', '13', '777'),
+ ('4', '2455128.121295811', '127.0.0.1', '1345', '0', '3', '4', '13', '777'),
+ ]:
+ dba.insert_verbose_messages_row(t)
+
+ dba.get_status()
+
+ dba.select_btclient("Tribler")
+
+ dba.disconnect()
+
+ """
+ for t in [('1', 'mumu', '1024', 'ceva', 'URL'),
+ ('2', 'gugu', '1024', 'ceva', 'URL'),
+ ('3', 'gaga', '1024', 'ceva', 'URL'),
+ ]:
+ insert_swarms(t)
+
+ for t in [('1', 'tribler', 'python', 1, 0),
+ ]:
+ insert_btclients(t)
+
+ for t in [('1', '1', '2', 'Linux', '2.6.30', '256', '1833', '0.0.0.0', '6969', '256', '96', '123131.1231')
+ ]:
+ insert_client_sessions(t)
+
+ tables = ['swarms', 'btclients', 'swarms', 'client_sessions', 'status_messages', 'verbose_messages']
+ for t in tables:
+ curs.execute("select * from '%s'" %t)
+ for row in curs:
+ print row
+ """
if __name__ == "__main__":
- sys.exit(main())
+ sys.exit(main())
--- /dev/null
+"""sidereal.py: A Python module for astronomical calculations.
+
+ For documentation, see:
+ http://www.nmt.edu/tcc/help/lang/python/examples/sidereal/ims/
+"""
+#================================================================
+# Imports
+#----------------------------------------------------------------
+
+from math import *
+import re
+import datetime
+#================================================================
+# Manifest constants
+#----------------------------------------------------------------
+
+FIRST_GREGORIAN_YEAR = 1583
+TWO_PI = 2.0 * pi
+PI_OVER_12 = pi / 12.0
+JULIAN_BIAS = 2200000 # 2,200,000
+SIDEREAL_A = 0.0657098
+FLOAT_PAT = re.compile (
+ r'\d+' # Matches one or more digits
+ r'(' # Start optional fraction
+ r'[.]' # Matches the decimal point
+ r'\d+' # Matches one or more digits
+ r')?' ) # End optional group
+D_PAT = re.compile ( r'[dD]' )
+M_PAT = re.compile ( r'[mM]' )
+S_PAT = re.compile ( r'[sS]' )
+H_PAT = re.compile ( r'[hH]' )
+NS_PAT = re.compile ( r'[nNsS]' )
+EW_PAT = re.compile ( r'[eEwW]' )
+# - - - h o u r s T o R a d i a n s
+
+def hoursToRadians ( hours ):
+ """Convert hours (15 degrees) to radians.
+ """
+ return hours * PI_OVER_12
+# - - - r a d i a n s T o H o u r s
+
+def radiansToHours ( radians ):
+ """Convert radians to hours (15 degrees).
+ """
+ return radians / PI_OVER_12
+# - - - h o u r A n g l e T o R A
+
+def hourAngleToRA ( h, ut, eLong ):
+ """Convert hour angle to right ascension.
+
+ [ (h is an hour angle in radians as a float) and
+ (ut is a timestamp as a datetime.datetime instance) and
+ (eLong is an east longitude in radians) ->
+ return the right ascension in radians corresponding
+ to that hour angle at that time and location ]
+ """
+ #-- 1 --
+ # [ gst := the Greenwich Sidereal Time equivalent to
+ # ut, as a SiderealTime instance ]
+ gst = SiderealTime.fromDatetime ( ut )
+ #-- 2 --
+ # [ lst := the local time corresponding to gst at
+ # longitude eLong ]
+ lst = gst.lst ( eLong )
+ #-- 3 --
+ # [ alpha := lst - h, normalized to [0,2*pi) ]
+ alpha = (lst.radians - h) % TWO_PI
+
+ #-- 4 --
+ return alpha
+# - - - r a T o H o u r A n g l e
+
+def raToHourAngle ( ra, ut, eLong ):
+ """Convert right ascension to hour angle.
+
+ [ (ra is a right ascension in radians as a float) and
+ (ut is a timestamp as a datetime.datetime instance) and
+ (eLong is an east longitude in radians) ->
+ return the hour angle in radians at that time and
+ location corresponding to that right ascension ]
+ """
+ #-- 1 --
+ # [ gst := the Greenwich Sidereal Time equivalent to
+ # ut, as a SiderealTime instance ]
+ gst = SiderealTime.fromDatetime ( ut )
+
+ #-- 2 --
+ # [ lst := the local time corresponding to gst at
+ # longitude eLong ]
+ lst = gst.lst ( eLong )
+ #-- 3 --
+ # [ h := lst - ra, normalized to [0,2*pi) ]
+ h = (lst.radians - ra) % TWO_PI
+
+ #-- 4 --
+ return h
+# - - - d a y N o
+
+def dayNo ( dt ):
+ """Compute the day number within the year.
+
+ [ dt is a date as a datetime.datetime or datetime.date ->
+ return the number of days between dt and Dec. 31 of
+ the preceding year ]
+ """
+ #-- 1 --
+ # [ dateOrd := proleptic Gregorian ordinal of dt
+ # jan1Ord := proleptic Gregorian ordinal of January 1
+ # of year (dt.year) ]
+ dateOrd = dt.toordinal()
+ jan1Ord = datetime.date ( dt.year, 1, 1 ).toordinal()
+
+ #-- 2 --
+ return dateOrd - jan1Ord + 1
+# - - - p a r s e D a t e t i m e
+
+T_PATTERN = re.compile ( '[tT]' )
+
+def parseDatetime ( s ):
+ """Parse a date with optional time.
+
+ [ s is a string ->
+ if s is a valid date with optional time ->
+ return that timestamp as a datetime.datetime instance
+ else -> raise SyntaxError ]
+ """
+ #-- 1 --
+ # [ if s contains "T" or "t" ->
+ # rawDate := s up to the first such character
+ # rawTime := s from just after the first such
+ # character to the end
+ # else ->
+ # rawDate := s
+ # rawTime := None ]
+ m = T_PATTERN.search ( s )
+ if m is None:
+ rawDate, rawTime = s, None
+ else:
+ rawDate = s[:m.start()]
+ rawTime = s[m.end():]
+ #-- 2 --
+ # [ if rawDate is a valid date ->
+ # datePart := rawDate as a datetime.datetime instance
+ # else -> raise SyntaxError ]
+ datePart = parseDate ( rawDate )
+ #-- 3 --
+ # [ if rawTime is None ->
+ # timePart := 00:00 as a datetime.time
+ # else if rawTime is valid ->
+ # timePart := rawTime as a datetime.time
+ # else -> raise SyntaxError ]
+ if rawTime is None:
+ timePart = datetime.time ( 0, 0 )
+ else:
+ timePart = parseTime ( rawTime )
+ #-- 4 --
+ return datetime.datetime.combine ( datePart, timePart )
+# - - - p a r s e D a t e
+
+YEAR_FIELD = "Y"
+MONTH_FIELD = "M"
+DAY_FIELD = "D"
+
+dateRe = (
+ r'(' # Begin YEAR_FIELD
+ r'?P<%s>' # Name this group YEAR_FIELD
+ r'\d{4}' # Match exactly four digits
+ r')' # End YEAR_FIELD
+ r'\-' # Matches one hyphen
+ r'(' # Begin MONTH_FIELD
+ r'?P<%s>' # Name this group MONTH_FIELD
+ r'\d{1,2}' # Matches one or two digits
+ r')' # End MONTH_FIELD
+ r'\-' # Matches "-"
+ r'(' # Begin DAY_FIELD
+ r'?P<%s>' # Name this group DAY_FIELD
+ r'\d{1,2}' # Matches one or two digits
+ r')' # End DAY_FIELD
+ r'$' # Make sure all characters match
+ ) % (YEAR_FIELD, MONTH_FIELD, DAY_FIELD)
+DATE_PAT = re.compile ( dateRe )
+
+def parseDate ( s ):
+ """Validate and convert a date in external form.
+
+ [ s is a string ->
+ if s is a valid external date string ->
+ return that date as a datetime.date instance
+ else -> raise SyntaxError ]
+ """
+ #-- 1 --
+ # [ if DATE_PAT matches s ->
+ # m := a match instance describing the match
+ # else -> raise SyntaxError ]
+ m = DATE_PAT.match ( s )
+ if m is None:
+ raise SyntaxError, ( "Date does not have pattern YYYY-DD-MM: "
+ "'%s'" % s )
+ #-- 2 --
+ year = int ( m.group ( YEAR_FIELD ) )
+ month = int ( m.group ( MONTH_FIELD ) )
+ day = int ( m.group ( DAY_FIELD ) )
+
+ #-- 3 --
+ return datetime.date ( year, month, day )
+# - - - p a r s e T i m e
+
+def parseTime ( s ):
+ """Validate and convert a time and optional zone.
+
+ [ s is a string ->
+ if s is a valid time with optional zone suffix ->
+ return that time as a datetime.time
+ else -> raise SyntaxError ]
+ """
+ #-- 1 -
+ # [ if s starts with FLOAT_PAT ->
+ # decHour := matching part of s as a float
+ # minuteTail := part s past the match
+ # else -> raise SyntaxError ]
+ decHour, minuteTail = parseFloat ( s, "Hour number" )
+ #-- 2 --
+ # [ if minuteTail starts with ":" followed by FLOAT_PAT ->
+ # decMinute := part matching FLOAT_PAT as a float
+ # secondTail := part of minuteTail after the match
+ # else if minuteTail starts with ":" not followed by
+ # FLOAT_PAT ->
+ # raise SyntaxError
+ # else ->
+ # decMinute := 0.0
+ # secondTail := minuteTail ]
+ if minuteTail.startswith(':'):
+ m = FLOAT_PAT.match ( minuteTail[1:] )
+ if m is None:
+ raise SyntaxError, ( "Expecting minutes: '%s'" %
+ minuteTail )
+ else:
+ decMinute = float(m.group())
+ secondTail = minuteTail[m.end()+1:]
+ else:
+ decMinute = 0.0
+ secondTail = minuteTail
+ #-- 3 --
+ # [ if secondTail starts with ":" followed by FLOAT_PAT ->
+ # decSecond := part matching FLOAT_PAT as a float
+ # zoneTail := part of secondTail after the match
+ # else if secondTail starts with ":" not followed by
+ # FLOAT_PAT ->
+ # raise SyntaxError
+ # else ->
+ # decSecond := 0.0
+ # zoneTail := secondTail ]
+ if secondTail.startswith(':'):
+ m = FLOAT_PAT.match ( secondTail[1:] )
+ if m is None:
+ raise SyntaxError, ( "Expecting seconds: '%s'" %
+ secondTail )
+ else:
+ decSecond = float(m.group())
+ zoneTail = secondTail[m.end()+1:]
+ else:
+ decSecond = 0.0
+ zoneTail = secondTail
+ #-- 4 --
+ # [ if zoneTail is empty ->
+ # tz := None
+ # else if zoneTail is a valid zone suffix ->
+ # tz := that zone information as an instance of a class
+ # that inherits from datetime.tzinfo
+ # else -> raise SyntaxError ]
+ if len(zoneTail) == 0:
+ tz = None
+ else:
+ tz = parseZone ( zoneTail )
+ #-- 5 --
+ # [ hours := decHour + decMinute/60.0 + decSecond/3600.0 ]
+ hours = dmsUnits.mixToSingle ( (decHour, decMinute, decSecond) )
+ #-- 6 --
+ # [ return a datetime.time representing hours ]
+ hh, mm, seconds = dmsUnits.singleToMix ( hours )
+ wholeSeconds, fracSeconds = divmod ( seconds, 1.0 )
+ ss = int(wholeSeconds)
+ usec = int ( fracSeconds * 1e6 )
+ return datetime.time ( hh, mm, ss, usec, tz )
+# - - - p a r s e Z o n e
+
+def parseZone ( s ):
+ """Validate and convert a time zone suffix.
+
+ [ s is a string ->
+ if s is a valid time zone suffix ->
+ return that zone's information as an instance of
+ a class that inherits from datetime.tzinfo
+ else -> raise SyntaxError ]
+ """
+ #-- 1 --
+ # [ if s starts with "+" or "-" and is a valid fixed-offset
+ # time zone suffix ->
+ # return that zone's information as a datetime.tzinfo instance
+ # else if is starts with "+" or "-" but is not a valid
+ # fixed-offset time zone suffix ->
+ # raise SyntaxError
+ # else -> I ]
+ if s.startswith("+") or s.startswith("-"):
+ return parseFixedZone ( s )
+
+ #-- 2 --
+ # [ if s.upper() is a key in zoneCodeMap ->
+ # return the corresponding value
+ # else -> raise SyntaxError ]
+ try:
+ tz = zoneCodeMap[s.upper()]
+ return tz
+ except KeyError:
+ raise SyntaxError, ( "Unknown time zone code: '%s'" % s )
+# - - - p a r s e F i x e d Z o n e
+
+HHMM_PAT = re.compile (
+ r'\d{4}' # Matches exactly four digits
+ r'$' ) # Be sure everything is matched
+
+def parseFixedZone ( s ):
+ """Convert a +hhmm or -hhmm zone suffix.
+
+ [ s is a string ->
+ if s is a time zone suffix of the form "+hhmm" or "-hhmm" ->
+ return that zone information as an instance of a class
+ that inherits from datetime.tzinfo
+ else -> raise SyntaxError ]
+ """
+ #-- 1 --
+ if s.startswith('+'): sign = 1
+ elif s.startswith('-'): sign = -1
+ else:
+ raise SyntaxError, ( "Expecting zone modifier as %shhmm: "
+ "'%s'" % (s[0], s) )
+ #-- 2 --
+ # [ if s[1:] matches HHMM_PAT ->
+ # hours := the HH part as an int
+ # minutes := the MM part as an int
+ # else -> raise SyntaxError ]
+ rawHHMM = s[1:]
+ m = HHMM_PAT.match ( rawHHMM )
+ if m is None:
+ raise SyntaxError, ( "Expecting zone modifier as %sHHMM: "
+ "'%s'" % (s[0], s) )
+ else:
+ hours = int ( rawHHMM[:2] )
+ minutes = int ( rawHHMM[2:] )
+
+ #-- 3 --
+ return FixedZone ( sign*hours, sign*minutes, s )
+
+# - - - - - c l a s s F i x e d Z o n e
+
+DELTA_ZERO = datetime.timedelta(0)
+DELTA_HOUR = datetime.timedelta(hours=1)
+
+class FixedZone(datetime.tzinfo):
+ """Represents a time zone with a fixed offset east of UTC.
+
+ Exports:
+ FixedZone ( hours, minutes, name ):
+ [ (hours is a signed offset in hours as an int) and
+ (minutes is a signed offset in minutes as an int) ->
+ return a new FixedZone instance representing
+ those offsets east of UTC ]
+ State/Invariants:
+ .__offset:
+ [ a datetime.timedelta representing self's offset
+ east of UTC ]
+ .__name:
+ [ as passed to the constructor's name argument ]
+ """
+ def __init__ ( self, hh, mm, name ):
+ """Constructor for FixedZone.
+ """
+ self.__offset = datetime.timedelta ( hours=hh, minutes=mm )
+ self.__name = name
+ def utcoffset(self, dt):
+ """Return self's offset east of UTC.
+ """
+ return self.__offset
+ def tzname(self, dt):
+ """Return self's name.
+ """
+ return self.__name
+ def dst(self, dt):
+ """Return self's daylight time offset.
+ """
+ return DELTA_ZERO
+def firstSundayOnOrAfter ( dt ):
+ """Find the first Sunday on or after a given date.
+
+ [ dt is a datetime.date ->
+ return a datetime.date representing the first Sunday
+ on or after dt ]
+ """
+ daysToGo = dt.weekday()
+ if daysToGo:
+ dt += datetime.timedelta ( daysToGo )
+ return dt
+
+# - - - - - c l a s s U S T i m e Z o n e
+
+class USTimeZone(datetime.tzinfo):
+ """Represents a U.S. time zone, with automatic daylight time.
+
+ Exports:
+ USTimeZone ( hh, mm, name, stdName, dstName ):
+ [ (hh is an offset east of UTC in hours) and
+ (mm is an offset east of UTC in minutes) and
+ (name is the composite zone name) and
+ (stdName is the non-DST name) and
+ (dstName is the DST name) ->
+ return a new USTimeZone instance with those values ]
+
+ State/Invariants:
+ .__offset:
+ [ self's offset east of UTC as a datetime.timedelta ]
+ .__name: [ as passed to constructor's name ]
+ .__stdName: [ as passed to constructor's stdName ]
+ .__dstName: [ as passed to constructor's dstName ]
+ """
+ DST_START_OLD = datetime.datetime ( 1, 4, 1, 2 )
+ DST_END_OLD = datetime.datetime ( 1, 10, 25, 2 )
+ DST_START_2007 = datetime.datetime ( 1, 3, 8, 2 )
+ DST_END_2007 = datetime.datetime ( 1, 11, 1, 2 )
+ def __init__ ( self, hh, mm, name, stdName, dstName ):
+ self.__offset = datetime.timedelta ( hours=hh, minutes=mm )
+ self.__name = name
+ self.__stdName = stdName
+ self.__dstname = dstName
+ def tzname(self, dt):
+ if self.dst(dt): return self.__dstName
+ else: return self.__stdName
+ def utcoffset(self, dt):
+ return self.__offset + self.dst(dt)
+ def dst(self, dt):
+ """Return the current DST offset.
+
+ [ dt is a datetime.date ->
+ if daylight time is in effect in self's zone on
+ date dt ->
+ return +1 hour as a datetime.timedelta
+ else ->
+ return 0 as a datetime.delta ]
+ """
+ #-- 1 --
+ # [ dtStart := Sunday when DST starts in year dt.year
+ # dtEnd := Sunday when DST ends in year dt.year ]
+ if dt.year >= 2007:
+ startDate = self.DST_START_2007.replace ( year=dt.year )
+ endDate = self.DST_END_2007.replace ( year=dt.year )
+ else:
+ startDate = self.DST_START_OLD.replace ( year=dt.year )
+ endDate = self.DST_END_OLD.replace ( year=dt.year )
+ dtStart = firstSundayOnOrAfter ( startDate )
+ dtEnd = firstSundayOnOrAfter ( endDate )
+ #-- 2 --
+ # [ naiveDate := dt with its tzinfo member set to None ]
+ naiveDate = dt.replace ( tzinfo=None )
+ #-- 3 --
+ # [ if naiveDate is in the interval (dtStart, dtEnd) ->
+ # return DELTA_HOUR
+ # else ->
+ # return DELTA_ZERO ]
+ if dtStart <= naiveDate < dtEnd:
+ return DELTA_HOUR
+ else:
+ return DELTA_ZERO
+utcZone = FixedZone(0, 0, "UTC")
+
+estZone = FixedZone(-5, 0, "EST")
+edtZone = FixedZone(-4, 0, "EDT")
+etZone = USTimeZone(-5, 0, "ET", "EST", "EDT")
+
+cstZone = FixedZone(-6, 0, "CST")
+cdtZone = FixedZone(-5, 0, "CDT")
+ctZone = USTimeZone(-6, 0, "CT", "CST", "CDT")
+
+mstZone = FixedZone(-7, 0, "MST")
+mdtZone = FixedZone(-6, 0, "MDT")
+mtZone = USTimeZone(-7, 0, "MT", "MST", "MDT")
+
+pstZone = FixedZone(-8, 0, "PST")
+pdtZone = FixedZone(-7, 0, "PDT")
+ptZone = USTimeZone(-8, 0, "PT", "PST", "PDT")
+
+zoneCodeMap = {
+ "UTC": utcZone,
+ "EST": estZone, "EDT": edtZone, "ET": etZone,
+ "CST": cstZone, "CDT": cdtZone, "CT": ctZone,
+ "MST": mstZone, "MDT": mdtZone, "MT": mtZone,
+ "PST": pstZone, "PDT": pdtZone, "PT": ptZone }
+# - - - p a r s e A n g l e
+
+def parseAngle ( s ):
+ """Validate and convert an external angle.
+
+ [ s is a string ->
+ if s is a valid external angle ->
+ return s in radians
+ else -> raise SyntaxError ]
+ """
+ #-- 1 --
+ minute = second = 0.0
+ #-- 2 --
+ # [ if s starts with a float followed by 'd' or 'D' ->
+ # degree := that float as type float
+ # minTail := s after that float and suffix
+ # else -> raise SyntaxError ]
+ degree, minTail = parseFloatSuffix ( s, D_PAT,
+ "Degrees followed by 'd'" )
+
+ #-- 3 --
+ # [ if minTail is empty -> I
+ # else if minTail has the form "(float)m" ->
+ # minute := that (float)
+ # else if minTail has the form "(float)m(float)s" ->
+ # minute := the first (float)
+ # second := the second (float)
+ # else -> raise SyntaxError ]
+ if len(minTail) != 0:
+ #-- 3.1 --
+ # [ if minTail starts with a float followed by 'm' or 'M' ->
+ # minute := that float as type float
+ # secTail := minTail after all that
+ # else -> raise SyntaxError ]
+ minute, secTail = parseFloatSuffix ( minTail, M_PAT,
+ "Minutes followed by 'm'" )
+
+ #-- 3.2 --
+ # [ if secTail is empty -> I
+ # else if secTail starts with a float followed by
+ # 's' or 'S' ->
+ # second := that float as type float
+ # checkTail := secTail after all that
+ # else -> raise SyntaxError ]
+ if len(secTail) != 0:
+ second, checkTail = parseFloatSuffix ( secTail,
+ S_PAT, "Seconds followed by 's'" )
+ if len(checkTail) != 0:
+ raise SyntaxError, ( "Unidentifiable angle parts: "
+ "'%s'" % checkTail )
+ #-- 4 --
+ # [ return the angle (degree, minute, second) in radians ]
+ angleDegrees = dmsUnits.mixToSingle ( (degree, minute, second) )
+ return radians ( angleDegrees )
+# - - - p a r s e F l o a t S u f f i x
+
+def parseFloatSuffix ( s, codeRe, message ):
+ """Parse a float followed by a letter code.
+
+ [ (s is a string) and
+ (codeRe is a compiled regular expression) and
+ (message is a string describing what is expected) ->
+ if s starts with a float, followed by code (using
+ case-insensitive comparison) ->
+ return (x, tail) where x is that float as type float
+ and tail is the part of s after the float and code
+ else -> raise SyntaxError, "Expecting (message)" ]
+ """
+ #-- 1 --
+ # [ if s starts with a float ->
+ # x := that float as type float
+ # codeTail := the part of s after that float
+ # else -> raise SyntaxError, "Expecting (message)" ]
+ x, codeTail = parseFloat ( s, message )
+
+ #-- 2 --
+ # [ if codeTail starts with code (case-insensitive) ->
+ # return (x, the part of codeTail after the match)
+ # else -> raise SyntaxError ]
+ discard, tail = parseRe ( codeTail, codeRe, message )
+
+ #-- 3 --
+ return (x, tail)
+# - - - p a r s e F l o a t
+
+def parseFloat ( s, message ):
+ """Parse a floating-point number at the front of s.
+
+ [ (s is a string) and
+ (message is a string describing what is expected) ->
+ if s begins with a floating-point number ->
+ return (x, tail) where x is the number as type float
+ and tail is the part of s after the match
+ else -> raise SyntaxError, "Expecting (message)" ]
+ """
+ #-- 1 --
+ # [ if the front of s matches FLOAT_PAT ->
+ # m := a Match object describing the match
+ # else -> raise SyntaxError ]
+ rawFloat, tail = parseRe ( s, FLOAT_PAT, message )
+
+ #-- 2 --
+ return (float(rawFloat), tail)
+# - - - p a r s e R e
+
+def parseRe ( s, regex, message ):
+ """Parse a regular expression at the head of a string.
+
+ [ (s is a string) and
+ (regex is a compiled regular expression) and
+ (message is a string describing what is expected) ->
+ if s starts with a string that matches regex ->
+ return (head, tail) where head is the part of s
+ that matched and tail is the rest
+ else ->
+ raise SyntaxError, "Expecting (message)" ]
+ """
+
+ #-- 1 --
+ # [ if the head of s matches regex ->
+ # m := a match object describing the matching part
+ # else -> raise SyntaxError, "Expecting (message)" ]
+ m = regex.match ( s )
+ if m is None:
+ raise SyntaxError, "Expecting %s: '%s'" % (message, s)
+ #-- 2 --
+ # [ return (matched text from s, text from s after match) ]
+ head = m.group()
+ tail = s[m.end():]
+ return (head, tail)
+# - - - p a r s e L a t
+
+def parseLat ( s ):
+ """Validate and convert an external latitude.
+
+ [ s is a nonempty string ->
+ if s is a valid external latitude ->
+ return that latitude in radians
+ else -> raise SyntaxError ]
+ """
+ #-- 1 --
+ # [ last := last character of s
+ # rawAngle := s up to the last character ]
+ last = s[-1]
+ rawAngle = s[:-1]
+
+ #-- 2 --
+ # [ if last matches NS_PAT ->
+ # nsFlag := last, lowercased
+ # else -> raise SyntaxError ]
+ m = NS_PAT.match ( last )
+ if m is None:
+ raise SyntaxError, ( "Latitude '%s' does not end with 'n' "
+ "or 's'." % s )
+ else:
+ nsFlag = last.lower()
+ #-- 3 --
+ # [ if rawAngle is a valid angle ->
+ # absAngle := that angle in radians
+ # else -> raise SyntaxError ]
+ absAngle = parseAngle ( rawAngle )
+ #-- 4 --
+ if nsFlag == 's': angle = - absAngle
+ else: angle = absAngle
+
+ #-- 5 --
+ return angle
+# - - - p a r s e L o n
+
+def parseLon ( s ):
+ """Validate and convert an external longitude.
+
+ [ s is a nonempty string ->
+ if s is a valid external longitude ->
+ return that longitude in radians
+ else -> raise SyntaxError ]
+ """
+ #-- 1 --
+ # [ last := last character of s
+ # rawAngle := s up to the last character ]
+ last = s[-1]
+ rawAngle = s[:-1]
+
+ #-- 2 --
+ # [ if EW_PAT matches last ->
+ # ewFlag := last, lowercased
+ # else -> raise SyntaxError ]
+ m = EW_PAT.match ( last )
+ if m is None:
+ raise SyntaxError, ( "Longitude '%s' does not end with "
+ "'e' or 'w'." % s )
+ else:
+ ewFlag = last.lower()
+ #-- 3 --
+ # [ if rawAngle is a valid angle ->
+ # absAngle := that angle in radians
+ # else -> raise SyntaxError ]
+ absAngle = parseAngle ( rawAngle )
+ #-- 4 --
+ if ewFlag == 'w': angle = TWO_PI - absAngle
+ else: angle = absAngle
+
+ #-- 5 --
+ return angle
+# - - - p a r s e H o u r s
+
+def parseHours ( s ):
+ """Validate and convert a quantity in hours.
+
+ [ s is a non-empty string ->
+ if s is a valid mixed hours expression ->
+ return the value of s as decimal hours
+ else -> raise SyntaxError ]
+ """
+ #-- 1 --
+ minute = second = 0.0
+
+ #-- 2 --
+ # [ if s starts with a float followed by 'h' or 'H' ->
+ # hour := that float as type float
+ # minTail := s after that float and suffix
+ # else -> raise SyntaxError ]
+ hour, minTail = parseFloatSuffix ( s, H_PAT,
+ "Hours followed by 'h'" )
+
+ #-- 3 --
+ # [ if minTail is empty -> I
+ # else if minTail has the form "(float)m" ->
+ # minute := that (float)
+ # else if minTail has the form "(float)m(float)s" ->
+ # minute := the first (float)
+ # second := the second (float)
+ # else -> raise SyntaxError ]
+ if len(minTail) != 0:
+ #-- 3.1 --
+ # [ if minTail starts with a float followed by 'm' or 'M' ->
+ # minute := that float as type float
+ # secTail := minTail after all that
+ # else -> raise SyntaxError ]
+ minute, secTail = parseFloatSuffix ( minTail, M_PAT,
+ "Minutes followed by 'm'" )
+
+ #-- 3.2 --
+ # [ if secTail is empty -> I
+ # else if secTail starts with a float followed by
+ # 's' or 'S' ->
+ # second := that float as type float
+ # checkTail := secTail after all that
+ # else -> raise SyntaxError ]
+ if len(secTail) != 0:
+ second, checkTail = parseFloatSuffix ( secTail,
+ S_PAT, "Seconds followed by 's'" )
+ if len(checkTail) != 0:
+ raise SyntaxError, ( "Unidentifiable angle parts: "
+ "'%s'" % checkTail )
+ #-- 4 --
+ # [ return the quantity (hour, minute, second) in hours ]
+ result = dmsUnits.mixToSingle ( (hour, minute, second) )
+ return result
+
+# - - - - - c l a s s M i x e d U n i t s
+
+class MixedUnits:
+ """Represents a system with mixed units, e.g., hours/minutes/seconds
+ """
+# - - - M i x e d U n i t s . _ _ i n i t _ _
+
+ def __init__ ( self, factors ):
+ """Constructor
+ """
+ self.factors = factors
+# - - - M i x e d U n i t s . m i x T o S i n g l e
+
+ def mixToSingle ( self, coeffs ):
+ """Convert mixed units to a single value.
+
+ [ coeffs is a sequence of numbers not longer than
+ len(self.factors)+1 ->
+ return the equivalent single value in self's system ]
+ """
+ #-- 1 --
+ total = 0.0
+
+ #-- 2 --
+ # [ if len(coeffs) <= len(self.factors)+1 ->
+ # coeffList := a copy of coeffs, right-padded to length
+ # len(self.factors)+1 with zeroes if necessary ]
+ coeffList = self.__pad ( coeffs )
+ #-- 3 --
+ # [ total +:= (coeffList[-1] *
+ # (product of all elements of self.factors)) +
+ # (coeffList[-2] *
+ # (product of all elements of self.factors[:-1])) +
+ # (coeffList[-3] *
+ # (product of all elements of self.factors[:-2]))
+ # ... ]
+ for i in range ( -1, -len(self.factors)-1, -1):
+ total += coeffList[i]
+ total /= self.factors[i]
+ #-- 4 --
+ total += coeffList[0]
+
+ #-- 5 --
+ return total
+# - - - M i x e d U n i t s . _ _ p a d
+
+ def __pad ( self, coeffs ):
+ """Pad coefficient lists to standard length.
+
+ [ coeffs is a sequence of numbers ->
+ if len(coeffs) > len(self.factors)+1 ->
+ raise ValueError
+ else ->
+ return a list containing the elements of coeff,
+ plus additional zeroes on the right if necessary
+ so that the result has length len(self.factors)+1 ]
+ """
+ #-- 1 --
+ # [ stdLen := 1 + len(self.factors)
+ # shortage := 1 + len(self.factors) - len(coeffs)
+ # result := a copy of coeffs as a list ]
+ stdLen = 1 + len(self.factors)
+ shortage = stdLen - len(coeffs)
+ result = list(coeffs)
+
+ #-- 2 --
+ # [ if shortage < 0 ->
+ # raise ValueError
+ # else ->
+ # result := result + (a list of shortage zeroes) ]
+ if shortage < 0:
+ raise ValueError, ( "Value %s has too many elements; "
+ "max is %d." % (coeffs, stdLen) )
+ elif shortage > 0:
+ result += [0.0] * shortage
+
+ #-- 3 --
+ return result
+# - - - M i x e d U n i t s . s i n g l e T o M i x
+
+ def singleToMix ( self, value ):
+ """Convert to mixed units.
+
+ [ value is a float ->
+ return value as a sequence of coefficients in
+ self's system ]
+ """
+ #-- 1 --
+ # [ whole := whole part of value
+ # frac := fractional part of value ]
+ whole, frac = divmod ( value, 1.0 )
+ result = [int(whole)]
+ #-- 2 --
+ # [ result := result with integral parts of value
+ # in self's system appended ]
+ for factorx in range(len(self.factors)):
+ frac *= self.factors[factorx]
+ whole, frac = divmod ( frac, 1.0 )
+ result.append ( int(whole) )
+ #-- 3 --
+ # [ result := result with frac added to its last element ]
+ result[-1] += frac
+
+ #-- 4 --
+ return result
+# - - - M i x e d U n i t s . f o r m a t
+
+ def format ( self, coeffs, decimals=0, lz=False ):
+ """Format mixed units.
+
+ [ (coeffs is a sequence of numbers as returned by
+ MixedUnits.singleToMix()) and
+ (decimals is a nonnegative integer) and
+ (lz is a bool) ->
+ return a list of strings corresponding to the values
+ of coeffs, with all the values but the last formatted
+ as integers, all values zero padded iff lz is true,
+ and the last value with (decimals) digits after the
+ decimal point ]
+ """
+ #-- 1 --
+ coeffList = self.__pad ( coeffs )
+
+ #-- 2 --
+ # [ result := the values from coeffList[:-1] formatted
+ # as integers ]
+ if lz: fmt = "%02d"
+ else: fmt = "%d"
+ result = [ fmt % x
+ for x in coeffList[:-1] ]
+ #-- 2 --
+ # [ whole := whole part of coeffList[-1]
+ # frac := fractional part of coeffList[-1]
+ # fuzz := 0.5 * (10 ** (-decimals) ]
+ whole, frac = divmod ( float(coeffList[-1]), 1.0 )
+ fuzz = 0.5 * (10.0 ** (-decimals))
+ #-- 3 --
+ # [ if frac >= (1-fuzz) ->
+ # result +:= [whole+frac-fuzz], formatted with
+ # (decimals) digits after the decimal
+ # else ->
+ # result += coeffList[-1], formatted with (decimals)
+ # digits after the decimal ]
+ if frac >= (1.0-fuzz):
+ corrected = whole + frac - fuzz
+ else:
+ corrected = coeffList[-1]
+ #-- 4 --
+ # [ if lz ->
+ # s := corrected, formatted with 2 digits of left-zero
+ # padding and (decimals) precision
+ # else ->
+ # s := corrected, formatted with (decimals) precision ]
+ if lz:
+ if decimals: n = decimals+3
+ else: n = decimals+2
+
+ s = "%0*.*f" % (n, decimals, corrected)
+ else:
+ s = "%.*f" % (decimals, corrected)
+
+ #-- 5 --
+ result.append ( s )
+
+ #-- 6 --
+ return result
+dmsUnits = MixedUnits ( (60, 60) )
+
+# - - - - - c l a s s L a t L o n
+
+class LatLon:
+ """Represents a latitude+longitude.
+ """
+# - - - L a t L o n . _ _ i n i t _ _
+
+ def __init__ ( self, lat, lon ):
+ """Constructor for LatLon.
+ """
+ self.lat = lat
+ self.lon = lon % TWO_PI
+# - - - L a t L o n . _ _ s t r _ _
+
+ def __str__ ( self ):
+ """Return self as a string.
+ """
+ #-- 1 --
+ if self.lon >= pi:
+ e_w = "W"
+ lonDeg = degrees ( TWO_PI - self.lon )
+ else:
+ e_w = "E"
+ lonDeg = degrees ( self.lon )
+
+ #-- 2 --
+ if self.lat < 0:
+ n_s = "S"
+ latDeg = degrees ( - self.lat )
+ else:
+ n_s = "N"
+ latDeg = degrees ( self.lat )
+ #-- 3 --
+ # [ latList := three formatted values of latDeg in
+ # degrees/minutes/seconds
+ # lonList := three formatted values of lonDeg similarly ]
+ latList = dmsUnits.format ( dmsUnits.singleToMix(latDeg), 1 )
+ lonList = dmsUnits.format ( dmsUnits.singleToMix(lonDeg), 1 )
+
+ #-- 4 --
+ return ( '[%sd %s\' %s" %s Lat %sd %s\' %s" %s Lon]' %
+ (latList[0], latList[1], latList[2], n_s,
+ lonList[0], lonList[1], lonList[2], e_w) )
+
+# - - - - - c l a s s J u l i a n D a t e
+
+class JulianDate:
+ """Class to represent Julian-date timestamps.
+
+ State/Invariants:
+ .f: [ (Julian date as a float) - JULIAN_BIAS ]
+ """
+# - - - J u l i a n D a t e . _ _ i n i t _ _
+
+ def __init__ ( self, j, f=0.0 ):
+ """Constructor for JulianDate.
+ """
+ self.j = j - JULIAN_BIAS + f
+# - - - J u l i a n D a t e . _ _ f l o a t _ _
+
+ def __float__ ( self ):
+ """Convert self to a float.
+ """
+ return self.j + JULIAN_BIAS
+# - - - J u l i a n D a t e . d a t e t i m e
+
+ def datetime ( self ):
+ """Convert to a standard Python datetime object in UT.
+ """
+ #-- 1 --
+ # [ i := int(self.j + 0.5)
+ # f := (self.j + 0.5) % 1.0 ]
+ i, f = divmod ( self.j + 0.5, 1.0 )
+ i += JULIAN_BIAS
+ #-- 2 --
+ if i > 2299160:
+ a = int((i-1867216.25)/36524.25)
+ b = i + 1 + a - int ( a / 4.0 )
+ else:
+ b = i
+ #-- 3 --
+ c = b + 1524
+ #-- 4 --
+ d = int((c-122.1)/365.25)
+ #-- 5 --
+ e = int(365.25*d)
+ #-- 6 --
+ g = int((c-e)/30.6001)
+ #-- 7 --
+ dayFrac = c - e + f - int ( 30.6001 * g )
+ day, frac = divmod ( dayFrac, 1.0 )
+ dd = int(day)
+ hr, mn, sc = dmsUnits.singleToMix ( 24.0*frac )
+ #-- 8 --
+ if g < 13.5: mm = int(g - 1)
+ else: mm = int(g - 13)
+ #-- 9 --
+ if mm > 2.5: yyyy = int(d-4716)
+ else: yyyy = int(d-4715)
+ #-- 10 --
+ sec, fracSec = divmod(sc, 1.0)
+ usec = int(fracSec * 1e6)
+ return datetime.datetime ( yyyy, mm, dd, hr, mn, int(sec),
+ usec )
+# - - - J u l i a n D a t e . o f f s e t
+
+ def offset ( self, delta ):
+ """Return a new JulianDate for self+(delta days)
+
+ [ delta is a number of days as a float ->
+ return a new JulianDate (delta) days in the
+ future, or past if negative ]
+ """
+ #-- 1 --
+ newJ = self.j + delta
+
+ #-- 2 --
+ # [ newWhole := whole part of newJ
+ # newFrac := fractional part of newJ ]
+ newWhole, newFrac = divmod ( newJ )
+
+ #-- 3 --
+ return JulianDate ( newWhole+JULIAN_BIAS, newFrac )
+# - - - J u l i a n D a t e . _ _ s u b _ _
+
+ def __sub__ ( self, other ):
+ """Implement subtraction.
+
+ [ other is a JulianDate instance ->
+ return self.j - other.j ]
+ """
+ return self.j - other.j
+# - - - J u l i a n D a t e . _ _ c m p _ _
+
+ def __cmp__ ( self, other ):
+ """Compare two instances.
+
+ [ other is a JulianDate instance ->
+ if self.j < other.j -> return a negative number
+ else if self.j == other.j -> return zero
+ else -> return a positive number ]
+ """
+ return cmp ( self.j, other.j )
+# - - - J u l i a n D a t e . f r o m D a t e t i m e
+
+# @staticmethod
+ def fromDatetime ( dt ):
+ """Create a JulianDate instance from a datetime.datetime.
+
+ [ dt is a datetime.datetime instance ->
+ if dt is naive ->
+ return the equivalent new JulianDate instance,
+ assuming dt expresses UTC
+ else ->
+ return a new JulianDate instance for the UTC
+ time equivalent to dt ]
+ """
+ #-- 1 --
+ # [ if dt is naive ->
+ # utc := dt
+ # else ->
+ # utc := dt - dt.utcoffset() ]
+ utc = dt
+ offset = dt.utcoffset()
+ if offset:
+ utc = dt - offset
+ #-- 2 --
+ # [ fracDay := fraction of a day in [0.0,1.0) made from
+ # utc.hour, utc.minute, utc.second, and utc.microsecond ]
+ s = float(utc.second) + float(utc.microsecond)*1e-6
+ hours = dmsUnits.mixToSingle ( (utc.hour, utc.minute, s) )
+ fracDay = hours / 24.0
+ #-- 3 --
+ y = utc.year
+ m = utc.month
+ d = utc.day
+ #-- 4 --
+ if m <= 2:
+ y, m = y-1, m+12
+ #-- 5 --
+ if ( (y, m, d) >= (1582, 10, 15) ):
+ A = int ( y / 100 )
+ B = 2 - A + int ( A / 4 )
+ else:
+ B = 0
+ #-- 6 --
+ C = int ( 365.25 * y )
+ D = int ( 30.6001 * ( m + 1 ) )
+ #-- 7 --
+ # [ if fracDay+0.5 >= 1.0 ->
+ # s += 1
+ # fracDay := (fracDay+0.5) % 1.0
+ # else ->
+ # fracDay := fracDay + 0.5 ]
+ dayCarry, fracDay = divmod ( fracDay+0.5, 1.0 )
+ d += dayCarry
+
+ #-- 8 --
+ j = B + C + D + d + 1720994
+
+ #-- 9 --
+ return JulianDate ( j, fracDay )
+ fromDatetime = staticmethod(fromDatetime)
+
+# - - - - - c l a s s S i d e r e a l T i m e
+
+class SiderealTime:
+ """Represents a sidereal time value.
+
+ State/Internals:
+ .hours: [ self as 15-degree hours ]
+ .radians: [ self as radians ]
+ """
+# - - - S i d e r e a l T i m e . _ _ i n i t _ _
+
+ def __init__ ( self, hours ):
+ """Constructor for SiderealTime
+ """
+ self.hours = hours % 24.0
+ self.radians = hoursToRadians ( self.hours )
+# - - - S i d e r e a l T i m e . _ _ s t r _ _
+
+ def __str__ ( self ):
+ """Convert to a string such as "[04h 40m 5.170s]".
+ """
+
+ #-- 1 --
+ # [ values := self.hours as a list of mixed units
+ # in dmsUnits terms, formatted as left-zero
+ # filled strings with 3 digits after the decimal ]
+ mix = dmsUnits.singleToMix ( self.hours )
+ values = dmsUnits.format ( mix, decimals=3, lz=True )
+
+ #-- 2 --
+ return "[%sh %sm %ss]" % tuple(values)
+# - - - S i d e r e a l T i m e . u t c
+
+ def utc ( self, date ):
+ """Convert GST to UTC.
+
+ [ date is a UTC date as a datetime.date instance ->
+ return the first or only time at which self's GST
+ occurs at longitude 0 ]
+ """
+ #-- 1 --
+ # [ nDays := number of days between Jan. 0 of year
+ # (date.year) and date ]
+ nDays = dayNo ( date )
+ #-- 2 --
+ # [ t0 := (nDays * A - B(date.year)), normalized to
+ # interval [0,24) ]
+ t0 = ( ( nDays * SIDEREAL_A -
+ SiderealTime.factorB ( date.year ) ) % 24.0 )
+ #-- 3 --
+ # [ t1 := ((self in decimal hours)-t0), normalized to
+ # the interval [0,24) ]
+ t1 = ( radiansToHours ( self.radians ) - t0 ) % 24.0
+ #-- 4 --
+ gmtHours = t1 * 0.997270
+ #-- 5 --
+ # [ dt := a datetime.datetime instance whose date comes
+ # from (date) and whose time is (gmtHours)
+ # decimal hours ]
+ hour, minute, floatSec = dmsUnits.singleToMix ( gmtHours )
+ wholeSec, fracSec = divmod ( floatSec, 1.0 )
+ second = int ( wholeSec )
+ micros = int ( fracSec * 1e6 )
+ dt = datetime.datetime ( date.year, date.month,
+ date.day, hour, minute, second, micros )
+
+ #-- 6 --
+ return dt
+# - - - S i d e r e a l T i m e . f a c t o r B
+
+# @staticmethod
+ def factorB ( yyyy ):
+ """Compute sidereal conversion factor B for a given year.
+
+ [ yyyy is a year number as an int ->
+ return the GST at time yyyy-01-00T00:00 ]
+ """
+ #-- 1 --
+ # [ janJD := the Julian date of January 0.0 of year
+ # (yyyy), as a float ]
+ janDT = datetime.datetime ( yyyy, 1, 1 )
+ janJD = float(JulianDate.fromDatetime(janDT)) - 1.0
+ #-- 2 --
+ s = janJD - 2415020.0
+
+ #-- 3 --
+ t = s / 36525.0
+
+ #-- 4 --
+ r = ( 0.00002581 * t +
+ 2400.051262 ) * t + 6.6460656
+ #-- 5 --
+ u = r - 24 * ( yyyy-1900)
+
+ #-- 6 --
+ return 24.0 - u
+
+ factorB = staticmethod(factorB)
+# - - - S i d e r e a l T i m e . g s t
+
+ def gst ( self, eLong ):
+ """Convert LST to GST.
+
+ [ self is local sidereal time at longitude eLong
+ radians east of Greenwich ->
+ return the equivalent GST as a SiderealTime instance ]
+ """
+ #-- 1 --
+ # [ deltaHours := eLong expressed in hours ]
+ deltaHours = radiansToHours ( eLong )
+
+ #-- 2 --
+ gstHours = ( self.hours - deltaHours ) % 24.0
+
+ #-- 3 --
+ return SiderealTime ( gstHours )
+# - - - S i d e r e a l T i m e . l s t
+
+ def lst ( self, eLong ):
+ """Convert GST to LST.
+
+ [ (self is Greenwich sidereal time) and
+ (eLong is a longitude east of Greenwich in radians) ->
+ return a new SiderealTime representing the LST
+ at longitude eLong ]
+ """
+ #-- 1 --
+ # [ deltaHours := eLong expressed in hours ]
+ deltaHours = radiansToHours ( eLong )
+
+ #-- 2 --
+ gmtHours = (self.hours + deltaHours) % 24.0
+
+ #-- 3 --
+ return SiderealTime ( gmtHours )
+# - - - S i d e r e a l T i m e . f r o m D a t e t i m e
+
+ SIDEREAL_C = 1.002738
+
+# @staticmethod
+ def fromDatetime ( dt ):
+ """Convert civil time to Greenwich Sidereal.
+
+ [ dt is a datetime.datetime instance ->
+ if dt has time zone information ->
+ return the GST at the UTC equivalent to dt
+ else ->
+ return the GST assuming dt is UTC ]
+ """
+ #-- 1 --
+ # [ if dt is naive ->
+ # utc := dt
+ # else ->
+ # utc := the UTC time equivalent to dt ]
+ utc = dt
+ tz = dt.tzinfo
+ if tz is not None:
+ offset = tz.utcoffset ( dt )
+ if offset is not None:
+ utc = dt - offset
+ #-- 2 --
+ # [ nDays := number of days between January 0.0 and utc ]
+ nDays = dayNo ( utc )
+ #-- 3 --
+ t0 = ( nDays * SIDEREAL_A -
+ SiderealTime.factorB ( utc.year ) )
+ #-- 4 --
+ # [ decUTC := utc as decimal hours ]
+ floatSec = utc.second + float ( utc.microsecond ) / 1e6
+ decUTC = dmsUnits.mixToSingle (
+ (utc.hour, utc.minute, floatSec) )
+ #-- 4 --
+ # [ gst := (decUTC * C + t0), normalized to interval [0,24) ]
+ gst = ( decUTC * SiderealTime.SIDEREAL_C + t0) % 24.0
+
+ #-- 5 --
+ return SiderealTime ( gst )
+ fromDatetime = staticmethod ( fromDatetime )
+
+# - - - - - c l a s s A l t A z
+
+class AltAz:
+ """Represents a sky location in horizon coords. (altitude/azimuth)
+
+ Exports/Invariants:
+ .alt: [ altitude in radians, in [-pi,+pi] ]
+ .az: [ azimuth in radians, in [0,2*pi] ]
+ """
+# - - - A l t A z . _ _ i n i t _ _
+
+ def __init__ ( self, alt, az ):
+ """Constructor for AltAz, horizon coordinates.
+
+ [ (alt is an altitude in radians) and
+ (az is an azimuth in radians) ->
+ return a new AltAz instance with those values,
+ normalized as per class invariants ]
+ """
+ self.alt = alt
+ self.az = az
+# - - - A l t A z . r a D e c
+
+ def raDec ( self, lst, latLon ):
+ """Convert horizon coordinates to equatorial.
+
+ [ (lst is a local sidereal time as a SiderealTime instance) and
+ (latLon is the observer's position as a LatLon instance) ->
+ return the corresponding equatorial coordinates as a
+ RADec instance ]
+ """
+ #-- 1 --
+ # [ dec := declination of self at latLon in radians
+ # hourRadians := hour angle of self at latlon in radians ]
+ dec, hourRadians = coordRotate ( self.alt, latLon.lat,
+ self.az )
+
+ #-- 2 --
+ # [ hourRadians is an hour angle in radians ->
+ # h := hourRadians in hours ]
+ h = radiansToHours ( hourRadians )
+ #-- 3 --
+ # [ ra := right ascension for hour angle (h) at local
+ # sidereal time (lst) and location (latLon) ]
+ ra = hoursToRadians ( ( lst.hours - h ) % 24.0 )
+
+ #-- 4 --
+ return RADec ( ra, dec )
+# - - - A l t A z . _ _ s t r _ _
+
+ def __str__ ( self ):
+ """Convert self to a string.
+ """
+ #-- 1 --
+ # [ altList := self.alt, formatted as degrees, minutes,
+ # and seconds
+ # azList := self.az, formatted as degrees, minutes, and
+ # seconds ]
+ altList = dmsUnits.format ( dmsUnits.singleToMix (
+ degrees(self.alt) ), lz=True, decimals=3 )
+ azList = dmsUnits.format ( dmsUnits.singleToMix (
+ degrees(self.az) ), lz=True, decimals=3 )
+
+ #-- 2 --
+ return ( "[az %sd %s' %s\" alt %sd %s' %s\"]" %
+ (tuple(azList)+tuple(altList)) )
+# - - - c o o r d R o t a t e
+
+def coordRotate ( x, y, z ):
+ """Used to convert between equatorial and horizon coordinates.
+
+ [ x, y, and z are angles in radians ->
+ return (xt, yt) where
+ xt=arcsin(sin(x)*sin(y)+cos(x)*cos(y)*cos(z)) and
+ yt=arccos((sin(x)-sin(y)*sin(xt))/(cos(y)*cos(xt))) ]
+ """
+ #-- 1 --
+ xt = asin ( sin(x) * sin(y) +
+ cos(x) * cos(y) * cos(z) )
+ #-- 2 --
+ yt = acos ( ( sin(x) - sin(y) * sin(xt) ) /
+ ( cos(y) * cos(xt) ) )
+ #-- 3 --
+ if sin(z) > 0.0:
+ yt = TWO_PI - yt
+
+ #-- 4 --
+ return (xt, yt)
+
+# - - - - - c l a s s R A D e c
+
+class RADec:
+ """Represents a celestial location in equatorial coordinates.
+
+ Exports/Invariants:
+ .ra: [ right ascension in radians ]
+ .dec: [ declination in radians ]
+ """
+# - - - R A D e c . _ _ i n i t _ _
+
+ def __init__ ( self, ra, dec ):
+ """Constructor for RADec.
+ """
+ self.ra = ra % TWO_PI
+ self.dec = dec
+# - - - R A D e c . h o u r A n g l e
+
+ def hourAngle ( self, utc, eLong ):
+ """Find the hour angle at a given observer's location.
+
+ [ (utc is a Universal Time as a datetime.datetime) and
+ (eLong is an east longitude in radians) ->
+ return the hour angle of self at that time and
+ longitude, in radians ]
+ """
+ return raToHourAngle ( self.ra, utc, eLong )
+# - - - R A D e c . a l t A z
+
+ def altAz ( self, h, lat ):
+ """Convert equatorial to horizon coordinates.
+
+ [ (h is an object's hour angle in radians) and
+ (lat is the observer's latitude in radians) ->
+ return self's position in the observer's sky
+ in horizon coordinates as an AltAz instance ]
+ """
+ #-- 1 --
+ # [ alt := altitude of self as seen from latLon at utc
+ # az := azimuth of self as seen from latLon at utc ]
+ alt, az = coordRotate ( self.dec, lat, h )
+
+ #-- 2 --
+ return AltAz ( alt, az )
+# - - - R A D e c . _ _ s t r _ _
+
+ def __str__ ( self ):
+ """Return self as a string.
+ """
+ #-- 1 --
+ # [ raUnits := units of self.ra as hours/minutes/seconds
+ # decUnits := units of self.dec as degrees/minutes/seconds
+ raUnits = dmsUnits.format (
+ dmsUnits.singleToMix ( radiansToHours(self.ra) ),
+ lz=True, decimals=3 )
+ decUnits = dmsUnits.format (
+ dmsUnits.singleToMix ( degrees(self.dec) ),
+ lz=True, decimals=3 )
+
+ #-- 2 --
+ return ( "[%sh %sm %ss, %sd %s' %s\"]" %
+ (tuple(raUnits)+tuple(decUnits)) )