""" camera_display.py """
import math
import warnings
import tkinter as tk
from tkinter import ttk
import numpy as np
from matplotlib.figure import Figure
from matplotlib.collections import PathCollection
from astropy.coordinates import SkyCoord
from .SatelliteCamera import SatelliteCamera, SatelliteCameraError
from .BSC5Stars import BSC5Stars
from .NikonD5Camera import NikonD5Camera
from .DoCameraImage import DoCameraImage
from .DoCubesatViewer import DoCubesatViewer
from .misc import arcseconds_to_radians, ra_fix, mag_map, split_plot_mollweide_line_ra_dec_deg, split_plot_mollweide_line
from .ecliptic import ecliptic, body, galactic_plane, moon_illumination
from .constellation_database import ConstellationDatabase
from .stars_in_polygon_icrs import stars_in_polygon_icrs
#
# Do this to debug:
# .../matplotlib/projections/geo.py:397: RuntimeWarning: invalid value encountered in arcsin
# theta = np.arcsin(y / np.sqrt(2))
# Turn all RuntimeWarnings into exceptions
warnings.filterwarnings('error', category=RuntimeWarning)
[docs]
class UserInterface:
""" UserInterface """
_full_sky = None
_title_label = None
_camera_info_box = None
_star_found_text_box = None
_misc_text_box = None
_realtime_button = None
_focal_length_buttons = {}
_star_mag_buttons = {}
_rpy_label = None
_sat_label = None
_photo_label = None
_rpy_sliders = {}
rpy_values_deg = {
'roll': 0.0,
'pitch': 0.0,
'yaw': 0.0
}
""" rpy_values_deg - values of Roll, Pitch, and Yaw sliders. """
_cam_slider_rpy_text = {
'roll': 'Roll (X) side-to-side',
'pitch': 'Pitch (Y) nose-up-down',
'yaw': 'Yaw((Z) left-right'
}
[docs]
@classmethod
def register_full_sky(cls, f):
""" register_full_sky """
cls._full_sky = f
# TITLE
[docs]
@classmethod
def title_label(cls, parent, text):
""" title_label """
l = ttk.Label(parent, text=text, justify='left', font=('', 24, 'bold'))
l.pack(padx=5, pady=2)
cls._title_label = l
# INFO TEXT BOXES
[docs]
@classmethod
def camera_info_box(cls, parent, row, col):
""" camera_info_box """
t = tk.Text(parent, width=80, height=3, state='disabled', wrap=tk.WORD)
t.grid(row=row, column=col, padx=5, pady=2, sticky='ew')
cls._camera_info_box = t
[docs]
@classmethod
def misc_text_box(cls, parent, row, col):
""" misc_text_box """
t = tk.Text(parent, width=80, height=3, state='disabled', wrap=tk.WORD)
t.grid(row=row, column=col, padx=5, pady=2, sticky='ew')
cls._misc_text_box = t
[docs]
@classmethod
def star_found_text_box(cls, parent, row, col):
""" star_found_text_box """
t = tk.Text(parent, width=80, height=3, state='disabled', wrap=tk.WORD)
t.grid(row=row, column=col, padx=5, pady=2, sticky='ew')
cls._star_found_text_box = t
[docs]
@classmethod
def camera_info(cls, text):
""" camera_info """
cls._camera_info_box.config(state='normal')
cls._camera_info_box.delete('1.0', tk.END)
cls._camera_info_box.insert(tk.END, '%s' % (text))
cls._camera_info_box.config(state='disabled')
[docs]
@classmethod
def star_found_text(cls, text):
""" star_found_text """
cls._star_found_text_box.config(state='normal')
cls._star_found_text_box.delete('1.0', tk.END)
cls._star_found_text_box.insert(tk.END, '%s' % (text))
cls._star_found_text_box.config(state='disabled')
[docs]
@classmethod
def misc_text(cls, text):
""" misc_text """
cls._misc_text_box.config(state='normal')
cls._misc_text_box.delete('1.0', tk.END)
cls._misc_text_box.insert(tk.END, '%s' % (text))
cls._misc_text_box.config(state='disabled')
# BUTTONS
[docs]
@classmethod
def do_realtime(cls, value):
""" do_realtime """
cls._full_sky.do_realtime(value)
[docs]
@classmethod
def do_stars(cls, value):
""" do_stars """
cls._full_sky.do_stars(value)
[docs]
@classmethod
def do_match_stars(cls, value):
""" do_match_stars """
cls._full_sky.do_match_stars(value)
# STAR MAGNITUDE
[docs]
@classmethod
def do_mag(cls, value):
""" do_mag """
cls._full_sky.do_mag(value)
# FOCAL LENGTH
[docs]
@classmethod
def do_focal_length(cls, f):
""" do_focal_length """
cls._full_sky.do_focal_length(f)
# ROLL PITCH YAW
[docs]
@classmethod
def rpy_label(cls, parent, text, row, col):
""" rpy_label """
l = ttk.Label(parent, text=text, justify='left', wraplength=160)
l.grid(row=row, column=col, padx=5, pady=2, sticky='w')
cls._rpy_label = l
[docs]
@classmethod
def do_rpy(cls, val, k):
""" do_rpy """
cls._full_sky.do_rpy(val, k)
[docs]
@classmethod
def rpy_sliders(cls, parent, row, col):
""" rpy_sliders """
cls.v_sliders = {}
for k,v in cls.rpy_values_deg.items():
cls.v_sliders[k] = tk.IntVar(value=int(v))
s = tk.Scale(parent, label=cls._cam_slider_rpy_text[k], variable=cls.v_sliders[k], from_=-90, to=90, resolution=10.0, showvalue=True,
orient='horizontal', command=lambda val,k=k: cls.do_rpy(val, k))
s.grid(row=row, column=col, padx=5, pady=2, sticky='ew')
cls._rpy_sliders[k] = s
row += 1
# 3D cubesat image
[docs]
@classmethod
def sat_label(cls, parent, row, col, width=300, height=300):
""" sat_label """
l = tk.Label(parent, bg='whitesmoke', borderwidth=0, width=width, height=height)
l.grid(row=row, column=col, padx=5, pady=2, sticky='w')
cls._sat_label = l
return cls._sat_label
# photo image
[docs]
@classmethod
def photo_label(cls, parent, row, col, width=300, height=300):
""" photo_label """
l = tk.Label(parent, bg='cyan', borderwidth=0, width=width, height=height)
l.grid(row=row, column=col, padx=5, pady=2, sticky='w')
cls._photo_label = l
# cls._image150x150(width, height)
return cls._photo_label
# RESET BUTTON
[docs]
@classmethod
def do_reset(cls):
""" do_reset """
cls._full_sky.do_reset()
[docs]
class FullSky:
"""
FullSky - the core drawing code for the stars - also includes logic for the user interface
:param root: The tk root value.
:type root: Tk
:param font_family: Name of font to use inside pyplot area.
:type font_familiy: str
:param font_size: Size of font to use inside pyplot area.
:type font_size: str
"""
# https://matplotlib.org/stable/gallery/color/named_colors.html#css-colors
_COLORS = {
'fig-facecolor': '#ebebeb',
'sky-facecolor': 'whitesmoke',
'sky-grid': 'lightgrey',
'sky-axis': 'dimgrey',
'sky-label': 'black',
'sky-ecliptic': 'dimgrey',
'sky-galactic': 'saddlebrown',
'reset-color': 'lightcoral',
'reset-text': 'cornsilk',
'plot-center': 'darkgreen',
'plot-corners': 'darkblue',
'plot-polygon': 'orange',
'plot-hull': 'orange',
'plot-pixels': 'cornflowerblue',
'plot-pixels-corner': 'royalblue',
'stars': 'black',
'stars-found': 'magenta',
'constellations': 'red',
'sun': 'darkorange',
'moon': 'dimgrey',
'match': 'red',
}
_verbose = False
_switch_accelerate_time = False
_switch_match_stars = False
def __init__(self, root=None, font_family='san-serif', font_size=10):
"""
FullSky - the core drawing code for the stars - also includes logic for the user interface
:param root: The tk root value.
:type root: Tk
:param font_family: Name of font to use inside pyplot area.
:type font_familiy: str
:param font_size: Size of font to use inside pyplot area.
:type font_size: str
"""
if root is None:
raise ValueError('FullSky() needs root value')
self._root = root
self._timer_id = None
self._font_family = font_family
self._font_size = font_size
self._create_subplot()
self._paint_axis()
self._nikon = NikonD5Camera()
self._canvas = None
self.plot_ecliptic()
self.plot_sun_moon()
if False:
# not needed presently becaused the number of stars plotted is so low
self.plot_galactic_plane()
self._items_plotted = []
self._center_line_plot = None
self._center_line_data = None
self._stars_plot = None
mag = 5.0
self._scbsc5 = StarsConstellationsBSC5(mag)
[docs]
def cubesat_viewer_register(self, u=3, label=None, w=400, h=300):
"""
cubesat_viewer_register - setup Cubesat.
"""
self._cv = DoCubesatViewer(u=u, label=label, w=w, h=h)
[docs]
def camera_image_register(self, label=None, nx=400, ny=300, w=400, h=300):
"""
camera_image_register - setup Camera Image.
"""
self._ci = DoCameraImage(label=label, nx=nx, ny=ny, w=w, h=h)
[docs]
def pyplot_canvas_area_register(self, canvas):
""" pyplot_canvas_area_register """
self._canvas = canvas
def _create_subplot(self):
""" _create_subplot - core logic to build the pyplot area. """
# plt.style.use('classic')
# plt.rcParams['toolbar'] = 'None' # was 'toolbar2' but we don't need the controls
self._fig = Figure(figsize=(14.0, 7.04), dpi=65, layout='tight')
self._fig.patch.set_linewidth(0)
self._fig.tight_layout(pad=0.0, h_pad=0.0, w_pad=0.0)
self._fig.set_layout_engine(layout='tight')
self._fig.patch.set_facecolor(FullSky._COLORS['fig-facecolor'])
# projection='mollweide' is an equal-area, pseudocylindrical projection where parallels are straight lines.
self._ax = self._fig.add_subplot(1, 1, 1, projection='mollweide')
self._ax.set_facecolor(FullSky._COLORS['sky-facecolor'])
def _paint_axis(self):
""" _paint_axis - x and y axis (which is really ra and dec). """
# grid lines
self._ax.grid(color=FullSky._COLORS['sky-grid'], alpha=0.5)
# x axis (ra)
# 24 hours is 360 degrees - one hour is 15 degrees. Mark every other hour (i.e 30 degrees)
# note that this projection is -180 to +180 - we need to handle that later on - for now we simply change the lables
# When dealing with astronomical coordinates the right ascension increases eastward, located conventionally left on celestial charts.
x_values = [math.radians(v) for v in range(-180,180+30,30)]
x_labels = ['', '10h','8h','6h','4h','2h','0h','22h','20h','18h','16h','14h', '']
self._ax.set_xticks(x_values, x_labels, rotation='vertical', color=FullSky._COLORS['sky-axis'], alpha=0.5,
fontdict={'fontsize':self._font_size, 'horizontalalignment':'center', 'verticalalignment':'center'}
)
self._ax.set_xlabel('Right Ascension (hours)', color=FullSky._COLORS['sky-label'],
fontdict={'fontsize':self._font_size, 'horizontalalignment':'center', 'verticalalignment':'top'}
)
# y axis (dec)
y_values = [math.radians(v) for v in range(-90,90+10,10)]
y_labels = [str(v) for v in range(-90,90+10,10)]
# remove 90 and 80 and -80 and -90 (as they don't draw correctly - and aren't needed)
for ii in [0, 1, -1, -2]:
y_labels[ii] = ''
self._ax.set_yticks(y_values, y_labels, color=FullSky._COLORS['sky-axis'],
fontdict={'fontsize':self._font_size, 'horizontalalignment':'center', 'verticalalignment':'center'}
)
self._ax.set_ylabel('Declination (degrees)', color=FullSky._COLORS['sky-label'],
fontdict={'fontsize':self._font_size, 'horizontalalignment':'right', 'verticalalignment':'center'}
)
[docs]
def update_full_sky(self):
""" update_full_sky - primary logic to redraw the sky plot. """
# Key Details for Mollweide Projection in Matplotlib:
# Input Data Formats: Data must be in radians, not degrees, for proper positioning
# X-Axis (Longitude): Ranges from -PI to +PI
# Y-Axis (Latitude): Ranges from -PI/2 to +PI/2
# Plotting with longitude (x) and latitude (y) in radians
# plt.plot(lon_in_radians, lat_in_radians)
# clean up from previous run
self.items_plotted_clear()
# center
self.plot_center_data()
self.plot_center()
# adjust camera/satellite attitude
self.nikon.camera.choose_attitude(
'vv',
cam_yaw_deg=UserInterface.rpy_values_deg['roll'],
cam_pitch_deg=UserInterface.rpy_values_deg['pitch'],
cam_roll_deg=UserInterface.rpy_values_deg['yaw']
)
#self.nikon.camera.choose_attitude('star', star_ra_deg=100, star_dec_deg=70)
#self.nikon.camera.choose_attitude('nadir')
#self.nikon.camera.choose_attitude('ground', earth_lat_deg=36.974117, earth_lon_deg=-122.030792)
# border
p = self.plot_border_vectors()
self.items_plotted_add(p)
# camera view /box/polygon
show_box = False
show_poly = False
show_hull = False
if show_box:
# A box does not show edges correctly
p = self.plot_corners()
self.items_plotted_add(p)
if show_poly:
# Four dots in the corners
p = self.plot_polygons()
self.items_plotted_add(p)
if show_hull:
p = self.plot_hull()
self.items_plotted_add(p)
# matrix of pixels (a matrix of pixels)
p = self.plot_pixels(nsteps=1)
self.items_plotted_add(p)
if self._switch_match_stars:
p = self.plot_matched_closest_star()
if p:
self.items_plotted_add(p)
p = self.camera_image_matched_stars()
if p:
self.items_plotted_add(p)
# build return string - showing camera info
angular_width, angular_height, solid_angle_steradians = self.nikon.camera_fov_angular_width_height()
s = '%s\n%s' % (self.nikon.obs_time.strftime('%Y-%m-%d %H:%M:%S %Z'), str(self.nikon.camera))
s += '\n%.1f deg width by %.1f deg height |' % (angular_width, angular_height)
s += ' %.1f%% of whole sky' % (100.0*solid_angle_steradians/(4*math.pi))
UserInterface.camera_info(s)
_sun = None
_moon = None
[docs]
def plot_sun_moon(self):
""" plot_sun_moon - add sun and moon to the sky plot. """
fraction = moon_illumination(self.nikon.obs_time)
moon_ra_rad, moon_dec_rad = body('moon', self.nikon.obs_time)
moon_ra_rad = ra_fix([moon_ra_rad])[0]
if self._moon:
self._moon.set_offsets([[moon_ra_rad, moon_dec_rad]])
else:
self._moon = self._ax.scatter([moon_ra_rad], [moon_dec_rad], color=FullSky._COLORS['moon'], alpha=1.0, s=50.0)
sun_ra_rad, sun_dec_rad = body('sun', self.nikon.obs_time)
sun_ra_rad = ra_fix([sun_ra_rad])[0]
# sun diameter is 0.533 degrees
if self._sun:
self._sun.set_offsets([[sun_ra_rad, sun_dec_rad]])
else:
self._sun = self._ax.scatter([sun_ra_rad], [sun_dec_rad], color=FullSky._COLORS['sun'], alpha=1.0, s=300.0)
[docs]
def plot_galactic_plane(self):
""" plot_galactic_plane - add galactic plan line to the sky plot. """
ra_dec_rad = galactic_plane()
segments = split_plot_mollweide_line(ra_dec_rad[0], ra_dec_rad[1], is_radians=True)
for ra_rad, dec_rad in segments:
ra_rad = ra_fix(ra_rad)
self._ax.plot(ra_rad, dec_rad, label='Galactic Plane', alpha=0.2, color=FullSky._COLORS['sky-galactic'], linewidth=30.0) #, linestyle='dashed')
[docs]
def plot_ecliptic(self):
""" plot_ecliptic - add ecliptic line to the sky plot """
ra_dec_rad = ecliptic()
segments = split_plot_mollweide_line(ra_dec_rad[0], ra_dec_rad[1], is_radians=True)
for ra_rad, dec_rad in segments:
ra_rad = ra_fix(ra_rad)
self._ax.plot(ra_rad, dec_rad, label='Ecliptic Plane', alpha=0.75, color=FullSky._COLORS['sky-ecliptic'], linewidth=0.75, linestyle='dotted')
[docs]
def plot_stars(self):
""" plot_star - add all the stars to the sky plot """
# BSC5 stars
stars_ra_rad, stars_dec_rad, stars_mag = self._scbsc5.get_stars()
stars_ra_rad = ra_fix(stars_ra_rad)
stars_size_pixels = mag_map(stars_mag)
const_ra_rad, const_dec_rad, const_mag = self._scbsc5.get_constellations()
const_ra_rad = ra_fix(const_ra_rad)
const_size_pixels = mag_map(const_mag)
p1 = self._ax.scatter(stars_ra_rad, stars_dec_rad, s=stars_size_pixels, alpha=1, color=FullSky._COLORS['stars'], zorder=5)
p2 = self._ax.scatter(const_ra_rad, const_dec_rad, s=const_size_pixels, alpha=1, color=FullSky._COLORS['constellations'], zorder=5)
return p1, p2
[docs]
def draw(self):
""" draw - flush everything to the screen. """
self._canvas.draw()
@property
def nikon(self):
""" nikon - return NikonD5 class. """
return self._nikon
@property
def fig(self):
""" fig """
return self._fig
@property
def ax(self):
""" ax """
return self._ax
@property
def root(self):
""" root """
return self._root
@property
def canvas(self):
""" canvas """
return self._canvas
[docs]
def items_plotted_clear(self):
""" items_plotted_clear """
if len(self._items_plotted) == 0:
return
for p in self._items_plotted:
if isinstance(p, PathCollection):
p.remove()
elif isinstance(p, list):
p[0].remove()
self._items_plotted = []
[docs]
def items_plotted_add(self, p):
""" items_plotted_add """
if isinstance(p, list):
self._items_plotted += p
else:
self._items_plotted.append(p)
[docs]
def plot_center_data(self):
""" plot_center_data """
ra_deg, dec_deg = self.nikon.pixel_to_radec(self.nikon.camera.nx/2, self.nikon.camera.ny/2)
ra_rad = math.radians(ra_deg)
dec_rad = np.array([math.radians(dec_deg)])
ra_rad = ra_fix(np.array([ra_rad]))
new_data = np.array([np.array([ra_rad[0], dec_rad[0]])])
if self._center_line_data is None:
self._center_line_data = new_data
else:
self._center_line_data = np.append(self._center_line_data, new_data, axis=0)
# we keep this one on the screen - so processed differently
[docs]
def plot_center(self):
""" plot_center """
if self._center_line_plot is None:
# this is an empty plot
p = self._ax.plot([], [], color=FullSky._COLORS['plot-center'], alpha=1.0, linewidth=1.5, zorder=10)
self._center_line_plot = p
if self._center_line_data is None:
return
# trim data (43 is a random number)
self._center_line_data = self._center_line_data[-43:]
if isinstance(self._center_line_plot, PathCollection):
self._center_line_plot.set_offsets(self._center_line_data)
else:
# print('center_line_data =', np.degrees(self._center_line_data))
# self._center_line_plot[0].set_data(self._center_line_data[:, 0], self._center_line_data[:, 1])
segments = split_plot_mollweide_line(np.degrees(self._center_line_data[:, 0]), np.degrees(self._center_line_data[:, 1]))
# TODO - off by one error still!
all_ra_rad = []
all_dec_rad = []
for ra_rad, dec_rad in segments:
# ra_rad = ra_fix(ra_rad)
all_ra_rad.extend(ra_rad[1:])
all_dec_rad.extend(dec_rad[1:])
if len(all_ra_rad) == 1:
all_ra_rad.extend(ra_rad)
all_dec_rad.extend(dec_rad)
all_ra_rad.append(np.nan)
all_dec_rad.append(np.nan)
# print('all_ra_rad =', np.degrees(all_ra_rad))
# print('all_dec_rad =', np.degrees(all_dec_rad))
self._center_line_plot[0].set_data(all_ra_rad, all_dec_rad)
[docs]
def plot_center_clear(self):
""" plot_center_clear """
self._center_line_data = None
[docs]
def plot_corners(self):
""" plot_corners """
box, _ = self.nikon.camera_fov_radec_box()
corners = [
box['top_left'],
box['top_right'],
box['bottom_right'],
box['bottom_left'],
box['top_left'], # close rectangle back to first point
]
ra_rad = [math.radians(v['ra_deg']) for v in corners]
dec_rad = [math.radians(v['dec_deg']) for v in corners]
ra_rad = ra_fix(ra_rad)
# not fixed for wrap yet
p = self._ax.plot(ra_rad, dec_rad, color=FullSky._COLORS['plot-corners'], alpha=0.25, linewidth=3.0, linestyle='dashed')
return p
[docs]
def plot_polygons(self):
""" plot_polygons """
_, polygon = self.nikon.camera_fov_radec_box()
ra_rad = [math.radians(float(v)) for v in polygon[0]]
dec_rad = [math.radians(float(v)) for v in polygon[1]]
ra_rad = ra_fix(ra_rad)
p = self._ax.scatter(ra_rad, dec_rad, color=FullSky._COLORS['plot-polygon'], alpha=0.5, s=40.0)
return p
[docs]
def plot_hull(self):
""" plot_hull """
hull_coords, _ = self.nikon.camera_fov_convex_hull()
ra_rad = [math.radians(float(v)) for v in hull_coords[0]]
dec_rad = [math.radians(float(v)) for v in hull_coords[1]]
ra_rad = ra_fix(ra_rad)
# not fixed for wrap yet
p = self._ax.plot(ra_rad, dec_rad, color=FullSky._COLORS['plot-hull'], alpha=0.15, linewidth=2.0, linestyle='solid')
return p
[docs]
def plot_pixels(self, nsteps=3):
""" plot_pixels """
pixels = self.nikon.sensor_to_radec(nsteps=nsteps)
ra_rad = [math.radians(v[0]) for v in pixels.values()]
dec_rad = [math.radians(v[1]) for v in pixels.values()]
ra_rad = ra_fix(ra_rad)
# all points
p3 = self._ax.scatter(ra_rad, dec_rad, color=FullSky._COLORS['plot-pixels'], alpha=0.75, s=20.0)
# learing left point/corner
p1 = self._ax.scatter(ra_rad[nsteps:nsteps+1], dec_rad[nsteps:nsteps+1], color=FullSky._COLORS['plot-pixels-corner'], alpha=1.0, s=100.0)
# learing right point/corner
p2 = self._ax.scatter(ra_rad[-1:], dec_rad[-1], color=FullSky._COLORS['plot-pixels-corner'], alpha=1.0, s=100.0)
return [p1,p2,p3]
[docs]
def plot_border_vectors(self):
""" plot_border_vectors """
border_vectors = self.nikon.camera_fov_border_vectors()
plots = []
for ra_rad, dec_rad in split_plot_mollweide_line_ra_dec_deg(border_vectors):
ra_rad = ra_fix(ra_rad)
p = self._ax.plot(ra_rad, dec_rad, color='red', alpha=1.0, linewidth=1.5, linestyle='solid')
plots.append(p)
return plots
def _match_stars_in_polygon(self):
""" match_stars_in_polygon """
border_vectors = self.nikon.camera_fov_border_vectors(border_step=10)
# look for stars ...
inside_mask = stars_in_polygon_icrs(self._scbsc5.skycoords, border_vectors)
found_stars = self._scbsc5.skycoords[inside_mask]
return found_stars, inside_mask
[docs]
def camera_image_matched_stars(self):
""" camera_image_matched_stars """
# now find all stars inside camara view ...
found_stars, inside_mask = self._match_stars_in_polygon()
if len(found_stars) == 0:
return None
# all the stars (will be masked before use)
stars_ra_rad, stars_dec_rad, stars_mag = self._scbsc5.get_stars()
stars_ra_rad = stars_ra_rad[inside_mask]
stars_dec_rad = stars_dec_rad[inside_mask]
stars_mag = stars_mag[inside_mask]
# camera image
def build_xy_list(found_stars):
""" build_xy_list """
xy_list = []
for star in found_stars:
ra_deg = star.ra.degree
dec_deg = star.dec.degree
try:
px, py = self.nikon.radec_to_pixel(ra_deg, dec_deg)
except SatelliteCameraError as e:
# outside the view of the camera - should not happen if polygon is accurate
continue
xy_list.append((px,py))
if len(found_stars) != len(xy_list):
print('build_xy_list():', len(found_stars), '!=', len(xy_list))
return xy_list
def build_mag_bucket_list(stars_mag):
""" build_mag_bucket_list """
bucket = {}
for mag in stars_mag:
try:
bucket[int(mag)] += 1
except KeyError:
bucket[int(mag)] = 1
s = sorted(['%d:%d' % (k, v) for k,v in bucket.items()])
return '[' + ', '.join(s) + ']'
self._ci.stars(xy_list=build_xy_list(found_stars), mag_list=stars_mag)
#self._ci.outline()
s = 'Camera image has %d stars' % len(found_stars)
s += '\nMagnitude: ' + build_mag_bucket_list(stars_mag)
UserInterface.misc_text(s)
stars_ra_rad = ra_fix(stars_ra_rad)
stars_size_pixels = mag_map(stars_mag)
# plot
p1 = self._ax.scatter(stars_ra_rad, stars_dec_rad, s=stars_size_pixels, alpha=1, color=FullSky._COLORS['stars-found'], zorder=5)
return p1
[docs]
def plot_matched_closest_star(self):
""" plot_matched_closest_star """
# look for matched (closest) star
matches = self._match_against_bsc5()
ra_rad = []
dec_rad = []
for m in matches:
center_ra_rad = m['coords'].ra.radian
center_dec_rad = m['coords'].dec.radian
star_ra_rad = m['star_coord'].ra.radian
star_dec_rad = m['star_coord'].dec.radian
star = matches[0]['bsc5']
if star.constellation:
c = ConstellationDatabase.fullmatch(star.constellation)
c = c[0].constellation + ' (' + c[0].meaning + ')'
else:
c = None
s = 'camera [%.1f,%.1f] deg\n star [%.1f,%.1f] deg +/- %.3f deg\n %s%s @ %.1f mag' % (
math.degrees(center_ra_rad), math.degrees(center_dec_rad),
math.degrees(star_ra_rad), math.degrees(star_dec_rad),
math.degrees(arcseconds_to_radians(m['separation_arcsec'])),
star.name if star.name else '-',
' in ' + c if c else '',
star.mag
)
UserInterface.star_found_text(s)
ra_rad.append(center_ra_rad)
dec_rad.append(center_dec_rad)
ra_rad.append(star_ra_rad)
dec_rad.append(star_dec_rad)
# TODO - only one for now
break
# TODO powered down for now - does not unpaint itself (yet)
if self._switch_match_stars:
star = matches[0]['bsc5']
diameter = mag_map(star.mag, multiplier=4.0)
ra_rad = ra_fix(ra_rad)
p1 = self._ax.plot(ra_rad, dec_rad, label='Star match', alpha=1.0, color=FullSky._COLORS['match'], linewidth=3.0)
p2 = self._ax.scatter(ra_rad[-1:], dec_rad[-1:], facecolors='none', edgecolors=FullSky._COLORS['match'], s=diameter)
p3 = self._ax.scatter(ra_rad[0:1], dec_rad[0:1], facecolors=FullSky._COLORS['plot-center'], edgecolors=FullSky._COLORS['plot-center'], s=60)
return [p1, p2, p3]
return None
[docs]
def do_match_stars(self, value):
""" do_match_stars """
match_stars_show = value.get()
self._switch_match_stars = match_stars_show
if match_stars_show:
# also show stars if not showing
self._do_stars_real(False)
UserInterface.stars_button_set(True)
self._do_stars_real(True)
else:
s = ''
UserInterface.star_found_text(s)
UserInterface.misc_text(s)
[docs]
def do_mag(self, value):
""" do_mag """
self._do_stars_real(False)
self._scbsc5.max_mag = value
# turn on stars button
UserInterface.stars_button_set(True)
self._do_stars_real(True)
[docs]
def do_focal_length(self, focal_length):
""" do_focal_length """
self.nikon.camera.reload(focal_length_mm=float(focal_length))
# refresh everything
self.update_full_sky()
self.draw()
[docs]
def do_realtime(self, value):
""" do_realtime """
self._switch_accelerate_time = value.get()
# reset line data - otherwise it's messy
self.plot_center_clear()
self.draw()
# reset timer interval
self.timer_reset()
[docs]
def do_stars(self, value):
""" do_stars """
self._do_stars_real(value.get())
def _do_stars_real(self, value):
""" do_stars_real """
self.do_stars_clear()
if value:
# show stars
self._stars_plot = self.plot_stars()
self.draw()
[docs]
def do_stars_clear(self):
""" do_stars_clear """
if self._stars_plot is not None:
for p in self._stars_plot:
p.remove()
self._stars_plot = None
[docs]
def do_rpy(self, val, k):
""" do_rpy """
UserInterface.rpy_values_deg[k] = float(val)
# refresh everything
self.update_full_sky()
self.draw()
self._cv.update_orientation(
UserInterface.rpy_values_deg['pitch'],
UserInterface.rpy_values_deg['roll'],
UserInterface.rpy_values_deg['yaw']
)
[docs]
def do_reset(self):
""" do_reset """
# RESET focus
focal_length = 50
UserInterface.focal_length_buttons_set(focal_length=focal_length)
self.nikon.camera.reload(focal_length_mm=float(focal_length))
# RESET star magnitude
mag = 5.0
UserInterface.star_mag_buttons_set(mag=mag)
self._scbsc5.max_mag = mag
# RESET accelerate
UserInterface.realtime_button_set(False)
self._switch_accelerate_time = False
# RESET show stars
self.do_stars_clear()
UserInterface.stars_button_set(False)
# RESET rpy
for k in UserInterface.rpy_values_deg:
UserInterface.rpy_values_deg[k] = 0.0
UserInterface.v_sliders[k].set(0.0)
# RESET match
self._switch_match_stars = False
UserInterface.match_stars_button_set(False)
# RESET text boxes
s = ''
UserInterface.star_found_text(s)
UserInterface.misc_text(s)
# RESET camera image
self._ci.reset()
# Now redraw everything - by reseting the timer.
self.plot_center_clear()
# self.update_full_sky()
# self.draw()
self._cv.update_orientation(
UserInterface.rpy_values_deg['pitch'],
UserInterface.rpy_values_deg['roll'],
UserInterface.rpy_values_deg['yaw']
)
# RESET timer interval
self.timer_reset()
def _match_against_bsc5(self):
""" _match_against_bsc5 """
# see if we actually match any stars within the view from the camera?
center_pixel_x = self.nikon.camera.nx/2
center_pixel_y = self.nikon.camera.ny/2
ra_deg, dec_deg = self.nikon.pixel_to_radec(center_pixel_x, center_pixel_y)
# center
# create a (for now) array of length one to test against
pixels = {(center_pixel_x, center_pixel_y): (ra_deg, dec_deg)}
# pixel_list & coords_list need to be in-sync (obviously)
pixel_list = list(pixels.keys())
coords_list = list(pixels.values())
# get into SkyCoords's with ICRS
coords_list_icrs = SkyCoord(coords_list, unit='deg', frame='icrs')
# For each image direction, find nearest star in catalog
idx, sep2d, _ = coords_list_icrs.match_to_catalog_sky(self._scbsc5.skycoords)
# Return matches with separation info
matches = []
for ii in range(len(coords_list_icrs)):
matches.append({
'pixel': pixel_list[ii], 'coords': coords_list_icrs[ii],
'bsc5': self._scbsc5.stars[idx[ii]],
'star_coord': self._scbsc5.skycoords[idx[ii]],
'separation_arcsec': sep2d[ii].arcsec,
})
return matches
[docs]
def timer_reset(self):
""" timer_reset """
if self._timer_id is not None:
self.root.after_cancel(self._timer_id)
self._timer_id = None
# reprime
self.timer_went_off()
[docs]
def timer_went_off(self, repaint=True):
""" timer_went_off """
self._timer_id = None
# update the time and recaculate the attitude (based on the new time)
# and deal with time interval and timers
if self._switch_accelerate_time is True:
# jump time ahead quickly - TODO this value should be based on orbital params
seconds = (60 - self.nikon.obs_time.second) + 60
self.nikon.adjust_by_seconds(seconds)
# hence step is half a second
timer_step = 500
else:
# realtime
self.nikon.now()
# hence step is five seconds
timer_step = 5 * 1000
if repaint:
# now repaint/update sky
self.update_full_sky()
self.plot_sun_moon() # add sun and moon move when time changes (slightly)
self.draw()
# and finally, setup trigger the next timer
self._timer_id = self.root.after(timer_step, lambda: self.timer_went_off())
[docs]
class StarsConstellationsBSC5:
""" StarsConstellationsBSC5 """
def __init__(self, mag=5.0):
""" StarsConstellationsBSC5 """
self._bsc5 = BSC5Stars(max_mag=mag)
# self.stars = self._bsc5.stars
# self.skycoords = self._bsc5.skycoords
@property
def stars(self):
""" stars """
return self._bsc5.stars
@property
def skycoords(self):
""" skycoords """
return self._bsc5.skycoords
@property
def max_mag(self):
""" max_mag """
return self._bsc5.max_mag
@max_mag.setter
def max_mag(self, value):
""" max_mag """
self._bsc5.max_mag = value
[docs]
def get_stars(self):
""" stars """
stars_ra_rad, stars_dec_rad, stars_mag = self._bsc5.get_stars()
return stars_ra_rad, stars_dec_rad, stars_mag
[docs]
def get_constellations(self, constellations=['Orion', 'Leo']):
""" constellations """
const_ra_rad, const_dec_rad, const_mag = self._bsc5.get_constellations(constellations=constellations)
return const_ra_rad, const_dec_rad, const_mag