Compare commits

..

No commits in common. "main" and "pyephem" have entirely different histories.

2 changed files with 136 additions and 96 deletions

View File

@ -34,26 +34,22 @@ from moons import backgrounds
# If you change the aspect ratio, the canned backgrounds won't work. # If you change the aspect ratio, the canned backgrounds won't work.
ASPECTRATIO = 0.5 ASPECTRATIO = 0.5
def putmoon(fracphase, lines, hemisphere, atfiller='@'): # pylint: disable=too-many-locals,too-many-branches,too-many-statements,too-many-arguments def putmoon(pctphase, lines, atfiller, hemisphere): # pylint: disable=too-many-locals,too-many-branches,too-many-statements,too-many-arguments
"""Print the moon """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 = "" output = ""
def putchar(char):
nonlocal output
output += char
# Find the length of the atfiller string # Find the length of the atfiller string
atflrlen = len(atfiller) atflrlen = len(atfiller)
# Fix waxes and wanes direction for south hemisphere # Fix waxes and wanes direction for south hemisphere
if hemisphere == 'south': if hemisphere == 'south':
fracphase = 1 - pctphase pctphase = 1 - pctphase
angphase = fracphase * 2.0 * math.pi angphase = pctphase * 2.0 * math.pi
mcap = -math.cos(angphase) mcap = -math.cos(angphase)
# Figure out how big the moon is # Figure out how big the moon is
@ -79,7 +75,7 @@ def putmoon(fracphase, lines, hemisphere, atfiller='@'): # pylint: disable=too-
# Now output the slice # Now output the slice
col = 0 col = 0
while col < colleft: while col < colleft:
output += ' ' putchar(' ')
col += 1 col += 1
while col <= colright: while col <= colright:
if hemisphere == 'north': if hemisphere == 'north':
@ -98,16 +94,16 @@ def putmoon(fracphase, lines, hemisphere, atfiller='@'): # pylint: disable=too-
# rotate char upside-down if needed # rotate char upside-down if needed
char = char.translate(str.maketrans("().`_'", char = char.translate(str.maketrans("().`_'",
"!!!!!!")) ")(`.^,"))
if char != '@': if char != '@':
output += char putchar(char)
else: else:
output += atfiller[atflridx] putchar(atfiller[atflridx])
atflridx = (atflridx + 1) % atflrlen atflridx = (atflridx + 1) % atflrlen
col += 1 col += 1
output += '\n' putchar('\n')
lin += 1 lin += 1
return output return output

View File

