#!/usr/local/bin/python


###############################################################################################
###############################################################################################
##                                                                                           ##
##                          ___       _                          _                           ##
##                         |_ _|_ __ | |_ ___ _ __ ___  ___  ___| |_                         ##
##                          | || '_ \| __/ _ \ '__/ __|/ _ \/ __| __|                        ##
##                          | || | | | ||  __/ |  \__ \  __/ (__| |_                         ##
##                         |___|_| |_|\__\___|_|  |___/\___|\___|\__|                        ##
##                                                                                           ##
##                                       Version 1.1                                         ##
##                                                                                           ##
##   A tool for finding and exploring the intersections between multiple sets of database    ##
##   searches.                                                                               ##
##                                                                                           ##
##   For instructions on the use of this program, please refer to the user manual.           ##
##                                                                                           ##
##   This program was created by Scott C-H. Pegg, Ph.D. and Walter Novak, in the laboratory  ##
##   of Patricia C. Babbitt, at the University of California, San Francisco.                 ##
##                                                                                           ##
##   Created: 3-25-02                                                                        ##
##   Version 1.1: 2-20-03                                                                    ##
##                                                                                           ##
##   (c) Copyright (2002) University of California                                           ##
##                                                                                           ##
###############################################################################################
###############################################################################################



###############################################################################################
#                                                                                             #
# Imported modules                                                                            #
#                                                                                             #
###############################################################################################

from Tkinter import *
import Pmw
from string import *
from math import *
from time import asctime
import tkFileDialog
import tkMessageBox
from tkSimpleDialog import Dialog
import os


###############################################################################################
###############################################################################################
##                                                                                           ##
## Data structures and functions for parsing the output files of sequence database searches. ##
##                                                                                           ##
##  class Hit  -  class to store information (one Hit per sequence)                          ##
##  check_file_type()  -  function to determine the type of a given output file              ##
##  parse_wustl_blast()  -  parser for WashU BLAST output files                              ##
##  parse_ncbi_blast()  -  parser for NCBI BLAST output files                                ##
##  parse_fasta()  -  parser for FASTA output files                                          ##
##  parse_psiblast()  -  parser for PSIBLAST output files (not implemented yet)              ##
##                                                                                           ##
###############################################################################################
###############################################################################################


###############################################################################################
#                                                                                             #
# class Hit                                                                                   #
#                                                                                             #
# This class holds information about a sequence reported in an output file. Each sequence     #
# found within a database search file (eg. BLAST file) is considered a 'Hit'.                 #
#                                                                                             #
###############################################################################################

class Hit:
   
  def __init__(self, name, desc):
    self.name = name          # usually the sequence id tag
    self.description = desc   # the one-line description
    self.shotgun_score = 0    # the number of output files reporting this sequence
    self.files_found = []     # a list of (set number, filename, prob./expect. score) tuples
    self.sets_found = []      # just a list of the output file sets reporting this sequence

  # Function called when this hit is found in a database search file
  def found(self, set_num, filename, prob_score):
    self.files_found.append([set_num, filename, prob_score])
    self.shotgun_score = self.shotgun_score + 1
    if set_num not in self.sets_found: self.sets_found.append(set_num)
     
     
###############################################################################################
#                                                                                             #
# parse_wustl_blast()                                                                         #
#                                                                                             #
# Given a table of current Hit instances, read a WashU format BLAST file, and add to the      #
# table.                                                                                      #
#                                                                                             #
###############################################################################################

def parse_wustl_blast(hit_table, filename, set_num, pmax, pmin):

  l = open(filename, 'r').readlines()
  i = 0
  while find(l[i], 'Sequences producing') == -1: i = i + 1
  i = i + 2

  while i < len(l):
    if strip(l[i]) == '': break
    t = split(l[i])
    name = t[0]
    desc = join(t[1:-3])
    prob = t[-2]

    # check if the leading digit was removed (eg. 1e-100  ->  e-100)
    if prob[0] == 'e': prob = "1" + prob

    # make sure it fits within the filter criteria
    p = atof(prob)
    if p > pmax or p < pmin:
      i = i + 1
      continue
    
    try:
      hit = hit_table[name]
    except KeyError:
      hit = Hit(name, desc)
      hit_table[name] = hit
      
    hit.found(set_num, filename, prob)
    i = i + 1

  return hit_table
  
  
###############################################################################################
#                                                                                             #
# parse_ncbi_blast()                                                                          #
#                                                                                             #
# Given a table of current Hit instances, read an NCBI format BLAST file, and add to the      #
# table.                                                                                      #
#                                                                                             #
###############################################################################################

def parse_ncbi_blast(hit_table, filename, set_num, pmax, pmin):
  
  l = open(filename, 'r').readlines()
  i = 0
  while find(l[i], 'Sequences producing') == -1: i = i + 1
  i = i + 2

  while i < len(l):
    if strip(l[i]) == '': break
    t = split(l[i])
    name = t[0]
    desc = join(t[1:-2])
    prob = t[-1]
    
    # check if the leading digit was removed (eg. 1e-100  ->  e-100)
    if prob[0] == 'e': prob = "1" + prob

    # make sure it fits within the filter criteria
    p = atof(prob)
    if p > pmax or p < pmin:
      i = i + 1
      continue
    
    try:
      hit = hit_table[name]
    except KeyError:
      hit = Hit(name, desc)
      hit_table[name] = hit
      
    hit.found(set_num, filename, prob)
    i = i + 1

  return hit_table


###############################################################################################
#                                                                                             #
# parse_fasta()                                                                               #
#                                                                                             #
# Given a table of current Hit instances, read a FASTA output file, and add to the table.     #
#                                                                                             #
###############################################################################################

def parse_fasta(hit_table, filename, set_num, pmax, pmin):

  l = open(filename, 'r').readlines()
  i = 0
  while find(l[i], 'The best scores') == -1: i = i + 1
  i = i + 2

  while i < len(l):
    if strip(l[i]) == '': break
    t = split(l[i])
    name = t[0]
    desc = join(t[1:-6])
    prob = t[-1]

    # make sure it fits within the filter criteria
    p = atof(prob)
    if p > pmax or p < pmin:
      i = i + 1
      continue

    try:
      hit = hit_table[name]
    except KeyError:
      hit = Hit(name, desc)
      hit_table[name] = hit
      
    hit.found(set_num, filename, prob)
    i = i + 1

  return hit_table


###############################################################################################
#                                                                                             #
# parse_psiblast()                                                                            #
#                                                                                             #
# Given a table of current Hit instances, read a PSIBLAST output file, and add to the table.  #
# (The round information is included with the probability score, eg. 0.02 (3).) Sequences     #
# are often reported multiple times as PSI-BLAST finds them with better scores. Only the      #
# lowest E-score hits are put into the table.                                                 #
#                                                                                             #
###############################################################################################

def parse_psiblast(hit_table, filename, set_num, pmax, pmin):
  
  l = open(filename, 'r').readlines()
  num_lines = len(l) - 1
  i = 0
  while i < num_lines: 
  
    # get the round number
    while find(l[i], 'Results from round') == -1:
      if i >= num_lines: break 
      i = i + 1
    if i >= num_lines: break    
    round = split(l[i])[-1]
    
    while find(l[i], 'Sequences producing significant alignments') == -1 and i < num_lines: i = i + 1
    i = i + 2
    
    # If this is the first round, take all of the sequences reported
    if round == '1':
      while strip(l[i]) != '' and l[i][0] != '>':
        t = split(l[i])
        name = t[0]
        desc = join(t[1:-2])
        prob = t[-1]

        # check if the leading digit was removed (eg. 1e-100  ->  e-100)
        if prob[0] == 'e': prob = "1" + prob

        # make sure it fits within the filter criteria
        p = atof(prob)
        if p > pmax or p < pmin:
          i = i + 1
          continue

        # add round info to probability info
        prob = prob + ' (%s)' % round
      
        # add it to the table
        try:
          hit = hit_table[name]
        except KeyError:
          hit = Hit(name, desc)
          hit_table[name] = hit      
        hit.found(set_num, filename, prob)
      
        i = i + 1
      
    # If this is a later round, take only the new sequences reported
    if round != '1':
      while find(l[i], 'Sequences not found previously') == -1 and i < num_lines: i = i + 1
      i = i + 2

      # if there were new sequences reported
      if find(l[i], 'Searching.....') == -1 and find(l[i], 'Database:') == -1:
      
        while strip(l[i]) != '' and l[i][0] != '>':
          t = split(l[i])
          name = t[0]
          desc = join(t[1:-2])
          prob = t[-1]
          # check if the leading digit was removed (eg. 1e-100  ->  e-100)
          if prob[0] == 'e': prob = "1" + prob

          # make sure it fits within the filter criteria
          p = atof(prob)
          if p > pmax or p < pmin:
            i = i + 1
            continue

          # add round info to probability info
          prob = prob + ' (%s)' % round

          # check if the hit is already in the hit table
          keep = 0
          try:
            hit = hit_table[name]
          except KeyError:
            hit = Hit(name, desc)
            hit_table[name] = hit
            keep = 1
     
          # check if there is already a hit from this file
          for j in range(len(hit.files_found)):
            ffound = 0
            if filename == hit.files_found[j][1]:

              ffound = 1
            
              # compare the probability values
              temp = atof(split(prob)[0])
              temp2 = atof(split(hit.files_found[j][2])[0])
              if temp <= temp2:
                # remove the previous entry and subtract one from the score
                hit.files_found.remove(hit.files_found[j])
                hit.shotgun_score = hit.shotgun_score - 1
                keep = 1
              break      

          # in case the hit.files_found list is not empty, but doesn't contain this file
          if not keep and not ffound: keep = 1
        
          if keep == 1:      
            hit.found(set_num, filename, prob)
          
          i = i + 1
        
    i = i + 1
    
  return hit_table
  

