|
| 1 | +import datetime |
| 2 | +from typing import Dict, List, Optional, Tuple |
| 3 | + |
| 4 | +import astropy.units as u |
| 5 | +import matplotlib.dates as mpldates |
| 6 | +import matplotlib.pyplot as plt |
| 7 | +import numpy as np |
| 8 | +import pandas as pd |
| 9 | +import pytz |
| 10 | +import suntime |
| 11 | +import timezonefinder |
| 12 | +from astropy.coordinates import AltAz, EarthLocation, SkyCoord |
| 13 | +from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui |
| 14 | + |
| 15 | +from location import location_server, location_ui |
| 16 | + |
| 17 | +app_ui = ui.page_fixed( |
| 18 | + ui.tags.h3("Air mass calculator"), |
| 19 | + ui.div( |
| 20 | + ui.markdown( |
| 21 | + """This Shiny app uses [Astropy](https://www.astropy.org/) to calculate the |
| 22 | + altitude (degrees above the horizon) and airmass (the amount of atmospheric |
| 23 | + air along your line of sight to an object) of one or more astronomical |
| 24 | + objects, over a given evening, at a given geographic location. |
| 25 | + """ |
| 26 | + ), |
| 27 | + class_="mb-5", |
| 28 | + ), |
| 29 | + ui.row( |
| 30 | + ui.column( |
| 31 | + 8, |
| 32 | + ui.output_ui("timeinfo"), |
| 33 | + ui.output_plot("plot", height="800px"), |
| 34 | + # For debugging |
| 35 | + # ui.output_table("table"), |
| 36 | + class_="order-2 order-sm-1", |
| 37 | + ), |
| 38 | + ui.column( |
| 39 | + 4, |
| 40 | + ui.panel_well( |
| 41 | + ui.input_date("date", "Date"), |
| 42 | + class_="pb-1 mb-3", |
| 43 | + ), |
| 44 | + ui.panel_well( |
| 45 | + ui.input_text_area( |
| 46 | + "objects", "Target object(s)", "M1, NGC35, PLX299", rows=3 |
| 47 | + ), |
| 48 | + class_="pb-1 mb-3", |
| 49 | + ), |
| 50 | + ui.panel_well( |
| 51 | + location_ui("location"), |
| 52 | + class_="mb-3", |
| 53 | + ), |
| 54 | + class_="order-1 order-sm-2", |
| 55 | + ), |
| 56 | + ), |
| 57 | +) |
| 58 | + |
| 59 | + |
| 60 | +def server(input: Inputs, output: Outputs, session: Session): |
| 61 | + loc = location_server("location") |
| 62 | + time_padding = datetime.timedelta(hours=1.5) |
| 63 | + |
| 64 | + @reactive.Calc |
| 65 | + def obj_names() -> List[str]: |
| 66 | + """Returns a split and *slightly* cleaned-up list of object names""" |
| 67 | + req(input.objects()) |
| 68 | + return [x.strip() for x in input.objects().split(",") if x.strip() != ""] |
| 69 | + |
| 70 | + @reactive.Calc |
| 71 | + def obj_coords() -> List[SkyCoord]: |
| 72 | + return [SkyCoord.from_name(name) for name in obj_names()] |
| 73 | + |
| 74 | + @reactive.Calc |
| 75 | + def times_utc() -> Tuple[datetime.datetime, datetime.datetime]: |
| 76 | + req(input.date()) |
| 77 | + lat, long = loc() |
| 78 | + sun = suntime.Sun(lat, long) |
| 79 | + return ( |
| 80 | + sun.get_sunset_time(input.date()), |
| 81 | + sun.get_sunrise_time(input.date() + datetime.timedelta(days=1)), |
| 82 | + ) |
| 83 | + |
| 84 | + @reactive.Calc |
| 85 | + def timezone() -> Optional[str]: |
| 86 | + lat, long = loc() |
| 87 | + return timezonefinder.TimezoneFinder().timezone_at(lat=lat, lng=long) |
| 88 | + |
| 89 | + @reactive.Calc |
| 90 | + def times_at_loc(): |
| 91 | + start, end = times_utc() |
| 92 | + tz = pytz.timezone(timezone()) |
| 93 | + return (start.astimezone(tz), end.astimezone(tz)) |
| 94 | + |
| 95 | + @reactive.Calc |
| 96 | + def df() -> Dict[str, pd.DataFrame]: |
| 97 | + start, end = times_at_loc() |
| 98 | + times = pd.date_range( |
| 99 | + start - time_padding, |
| 100 | + end + time_padding, |
| 101 | + periods=100, |
| 102 | + ) |
| 103 | + lat, long = loc() |
| 104 | + eloc = EarthLocation(lat=lat * u.deg, lon=long * u.deg, height=0) |
| 105 | + altaz_list = [ |
| 106 | + obj.transform_to(AltAz(obstime=times, location=eloc)) |
| 107 | + for obj in obj_coords() |
| 108 | + ] |
| 109 | + return { |
| 110 | + obj: pd.DataFrame( |
| 111 | + { |
| 112 | + "obj": obj, |
| 113 | + "time": times, |
| 114 | + "alt": altaz.alt, |
| 115 | + # Filter out discontinuity |
| 116 | + "secz": np.where(altaz.alt > 0, altaz.secz, np.nan), |
| 117 | + } |
| 118 | + ) |
| 119 | + for (altaz, obj) in zip(altaz_list, obj_names()) |
| 120 | + } |
| 121 | + |
| 122 | + @output |
| 123 | + @render.plot |
| 124 | + def plot(): |
| 125 | + fig, [ax1, ax2] = plt.subplots(nrows=2) |
| 126 | + |
| 127 | + sunset, sunrise = times_at_loc() |
| 128 | + |
| 129 | + def add_boundary(ax, xval): |
| 130 | + ax.axvline(x=xval, c="#888888", ls="dashed") |
| 131 | + |
| 132 | + ax1.set_ylabel("Altitude (deg)") |
| 133 | + ax1.set_xlabel("Time") |
| 134 | + ax1.set_ylim(-10, 90) |
| 135 | + ax1.set_xlim(sunset - time_padding, sunrise + time_padding) |
| 136 | + ax1.grid() |
| 137 | + add_boundary(ax1, sunset) |
| 138 | + add_boundary(ax1, sunrise) |
| 139 | + for obj_name, data in df().items(): |
| 140 | + ax1.plot(data["time"], data["alt"], label=obj_name) |
| 141 | + ax1.xaxis.set_major_locator(mpldates.AutoDateLocator()) |
| 142 | + ax1.xaxis.set_major_formatter( |
| 143 | + mpldates.DateFormatter("%H:%M", tz=pytz.timezone(timezone())) |
| 144 | + ) |
| 145 | + ax1.legend(loc="upper right") |
| 146 | + |
| 147 | + ax2.set_ylabel("Air mass") |
| 148 | + ax2.set_xlabel("Time") |
| 149 | + ax2.set_ylim(4, 1) |
| 150 | + ax2.set_xlim(sunset - time_padding, sunrise + time_padding) |
| 151 | + ax2.grid() |
| 152 | + add_boundary(ax2, sunset) |
| 153 | + add_boundary(ax2, sunrise) |
| 154 | + for data in df().values(): |
| 155 | + ax2.plot(data["time"], data["secz"]) |
| 156 | + ax2.xaxis.set_major_locator(mpldates.AutoDateLocator()) |
| 157 | + ax2.xaxis.set_major_formatter( |
| 158 | + mpldates.DateFormatter("%H:%M", tz=pytz.timezone(timezone())) |
| 159 | + ) |
| 160 | + |
| 161 | + return fig |
| 162 | + |
| 163 | + @output |
| 164 | + @render.table |
| 165 | + def table() -> pd.DataFrame: |
| 166 | + return pd.concat(df()) |
| 167 | + |
| 168 | + @output |
| 169 | + @render.ui |
| 170 | + def timeinfo(): |
| 171 | + start_utc, end_utc = times_utc() |
| 172 | + start_at_loc, end_at_loc = times_at_loc() |
| 173 | + return ui.TagList( |
| 174 | + f"Sunset: {start_utc.strftime('%H:%M')}, ", |
| 175 | + f"Sunrise: {end_utc.strftime('%H:%M')} ", |
| 176 | + "(UTC)", |
| 177 | + ui.tags.br(), |
| 178 | + f"Sunset: {start_at_loc.strftime('%H:%M')}, ", |
| 179 | + f"Sunrise: {end_at_loc.strftime('%H:%M')} ", |
| 180 | + f"({timezone()})", |
| 181 | + ) |
| 182 | + |
| 183 | + |
| 184 | +# The debug=True causes it to print messages to the console. |
| 185 | +app = App(app_ui, server, debug=False) |
0 commit comments