@ -1,70 +1,105 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Xaphoon - Displays the phase of the moon as well as other related information."""
import time
from argparse import ArgumentParser
from datetime import datetime, timezone from datetime import datetime, timezone
from skyfield import almanac import math
from skyfield_data import get_skyfield_data_path import time
import skyfield.api from argparse import ArgumentParser
import ephem
from pyphoon import putmoon from pyphoon import putmoon
# Initialize certain skyfield parameters globally # Second resolution for culmination/illumination calculations
sf_load = skyfield.api.Loader(get_skyfield_data_path(), expire=False) # loader DAY_INCREMENT=1/86400
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_timestr(t, date=False, local=True): def to_deg(rad):
"""Convert a skyfield time to a time string, optionally in the local time zone.""" """Convert radians to a displayable integer number of degrees."""
t = t.utc_datetime() return round(math.degrees(rad))
def to_timestr(date, local=True):
"""Convert a pyephem date to a time string in the local time zone."""
if local: if local:
t = t.astimezone() date = ephem.localtime(date)
if date: else:
return t.strftime('%Y-%m-%d %H:%M:%S') date = date.datetime()
return t.strftime('%H:%M:%S') return date.strftime("%H:%M:%S")
def fmt(cols, t, az, el, phase, illum, moonrise, transit, moonset, moonlines, hemi): def find_target_rising(moon, me):
"""Formats data into string to print""" """Return the relevant moonrise to base display and calculations off of."""
_date = t.utc_datetime().astimezone().strftime('%Y-%m-%d %H:%M:%S') # 18 chars if moon.alt == 0: # i would love a better way to do this
_azel = f"Az:{az.degrees:.0f}° El:{el.degrees:.0f}°".ljust(16) # 16 chars me = me.copy()
_phil = f"Ph: {phase.degrees:.0f}° Ill:{illum*100:.0f}%".rjust(16) # 16 chars me.date = me.previous_rising(moon)
_r = f"R:{to_timestr(moonrise)}" # 10 chars return me.next_rising(moon)
_t = f"T:{to_timestr(transit)}" # 10 chars if moon.alt > 0:
_s = f"S:{to_timestr(moonset)}" # 10 chars return me.previous_rising(moon)
_moon = putmoon(phase.degrees/360, moonlines, hemi) # ! scalable width # moon.alt < 0
return me.next_rising(moon)
# 2 groups of spacing, filling cols minus total RTS width def cmp_culmination(moon, me, t):
_rts_spacing = ' '*int((cols-30)/2) """Determine whether the culmination is before, after, or at t.
ret = f"{_date.center(cols)}\n" Returns 0 if t is the culmination, -1 if t if culmination is before t, or 1
ret += f"{_azel}{' '*(cols-32)}{_phil}\n" if culmination is after t. Assumes there is exactly one peak elevation,
# split moon on newlines, right-pad to center moon in original width, center to center which seems to cause error of up to about 7 seconds due to float precision.
# 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' me.date = t - DAY_INCREMENT
ret += f"{_r}{_rts_spacing}{_t}{_rts_spacing}{_s}" moon.compute(me)
return ret 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
def main(): def main():
"""Main function
Parses arguments, calculates values, and displays them.
"""
parser = ArgumentParser() parser = ArgumentParser()
parser.add_argument("lat", parser.add_argument("lat",
help="Observer latitude", help="Observer latitude")
type=float) parser.add_argument("long",
parser.add_argument("lon", help="Observer longitude")
help="Observer longitude",
type=float)
parser.add_argument("elevation", parser.add_argument("elevation",
help="Observer elevation in meters", 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) type=int)
parser.add_argument("-c", "--columns", parser.add_argument("-c", "--columns",
help="Number of columns for the output to use (default 70)", help="Number of columns for the output to use (default 70)",
@ -76,38 +111,47 @@ def main():
type=int) type=int)
args = parser.parse_args() args = parser.parse_args()
t = ts.from_datetime(datetime.fromtimestamp(args.time, timezone.utc)) # current time now = ephem.date(datetime.fromtimestamp(args.time, timezone.utc))
print(f"Current time: {to_timestr(now)}")
obs_geo = skyfield.api.wgs84.latlon(args.lat, args.lon, me = ephem.Observer()
elevation_m=args.elevation) # geographic position vector me.date = now
obs = earth + obs_geo # barycentric position vector me.lat = args.lat
me.lon = args.long
me.elevation = args.elevation
moon_apparent = obs.at(t).observe(moon).apparent() moon = ephem.Moon(me)
el, az, _ = moon_apparent.altaz('standard')
# Find relevant moonrise. el is based on apparent location, so accounts az = to_deg(moon.az)
# for atmospheric refraction. y shouldn't be needed unless user is near el = to_deg(moon.alt)
# one of the poles, so ignored for now. First [0] discards y (second print(f"Az: {az}° El: {el}°")
# element of tuple); second [] selects from array of moonrises/moonsets
if el.degrees > 0: rising = find_target_rising(moon, me)
# Moon is up. Find last moonrise in the past 24 hours. setting = me.next_setting(moon)
moonrise = almanac.find_risings(obs, moon, t-1, t)[0][-1]
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)
else: else:
# Moon is not up. Find first moonrise in the next 24 hours. full_cycle_phase = moon.moon_phase / 2
moonrise = almanac.find_risings(obs, moon, t, t+1)[0][0]
# Find first moonset in the next 24 hours after moonrise. print(putmoon(full_cycle_phase, 20, '@', 'northern' if me.lat > 0 else 'southern'))
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() main()