###############################################################################################
#                                                                                             #
# check_file_type()                                                                           #
#                                                                                             #
# Given a filename, determine what type of output file it is and return a string indicating   #
# the type.                                                                                   #
#                                                                                             #
###############################################################################################

def check_file_type(filename):

  try:
    l = open(filename, 'r').readlines()
  except IOError:
    return "Error: Cannot open file %s" % filename
    
  type = ''
  # We can look at the first line to find Wustl BLAST and FASTA files
  if find(l[0], 'WashU') != -1: type = 'Wustl-BLAST'
  elif find(l[0], 'FASTA') != -1: type = 'FASTA'
  else:
    # Look in the next 5 lines for Altschul reference
    for i in range(5):
      if find(l[i], 'Reference: Altschul') != -1:
        type = 'NCBI-BLAST'
        # Look for PSI-BLAST marker
        for j in range(100):
          if j >= len(l): return 'NCBI-BLAST'
          if find(l[j], 'Results from round 1') != -1: 
            type = 'PSI-BLAST'
            break
        break
          
  # probably need to catch case of files being less than 25 lines
  
  if type == '': return "Error: File %s not a recognized format" % filename
  
  return type
  
  
###############################################################################################
###############################################################################################
##                                                                                           ##
## Utility functions                                                                         ##
##                                                                                           ##
##  add_colors()  -  add two colors using the Tkinter string format                          ##
##  dist()  -  calculate the Euclidean distance between two points                           ##
##  sort_hit_keys(list)  -  sort a list of hit hash keys by set size then alphanumerically   ##
##                                                                                           ##
###############################################################################################
###############################################################################################


###############################################################################################
#                                                                                             #
# add_colors()                                                                                #
#                                                                                             #
# Add two colors using the Tkinter string format. This format is "#xxxxxx" where x is a hex   #
# digit, and the RGB format "#RRGGBB". The three colors are added separately, with a ceiling  #
# at 0xff (255), and returned in the string format.                                           #
#                                                                                             #
###############################################################################################

def add_colors(c1, c2):

  # break colors into R, G, B hex values
  r1 = atoi(c1[1:3], 16)
  r2 = atoi(c2[1:3], 16)
  g1 = atoi(c1[3:5], 16)
  g2 = atoi(c2[3:5], 16)
  b1 = atoi(c1[5:7], 16)
  b2 = atoi(c2[5:7], 16)
  
  # add each color, but leave 255 (0xff) as max
  r = r1 + r2
  if r > 255: r = 255
  g = g1 + g2
  if g > 255: g = 255
  b = b1 + b2
  if b > 255: b = 255
  
  # rebuild the string
  s = "#%.2x%.2x%.2x" % (r, g, b)

  return s


###############################################################################################
#                                                                                             #
# dist_3d()                                                                                   #
#                                                                                             #
# Return the Euclidean distance between two given points (in 3-space).                        #
#                                                                                             #
###############################################################################################

def dist_3d(point1, point2):
  t = (point1[0] - point2[0]) * (point1[0] - point2[0])
  t2 = (point1[1] - point2[1]) * (point1[1] - point2[1])
  t3 = (point1[2] - point2[2]) * (point1[2] - point2[2])
  return sqrt(t + t2 + t3)


###############################################################################################
#                                                                                             #
# furthest_color()                                                                            #
#                                                                                             #
# Given a list of RGB colors, choose the color furthest from all of them. Since the space is  #
# only 256^3 (=16,777,216), maybe I can sample it exhaustively...                             #
#                                                                                             #
###############################################################################################

def furthest_color(colors):

  # change the colors from #xxxxxx hex values to decimal tuples
  list = []
  for i in range(len(colors)):
    rc = atoi(colors[i][1:3], 16)
    gc = atoi(colors[i][3:5], 16)
    bc = atoi(colors[i][5:7], 16)
    list.append((rc, gc, bc))

  # find the furthest values of r,g,b from the given colors
  best = (0,0,0)
  best_dist = 0.0
  for r in range(0, 256, 20):
    for g in range(0, 256, 20):
      for b in range(0, 256, 20):
        test = (r, g, b)
        valid = 1
        sum = []
        for i in range(len(list)):
          if test == list[i]:
            valid = 0
            break
          sum.append(dist_3d(test, list[i]))
        if valid:
          sum.sort()
          low = sum[0]
          if low > best_dist:
            best_dist = low
            best = test

  # convert the best point back to hex values
  s = "#%.2x%.2x%.2x" % (best[0], best[1], best[2])

  return s


###############################################################################################
#                                                                                             #
# dist()                                                                                      #
#                                                                                             #
# Return the Euclidean distance between two given points (in 2-space).                        #
#                                                                                             #
###############################################################################################

def dist(point1, point2):
  t = (point1[0] - point2[0]) * (point1[0] - point2[0])
  t2 = (point1[1] - point2[1]) * (point1[1] - point2[1])
  return sqrt(t + t2)
  
  
###############################################################################################
#                                                                                             #
# sort_hit_keys(list)                                                                         #
#                                                                                             #
# Return a list of hit keys sorted first by the number of sets found and second by simple     #
# alphanumerics.                                                                              #
#                                                                                             #
###############################################################################################

def sort_hit_keys(list):
  # deconstruct each key into a list of integers
  tl = []
  for i in range(len(list)):
    t = splitfields(list[i], '|')[:-1]
    for j in range(len(t)):
      t[j] = atoi(t[j])
    tl.append(t)
  tl.sort(lambda x,y: cmp(len(x), len(y)))
  tl.reverse()
  # bin each list by length
  bins = []
  cur_len = len(tl[0])
  temp = []
  for i in range(len(tl)):
    if len(tl[i]) != cur_len:
      bins.append(temp)
      temp = []
      temp.append(tl[i])
      cur_len = len(tl[i])
    else:
      temp.append(tl[i])
  bins.append(temp)
  # sort each bin alphanumerically
  for i in range(len(bins)):
    bins[i].sort()
  # rebuild the key list
  kl = []
  for i in range(len(bins)):
    for j in range(len(bins[i])):
      key = ''
      for k in range(len(bins[i][j])):
        key = key + "%s|" % bins[i][j][k]
      kl.append(key)
  return kl
  
      

###############################################################################################
###############################################################################################
##                                                                                           ##
## Classes defining child (ie. pop-up) windows used by the Buckshot interface.               ##
##                                                                                           ##
##  class TextDisplay  -  a simple text display window                                       ##
##  class FilterDialog  -  a window to let user enter parameters for filtering text results  ##
##  class ColorChooser  -  a window allowing the user to choose a color based on RGB sliders ##
##                                                                                           ##
###############################################################################################
###############################################################################################


###############################################################################################
#                                                                                             #
# class TextDisplay                                                                           #
#                                                                                             #
# A simple text display window, with the option of saving the contents of the display to a    #
# file. Note that the user is allowed to edit the contents of the display.                    #
#                                                                                             #
###############################################################################################

