#!/usr/bin/env python3 # -*- coding: utf-8 -*- ''' ------ Classes ------ ScrollFrame Application Home_Page Table_Page Post_Card_Page Spectral_Feature_Catalogue_Page Photometer_Image_Page Help_Page ------ Functions ------ isFloat Create_Thread ''' import tkinter as tk import numpy as np "matplotlib.use('Agg')" import matplotlib.pyplot as plt from os import path import urllib import urllib.request import webbrowser import pickle import threading import queue from matplotlib import figure from tkinter import ttk from tkinter import filedialog as fd from tkinter import WORD from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from PIL import Image, ImageTk from io import BytesIO from astropy import units as u from astropy.coordinates import SkyCoord from astropy.table import unique, Table from astropy.io import fits from .figtools import sky_image from . import io as spio import urllib3 http = urllib3.PoolManager() # Creates some usefull colors and fonts that will be used throughout the application #not entirely the same colours, variable naming hindsight is 20/20 #matplotlib colors but a bit fainter Normal_Font = ('Verdana 11') Arial_Font = ('Times') Big_Font_Important = ('Verdana 15 bold') GUI_Green = '#9aed9a' #1 GUI_Red = '#ffb06b' #2 GUI_Blue = '#91bbd9' #3 GUI_Dark3 = '#1f77b4' def GUI(SAFECAT, save_dir): class ScrollFrame(tk.Frame): ''' This class defines a custom widget that is not contained in the normal tkinter module. This widget acts a normal frame with the additional feature of being scrollable. To use, simply create an instance of this class and pass the parent frame as the only parameter. To then place widgets inside this special frame, pack them into "instance_variable.Container" Attributes ------------ Canvas : a tkinter canvas widget Container : a tkinter frame widget that holds any other items or widgets you would want to put into this class Methods ------------ reconfigure : resizes the canvas so it will match any changes in the conatiners size ''' def __init__(self, parent): ''' Creates and places all of the necessary widgets to have a scrollable frame widget. Parameters ---------- parent : tkinter frame This is the frame in which this class widget will be placed. Returns ------- None. ''' tk.Frame.__init__(self, parent) self.Canvas = tk.Canvas(master=self, bg=GUI_Blue, highlightthickness=0) self.Container = tk.Frame(master=self.Canvas, bg=GUI_Blue) Scrollbar = tk.Scrollbar(master=self, orient=tk.VERTICAL, bg=GUI_Blue, highlightbackground=GUI_Blue, highlightthickness=0, troughcolor=GUI_Blue, command=self.Canvas.yview) self.Container.bind("", self.reconfigure) self.Canvas.create_window((0, 0), window=self.Container, anchor="nw") self.Canvas.configure(yscrollcommand=Scrollbar.set) Scrollbar.pack(side='right', fill='y', expand=True) self.Canvas.pack(side='left', fill='both', expand=True) def reconfigure(self, event): ''' reconfigures the canvases width so it can match any changes the container experiences. Parameters ---------- event : tkinter event Used to determine the new width of the canvas Returns ------- None. ''' w = event.width # finds the width of the container window so the canvas can be resized accordingly self.Canvas.configure(width=w, scrollregion=self.Canvas.bbox("all") ) def isFloat(value): ''' Is used to check the users inputs and make sure it can be converted into a float value. Parameters ---------- value : unknown type from an entry box This is the value the function is checking. Returns ------- bool Returns True if the value can be converted to a float. Returns False if the value cannot be converted. ''' try: float(value) return True except ValueError: return False def Create_Thread(Function): ''' Starts a new thread that runs the given Function Parameters ---------- Function : function The function that will be run in the thread Returns ------- None. ''' q = queue.Queue() q.put(Function) T = threading.Thread(target=Function) T.start() class Application(tk.Tk): ''' This class is called in the very last lines of the program and is used to instantiate all of the other classes and place them into a tkinter Notebook widget. This allows all of the pages to be created in seperate classes and then combined here. Attributes ------------ None Methods ------------ None ''' def __init__(self): # Initilize the GUI window tk.Tk.__init__(self) tk.Tk.wm_title(self, 'SAFECAT Filtering Application') tk.Tk.grid_rowconfigure(self, 0, weight=1) tk.Tk.grid_columnconfigure(self, 0, weight=1) global menuTabs global Home_Page_instance global PostCard_Page_instance global Expanded_Table_instance global Spectral_Feature_Catalogue_instance global Photometer_Image_instance global Help_Page_instance global Ack_Page_instance # Create the tabs at the top of the GUI to help navigate between the various pages menuTabs = ttk.Notebook(self) menuTabs.pack(fill='both', expand=True) # Create an instance for each page of the GUI and pack them into the menutabs Help_Page_instance = Help_Page(parent_frame=menuTabs) Help_Page_instance.grid(row=0, column=0, sticky='nsew') Ack_Page_instance = Ack_Page(parent_frame=menuTabs) Ack_Page_instance.grid(row=0, column=0, sticky='nsew') Photometer_Image_instance = Photometer_Image_Page(parent_frame=menuTabs) Photometer_Image_instance.grid(row=0, column=0, sticky='nsew') Spectral_Feature_Catalogue_instance = Spectral_Feature_Catalogue_Page(parent_frame=menuTabs) Spectral_Feature_Catalogue_instance.grid(row=0, column=0, sticky='nsew') Expanded_Table_instance = Table_Page(parent_frame=menuTabs) Expanded_Table_instance.grid(row=0, column=0, sticky='nsew') PostCard_Page_instance = Post_Card_Page(parent_frame=menuTabs) PostCard_Page_instance.grid(row=0, column=0, sticky='nsew') Home_Page_instance = Home_Page(parent_frame=menuTabs) Home_Page_instance.grid(row=0, column=0, sticky='nsew') # Add the page instances to an individual menu tab menuTabs.add(Home_Page_instance, text='Home') menuTabs.add(PostCard_Page_instance, text='Post Cards') menuTabs.add(Expanded_Table_instance, text='Expanded Table') menuTabs.add(Spectral_Feature_Catalogue_instance, text='Spectral Feature Catalogue') menuTabs.add(Photometer_Image_instance, text='Photometer Image') menuTabs.add(Help_Page_instance, text='Help') menuTabs.add(Ack_Page_instance, text='Acknowledgements and Citation') class Home_Page(tk.Frame): ''' This class is responsible for creating all of the functions and widgets that run on the home page of the application. Attributes -------------- Scroll_Filters : ScrollFrame An instance of the ScrollFrame class which is used to encapsulate all of the filtering options freq_menu_var : tk.StringVar variable to hold any one of the options displayed to the user (All Frequenies, Choose a Specific Range, etc) min_freq_input : tk.Entry variable to hold the users input for the minimum frequncy max_freq_input : tk.Entry variable to hold the users input for the maximum frequncy velocity_menu_var : tk.StringVar variable to hold any one of the options displayed to the user (All Velocities, Choose a Specific Range) min_velocity_input : tk.Entry variable to hold the users input for the minimum velocity max_velocity_input : tk.Entry variable to hold the users input for the maximum velocity cal_menu_var : tk.StringVar variable to hold any one of the options displayed to the user (All Calibration Methods, Point Source, etc) sampling_menu_var : tk.StringVar variable to hold any one of the options displayed to the user (All Sampling Types, Sparse, etc) opDay_menu_var : tk.StringVar variable to hold any one of the options displayed to the user (All Operation Days, Choose a Specific Range) min_opDay_input : tk.Entry variable to hold the users input for the minimum operation day max_opDay_input : tk.Entry variable to hold the users input for the maximum operation day SNR_menu_var : tk.StringVar variable to hold any one of the options displayed to the user (All Signal to Noise Ratios, Choose a Specific Range) min_SNR_input : tk.Entry variable to hold the users input for the minimum signal to noise ration max_SNR_input : tk.Entry variable to hold the users input for the maximum signal to noise ration Sky_Coordinate_menu_var : tk.StringVar variable to hold any one of the options displayed to the user (All Sky Coordinates, Choose a Specific Range) RA_input : tk.Entry variable to hold the users input for the RA coordinate Dec_input : tk.Entry variable to hold the users input for Dec coordinate radius_input : tk.Entry variable to hold the users input for the radius of search around their previous RA and Dec coordinate degree_toggle_var : tk.IntVar holds an integer value representing whether the user has toggled for degrees or arcmins detector_menu_var : tk.StringVar variable to hold any one of the options displayed to the user (Both, SLW, SSW) species_menu_var : tk.StringVar variable to hold any one of the options displayed to the user (All Species, Choose Specific Species) Species_Listbox : tk.ListBox a listbox widget that holds all of the different molecular species the user can select out1, out2, out3 : tk.Label label widgets used to display information to the user such as the number of uniques obsids contained within their search obsid_info1 - obsid_info10 : tk.Label 10 different label widgets used to display specific information about 1 specific obsid info_table : tk.Treeview This widget is what the table was created inside. You can add columns and headers to it! Table_Loading_window : tk.Frame a frame widget that contains a progressbar showing the user how long it will take to produce the table Table_progressbar : ttk.Progressbar a progress bar that shows how long it will take to populate the table freq_input_window : tk.Frame A frame widget that contains additional widgets the user can us to enter information velocity_input_window : tk.Frame A frame widget that contains additional widgets the user can us to enter information opDay_input_window : tk.Frame A frame widget that contains additional widgets the user can us to enter information SNR_input_window : tk.Frame A frame widget that contains additional widgets the user can us to enter information Sky_Coordinate_input_window : tk.Frame A frame widget that contains additional widgets the user can us to enter information species_input_window : tk.Frame A frame widget that contains additional widgets the user can us to enter information Filter_Progressbar : ttk.Progressbar A progressbar that shows how long the program will take to filter the SAFECAT table Output_Entry_List : list A list that contains all of the obsids the program finds that fit the users search parameters All_Info_table_button : tk.Button A button that enables the user to view the table with all of the filtered entries obsid_info_window : tk.Frame A frame that contains all of the specific information to 1 specific obsid unique_list_calibration_type : list A list containing all of the different calibration types for a specific obsid unique_list_sampling_type : list A list containing all of the different sampling types for a specific obsid search_var : tk.StringVar A variable that holds the users obsid search num_of_searchs_found : tk.Label A label that displays how many obsids match the users obsid search Methods ------------ Create_Filter_Options : Creates all of the widgets the user will use to filter the SAFECAT table Create_Output_Display : Creates all of the widgets that will be used to display the resulting filtered entries to the user Create_Table_Display : Creates the table that the program will use to diplay the entries to the user _close_table : Simple method that removes the table from the home page _freq_pop_up : Adds the freq_input_window frame _velocity_pop_up : Adds the velocity_input_window frame _opDay_pop_up : Adds the opDay_input_window frame _SNR_pop_up : Adds the SNR_input_window frame _Sky_Coordinate_pop_up : Adds the Sky_Coordinate_input_window frame _species_pop_up : Adds the species_input_window frame _Execute_Filters : Applies all of the filters to the SAFECAT table and displays the information to the widgets created in the Create_Output_Display method _obsid_info_popup : Adds the obsid_info_window frame and displays all of the relevant info for the chosen obsid Full_Obsid_info_table : Displays all of the filtered table entries in the table created in the Create_Table_Display method Single_Obsid_info_table : Displays all of the entries relevent to the selected obsid that also fit within the filtering parameters List_URLs : A method that returns all of the post card and catalogue urls for a given obsid. Also includes aff-axis urls highlight_obsid_list : Gets the search variable from the GUI and finds all obsid entries that contain the search variable. Also highlights any of these entries. display_input_error(text) : Displays the text parameter on the main output screen _Go_to_HSA_Website : Opens the HSA website and goes to the specific obsids entry. ''' def __init__(self, parent_frame): ''' Creates a tkinter frame that all other class widgets will be placed into. Also calls the Create_Filter_Options, Create_Output_Display and Create_Table_Display methods to produce the three main graphical components of the home page. Parameters ---------- parent_frame : tkinter frame widget The frame that this class will contained inside. Returns ------- None. ''' tk.Frame.__init__(self, parent_frame) # This allows all widgets to scale properly to the size of the GUI Window for i in range(2): tk.Frame.columnconfigure(self, index=i, weight=1) for i in range(2): tk.Frame.rowconfigure(self, index=i, weight=1) # Call all of the methods created below which populate the home page with their corresponding widgets self.Create_Filter_Options() self.Create_Output_Display() self.Create_Table_Display() def Create_Filter_Options(self): ''' Creates all of the widgets and variables that the user can use to specify their filtering parameters. Returns ------- None. ''' # Create the main input frame which will apear on the left and contain all of the filtering options for the user to inteact with Main_Input_Frame = tk.Frame(master=self, relief=tk.SUNKEN, borderwidth=5, bg=GUI_Blue) Main_Input_Frame.grid(row=0, column=0, sticky='ns', pady=(30, 10), padx=10) Main_Input_Frame.rowconfigure(index=1, weight=1, minsize=400) # Create the title for this section of the GUI Title = tk.Label(master=Main_Input_Frame, text='Filter Options', bg=GUI_Blue, fg='white', font='Verdana 18 bold underline') Title.grid(row=0, column=0, columnspan=2) # Create an instance of the custum widget created above. It is used to scroll through the list of filtering options self.Scroll_Filters = ScrollFrame(Main_Input_Frame) self.Scroll_Filters.grid(row=1, column=1, sticky='nsew') ######################################################################################################### # Create the frequency Filter frame ######################################################################### ######################################################################################################### frequency_frame = tk.Frame(master=self.Scroll_Filters.Container, relief=tk.GROOVE, borderwidth=5) freq_title = tk.Label(master=frequency_frame, text='Select the range of frequencies:') frequency_frame.pack(pady=10, padx=10, fill='both', expand=True) freq_title.pack() freq_menu_list = ('All Frequencies', 'Choose a Specific Range', 'SLW Range (447-990)', 'SSW Range (958-1546)') self.freq_menu_var = tk.StringVar() self.freq_menu_var.set(freq_menu_list[0]) ############ Create the window that will pop up to enter the frequency range ######## self.freq_input_window = tk.Frame(master=frequency_frame, relief=tk.RAISED, borderwidth=5) for i in range(8): self.freq_input_window.columnconfigure(i, weight=1, minsize=5) for i in range(1): self.freq_input_window.rowconfigure(i, weight=1, minsize=2) min_freq_label = tk.Label(master=self.freq_input_window, text='Min:') min_freq_label.grid(row=0, column=0) self.min_freq_input = tk.Entry(master=self.freq_input_window, width=8) self.min_freq_input.grid(row=0, column=1) freq_units1 = tk.Label(master=self.freq_input_window, text='GHz') freq_units1.grid(row=0, column=2) max_freq_label = tk.Label(master=self.freq_input_window, text='Max:') max_freq_label.grid(row=0, column=5) self.max_freq_input = tk.Entry(master=self.freq_input_window, width=8) self.max_freq_input.grid(row=0, column=6) freq_units2 = tk.Label(master=self.freq_input_window, text='GHz') freq_units2.grid(row=0, column=7) #################################################################################### freq_menu = tk.OptionMenu(frequency_frame, self.freq_menu_var, *freq_menu_list, command=self._freq_pop_up) freq_menu.pack() #################################################################################################################### # Create the Velocity Filter frame #################################################################################### #################################################################################################################### velocity_frame = tk.Frame(master=self.Scroll_Filters.Container, relief=tk.GROOVE, borderwidth=5) velocity_title = tk.Label(master=velocity_frame, text='Select the range of velocities:') velocity_frame.pack(pady=10, padx=10, fill='both', expand=True) velocity_title.pack() velocity_menu_list = ('All Velocities', 'Choose a Specific Range') self.velocity_menu_var = tk.StringVar() self.velocity_menu_var.set(velocity_menu_list[0]) ############### Create the window that will pop up to enter the velocity range ######### self.velocity_input_window = tk.Frame(master=velocity_frame, relief=tk.RAISED, borderwidth=5) for i in range(8): self.velocity_input_window.columnconfigure(i, weight=1, minsize=5) for i in range(1): self.velocity_input_window.rowconfigure(i, weight=1, minsize=2) min_velocity_label = tk.Label(master=self.velocity_input_window, text='Min:') min_velocity_label.grid(row=0, column=0) self.min_velocity_input = tk.Entry(master=self.velocity_input_window, width=8) self.min_velocity_input.grid(row=0, column=1) velocity_units1 = tk.Label(master=self.velocity_input_window, text='Km/s') velocity_units1.grid(row=0, column=2) max_velocity_label = tk.Label(master=self.velocity_input_window, text='Max:') max_velocity_label.grid(row=0, column=5) self.max_velocity_input = tk.Entry(master=self.velocity_input_window, width=8) self.max_velocity_input.grid(row=0, column=6) velocity_units2 = tk.Label(master=self.velocity_input_window, text='Km/s') velocity_units2.grid(row=0, column=7) #################################################################################### velocity_menu = tk.OptionMenu(velocity_frame, self.velocity_menu_var, *velocity_menu_list, command=self._velocity_pop_up) velocity_menu.pack() ################################################################################################## # Create the calibration Filter frame ############################################################### ################################################################################################## calibration_frame = tk.Frame(master=self.Scroll_Filters.Container, relief=tk.GROOVE, borderwidth=5) calibration_title = tk.Label(master=calibration_frame, text='Choose the type of calibration performed:') calibration_frame.pack(pady=10, padx=10, fill='both', expand=True) calibration_title.pack() cal_menu_list = ('All Calibration Methods', 'Point Source', 'Extended Source') self.cal_menu_var = tk.StringVar() self.cal_menu_var.set(cal_menu_list[0]) cal_menu = tk.OptionMenu(calibration_frame, self.cal_menu_var, *cal_menu_list) cal_menu.pack() ################################################################################################## # Create the sampling Filter frame ############################################################### ################################################################################################## sampling_frame = tk.Frame(master=self.Scroll_Filters.Container, relief=tk.GROOVE, borderwidth=5) sampling_title = tk.Label(master=sampling_frame, text='Choose the sampling type:') sampling_frame.pack(pady=10, padx=10, fill='both', expand=True) sampling_title.pack() sample_menu_list = ('All Sampling Types', 'Sparse', 'Intermediate', 'Full') self.sample_menu_var = tk.StringVar() self.sample_menu_var.set(sample_menu_list[0]) sample_menu = tk.OptionMenu(sampling_frame, self.sample_menu_var, *sample_menu_list) sample_menu.pack() ################################################################################################## # Create the operation day Filter frame ############################################################### ################################################################################################## opDay_frame = tk.Frame(master=self.Scroll_Filters.Container, relief=tk.GROOVE, borderwidth=5) opDay_title = tk.Label(master=opDay_frame, text='Choose the range of operation days:') opDay_frame.pack(pady=10, padx=10, fill='both', expand=True) opDay_title.pack() opDay_menu_list = ('All Operation Days', 'Choose a Specific Range') self.opDay_menu_var = tk.StringVar() self.opDay_menu_var.set(opDay_menu_list[0]) ############### Create the window that will pop up to enter the operation day range ######### self.opDay_input_window = tk.Frame(master=opDay_frame, relief=tk.RAISED, borderwidth=5) for i in range(8): self.opDay_input_window.columnconfigure(i, weight=1, minsize=5) for i in range(1): self.opDay_input_window.rowconfigure(i, weight=1, minsize=2) min_opDay_label = tk.Label(master=self.opDay_input_window, text='Min:') min_opDay_label.grid(row=0, column=0) self.min_opDay_input = tk.Entry(master=self.opDay_input_window, width=8) self.min_opDay_input.grid(row=0, column=1) opDay_units1 = tk.Label(master=self.opDay_input_window, text='Days') opDay_units1.grid(row=0, column=2) max_opDay_label = tk.Label(master=self.opDay_input_window, text='Max:') max_opDay_label.grid(row=0, column=5) self.max_opDay_input = tk.Entry(master=self.opDay_input_window, width=8) self.max_opDay_input.grid(row=0, column=6) opDay_units2 = tk.Label(master=self.opDay_input_window, text='Days') opDay_units2.grid(row=0, column=7) #################################################################################### opDay_menu = tk.OptionMenu(opDay_frame, self.opDay_menu_var, *opDay_menu_list, command=self._opDay_pop_up) opDay_menu.pack() ################################################################################################## # Create the signal to noise ratio Filter frame ############################################################### ################################################################################################## SNR_frame = tk.Frame(master=self.Scroll_Filters.Container, relief=tk.GROOVE, borderwidth=5) SNR_title = tk.Label(master=SNR_frame, text='Choose the range of SNR:') SNR_frame.pack(pady=10, padx=10, fill='both', expand=True) SNR_title.pack() SNR_menu_list = ('All Signal to Noise Ratios', 'Choose a Specific Range') self.SNR_menu_var = tk.StringVar() self.SNR_menu_var.set(SNR_menu_list[0]) ############### Create the window that will pop up to enter the SNR range ######### self.SNR_input_window = tk.Frame(master=SNR_frame, relief=tk.RAISED, borderwidth=5) for i in range(8): self.SNR_input_window.columnconfigure(i, weight=1, minsize=5) for i in range(1): self.SNR_input_window.rowconfigure(i, weight=1, minsize=2) min_SNR_label = tk.Label(master=self.SNR_input_window, text='Min:') min_SNR_label.grid(row=0, column=0) self.min_SNR_input = tk.Entry(master=self.SNR_input_window, width=8) self.min_SNR_input.grid(row=0, column=1) max_SNR_label = tk.Label(master=self.SNR_input_window, text='Max:') max_SNR_label.grid(row=0, column=5) self.max_SNR_input = tk.Entry(master=self.SNR_input_window, width=8) self.max_SNR_input.grid(row=0, column=6) #################################################################################### SNR_menu = tk.OptionMenu(SNR_frame, self.SNR_menu_var, *SNR_menu_list, command=self._SNR_pop_up) SNR_menu.pack() ################################################################################################## # Create the sky coordinates Filter frame ############################################################### ################################################################################################## Sky_Coordinate_frame = tk.Frame(master=self.Scroll_Filters.Container, relief=tk.GROOVE, borderwidth=5) Sky_Coordinate_title = tk.Label(master=Sky_Coordinate_frame, text='Choose a range of sky coordinates:') Sky_Coordinate_frame.pack(pady=10, padx=10, fill='both', expand=True) Sky_Coordinate_title.pack() Sky_Coordinate_menu_list = ('All Sky Coordinates', 'Choose a Specific Range') self.Sky_Coordinate_menu_var = tk.StringVar() self.Sky_Coordinate_menu_var.set(Sky_Coordinate_menu_list[0]) ############### Create the window that will pop up to enter the coordinate range ######### self.Sky_Coordinate_input_window = tk.Frame(master=Sky_Coordinate_frame, relief=tk.RAISED, borderwidth=5) for i in range(8): self.Sky_Coordinate_input_window.columnconfigure(i, weight=1, minsize=3) for i in range(2): self.Sky_Coordinate_input_window.rowconfigure(i, weight=1, minsize=2) RA_label = tk.Label(master=self.Sky_Coordinate_input_window, text='RA:') RA_label.grid(row=1, column=0) self.RA_input = tk.Entry(master=self.Sky_Coordinate_input_window, width=6) self.RA_input.grid(row=1, column=1) Dec_label = tk.Label(master=self.Sky_Coordinate_input_window, text='Dec:') Dec_label.grid(row=1, column=3) self.Dec_input = tk.Entry(master=self.Sky_Coordinate_input_window, width=6) self.Dec_input.grid(row=1, column=4) radius_label = tk.Label(master=self.Sky_Coordinate_input_window, text='Radius:') radius_label.grid(row=1, column=6) self.radius_input = tk.Entry(master=self.Sky_Coordinate_input_window, width=6) self.radius_input.grid(row=1, column=7) #################################################################################### # Add the toggle to select between degrees and arcmins degree_toggle_frame = tk.Frame(master=self.Sky_Coordinate_input_window) degree_toggle_frame.grid(row=0, column=2, columnspan=4) self.degree_toggle_var = tk.IntVar() self.degree_toggle_var.set(1) degree_toggle1 = tk.Radiobutton(master=degree_toggle_frame, text='Degrees', variable=self.degree_toggle_var, value=1) degree_toggle2 = tk.Radiobutton(master=degree_toggle_frame, text='arcmin', variable=self.degree_toggle_var, value=2) degree_toggle1.pack(side=tk.LEFT) degree_toggle2.pack(side=tk.RIGHT) Sky_Coordinate_menu = tk.OptionMenu(Sky_Coordinate_frame, self.Sky_Coordinate_menu_var, *Sky_Coordinate_menu_list, command=self._Sky_Coordinate_pop_up) Sky_Coordinate_menu.pack() ################################################################################################## # Create the detector type Filter frame ############################################################### ################################################################################################## detector_frame = tk.Frame(master=self.Scroll_Filters.Container, relief=tk.GROOVE, borderwidth=5) detector_title = tk.Label(master=detector_frame, text='Sort by Detector type:') detector_frame.pack(pady=10, padx=10, fill='both', expand=True) detector_title.pack() detector_menu_list = ('both', 'SSW', 'SLW') self.detector_menu_var = tk.StringVar() self.detector_menu_var.set(detector_menu_list[0]) detector_menu = tk.OptionMenu(detector_frame, self.detector_menu_var, *detector_menu_list) detector_menu.pack() ################################################################################################## # Create the Species Filter frame ############################################################### ################################################################################################## species_frame = tk.Frame(master=self.Scroll_Filters.Container, relief=tk.GROOVE, borderwidth=5) species_title = tk.Label(master=species_frame, text='Select the Species included in the search:') species_frame.pack(pady=10, padx=10, fill='both', expand=True) species_title.pack() species_menu_list = ('All Species', 'Choose Specific Species') self.species_menu_var = tk.StringVar() self.species_menu_var.set(species_menu_list[0]) ############### Create the window that will pop up to enter the SNR range ######### self.species_input_window = tk.Frame(master=species_frame, relief=tk.RAISED, borderwidth=5) self.Species_Listbox = tk.Listbox(master=self.species_input_window, selectmode=tk.MULTIPLE, height=15) self.Species_Listbox.pack() list_of_species = ['Carbon', 'Carbon Monoxide', 'Nitrogen', 'Water', 'Hydrogen Cyanide', 'Hydrogen Isocyanide', 'Silicon Monoxide', 'Hydrogen Fluoride', 'Hydrogen Sulfide', 'Formyl (1+) ion', 'Formaldehyde', 'Sulfur Monoxide', 'Diazenylium', 'Carbon Monosulfide', 'Methylidyne (1+) ion'] for specie in list_of_species: self.Species_Listbox.insert('end', specie) #################################################################################### species_menu = tk.OptionMenu(species_frame, self.species_menu_var, *species_menu_list, command=self._species_pop_up) species_menu.pack() # Enter Button to submit the users inputs Enter_Button = tk.Button(master=Main_Input_Frame, text='Find Features', highlightbackground=GUI_Blue, command= self._Execute_Filters) Enter_Button.grid(row=2, column=0, columnspan=2, pady=(15, 5)) def Create_Output_Display(self): ''' Creates all of the widgets that will be used to display information to the user on the home page. Returns ------- None. ''' # Create the main output frame Output_Frame = tk.Frame(master=self, relief=tk.SUNKEN, borderwidth=5, background='black') Output_Frame.grid(row=0, column=1, sticky='nsew', padx=(10, 30), pady=(30, 10)) # These lines create two label frames that the 'run' function will print to self.out1 = tk.Label(master=Output_Frame, text='Please Choose From The Search Options On The Left', fg='white', bg='black', font=Big_Font_Important) self.out1.pack(padx=2) self.out2 = tk.Label(master=Output_Frame, text=' ', fg='white', bg='black', font=Big_Font_Important) self.out3 = tk.Label(master=Output_Frame, text=' ', fg='white', bg='black', font=Big_Font_Important) # Add the button to display the table with all applicable obsids self.All_Info_table_button = tk.Button(master=Output_Frame, text='View Table For All Obsids In Selected Parameters', highlightbackground='black', command= self.Full_Obsid_info_table) # Add the progress bar self.Filter_Progressbar = ttk.Progressbar(master=Output_Frame, orient='horizontal', length=300) # Create a container to hold list of obsids and other relevant widgets self.container = tk.Frame(master=Output_Frame, background='black') # This creates a scrollable list to display the all of the obsids that correspond to the filters Top_Descripton = tk.Label(master=self.container, text='Scrollable list of all the observation IDs \n along with their target name:',fg='white', bg='black', font=Normal_Font ) Obsid_Scroll_Frame = tk.Frame(master=self.container, background='black') scrollbar = tk.Scrollbar(master=Obsid_Scroll_Frame) self.Obsid_List = tk.Listbox(master=Obsid_Scroll_Frame, selectmode=tk.SINGLE, width=34, yscrollcommand=scrollbar.set) self.Obsid_List.bind('<>', self._obsid_info_popup) scrollbar.config(command=self.Obsid_List.yview) Bottom_Description = tk.Label(master=self.container, text='Click on an entry for more information!',fg='white', bg='black', font=Normal_Font ) # Create a search bar for the obsid list search_bar_frame = tk.Frame(master=self.container, bg='black') search_label = tk.Label(master=search_bar_frame, text='Search the Obsid List:', bg='black', fg='white') search_label.pack(side='left') self.search_var = tk.StringVar() self.search_var.trace_add('write', self.highlight_obsid_list) search_entry = tk.Entry(master=search_bar_frame, textvariable=self.search_var, width=10, highlightbackground='black') search_entry.pack(side='left') self.num_of_searchs_found = tk.Label(master=search_bar_frame, bg='black', fg='white') self.num_of_searchs_found.pack(side='right') # Pack all of the widgets into the parent frame Top_Descripton.pack() Obsid_Scroll_Frame.pack(fill='y', expand=True) scrollbar.pack(side='right', fill='y', expand=True) self.Obsid_List.pack(side='left', fill='y', expand=True) Bottom_Description.pack() search_bar_frame.pack() # Creatse a pop up window to display relevent information about the selected obsid self.obsid_info_window = tk.Frame(master=Output_Frame, relief=tk.RAISED, borderwidth=5, background=GUI_Red) self.obsid_info1 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg=GUI_Dark3, font = "Verdana 14 bold") self.obsid_info1.pack() self.obsid_info2 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg='black') self.obsid_info2.pack() self.obsid_info3 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg='black') self.obsid_info3.pack() self.obsid_info4 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg='black') self.obsid_info4.pack() self.obsid_info5 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg='black') self.obsid_info5.pack() self.obsid_info6 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg='black') self.obsid_info6.pack() self.obsid_info7 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg='black') self.obsid_info7.pack() self.obsid_info8 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg='black') self.obsid_info8.pack() self.obsid_info9 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg='black') self.obsid_info9.pack() self.obsid_info10 = tk.Label(master=self.obsid_info_window, text=' ', bg=GUI_Red, fg='black') self.obsid_info10.pack() # Create a container to hold all of the user buttons Button_Matrix_Container = tk.Frame(master=self.obsid_info_window, bg=GUI_Red) Button_Matrix_Container.pack(side='bottom') SAFECAT_Table_button = tk.Button(master=Button_Matrix_Container, text='View SAFECAT Table For This Obsid', highlightbackground=GUI_Red, command=lambda: Create_Thread(self.Single_Obsid_info_table)) SAFECAT_Table_button.grid(row=0, column=0, columnspan=3, padx=3, pady=3, sticky='nsew') spectral_feature_catalogue_button = tk.Button(master=Button_Matrix_Container, text='View Spectral Feature Catalogue Information', highlightbackground=GUI_Red, command=lambda: Create_Thread(Spectral_Feature_Catalogue_instance.Display_Fits_File_Data)) spectral_feature_catalogue_button.grid(row=1, column=0, columnspan=3, padx=3, pady=3, sticky='nsew') post_card_button = tk.Button(master=Button_Matrix_Container, text='Post Cards', highlightbackground=GUI_Red, command=lambda: Create_Thread(PostCard_Page_instance.Display_Post_Cards)) post_card_button.grid(row=2, column=0, padx=3, pady=3, sticky='nsew') photometer_image_button = tk.Button(master=Button_Matrix_Container, text='Photometer Image', highlightbackground=GUI_Red, #command=lambda: Create_Thread( command = Photometer_Image_instance.Produce_Photometer_Image) photometer_image_button.grid(row=2, column=1, padx=3, pady=3, sticky='nsew') HSA_Link_Button = tk.Button(master=Button_Matrix_Container, text='HSA Link', highlightbackground=GUI_Red, command=self._Go_to_HSA_Website) HSA_Link_Button.grid(row=2, column=2, padx=3, pady=3, sticky='nsew') def Create_Table_Display(self): ''' Creates the table the program will use to display information to the user. All of the columns and header text is set in this method. Returns ------- None. ''' # Create the Frame that will display all of the table data self.table_output_window = tk.Frame(master=self, relief=tk.SUNKEN, borderwidth=8) side_bar = tk.Frame(master=self.table_output_window) side_bar.pack(side=tk.RIGHT, fill='y', expand=True) table_x_button = tk.Button(master=side_bar, text='X', bg=GUI_Red, command=self._close_table) table_x_button.pack() table_Y_scrollbar = tk.Scrollbar(master=side_bar) table_Y_scrollbar.pack(fill='y', expand=True) self.info_table = ttk.Treeview(master=self.table_output_window, yscrollcommand=table_Y_scrollbar.set) self.info_table.pack(side=tk.LEFT, fill='both', expand=True) # Set up column widths: self.info_table["columns"]=("one","two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty", "twentyone") self.info_table.column("#0", width=110, minwidth=110, stretch=tk.NO) self.info_table.column("one", width=50, minwidth=50, stretch=tk.NO) self.info_table.column("two",width=110, minwidth=110, stretch=tk.NO) self.info_table.column("three", width=70, minwidth=70, stretch=tk.NO) self.info_table.column("four", width=90, minwidth=80, stretch=tk.NO) self.info_table.column("five", width=110, minwidth=110, stretch=tk.NO) self.info_table.column("six",width=75, minwidth=75, stretch=tk.NO) self.info_table.column("seven", width=45, minwidth=45, stretch=tk.NO) self.info_table.column("eight", width=50, minwidth=50, stretch=tk.NO) self.info_table.column("nine", width=65, minwidth=65, stretch=tk.NO) self.info_table.column("ten", width=65, minwidth=65, stretch=tk.NO) self.info_table.column("eleven", width=65, minwidth=65, stretch=tk.NO) self.info_table.column("twelve", width=75, minwidth=75, stretch=tk.NO) self.info_table.column("thirteen", width=70, minwidth=70, stretch=tk.NO) self.info_table.column("fourteen", width=110, minwidth=110, stretch=tk.NO) self.info_table.column("fifteen", width=110, minwidth=110, stretch=tk.NO) self.info_table.column("sixteen",width=40, minwidth=40, stretch=tk.NO) self.info_table.column("seventeen", width=40, minwidth=40, stretch=tk.NO) self.info_table.column("eighteen", width=40, minwidth=40, stretch=tk.NO) self.info_table.column("nineteen", width=100, minwidth=100, stretch=tk.NO) self.info_table.column("twenty", width=120, minwidth=120, stretch=tk.NO) self.info_table.column("twentyone", width=50, minwidth=50, stretch=tk.NO) # Create the column titles self.info_table.heading("#0",text="Obsid",anchor=tk.CENTER) self.info_table.heading("one", text="opDay",anchor=tk.CENTER) self.info_table.heading("two", text="Frequency (GHz)",anchor=tk.CENTER) self.info_table.heading("three",text="VelocityCorrFreq",anchor=tk.CENTER) self.info_table.heading("four", text="Template Freq",anchor=tk.CENTER) self.info_table.heading("five", text="Velocity (Km/s)",anchor=tk.CENTER) self.info_table.heading("six", text="Velocity Flag",anchor=tk.CENTER) self.info_table.heading("seven", text="row",anchor=tk.CENTER) self.info_table.heading("eight",text="column",anchor=tk.CENTER) self.info_table.heading("nine", text="ra",anchor=tk.CENTER) self.info_table.heading("ten", text="dec",anchor=tk.CENTER) self.info_table.heading("eleven", text="SNR",anchor=tk.CENTER) self.info_table.heading("twelve", text="Detector",anchor=tk.CENTER) self.info_table.heading("thirteen", text="Feature Flag",anchor=tk.CENTER) self.info_table.heading("fourteen",text="calibration",anchor=tk.CENTER) self.info_table.heading("fifteen", text="sampling",anchor=tk.CENTER) self.info_table.heading("sixteen", text="hpdp",anchor=tk.CENTER) self.info_table.heading("seventeen", text="bgs",anchor=tk.CENTER) self.info_table.heading("eighteen",text="nccFlag",anchor=tk.CENTER) self.info_table.heading("nineteen", text="species",anchor=tk.CENTER) self.info_table.heading("twenty", text="transition",anchor=tk.CENTER) self.info_table.heading("twentyone", text="comment",anchor=tk.CENTER) table_Y_scrollbar.config(command=self.info_table.yview) # Create the window to display the loading bar self.Table_Loading_window = tk.Frame(master=self) progress_title = tk.Label(master=self.Table_Loading_window, text='Populating Table...', font=Big_Font_Important) progress_title.pack() self.Table_progressbar = ttk.Progressbar(master=self.Table_Loading_window, orient='horizontal', length=500) self.Table_progressbar.pack() def _close_table(self): self.table_output_window.grid_forget() def _freq_pop_up(self, selected_option): ''' If the user selects 'Choose a Specific Range' for the frequency, this method adds additional widgets for the user to select a specific range. Parameters ---------- selected_option : string The option the user selected when choosing the frequency filter Returns ------- None. ''' if selected_option == 'Choose a Specific Range': self.freq_input_window.pack(padx=5, pady=5) else: self.freq_input_window.forget() def _velocity_pop_up(self, option): ''' If the user selects 'Choose a Specific Range' for the velocity, this method adds additional widgets for the user to select a specific range. Parameters ---------- option : string The option the user selected when choosing the velocity filter Returns ------- None. ''' if option == 'All Velocities': self.velocity_input_window.forget() if option == 'Choose a Specific Range': self.velocity_input_window.pack(padx=5, pady=5) def _opDay_pop_up(self, option): ''' If the user selects 'Choose a Specific Range' for the operation day, this method adds additional widgets for the user to select a specific range. Parameters ---------- option : string The option the user selected when choosing the opDay filter Returns ------- None. ''' if option == 'All Operation Days': self.opDay_input_window.forget() if option == 'Choose a Specific Range': self.opDay_input_window.pack(padx=5, pady=5) def _SNR_pop_up(self, option): ''' If the user selects 'Choose a Specific Range' for the signal to noise ratio, this method adds additional widgets for the user to select a specific range. Parameters ---------- option : string The option the user selected when choosing the SNR filter Returns ------- None. ''' if option == 'All Signal to Noise Ratios': self.SNR_input_window.forget() if option == 'Choose a Specific Range': self.SNR_input_window.pack(padx=5, pady=5) def _Sky_Coordinate_pop_up(self, option): ''' If the user selects 'Choose a Specific Range' for the sky coordiantes, this method adds additional widgets for the user to select a specific area of sky. Parameters ---------- option : string The option the user selected when choosing the Sky Coordinates filter Returns ------- None. ''' if option == 'All Sky Coordinates': self.Sky_Coordinate_input_window.forget() if option == 'Choose a Specific Range': self.Sky_Coordinate_input_window.pack(padx=5, pady=5) def _species_pop_up(self, option): ''' If the user selects 'Choose Specific Species' for the species filter option, this method adds additional widgets for the user to select specific molecular species. Parameters ---------- option : string The option the user selected when choosing which species they want. Returns ------- None. ''' if option == 'All Species': self.species_input_window.forget() if option == 'Choose Specific Species': self.species_input_window.pack(padx=5, pady=5) self.Scroll_Filters.update() self.Scroll_Filters.Canvas.yview_moveto(1) def _Execute_Filters(self): ''' Checks every user entry and then starts to filter the SAFECAT table using the parameters supplied by the user. This method then displays the result to the user via a listbox and a few label widgets. Returns ------- None. ''' # If statements check to see if the user input is correct if (self.freq_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.min_freq_input.get())): self.display_input_error('Invalid Minimum Frequency!') elif (self.freq_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.max_freq_input.get())): self.display_input_error('Invalid Maximum Frequency!') elif (self.velocity_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.min_velocity_input.get())): self.display_input_error('Invalid Minimum Velocity!') elif (self.velocity_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.max_velocity_input.get())): self.display_input_error('Invalid Maximum Velocity!') elif (self.opDay_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.min_opDay_input.get())): self.display_input_error('Invalid Minimum Operation Day!') elif (self.opDay_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.max_opDay_input.get())): self.display_input_error('Invalid Maximum Operation Day!') elif (self.SNR_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.min_SNR_input.get())): self.display_input_error('Invalid Minimum Signal to Noise Ratio!') elif (self.SNR_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.max_SNR_input.get())): self.display_input_error('Invalid Maximum Signal to Noise Ratio!') elif (self.Sky_Coordinate_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.RA_input.get())): self.display_input_error('Invalid Right Ascension!') elif (self.Sky_Coordinate_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.Dec_input.get())): self.display_input_error('Invalid Declination') elif (self.Sky_Coordinate_menu_var.get() == 'Choose a Specific Range') & (not isFloat(self.radius_input.get())): self.display_input_error('Invalid Radius!') elif (self.species_menu_var.get() == 'Choose Specific Species') & (len(self.Species_Listbox.curselection()) == 0): self.display_input_error('No Species Selected!') else: # Gets rid of the last sessions obsid info window and text (I use the display_input_error function even though we are displaying a message not an error) self.display_input_error('Applying Search Criteria to SAFECAT Table ...') # Add the progress bar and text self.Filter_Progressbar.pack() self.Filter_Progressbar['value'] = 0 self.Filter_Progressbar.update() # Frequency parameter ########################################################## ################################################################################ if self.freq_menu_var.get() == 'All Frequencies': SAFECAT_Mod1 = SAFECAT else: if self.freq_menu_var.get() == 'SLW Range (447-990)': Minimum_Frequency = 447 Maximum_Frequency = 990 elif self.freq_menu_var.get() == 'SSW Range (958-1546)': Minimum_Frequency = 958 Maximum_Frequency = 1546 else: Minimum_Frequency = float(self.min_freq_input.get()) Maximum_Frequency = float(self.max_freq_input.get()) index = np.where((SAFECAT['frequency'] >= Minimum_Frequency) & (SAFECAT['frequency'] <= Maximum_Frequency))[0] SAFECAT_Mod1 = SAFECAT[index] self.Filter_Progressbar.step(5) self.Filter_Progressbar.update() # Velocity parameters ########################################################## ################################################################################ if self.velocity_menu_var.get() == 'All Velocities': SAFECAT_Mod2 = SAFECAT_Mod1 else: Minimum_Velocity = float(self.min_velocity_input.get()) Maximum_Velocity = float(self.max_velocity_input.get()) index2 = np.where((SAFECAT_Mod1['velocity'] >= Minimum_Velocity) & (SAFECAT_Mod1['velocity'] <= Maximum_Velocity))[0] SAFECAT_Mod2 = SAFECAT_Mod1[index2] self.Filter_Progressbar.step(5) self.Filter_Progressbar.update() # Operation Day ################################################################ ################################################################################ if self.opDay_menu_var.get() == 'All Operation Days': SAFECAT_Mod3 = SAFECAT_Mod2 else: Minimum_opDay = float(self.min_opDay_input.get()) Maximum_opDay = float(self.max_opDay_input.get()) index3 = np.where((SAFECAT_Mod2['opDay'] >= Minimum_opDay) & (SAFECAT_Mod2['opDay'] <= Maximum_opDay))[0] SAFECAT_Mod3 = SAFECAT_Mod2[index3] self.Filter_Progressbar.step(5) self.Filter_Progressbar.update() # Siganl to Noise Ration Parameters ############################################ ################################################################################ if self.SNR_menu_var.get() == 'All Signal to Noise Ratios': SAFECAT_Mod4 = SAFECAT_Mod3 else: Minimum_SNR = float(self.min_SNR_input.get()) Maximum_SNR = float(self.max_SNR_input.get()) index4 = np.where((SAFECAT_Mod3['SNR'] >= Minimum_SNR) & (SAFECAT_Mod3['SNR'] <= Maximum_SNR))[0] SAFECAT_Mod4 = SAFECAT_Mod3[index4] self.Filter_Progressbar.step(5) self.Filter_Progressbar.update() # Applying the Calibration filter ############################################## ################################################################################ if self.cal_menu_var.get() == 'All Calibration Methods': SAFECAT_Mod5 = SAFECAT_Mod4 elif self.cal_menu_var.get() == 'Point Source': index5 = np.where(SAFECAT_Mod4['calibration'] == 'pointSource')[0] SAFECAT_Mod5 = SAFECAT_Mod4[index5] else: index5 = np.where(SAFECAT_Mod4['calibration'] == 'extended')[0] SAFECAT_Mod5 = SAFECAT_Mod4[index5] self.Filter_Progressbar.step(5) self.Filter_Progressbar.update() # Applying the Sampling type filter ############################################ ################################################################################ if self.sample_menu_var.get() == 'All Sampling Types': SAFECAT_Mod6 = SAFECAT_Mod5 elif self.sample_menu_var.get() == 'Sparse': index6 = np.where(SAFECAT_Mod5['sampling'] == 'sparse')[0] SAFECAT_Mod6 = SAFECAT_Mod5[index6] elif self.sample_menu_var.get() == 'Full': index6 = np.where(SAFECAT_Mod5['sampling'] == 'full')[0] SAFECAT_Mod6 = SAFECAT_Mod5[index6] else: index6 = np.where(SAFECAT_Mod5['sampling'] == 'intermediate')[0] SAFECAT_Mod6 = SAFECAT_Mod5[index6] self.Filter_Progressbar.step(5) self.Filter_Progressbar.update() # Sky Coordinates parameters #################################################### ################################################################################# if self.Sky_Coordinate_menu_var.get() == 'All Sky Coordinates': SAFECAT_Mod7 = SAFECAT_Mod6 elif (self.Sky_Coordinate_menu_var.get() == 'Choose a Specific Range') & (self.degree_toggle_var.get() == 1): input_coord = SkyCoord(ra=float(self.RA_input.get()), dec=float(self.Dec_input.get()), frame='icrs', unit='deg') radius = float(self.radius_input.get()) * u.deg catalog = SkyCoord(SAFECAT_Mod6['ra'],SAFECAT_Mod6['dec'], frame='icrs', unit='deg') Seperation_Distance = input_coord.separation(catalog) in_radius = (Seperation_Distance <= radius) index7 = np.where(in_radius)[0] SAFECAT_Mod7 = SAFECAT_Mod6[index7] else: input_coord = SkyCoord(ra=float(self.RA_input.get()), dec=float(self.Dec_input.get()), frame='icrs', unit='arcmin') radius = float(self.radius_input.get()) * u.arcmin catalog = SkyCoord(SAFECAT_Mod6['ra'],SAFECAT_Mod6['dec'], frame='icrs', unit='arcmin') Seperation_Distance = input_coord.separation(catalog) in_radius = (Seperation_Distance <= radius) index7 = np.where(in_radius)[0] SAFECAT_Mod7 = SAFECAT_Mod6[index7] self.Filter_Progressbar.step(5) self.Filter_Progressbar.update() # Detector Type Parameter #################################################### ############################################################################## index8 = [] if self.detector_menu_var.get() == 'both': SAFECAT_Mod8 = SAFECAT_Mod7 elif self.detector_menu_var.get() == 'SLW': for i in range(len(SAFECAT_Mod7)): detector_type = SAFECAT_Mod7['detector'][i] if detector_type[0:3] == 'SLW': index8.append(i) SAFECAT_Mod8 = SAFECAT_Mod7[index8] else: for i in range(len(SAFECAT_Mod7)): detector_type = SAFECAT_Mod7['detector'][i] if detector_type[0:3] == 'SSW': index8.append(i) SAFECAT_Mod8 = SAFECAT_Mod7[index8] self.Filter_Progressbar.step(5) self.Filter_Progressbar.update() # Species Type Parameter ###################################################### ############################################################################### if self.species_menu_var.get() == 'All Species': global SAFECAT_MOD9 SAFECAT_MOD9 = SAFECAT_Mod8 self.Filter_Progressbar.step(30) self.Filter_Progressbar.update() else: # This creates a list of all the species in the safecat species column but without the '-13-' isotope naming convention in it new_species_list = [] for i in range(len(SAFECAT_Mod8)): specie_ = str(SAFECAT_Mod8['species'][i]) if specie_.find('13') != -1: new_named_specie = specie_.replace('-13-', '') new_species_list.append(new_named_specie) else: new_species_list.append(specie_) # Get the list of species the user requested user_selected_species = self.Species_Listbox.curselection() index9 = [] for selection in user_selected_species: if selection == 0: for i, spc in enumerate(new_species_list): if spc.find('C-atom') != -1: index9.append(i) elif spc.find('C+') != -1: index9.append(i) elif selection == 1: for i, spc in enumerate(new_species_list): if spc[0:2] == 'CO': index9.append(i) elif selection == 2: for i, spc in enumerate(new_species_list): if spc[0:3] == 'NII': index9.append(i) elif selection == 3: for i, spc in enumerate(new_species_list): if spc.find('H2O') != -1: index9.append(i) elif selection == 4: for i, spc in enumerate(new_species_list): if spc.find('HCN') != -1: index9.append(i) elif selection == 5: for i, spc in enumerate(new_species_list): if spc.find('HNC') != -1: index9.append(i) elif selection == 6: for i, spc in enumerate(new_species_list): if spc.find('SiO') != -1: index9.append(i) elif selection == 7: for i, spc in enumerate(new_species_list): if spc.find('HF') != -1: index9.append(i) elif selection == 8: for i, spc in enumerate(new_species_list): if spc.find('H2S') != -1: index9.append(i) elif selection == 9: for i, spc in enumerate(new_species_list): if spc.find('HCO') != -1: index9.append(i) elif selection == 10: for i, spc in enumerate(new_species_list): if spc.find('H2CO') != -1: index9.append(i) elif selection == 11: for i, spc in enumerate(new_species_list): if spc.find('SO') != -1: index9.append(i) elif selection == 12: for i, spc in enumerate(new_species_list): if spc.find('N2H+') != -1: index9.append(i) elif selection == 13: for i, spc in enumerate(new_species_list): if spc.find('CS') != -1: index9.append(i) elif selection == 14: for i, spc in enumerate(new_species_list): if spc.find('CH+') != -1: index9.append(i) self.Filter_Progressbar.step(30/len(user_selected_species)) self.Filter_Progressbar.update() SAFECAT_MOD9 = SAFECAT_Mod8[index9] # Create a list in order to keep track of all the obsids that correspond to the users search parameters self.Output_Entry_List = [] self.Output_Entry_List.clear() # clears it each time the user selects different search parameters # Calculate the number of table entries and number of unique obsids total_entries = len(SAFECAT_MOD9) total_objects = len(np.unique(SAFECAT_MOD9['obsid'])) unique_features_table = unique(SAFECAT_MOD9, keys=['opDay', 'frequency', 'row', 'column', 'detector']) self.Obsid_List.delete(0,tk.END) # Delete all previous entries # read in the pcikled file containing a list of target names assocaited with the obsid with open(path.join(path.dirname(__file__), 'obsid_targetName_list.pickle'), 'rb') as pickled_file: pickled_obsid_list, pickled_target_name_list = pickle.load(pickled_file) # Counter, threshold, and step amount are used to control how often the GUI screen is updated during the following 'for' loop counter=0 threshold = total_objects/30 step_amount = 30*threshold/total_objects for i in range(total_objects): obsid_string = str(np.unique(SAFECAT_MOD9['obsid'])[i]) # Find the associated target name ind = pickled_obsid_list.index(obsid_string) target_name_string = str(pickled_target_name_list[ind]) entry = obsid_string + ' ' + '[' + target_name_string + ']' self.Obsid_List.insert('end', entry) # Also add to entry list in order to use the search bar function self.Output_Entry_List.append(entry) counter += 1 if counter > threshold: # Allows the application to update every time the loop has iterated more than the threshold self.Filter_Progressbar.step(step_amount) self.Filter_Progressbar.update() counter=0 # pack the scrollable obsid frame, the button, and the text into the output frame self.Filter_Progressbar.forget() self.out2.pack(padx=2) self.out3.pack(padx=2) self.All_Info_table_button.pack(side=tk.TOP) self.container.pack(side='right', fill='y', expand=True, pady=10, padx=10) # print out the total features, total table entries, and unique object counts self.out1["text"] = f'There are {total_entries} total table entries within your search parameters!' self.out2["text"] = f'There are {total_objects} unique observation IDs within your search parameters!' self.out3["text"] = f'There are {len(unique_features_table)} features within your search parameters!' def _obsid_info_popup(self, event): ''' Displays information about a specific obsid including the its sampling and calibration type as well as max and min SNR values. Parameters ---------- event : tkinter event event passed to this function whenever an obsid item is selected in the listbox Returns ------- None. ''' # Find out what obsid the user picked selected_obsid_list_index = self.Obsid_List.curselection() # must create an if statement because if the user deselects the listbox then we want nothing to happen if selected_obsid_list_index: global current_selected_obsid current_selected_obsid = self.Obsid_List.get(selected_obsid_list_index)[0:10] # find the indexs of the SAFECAT table that correspond to the currently selected obsid index = np.where(SAFECAT_MOD9['obsid'] == int(current_selected_obsid))[0] SAFECAT_Mod = SAFECAT_MOD9[index] # print the pop up window with the choosen obsid self.obsid_info_window.pack(side='left', fill='both', expand=True, pady=30, padx=10 ) self.obsid_info1["text"] = f'Information For Observation ID: {current_selected_obsid}' # Print the target name target = self.Obsid_List.get(selected_obsid_list_index)[14:-1] self.obsid_info2['text'] = f'Target Name: {target}' # Print the RA and DEC coordinated for this target ra = np.round(SAFECAT_Mod['ra'][0], 4) dec = np.round(SAFECAT_Mod['dec'][0], 4) self.obsid_info3['text'] = f'RA: {ra} degrees' self.obsid_info4['text'] = f'DEC: {dec} degrees' #find how many table entries this obsid has number_of_entries = len(SAFECAT_Mod) self.obsid_info5["text"] = f'This observation ID has {number_of_entries} table entries associated with it' # Find how many features are associated with this obsid features_table = unique(SAFECAT_Mod, keys=['opDay', 'frequency', 'row', 'column', 'detector']) self.obsid_info6["text"] = f'This observation ID has {len(features_table)} features associated with it' # List the sampling type for this object self.unique_list_sampling_type = [] self.unique_list_sampling_type.clear() self.unique_list_sampling_type.append(SAFECAT_Mod['sampling'][0]) for i in range(number_of_entries): if SAFECAT_Mod['sampling'][i] != SAFECAT_Mod['sampling'][0]: self.unique_list_sampling_type.append(SAFECAT_Mod['sampling'][i]) for k in range(number_of_entries): if (SAFECAT_Mod['sampling'][k] != SAFECAT_Mod['sampling'][i]) & (SAFECAT_Mod['sampling'][k] != SAFECAT_Mod['sampling'][0]): self.unique_list_sampling_type.append(SAFECAT_Mod['sampling'][k]) break break self.obsid_info7["text"] = f'Sampling Type: {", ".join(str(x) for x in self.unique_list_sampling_type)}' # Find out what types of calibration were performed on this object self.unique_list_calibration_type = [] self.unique_list_calibration_type.clear() self.unique_list_calibration_type.append(SAFECAT_Mod['calibration'][0]) for i in range(number_of_entries): if SAFECAT_Mod['calibration'][i] != SAFECAT_Mod['calibration'][0]: self.unique_list_calibration_type.append(SAFECAT_Mod['calibration'][i]) for k in range(number_of_entries): if (SAFECAT_Mod['calibration'][k] != SAFECAT_Mod['calibration'][i]) & (SAFECAT_Mod['calibration'][k] != SAFECAT_Mod['calibration'][0]): self.unique_list_calibration_type.append(SAFECAT_Mod['calibration'][k]) break break self.obsid_info8["text"] = f'Calibration Type(s): {", ".join(str(x) for x in self.unique_list_calibration_type)}' # Find the maximum and minimum SNR's for this object obsid_SNR = [SAFECAT_Mod['SNR'][i] for i in range(number_of_entries)] self.obsid_info9["text"] = f'The maximum SNR for this object is: {np.round(max(obsid_SNR), 3)}' self.obsid_info10["text"] = f'The minimum SNR for this object is: {np.round(min(obsid_SNR), 3)}' def Full_Obsid_info_table(self): ''' Populates both the table on the home page as well as the extended table page with all of the SAFECAT entries that were within the users search criteria. Returns ------- None. ''' SAFECAT_Mod9 = SAFECAT_MOD9 # Place the loading screen into the GUI self.table_output_window.grid_forget() self.Table_Loading_window.grid(row=1, column=0, columnspan=2, padx=10) self.Table_progressbar['value'] = 0 self.update() # Save on processing time by only updating the progressbar every so often. Larger tables can be updated less often threshold = len(SAFECAT_Mod9)/11 # Calculate the progress bar step amount step_amount = (100*threshold)/(len(SAFECAT_Mod9)*2) counter=0 # This keeps track of how many times the loop has iterated since the last progressbar update # Populate the rows of the table for table in (self.info_table, Table_): table.delete(*table.get_children()) # this line deletes all inserts from the last time "more info" button was selected # fill the table for i in range(len(SAFECAT_Mod9)): table.insert("", "end", f"{i}", text=f"{SAFECAT_Mod9['obsid'][i]}", values=( str(SAFECAT_Mod9['opDay'][i]), str(np.round(SAFECAT_Mod9['frequency'][i], 2)) + '±' + str(np.round(SAFECAT_Mod9['frequencyError'][i], 2)), str(np.round(SAFECAT_Mod9['velocityCorrFreq'][i], 2)), str(np.round(SAFECAT_Mod9['templateFrequency'][i], 2)), str(np.round(SAFECAT_Mod9['velocity'][i], 2)) + '±' + str(np.round(SAFECAT_Mod9['velocityError'][i], 2)), str(SAFECAT_Mod9['velocityFlag'][i]), str(SAFECAT_Mod9['row'][i]), str(SAFECAT_Mod9['column'][i]), str(np.round(SAFECAT_Mod9['ra'][i], 2)), str(np.round(SAFECAT_Mod9['dec'][i], 2)), str(np.round(SAFECAT_Mod9['SNR'][i], 2)), str(SAFECAT_Mod9['detector'][i]), str(SAFECAT_Mod9['featureFlag'][i]), str(SAFECAT_Mod9['calibration'][i]), str(SAFECAT_Mod9['sampling'][i]), str(SAFECAT_Mod9['hpdp'][i]), str(SAFECAT_Mod9['bgs'][i]), str(SAFECAT_Mod9['nccFlag'][i]), str(SAFECAT_Mod9['species'][i]), str(SAFECAT_Mod9['transition'][i]), str(SAFECAT_Mod9['comment'][i]))) counter+=1 if counter > threshold: counter = 0 self.Table_progressbar.step(step_amount) self.Table_progressbar.update() # pack the table output window into the position 2,0 on the overall grid self.Table_Loading_window.grid_forget() self.table_output_window.grid(row=1, column=0, columnspan=2, padx=10) def Single_Obsid_info_table(self): ''' Populates both the table on the home page as well as the extended table page with all of the SAFECAT entries that belong to the currently selected obsid and fall within the chosen search parameters. Returns ------- None. ''' # find the indexs of the SAFECAT table that correspond to the currently selected obsid index = np.where(SAFECAT_MOD9['obsid'] == int(current_selected_obsid))[0] SAFECAT_Mod = SAFECAT_MOD9[index] # Place the loading screen into the GUI self.table_output_window.grid_forget() self.Table_Loading_window.grid(row=1, column=0, columnspan=2, padx=10) self.Table_progressbar['value'] = 0 self.update() # Save on processing time by only updating the progressbar every so often. Larger tables can be updated less often threshold = len(index)/9 # Calculate the progress bar step amount step_amount = (100*threshold)/(len(index)*2) counter=0 # This keeps track of how many times the loop has iterated since the last progressbar update # Populate the rows of the table for table in (self.info_table, Table_): table.delete(*table.get_children()) # this line deletes all inserts from the last time "more info" button was selected # fill the table for i in range(len(index)): table.insert("", "end", f"{i}", text=f"{current_selected_obsid}", values=( str(SAFECAT_Mod['opDay'][i]), str(np.round(SAFECAT_Mod['frequency'][i], 2)) + '±' + str(np.round(SAFECAT_Mod['frequencyError'][i], 2)), str(np.round(SAFECAT_Mod['velocityCorrFreq'][i], 2)), str(np.round(SAFECAT_Mod['templateFrequency'][i], 2)), str(np.round(SAFECAT_Mod['velocity'][i], 2)) + '±' + str(np.round(SAFECAT_Mod['velocityError'][i], 2)), str(SAFECAT_Mod['velocityFlag'][i]), str(SAFECAT_Mod['row'][i]), str(SAFECAT_Mod['column'][i]), str(np.round(SAFECAT_Mod['ra'][i], 2)), str(np.round(SAFECAT_Mod['dec'][i], 2)), str(np.round(SAFECAT_Mod['SNR'][i], 2)), str(SAFECAT_Mod['detector'][i]), str(SAFECAT_Mod['featureFlag'][i]), str(SAFECAT_Mod['calibration'][i]), str(SAFECAT_Mod['sampling'][i]), str(SAFECAT_Mod['hpdp'][i]), str(SAFECAT_Mod['bgs'][i]), str(SAFECAT_Mod['nccFlag'][i]), str(SAFECAT_Mod['species'][i]), str(SAFECAT_Mod['transition'][i]), str(SAFECAT_Mod['comment'][i]))) counter+=1 if counter > threshold: counter = 0 self.Table_progressbar.step(step_amount) self.Table_progressbar.update() # pack the table output window into the position 2,0 on the overall grid self.Table_Loading_window.grid_forget() self.table_output_window.grid(row=1, column=0, columnspan=2, padx=10) def List_URLs(self): ''' Returns two lists containg the urls to the currently selected obsids post cards as well as their catalogue entries. Returns ------- PostCard_url_list : List or strings A list containing the urls of every postcard associated with this obsid spectral_feature_catalogue_url_list : List of strings A list containing the urls of every catalogue associated with this obsid ''' archive = 'http://archives.esac.esa.int/hsa/legacy/HPDP/SPIRE/SPIRE-S/spectral_feature_catalogue' PostCard_url_list = [] PostCard_url_list.clear() spectral_feature_catalogue_url_list = [] spectral_feature_catalogue_url_list.clear() # Add every possible url that could contain info about this obsid and we will check to see if they actually exists later spectral_feature_catalogue_url_list.append(f'{archive}/HRextProducts/featureCatalogues/{int(current_selected_obsid)}_featuresFound_pnt.fits') spectral_feature_catalogue_url_list.append(f'{archive}/HRmapping/featureCatalogues/{int(current_selected_obsid)}_featuresFound.fits') spectral_feature_catalogue_url_list.append(f'{archive}/HRextProducts/featureCatalogues/{int(current_selected_obsid)}_featuresFound_ext.fits') spectral_feature_catalogue_url_list.append(f'{archive}/HRextProducts-offAxis/featureCatalogues/{int(current_selected_obsid)}_featuresFound_offAx.fits') PostCard_url_list.append(f'http://archives.esac.esa.int/hsa/legacy/HPDP/SPIRE/SPIRE-S/spectral_feature_catalogue/.images/postcards_png/{int(current_selected_obsid)}_postcard_pnt.png') PostCard_url_list.append(f'{archive}/HRmapping/postcards/{int(current_selected_obsid)}_postcard_comb_2x3.png') PostCard_url_list.append(f'http://archives.esac.esa.int/hsa/legacy/HPDP/SPIRE/SPIRE-S/spectral_feature_catalogue/.images/postcards_png/{int(current_selected_obsid)}_postcard_ext.png') PostCard_url_list.append(f'{archive}/HRextProducts-offAxis/postcards/{int(current_selected_obsid)}_postcard_offAx.png') # Now we can check to see if any of these urls actually exist counter = 0 for i, post_file in enumerate(PostCard_url_list): r = http.request('HEAD', post_file) if (r.status != 200): PostCard_url_list[i] = 'NONE' counter +=1 # Remove any list entries that contain the string 'NONE' for _ in range(counter): PostCard_url_list.remove('NONE') counter = 0 for i, cat_file in enumerate(spectral_feature_catalogue_url_list): r = http.request('HEAD', cat_file) if (r.status != 200): spectral_feature_catalogue_url_list[i] = 'NONE' counter +=1 # Remove any list entries that contain the string 'NONE' for _ in range(counter): spectral_feature_catalogue_url_list.remove('NONE') return PostCard_url_list, spectral_feature_catalogue_url_list def highlight_obsid_list(self, *args): ''' Searches each entry in the obsid Listbox and highlights it if the users search is contained within the listbox entry. Also counts how many entires are highlighted and displays the number to the user. Parameters ---------- *args : any Used to handle any events that tkinter may pass into the function when this method is called. None of these parameters are needed. Returns ------- None. ''' search = self.search_var.get() if search == '': self.Obsid_List.selection_clear(0, 'end') self.num_of_searchs_found.forget() else: counter = 0 for i, entry in enumerate(self.Output_Entry_List): if search in entry: self.Obsid_List.selection_set(i) counter += 1 else: self.Obsid_List.selection_clear(i) self.num_of_searchs_found['text'] = f'{counter} found!' self.num_of_searchs_found.pack(side='right') def display_input_error(self, text): ''' Takes a given string and displays it to the user on the output screen of the home page. Mostly used internally to display any invalid entry errors to the user. Parameters ---------- text : String A string containing the text the method will display to the user. Returns ------- None. ''' # This function may be called at the beginning of the run function if a user inputs an incorrect value self.out1["text"] = text self.out2["text"] = '' self.out3["text"] = '' self.obsid_info_window.forget() self.All_Info_table_button.forget() self.container.forget() self.table_output_window.grid_forget() def _Go_to_HSA_Website(self): ''' Opens the HSA website in a webbrowser and directs the user to the currently selected obsids entry. Returns ------- None. ''' url = f'https://archives.esac.esa.int/hsa/whsa/?ACTION=OBSERVATION&ID={current_selected_obsid}' webbrowser.open_new(url) class Table_Page(tk.Frame): ''' Adds all of the widgets neccessary to create a large table. This class adds all of the columns and headers so the 'Full_Obsid_info_table' and 'Single_Obsid_info_table' methods of the Home_Page class can then simply add the data to this preexisting table. Attributes --------------- None Methods --------------- None ''' def __init__(self, parent_frame): ''' Creates a tkinter Treeview widget that acts as the table and binds vertical and horizontal scrollbars to it. Parameters ---------- parent_frame : tk.Frame The tkinter frame that this class will be put inside. Returns ------- None. ''' tk.Frame.__init__(self, parent_frame) tk.Frame.columnconfigure(self, index=0, weight=1) tk.Frame.columnconfigure(self, index=0, weight=1) global Table_ # Create the Frame that will display all of the table data Table_Window = tk.Frame(master=self, relief=tk.SUNKEN, borderwidth=10) Table_Window.grid(row=0, column=0, sticky='nsew') table_X_scrollbar = tk.Scrollbar(master=Table_Window, orient=tk.HORIZONTAL) table_X_scrollbar.pack(side=tk.BOTTOM, fill='x', expand=True) table_Y_scrollbar = tk.Scrollbar(master=Table_Window) table_Y_scrollbar.pack(side=tk.RIGHT, fill='y', expand=True) Table_ = ttk.Treeview(master=Table_Window, height=41, yscrollcommand=table_Y_scrollbar.set, xscrollcommand=table_X_scrollbar.set) Table_.pack(side=tk.LEFT, fill='both', expand=True) # Set up column widths: Table_["columns"]=("one","two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty", "twentyone") Table_.column("#0", width=110, minwidth=30, stretch=tk.NO) Table_.column("one", width=50, minwidth=30, stretch=tk.NO) Table_.column("two",width=110, minwidth=30, stretch=tk.NO) Table_.column("three", width=70, minwidth=30, stretch=tk.NO) Table_.column("four", width=90, minwidth=30, stretch=tk.NO) Table_.column("five", width=110, minwidth=30, stretch=tk.NO) Table_.column("six",width=75, minwidth=30, stretch=tk.NO) Table_.column("seven", width=45, minwidth=30, stretch=tk.NO) Table_.column("eight", width=50, minwidth=30, stretch=tk.NO) Table_.column("nine", width=65, minwidth=30, stretch=tk.NO) Table_.column("ten", width=65, minwidth=30, stretch=tk.NO) Table_.column("eleven", width=65, minwidth=30, stretch=tk.NO) Table_.column("twelve", width=75, minwidth=30, stretch=tk.NO) Table_.column("thirteen", width=70, minwidth=30, stretch=tk.NO) Table_.column("fourteen", width=110, minwidth=30, stretch=tk.NO) Table_.column("fifteen", width=110, minwidth=30, stretch=tk.NO) Table_.column("sixteen",width=40, minwidth=30, stretch=tk.NO) Table_.column("seventeen", width=40, minwidth=30, stretch=tk.NO) Table_.column("eighteen", width=40, minwidth=30, stretch=tk.NO) Table_.column("nineteen", width=100, minwidth=30, stretch=tk.NO) Table_.column("twenty", width=120, minwidth=30, stretch=tk.NO) Table_.column("twentyone", width=50, minwidth=30, stretch=tk.NO) # Create the column titles Table_.heading("#0",text="Obsid",anchor=tk.CENTER) Table_.heading("one", text="opDay",anchor=tk.CENTER) Table_.heading("two", text="Frequency (GHz)",anchor=tk.CENTER) Table_.heading("three",text="VelocityCorrFreq",anchor=tk.CENTER) Table_.heading("four", text="Template Freq",anchor=tk.CENTER) Table_.heading("five", text="Velocity (Km/s)",anchor=tk.CENTER) Table_.heading("six", text="Velocity Flag",anchor=tk.CENTER) Table_.heading("seven", text="row",anchor=tk.CENTER) Table_.heading("eight",text="column",anchor=tk.CENTER) Table_.heading("nine", text="ra",anchor=tk.CENTER) Table_.heading("ten", text="dec",anchor=tk.CENTER) Table_.heading("eleven", text="SNR",anchor=tk.CENTER) Table_.heading("twelve", text="Detector",anchor=tk.CENTER) Table_.heading("thirteen", text="Feature Flag",anchor=tk.CENTER) Table_.heading("fourteen",text="calibration",anchor=tk.CENTER) Table_.heading("fifteen", text="sampling",anchor=tk.CENTER) Table_.heading("sixteen", text="hpdp",anchor=tk.CENTER) Table_.heading("seventeen", text="bgs",anchor=tk.CENTER) Table_.heading("eighteen",text="nccFlag",anchor=tk.CENTER) Table_.heading("nineteen", text="species",anchor=tk.CENTER) Table_.heading("twenty", text="transition",anchor=tk.CENTER) Table_.heading("twentyone", text="comment",anchor=tk.CENTER) table_Y_scrollbar.config(command=Table_.yview) table_X_scrollbar.config(command=Table_.xview) class Post_Card_Page(tk.Frame): ''' Displays all of the postcards for a given obsid and adds navigation widgets so the user can switch between them. Attributes --------------- title : tk.Label a label to display information to the user Page_Container : tk.Frame a container to hold the postcards and the navigation buttons PostCard_Frame : tk.Frame Creates a blue border around the postcards forward_button : tk.Button A button for the user to look at the next postcard back_button : tk.Button A button that allows the user to look at the previous postcard figure_num : tk.Label A label to display the postcard number the user is viewing (ex. viewing postcard 1 of 3) Go_To_URL_Button : tk.Button A button that allows the user to view the current postcard in a webbrowser using its url url_list : list A list containing all of the urls for the postcards pertaining to the currently selected obsid panels : list of tk.Labels Contains a list with the same length as url_list which will contain labels with the postcards in them. current_postCard_index : int A integer number representing the current postcard the user is viewing. Methods --------------- Display_Post_Cards : Displays all of the postcards from the urls contained in the variable url_list. _View_Last_PostCard : Uses the current_postCard_index to help display the previous postcard the user viewed. _View_Next_PostCard : Uses the current_postCard_index to help display the next postcard the user can view. _Perform_index_check : Uses the current_postCard_index to determine which buttons to enable and disable. _Go_to_URL : Opens the postcards url in a new webbrowser. _Display_Error : Removes any widgets on the page and displays an error message to the user. ''' def __init__(self, parent_frame): ''' Creates the navigation widgets, the frame that the postcards will be displayed in, and the page title. Parameters ---------- parent_frame : tk.Frame The frame that this class will be contained within. Returns ------- None. ''' tk.Frame.__init__(self, parent_frame) for i in range(2): tk.Frame.columnconfigure(self, index=i, weight=1) # Create the title for the post card page self.title = tk.Label(master=self, text ='Select a List Entry From the Home Page and click the Post Card Button!', font='Verdana 20 bold') self.title.pack() # Create a window for displaying the postcard frame and navigation buttons self.Page_Container = tk.Frame(master=self) self.Page_Container.columnconfigure(index=0, weight=1) self.Page_Container.columnconfigure(index=1, weight=1) self.Page_Container.rowconfigure(index=0, weight=1) self.Page_Container.rowconfigure(index=1, weight=1) # add all of the widgets that will be in the Page_Container button_container1 = tk.Frame(master=self.Page_Container) button_container1.grid(row=1, column=0, sticky='sw') button_container2 = tk.Frame(master=self.Page_Container) button_container2.grid(row=1, column=1, sticky='se') self.forward_button = ttk.Button(master=button_container2, text='Next', state='disabled', command=self._View_Next_PostCard) self.forward_button.pack(side=tk.RIGHT, fill='both', expand=True) self.back_button = ttk.Button(master=button_container2, text='Last', state='disabled', command=self._View_Last_PostCard) self.back_button.pack(side=tk.RIGHT, fill='both', expand=True) self.figure_num = tk.Label(master=button_container2, text = ' ') self.figure_num.pack(side=tk.RIGHT, fill='both', expand=True) self.Go_To_URL_Button = tk.Button(master=button_container1, text='Go To URL', command=self._Go_to_URL) self.Go_To_URL_Button.pack(side=tk.RIGHT, fill='both', expand=True, padx=3) self.Save_Button = tk.Button(master=button_container1, text='Save Post Card', command=self._Save) self.Save_Button.pack(side=tk.RIGHT, fill='both', expand=True, padx=3) def Display_Post_Cards(self): ''' Displays the postcards that the 'Home_Page_instance.List_URLs' method produces. Returns ------- None. ''' global current_selected_obsid # Set up screen to show the user that the postcard data is being collected and then move user to postcard page self.Page_Container.forget() self.title['text'] = 'Fetching Post Card Data...' self.title['fg'] = 'Black' self.title['font'] = 'Verdana 20 bold' menuTabs.select(1) menuTabs.update() self.url_list, _ = Home_Page_instance.List_URLs() # This creates the same amount of frames and labels as there are post cards for the particular obsid self.panels = ["panel_%d" % x for x in range(len(self.url_list))] self.postCard_Frames = ["postCard_Frames_%d" % x for x in range(len(self.url_list))] # This loop goes through the list of urls created by the method "Post_Card_URLs" in the "Home_Page" class # and fetches the image from the url to then places in one of the frames located in the 'panels' list for i, url in enumerate(self.url_list): try: with urllib.request.urlopen(url) as u: raw_data = u.read() except ConnectionError: self._Display_Error() return img = Image.open(BytesIO(raw_data)) # Resize the postcards if 'postcard_offAx' in url: img = img.resize((1250,580), Image.ANTIALIAS) else: img = img.resize((1000,630), Image.ANTIALIAS) image = ImageTk.PhotoImage(img) # Create an individual label and frame for each postcard self.postCard_Frames[i] = tk.Frame(master=self.Page_Container, bg=GUI_Blue, relief=tk.GROOVE, borderwidth=20) self.panels[i] = tk.Label(master=self.postCard_Frames[i], image=image) self.panels[i].image = image self.panels[i].grid(row=0, column=0) # reset the counter and raise the first postcard to the top for inital viewing self.current_postCard_index = 0 self.postCard_Frames[self.current_postCard_index].grid(row=0, column=0, columnspan=2) # Change the title self.title['fg'] = 'Black' self.title['font'] = 'Verdana 20 bold' if len(self.url_list) == 1: self.title['text'] = f'There is 1 post card associated with the observation ID: {current_selected_obsid}' else: self.title['text'] = f'There are {len(self.url_list)} post cards associated with the observation ID: {current_selected_obsid}' # Update the figure number of post cards displayed on the bottom right self.figure_num['text'] = f'Showing Post Card 1 of {len(self.panels)}' # check to see if we need to use the next button if len(self.panels) > 1: self.forward_button['state'] = 'normal' else: self.forward_button['state'] = 'disabled' # Pack the page container self.Page_Container.pack(fill='both', expand=True) def _View_Last_PostCard(self): ''' Decreases the 'current_postCard_index', changes the 'figure_num' label, and displayes the previous postcard. Returns ------- None. ''' self.postCard_Frames[self.current_postCard_index].grid_forget() self.current_postCard_index -= 1 self.postCard_Frames[self.current_postCard_index].grid(row=0, column=0, columnspan=2) self._Perform_index_check() self.figure_num['text'] = f'Showing Post Card {self.current_postCard_index + 1} of {len(self.panels)}' def _View_Next_PostCard(self): ''' Increases the 'current_postCard_index', changes the 'figure_num' label, and displayes the Next postcard in the postcard list. Returns ------- None. ''' self.postCard_Frames[self.current_postCard_index].grid_forget() self.current_postCard_index += 1 self.postCard_Frames[self.current_postCard_index].grid(row=0, column=0, columnspan=2) self._Perform_index_check() self.figure_num['text'] = f'Showing Post Card {self.current_postCard_index + 1} of {len(self.panels)}' def _Perform_index_check(self): ''' Uses the 'current_postCard_index' to determine which navigation buttons should be enabled or disabled. Returns ------- None. ''' if self.current_postCard_index == 0: self.back_button['state'] = 'disabled' self.forward_button['state'] = 'normal' elif self.current_postCard_index == (len(self.panels)-1): self.forward_button['state'] = 'disabled' self.back_button['state'] = 'normal' else: self.forward_button['state'] = 'normal' self.back_button['state'] = 'normal' def _Go_to_URL(self): webbrowser.open_new(self.url_list[self.current_postCard_index]) def _Save(self): savePathName = fd.asksaveasfilename(defaultextension='.png', initialdir=save_dir, title='Save Post Card') urllib.request.urlretrieve(self.url_list[self.current_postCard_index], savePathName) def _Display_Error(self): ''' Removes all of the widgets from the page and displays an error to the user. Returns ------- None. ''' self.Page_Container.forget() self.title['text'] ='Cannot Connect to Post Card URL! \n Please Check Your Internet Connection!' self.title['fg'] = GUI_Red self.title['font'] = 'Verdana 30 bold' class Spectral_Feature_Catalogue_Page(tk.Frame): ''' Pulls the Fits files for a specific obsid and displays the main tables, header files, and the postcards. Attributes --------------- FitsFileLayer1 - FitsFileLayer4 : tk.Frames These are four seperate frames that cover almost the entire page and are packed on top of each other. These are the layers that the fits file data will be placed in. MessageLayer : tk.Frame A Frame that is stacked on the FitsFileLayer variables (described above) and can be used to display information to the user while hiding the fits file data from the user. Layer_List : list A list containing all four frame variables FitsFileLayer1 - FitsFileLayer4 Nav_Buttons : tk.Frame A frame that contains the navigation buttons. Forward_Button : tk.Button A navigation button that allows the user to view the data for the next fits file in the list. Previous_Button : tk.Button A navigation button that allows the user to view the data for the previous fits file in the list. url_label : tk.Label Displays the url that the fits file was pulled from. spectral_feature_catalogue_url_list : list A list containing all of the urls of the fits files for the currently selected obsid Error : bool Boolean that represents whether something went wrong in the program. Table_Title : tk.Label Informs the user that the table contains the data from the main table in the fits file Table_Frame : tk.Frame Frame containing the tkinter treeview table Header_Title : tk.Label Informs the user that the following text is the header data from the fits file TextBox : tk.Text Textbox where all of the header data will be written to. PostCard_title : tk.Label Informative title about the obsids postcards PostCard_Frame : tk.Frame Frame that contains the postcards. Current_Layer_index : int An integer that represents which fits file in the list is currently being displayed Methods --------------- Display_Fits_File_Data : Fetches the fits file data from the 'spectral_feature_catalogue_url_list' and presents the data using the other methods. _Create_Table(Fits_File, parent) : takes the given fits file parameter and displays its table data in the fits file layer specified by the parent parameter. _Create_Header_Text(Fits_File, parent) : takes the given fits file parameter and displays its header data in the fits file layer specified by the parent parameter. _Create_Post_Card_Image(Post_Card_URL, parent) : Grabs the postcard image from the url and displays it in the fits file layer specified by the parent parameter. _Display_Message(message, text_color, parent, error, font='Verdana 30 bold') : Displays the message parameter in the fits file layer specified by the parent parameter. You can also change the message color using text_color and can activate the Error variable using the error parameter. _Forward_Button_Command : Displays the next set of data from the following fits file in the list. _Previous_Button_Command : Displays the previous set of fits file data. _Perform_index_check : Uses the 'Current_Layer_index' variable to determine which nav buttons should be disabled. ''' def __init__(self, parent_frame): ''' Creates navigation buttons and five different layers where the different fits file data can be placed. Parameters ---------- parent_frame : tk.Frame The frame in which this class will be placed inside. Returns ------- None. ''' tk.Frame.__init__(self, parent_frame) for i in range(2): self.rowconfigure(index=i, weight=1) for i in range(1): self.columnconfigure(index=i, weight=1) # Create five different layers to display different info self.FitsFileLayer1 = tk.Frame(master=self) self.FitsFileLayer1.grid(row=0, column=0, sticky='nsew') for i in range(4): self.FitsFileLayer1.rowconfigure(index=i, weight=1) for i in range(2): self.FitsFileLayer1.columnconfigure(index=i, weight=1) self.FitsFileLayer2 = tk.Frame(master=self) self.FitsFileLayer2.grid(row=0, column=0, sticky='nsew') for i in range(4): self.FitsFileLayer2.rowconfigure(index=i, weight=1) for i in range(2): self.FitsFileLayer2.columnconfigure(index=i, weight=1) self.FitsFileLayer3 = tk.Frame(master=self) self.FitsFileLayer3.grid(row=0, column=0, sticky='nsew') for i in range(4): self.FitsFileLayer3.rowconfigure(index=i, weight=1) for i in range(2): self.FitsFileLayer3.columnconfigure(index=i, weight=1) self.FitsFileLayer4 = tk.Frame(master=self) self.FitsFileLayer4.grid(row=0, column=0, sticky='nsew') for i in range(4): self.FitsFileLayer4.rowconfigure(index=i, weight=1) for i in range(2): self.FitsFileLayer4.columnconfigure(index=i, weight=1) self.MessageLayer = tk.Frame(master=self) self.MessageLayer.grid(row=0, column=0, rowspan=2, sticky='nsew') self.Message_Label = tk.Label(master=self.MessageLayer, text='Select a List Entry From the Home Page and click the \n View Spectral Feature Catalogue Information Button!', font='Verdana 20 bold') self.Message_Label.pack(fill='both', expand=True) # Put the four Fits File Layers into a list self.Layer_List = [self.FitsFileLayer1, self.FitsFileLayer2, self.FitsFileLayer3, self.FitsFileLayer4] # Create the navigation buttons for moving between multiple fits file data self.Nav_Buttons = tk.Frame(master=self, bg='black') self.Forward_Button = tk.Button(master=self.Nav_Buttons, text='Next Fits File', highlightbackground='black', state='disabled', command=self._Forward_Button_Command) self.Forward_Button.pack(side=tk.RIGHT) self.Previous_Button = tk.Button(master=self.Nav_Buttons, text='Previous Fits File', state='disabled', highlightbackground='black', command=self._Previous_Button_Command) self.Previous_Button.pack(side=tk.RIGHT) self.Nav_Button_Text = tk.Label(master=self.Nav_Buttons, text ='', bg='black', fg='white') self.Nav_Button_Text.pack(side=tk.RIGHT) self.url_label = tk.Label(master=self.Nav_Buttons, text=' ', bg='black', fg='white', font='verdana 12') #nominally 9 self.url_label.pack(side=tk.LEFT) def Display_Fits_File_Data(self): ''' Uses the Home_Page_instance.List_URLs method to find the urls for the fits files and postcards. It then uses the other methods to display the postcards, header data, and table data to the user. Returns ------- None. ''' # Go to the spectral feature catalogue page and display reading in data message menuTabs.select(3) self._Display_Message('Reading in Fits File Data...', 'black', self.MessageLayer, error=False) # Get the list of urls PostCard_url_list , self.spectral_feature_catalogue_url_list = Home_Page_instance.List_URLs() # collect fits files from the urls and display the data in a header frame, table frame, and velocity picture for i in range(len(self.spectral_feature_catalogue_url_list)): try: Fits_File = fits.open(self.spectral_feature_catalogue_url_list[i]) self._Create_Header_Text(Fits_File, self.Layer_List[i]) self._Create_Post_Card_Image(PostCard_url_list[i], self.Layer_List[i]) self._Create_Table(Fits_File, self.Layer_List[i]) except: self._Display_Message('Error Finding Fits File!', GUI_Red, self.MessageLayer, error=True) # Raise the first Layer up if not self.Error: # Checks to see if an error message was tripped self.Current_Layer_index = 0 # Reset the current index self.Layer_List[self.Current_Layer_index].tkraise() # Add navigation buttons and change the text to reflect the number of fits files self.Nav_Buttons.grid(row=1, column=0, sticky='nsew') self.Nav_Buttons.tkraise() self.Nav_Button_Text['text'] = f'Showing Fits File 1 of {len(self.spectral_feature_catalogue_url_list)}' # Add the url for the fits file fileName = self.spectral_feature_catalogue_url_list[self.Current_Layer_index].split('/')[-1] urlLabelText = self.spectral_feature_catalogue_url_list[self.Current_Layer_index][:-len(fileName)] + \ '/\n' +fileName self.url_label['text'] = urlLabelText # Set up the buttons self.Previous_Button['state'] = 'disabled' if len(self.spectral_feature_catalogue_url_list) > 1: self.Forward_Button['state'] = 'active' else: self.Forward_Button['state'] = 'disabled' def _Create_Table(self, Fits_File, parent): ''' Displays the table data from the supplied Fits File to the page layer supplied by the parent parameter. Parameters ---------- Fits_File : opened fits file This method will read and display the table from this fits file. parent : tk.Frame This specifies which layer this specific fits file should be displayed in. Options are FitsFileLayer1, FitsFileLayer2, FitsFileLayer3, or FitsFileLayer4. Returns ------- None. ''' self.Table_Title = tk.Label(master=parent, text='Main Data Table From Fits File', bg=GUI_Red, font=Big_Font_Important) self.Table_Title.grid(row=2, column=0, columnspan=2, sticky='nsew') # Read in the first table from the fits file (it is the data table) Table1 = Table.read(Fits_File[1]) # Create the Frame that will display all of the table data self.Table_Frame = tk.Frame(master=parent, highlightbackground=GUI_Red, highlightthickness=10, highlightcolor=GUI_Red) self.Table_Frame.grid(row=3, column=0, columnspan=2, sticky='nsew') self.Table_Frame.grid_columnconfigure(0, weight=1) self.Table_Frame.grid_columnconfigure(1, weight=1) self.Table_Frame.grid_rowconfigure(0, weight=1) self.Table_Frame.grid_rowconfigure(1, weight=1) # Add the scrollbars and treeview widget to the Table_frmae table_X_scrollbar = tk.Scrollbar(master=self.Table_Frame, orient=tk.HORIZONTAL) table_X_scrollbar.grid(row=1, column=0, sticky='ew') table_Y_scrollbar = tk.Scrollbar(master=self.Table_Frame) table_Y_scrollbar.grid(row=0, column=1, rowspan=2, sticky='ns') Fits_Table = ttk.Treeview(master=self.Table_Frame, height=17, yscrollcommand=table_Y_scrollbar.set, xscrollcommand=table_X_scrollbar.set) Fits_Table.grid(row=0, column=0, sticky='nsew') table_Y_scrollbar.config(command=Fits_Table.yview) table_X_scrollbar.config(command=Fits_Table.xview) # set up the table according to the number expected columns if len(Table1.columns) > 6: # Set up column widths: Fits_Table["columns"]=("one","two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve") Fits_Table.column("#0", width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("one", width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("two",width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("three", width=80, minwidth=50, stretch=tk.NO) Fits_Table.column("four", width=80, minwidth=50, stretch=tk.NO) Fits_Table.column("five", width=80, minwidth=50, stretch=tk.NO) Fits_Table.column("six",width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("seven", width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("eight", width=80, minwidth=50, stretch=tk.NO) Fits_Table.column("nine", width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("ten", width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("eleven", width=80, minwidth=50, stretch=tk.NO) Fits_Table.column("twelve", width=80, minwidth=50, stretch=tk.NO) # Create the column titles Fits_Table.heading("#0",text="Frequency",anchor=tk.CENTER) Fits_Table.heading("one", text="FrequencyError",anchor=tk.CENTER) Fits_Table.heading("two", text="SNR",anchor=tk.CENTER) Fits_Table.heading("three",text="detector",anchor=tk.CENTER) Fits_Table.heading("four", text="Row",anchor=tk.CENTER) Fits_Table.heading("five", text="Column",anchor=tk.CENTER) Fits_Table.heading("six", text="ra",anchor=tk.CENTER) Fits_Table.heading("seven", text="dec",anchor=tk.CENTER) Fits_Table.heading("eight",text="FeatureFlag",anchor=tk.CENTER) Fits_Table.heading("nine", text="Velocity",anchor=tk.CENTER) Fits_Table.heading("ten", text="VelocityError",anchor=tk.CENTER) Fits_Table.heading("eleven", text="vFlag",anchor=tk.CENTER) Fits_Table.heading("twelve", text="nccFlag",anchor=tk.CENTER) # Delete prevoius table entries Fits_Table.delete(*Fits_Table.get_children()) # Create the table row by row for i in range(len(Table1)): Fits_Table.insert("", "end", f"{i}", text=f"{Table1.columns[0][i]}", values=( str(Table1.columns[1][i]), str(Table1.columns[2][i]), str(Table1.columns[3][i]), str(Table1.columns[4][i]), str(Table1.columns[5][i]), str(Table1.columns[6][i]), str(Table1.columns[7][i]), str(Table1.columns[8][i]), str(Table1.columns[9][i]), str(Table1.columns[10][i]), str(Table1.columns[11][i]), str(Table1.columns[12][i]))) else: # Set up column widths: Fits_Table["columns"]=("one","two", "three", "four", "five", "six") Fits_Table.column("#0", width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("one", width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("two",width=200, minwidth=50, stretch=tk.NO) Fits_Table.column("three", width=90, minwidth=50, stretch=tk.NO) Fits_Table.column("four", width=80, minwidth=50, stretch=tk.NO) Fits_Table.column("five", width=80, minwidth=50, stretch=tk.NO) Fits_Table.column("six", width=450, minwidth=50, stretch=tk.NO) # Create the column titles Fits_Table.heading("#0",text="Frequency",anchor=tk.CENTER) Fits_Table.heading("one", text="FrequencyError",anchor=tk.CENTER) Fits_Table.heading("two", text="SNR",anchor=tk.CENTER) Fits_Table.heading("three",text="Detector",anchor=tk.CENTER) Fits_Table.heading("four", text="FeatureFlag",anchor=tk.CENTER) Fits_Table.heading("five", text="nccFlag",anchor=tk.CENTER) Fits_Table.heading("six", text=" ",anchor=tk.CENTER) # Delete prevoius table entries Fits_Table.delete(*Fits_Table.get_children()) # Create the table row by row for i in range(len(Table1)): Fits_Table.insert("", "end", f"{i}", text=f"{Table1.columns[0][i]}", values=( str(Table1.columns[1][i]), str(Table1.columns[2][i]), str(Table1.columns[3][i]), str(Table1.columns[4][i]), str(Table1.columns[5][i]), str(''))) def _Create_Header_Text(self, Fits_File, parent): ''' Displays the header data from the supplied Fits File to the page layer specified by the parent parameter. Parameters ---------- Fits_File : opened fits file This method will read and display the header from this fits file. parent : tk.Frame This specifies which layer this specific fits file should be displayed in. Options are FitsFileLayer1, FitsFileLayer2, FitsFileLayer3, or FitsFileLayer4. Returns ------- None. ''' # Create the header title self.Header_Title = tk.Label(master=parent, text='Header', bg=GUI_Green, font=Big_Font_Important) self.Header_Title.grid(row=0, column=0, sticky='nsew') # create a container to put the textbox and a scrollbar in container = tk.Frame(master=parent, bg=GUI_Green, highlightbackground=GUI_Green, highlightthickness=10, highlightcolor=GUI_Green) container.grid(row=1, column=0, sticky='nsew') # create the textbox and scrollbar self.TextBox = tk.Text(master=container) self.TextBox.pack(side=tk.LEFT, fill='both', expand=True) Header_Scroll = tk.Scrollbar(master=container, orient='vertical', command=self.TextBox.yview) Header_Scroll.pack(side=tk.RIGHT, fill='y') self.TextBox['yscrollcommand'] = Header_Scroll.set # Add the text to the textbox self.TextBox.insert(tk.END, repr(Fits_File[1].header)) def _Create_Post_Card_Image(self, Post_Card_URL, parent): ''' Displays the header data from the supplied Fits File to the page layer specified by the parent parameter. Parameters ---------- Post_Card_URL : string The url of the postcard this method will fetch. parent : tk.Frame This specifies which layer postcard should be displayed in. Options are FitsFileLayer1, FitsFileLayer2, FitsFileLayer3, or FitsFileLayer4. Returns ------- None. ''' self.PostCard_title = tk.Label(master=parent, text='Post Card', bg=GUI_Blue, font=Big_Font_Important) self.PostCard_title.grid(row=0, column=1, sticky='nsew') # Create a frame to house the image self.PostCard_Frame = tk.Frame(master=parent, highlightbackground=GUI_Blue, highlightthickness=10, highlightcolor=GUI_Blue) self.PostCard_Frame.grid(row=1, column=1, sticky='nsew') # Display the postcard try: with urllib.request.urlopen(str(Post_Card_URL)) as u: raw_data = u.read() img = Image.open(BytesIO(raw_data)) # resize the image if 'postcard_offAx' in Post_Card_URL: img = img.resize((625,290), Image.ANTIALIAS) else: img = img.resize((500,315), Image.ANTIALIAS) image = ImageTk.PhotoImage(img) Post_Card_Label = tk.Label(master=self.PostCard_Frame, image=image) Post_Card_Label.image = image Post_Card_Label.pack() except: self._Display_Message(f'[Error] \n URL: {Post_Card_URL} \n Not Found! Please Check Your Internent Connection!', GUI_Red, self.MessageLayer, error=True, font='Verdana 14 bold') def _Display_Message(self, message, text_color, parent, error, font='Verdana 30 bold'): ''' Hides the data from the user and displays a message to the user. Parameters ---------- message : string The message you wish to display text_color : string The color of the text when it is displayed (ex. GUI_Red, GUI_Green, GUI_Blue) parent : tk.Frame The layer you wish to display the message in. Should almost always be the 'self.MessageLayer' variable. error : bool If you wish to stop running any of the methods, set to True. This means that an error occured and you do not wish to continue fetching data or displaying the data. font : string, optional Changes the font of the displayed message. The default is 'Verdana 30 bold'. Returns ------- None. ''' # Get rid of all of the displayed data try: self.Nav_Buttons.grid_forget() except: pass # Display Message self.Message_Label['text'] = message self.Message_Label['fg'] = text_color self.Message_Label['font'] = font if error: self.Error = True else: self.Error = False # Raise the message to the top of the screen parent.tkraise() self.update() def _Forward_Button_Command(self): ''' Raises the next fits file layer to display the next set of data. Returns ------- None. ''' # Increase the current layer index self.Current_Layer_index += 1 self._Perform_index_check() # Display new fits file url fileName = self.spectral_feature_catalogue_url_list[self.Current_Layer_index].split('/')[-1] urlLabelText = self.spectral_feature_catalogue_url_list[self.Current_Layer_index][:-len(fileName)] + \ '/\n' +fileName self.url_label['text'] = urlLabelText #self.url_label['text'] = self.spectral_feature_catalogue_url_list[self.Current_Layer_index] # Change the navigation buttons text self.Nav_Button_Text['text'] = f'Showing Fits File {self.Current_Layer_index + 1} of {len(self.spectral_feature_catalogue_url_list)}' # Rasie the new layer to the top self.Layer_List[self.Current_Layer_index].tkraise() def _Previous_Button_Command(self): ''' Rasies the previous fits file layer to display the previous set of data. Returns ------- None. ''' # Increase the current layer indes self.Current_Layer_index -= 1 self._Perform_index_check() # Display new fits file url fileName = self.spectral_feature_catalogue_url_list[self.Current_Layer_index].split('/')[-1] urlLabelText = self.spectral_feature_catalogue_url_list[self.Current_Layer_index][:-len(fileName)] + \ '/\n' +fileName self.url_label['text'] = urlLabelText #self.url_label['text'] = self.spectral_feature_catalogue_url_list[self.Current_Layer_index] # Change the navigation buttons text self.Nav_Button_Text['text'] = f'Showing Fits File {self.Current_Layer_index + 1} of {len(self.spectral_feature_catalogue_url_list)}' # Rasie the new layer to the top self.Layer_List[self.Current_Layer_index].tkraise() def _Perform_index_check(self): ''' Disables or enables the navigation buttons depending on the 'Current_Layer_index' variable. Returns ------- None. ''' if self.Current_Layer_index == 0: self.Previous_Button['state'] = 'disabled' self.Forward_Button['state'] = 'normal' elif self.Current_Layer_index == (len(self.spectral_feature_catalogue_url_list)-1): self.Forward_Button['state'] = 'disabled' self.Previous_Button['state'] = 'normal' else: self.Forward_Button['state'] = 'normal' self.Previous_Button['state'] = 'normal' class Photometer_Image_Page(tk.Frame): ''' Uses functions from the custom made io library to create an image in matplotlib using an obsids photometer data. Attributes --------------- Message_Layer : tk.Frame A frame that only contains label widgets used to display quick messages to the user. Placed on top of the Photometer_Layer. Photometer_Layer : tk.Frame A frame that holds the photometer image and a save button. Image_Resolution_container : tk.Frame A frame that contains the widgets the user can interct with to specify the resolution they wish to save the image as. Photometer_Save_Button : tk.Button A button that will save the photometer image to a user selected location at a user specified resolution. Message_Label : tk.Label The label used to display messages on the Message_Layer Error : bool Variable used to stop methods from running or finishing if an error occurs somewhere else in the class. fig : plt.figure A figure from matplotlib used to display the photometer image. save_dir : string A string containing the path to a directory where all the downloaded data will be saved. This directory will be created if it is not present and allows users to save time when viewing photometer images a second time. Methods --------------- Produce_Photometer_Image : Downloads FF and HSA data in order to determine the photometer ID before downloading the photometer data. The photometer image is then created using this data and functions from the io library. _Display_Message(msg, color='black', error=False) : Displays a message to the user on the Message_Layer. Ask_For_Resolution : Gets rid of the save button and adds the 'Image_Resolution_container' to the page. SaveImage : Ask where the image should be saved and then saves the image using the specified dpi resolution. ''' def __init__(self, parent_frame): ''' Adds all of the widgets needed to create this page of the application including the two seperate layers and the save buttons. Parameters ---------- parent_frame : tk.Frame The frame where this class will be placed into. Returns ------- None. ''' tk.Frame.__init__(self, parent_frame) self.rowconfigure(index=0, weight=1) self.columnconfigure(index=0, weight=1) # Create a Message/Update layer and a layer for displaying the actual image self.Message_Layer = tk.Frame(master=self) self.Message_Layer.grid(row=0, column=0, sticky='nsew') self.Photometer_Layer = tk.Frame(master=self) self.Photometer_Layer.grid(row=0, column=0, sticky='nsew') self.Photometer_Layer.rowconfigure(index=0, weight=1) self.Photometer_Layer.columnconfigure(index=0, weight=1) # Place the save image resolution container into the GUI self.Image_Resolution_container = tk.Frame(master=self.Photometer_Layer) Resolution_title = tk.Label(master=self.Image_Resolution_container, text='Choose an image resolution:', font='Verdana 14 bold') Resolution_title.grid(row=0, column=0, columnspan=3, sticky='nsew') resolution_enter_box = tk.Entry(master=self.Image_Resolution_container, width=4) resolution_enter_box.grid(row=1, column=0, sticky='nsew', padx=(10, 0), pady=(0, 10)) dpi_label = tk.Label(master=self.Image_Resolution_container, text='dpi') dpi_label.grid(row=1, column=1, sticky='w', pady=(0, 10)) Resolution_enter_button = tk.Button(master=self.Image_Resolution_container, text='Save', command= lambda: self.SaveImage(dpi=resolution_enter_box.get())) Resolution_enter_button.grid(row=1, column=2, sticky='nsew', padx=10, pady=(0, 10)) # place the save button into the GUI self.Photometer_Save_Button = tk.Button(master=self.Photometer_Layer, text='Save Image', command=self.Ask_For_Resolution) self.Photometer_Save_Button.grid(row=1, column=0, sticky='sw', pady=5, padx=5) # Add a message label self.Message_Label = tk.Label(master=self.Message_Layer, text='Select a List Entry From the Home Page and click the \n View Photometer Image Button!', font='Verdana 22 bold') self.Message_Label.pack(fill='both', expand=True) self.Message_Layer.tkraise() def Produce_Photometer_Image(self): ''' uses the 'get_observation' function from the io library to get FF and HSA data for the currently selected obsid. It then determines the photometer ID before using the same function to fetch the photometer data. The sky_image function is then called from the figtools library to create the image. Returns ------- None. ''' # Go to the spectral feature catalogue page and display reading in data message menuTabs.select(4) self.Message_Layer.tkraise() self.Error = False # Get FF products and corresponding HSA spectrum self._Display_Message('Reading in Feature Finder Products and Corresponding HSA Spectrum... \n If you have previously pulled the data from the HSA we will use it, otherwise it may take a few minutes!') try: result = spio.get_observation(obsid=int(current_selected_obsid), cal='ext', useHsa=True, off_axis=True, save=True, save_dir=save_dir) except ConnectionError: self._Display_Message('[Error 1] Cannot Locate Products! Please Check Your Internet Connection!', color=GUI_Red, error=True) # If the get_observation function from the spio library successfully returned data we can continue, otherwise, halt the code and display an error message if not self.Error: # Determine associated Photometer Map Obsid self._Display_Message('Determining Associated Photometer Map Obsid...') try: meta = result['HR_spectrum_ext'][0].header Mapping_Observation = False except KeyError: meta = result['HR_SSW_cube_convol'][0].header Mapping_Observation = True self._Display_Message('Reading in Photometer Data from the HSA... \n It may take up to 9 minutes!') i = -1 while True: i +=1 phot_obsid = f'photObsid00{i}' meta_map = spio._map_meta(phot_obsid, meta) try: photObsid = meta[meta_map[phot_obsid]] phot_result = spio.get_observation(photObsid, useHsa=True, photo_band='PSW', save=True, save_dir=save_dir) if 'psrcPSW' in phot_result.keys(): break except: self._Display_Message('[Error 2] This observation may not have any photometer data!', color=GUI_Red, error=True) break # If the get_observation function from the spio library successfully returned data we can continue, otherwise, halt the code and display an error message if not self.Error: # Generate sky image self._Display_Message('Generating Image...') ax_kwargs = {'title': meta['Object']} self.fig = figure.Figure(figsize=(25, 25)) if Mapping_Observation: self.fig, ax, img, wcs = sky_image(phot_result['psrcPSW'], foot_data=[result['HR_SLW_cube_convol'], result['HR_SSW_cube_convol']], fig=self.fig, ax_kwargs=ax_kwargs) else: spec_hdu = result['HR_spectrum_ext'] self.fig, ax, img, wcs = sky_image(phot_result['psrcPSW'], foot_data=spec_hdu, fig=self.fig, ax_kwargs=ax_kwargs) # Place the plot in a tkinter canvas with a blue frame wrapped around it Image_Frame = tk.Frame(master=self.Photometer_Layer, bg=GUI_Blue, relief=tk.GROOVE, borderwidth=40) Image_Frame.grid(row=0, column=0,sticky='nsew', padx=250, pady=50) canvas = FigureCanvasTkAgg(self.fig, master=Image_Frame) # A tk.DrawingArea. canvas.draw() canvas.get_tk_widget().pack(fill='both', expand=True) self.Photometer_Layer.tkraise() self.Image_Resolution_container.grid_forget() self.Photometer_Save_Button.grid(row=1, column=0, sticky='sw') def _Display_Message(self, msg, color='black', error=False): ''' Displays a message to the user. Parameters ---------- msg : string the message you would like to display to the user color : string, optional Specify the color of the displayed text. The default is 'black'. error : Bool, optional Can set the variable 'self.Error' which can stop certain actions from performing if set to True. The default is False. Returns ------- None. ''' self.Message_Label['text'] = msg self.Message_Label['fg'] = color self.Message_Layer.update() if error: self.Error = True def Ask_For_Resolution(self): ''' Removes the Save button from the page and adds the Image_Resolution_container. Returns ------- None. ''' self.Image_Resolution_container.grid(row=1, column=0, sticky='sw') self.Photometer_Save_Button.grid_forget() def SaveImage(self, dpi): ''' Asks the user where they would like to save the image and then saves it using the specified dpi resolution. Parameters ---------- dpi : TYPE DESCRIPTION. Returns ------- None. ''' savePathName = fd.asksaveasfilename(defaultextension='.pdf', initialdir=save_dir, title='Save Image') if not isFloat(dpi): dpi = 300 self.fig.savefig(savePathName, dpi=int(dpi)) self.Image_Resolution_container.grid_forget() self.Photometer_Save_Button.grid(row=1, column=0, sticky='sw') class Help_Page(tk.Frame): ''' Creates a page on the application that contains text explaining the basics. ''' def __init__(self, parent_frame): ''' Creates the widgets that will allow the text conatined within the 'GUI_Help_file.txt' to be displayed. Parameters ---------- parent_frame : tk.Frame The frame where this class will be placed inside Returns ------- None. ''' tk.Frame.__init__(self, parent_frame) self.rowconfigure(index=0, weight=1) self.columnconfigure(index=0, weight=1) # create a container to put the textbox and a scrollbar in container = tk.Frame(master=self) container.grid(row=0, column=0, sticky='nsew') # create the textbox and scrollbar Help_TextBox = tk.Text(master=container, wrap = WORD, spacing1 = 10, spacing2 = 10, font = ('Verdana', 12)) Help_TextBox.pack(side=tk.LEFT, fill='both', expand=True) Help_Scroll = tk.Scrollbar(master=container, orient='vertical', command=Help_TextBox.yview) Help_Scroll.pack(side=tk.RIGHT, fill='y') Help_TextBox['yscrollcommand'] = Help_Scroll.set # Add the text to the textbox with open(path.join(path.dirname(__file__), 'GUI_Help_file.txt'), 'rU') as f: Help_TextBox.insert(tk.END, f.read()) Help_TextBox.config(state=tk.DISABLED) class Ack_Page(tk.Frame): ''' Creates a page on the application that contains the acknowledgement and citing guide. ''' def __init__(self, parent_frame): ''' Creates the widgets that will allow the text conatined within the 'FF_acknowledgements.txt' to be displayed. Parameters ---------- parent_frame : tk.Frame The frame where this class will be placed inside Returns ------- None. ''' tk.Frame.__init__(self, parent_frame) self.rowconfigure(index=0, weight=1) self.columnconfigure(index=0, weight=1) # create a container to put the textbox and a scrollbar in container = tk.Frame(master=self) container.grid(row=0, column=0, sticky='nsew') # create the textbox and scrollbar Acknowledge_textbox = tk.Text(master=container, wrap = WORD, spacing1 = 10, spacing2 = 10) Acknowledge_textbox.pack(side=tk.TOP, fill='both', expand=True) Acknowledge_textbox.tag_configure('Arial', font = ('Arial', 12)) # Add the text to the textbox with open(path.join(path.dirname(__file__), 'FF_acknowledgements.txt'), 'rU') as f: Acknowledge_textbox.insert(tk.END, f.read(), 'Arial') Acknowledge_textbox.config(state=tk.DISABLED) SAFECAT_Application = Application() SAFECAT_Application.mainloop()