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.
ASPECTRATIO = 0.5
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 '@'
"""
def putmoon(pctphase, lines, atfiller, hemisphere): # pylint: disable=too-many-locals,too-many-branches,too-many-statements,too-many-arguments
"""Print the moon"""
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':
fracphase = 1 - pctphase
pctphase = 1 - pctphase
angphase = fracphase * 2.0 * math.pi
angphase = pctphase * 2.0 * math.pi
mcap = -math.cos(angphase)
# Figure out how big the moon is
@ -79,7 +75,7 @@ def putmoon(fracphase, lines, hemisphere, atfiller='@'): # pylint: disable=too-
# Now output the slice
col = 0
while col < colleft:
output += ' '
putchar(' ')
col += 1
while col <= colright:
if hemisphere == 'north':
@ -98,16 +94,16 @@ def putmoon(fracphase, lines, hemisphere, atfiller='@'): # pylint: disable=too-
# rotate char upside-down if needed
char = char.translate(str.maketrans("().`_'",
"!!!!!!"))
")(`.^,"))
if char != '@':
output += char
putchar(char)
else:
output += atfiller[atflridx]
putchar(atfiller[atflridx])
atflridx = (atflridx + 1) % atflrlen
col += 1
output += '\n'
putchar('\n')
lin += 1
return output

View File

@ -1,70 +1,105 @@
#!/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 skyfield import almanac
from skyfield_data import get_skyfield_data_path
import skyfield.api
import math
import time
from argparse import ArgumentParser
import ephem
from pyphoon import putmoon
# 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
# Second resolution for culmination/illumination calculations
DAY_INCREMENT=1/86400
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()
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."""
if local:
t = t.astimezone()
if date:
return t.strftime('%Y-%m-%d %H:%M:%S')
return t.strftime('%H:%M:%S')
date = ephem.localtime(date)
else:
date = date.datetime()
return date.strftime("%H:%M:%S")
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 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)
# 2 groups of spacing, filling cols minus total RTS width
_rts_spacing = ' '*int((cols-30)/2)
def cmp_culmination(moon, me, t):
"""Determine whether the culmination is before, after, or at t.
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
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
def main():
"""Main function
Parses arguments, calculates values, and displays them.
"""
parser = ArgumentParser()
parser.add_argument("lat",
help="Observer latitude",
type=float)
parser.add_argument("lon",
help="Observer longitude",
type=float)
help="Observer latitude")
parser.add_argument("long",
help="Observer longitude")
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)",
@ -76,38 +111,47 @@ def main():
type=int)
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,
elevation_m=args.elevation) # geographic position vector
obs = earth + obs_geo # barycentric position vector
me = ephem.Observer()
me.date = now
me.lat = args.lat
me.lon = args.long
me.elevation = args.elevation
moon_apparent = obs.at(t).observe(moon).apparent()
el, az, _ = moon_apparent.altaz('standard')
moon = ephem.Moon(me)
# 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]
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)
else:
# Moon is not up. Find first moonrise in the next 24 hours.
moonrise = almanac.find_risings(obs, moon, t, t+1)[0][0]
full_cycle_phase = moon.moon_phase / 2
# 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))
print(putmoon(full_cycle_phase, 20, '@', 'northern' if me.lat > 0 else 'southern'))
main()