class TextDisplay(Toplevel):

  def __init__(self, master, text):
    self.text = text
    self.master = master
    self.root = Toplevel(master)
    self.root.title("Intersection Display")
    self.root.iconname("Intersection Display")
    
    self.root.geometry('+%d+%d' % (master.winfo_rootx()+200, master.winfo_rooty()+20) )
    self.root.resizable(0, 0)

    frame = Frame(self.root)
    frame.pack(expand=1)
    
    # the text display
    self.st = Pmw.ScrolledText(frame, usehullsize=1, hull_width=550, hull_height=600)
    self.st.component('text').config(font=('Courier', 10))
    self.st.settext(self.text)
    self.st.pack(expand=1, fill=Y)
    
    # buttons
    bf = Frame(frame)
    bf.pack(side=BOTTOM, expand=1)
    ok = Button(bf, text="OK", command=self.done)
    ok.pack(side=LEFT, padx=5, pady=5)    
    save = Button(bf, text="Save to File", command=self.savetofile)
    save.pack(side=RIGHT, padx=5, pady=5)
    fb = Button(bf, text="Filter", command=self.filter)
    fb.pack(side=RIGHT, padx=5, pady=5) 
  
  # filter the contents of the text display
  def filter(self):
    res = FilterDialog(self.root, 1.0, 0.0)
    pv = res.result
    if pv == None: return
    if len(res.result) < 2: return
    max = atof(res.result[0])
    min = atof(res.result[1])
        
    # read until you hit two blank lines, putting read lines into new text
    l = splitfields(self.text, "\n")
    new = ""
    i = 0
    while strip(l[i]) != "" or strip(l[i+1]) != "":
      new = new + l[i] + "\n"
      i = i + 1
    i = i + 2
    new = new + "\n\n"
    
    while i < len(l):
      # hold onto the next two lines
      hit = l[i] + "\n" + l[i+1] + "\n"
      i = i + 2
      valid = 1
      while strip(l[i]) != "":  # read until the next blank line
        t = split(l[i])
        # check if last element is a psi-blast round
        if t[-1][0] == '(' and t[-1][-1] == ')': p = t[-2]
        else: p = t[-1]
        val = atof(p)
        if val > max or val < min:  valid = 0
        hit = hit + l[i] + "\n"
        i = i + 1     
        
      # if valid, include in new text
      if valid: new = new + hit + "\n"
      i = i + 1 
           
    self.st.settext(new)

  # save the contents of the text display to a file chosen by the user  
  def savetofile(self):
    filename = tkFileDialog.asksaveasfilename()
    if filename == None: return
    self.st.exportfile(filename)

  def done(self):
    self.root.destroy()


###############################################################################################
#                                                                                             #
# class FilterDialog                                                                          #
#                                                                                             #
# A window allowing the user to choose filtering options of text (the formatted results of    #
# set intersections). The filter parameters are returned when the window is closed using the  #
# "OK" button.                                                                                #
#                                                                                             #
###############################################################################################

class FilterDialog(Dialog):

  def __init__(self, master, pmax, pmin):
    self.default_pmax = pmax
    self.default_pmin = pmin
    Dialog.__init__(self, master)
    
  def body(self, master):

    self.title("Filter Parameters")

    frame = Frame(self)
    frame.pack(expand=1)
    
    # P-value filter
    lp = Label(frame, text="P-value max:")
    lp.grid(row=0, column=0, padx=0, pady=5)
    default_max = "%f" % self.default_pmax
    default_min = "%f" % self.default_pmin
    self.pmax = Pmw.EntryField(frame, validate={'validator':'real', 'min':0}, value=default_max)
    self.pmax._entryFieldEntry.config(width=8)
    self.pmax.grid(row=0, column=1, padx=0, pady=5)
    lp2 = Label(frame, text="P-value min:")
    lp2.grid(row=1, column=0, padx=0, pady=5)
    self.pmin = Pmw.EntryField(frame, validate={'validator':'real', 'min':0}, value=default_min)
    self.pmin._entryFieldEntry.config(width=8)
    self.pmin.grid(row=1, column=1, padx=0, pady=5)


  def apply(self):
    self.result = [self.pmax.get(), self.pmin.get()]


###############################################################################################
#                                                                                             #
# class ColorChooser                                                                          #
#                                                                                             #
# A window allowing the user to choose a color via RGB sliders. The color is returned (in the #
# Tkinter string format) when the window is closed using the "OK" button.                     #
#                                                                                             #
###############################################################################################

class ColorChooser(Dialog):

  def __init__(self, master, color):
    self.color = color
    Dialog.__init__(self, master)
  
  # update the color of the frame (this gets called immediately after the scales are shown)
  def UpdateColor(self, x):
    R = self.r_scale.get()
    G = self.g_scale.get()
    B = self.b_scale.get()
    color = "#%.2x%.2x%.2x" % (R, G, B)
    self.cf.config(bg=color)
    self.color = color
   
  def body(self, master):
    
    # a major frame
    frame = Frame(self)
    frame.pack()
    
    # a frame to show the color in
    self.cf = Frame(frame, height=200, width=150, bg=self.color, borderwidth=5)
    self.cf.grid(row=0, column=0, rowspan=3, padx=5, pady=5)    

    # a slider for the RGB values
    self.r_scale = Scale(frame, length=256, from_=0, to=255, label="Red", orient=HORIZONTAL, tickinterval=50, command=self.UpdateColor)
    self.r_scale.grid(row=0, column=1)
    self.g_scale = Scale(frame, length=256, from_=0, to=255, label="Green", orient=HORIZONTAL, tickinterval=50, command=self.UpdateColor)
    self.g_scale.grid(row=1, column=1)
    self.b_scale = Scale(frame, length=256, from_=0, to=255, label="Blue", orient=HORIZONTAL, tickinterval=50, command=self.UpdateColor)
    self.b_scale.grid(row=2, column=1)

    # set the scales to the current colors
    r = atoi(self.color[1:3], 16)
    g = atoi(self.color[3:5], 16)
    b = atoi(self.color[5:7], 16)
    self.r_scale.set(r)
    self.g_scale.set(g)
    self.b_scale.set(b)

  def apply(self):
    self.result = self.color
    
    
###############################################################################################
###############################################################################################
##                                                                                           ##
## The main interface                                                                        ##
##                                                                                           ##
##  class GUI  -  the main window, containing the display and controls                       ##
##    Member functions:                                                                      ##
##      AddSet()  -  add a set of controls to for a set of files                             ##
##      ClearAll()  -  remove all widgets from each frame of the interface                   ##
##      ChangeColor()  -  allow the user to change the color of a set                        ##
##      AddFile()  -  let the user add a file to a set                                       ##
##      RemoveFile()  -  let the user remove a file from a set                               ##
##      UpdateData()  -  do the file parsing and update the results frame                    ##
##      DrawVenn()  -  draw a Venn diagram of the set information in the display frame       ##
##      set_to_text()  -  construct the text to display the information from an intersection ##
##      DisplayClick()  -  show set info in a popup when user clicks on Venn diagram         ##
##      ResframeClick()  -  show set info in a popup when user clicks on results frame set   ##
##      ToggleOutlines()  -  toggle the display of Venn diagram outlines (redraws diagram)   ##
##      ToggleLabels()  -  toggle the display of Venn diagram labels (redraws diagram)       ##
##      ToggleSortbySetnum()  -  set the sort criteria for hits to the set number            ##
##      ToggleSortbyScoreLow()  -  set the sort criteria to the search score (low to high)   ##
##      ToggleSortbyScoreHigh()  -  set the sort criteria to the search score (high to low)  ##
##      SetFilterCriteria()  -  set the filtering criteria for displaying hits               ##
##      ToggleWebBrowser()  -  toggle use of web browser in displaying hits                  ##
##      PrintDisplay()  -  print the current display                                         ##
##      SaveClusterings()  -  save the info from every set/intersection to a set of files    ##
##      SaveSession()  -  save the current settings to a file                                ##
##      OpenSession()  -  open a file of current settings and update the display             ##
##                                                                                           ##
###############################################################################################
###############################################################################################


###############################################################################################
#                                                                                             #
# class GUI                                                                                   #
#                                                                                             #
# This class represents the main application window, including display and controls.          #
#                                                                                             #
###############################################################################################

