Index: /misc/tools/plotcsv/plotcsv.py
===================================================================
--- /misc/tools/plotcsv/plotcsv.py	(revision 6343)
+++ /misc/tools/plotcsv/plotcsv.py	(revision 6344)
@@ -14,4 +14,8 @@
 import getopt
 import pylab
+try:
+    import cpickle
+except:
+    import pickle
 
 try:
@@ -93,4 +97,17 @@
 MinimumHeight = ''
 MaximumHeight = ''
+
+# Flag strings - keys in the 'options' dictionary
+X_DATACOL = 'x_datacol'
+Y_DATACOL = 'y_datacol'
+X_RANGE = 'x_range'
+Y_RANGE = 'y_range'
+FILENAME = 'filename'
+SIZE = 'size'
+TITLE = 'title'
+X_LABEL = 'x_label'
+Y_LABEL = 'y_label'
+
+CSVWildcard = 'CSV files (*.csv)|*.csv|All files (*.*)|*.*'
 
 # Flag strings - keys in the 'options' dictionary
@@ -130,23 +147,121 @@
 
 
-##
-# @brief Issue an error message.
-# @param msg The message.
-def error(msg):
-    print >>sys.stderr, msg
-    sys.exit(10)
-
- 
+################################################################################
+# An object that behaves like a dictionary but is pickled/unpickled from a file.
+################################################################################
+
+class Config(object):
+    """
+    An object that behaves like a dictionary but is persistent:
+
+        cfg = Config('filename')
+        cfg['save'] = 'A saved value'
+        cfg[123] = 'value 123'
+        print cfg['save']
+
+    The values stored in the file 'filename' will be available to any
+    application using that config file.
+    """
+
+    def __init__(self, configfile=None):
+        """
+        __init__(self, String filename=None) -> Config
+        """
+
+        self.delconf = False
+        self.cfgdict = {}
+        self.changed = False
+        if not configfile:
+            self.delconf = True
+            configfile =tempfile.mktemp()
+        self.configfile = os.path.abspath(configfile)
+        if os.path.exists(self.configfile):
+            try:
+                f = open(configfile, "r")
+                u = pickle.Unpickler(f)
+                self.cfgdict = u.load()
+            except pickle.UnpicklingError, e:
+                print e
+            except:
+                pass
+            else:
+                f.close()
+
+    def __setitem__(self, key, value):
+        """
+        Override to allow: cfg[<key>] = <value>
+        """
+        self.cfgdict[key] = value
+        self.changed = True
+
+    def __getitem__(self, key):
+        """
+        Override to allow: <var> = cfg[<key>]
+        """
+        return self.cfgdict.get(key, None)
+
+    def __str__(self):
+        """
+        __str__(self) -> String
+        """
+        return "<config object at %s>" % hex(id(self))
+
+    def getfilename(self):
+        """
+        getfilename(self) -> String filename
+        """
+        return self.configfile
+
+    def setdeleted(self):
+        """
+        setdeleted(self)
+        """
+        self.delconf = True
+
+    def save(self):
+        """
+        save(self)
+        """
+        try:
+            f = open(self.configfile, "w")
+            p = pickle.Pickler(f)
+            p.dump(self.cfgdict)
+        except pickle.PicklingError, e:
+            print e
+        else:
+            f.close()
+        self.changed = False
+
+    def close(self):
+        """
+        close(self)
+        """
+        if self.changed:
+            self.save()
+        if self.delconf:
+            if os.path.exists(self.configfile):
+                os.remove(self.configfile)
+
+    def __del__(self):
+        """
+        __del__(self)
+        """
+        self.close()
+
+################################################################################
+# Plot routines
+################################################################################
+
 ##
 # @brief Plot a sequence of data.
 # @param x_data The X data sequence to plot.
-# @param y_data A list of one or more Y data sequences to plot.
+# @param y_data The Y data sequence to plot.
 # @param options A dictionary of plot options.
 def plot_data(x_data, y_data, options):
