## datasetbrowser.py ## author: Matthew Marquissee, NASA GSFC, Code 632 ## date started: June 16, 2003 ## last modified: July 28, 2003 ## purpose: This file sets up a GUI for an ftp/xml module that will search ## NASA datasets stored on various independent servers. XML is used to ## add metadata which eases search and synchronizes different data formats ## and hierarchies. The "Space Physics Dataset Browser" laid out in this ## file allows fast navigation of the XML tree structure (Document Object ## Model) and access to the search algorithm. I'm using Python's Tk module ## for design. # import the relevant Python modules from Tkinter import * import tkMessageBox import tkFileDialog import datasetfinder # the ftp/xml search module import timeRange # the time range module import webbrowser # messages MSG_WELCOME = """Welcome to the Space Physics Dataset Browser!""" MSG_ABOUT = """Space Physics Dataset Browser Created by Matthew Marquissee, June/July 2003 Space Physics Data Facility, NASA Goddard Space Flight Center Last Updated: 7/28/03""" # Application class inherits from Frame class DatasetBrowser(Frame): def __init__(self, master = None): # constructor Frame.__init__(self, master) self.grid(sticky=N+E+S+W) self.createWidgets() self.master.title('Space Physics Dataset Browser') self.manager = BusyManager(self.master) def createWidgets(self): self.drawMenu() frame3 = self.drawStatus() frame2 = self.drawResultWin() frame1 = self.drawOptionWin() frame3.pack(side=BOTTOM, fill=X, expand=1) frame1.pack(side=TOP, expand=1, fill=BOTH, padx=4, pady=4) frame2.pack(side=TOP, expand=1, fill=BOTH, padx=4, pady=4) def drawMenu(self): # create the toplevel menu menubar = Menu(self.master) # create a pulldown menu, and add it to the menu bar filemenu = Menu(menubar, tearoff=0) filemenu.add_command(label="Exit", command=root.quit) menubar.add_cascade(label="File", menu=filemenu) helpmenu = Menu(menubar, tearoff=0) helpmenu.add_command(label="About", command=self.popabout) menubar.add_cascade(label="Help", menu=helpmenu) self.master.config(menu=menubar) # let's draw an alwa status bar def drawStatus(self): w = Frame(self.master, relief=RAISED, bd=1) self.statusbar = Label(w, relief=SUNKEN, bd=1, anchor=W) self.statusbar.pack(side=LEFT, padx=3, pady=3, fill=X, expand=1) self.setStatus('please select a site') # status bar member function return w def drawResultWin(self): # set up frame right above status bar self.resultframe = Frame(self.master, height=200, width=500) #self.resultframe.grid(row=1, column=0, sticky=N+E+S+W) # label it self.resultlabel = Label(self.resultframe, text='Results:') self.resultlabel.grid(ipadx=2, ipady=2, row=0, column=0, columnspan=3, sticky=SW) # add scrollbar and result text box self.resultscroll = Scrollbar(self.resultframe) self.resultscroll.grid(row=1, column=1, rowspan=3, sticky=N+S) self.resultbox = Text(self.resultframe, height=15, width=100, state=DISABLED, yscrollcommand=self.resultscroll.set) self.resultbox.grid(ipadx=2, ipady=2, row=1, column=0, rowspan=3, sticky=N+E+S+W) self.resultscroll.config(command=self.resultbox.yview) self.chResult(MSG_WELCOME) # member function that replaces all text in box # allow for hyperlink tags in result box self.resultbox.tag_config("a", foreground="blue", underline=1) self.resultbox.tag_bind("a", "", self.show_hand_cursor) self.resultbox.tag_bind("a", "", self.show_arrow_cursor) self.resultbox.tag_bind("a", "", self.click_link) self.resultbox.config(cursor="arrow") # control buttons 1, 2, and 3 # 1. going up the tree one level self.b1 = Button(self.resultframe, text='back one\nlevel', command=self.resetall) self.b1.grid(ipadx=5, ipady=2, padx=5, pady=5, row=1, column=2, sticky=N+E+W) # 2. going up to the root self.b2 = Button(self.resultframe, text='reset all\noptions', command=self.resetall) self.b2.grid(ipadx=5, ipady=2, padx=5, pady=5, row=2, column=2, sticky=E+W) # 3. saving results self.b3 = Button(self.resultframe, text='save results', command=self.saveresults, state=DISABLED) self.b3.grid(ipadx=5, ipady=2, padx=5, pady=5, row=3, column=2, sticky=S+E+W) return self.resultframe def drawOptionWin(self): # get the user-defined preferences (more to come) # Pref 1: Choice of site to grab data from # Most users probably don't care about this, so # it is turned off by default self.opt1enable = 0 # set up frame between results and top menu self.optionframe = Frame(self.master, height=200) # self.optionframe.grid(row=0, column=0, sticky=E+S+W) # Options # 1. FTP Site (this will be optional; if not set, will use a list of sites) if self.opt1enable: # See note about Program Preference above self.opt1data = StringVar() # label it self.opt1label = Label(self.optionframe, text='Select a Site:') self.opt1label.grid(ipadx=2, ipady=2, row=0, column=0, sticky=NW) # scroll and select box self.opt1scroll = Scrollbar(self.optionframe, orient=VERTICAL) self.opt1select = Listbox(self.optionframe, height=3, width=30, bg='#fff', yscrollcommand=self.opt1scroll.set) self.opt1scroll.config(command=self.opt1select.yview) self.opt1scroll.grid(ipadx=2, ipady=2, row=0, column=2, sticky=N+S) self.opt1select.grid(ipadx=2, ipady=2, row=0, column=1, sticky=N+E+W) # set button self.opt1button = Button(self.optionframe, text = 'select', command=self.setopt1) self.opt1button.grid(ipadx=2, ipady=2, padx=5, pady=0, row=0, column=3, sticky=S+E+W) # 2. Spacecraft self.opt2data = StringVar() # label it self.opt2label = Label(self.optionframe, text='Select a Spacecraft:') self.opt2label.grid(ipadx=2, ipady=2, row=1, column=0, sticky=NW) # scroll and select box self.opt2scroll = Scrollbar(self.optionframe, orient=VERTICAL) self.opt2select = Listbox(self.optionframe, height=3, width=30, bg='#fff', yscrollcommand=self.opt2scroll.set) self.opt2scroll.config(command=self.opt2select.yview) self.opt2scroll.grid(ipadx=2, ipady=2, row=1, column=2, sticky=N+S) self.opt2select.grid(ipadx=2, ipady=2, row=1, column=1, sticky=N+E+W) # set button (disabled until option 1 is set, or if option 1 turned off) self.opt2button = Button(self.optionframe, text = 'select', state=DISABLED, command=self.setopt2) self.opt2button.grid(ipadx=2, ipady=2, padx=5, pady=0, row=1, column=3, sticky=S+E+W) # 3. Dataset self.opt3data = StringVar() # label it self.opt3label = Label(self.optionframe, text='Select a Dataset:') self.opt3label.grid(ipadx=2, ipady=2, row=2, column=0, sticky=NW) # scrolling select box self.opt3scroll = Scrollbar(self.optionframe, orient=VERTICAL) self.opt3select = Listbox(self.optionframe, height=3, width=30, bg='#fff', yscrollcommand=self.opt3scroll.set) self.opt3scroll.config(command=self.opt3select.yview) self.opt3scroll.grid(ipadx=2, ipady=2, row=2, column=2, sticky=N+S) self.opt3select.grid(ipadx=2, ipady=2, row=2, column=1, sticky=N+E+W) # set button (disabled until option 2 is set) self.opt3button = Button(self.optionframe, text = 'select', state=DISABLED, command=self.setopt3) self.opt3button.grid(ipadx=2, ipady=2, padx=5, pady=0, row=2, column=3, sticky=S+E+W) # 4. Time Input Fields self.start = StringVar() self.end = StringVar() self.timeunits = StringVar() # 4.1 Start Field # label it self.opt4label1 = Label(self.optionframe, text='Enter Start Time:') self.opt4label1.grid(ipadx=2, ipady=2, row=3, column=0, sticky=NW) # entry field (disabled until option 3 set) self.opt4box1 = Entry(self.optionframe, width=30, bg='#fff', state=DISABLED, textvariable=self.start) self.opt4box1.grid(ipadx=2, ipady=2, row=3, column=1, sticky=N+E+W) # a time verification button (disabled until option 3 set) self.opt4button1 = Button(self.optionframe, text = 'verify', state=DISABLED, command=self.verifytimes) self.opt4button1.grid(ipadx=2, ipady=2, padx=5, pady=0, row=3, column=3, sticky=S+E+W) # 4.2 End Field # label it self.opt4label2 = Label(self.optionframe, text='Enter End Time:') self.opt4label2.grid(ipadx=2, ipady=2, row=4, column=0, sticky=NW) # entry field (disabled until option 3 set) self.opt4box2 = Entry(self.optionframe, width=30, bg='#fff', state=DISABLED, textvariable=self.end) self.opt4box2.grid(ipadx=2, ipady=2, row=4, column=1, sticky=N+E+W) # set button (disabled until option 3 set) self.opt4button2 = Button(self.optionframe, text = 'send query', state=DISABLED, command=self.sendquery) self.opt4button2.grid(ipadx=2, ipady=2, padx=5, pady=0, row=4, column=3, sticky=S+E+W) # set up a stack of dictionaries (associative arrays) # this will allow the select box to display one thing # and have it be a key for a tree node reference self.dictstack = [] self.curdict = get_datasites() # fill the first option's select box with toplevel # dictionary if it is enabled if self.opt1enable: for key, val in self.curdict.items(): self.opt1select.insert(END, key) else: self.setopt1list() return self.optionframe # Set Option 1: Site # non-list version: user chooses the sites he/she wants def setopt1(self): if self.opt1select.curselection() != (): # get the selected item and get its value from current dictionary tmp = self.curdict.get(self.opt1select.get(ACTIVE)) self.opt1data.set(tmp) # load a single xml structure document # store document and another reference to it self.xmldoclst = datasetfinder.load_xml(self.opt1data.get()) self.curnode = self.xmldoc # button stuff self.opt1button.config(state=DISABLED) self.opt2button.config(state=NORMAL) # store Option 1 dictionary self.dictstack.append(self.curdict) self.curdict = {} # fill the Option 2 box and dictionary lst = self.curnode.getElementsByTagName('spacecraft') for x in lst: self.curdict[x.getAttribute('name')] = x self.opt2select.insert(END, x.getAttribute('name')) # display info for selected Option 1 node self.chResult(self.curnode.getElementsByTagName('description').item(0).firstChild.data) self.setStatus('please select a spacecraft') else: tkMessageBox.showwarning('Nothing selected', 'Please select something first') # Set Option 1: Data Site (list version) def setopt1list(self): datasitelist = get_datasites() # load all of the XML files lst = map(datasetfinder.load_xml, datasitelist.values()) self.xmldoclst = lst self.curnode = lst[0] # activate reset buttons self.b1.config(command=self.resettolist) self.b2.config(command=self.resettolist) self.opt2button.config(state=NORMAL, command=self.setopt2list) self.curdict = {} # load all spacecraft into first selection box and dictionary for x in lst: for y in x.getElementsByTagName('spacecraft'): self.curdict[y.getAttribute('name')] = y # fill the selection box for x in self.curdict.keys(): self.opt2select.insert(END, x) self.setStatus('please select a spacecraft') # return to root of tree (nonlist version) def resetall(self): # clear all variables self.opt1data.set('') self.opt2data.set('') self.opt3data.set('') self.start.set('') self.end.set('') self.timeunits.set('') # disable all but first button self.opt1button.config(state=NORMAL) self.opt2button.config(state=DISABLED) self.opt3button.config(state=DISABLED) self.opt4button1.config(state=DISABLED) self.opt4button2.config(state=DISABLED) self.opt4box1.config(state=DISABLED) self.opt4box2.config(state=DISABLED) # return all text boxes to initial state self.opt4box1.delete(0, END) self.opt4box2.delete(0, END) self.opt2select.delete(0, END) self.opt3select.delete(0, END) self.chResult(MSG_WELCOME) # clear dictionary stack self.dictstack = [] self.curdict = get_datasites() # set control button 1 to initial state self.b1.config(command=self.resetall) self.b3.config(state=DISABLED) self.setStatus('please select a site') # return to root of tree (list version) def resettolist(self): # clear all variables self.opt2data.set('') self.opt3data.set('') self.start.set('') self.end.set('') self.timeunits.set('') # disable all but first button self.opt2button.config(state=NORMAL) self.opt3button.config(state=DISABLED) self.opt4button1.config(state=DISABLED) self.opt4button2.config(state=DISABLED) self.opt4box1.config(state=DISABLED) self.opt4box2.config(state=DISABLED) # return all text boxes to initial state self.opt4box1.delete(0, END) self.opt4box2.delete(0, END) self.opt3select.delete(0, END) self.chResult(MSG_WELCOME) # clear dictionary stack self.dictstack = [] self.curdict = get_datasites() # set control button 1 to initial state self.b1.config(command=self.resettolist) self.b3.config(state=DISABLED) self.setStatus('please select a spacecraft') # Set Option 2: Spacecraft def setopt2(self): if self.opt2select.curselection() != (): # get the selected item and get its value from current dictionary (a node) self.curnode = self.curdict.get(self.opt2select.get(ACTIVE)) self.opt2data.set(self.curnode.getAttribute('name')) # button stuff self.opt2button.config(state=DISABLED) self.opt3button.config(state=NORMAL) # store Option 2 dictionary self.dictstack.append(self.curdict) self.curdict = {} # fill the Option 3 box and dictionary lst = self.curnode.getElementsByTagName('dataset') for x in lst: self.curdict[x.getAttribute('name')] = x self.opt3select.insert(END, x.getAttribute('name')) # display info for selected Option 2 node self.chResult(self.curnode.getElementsByTagName('description').item(0).firstChild.data) self.setStatus('please select a specific dataset') # allow for going back up the tree one level self.b1.config(command=self.unsetopt2) else: tkMessageBox.showwarning('Nothing selected', 'Please select something first') def setopt2list(self): if self.opt2select.curselection() != (): # get the selected item and get its value from current dictionary (a node) temp = self.opt2select.get(ACTIVE) lst = [] # store Option 2 dictionary self.dictstack.append(self.curdict) self.curdict = {} # load all datasets into first selection box and dictionary for x in self.xmldoclst: # look two levels up because instruments are optional for y in [z for z in x.getElementsByTagName('dataset') \ if z.parentNode.parentNode.getAttribute('name') == temp \ or z.parentNode.getAttribute('name') == temp]: self.curdict[y.getAttribute('name')] = x, y self.opt3select.insert(END, y.getAttribute('name')) # button stuff self.opt2button.config(state=DISABLED) self.opt3button.config(state=NORMAL) self.setStatus('please select a specific dataset') else: tkMessageBox.showwarning('Nothing selected', 'Please select something first') def unsetopt2(self): # remove dictionary 2 self.curdict = self.dictstack.pop() # up to parent (datasite node) self.curnode = self.curnode.parentNode # fix buttons self.opt3button.config(state=DISABLED) self.opt2button.config(state=NORMAL) # clear select boxes and option 2 self.opt3select.delete(0, END) self.opt2data.set('') self.b1.config(command=self.resetall) self.setStatus('please select a site') # Set Option 3: Dataset def setopt3(self): if self.opt3select.curselection() != (): # get the selected item and get its value from current dictionary (a node) if not self.opt1enable: self.xmldoc, self.curnode = self.curdict.get(self.opt3select.get(ACTIVE)) else: self.curnode = self.curdict.get(self.opt3select.get(ACTIVE)) self.opt3data.set(self.curnode.getAttribute('name')) # button stuff self.opt3button.config(state=DISABLED) self.opt4button1.config(state=NORMAL) self.opt4button2.config(state=NORMAL) self.opt4box1.config(state=NORMAL) self.opt4box2.config(state=NORMAL) # store Option 3 dictionary self.dictstack.append(self.curdict) self.curdict = {} # default Option 4 to timerange in XML file tmp = self.curnode.getElementsByTagName('timerange').item(0) self.start.set(tmp.getAttribute('start')) self.end.set(tmp.getAttribute('stop')) self.timeunits.set(tmp.getAttribute('units')) # make times look nice self.verifytimes(True) # allow for going back up the tree one level self.b1.config(command=self.unsetopt3) self.setStatus('please enter a time range and click "send query"') else: tkMessageBox.showwarning('Nothing selected', 'Please select something first') def unsetopt3(self): # remove dictionary 3 self.curdict = self.dictstack.pop() # up to grandparent (instrument nodes are skipped for now) self.curnode = self.curnode.parentNode.parentNode # fix buttons self.opt3button.config(state=NORMAL) self.opt4button1.config(state=DISABLED) self.opt4button2.config(state=DISABLED) # clear text boxes and option 3 self.opt4box1.delete(0, END) self.opt4box2.delete(0, END) self.opt4box1.config(state=DISABLED) self.opt4box2.config(state=DISABLED) self.opt3data.set('') if self.opt1enable: self.b1.config(command=self.unsetopt2) else: self.b1.config(command=self.resettolist) self.b3.config(state=DISABLED) self.setStatus('please select a specific dataset') def sendquery(self): # turn on hourglass self.manager.busy() # run query self.setopt4() # turn hourglass off self.manager.notbusy() # Set Option 4: Time Range (final option) def setopt4(self): # check that start time preceeds stop time, form time range object t1 = timeRange.get_time(self.start.get()) t2 = timeRange.get_time(self.end.get()) if t1.start() > t2.start(): self.chResult('Start time exceeds the stop time. Please change this ' + 'before proceeding.') return t_range = timeRange.combine(t1, t2) # display header to URL results self.chResult('Dataset: ' + self.opt3data.get() + '\n' + 'Start Time: ' + t_range.start().isoformat('\t') + '\n' + 'End Time: ' + t_range.stop().isoformat('\t') + '\n' + '\nHere is a list of Relevant URLs to the data:\n') # search the FTP structure laid out in the XML document for x in datasetfinder.search_xml(self.xmldoc, self.opt3data.get(), self.start.get(), self.end.get()): if x == 'no result': self.chResult('Sorry, there were no results.') self.addResultTimeLink(x) # display results as time, then hyperlink # allow copying and saving of results self.resultbox.config(state=NORMAL) self.b3.config(state=NORMAL) self.setStatus('retrieval complete...') # nicely format times and make them reasonable def verifytimes(self, fromxml = False): if self.end.get().upper() == 'LATEST': self.end.set('LATEST') if self.start.get().upper() == 'BEGINNING': self.start.set('BEGINNING') # prepend a 'b' to any four-digit number so we know it uses bartel rotations # if relevant if self.timeunits.get() == 'bartels': if fromxml and self.start.get() != 'BEGINNING' and \ not timeRange.is_bartel_form(self.start.get()): self.start.set('b' + self.start.get()) if fromxml and self.end.get() != 'LATEST' and \ not timeRange.is_bartel_form(self.end.get()): self.end.set('b' + self.end.get()) # convert from bartels to yyyy-mm-dd for added usability t1 = timeRange.get_time(self.start.get()).start() t2 = timeRange.get_time(self.end.get()).start() self.start.set(t1.strftime('%Y-%m-%d')) self.end.set(t2.strftime('%Y-%m-%d')) # special key words if timeRange.get_time(self.start.get()).start() == timeRange.datetime.min: self.start.set('BEGINNING') elif timeRange.get_time(self.start.get()).start() == timeRange.datetime.max: self.start.set('LATEST') if timeRange.get_time(self.end.get()).stop() == timeRange.datetime.min: self.end.set('BEGINNING') elif timeRange.get_time(self.end.get()).stop() == timeRange.datetime.max: self.end.set('LATEST') def saveresults(self): str = self.resultbox.get(1.0, END) file_nm = tkFileDialog.asksaveasfilename(parent=root, defaultextension='.txt') try: f = open(file_nm, 'w') except IOError: # problem with file name return f.write(str) f.close() # change status bar text def setStatus(self, txt): self.statusbar.config(text=txt) # overwrite result box with string 'txt' # notice the locking/unlocking of the text box def chResult(self, txt): self.resultbox.config(state=NORMAL) self.resultbox.delete(1.0, END) self.resultbox.insert(END, txt) self.resultbox.config(state=DISABLED) # just append a line of text to result box # notice the locking/unlocking of the text box def addResultLine(self, line): self.resultbox.config(state=NORMAL) self.resultbox.insert(END, line + '\n') self.resultbox.config(state=DISABLED) # just append a line of text to result box AS A HYPERLINK # notice the locking/unlocking of the text box def addResultTimeLink(self, result): self.resultbox.config(state=NORMAL) if len(result) != 2: self.resultbox.insert(END, '\n') else: t_range, url = result the_time = 'START: ' + t_range.start().isoformat(' ') + \ '\t\tSTOP: ' + t_range.stop().isoformat(' ') + ':\n\t' self.resultbox.insert(END, the_time) self.resultbox.insert(END, url, 'a') self.resultbox.insert(END, '\n') self.resultbox.config(state=DISABLED) # call web browser for hyperlinks def click_link(self, event): curindex = event.widget.index(INSERT) range = event.widget.tag_prevrange('a', curindex) if range == (): return start, end = range url = event.widget.get(start, end) try: webbrowser.open(url) except WindowsError: pass # ignore def show_hand_cursor(self, event): event.widget.config(cursor='hand2') def show_arrow_cursor(self, event): event.widget.config(cursor='arrow') def popabout(self): tkMessageBox.showinfo("About This Program", MSG_ABOUT) # get list of datasites from 'datasites.pref' def get_datasites(): try: f = open('datasites.pref') except IOError: return dict() else: lines = f.readlines() lines = map(lambda x: x.strip().split('|'), lines) f.close() return dict(lines) # Busy Manager (lets the user know that query is in progress class BusyManager: def __init__(self, widget): self.toplevel = widget.winfo_toplevel() self.widgets = {} def busy(self, widget=None): # attach busy cursor to toplevel, plus all windows # that define their own cursor. if widget is None: w = self.toplevel # myself else: w = widget if not self.widgets.has_key(str(w)): try: # attach cursor to this widget cursor = w.cget("cursor") if cursor != "watch": self.widgets[str(w)] = (w, cursor) w.config(cursor="watch") except TclError: pass ## for w in w.children.values(): ## self.busy(w) def notbusy(self): # restore cursors for w, cursor in self.widgets.values(): try: w.config(cursor=cursor) except TclError: pass self.widgets = {} if __name__ == '__main__': # instantiate a Tk window root = Tk() root.config(cursor='arrow') # instantiate the application app = DatasetBrowser(root) # run the application root.mainloop()