class GUI(Frame):

  def __init__(self):
    Frame.__init__(self)
    self.pack(expand=YES, fill = BOTH)
    self.master.title('Intersect')

    # set the os-specific stuff
    self.set_environ()
    
    # set the icon for the program (requires Tcl/Tk 8.3.3 (or newer)
    #self.master.iconname('Intersect')
    #self.master.iconbitmap()
    #filename = "C:\Program Files\Intersect\icon32.ico"
    #filename = "chimera32.ico"
    #top = self.winfo_toplevel()
    #print "top =", top
    #top.tk.call('wm', 'iconbitmap', top._w, "chimera32.ico")
    #print "current icon =", self.iconbitmap()
    
    self.master.geometry('+%d+%d' % (20, 0))
    self.master.resizable(0, 0)
    
    self.sessionfile = ''
    self.hits = {}      # table of Hit instances
    self.set_hits = {}  # list of Hit instances indexed by string of sets (eg. '1|2|3|')

    # somewhere in here we should be looking for a .intersect file to read setting from
    # set a default location for the web browser (should probably check according to os type)
    self.web_browser = "C:\Program Files\Internet Explorer\IEXPLORE.EXE"
    self.display_in_browser = 0
    self.html_tempfile = ""

    # a flag defining how to sort the hits (default is by search score, low to high)
    self.hit_sort_criteria = 'search score low'

    # criteria for filtering hits from the search files
    # (In the future, there should probably be a separate set of max and min for each type
    # of search file supported.)
    self.filter_pmax = 1.0
    self.filter_pmin = 0.0
    
    self.bind_all('<Control-KeyPress-n>', self.AddSet)
    self.bind_all('<Control-KeyPress-v>', self.DrawVenn)
    self.bind_all('<Control-KeyPress-u>', self.UpdateData)
    self.bind_all('<Control-KeyPress-p>', self.PrintDisplay)
    self.bind_all('<Control-KeyPress-o>', self.OpenSession)
    self.bind_all('<Control-KeyPress-s>', self.SaveSession)
    
    # menus 
    balloon = Pmw.Balloon(self)
    self.menubar = Pmw.MenuBar(self, balloon=balloon)
    self.menubar.grid(row=0, sticky=EW)
    self.menubar.addmenu('File', 'Open, Save, and Quit sessions')
    self.menubar.addmenuitem('File', 'command', 'Open a previous session', label='Open Session...   Ctrl+O', command=self.OpenSession)
    self.menubar.addmenuitem('File', 'command', 'Save current session', label='Save Session...   Ctrl+S', command=self.SaveSession)
    self.menubar.addmenuitem('File', 'separator')
    self.menubar.addmenuitem('File', 'command', 'Save clusterings', label='Save Clusterings...', command=self.SaveClusterings)
    self.menubar.addmenuitem('File', 'separator')
    self.menubar.addmenuitem('File', 'command', 'Exit the program', label='Exit', command=self.quit)
    self.menubar.addmenu('Data', 'Manipulate Data Sets')
    self.menubar.addmenuitem('Data', 'command', 'Add Set', label='Add Set    Ctrl+N', command=lambda s=self, x='': s.AddSet(x))
    self.menubar.addmenuitem('Data', 'separator')
    self.menubar.addmenuitem('Data', 'command', 'Update', label='Update     Ctrl+U', command=self.UpdateData)
    self.menubar.addmenuitem('Data', 'separator')
    self.menubar.addmenuitem('Data', 'command', 'Clear', label='Clear All', command=self.ClearAll)
    self.menubar.addmenu('Diagram', 'Update and change Venn diagram')
    self.menubar.addmenuitem('Diagram', 'command', 'Draw Venn diagram', label='Draw Venn diagram     Ctrl+V', command=self.DrawVenn)
    self.menubar.addmenuitem('Diagram', 'separator')
    self.menubar.addmenuitem('Diagram', 'checkbutton', 'Draw with black outlines', label='Show Outlines', command=self.ToggleOutlines)
    self.menubar.addmenuitem('Diagram', 'checkbutton', 'Draw with set labels', label='Show Set Labels', command=self.ToggleLabels)
    self.menubar.addmenuitem('Diagram', 'separator')
    self.menubar.addmenuitem('Diagram', 'command', 'Print Venn diagram', label='Print diagram               Ctrl+P', command=self.PrintDisplay)

    self.menubar.addmenu('Settings', 'Program Settings')
    self.menubar.addcascademenu('Settings', 'Sort hits by')
    self.menubar.addmenuitem('Sort hits by', 'radiobutton', 'Set number', label='Set number', command=self.ToggleSortbySetnum)
    self.menubar.addmenuitem('Sort hits by', 'radiobutton', 'Search score (low to high)', label='Search score (low to high)', command=self.ToggleSortbyScoreLow)
    self.menubar.addmenuitem('Sort hits by', 'radiobutton', 'Search score (high to low)', label='Search score (high to low)', command=self.ToggleSortbyScoreHigh)
    self.menubar.addmenuitem('Settings', 'command', 'Filter criteria...', label='Filter values...', command=self.SetFilterCriteria)
    self.menubar.addmenuitem('Settings', 'separator')
    self.menubar.addmenuitem('Settings', 'checkbutton', 'Use Web Browser', label='Use Web Browser', command=self.ToggleWebBrowser)
    self.menubar.component('Sort hits by-menu').invoke(1)

    # don't need this one on Windows
    if sys.platform != 'win32':
      self.menubar.addmenuitem('Settings', 'command', 'Set Web Browser', label='Set Web Browser', command=self.SetWebBrowser)
    
    # the display canvas
    self.display = Pmw.ScrolledCanvas(self, usehullsize=1, hull_width=480, hull_height=480)
    self.display.component('canvas').config(bg="white")
    self.display.grid(row=1, column=0, sticky=NSEW)
    
    # scrollable frame for listing sets on the right
    self.resframe = Pmw.ScrolledFrame(self, usehullsize=1, hull_width=500, hull_height=480)
    self.resframe.grid(row=1, column=1) 
    self.resframe_items = []  # hang onto this for later  
    self.result_frames = []  # these too
    
    # scrollable frame at the bottom to hold the set controls
    self.conframe = Pmw.ScrolledFrame(self, usehullsize=1, hull_width=980, hull_height=215)
    self.conframe.grid(row=2, columnspan=2, ipadx=2, ipady=2)
    self.conframe.bind('<KeyPress-space>', self.AddSet)
    self.conframe_items = []  # hang onto this for later
    
    # The controls for each set can be accessed via lists of important elements. These lists
    # grow or shrink via the AddSet and RemoveSet member functions.
    self.set_groupings = []
    self.set_names = []
    self.set_listboxes = []
    self.set_colorboxes = []
    self.set_active = []
    
    # true if clicking on display is activated
    self.display_active = 0
        
    # do not draw the outlines or labels by default
    self.draw_outlines = 0
    self.draw_labels = 0
    
    # A list of precomputed rectangle corners to make drawing the Venn diagram with squares faster
    # The list is indexed by the number of active sets (0th entry is empty), and each entry is of the 
    # form [x0, y0, x1, y1, set_key, ''], where set_key is a list specifying which major rectangles 
    # the given rectangle represents the intersection of, and the empty strings are used later to hold
    # the computed color and set_key hash. The order of rectangles is important because of painting 
    # overlaps!
    self.venn_coords = [[]] # set size = 0
    # set size = 1
    self.venn_coords.append([ [120, 120, 360, 360, [0], '', ''] ])
    # set size = 2
    self.venn_coords.append([ [120, 212, 360, 452, [0], '', ''], 
                              [120, 28, 360, 267, [1], '', ''], 
                              [120, 212, 360, 268, [0,1], '', ''] ])
    # set size = 3
    self.venn_coords.append([ [120, 190, 360, 430, [0], '', ''],
                              [200, 52, 440, 292, [1], '', ''],
                              [40, 52, 280, 292, [2], '', ''],
                              [200, 52, 280, 190, [1,2], '', ''],
                              [120, 190, 200, 292, [0,2], '', ''],
                              [200, 190, 280, 292, [0,1,2], '', ''],
                              [280, 190, 360, 292, [0,1], '', ''] ])
    # set size = 4
    self.venn_coords.append([ [160, 26, 320, 160, [0], '', ''],
                              [320, 160, 426, 275, [1], '', ''],
                              [214, 320, 426, 435, [2], '', ''],
                              [54, 142, 214, 461, [3], '', ''],
                              [106, 320, 214, 435, [2,3], '', ''],
                              [214, 160, 320, 275, [0,1], '', ''],
                              [214, 320, 320, 346, [0,2], '', ''],
                              [160, 142, 214, 160, [0,3], '', ''],
                              [320, 275, 426, 320, [1,2], '', ''],
                              [106, 160, 160, 275, [1,3], '', ''],
                              [214, 275, 320, 320, [0,1,2], '', ''],
                              [160, 160, 214, 275, [0,1,3], '', ''],
                              [160, 320, 214, 346, [0,2,3], '', ''],
                              [106, 275, 160, 320, [1,2,3], '', ''],
                              [160, 275, 214, 320, [0,1,2,3], '', ''] ])

    # A list of coordinates for putting labels, including how the label should be justified
    self.venn_labels = [ [] ]
    self.venn_labels.append([ [240, 240, CENTER] ])
    self.venn_labels.append([ [240, 360, CENTER], [240, 120, CENTER] ])
    self.venn_labels.append([ [240, 380, CENTER], [300, 116, W], [180, 116, E] ])
    self.venn_labels.append([ [240, 64, CENTER], [328, 200, W], [264, 384, W], [80, 448, W] ])


  #############################################################################################
  #                                                                                           #
  # set_environ()                                                                             #
  #                                                                                           #
  # Set the os-specific stuff (icons, environment variables, etc.)                            #
  #                                                                                           #
  #############################################################################################

  def set_environ(self):

    # for now let's just deal with Windows
    if sys.platform == 'win32':

      # check for the hackicon DLL
      found = 1
      try:
        import hackicon
      except ImportError:
        found = 0

      if found:
        # set the icon
        iconpath = os.getcwd()
        os.path.join(iconpath, 'intersect.ico')
        
    return


    
  #############################################################################################
  #                                                                                           #
  # AddSet()                                                                                  #
  #                                                                                           #
  # Add a set of control widgets to the control panel frame.                                  #
  #                                                                                           #
  #############################################################################################

  def AddSet(self, event):
    
    # get the current number of sets
    num = len(self.set_names)
    
    # add a raised grouping
    rg = Frame(self.conframe.interior(), relief=RAISED, bd=2)
    rg.grid(row=0, column=num, pady=2, padx=2, sticky=NS)
    self.conframe_items.append(rg)
    
    # add a label box
    rgn_label = Label(rg, text="Name")
    rgn_label.grid(row=0, column=0, sticky=W, padx=2, pady=2)
    rgn_name = Entry(rg)
    rgn_name.grid(row=0, column=1, sticky=EW, padx=2, pady=2)
    self.set_names.append(rgn_name)
    self.conframe_items.append(rgn_label)
    self.conframe_items.append(rgn_name)

    # add a color box
    # let's try choosing the color furthest from those already being used
    # first, make a list of all the current colors
    list = ["#000000", "#ffffff"]  # put in black and white so they don't get chosen
    for i in range(len(self.set_colorboxes)):
      c = self.set_colorboxes[i].cget('bg')
      list.append(c)
    color = furthest_color(list)
    
    color_button = Button(rg, text="    ")
    color_button.config(bg=color)
    self.set_colorboxes.append(color_button)
    self.set_colorboxes[num].config(command=lambda s=self, x=self.set_colorboxes[num]: s.ChangeColor(x))
    color_button.grid(row=1, column=0, padx=5, pady=3, sticky=W)
    self.conframe_items.append(color_button)
    
    # add an active checkbox
    var = BooleanVar()
    var.set(0)
    self.set_active.append(var)
    ac = Checkbutton(rg, text="active", variable=self.set_active[num])
    ac.select()
    ac.grid(row=1, column=1, padx=3, sticky=W)
    self.conframe_items.append(ac)
    
    # add a scrollable listbox
    fb = Pmw.Group(rg, tag_text="Files")
    fb.grid(row=2, column=0, columnspan=2, padx=2, pady=2)
    file_display = Pmw.ScrolledListBox(fb.interior())
    file_display.component('listbox').config(height=5)
    file_display.grid(row=0, column=0, rowspan=2)
    self.set_listboxes.append(file_display)
    self.conframe_items.append(fb)
    self.conframe_items.append(file_display)

    # add a button to add files
    add_file_button = Button(fb.interior(), text="Add", width=3, command=lambda s=self, x=self.set_listboxes[num]: s.AddFile(x))
    add_file_button.grid(row=0, column=1, padx=2)
    self.conframe_items.append(add_file_button)

    # add a button to remove files
    remove_file_button = Button(fb.interior(), text="Rem", width=3, command=lambda s=self, x=self.set_listboxes[num]: s.RemoveFile(x))
    remove_file_button.grid(row=1, column=1, padx=2)
    self.conframe_items.append(remove_file_button)

   
  #############################################################################################
  #                                                                                           #
  # ClearAll()                                                                                #
  #                                                                                           #
  # Remove all objects in all three frames.                                                   #
  #                                                                                           #
  #############################################################################################

  def ClearAll(self):
  
    # clear the display canvas
    items = self.display.interior().find_all()
    for i in range(len(items)):
      self.display.interior().delete(items[i])

    # clear the results frame
    for i in range(len(self.resframe_items)):
      self.resframe_items[i].destroy()

    # clear the control frame
    for i in range(len(self.conframe_items)):
      self.conframe_items[i].destroy()
    
    # empty global lists of widgets
    self.set_names = []
    self.set_listboxes = []
    self.set_colorboxes = []
    self.set_active = []
    self.resframe_items = []
    self.conframe_items = []
    
  
  #############################################################################################
  #                                                                                           #
  # ChangeColor()                                                                             #
  #                                                                                           #
  # Let the user choose a new color for the given set, then update the color and visible      #
  # color box.                                                                                #
  #                                                                                           #
  #############################################################################################

  def ChangeColor(self, button):
  
    current_color = button.cget('bg')
    x = ColorChooser(self, current_color)
    new_color = x.result
    if new_color != None:
      button.config(bg=new_color)

            
  #############################################################################################
  #                                                                                           #
  # AddFile()                                                                                 #
  #                                                                                           #
  # Let the user add a file to a set, updating the listbox                                    #
  #                                                                                           #
  #############################################################################################

  def AddFile(self, set_display):
  
    filename = tkFileDialog.askopenfilename()
    if not filename: return
        
    # add name to display
    current = set_display.get()
    new = current + tuple([filename])
    set_display.setlist(new)
    return 
  
  
  #############################################################################################
  #                                                                                           #
  # RemoveFile()                                                                              #
  #                                                                                           #
  # Let the user remove file(s) from a set, updating the listbox.                             #
  #                                                                                           #
  #############################################################################################

  def RemoveFile(self, set_display):
  
    items = set_display.component('listbox').curselection()
    for i in range(len(items)):
      set_display.component('listbox').delete(items[i])
    return

    
  #############################################################################################
  #                                                                                           #
  # UpdateData()                                                                              #
  #                                                                                           #
  # Update the data. This involves calling the file parsing functions to construct the        #
  # intersection data, and then displaying the results in the results frame.                  #
  #                                                                                           #
  #############################################################################################
  
  def UpdateData(self, event=None):

    # remove the stuff already being displayed in the results frame
    for i in range(len(self.resframe_items)):
      self.resframe_items[i].destroy()
    self.result_frames = []
 
    # empty the current hit list and set list
    self.hits = {}
    self.set_hits = {}

    # get the files and sets from each listbox
    files = [] # indexed by set-1
    for i in range(len(self.set_listboxes)):
      # if the set is not active, just append an empty list
      active = self.set_active[i].get()
      if not active:  files.append([])
      else:  files.append(self.set_listboxes[i].get())
    
    # get the total number of files
    total_files = 0
    for i in range(len(files)):
      total_files = total_files + len(files[i])
    if total_files == 0: return
    
    # for each set
    count = 0
    for i in range(len(files)):
      # for each file
      for j in range(len(files[i])):
      
        # check the format of the file
        res = check_file_type(files[i][j])
        if split(res)[0] == 'Error:':
          # there's no working_label yet
          #self.display.interior().delete(self.working_label)
          tkMessageBox.showerror('Error', res)
          return
        
        # send to correct parser
        if res == 'Wustl-BLAST': self.hits = parse_wustl_blast(self.hits, files[i][j], i+1, self.filter_pmax, self.filter_pmin)
        elif res == 'NCBI-BLAST': self.hits = parse_ncbi_blast(self.hits, files[i][j], i+1, self.filter_pmax, self.filter_pmin)
        elif res == 'FASTA': self.hits = parse_fasta(self.hits, files[i][j], i+1, self.filter_pmax, self.filter_pmin)
        elif res == 'PSI-BLAST': self.hits = parse_psiblast(self.hits, files[i][j], i+1, self.filter_pmax, self.filter_pmin)

        count = count + 1
                        
    # build the list of Hit instances indexed by the groups of sets which found them
    t = self.hits.keys()
    for i in range(len(t)):
      g = self.hits[t[i]].sets_found
      # cannot hash on a list, so turn it into a string
      # but now order is important, so sort the list
      g.sort()
      group = ''
      for j in range(len(g)):
        group = group + '%d|' % g[j]
      try:
        list = self.set_hits[group]
      except KeyError:
        list = []
      list.append(self.hits[t[i]])
      self.set_hits[group] = list
          
    # sort the sets of hits by the number of sets found and alphanumerically    
    sets = self.set_hits.keys()
    sets = sort_hit_keys(sets)
    
    # put each set into a frame
    for i in range(len(sets)):
      g = Frame(self.resframe.interior(), borderwidth=2, relief=RAISED)
      g.grid(row=i, column=0, padx=2, pady=2, sticky=EW)
      g.grid_columnconfigure(0, minsize=360)
      self.resframe_items.append(g)
      g.bind('<Button-1>', self.ResframeClick)

      self.result_frames.append([g, sets[i]])
      
      # make a clickable label for the set
      limit = 50
      nums = splitfields(sets[i], '|')[:-1]
      text = ""
      for j in range(len(nums)):
        index = atoi(nums[j]) - 1
        t = self.set_names[index].get()
        if j != 0:  text = text + ", " + t
        else: text = t
      if len(text) > limit: text = text[:limit]
      l = Label(g, text=text, font=('Courier', 8, 'normal'), anchor=W)
      l.grid(row=0, column=0, sticky=W)
      self.resframe_items.append(l)
      l.bind('<Button-1>', self.ResframeClick)
      
      # make colored boxes for next to the label
      if i == 0: maxpos = len(nums)
      pos = maxpos
      for j in range(maxpos):
        if j < len(nums): 
          index = atoi(nums[j]) - 1
          # get the color from the set information
          color = self.set_colorboxes[index].cget('bg')
          b = Canvas(g, height=10, width=10, bg=color)
        else:
          b = Canvas(g, height=10, width=10)
        b.grid(row=0, column=pos, padx=0, pady=0, sticky=E)
        pos = pos - 1
        self.resframe_items.append(b)
        b.bind('<Button-1>', self.ResframeClick)

    
  #############################################################################################
  #                                                                                           #
  # DrawVenn()                                                                                #
  #                                                                                           #
  # Draw a Venn diagram of the current data (data is not updated when this function is        #
  # called). If more than 4 sets are active, a message to the user is displayed instead.      #
  #                                                                                           #
  #############################################################################################
  
  def DrawVenn(self, event=None):
     
    # figure out which sets are active
    actives = []
    for i in range(len(self.set_active)):
      if self.set_active[i].get(): actives.append(i)
      
    # check if less than five
    if len(actives) > 4:
      # tell user there are too many sets
      self.display.interior().create_text(200, 200, anchor=CENTER, text="Too many active sets\n  for Venn diagram", font=('Verdana', 16))
      return
      
    else: 
      # make sure we are not keeping bad bindings
      self.display.interior().unbind('<Button-1>')
    
      # get the list of precomputed rectangle corners
      ps = self.venn_coords[len(actives)]
    
      # add the correct color to the list
      for i in range(len(ps)):
        set_list = [] # list of sets represented by current region
        for j in range(len(ps[i][4])):
          set_list.append(actives[ps[i][4][j]])
        color = ''
      
        # check if set intersection is empty
        set_key = ''
        for j in range(len(set_list)):
          set_key = set_key + '%d|' % (set_list[j] + 1)
        try:
          hits = self.set_hits[set_key]
        except KeyError:
          color = '#000000'
      
        # if not, then determine the correct color
        if color != '#000000':
          color = '#000000'
          for j in range(len(set_list)):
            set_color = self.set_colorboxes[set_list[j]].cget('bg')
            color = add_colors(color, set_color)
        
        ps[i][5] = color
        ps[i][6] = set_key
      
      # clear the canvas
      items = self.display.interior().find_all()
      for i in range(len(items)):
        self.display.interior().delete(items[i])
     
      # draw the rectangles
      self.rectangles = []
      for i in range(len(ps)):
        x = self.display.interior().create_rectangle(ps[i][0], ps[i][1], ps[i][2], ps[i][3], fill=ps[i][5], outline="")
      
        # bind each rectangle to the DisplayClick callback
        self.display.interior().tag_bind(x, '<Button-1>', lambda event, a=self.DisplayClick, b=x:a(b))
      
        # keep track of which rectangles go with which intersections
        self.rectangles.append([x, ps[i][6]])
            
      # draw the outlines of the regions in black
      if self.draw_outlines:
        for i in range(len(ps)):
          self.display.interior().create_rectangle(ps[i][0], ps[i][1], ps[i][2], ps[i][3])

      # draw the lables 
      if self.draw_labels:
        lc = self.venn_labels[len(actives)]
        for i in range(len(actives)):
          name = strip(self.set_names[actives[i]].get())
          self.display.interior().create_text(lc[i][0], lc[i][1], anchor=lc[i][2], text=name, font=('Verdana', 12))


      # activate the ability to click on the display
      self.display_active = 1
 
     
  #############################################################################################
  #                                                                                           #
  # sort_hits()                                                                               #
  #                                                                                           #
  # Sort the hits by shotgun score, and within each shotgun score sort according to the       #
  # sorting criteria.                                                                         #
  #                                                                                           #
  #############################################################################################
  
  def sort_hits(self, hits):

    # bin the hits by shotgun_score
    table = {}
    for i in range(len(hits)):
      sc = hits[i].shotgun_score
      try:
        l = table[sc]
      except KeyError:
        table[sc] = []
      table[sc].append(hits[i])
    keys = table.keys()
    keys.sort()
    keys.reverse()

    # within each hit, sort according to the sort criteria chosen by the user
    if self.hit_sort_criteria == 'search score low':
      # sort by probability value so I can easily find the lowest one
      new = []
      for i in range(len(keys)):
        l = table[keys[i]]
        for j in range(len(l)):  # for each hit at this shotgun score
          l[j].files_found.sort(lambda x,y: cmp(atof(split(x[2])[0]), atof(split(y[2])[0])))
        # now sort each bin by the lowest probability score
        l.sort(lambda x,y: cmp(atof(split(x.files_found[0][2])[0]), atof(split(y.files_found[0][2])[0])))
        # now rebuild the hit list in this order
        for j in range(len(l)):
          new.append(l[j])
      hits = new        

    elif self.hit_sort_criteria == 'search score high':
      new = []
      for i in range(len(keys)):
        l = table[keys[i]]
        for j in range(len(l)):
          l[j].files_found.sort(lambda x,y: cmp(atof(split(x[2])[0]), atof(split(y[2])[0])))
          l[j].files_found.reverse()
        # now sort each bin by the highest probability score
        l.sort(lambda x,y: cmp(atof(split(x.files_found[0][2])[0]), atof(split(y.files_found[0][2])[0])))
        l.reverse()
        # now rebuild the hit list in this order
        for j in range(len(l)):
          new.append(l[j])
      hits = new        

    elif self.hit_sort_criteria == 'set number':
      new = []
      for i in range(len(keys)):
        l = table[keys[i]]
        for j in range(len(l)):
          l[j].files_found.sort(lambda x,y: cmp(atof(split(x[2])[0]), atof(split(y[2])[0])))
        # now sort each bin by the lowest probability score
        l.sort(lambda x,y: cmp(atof(split(x.files_found[0][2])[0]), atof(split(y.files_found[0][2])[0])))
        # now rebuild the hit list in this order
        for j in range(len(l)):
          new.append(l[j])
      hits = new        
      
      # group the files for each hit by ascending set number
      for i in range(len(hits)):
        hits[i].files_found.sort(lambda x,y: cmp(x[0], y[0]))

    return hits
  

  #############################################################################################
  #                                                                                           #
  # set_to_text()                                                                             #
  #                                                                                           #
  # Given a set key to index into the class set information table, create a nice-looking      #
  # text string (to be displayed or saved later) of the Hit information for the given         #
  # intersection.                                                                             #
  #                                                                                           #
  #############################################################################################
  
  def set_to_text(self, set_key):
    
    # get the hits
    try:
      hits = self.set_hits[set_key]
    except KeyError:
      return None
        
    # break apart the key for display
    sets = []
    t = splitfields(set_key, '|')[:-1]
    set_text = "Hits for the intersection of sets: \n\n"
    # use the set labels if present
    for i in range(len(t)):
      set_index = atoi(t[i]) - 1
      label = self.set_names[set_index].get()
      if label != '':
        set_text = set_text + "     %s: %s\n" % (t[i], label)
      else: set_text = set_text + "     %s: (no label)\n" % t[i]
    set_text = set_text + '\n'
    
    hits = self.sort_hits(hits)
            
    # make the text for them
    hit_text = ''
    for i in range(len(hits)):
      
      # only write out 62 characters per line (to make printable on a standard page width)  
      hit_text = hit_text + "\n%-20s %-30s  Score: %2s\n" % (hits[i].name[:20], hits[i].description[:30], hits[i].shotgun_score)
      hit_text = hit_text + "--Query File----------------------------------------------Prob\n"
      for j in range(len(hits[i].files_found)):
        # crop the left side of long filenames
        filename = "%s: %s" % (hits[i].files_found[j][0], hits[i].files_found[j][1])
        if len(filename) > 50:
          end = len(filename) - 1
          filename = "%s:...%s" % (hits[i].files_found[j][0], filename[(end-45):end])
        hit_text = hit_text + "%-50s %11s\n" % (filename, hits[i].files_found[j][2])

    return set_text + hit_text
    
  
  #############################################################################################
  #                                                                                           #
  # CreateHTMLView()                                                                          #
  #                                                                                           #
  # Create the html to be displayed in the web browser. This should have links to the GenBank #
  # URLs for the hits.                                                                        #
  #                                                                                           #
  #############################################################################################

  def CreateHTMLView(self, set_key):

    # get the hits
    try:
      hits = self.set_hits[set_key]
    except KeyError:
      return None
        
    # break apart the key for display
    sets = []
    t = splitfields(set_key, '|')[:-1]
    set_html = "<h3>Hits for the intersection of sets: </h3><p>"
    # use the set labels if present
    for i in range(len(t)):
      set_index = atoi(t[i]) - 1
      label = self.set_names[set_index].get()
      if label != '':
        padding = '&nbsp' * 6
        set_html = set_html + padding + "%s: %s<br>\n" % (t[i], label)
      else: set_html = set_html + padding + padding + "%s: (no label)<br>\n" % t[i]
    set_html = set_html + '<br><br>\n'
    
    hits = self.sort_hits(hits)

    hits_html = '<table>\n'

    # make the text for each hit
    for i in range(len(hits)):
      hit_html = ''
      # check if the hit is a gi entry
      name = hits[i].name
      desc = hits[i].description
      score = hits[i].shotgun_score
      if name[0:2] == 'gi':
        ginum = splitfields(name, '|')[1]
        hit_html = '<tr><td><a href=http://www.ncbi.nlm.nih.gov/entrez/query.fcgi?cmd=Retrieve&db=protein&list_uids=%s&dopt=GenPept> %s %s </a></td> <td> Score: %s </td></tr>\n' % (ginum, name, desc, score)
      else:
        hit_html = '<tr><td>%s %s </td> <td> Score: %s </td></tr>\n' % (name, desc, score)
      hit_html = hit_html + '<tr><td bgcolor="#888888"> --Query File----------------------------------------------</td><td bgcolor="#888888">Prob </td></tr>\n'
      for j in range(len(hits[i].files_found)):
        # crop the left side of long filenames
        filename = "%s: %s" % (hits[i].files_found[j][0], hits[i].files_found[j][1])
        if len(filename) > 50:
          end = len(filename) - 1
          filename = "%s:...%s" % (hits[i].files_found[j][0], filename[(end-45):end])
        #padding = (50 - len(filename)) * '&nbsp'
        #hit_html = hit_html + filename + padding
        hit_html = hit_html + '<tr><td>%s </td> <td>%s</td></tr>\n' % (filename, hits[i].files_found[j][2])
        #padding = (12 - len(hits[i].files_found[j][2])) * '&nbsp'
        #hit_html = hit_html + hits[i].files_found[j][2] + padding + '<br>\n'
        # a blank row
      hit_html = hit_html + '<tr><td> &nbsp </td></tr>\n'
      hits_html = hits_html + hit_html

    hits_html = hits_html + '</table>'
    html = '<html><tt>' + set_html + hits_html + '</tt></html>'
    
    # need the complete path to the temp file
    dir = os.getcwd()
    self.html_tempfile = dir + '\\int_temp.htm'
    tout = open(self.html_tempfile, "w")
    tout.write(html)
    tout.close()


  #############################################################################################
  #                                                                                           #
  # DisplayClick()                                                                            #
  #                                                                                           #
  # When a user clicks on a set in the Venn diagram composed of rectangles, this function is  #
  # called to display the data for the intersection represented by that rectangle object.     #
  #                                                                                           #
  #############################################################################################
  
  def DisplayClick(self, canvas_item):
  
    if self.display_active:
      # find which rectangle was clicked on
      for i in range(len(self.rectangles)):
        if self.rectangles[i][0] == canvas_item:
          # get the index into the set information table
          set_key = self.rectangles[i][1]
          break
                  
      # determine how the text should be displayed
      if self.display_in_browser:
        self.CreateHTMLView(set_key)
          
        if sys.platform == 'win32':  # only on Windows
          # this assumes .htm files already associated with a browser
          os.startfile(self.html_tempfile)  

        #else:  # fix this for unix, (mac?)
          # try to call up the web browser using default setting
          # if failed, have use locate web brower, write to .intersect file
          #cmd = '""' + self.web_browser + '""' + ' ' + self.html_tempfile
          #res = os.spawnl(os.P_WAIT, self.web_browser, self.html_tempfile)
          #if res == 1: # there's probably a better way to do this
            #tkMessageBox.showerror('Error', "Web browser not found. Please locate it.")
            #self.web_browser = tkFileDialog.askopenfilename()
            
      else:
        text = self.set_to_text(set_key)
        TextDisplay(self, text)      
     
     
  #############################################################################################
  #                                                                                           #
  # ResframeClick()                                                                           #
  #                                                                                           #
  # When a user clicks on a frame in the result frame, this function is called to display the #
  # data for the intersection represented by that frame.                                      #
  #                                                                                           #
  #############################################################################################
  
  def ResframeClick(self, event):
  
    # get the set key for the frame which was clicked on
    set_key = None
    for i in range(len(self.result_frames)):
      if self.result_frames[i][0] == event.widget:
        set_key = self.result_frames[i][1]
        break
    
    # was a child of the frame that was clicked on
    if not set_key:
      x = event.widget.master
      for i in range(len(self.result_frames)):
        if self.result_frames[i][0] == x:
          set_key = self.result_frames[i][1]
          break
      
    # error if cannot find frame
    if not set_key:
      print "Error: could not find the frame clicked on"
      return
                                                            
    # determine how the text should be displayed
    if self.display_in_browser:
      self.CreateHTMLView(set_key)
          
      if sys.platform == 'win32':  # only on Windows
        # this assumes .htm files already associated with a browser
        os.startfile(self.html_tempfile)  

      #else:  # fix this for unix, (mac?)
        # try to call up the web browser using default setting
        # if failed, have use locate web brower, write to .intersect file
        #cmd = '""' + self.web_browser + '""' + ' ' + self.html_tempfile
        #res = os.spawnl(os.P_WAIT, self.web_browser, self.html_tempfile)
        #if res == 1: # there's probably a better way to do this
          #tkMessageBox.showerror('Error', "Web browser not found. Please locate it.")
          #self.web_browser = tkFileDialog.askopenfilename()
            
    else:
      text = self.set_to_text(set_key)
      TextDisplay(self, text)      

     
  #############################################################################################
  #                                                                                           #
  # ToggleOutlines()                                                                          #
  #                                                                                           #
  # Toggle the drawing of outlines on the Venn diagram. The diagram is redrawn via a call to  #
  # DrawVenn in this function.                                                                #
  #                                                                                           #
  #############################################################################################
  
  def ToggleOutlines(self):
     
    if self.draw_outlines == 0: self.draw_outlines = 1
    else: self.draw_outlines = 0
    self.DrawVenn()
     
        
  #############################################################################################
  #                                                                                           #
  # ToggleLabels()                                                                            #
  #                                                                                           #
  # Toggle the drawing of set lables on the Venn diagram. The diagram is redrawn via a call   #
  # to DrawVenn in this function.                                                             #
  #                                                                                           #
  #############################################################################################
  
  def ToggleLabels(self):
     
    if self.draw_labels == 0: self.draw_labels = 1
    else: self.draw_labels = 0
    self.DrawVenn()


  #############################################################################################
  #                                                                                           #
  # ToggleSortbySetnum()                                                                      #
  #                                                                                           #
  # Set the sorting critera for hits to be the set number.                                    #
  #                                                                                           #
  #############################################################################################

  def ToggleSortbySetnum(self):

    self.hit_sort_criteria = 'set number'

    
  #############################################################################################
  #                                                                                           #
  # ToggleSortbyScoreLow()                                                                    #
  #                                                                                           #
  # Set the sorting critera for hits to be the search score, from lowest to highest           #
  #                                                                                           #
  #############################################################################################

  def ToggleSortbyScoreLow(self):

    self.hit_sort_criteria = 'search score low'

    
  #############################################################################################
  #                                                                                           #
  # ToggleSortbyScoreHigh()                                                                   #
  #                                                                                           #
  # Set the sorting critera for hits to be the search score, from highest to lowest           #
  #                                                                                           #
  #############################################################################################

  def ToggleSortbyScoreHigh(self):

    self.hit_sort_criteria = 'search score high'


  #############################################################################################
  #                                                                                           #
  # SetFilterCriteria(self)                                                                   #
  #                                                                                           #
  # Set the values for filtering hits.                                                        #
  #                                                                                           #
  #############################################################################################

  def SetFilterCriteria(self):

    res = FilterDialog(self.master, self.filter_pmax, self.filter_pmin)
    if res.result == None: return
    self.filter_pmax = atof(res.result[0])
    self.filter_pmin = atof(res.result[1])

    
  #############################################################################################
  #                                                                                           #
  # ToggleWebBrowser()                                                                        #
  #                                                                                           #
  # Toggle the use of a web browser in the display of set information.                        #
  #                                                                                           #
  #############################################################################################
  
  def ToggleWebBrowser(self):
     
    if self.display_in_browser == 0: self.display_in_browser = 1
    else: self.display_in_browser = 0
  
                
  #############################################################################################
  #                                                                                           #
  # SetWebBrowser()                                                                           #
  #                                                                                           #
  # Bring up a file dialog which lets the user choose the web browser to use.                 #
  #                                                                                           #
  #############################################################################################
  
  def SetWebBrowser(self):
     
    filename = tkFileDialog.askopenfilename()
    self.web_browser = filename
    print "self.web_browser =", self.web_browser
  
                
  #############################################################################################
  #                                                                                           #
  # PrintDisplay()                                                                            #
  #                                                                                           #
  # Print the current display out to a file chosen by the user. Currently this will not print #
  # directly to a print spooler, and can only print to a file in Postscript format (the only  #
  # format provided by the Tkinter Canvas object).                                            #
  #                                                                                           #
  #############################################################################################
  
  def PrintDisplay(self, event=None):
    filename = tkFileDialog.asksaveasfilename()
    if filename == None: return
    self.display.interior().postscript(file=filename)  
  

  #############################################################################################
  #                                                                                           #
  # SaveClusterings()                                                                         #
  #                                                                                           #
  # Write all of the Hit clusterings (sets and intersections) in a nice-looking format to a   #
  # set of files. The user chooses a base filename to which numbers representing the cluster  #
  # are appended (eg. "out" -> "out-1", "out-1-3"). If a cluster is empty, then the file for  #
  # that cluster is not written.                                                              #
  #                                                                                           #
  #############################################################################################
  
  def SaveClusterings(self):
    basename = tkFileDialog.asksaveasfilename()
    if basename == None: return
    
    top_text = "Output from Buckshot 1.0\n\n"
    
    clusters = self.set_hits.keys()
    # for each clustering
    for i in range(len(clusters)):
      temp = "File %d of %d\n\n" % ((i+1), len(clusters))
      text = top_text + temp
      # make a new filename
      t = splitfields(clusters[i], '|')[:-1]
      tag = ''
      for j in range(len(t)):
        tag = tag + '-%s' % t[j]
      filename = basename + tag
      # make text using set_to_text
      body = self.set_to_text(clusters[i])
      text = text + body
      # write text to filename  
      out = open(filename, 'w')
      out.write(text)
      out.close()
      
   
  #############################################################################################
  #                                                                                           #
  # SaveSession()                                                                             #
  #                                                                                           #
  # Save the current session settings, including filenames to a file named by the user. The   #
  # format of the file is designed to be easily read (or created) by users:                   #
  #                                                                                           #
  # filename - Date and Time                                                                  #
  #                                                                                           #
  # Set 1  name  color                                                                        #
  # file1                                                                                     #
  # file2                                                                                     #
  #  ...                                                                                      #
  # file3                                                                                     #
  #                                                                                           #
  # Set 2  name  color                                                                        #
  #  ...                                                                                      #
  #                                                                                           #
  #############################################################################################
  
  def SaveSession(self, event=None):
  
    # get the filename
    filename = tkFileDialog.asksaveasfilename()
    if filename == None: return
    
    # write it and the date/time
    l = filename + " - " + asctime() + "\n\n"
        
    # Get the information for each set
    for i in range(len(self.set_listboxes)):
      
      files = self.set_listboxes[i].get()
      if files != ():
        name = self.set_names[i].get()
        color = self.set_colorboxes[i].cget('bg')
        l = l + "Set %d  %s  %s\n" % ((i+1), name, color)
        for j in range(len(files)):
          l = l + files[j] + '\n'
        l = l + '\n'
      
    out = open(filename, 'w')
    out.write(l)
    out.close()    
  
  
  #############################################################################################
  #                                                                                           #
  # OpenSession()                                                                             #
  #                                                                                           #
  # Open a session file (chosen by the user), and update the display. The format of the       #
  # session file is given in the comments above SaveSession().                                #
  #                                                                                           #
  #############################################################################################
  
  def OpenSession(self, event=None):
  
    # get the filename
    filename = tkFileDialog.askopenfilename()
    if not filename: return
  
    # attempt to open the file
    try:
      l = open(filename, 'r').readlines()
    except IOError:
      tkMessageBox.showerror("Error", "Could not open file: %s" % filename)
      return
      
    self.ClearAll()
    
    i = 1 # skip the first line
    # read to the next non-blank line
    while strip(l[i]) == '': i = i + 1
    
    # this line either has display info, or is the first set
    do_update = 1
    t = split(l[i])    
    if t[0] != "Display": do_update = 0
    else:   
      rest = t[1:]
      for j in range(len(rest)):
        if lower(rest[j]) == "ovals" or lower(rest[j]) == "oval":
          self.draw_ovals = 1
          self.draw_rectangles = 0
        if lower(rest[j]) == "outlines" or lower(rest[j]) == "outline": self.draw_outlines = 1
      # read to the next non-blank line
      i = i + 1
      while strip(l[i]) == '': i = i + 1
  
    while i < len(l):
    
      # get set info
      t = split(l[i])
      set_num = atoi(t[1]) - 1
      color = t[-1]
      name = join(t[2:-1])

      # add a set of controls
      # Note: This assumes the sets are listed in order--should probably be fixed by checking the
      #       total number of sets first and then adding controls outside this loop.
      self.AddSet(None)

      # set the color
      self.set_colorboxes[set_num].config(bg=color)
      
      # set the name    
      temp = self.set_names[set_num].get()
      index = len(temp)
      self.set_names[set_num].delete(0, index)
      self.set_names[set_num].insert(0, name)
      
      # remove the current files
      num_files = self.set_listboxes[set_num].size()
      self.set_listboxes[set_num].component('listbox').delete(0, num_files)
      
      # now get the new files
      files = []
      i = i + 1
      while strip(l[i]) != '':
        files.append(strip(l[i]))
        i = i + 1
      self.set_listboxes[set_num].setlist(files)
  
      # skip to the next non-blank line
      while i < len(l) and strip(l[i]) == '': i = i + 1

      
  
###############################################################################################
###############################################################################################
##                                                                                           ##
## Just a bit of stuff to make this file runnable.                                           ##
##                                                                                           ##
###############################################################################################
###############################################################################################

if __name__ == '__main__':
  GUI().mainloop()
