diff --git a/pyphoon.py b/pyphoon.py index a8e10c5..97db6ca 100644 --- a/pyphoon.py +++ b/pyphoon.py @@ -34,22 +34,26 @@ from moons import backgrounds # If you change the aspect ratio, the canned backgrounds won't work. ASPECTRATIO = 0.5 -def putmoon(pctphase, lines, atfiller, hemisphere): # pylint: disable=too-many-locals,too-many-branches,too-many-statements,too-many-arguments - """Print the moon""" +def putmoon(fracphase, lines, hemisphere, atfiller='@'): # pylint: disable=too-many-locals,too-many-branches,too-many-statements,too-many-arguments + """Print the moon + + Arguments: + fracphase: A float 0 <= n < 1 representing the current point in the cycle + lines: An integer representing the number of lines in the output + hemisphere: A string 'north' or 'south' representing the observer's hemisphere + atfiller: What character to use in place of '@' + """ output = "" - def putchar(char): - nonlocal output - output += char # Find the length of the atfiller string atflrlen = len(atfiller) # Fix waxes and wanes direction for south hemisphere if hemisphere == 'south': - pctphase = 1 - pctphase + fracphase = 1 - pctphase - angphase = pctphase * 2.0 * math.pi + angphase = fracphase * 2.0 * math.pi mcap = -math.cos(angphase) # Figure out how big the moon is @@ -75,7 +79,7 @@ def putmoon(pctphase, lines, atfiller, hemisphere): # pylint: disable=too-many- # Now output the slice col = 0 while col < colleft: - putchar(' ') + output += ' ' col += 1 while col <= colright: if hemisphere == 'north': @@ -94,16 +98,16 @@ def putmoon(pctphase, lines, atfiller, hemisphere): # pylint: disable=too-many- # rotate char upside-down if needed char = char.translate(str.maketrans("().`_'", - ")(`.^,")) + "!!!!!!")) if char != '@': - putchar(char) + output += char else: - putchar(atfiller[atflridx]) + output += atfiller[atflridx] atflridx = (atflridx + 1) % atflrlen col += 1 - putchar('\n') + output += '\n' lin += 1 return output diff --git a/xaphoon.py b/xaphoon.py index 1cb8ae2..b5fb49e 100755 --- a/xaphoon.py +++ b/xaphoon.py @@ -1,105 +1,70 @@ #!/usr/bin/env python -from datetime import datetime, timezone -import math +"""Xaphoon - Displays the phase of the moon as well as other related information.""" + import time + from argparse import ArgumentParser -import ephem +from datetime import datetime, timezone +from skyfield import almanac +from skyfield_data import get_skyfield_data_path +import skyfield.api + from pyphoon import putmoon -# Second resolution for culmination/illumination calculations -DAY_INCREMENT=1/86400 +# Initialize certain skyfield parameters globally +sf_load = skyfield.api.Loader(get_skyfield_data_path(), expire=False) # loader +ts = sf_load.timescale(builtin=False) # timescale +eph = sf_load('de421.bsp') # ephemerides +earth, sun, moon = eph['Earth'], eph['Sun'], eph['Moon'] # moooon -def to_deg(rad): - """Convert radians to a displayable integer number of degrees.""" - return round(math.degrees(rad)) - -def to_timestr(date, local=True): - """Convert a pyephem date to a time string in the local time zone.""" +def to_timestr(t, date=False, local=True): + """Convert a skyfield time to a time string, optionally in the local time zone.""" + t = t.utc_datetime() if local: - date = ephem.localtime(date) - else: - date = date.datetime() - return date.strftime("%H:%M:%S") + t = t.astimezone() + if date: + return t.strftime('%Y-%m-%d %H:%M:%S') + return t.strftime('%H:%M:%S') -def find_target_rising(moon, me): - """Return the relevant moonrise to base display and calculations off of.""" - if moon.alt == 0: # i would love a better way to do this - me = me.copy() - me.date = me.previous_rising(moon) - return me.next_rising(moon) - if moon.alt > 0: - return me.previous_rising(moon) - # moon.alt < 0 - return me.next_rising(moon) +def fmt(cols, t, az, el, phase, illum, moonrise, transit, moonset, moonlines, hemi): + """Formats data into string to print""" + _date = t.utc_datetime().astimezone().strftime('%Y-%m-%d %H:%M:%S') # 18 chars + _azel = f"Az:{az.degrees:.0f}° El:{el.degrees:.0f}°".ljust(16) # 16 chars + _phil = f"Ph: {phase.degrees:.0f}° Ill:{illum*100:.0f}%".rjust(16) # 16 chars + _r = f"R:{to_timestr(moonrise)}" # 10 chars + _t = f"T:{to_timestr(transit)}" # 10 chars + _s = f"S:{to_timestr(moonset)}" # 10 chars + _moon = putmoon(phase.degrees/360, moonlines, hemi) # ! scalable width -def cmp_culmination(moon, me, t): - """Determine whether the culmination is before, after, or at t. + # 2 groups of spacing, filling cols minus total RTS width + _rts_spacing = ' '*int((cols-30)/2) - Returns 0 if t is the culmination, -1 if t if culmination is before t, or 1 - if culmination is after t. Assumes there is exactly one peak elevation, - which seems to cause error of up to about 7 seconds due to float precision. - """ - me.date = t - DAY_INCREMENT - moon.compute(me) - e1 = moon.alt - me.date = t - moon.compute(me) - e2 = moon.alt - me.date = t + DAY_INCREMENT - moon.compute(me) - e3 = moon.alt - - if e1 > e2: - return -1 - if e3 > e2: - return 1 - return 0 - -def find_culmination(moon, me, rising, setting): - """Finds culmination via binary search. - - Assumes rising and setting are from same pass. - """ - moon = moon.copy() - me = me.copy() - t1 = rising - t3 = setting - while True: - t2 = (t1 + t3) / 2 - match cmp_culmination(moon,me,t2): - case 0: return ephem.date(t2) - case -1: t3 = t2 - case 1: t1 = t2 - -def cmp_illumination(moon, me, t): - """Determine whether the moon is waxing, waning, or either full or new. - - Returns 0 if the moon is either full or new, -1 if moon is waning, or 1 - if moon is waxing. - """ - moon = moon.copy() - me = me.copy() - me.date = t - DAY_INCREMENT - moon.compute(me) - i1 = moon.moon_phase - me.date = t + DAY_INCREMENT - moon.compute(me) - i2 = moon.moon_phase - - if i1 > i2: - return -1 - if i1 < i2: - return 1 - return 0 + ret = f"{_date.center(cols)}\n" + ret += f"{_azel}{' '*(cols-32)}{_phil}\n" + # split moon on newlines, right-pad to center moon in original width, center to center + # in new width, then rejoin with newlines and tack an extra newline on the end + ret += '\n'.join([line.ljust(44).center(cols) for line in _moon.split('\n')]) + '\n' + ret += f"{_r}{_rts_spacing}{_t}{_rts_spacing}{_s}" + return ret def main(): + """Main function + + Parses arguments, calculates values, and displays them. + """ parser = ArgumentParser() parser.add_argument("lat", - help="Observer latitude") - parser.add_argument("long", - help="Observer longitude") + help="Observer latitude", + type=float) + parser.add_argument("lon", + help="Observer longitude", + type=float) parser.add_argument("elevation", help="Observer elevation in meters", + type=float) + parser.add_argument("-l", "--lines", + help="Number of lines for the output to use (default 25)", + default=25, type=int) parser.add_argument("-c", "--columns", help="Number of columns for the output to use (default 70)", @@ -111,47 +76,38 @@ def main(): type=int) args = parser.parse_args() - now = ephem.date(datetime.fromtimestamp(args.time, timezone.utc)) - print(f"Current time: {to_timestr(now)}") + t = ts.from_datetime(datetime.fromtimestamp(args.time, timezone.utc)) # current time - me = ephem.Observer() - me.date = now - me.lat = args.lat - me.lon = args.long - me.elevation = args.elevation + obs_geo = skyfield.api.wgs84.latlon(args.lat, args.lon, + elevation_m=args.elevation) # geographic position vector + obs = earth + obs_geo # barycentric position vector - moon = ephem.Moon(me) + moon_apparent = obs.at(t).observe(moon).apparent() + el, az, _ = moon_apparent.altaz('standard') - az = to_deg(moon.az) - el = to_deg(moon.alt) - print(f"Az: {az}° El: {el}°") - - rising = find_target_rising(moon, me) - setting = me.next_setting(moon) - - print (f"Rise: {to_timestr(rising)} Set: {to_timestr(setting)}") - - culm = find_culmination(moon, me, rising, setting) - - print(f"Culmination: {to_timestr(culm)}") - - direction = cmp_illumination(moon, me, now) - match direction: - case -1: - direction_indicator = '-' - case 0: - direction_indicator = '' - case 1: - direction_indicator = '+' - - print(f"Phase: {moon.moon_phase:.0%}{direction_indicator}") - - # Convert illumination percentage and waxing/waning status to percent through full cycle - if direction < 0: # waning - full_cycle_phase = 1 - (moon.moon_phase / 2) + # Find relevant moonrise. el is based on apparent location, so accounts + # for atmospheric refraction. y shouldn't be needed unless user is near + # one of the poles, so ignored for now. First [0] discards y (second + # element of tuple); second [] selects from array of moonrises/moonsets + if el.degrees > 0: + # Moon is up. Find last moonrise in the past 24 hours. + moonrise = almanac.find_risings(obs, moon, t-1, t)[0][-1] else: - full_cycle_phase = moon.moon_phase / 2 + # Moon is not up. Find first moonrise in the next 24 hours. + moonrise = almanac.find_risings(obs, moon, t, t+1)[0][0] - print(putmoon(full_cycle_phase, 20, '@', 'northern' if me.lat > 0 else 'southern')) + # Find first moonset in the next 24 hours after moonrise. + moonset = almanac.find_settings(obs, moon, moonrise, moonrise+1)[0][0] + + transit = almanac.find_transits(obs, moon, moonrise, moonrise+1)[0] + + phase = almanac.moon_phase(eph, t) + illum = moon_apparent.fraction_illuminated(sun) + hemi = 'north' if args.lat > 0 else 'south' + + #print(phase.degrees/360*100) + #print(putmoon(phase.degrees/360, 21, 'north' if args.lat > 0 else 'south')) + print(fmt(args.columns, t, az, el, phase, illum, moonrise, transit, moonset, args.lines - 4,hemi)) + #print(putmoon(phase.degrees/360, 21, '@', hemi)) main()