+    print 'x_data=%s' % str(x_data)
+    print 'y_data=%s' % str(y_data)
     pylab.plot(x_data, y_data)
-    pylab.title(options.get(TITLE, ''))
-    pylab.grid(True)
-
-    # if user request a particular Y range
+
+    # if user requested a particular Y range
     if not options[Y_RANGE] is None:
         try:
@@ -158,25 +273,12 @@
         pylab.ylim(ymin=minimum, ymax=maximum)
 
-    pylab.xlabel(options.get(X_LABEL, ''))
-    pylab.ylabel(options.get(Y_LABEL, ''))
-
-    pylab.show()
-
 
 ##
-# @brief Plot a data file.
+# @brief Get CSV data from a file.
 # @param filename Path to the data file to plot.
+# @param x_hdr The X axis title string (or index).
+# @param y_hdr The Y axis title string (or index).
 # @param options A dictionary of options.
-def plot_file(filename, options=None):
-    # set options defaults
-    opts = options
-    if opts is None:
-        opts = {}
-
-    opts[X_DATACOL] = opts.get(X_DATACOL, '0')
-    opts[Y_DATACOL] = opts.get(Y_DATACOL, '1')
-    opts[SIZE] = opts.get(SIZE, '800,600')
-    opts[Y_RANGE] = opts.get(Y_RANGE, None)
-
+def getCSVData(filename, x_hdr, y_hdr, options):
     # get contents of data file
     # after this, 'header' is list of column header strings
@@ -189,28 +291,28 @@
     fd.close()
     header = data[0]
-    del data[0]
+    del data[0]         # get rid of header in dataset
 
     # convert column specifiers to 'int' if required
     try:
-        index = int(opts[X_DATACOL])
+        index = int(x_hdr)
     except:
         try:
-            index = header.index(opts[X_DATACOL])
+            index = header.index(x_hdr)
         except ValueError:
-            error("Sorry, X column header '%s' isn't in the data file." % opts[X_DATACOL])
-    opts[X_DATACOL] = index
+            error("Sorry, X column header '%s' isn't in the data file." % x_hdr)
+    options[X_DATACOL] = index
 
     try:
-        index = int(opts[Y_DATACOL])
+        index = int(y_hdr)
     except:
         try:
-            index = header.index(opts[Y_DATACOL])
+            index = header.index(y_hdr)
         except ValueError:
-            error("Sorry, Y column header '%s' isn't in the data file." % opts[Y_DATACOL])
-    opts[Y_DATACOL] = index
+            error("Sorry, Y column header '%s' isn't in the data file." % y_hdr)
+    options[Y_DATACOL] = index
     
-    # extract required columns from the data
-    x_col = opts[X_DATACOL]
-    y_col = opts[Y_DATACOL]
+    # extract required columns from the data (int at this point)
+    x_col = options[X_DATACOL]
+    y_col = options[Y_DATACOL]
 
     # get max column number, check requested columns
@@ -227,9 +329,32 @@
     y_label = header[y_col].title()
 
-    opts[TITLE] = 'File: %s' % filename
-    opts[X_LABEL] = x_label
-    opts[Y_LABEL] = y_label
-
-    plot_data(x_data, y_data, opts)
+    options[X_LABEL] = x_label
+    options[Y_LABEL] = y_label
+
+    return (x_data, y_data)
+
+
+##
+# @brief Plot data files.
+# @param filenames List of full pathnames to plot.
+# @param options A dictionary of options.
+def plot_files(filenames, options=None):
+    # set options defaults
+    opts = options
+    if opts is None:
+        opts = {}
+
+##    pylab.title(options.get(TITLE, ''))
+##    pylab.xlabel(options.get(X_LABEL, ''))
+##    pylab.ylabel(options.get(Y_LABEL, ''))
+##    pylab.grid(True)
+##
+    for f in filenames:
+        (x_data, y_data) = getCSVData(f, opts[X_LABEL], opts[Y_LABEL], options)
+        print 'x_data=%s' % str(x_data)
+        print 'y_data=%s' % str(y_data)
+        pylab.plot(x_data, y_data)
+
+    pylab.show()
 
 
@@ -258,20 +383,9 @@
         wx.StaticBox(p, -1, 'CSV files', (3, Y_OFFSET),
                      size=(BOX_CSV_WIDTH, BOX_CSV_HEIGHT))
