###################################
# Created on Jul 29, 2015
#
# @author: Grant Mercer
#
###################################
from Tkconstants import LEFT, END, RIGHT
import tkMessageBox
from Tkinter import Toplevel, Entry, Button, BOTH, Frame, \
Label, TOP, X, OptionMenu, StringVar
import constants
from tools.tools import center, Observer
from log.log import logger
import re
[docs]class Query(Observer):
"""
Observer object that holds a *ranges* dictionary which can be used
to query the database once updated. Notifies it's parent upon ranges
being changed
"""
def __init__(self):
Observer.__init__(self)
self._ranges = {}
@property
def ranges(self):
return self._ranges
@ranges.setter
def ranges(self, n_ranges):
self._ranges = n_ranges
self.notify()
def notify(self, modifier=None):
for observer in self._observers:
if modifier != observer:
observer.receive_advanced_search(self)
[docs]class AdvancedSearchDialog(Toplevel):
"""
A dialog for advanced searching, notifying `ImportDialog` when search parameters have
been chosen and entered. Uses the observer design pattern to notify ``ImportDialog``
when the ranges have been changed from an invalid state to valid.
:param parent: The class to attach the observer to
:param root: The base widget for ``Toplevel``
"""
# This dialog should be a singleton, so the caller will ensure
# no other windows are open by checking this variable
singleton = False
def __init__(self, parent, root):
AdvancedSearchDialog.singleton = True # pseudo singleton now active
Toplevel.__init__(self, root)
self.title = 'Advanced search'
self.transient(root)
self.shared_data = Query()
self.shared_data.attach(parent)
self.protocol('WM_DELETE_WINDOW', self.free)
center(self, (constants.IMADVWIDTH, constants.IMADVHEIGHT))
window_frame = Frame(self)
window_frame.pack(fill=BOTH, expand=True)
top_window_frame = Frame(window_frame)
top_window_frame.pack(side=TOP, fill=X, expand=False)
Label(top_window_frame, text='Filter by: ').pack(side=LEFT, padx=15, pady=5)
Label(top_window_frame, text='Leave fields untouched that you do not wish to search by',
font=('Helvetica', 8)).pack(side=RIGHT, padx=15, pady=5)
bottom_window_frame = Frame(window_frame)
bottom_window_frame.pack(side=TOP, fill=BOTH, expand=False, padx=15)
bottom_window_frame.config(highlightthickness=1)
bottom_window_frame.config(highlightbackground='grey')
Label(bottom_window_frame, text='Plot ').grid(row=0, column=0, padx=5, pady=5, sticky='w')
Label(bottom_window_frame, text='Date(YYYY-MM-DD) ').grid(row=1, column=0, padx=5, pady=5, sticky='w')
Label(bottom_window_frame, text='Time Range ').grid(row=2, column=0, padx=5, pady=5, sticky='w')
Label(bottom_window_frame, text='Latitude Range ').grid(row=3, column=0, padx=5, pady=5, sticky='w')
Label(bottom_window_frame, text='Altitude Range ').grid(row=4, column=0, padx=5, pady=5, sticky='w')
Label(bottom_window_frame, text='File ').grid(row=5, column=0, padx=5, pady=5, sticky='w')
self.plots = StringVar()
self.am_pm = StringVar()
self.plot_entry = OptionMenu(bottom_window_frame, self.plots, 'backscattered', 'depolarized', 'vfm')
self.plot_entry.grid(row=0, column=1, padx=10, pady=5, sticky='w', columnspan=3)
self.date_entry = Entry(bottom_window_frame, width=25)
self.date_entry.grid(row=1, column=1, padx=5, pady=5, sticky='w', columnspan=4)
self.date_entry.insert(END, '0000-00-00')
self.b_time_entry = Entry(bottom_window_frame, width=10)
self.b_time_entry.grid(row=2, column=1, padx=5, pady=5, sticky='w')
self.b_time_entry.insert(END, '00:00:00')
Label(bottom_window_frame, text='to').grid(row=2, column=2, pady=5, sticky='w')
self.e_time_entry = Entry(bottom_window_frame, width=10)
self.e_time_entry.grid(row=2, column=3, padx=5, pady=5, sticky='w')
self.e_time_entry.insert(END, '00:00:00')
self.am_pm_menu = OptionMenu(bottom_window_frame, self.am_pm, 'am', 'pm')
self.am_pm_menu.grid(row=2, column=4, pady=5, sticky='w')
self.b_lat_entry = Entry(bottom_window_frame, width=10)
self.b_lat_entry.grid(row=3, column=1, padx=5, pady=5, sticky='w')
self.b_lat_entry.insert(END, '0.0')
Label(bottom_window_frame, text='to').grid(row=3, column=2, pady=5, sticky='w')
self.e_lat_entry = Entry(bottom_window_frame, width=10)
self.e_lat_entry.grid(row=3, column=3, padx=5, pady=5, sticky='w')
self.e_lat_entry.insert(END, '0.0')
self.b_alt_entry = Entry(bottom_window_frame, width=10)
self.b_alt_entry.grid(row=4, column=1, padx=5, pady=5, sticky='w')
self.b_alt_entry.insert(END, '0.0')
Label(bottom_window_frame, text='to').grid(row=4, column=2, pady=5, sticky='w')
self.e_alt_entry = Entry(bottom_window_frame, width=10)
self.e_alt_entry.grid(row=4, column=3, padx=5, pady=5, sticky='w')
self.e_alt_entry.insert(END, '0.0')
self.file_entry = Entry(bottom_window_frame, width=25)
self.file_entry.grid(row=5, column=1, padx=5, pady=5, sticky='w', columnspan=4)
bottom_button_frame = Frame(window_frame)
bottom_button_frame.pack(side=TOP, fill=BOTH, expand=False)
Button(bottom_button_frame, text='Search', command=self.parse_ranges).\
pack(side=LEFT, padx=15, pady=10)
[docs] def parse_ranges(self):
"""
Command for the search button, upon the user clicking the search button this function
will perform a number of regex parsing to ensure all fields contain valid numbers, then
sets the observers range dictionary to the valid fields and destroys AdvancedSearchDialog.
If any fields are invalid, an error will be displayed and the function will return, keeping
the window open and allowing the user to fix their error.
"""
date = self.date_entry.get()
r_date = re.compile('[0-9]{4}-[0-9]{2}-[0-9]{2}')
valid_entries = dict()
if r_date.match(date) is None:
logger.error('Invalid date entered \'%s\'' % date)
tkMessageBox.showerror('Invalid field', 'Invalid date \'%s\' entered,' % date +
' must match year-mo-day format')
return
if date == '0000-00-00':
date = ''
valid_entries['date'] = date
beg_time = self.b_time_entry.get()
r_time = re.compile('[0-6]{2}:[0-6]{2}:[0-6]{2}')
if r_time.match(beg_time) is None:
logger.error('Invalid beginning time range entered \'%s\'' % beg_time)
tkMessageBox.showerror('Invalid field', 'Invalid beginning time range' +
' \'%s\', must match hr:mn:sc format' % beg_time)
return
end_time = self.e_time_entry.get()
if r_time.match(end_time) is None:
logger.error('Invalid ending time range entered \'%s\'' % end_time)
tkMessageBox.showerror('Invalid fieldy', 'Invalid ending time range' +
' \'%s\', must match hr:mn:sc format' % end_time)
return
if self.am_pm.get() == '' and (beg_time != '00:00:00' or end_time != '00:00:00'):
logger.error('am/pm not specified but time given')
tkMessageBox.showerror('Invalid field', 'Time entered but no am/pm specified,' +
'specify whether the time is am/pm')
return
if end_time == '00:00:00':
end_time = ''
if beg_time == '00:00:00':
beg_time = ''
valid_entries['etime'] = end_time
valid_entries['btime'] = beg_time
beg_lat = self.b_lat_entry.get()
r_lat = re.compile('[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?')
if r_lat.match(beg_lat) is None:
logger.error('Invalid beginning lat range entered \'%s\'' % beg_lat)
tkMessageBox.showerror('Invalid field', 'Invalid beginning latitude range' +
' \'%s\', must be a valid number(e.g. -2.3 , 4, 0.0)'
% beg_lat)
return
if beg_lat == '0.0':
beg_lat = ''
valid_entries['blat'] = beg_lat
end_lat = self.e_lat_entry.get()
if r_lat.match(end_lat) is None:
logger.error('Invalid ending lat range entered \'%s\'' % end_lat)
tkMessageBox.showerror('Invalid field', 'Invalid ending latitude range' +
' \'%s\', must be valid number(e.g. -2.3, 4, 0.0'
% end_lat)
return
if end_lat == '0.0':
end_lat = ''
valid_entries['elat'] = end_lat
beg_alt = self.b_alt_entry.get()
# r_lat is actually the same regex so we can just use that
if r_lat.match(beg_alt) is None:
logger.error('Invalid beginning alt range entered \'%s\'' % beg_alt)
tkMessageBox.showerror('Invalid field', 'Invalid beginning altitude range' +
' \'%s\', must be a valid number(e.g. -2.3, 4, 0.0'
% beg_alt)
return
if beg_alt == '0.0':
beg_alt = ''
valid_entries['balt'] = beg_alt
end_alt = self.e_alt_entry.get()
if r_lat.match(end_alt) is None:
logger.error('Invalid ending alt range entered \'%s\'' % end_alt)
tkMessageBox.showerror('Invalid field', 'Invalid beginning altitude range' +
' \'%s\', must be a valid number(e.g. -2.3, 4, 0.0'
% end_alt)
return
if end_alt == '0.0':
end_alt = ''
valid_entries['ealt'] = end_alt
file_ = self.file_entry.get()
if file_ and file_.find('.hdf') == -1:
logger.info('No extension found in file entry, appending .hdf')
file_ += '.hdf'
valid_entries['plot'] = self.plots.get()
valid_entries['ampm'] = self.am_pm.get()
valid_entries['file'] = file_
# update ranges dictionary which will call ImportDialog receive()
self.shared_data.ranges = valid_entries
AdvancedSearchDialog.singleton = False
self.destroy()
[docs] def free(self):
"""
Notify base class window has been destroyed.
"""
logger.info('Closing AdvancedSearchDialog')
AdvancedSearchDialog.singleton = False
self.destroy()