-
-##        wx.StaticText(p, -1, 'Minimum wave height',
-##                      pos=(GEN_LABELXOFFSET, Y_OFFSET),
-##                      size=(CTL_WIDTH, CTL_HEIGHT), style=wx.ALIGN_RIGHT)
-##        self.txtMinH = wx.TextCtrl(p, -1, size=(100, 20),
-##                                   pos=(GEN_TEXTXOFFSET, Y_OFFSET))
         Y_OFFSET += GEN_DELTAY 
-
-##        self.lstGenerate = wx.ListCtrl(p, -1, pos=(8, Y_OFFSET),
-##                                       size=(TEXTLIST_WIDTH,TEXTLIST_HEIGHT),
-##                                       style=wx.LC_SINGLE_SEL)
-##        self.txtCSVFiles = wx.TextCtrl(p, -1,
-##                        "Here is a looooooooooooooong line of text set in the control.\n\n"
-##                        "The quick brown fox jumped over the lazy dog...",
-##                       pos=(8, Y_OFFSET), size=(TXT_CSVFILE_WIDTH,TXT_CSVFILE_HEIGHT), style=wx.TE_MULTILINE|wx.TE_PROCESS_ENTER)
-        self.txtCSVFiles = wx.ListBox(p, -1, pos=(8, Y_OFFSET), size=(TXT_CSVFILE_WIDTH,TXT_CSVFILE_HEIGHT))
+        self.txtCSVFiles = wx.ListBox(p, -1, pos=(8, Y_OFFSET),
+                                      size=(TXT_CSVFILE_WIDTH,
+                                            TXT_CSVFILE_HEIGHT))
+        self.CSVFiles = []
         Y_OFFSET += TXT_CSVFILE_HEIGHT + MARGIN 
         x = FORM_WIDTH/2 - BUTTON_WIDTH - DOUBLE_BUTTON_OFFSET
@@ -288,131 +402,189 @@
         wx.StaticText(p, -1, 'X-Column',
                       pos=(COLLAB_X_OFFSET, Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT)
-        self.cbXColHdr = wx.ComboBox(p, -1, pos=(COLLAB_X_OFFSET+50, Y_OFFSET),
-                                     size=(80, -1), style=wx.CB_DROPDOWN)
+        self.cbXColHdr = wx.Choice(p, -1, pos=(COLLAB_X_OFFSET+50, Y_OFFSET),
+                                   size=(80, -1))
+        self.XColHdr = []
         wx.StaticText(p, -1, 'Y-Column',
                       pos=(FORM_WIDTH/2+COLLAB_X_OFFSET,
                            Y_OFFSET+LAB_CTRL_OFFSET), style=wx.ALIGN_LEFT)
         x = FORM_WIDTH/2 + COLLAB_X_OFFSET
-        self.cbYColHdr = wx.ComboBox(p, -1, pos=(x+50, Y_OFFSET),
-                                     size=(80, -1), style=wx.CB_DROPDOWN)
+        self.cbYColHdr = wx.Choice(p, -1, pos=(x+50, Y_OFFSET), size=(80, -1))
+        self.YColHdr = []
 
         Y_OFFSET += BUTTON_HEIGHT + MARGIN
         x = FORM_WIDTH/2 - BUTTON_WIDTH/2
-        self.btnPlote = wx.Button(p, label="Plot", pos=(x, Y_OFFSET),
+        self.btnPlot = wx.Button(p, label="Plot", pos=(x, Y_OFFSET),
                                      size=(100, BUTTON_HEIGHT))
 
-
-
-
-
-##        wx.StaticText(p, -1, 'Maximum wave height',
-##                      pos=(GEN_LABELXOFFSET, Y_OFFSET),
-##                      size=(CTL_WIDTH, CTL_HEIGHT), style=wx.ALIGN_RIGHT)
-##        self.chXColHdr = wx.Choice(p, -1, size=(CHBOX_WIDTH, CHBOX_HEIGHT),
-##                                   pos=(10,Y_OFFSET))
-
-
-
-
-
-
-##        wx.StaticText(p, -1, 'Maximum wave height',
-##                      pos=(GEN_LABELXOFFSET, Y_OFFSET),
-##                      size=(CTL_WIDTH, CTL_HEIGHT), style=wx.ALIGN_RIGHT)
-##        self.txtMaxH = wx.TextCtrl(p, -1, size=(100, 20),
-##                                   pos=(GEN_TEXTXOFFSET, Y_OFFSET))
-##        Y_OFFSET += GEN_DELTAY 
-##        x = FORM_WIDTH/2 - BUTTON_WIDTH/2
-##        self.btnGenerate = wx.Button(p, label="List",
-##                                     pos=(x, Y_OFFSET),
-##                                     size=(100, BUTTON_HEIGHT))
-##        Y_OFFSET += GEN_DELTAY + BUTTON_HEIGHT/2
-##
-##        self.lstGenerate = wx.ListCtrl(p, -1, pos=(8, Y_OFFSET),
-##                                       size=(TEXTLIST_WIDTH,TEXTLIST_HEIGHT),
-##                                       style=wx.LC_REPORT|wx.LC_SINGLE_SEL)
-##        
-##        Y_OFFSET += TEXTLIST_HEIGHT + 20
-##        self.Bind(wx.EVT_BUTTON, self.GenerateClick, self.btnGenerate)
-##        self.lstGenerate.Bind(wx.EVT_LIST_ITEM_SELECTED,
-##                              self.OnItemSelected, self.lstGenerate)
-##
-##        # start laying out get_multimux controls
-##        wx.StaticBox(p, -1, 'multimux_grid', (3, Y_OFFSET),
-##                     size=(BOX_WIDTH, BOX2_HEIGHT))
-##        Y_OFFSET += 18
-##        wx.StaticText(p, -1, 'Quake ID', pos=(GEN_LABELXOFFSET, Y_OFFSET),
-##                      size=(CTL_WIDTH, CTL_HEIGHT), style=wx.ALIGN_RIGHT)
-##        self.txtQuakeID = wx.TextCtrl(p, -1, size=(100, 20),
-##                                      pos=(GEN_TEXTXOFFSET, Y_OFFSET))
-##        Y_OFFSET += GEN_DELTAY 
-##        x = FORM_WIDTH/2 - BUTTON_WIDTH - DOUBLE_BUTTON_OFFSET
-##        self.btnMultimux = wx.Button(p, label="Multimux", pos=(x, Y_OFFSET),
-##                                     size=(100, BUTTON_HEIGHT))
-##        x = FORM_WIDTH/2 + DOUBLE_BUTTON_OFFSET
-##        self.btnGrid = wx.Button(p, label="Grid", pos=(x, Y_OFFSET),
-##                                     size=(100, BUTTON_HEIGHT))
-##        Y_OFFSET += GEN_DELTAY + BUTTON_HEIGHT/2
-##        self.lstMultimux = wx.ListCtrl(p, -1, pos=(8, Y_OFFSET),
-##                                       size=(TEXTLIST_WIDTH,TEXTLIST_HEIGHT),
-##                                       style=wx.LC_REPORT|wx.LC_SINGLE_SEL)
-##        Y_OFFSET += TEXTLIST_HEIGHT + 20
-##        self.Bind(wx.EVT_BUTTON, self.MultimuxClick, self.btnMultimux)
-##        self.Bind(wx.EVT_BUTTON, self.GridClick, self.btnGrid)
-
+        self.Bind(wx.EVT_BUTTON, self.AddCSVFile, self.btnAddCSVFile)
+        self.Bind(wx.EVT_BUTTON, self.DelCSVFile, self.btnDelCSVFile)
+        self.Bind(wx.EVT_BUTTON, self.PlotFiles, self.btnPlot)
         self.Bind(wx.EVT_CLOSE, self.doStateSave)
+        self.Bind(wx.EVT_CLOSE, self.doStateSave)
+        self.cfg = Config(ConfigFilename)
+        self.common_headers = None
         self.doStateRestore()
+        self.updateHdrChoices()
+        
+    def AddCSVFile(self, event):
+        """Add a CSV file to the listbox"""
+
+        dlg = wx.FileDialog(self, message='Choose a CSV file',
+                            defaultDir=self.file_dir, 
+                            defaultFile='',
+                            wildcard=CSVWildcard,
+                            style=wx.OPEN | wx.CHANGE_DIR)
+
+        if dlg.ShowModal() == wx.ID_OK:
+            # This returns a list of  files that were selected.
+            path = dlg.GetPath()
+            self.file_dir = dlg.GetDirectory()
+            dlg.Destroy()
+            if path not in self.CSVFiles:
+                headers = self.getHeaders(path)
+                if headers:
+                    common_headers = self.orHeaders(headers)
+                    if not common_headers or len(common_headers) < 2:
+                        self.error("Sorry, file '%s' doesn't have enough headers in common with current files" % path)
+                    else:
+                        self.headers.append(headers)
+                        self.common_headers = common_headers
+                        self.CSVFiles.append(path)
+                        self.txtCSVFiles.Append(path)
+                else:
+                    self.error("Sorry, file '%s' doesn't appear to be a CSV file" % path)
+                self.updateHdrChoices()
+
+    def DelCSVFile(self, event):
+        """Add a CSV file to the listbox"""
+
+        sel = self.txtCSVFiles.GetSelections()
+        if sel:
+            sel = sel[0]
+            self.txtCSVFiles.Delete(sel)
+            del self.CSVFiles[sel]
+            del self.headers[sel]
+            self.common_headers = self.GenCommonHeaders(self.headers)
+            self.updateHdrChoices()
 
     def doStateSave(self, event):
-        """
-        Save state here.
-        """
-##        self.cfg['OutputDirectory'] = self.txtOutputDir.GetValue()
-##        self.cfg['Region'] = self.cbRegion.GetValue()
-##        self.cfg['GaugeNumber'] = self.txtTGN.GetValue()
-##        self.cfg['MinimumHeight'] = self.txtMinH.GetValue()
-##        self.cfg['MaximumHeight'] = self.txtMaxH.GetValue()
-##        self.cfg.save()
+        """Save state from 'self' variables."""
+
+        self.cfg['CSVFiles'] = self.CSVFiles
+        self.cfg['XColHdr'] = self.XColHdr
+        self.cfg['XColHdrSelection'] = self.cbXColHdr.GetSelection()
+        self.cfg['YColHdr'] = self.YColHdr
+        self.cfg['YColHdrSelection'] = self.cbYColHdr.GetSelection()
+        self.cfg['file_dir'] = self.file_dir
+
+        self.cfg.save()
+        event.Skip(True)
+
+    def doStateRestore(self):
+        """Restore state to 'self' variables."""
+
+        self.CSVFiles = self.cfg['CSVFiles']
+        if self.CSVFiles is None:
+            self.CSVFiles = []
+        self.XColHdr = self.cfg['XColHdr']
+        if self.XColHdr is None:
+            self.XColHdr = []
+        # -1 here means 'no selection'
+        self.XColHdrSelection = self.cfg['XColHdrSelection', -1]
+        self.YColHdr = self.cfg['YColHdr']
+        if self.YColHdr is None:
+            self.YColHdr = []
+        # -1 here means 'no selection'
+        self.YColHdrSelection = self.cfg['YColHdrSelection', -1]
+        self.file_dir = self.cfg['file_dir']
+        if self.file_dir is None:
+            self.file_dir = os.getcwd()
+
+        # put data into controls
+        self.headers = []
+        for (i, f) in enumerate(self.CSVFiles):
+            headers = self.getHeaders(f)
+            self.headers.append(headers)
+            self.txtCSVFiles.Insert(f, i)
+
+    def getHeaders(self, filename):
+        '''Get header list from a file'''
+
+        try:
+            fd = open(filename, 'r')
+            hdr = fd.readline()
+            fd.close()
+        except:
+            return None
+
+        hdr = hdr.split(',')
+        hdr = [x.strip() for x in hdr]
+
+        return hdr
         
-        event.Skip(True)
-
-    def doStateRestore(self):
-        """
-        Restore state here - globals have been set and tested sane
-        """
-##        global Region, GaugeNumber, MinimumHeight, MaximumHeight
-##
-##        cfg = Config(ConfigFilename)
-##        output_dir = cfg['OutputDirectory']
-##        Region = cfg['Region']
-##        GaugeNumber = cfg['GaugeNumber']
-##        if GaugeNumber:
-##            GaugeNumber = int(GaugeNumber)
-##        MinimumHeight = cfg['MinimumHeight']
-##        if MinimumHeight:
-##            MinimumHeight = float(MinimumHeight)
-##        MaximumHeight = cfg['MaximumHeight']
-##        if MaximumHeight:
-##            MaximumHeight = float(MaximumHeight)
-##
-##        self.cfg = cfg
-##
-##        region_name = None
-##        for (name, dir) in Regions:
-##            if name == Region:
-##                region_name = name
-##        if not region_name:
-##            Region = ''
-##
-##        if output_dir:
-##            self.txtOutputDir.WriteText(output_dir)
-##        self.cbRegion.SetStringSelection(Region)
-##        if GaugeNumber:
-##            self.txtTGN.WriteText(str(GaugeNumber))
-##        if MinimumHeight:
-##            self.txtMinH.WriteText(str(MinimumHeight))
-##        if MaximumHeight:
-##            self.txtMaxH.WriteText(str(MaximumHeight))
+    def updateHdrChoices(self):
+        '''Update choice controls with header lists.
+
+        Keep initially visible strings visible afterwards, if posssible.
+        '''
+
+        selected_x = self.cbXColHdr.GetStringSelection()
+        selected_y = self.cbYColHdr.GetStringSelection()
         
+        self.cbXColHdr.Clear()
+        self.cbYColHdr.Clear()
+
+        if self.common_headers:
+            self.cbXColHdr.Enable()
+            self.cbYColHdr.Enable()
+            self.btnPlot.Enable()
+            for h in self.common_headers:
+                self.cbXColHdr.Append(h)
+                self.cbYColHdr.Append(h)
+            if selected_x in self.common_headers:
+                index = self.common_headers.index(selected_x)
+                self.cbXColHdr.SetSelection(index)
+            if selected_y in self.common_headers:
+                index = self.common_headers.index(selected_y)
+                self.cbYColHdr.SetSelection(index)
+        else:
+            self.cbXColHdr.Disable()
+            self.cbYColHdr.Disable()
+            self.btnPlot.Disable()
+
+    def orHeaders(self, new_header):
+        '''Update X & Y column header choices'''
+
+        if self.common_headers:
+            result = [x for x in new_header if x in self.common_headers]
+        else:
+            result = new_header
+        return result
+
+    def GenCommonHeaders(self, headers):
+        '''Get new set of common headers'''
+
+        result = []
+        for header in headers:
+            if result:
+                result = [x for x in result if x in header]
+            else:
+                result = header
+        return result
+
+    def PlotFiles(self, event):
+        selected_x = self.cbXColHdr.GetStringSelection()
+        selected_y = self.cbYColHdr.GetStringSelection()
+
+        if selected_x and selected_y:
+            options = {}
+            # set X and Y axis labels
+            options[X_LABEL] = selected_x
+            options[Y_LABEL] = selected_y
+            options[SIZE] = '800,600'
+            options[TITLE] = 'TITLE'
+
+            plot_files(self.CSVFiles, options) 
+            
     def error(self, msg):
         dlg = wx.MessageDialog(self, msg, 'Error',
