gt: 	    int h = Math.min(screen.height-100, (int)(dim.height*f));
context: /*
   * Open Source Physics software is free software as described near the bottom of this code file.
   *
   * For additional information and documentation on Open Source Physics please see:
   * <https://www.compadre.org/osp/>
   */
  
  package org.opensourcephysics.tools;
  
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.Frame;
  import java.awt.Point;
  import java.awt.Toolkit;
  import java.awt.datatransfer.Clipboard;
  import java.awt.datatransfer.DataFlavor;
  import java.awt.datatransfer.StringSelection;
  import java.awt.datatransfer.Transferable;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.awt.event.WindowAdapter;
  import java.awt.event.WindowEvent;
  import java.beans.PropertyChangeEvent;
  import java.beans.PropertyChangeListener;
  import java.beans.PropertyChangeSupport;
  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.OutputStreamWriter;
  import java.io.Reader;
  import java.io.StringReader;
  import java.io.Writer;
  import java.rmi.RemoteException;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Enumeration;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Locale;
  
  import javax.swing.AbstractAction;
  import javax.swing.AbstractButton;
  import javax.swing.Action;
  import javax.swing.ButtonGroup;
  import javax.swing.JButton;
  import javax.swing.JFileChooser;
  import javax.swing.JFrame;
  import javax.swing.JMenu;
  import javax.swing.JMenuBar;
  import javax.swing.JMenuItem;
  import javax.swing.JOptionPane;
  import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
  import javax.swing.JRadioButtonMenuItem;
  import javax.swing.JTabbedPane;
  import javax.swing.KeyStroke;
  import javax.swing.SwingConstants;
  import javax.swing.WindowConstants;
  import javax.swing.event.ChangeEvent;
  import javax.swing.event.ChangeListener;
  
  import org.opensourcephysics.controls.ControlsRes;
  import org.opensourcephysics.controls.OSPLog;
  import org.opensourcephysics.controls.XML;
  import org.opensourcephysics.controls.XMLControl;
  import org.opensourcephysics.controls.XMLControlElement;
  import org.opensourcephysics.controls.XMLProperty;
  import org.opensourcephysics.controls.XMLTree;
  import org.opensourcephysics.controls.XMLTreeChooser;
  import org.opensourcephysics.desktop.OSPDesktop;
  import org.opensourcephysics.display.Data;
  import org.opensourcephysics.display.DataFunction;
  import org.opensourcephysics.display.Dataset;
  import org.opensourcephysics.display.DatasetManager;
  import org.opensourcephysics.display.DisplayColors;
  import org.opensourcephysics.display.OSPFrame;
  import org.opensourcephysics.display.OSPRuntime;
  import org.opensourcephysics.display.TeXParser;
  import org.opensourcephysics.display.TextFrame;
  
  /**
   * This provides a GUI for analyzing OSP Data objects.
   *
   * @author Douglas Brown
   * @version 1.0
   */
  @SuppressWarnings("serial")
  public class DataTool extends OSPFrame implements Tool, PropertyChangeListener {
    // static fields
    @SuppressWarnings("javadoc")
  	public static boolean loadClass = false;
    protected static Dimension dim = new Dimension(800, 540);
    protected static final int defaultButtonHeight = 28;
    protected static int buttonHeight = defaultButtonHeight;
    protected static String[] delimiters = new String[] {" ", "\t", ",", ";"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
    protected static TextFrame helpFrame;
    protected static String helpName = "datatool/datatool_help.html";                                 //$NON-NLS-1$
    protected static String helpBase = "https://www.compadre.org/osp/online_help/tools/"; //$NON-NLS-1$
    private static ArrayList<Data> processedData = new ArrayList<Data>();
    
    // instance fields
    protected JTabbedPane tabbedPane;
    protected boolean useChooser = true;
    protected JPanel contentPane = new JPanel(new BorderLayout());
    protected PropertyChangeSupport support;
    protected XMLControlElement control = new XMLControlElement();
    protected Data addableData = null;
    protected boolean controlContainsData;
    protected JMenuBar emptyMenubar;
    protected JMenu emptyFileMenu;
    protected JMenuItem emptyNewTabItem;
    protected JMenuItem emptyOpenItem;
    protected JMenuItem emptyExitItem;
    protected JMenu emptyEditMenu;
    protected JMenuItem emptyPasteMenu;
    protected JMenuItem emptyPasteTabItem;
    protected JMenuBar menubar;
    protected JMenu fileMenu;
    protected JMenuItem newTabItem;
    protected JMenuItem openItem;
    protected JMenuItem importItem;
    protected JMenuItem exportItem;
    protected JMenuItem saveItem;
    protected JMenuItem saveAsItem;
    protected JMenuItem closeItem;
    protected JMenuItem closeAllItem;
    protected JMenuItem printItem;
    protected JMenuItem exitItem;
    protected JMenu editMenu;
    protected JMenuItem undoItem;
    protected JMenuItem redoItem;
    protected JMenu copyMenu;
    protected JMenuItem copyImageItem;
    protected JMenuItem copyTabItem;
    protected JMenuItem copyDataItem;
    protected JMenu pasteMenu;
    protected JMenuItem pasteTabItem;
    protected JMenuItem pasteColumnsItem;
    protected JMenu displayMenu;
    protected JMenu languageMenu;
    protected JMenuItem[] languageItems;
    protected JMenu fontSizeMenu;
    protected JMenuItem defaultFontSizeItem;
    protected ButtonGroup fontSizeGroup;
    protected JMenu helpMenu;
    protected JMenuItem helpItem;
    protected JMenuItem logItem;
    protected JMenuItem aboutItem;
    protected DataBuilder dataBuilder;
    protected boolean exitOnClose = false;
    protected boolean saveChangesOnClose = false;
    protected FitBuilder fitBuilder;
    protected boolean isLoading = false;
    protected JButton loadDataFunctionsButton, saveDataFunctionsButton;
    protected boolean slopeExtended = false;
  
    static {
      DATATOOL = new DataTool();
    }
  
    /**
     * A shared data tool.
     */
    final static DataTool DATATOOL;
  
    /**
     * Gets the shared DataTool.
     *
     * @return the shared DataTool
     */
    public static DataTool getTool() {
      return DATATOOL;
    }
  
    /**
     * Main entry point when used as application.
     *
     * @param args args[0] may be a data or xml file name
     */
    public static void main(String[] args) {
      DATATOOL.exitOnClose = true;
      DATATOOL.saveChangesOnClose = true;
      if((args!=null)&&(args.length>0)&&(args[0]!=null)) {
        DATATOOL.setVisible(true);
        DATATOOL.open(args[0]);
      } else {
      	DATATOOL.addWindowListener(new WindowAdapter() {
      		@Override
          public void windowOpened(WindowEvent e) {
          	if (DATATOOL.getTabCount()==0) {
              DataToolTab tab = DATATOOL.createTab(null);
              tab.setUserEditable(true);
              DATATOOL.addTab(tab);
          	}
          }
      	});
        DATATOOL.setVisible(true);
      }
      
    }
    
    /**
     * Constructs a blank DataTool.
     */
    public DataTool() {
      this(ToolsRes.getString("DataTool.Frame.Title"), "DataTool"); //$NON-NLS-1$ //$NON-NLS-2$
    }
  
    /**
     * Constructs a DataTool and opens the specified xml file.
     *
     * @param fileName the name of the xml file
     */
    public DataTool(String fileName) {
      this();
      open(fileName);
    }
  
    /**
     * Constructs a DataTool and loads data from an xml control.
     *
     * @param control the xml control
     */
    public DataTool(XMLControl control) {
      this();
      addTabs(control);
    }
  
    /**
     * Constructs a DataTool and loads the specified data object.
     *
     * @param data the data
     */
    public DataTool(Data data) {
      this();
      ArrayList<DataToolTab> tabs = createTabs(data);
      for(DataToolTab tab : tabs) {
        addTab(tab);
      }
    }
  
    /**
     * Sets the saveChangesOnClose flag.
     *
     * @param save true to save changes when exiting
     */
    public void setSaveChangesOnClose(boolean save) {
      saveChangesOnClose = save&&!OSPRuntime.appletMode;
    }
  
    /**
     * Adds tabs loaded with data from an xml control.
     *
     * @param control the xml control
     * @return a list of newly added tabs, or null if failed
     */
    public ArrayList<DataToolTab> addTabs(XMLControl control) {
      // if control is for DataToolTab class, load tab from control
      if(DataToolTab.class == control.getObjectClass()) {
      	// store this DataTool in the control so DataToolTab loader can use it for instantiation
      	control.setValue("datatool", this); //$NON-NLS-1$
      	
      	isLoading = true;
        DataToolTab tab = (DataToolTab) control.loadObject(null);
        addTab(tab);
        tab.refreshGUI();
      	isLoading = false;
        ArrayList<DataToolTab> tabs = new ArrayList<DataToolTab>();
        tabs.add(tab);
        return tabs;
      }
      // if control is for FourierToolTab, load the source data into a tab
    	if(control.getObjectClassName().endsWith("FourierToolTab")) { //$NON-NLS-1$
      	XMLControl child = control.getChildControl("source_data"); //$NON-NLS-1$
      	Data source = (Data)child.loadObject(null);
      	DataToolTab tab = createTab(source);
        addTab(tab);
        tab.refreshGUI();
        ArrayList<DataToolTab> tabs = new ArrayList<DataToolTab>();
        tabs.add(tab);
        return tabs;
      }
      // otherwise load data from control into tabs
      ArrayList<DataToolTab> tabs = loadTabsFromXML(control, useChooser);
      for(DataToolTab tab : tabs) {
        addTab(tab);
        tab.refreshGUI();
      }
      return tabs;
    }
  
    /**
     * Creates a tab for each Data object returned by DataTool.getDataList(source).
     * The tab names will be those of the Data objects in the list if they define
     * a getName() method.
     *
     * @param source the source Data
     * @return a list of new tabs
     */
    public ArrayList<DataToolTab> createTabs(Data source) {
      ArrayList<Data> dataList = getSelfContainedData(source);
      ArrayList<DataToolTab> tabList = new ArrayList<DataToolTab>();
      for(Data next : dataList) {
        DataToolTab tab = createTab(next);
        if(tab!=null) {
          tabList.add(tab);
        }
      }
      return tabList;
    }
  
    /**
     * Creates a tab for the specified Data object. The tab name will be
     * that of the Data object if it defines a getName() method.
     *
     * @param data the Data
     * @return the new tab
     */
    protected DataToolTab createTab(Data data) {
      // be sure fitBuilder is instantiated
      fitBuilder = getFitBuilder();
      DataToolTab tab = new DataToolTab(data, this);
      if(data!=null) {
        String name = data.getName();
        if((name!=null)&&!name.equals("")) { //$NON-NLS-1$) 
          tab.setName(name);
        }
      }
      return tab;
    }
  
    /**
     * Removes the tab at the specified index.
     *
     * @param index the tab number
     * @param saveChanges 
     * @return the removed tab, or null if none removed
     */
    public DataToolTab removeTab(int index, boolean saveChanges) {
      if((index>=0)&&(index<tabbedPane.getTabCount())) {
        if(saveChanges&&!saveChangesAt(index)) {
          return null;
        }
        DataToolTab tab = getTab(index);
        fitBuilder.curveFitters.remove(tab.curveFitter);
        fitBuilder.removePropertyChangeListener(tab.curveFitter.fitListener);
        String title = tabbedPane.getTitleAt(index);
        OSPLog.finer("removing tab "+title); //$NON-NLS-1$
        tabbedPane.removeTabAt(index);
        refreshTabTitles();
        refreshMenubar();
        refreshDataBuilder();
        return tab;
      }
      return null;
    }
  
    /**
     * Removes a specified tab.
     *
     * @param tab the tab
     * @return the removed tab, or null if none removed
     */
    public DataToolTab removeTab(DataToolTab tab) {
      return removeTab(getTabIndex(tab), true);
    }
  
    /**
     * Loads a Data object into existing tabs and/or newly created tabs as needed.
     *
     * @param data the Data
     * @return a list of the loaded tabs
     */
    public ArrayList<DataToolTab> loadData(Data data) {
      DataToolTab tab = null;
      ArrayList<DataToolTab> loadedTabs = new ArrayList<DataToolTab>();
      for(Data next : getSelfContainedData(data)) {
        tab = getTab(next); // tab may be null
        if(tab!=null) {
          tab.loadData(next, tab.replaceColumnsWithMatchingNames);
        } else {
          tab = createTab(next);
          addTab(tab);
        }
        loadedTabs.add(tab);
      }
      if(tab!=null) {
        setSelectedTab(tab);
      }
      return loadedTabs;
    }
  
    /**
     * Loads multiple Data objects into a single existing or newly created tab.
     *
     * @param data one or more Data objects
     * @return the loaded tab
     */
    public DataToolTab loadData(Data... data) {
      if(data==null) {
        return null;
      }
      ArrayList<Data> selfContained = new ArrayList<Data>();
      for(Data next : data) {
        selfContained.addAll(DataTool.getSelfContainedData(next));
      }
      DataToolTab tab = null;
      for(Data next : selfContained) {
        // retrieve or create tab for first Data object
        if(tab==null) {
          tab = getTab(next); // may still be null
          if(tab!=null) {
            tab.loadData(next, tab.replaceColumnsWithMatchingNames);
          } else {
            tab = createTab(next);
            addTab(tab);
          }
        }
        // add additional columns to tab
        else {
          ArrayList<DataColumn> columns = getDataColumns(next);
          columns.remove(0);
          tab.addColumns(columns, false, false, false);
        }
      }
      if(tab!=null) {
        setSelectedTab(tab);
      }
      return tab;
    }
  
    /**
     * Returns the tab associated with the specified Data object. May return null.
     *
     * @param data the Data
     * @return the tab
     */
    public DataToolTab getTab(Data data) {
      int i = getTabIndex(data);
      return(i>-1) ? getTab(i) : null;
    }
  
    /**
     * Returns the tab at the specified index. May return null.
     *
     * @param index the tab index
     * @return the tab
     */
    public DataToolTab getTab(int index) {
      return((index>-1)&&(index<tabbedPane.getTabCount())) ? (DataToolTab) tabbedPane.getComponentAt(index) : null;
    }
  
    /**
     * Returns the tab count.
     *
     * @return the number of tabs
     */
    public int getTabCount() {
      return tabbedPane.getTabCount();
    }
  
    /**
     * Returns a list of all open tabs.
     *
     * @return a list of DataToolTabs
     */
    public List<DataToolTab> getTabs() {
    	List<DataToolTab> tabs = new ArrayList<DataToolTab>();
    	for (int i=0; i< getTabCount(); i++) {
    		tabs.add(getTab(i));
    	}
      return tabs;
    }
  
    /**
     * Opens an xml or data file specified by name.
     *
     * @param fileName the file name
     * @return the file name, if successfully opened (datasets loaded)
     */
    public String open(String fileName) {
      OSPLog.fine("opening "+fileName); //$NON-NLS-1$
      Resource res = ResourceLoader.getResource(fileName);
      if(res!=null) {
        Reader in = res.openReader();
        String firstLine = readFirstLine(in);
        // if xml, read the file into an XML control and add tab
        if(firstLine.startsWith("<?xml")) { //$NON-NLS-1$
          XMLControlElement control = new XMLControlElement(fileName);
          ArrayList<DataToolTab> tabs = addTabs(control);
          if(!tabs.isEmpty()) {
            for(DataToolTab tab : tabs) {
              refreshDataBuilder();
              if(tabs.size()==1) {
                tab.fileName = fileName;
              }
              tab.tabChanged(false);
            }
            try {
  			in.close();
  		  } catch (IOException e) {
  			//e.printStackTrace();
  		   }
            return fileName;
          }
        }
        // if not xml, attempt to import data and add tab
        else if(res.getString()!=null) {
          Data data = parseData(res.getString(), fileName);
          if(data!=null) {
          	DataToolTab tab = createTab(data);
            addTab(tab);
            refreshDataBuilder();
            tab.fileName = fileName;
            tab.tabChanged(false);
            return fileName;
          }
        }
      }
      OSPLog.finest("no data found"); //$NON-NLS-1$
      return null;
    }
  
    /**
     * Imports an xml or data file into an existing tab.
     *
     * @param tab the tab
     * @param fileName the file name
     * @return the file name, if successfully imported (datasets loaded)
     */
    public String importFileIntoTab(DataToolTab tab, String fileName) {
      OSPLog.fine("importing "+fileName); //$NON-NLS-1$
      Resource res = ResourceLoader.getResource(fileName);
      if(res!=null) {
        Reader in = res.openReader();
        String firstLine = readFirstLine(in);
        // if xml, read the file into an XML control and add tab
        if(firstLine.startsWith("<?xml")) { //$NON-NLS-1$
          XMLControlElement control = new XMLControlElement(fileName);
          ArrayList<Data> dataList = getSelfContainedData(control, false);
          if(!dataList.isEmpty()) {
            DatasetManager manager = new DatasetManager();
            for(Data next : dataList) {
              for(DataColumn column : getDataColumns(next)) {
                manager.addDataset(column);
              }
            }
            tab.addColumns(manager, true, true, true);
            return fileName;
          }
        }
        // if not xml, attempt to import data and add tab
        else if(res.getString()!=null) {
          Data data = parseData(res.getString(), fileName);
          if(data!=null) {
            tab.addColumns(data, true, true, true);
            return fileName;
          }
        }
      }
      OSPLog.finest("no data found"); //$NON-NLS-1$
      return null;
    }
  
    /**
     * Sends a job to this tool and specifies a tool to reply to.
     *
     * @param job the Job
     * @param replyTo the tool to notify when the job is complete (may be null)
     * @throws RemoteException
     */
    public void send(Job job, Tool replyTo) throws RemoteException {
      XMLControlElement control = new XMLControlElement(job.getXML());
      if(control.failedToRead()||(control.getObjectClass()==Object.class)) {
        return;
      }
      // if control defines Data, construct the Data
      if(Data.class.isAssignableFrom(control.getObjectClass())) {
        Data data = (Data) control.loadObject(null, true, true);
        // if self-contained, then send job to an existing tab or create a new tab
        if(isSelfContained(data)) {
          DataToolTab tab = getTab(data);   // may be null
          if(tab==null) {
            tab = createTab(null);
            String name = data.getName();
            if((name!=null)&&!name.equals("")) { //$NON-NLS-1$) 
              tab.setName(name);
            }
            addTab(tab);
          }
          else {
          	setSelectedTab(tab);
          }
          tab.send(job, replyTo);
        }
        // if data is a container, then send a new job to an existing tab
        // or create a new tab
        else {
          for(Data next : getSelfContainedData(data)) {
            DataToolTab tab = getTab(next); // may be null
            if(tab==null) {
              tab = createTab(null);
              String name = next.getName();
              if((name!=null)&&!name.equals("")) { //$NON-NLS-1$) 
                tab.setName(name);
              }
              addTab(tab);
            }
            tab.send(new LocalJob(next), replyTo);
          }
        }
      }
      // else add tabs based on child Data objects, if any, within the control
      else {
        addTabs(control);                   // adds Data objects found in XMLControl
      }
    }
  
    /**
     * Sets the useChooser flag.
     *
     * @param useChooser true to load datasets with a chooser
     */
    public void setUseChooser(boolean useChooser) {
      this.useChooser = useChooser;
    }
  
    /**
     * Gets the useChooser flag.
     *
     * @return true if loading datasets with a chooser
     */
    public boolean isUseChooser() {
      return useChooser;
    }
    
    /**
     * Listens for property changes "function"
     *
     * @param e the event
     */
    public void propertyChange(PropertyChangeEvent e) {
      String name = e.getPropertyName();
      if(name.equals("function")) {                     //$NON-NLS-1$
        DataToolTab tab = getSelectedTab();
        if(tab!=null) {
          tab.tabChanged(true);
          tab.dataTable.refreshTable();
          tab.statsTable.refreshStatistics();
          if(e.getNewValue() instanceof DataFunction) { // new function has been created
            String funcName = e.getNewValue().toString();
            tab.dataTable.getWorkingData(funcName);
          }
          if(e.getOldValue() instanceof DataFunction) { // function has been deleted
            String funcName = e.getOldValue().toString();
            tab.dataTable.removeWorkingData(funcName);
          }
          if(e.getNewValue() instanceof String) {
            String funcName = e.getNewValue().toString();
            if(e.getOldValue() instanceof String) {     // function name has changed
              String prevName = e.getOldValue().toString();
              tab.columnNameChanged(prevName, funcName);
            } else {
              tab.dataTable.getWorkingData(funcName);
            }
          }
          tab.refreshPlot();
          tab.varPopup = null;
        }
      }
    }
  
    /**
     * Determines if an array contains any duplicate or Double.NaN values.
     *
     * @param values the array
     * @return true if at least one duplicate is found
     */
    protected static boolean containsDuplicateValues(double[] values) {
      for(int i = 0; i<values.length; i++) {
        if(Double.isNaN(values[i])) {
          return true;
        }
        int n = getIndex(values[i], values, i);
        if(n>-1) {
          return true;
        }
      }
      return false;
    }
  
    /**
     * Gets the first array index at which the specified value is found.
     *
     * @param value the value to find
     * @param array the array to search
     * @param ignoreIndex an array index to ignore
     * @return the index, or -1 if not found
     */
    protected static int getIndex(double value, double[] array, int ignoreIndex) {
      for(int i = 0; i<array.length; i++) {
        if(i==ignoreIndex) {
          continue;
        }
        if(array[i]==value) {
          return i;
        }
      }
      return -1;
    }
  
    /**
     * Returns an array of row numbers.
     *
     * @param rowCount length of the array
     * @return the array
     */
    protected static double[] getRowArray(int rowCount) {
      double[] rows = new double[rowCount];
      for(int i = 0; i<rowCount; i++) {
        rows[i] = i;
      }
      return rows;
    }
  
    /**
     * Parses a String into tokens separated by a specified delimiter.
     * A token may be "".
     *
     * @param text the text to parse
     * @param delimiter the delimiter
     * @return an array of String tokens
     */
    protected static String[] parseStrings(String text, String delimiter) {
      Collection<String> tokens = new ArrayList<String>();
      if(text!=null) {
        // get the first token
        String next = text;
        int i = text.indexOf(delimiter);
        if(i==-1) {   // no delimiter
          tokens.add(stripQuotes(next));
          text = null;
        } else {
          next = text.substring(0, i);
          text = text.substring(i+1);
          while (" ".equals(delimiter)  //$NON-NLS-1$
          		&& (text.startsWith(" ") || text.startsWith("\t"))) { //$NON-NLS-1$ //$NON-NLS-2$
          	// treat multiple spaces/tabs as a single delimiter
          	text = text.substring(1);
          }
        }
        // iterate thru the tokens and add to token list
        while(text!=null) {
          tokens.add(stripQuotes(next));
          i = text.indexOf(delimiter);
          if(i==-1) { // no delimiter
            next = text;
            tokens.add(stripQuotes(next));
            text = null;
          } else {
            next = text.substring(0, i).trim();
            text = text.substring(i+1);
            while (" ".equals(delimiter)  //$NON-NLS-1$
            		&& (text.startsWith(" ") || text.startsWith("\t"))) { //$NON-NLS-1$ //$NON-NLS-2$
            	// treat multiple spaces/tabs as a single delimiter
            	text = text.substring(1);
            }
          }
        }
      }
      return tokens.toArray(new String[0]);
    }
  
    /**
     * Strips quotation marks around a string.
     *
     * @param text the text to strip
     * @return the stripped string
     */
    private static String stripQuotes(String text) {
      if(text.startsWith("\"")) {       //$NON-NLS-1$
        String stripped = text.substring(1);
        int n = stripped.indexOf("\""); //$NON-NLS-1$
        if(n>-1 && n==stripped.length()-1) {
          return stripped.substring(0, n);
        }
      }
      return text;
    }
  
    /**
     * Parses a String into doubles separated by a specified delimiter.
     * Unparsable strings are set to Double.NaN.
     *
     * @param text the text to parse
     * @param delimiter the delimiter
     * @return an array of doubles
     */
    protected static double[] parseDoubles(String text, String delimiter) {
      String[] strings = parseStrings(text, delimiter);
      return parseDoubles(strings, delimiter);
    }
  
    /**
     * Parses a String array into doubles.
     * Unparsable strings are set to Double.NaN.
     *
     * @param strings the String array to parse
     * @param delimiter the delimiter that was used to parse the strings
     * @return an array of doubles
     */
    protected static double[] parseDoubles(String[] strings, String delimiter) {
      double[] doubles = new double[strings.length];
      for(int i = 0; i<strings.length; i++) {
        if(strings[i].indexOf("\t")>-1) { //$NON-NLS-1$
          doubles[i] = Double.NaN;
        } else {
          try {
            doubles[i] = Double.parseDouble(strings[i]);
          } catch(NumberFormatException e) {
          	// convert decimal separator commas with periods
          	if (strings[i].indexOf(",")>-1 && !delimiter.equals(",")) { //$NON-NLS-1$ //$NON-NLS-2$
          		strings[i] = strings[i].replace(",", "."); //$NON-NLS-1$ //$NON-NLS-2$
              try {
                doubles[i] = Double.parseDouble(strings[i]);
              } catch(NumberFormatException e1) {
              	doubles[i] = Double.NaN;
              }        		
          	}
          	else doubles[i] = Double.NaN;
          }
        }
      }
      return doubles;
    }
  
    /**
     * Parses a String into tokens separated by specified row and column delimiters.
     *
     * @param text the text to parse
     * @param rowDelimiter the column delimiter
     * @param colDelimiter the column delimiter
     * @return a 2D array of String tokens
     */
    protected static String[][] parseStrings(String text, String rowDelimiter, String colDelimiter) {
      String[] rows = parseStrings(text, rowDelimiter);
      String[][] tokens = new String[rows.length][0];
      for(int i = 0; i<rows.length; i++) {
        tokens[i] = parseStrings(rows[i], colDelimiter);
      }
      return tokens;
    }
  
    /**
     * Parses a String into doubles separated by specified row and column delimiters.
     *
     * @param text the text to parse
     * @param rowDelimiter the column delimiter
     * @param colDelimiter the column delimiter
     * @return a 2D array of doubles
     */
    protected static double[][] parseDoubles(String text, String rowDelimiter, String colDelimiter) {
      String[][] strings = parseStrings(text, rowDelimiter, colDelimiter);
      double[][] doubles = new double[strings.length][0];
      for(int i = 0; i<strings.length; i++) {
        double[] row = new double[strings[i].length];
        for(int j = 0; j<row.length; j++) {
          try {
            row[j] = Double.parseDouble(strings[i][j]);
          } catch(NumberFormatException e) {
            row[j] = Double.NaN;
          }
        }
        doubles[i] = row;
      }
      return doubles;
    }
  
    /**
     * Parses character-delimited data from a string.
     * This attempts to extract the following information from the string:
     *
     * 1. A title to be used for the tab name
     * 2. One or more columns of double data values
     * 3. Column names for the data columns
     *
     * @param dataString the data string
     * @param fileName name of file containing the data string (may be null)
     * @return DatasetManager with parsed data, or null if none found
     */
    public static DatasetManager parseData(String dataString, String fileName) {
      BufferedReader input = new BufferedReader(new StringReader(dataString));
      final String gnuPlotComment = "#"; //$NON-NLS-1$
      try {
        String textLine = input.readLine();
        for(int i = 0; i<DataTool.delimiters.length; i++) {
          ArrayList<double[]> rows = new ArrayList<double[]>();
          int columns = Integer.MAX_VALUE;
          String[] columnNames = null;
          String title = null;
          int lineCount = 0;
          while(textLine!=null) {    
          	// process each line of text
            if(textLine.startsWith("//")) { //$NON-NLS-1$
              // ignore comments (lines starting with "//")
            	textLine = input.readLine();
              continue;          	
            }
            if(textLine.contains(gnuPlotComment)) {
              // trim gnuPlot comments
              textLine = textLine.trim();                                                                                                // added by W. Christian
            }
            // look for gnuPlot-commented name and/or columnNames
            if(textLine.startsWith(gnuPlotComment)) {
              int k = textLine.indexOf("name:");                                                                                         //$NON-NLS-1$
              if(k>-1) {
                title = textLine.substring(k+5).trim();
              }
              k = textLine.indexOf("columnNames:");                                                                                      //$NON-NLS-1$
              if(k>-1) {
                textLine = textLine.substring(k+12).trim();
              } else {
                textLine = input.readLine();
                continue;
              }
            }
            // skip Vernier Format 2 header lines
            if((textLine.indexOf("Vernier Format")>-1                                                                                    //$NON-NLS-1$
              )||(textLine.indexOf(".cmbl")>-1)) {                                                                                       //$NON-NLS-1$
              textLine = input.readLine();
              continue;
            }
            String[] strings = DataTool.parseStrings(textLine, DataTool.delimiters[i]);
            double[] rowData = DataTool.parseDoubles(strings, DataTool.delimiters[i]);
            // set title if not yet set (null), String[] length > 0, all entries
            // are NaN and only one entry is not ""
            if(rows.isEmpty()&&(strings.length>0)&&(title==null)) {
              String s = "";                                                                                                             //$NON-NLS-1$
              for(int k = 0; k<strings.length; k++) {
                if(Double.isNaN(rowData[k])&&!strings[k].equals("")) {                                                                   //$NON-NLS-1$
                  if(s.equals("")) {                                                                                                     //$NON-NLS-1$
                    s = strings[k];
                  } else {
                    s = "";                                                                                                              //$NON-NLS-1$
                    break;
                  }
                }
              }
              if(!s.equals("")) {                                                                                                        //$NON-NLS-1$
                title = s;
                textLine = input.readLine();
                continue;
              }
            }
            // set column names if not yet set (null), String[] length > 0,
            // all entries are NaN, and no data yet loaded
            if(rows.isEmpty()&&(strings.length>0)&&(columnNames==null)) {
              boolean valid = true;
              for(int k = 0; k<strings.length; k++) {
                if(!Double.isNaN(rowData[k])) {
                  valid = false;
                  break;
                }
              }
              if(valid) {
              	// replace "" with "?"
              	for (int k=0; k<strings.length; k++) {
              		if ("".equals(strings[k])) { //$NON-NLS-1$
              			strings[k] = "?"; //$NON-NLS-1$
              		}
              	}
                columnNames = strings;
                columns = strings.length;
                textLine = input.readLine();
                continue;
              }
            }
            // add double[] of length 1 or longer to rows
            if(strings.length>0) {
              lineCount++;
              boolean validData = true;
              boolean emptyData = true;
              for(int k = 0; k<strings.length; k++) {
                // invalid if any NaN entries other than ""
                if(Double.isNaN(rowData[k])&&!strings[k].equals("")) {                                                                   //$NON-NLS-1$
                  validData = false;
                }
                // look for empty row--every entry is ""
                if(!strings[k].equals("")) {                                                                                             //$NON-NLS-1$
                  emptyData = false;
                }
              }
              // ignore blank lines (NaN data) that precede real data 
              // unless both title and column names are non-null
              // and number of columns is 1
              if(rows.isEmpty() && emptyData && title==null 
              		&& (columnNames==null || columnNames.length!=1)) {
                validData = false;
              }
              // add valid data
              if(validData) {
                rows.add(rowData);
                if (columns==Integer.MAX_VALUE) {
                	columns = rowData.length;
                }
                else {
                	columns = Math.max(rowData.length, columns);
                }
              }
            }
            // abort processing if no data found in first several lines
            if(rows.isEmpty()&&(lineCount>10)) {
              break;
            }
            textLine = input.readLine();
          } // end while loop
          
          // create datasets if data found
          if(!rows.isEmpty()&&(columns>0)) {
            input.close();
            // first reassemble data from rows into columns
            double[][] dataArray = new double[columns][rows.size()];
            for(int row = 0; row<rows.size(); row++) {
              double[] rowData = rows.get(row);
              for(int j = 0; j<columns; j++) {
                dataArray[j][row] = rowData.length>j? rowData[j]: Double.NaN;
              }
            }
            // then append data to datasets
            DatasetManager data = new DatasetManager();
            data.setName((title==null) ? XML.getName(fileName) : title);
            double[] rowColumn = DataTool.getRowArray(rows.size());
            for(int j = 0; j<columns; j++) {
              Dataset dataset = data.getDataset(j);
              String yColName = ((columnNames!=null)&&(columnNames.length>j)) ? columnNames[j] : 
              	((j==0)&&(title!=null)) ? title : "?"; //$NON-NLS-1$
              dataset.setXYColumnNames("row", yColName);  //$NON-NLS-1$
              dataset.setXColumnVisible(false);
              dataset.append(rowColumn, dataArray[j]);
            }
            OSPLog.finest("data found using delimiter \"" //$NON-NLS-1$
                          +DataTool.delimiters[i]+"\"");  //$NON-NLS-1$
            return data;
          }
          // close the reader and open a new one
          input.close();
          input = new BufferedReader(new StringReader(dataString));
          textLine = input.readLine();
        }
      } catch(IOException e) {
        e.printStackTrace();
      }
      try {
        input.close();
      } catch(IOException ex) {
        ex.printStackTrace();
      }
      return null;
    }
  
    //______________________________ protected methods ________________________
    protected String readFirstLine(Reader in) {
      BufferedReader input = null;
      if(in instanceof BufferedReader) {
        input = (BufferedReader) in;
      } else {
        input = new BufferedReader(in);
      }
      String openingLine;
      try {
        openingLine = input.readLine();
        while((openingLine==null)||openingLine.equals("")) { //$NON-NLS-1$
          openingLine = input.readLine();
        }
      } catch(IOException e) {
        e.printStackTrace();
        return null;
      }
      try {
        input.close();
      } catch(IOException ex) {
        ex.printStackTrace();
      }
      return openingLine;
    }
  
    /**
     * Gets a unique name.
     *
     * @param proposed the proposed name
     * @return the unique name
     */
    protected String getUniqueTabName(String proposed) {
      if((proposed==null)||proposed.equals("")) {                 //$NON-NLS-1$
        proposed = ToolsRes.getString("DataToolTab.DefaultName"); //$NON-NLS-1$
      }
      // collect existing tab names
      ArrayList<String> taken = new ArrayList<String>();
      for(int i = 0; i<getTabCount(); i++) {
        DataToolTab tab = getTab(i);
        taken.add(tab.getName());
      }
      if(!taken.contains(proposed)) {
        return proposed;
      }
      // strip existing numbered subscript if any
      String subscript = TeXParser.getSubscript(proposed);
      try {
        Integer.parseInt(subscript);
        proposed = TeXParser.removeSubscript(proposed);
      } catch(Exception ex) {}
      // construct a unique name from proposed by adding digit
      proposed += "_"; //$NON-NLS-1$
      int i = 1;
      String name = proposed+i;
      while(taken.contains(name)) {
        i++;
        name = proposed+i;
      }
      return name;
    }
  
    /**
     * Loads self-contained Data objects from an XMLControl.
     *
     * @param control the XMLControl
     * @param useChooser true to present data choices to user
     *
     * @return a list of self-contained Data objects
     */
    private static ArrayList<Data> getSelfContainedData(XMLControl control, boolean useChooser) {
      ArrayList<Data> dataList = new ArrayList<Data>();
      java.util.List<XMLProperty> xmlControls;
      // first get the Data XMLControls
      if(useChooser) {
        // get user-selected Data XMLControls from an xml tree chooser
        XMLTreeChooser chooser = new XMLTreeChooser(ToolsRes.getString("Chooser.Title"),        //$NON-NLS-1$
          ToolsRes.getString("Chooser.Label"), null);                                           //$NON-NLS-1$
        xmlControls = chooser.choose(control, Data.class);
      } else {
        // get all Data XMLControls
        XMLTree tree = new XMLTree(control);
        tree.setHighlightedClass(Data.class);
        tree.selectHighlightedProperties();
        xmlControls = tree.getSelectedProperties();
        if(xmlControls.isEmpty()) {
          JOptionPane.showMessageDialog(null, ToolsRes.getString("Dialog.NoDatasets.Message")); //$NON-NLS-1$
        }
      }
      // load the Data XMLControls and collect Data objects
      HashSet<Integer> IDs = new HashSet<Integer>();
      for(XMLProperty prop : xmlControls) {
        XMLControl next = (XMLControl) prop;
        Data data = null;
        if(next instanceof XMLControlElement) {
          XMLControlElement element = (XMLControlElement) next;
          data = (Data) element.loadObject(null, true, true);
        } else {
          data = (Data) next.loadObject(null);
        }
        if(data!=null) {
          for(Data nextData : getSelfContainedData(data)) {
            // check IDs to prevent duplicates
            Integer id = new Integer(nextData.getID());
            if(!IDs.contains(id)) {
              IDs.add(id);
              dataList.add(nextData);
              // remove any previously added Datasets within DatasetManagers
              if(nextData instanceof DatasetManager) {
                for(Dataset dataset : nextData.getDatasets()) {
                  dataList.remove(dataset);
                  id = new Integer(dataset.getID());
                  IDs.add(id);
                }
              }
            }
          }
        }
      }
      return dataList;
    }
  
    /**
     * Loads data from an XMLControl into one or more tabs.
     *
     * @param control the XMLControl describing the data
     * @param useChooser true to present data choices to user
     *
     * @return a list of loaded tabs
     */
    private ArrayList<DataToolTab> loadTabsFromXML(XMLControl control, boolean useChooser) {
      ArrayList<DataToolTab> loadedTabs = new ArrayList<DataToolTab>();
      ArrayList<Data> dataList = getSelfContainedData(control, useChooser);
      for(Data next : dataList) {
        loadedTabs.add(createTab(next));
      }
      return loadedTabs;
    }
  
    /**
     * Constructs a dataset from independent xColumn and yColumn datasets.
     *
     * @param xColumn the dataset containing data for the x column
     * @param yColumn the dataset containing data for the y column
     * @return the x-y dataset
     */
    public static Dataset createDatasetFromYPoints(Dataset xColumn, Dataset yColumn) {
      Dataset dataset = new Dataset();
      dataset.setXYColumnNames(xColumn.getYColumnName(), yColumn.getYColumnName());
  //    dataset.setConnected(true);
      dataset.setLineColor(yColumn.getLineColor());
      dataset.setMarkerShape(yColumn.getMarkerShape());
      dataset.setMarkerColor(yColumn.getFillColor(), yColumn.getEdgeColor());
      double[] xPoints = xColumn.getYPoints();
      double[] yPoints = yColumn.getYPoints();
      if (xPoints.length != yPoints.length) {
      	int len = Math.min(xPoints.length, yPoints.length);
      	double[] newPoints = new double[len];
      	for (int i = 0; i < len; i++) {
      		newPoints[i] = len<xPoints.length? xPoints[i]: yPoints[i];
      	}
      	if (len<xPoints.length)
      		xPoints = newPoints;
      	else yPoints = newPoints;
      }
      dataset.append(xPoints, yPoints);
      return dataset;
    }
  
    /**
     * Gets a list of Datasets from a self-contained source Data object.
     *
     * @param source the self-contained Data
     * @return a list of Datasets
     */
    public static ArrayList<Dataset> getDatasets(Data source) {
      // if the source supplies datasets, return them
      ArrayList<Dataset> datasets = source.getDatasets();
      if(datasets!=null) {
        return datasets;
      }
      // else create an empty dataset list and populate it from data2D
      datasets = new ArrayList<Dataset>();
      double[][] data2D = source.getData2D();
      if((data2D==null)||(data2D.length==0)||(data2D[0]==null)) {
        return datasets; // return empty list
      }
      // get column names--minimum 2
      // colNames[0] will be x-column name
      // colNames[1] and up will be y-column names
      String[] colNames = source.getColumnNames();
      // if colNames is null, create colNames
      if(colNames==null) {
        colNames = new String[2];
        // if only one point set in data2D, then first column is named "n"
        if(data2D.length==1) {
          colNames[0] = "n"; //$NON-NLS-1$
        }
      }
      int n = Math.max(2, data2D.length); // number of columns
      if(colNames.length>n) {
        n++;
      }
      colNames = getColumnNames(colNames, n);
      // create datasets for double[] columns
      // if more names than double[] columns, then first double[] is row numbers 
      boolean xPointsAreRowNumbers = colNames.length>data2D.length;
      double[] xPoints = xPointsAreRowNumbers ? getRowArray(data2D[0].length) : data2D[0];
      for(int i = 1; i<colNames.length; i++) {
        double[] yPoints = xPointsAreRowNumbers ? data2D[i-1] : data2D[i];
        Dataset dataset = createDataset(xPoints, yPoints, colNames[0], colNames[i], i, source);
        if(dataset!=null) {
          datasets.add(dataset);
        }
      }
      return datasets;
    }
  
    /**
     * Gets a list of all Datasets from any Data object.
     *
     * @param source a self-contained or container Data object
     * @return a list of all Datasets
     */
    public static ArrayList<Dataset> getAllDatasets(Data source) {
      ArrayList<Dataset> datasets = new ArrayList<Dataset>();
      for(Data next : getSelfContainedData(source)) {
        datasets.addAll(getDatasets(next));
      }
      return datasets;
    }
  
    /**
     * Gets a list of self-contained Data objects.
     *
     * @param container the container Data
     *
     * @return a list of self-contained Data objects
     */
    protected static ArrayList<Data> getSelfContainedData(Data container) {
      processedData.clear();
      ArrayList<Data> list = getSelfContainedDataWithTrap(container);
      return list;
    }
  
    /**
     * Gets a list of DataColumns from a self-contained Data object.
     *
     * @param source a self-contained Data object
     * @return a list of DataColumns
     */
    protected static ArrayList<DataColumn> getDataColumns(Data source) {
      if(!isSelfContained(source)) {
        return null;
      }
      ArrayList<DataColumn> columns = new ArrayList<DataColumn>();
      // look for datasets in Data
      ArrayList<Dataset> datasetList = source.getDatasets();
      if (datasetList!=null) {
        for (Dataset next : datasetList) {
        	// get new columns from next dataset
        	ArrayList<DataColumn> newColumns = createDataColumns(next);
        	for (DataColumn newCol: newColumns) {
          	// add new columns that are not exact duplicates of existing columns
        		boolean isDup = false;
        		for (DataColumn existing: columns) {
        			if (existing.getYColumnName().equals(newCol.getYColumnName())) {
        				double[] exPts = existing.getYPoints();
        				double[] nextPts = newCol.getYPoints();
        				if (exPts.length == nextPts.length) {
        					isDup = true;
        					for (int i = 0; i < exPts.length; i++) {
        						isDup = exPts[i]==nextPts[i] && isDup;
        					}
        				}
        			}
        		}
            if (!isDup)
            	columns.add(newCol);
        	}
        }
      } 
      else {
        double[][] data2D = source.getData2D();
        if((data2D==null)||(data2D.length==0)||(data2D[0]==null)) {
          return null;
        }
        // get column names
        String[] colNames = source.getColumnNames();
        // if colNames is null, create colNames
        if(colNames==null) {
          colNames = new String[2];
          // if only one point set in data2D, then first column is named "n"
          if(data2D.length==1) {
            colNames[0] = "n";              //$NON-NLS-1$
          }
        }
        int n = Math.max(2, data2D.length); // number of columns: minimum 2
        if(colNames.length>n) {
          n++;
        }
        colNames = getColumnNames(colNames, n);
        // create a data column for each name
        // if more names than double[] columns, then first double[] is row numbers 
        boolean includeRows = colNames.length>data2D.length;
        int index = data2D[0].length;
        for(int i = 0; i<data2D.length; i++) {
          if(data2D[i]!=null) {
            index = Math.max(index, data2D[i].length);
          }
        }
        for(int i = 0; i<colNames.length; i++) {
          double[] colData = includeRows ? (i==0) ? getRowArray(index) : data2D[i-1] : data2D[i];
          DataColumn dataset = createDataColumn(colData, colNames[i], i, source);
          if(dataset!=null) {
            columns.add(dataset);
          }
        }
      }
      return columns;
    }
  
    /**
     * Gets a list of all DataColumns from any Data object.
     *
     * @param source a self-contained or container Data object
     * @return a list of all DataColumns
     */
    protected static ArrayList<DataColumn> getAllDataColumns(Data source) {
      ArrayList<DataColumn> columns = new ArrayList<DataColumn>();
      for(Data next : getSelfContainedData(source)) {
        columns.addAll(getDataColumns(next));
      }
      return columns;
    }
  
    /**
     * Gets an array of column names.
     *
     * @param proposed the proposed names
     * @param nameCount the required number of names
     * @return the column names
     */
    private static String[] getColumnNames(String[] proposed, int nameCount) {
      String[] colNames = proposed;
      if(colNames.length!=nameCount) {
        colNames = new String[nameCount];
        int len = Math.min(proposed.length, colNames.length);
        System.arraycopy(proposed, 0, colNames, 0, len);
      }
      // deal with null names 
      ArrayList<String> taken = new ArrayList<String>();
      char c = 'A';
      for(int i = 0; i<nameCount; i++) {
        String next = colNames[i];
        if((next!=null)&&!taken.contains(next)) {
          taken.add(next);
          continue;
        }
        if(next==null) {
          // replace null names with capital letters
          next = String.valueOf(c++);
          while(taken.contains(next)) {
            next = String.valueOf(c++);
          }
          colNames[i] = next;
          taken.add(next);
        }
      }
      return colNames;
    }
  
    /**
     * Gets a list of Data objects which provide actual data. Each Data object
     * in the list is suitable for adding to a tab. This method traps for duplicate
     * Data objects and prevents infinite loops.
     *
     * @param source the source Data object
     * @return a list of data-producing Data objects
     */
    private static ArrayList<Data> getSelfContainedDataWithTrap(Data source) {
      ArrayList<Data> list = new ArrayList<Data>();
      if((source==null)||processedData.contains(source)) {
        return list;
      }
      processedData.add(source);
      if(isSelfContained(source)) {
        list.add(source);
      } else {
        for(Data next : source.getDataList()) {
          ArrayList<Data> subList = getSelfContainedDataWithTrap(next);
          list.addAll(subList);
        }
      }
      return list;
    }
  
    /**
     * Determines if a Data object is self-contained.
     *
     * @param data the Data object
     * @return true if self-contained
     */
    private static boolean isSelfContained(Data data) {
      return data.getDataList()==null;
    }
  
    /**
     * Creates data columns from the visible columns of a dataset.
     *
     * @param source the source dataset
     * @return a list of data columns
     */
    private static ArrayList<DataColumn> createDataColumns(Dataset source) {
      ArrayList<DataColumn> columns = new ArrayList<DataColumn>();
      if(source instanceof DataColumn) {
        columns.add((DataColumn) source);
        return columns;
      }
      String[] colNames = source.getColumnNames();
      String rowName = "row"; //$NON-NLS-1$
      for(int i = 0; i<2; i++) {
        if((i==0)&&!source.isXColumnVisible()) {
          continue;
        }
        if((i==1)&&!source.isYColumnVisible()) {
          continue;
        }
        DataColumn column = new DataColumn();
        column.setName(source.getName());
        column.setXYColumnNames(rowName, colNames[i]);
        column.setConnected(source.isConnected());
        column.setLineColor(source.getLineColor());
        column.setMarkerSize(source.getMarkerSize());
        column.setMarkerShape(source.getMarkerShape());
        column.setMarkerColor(source.getFillColor(), source.getLineColor());
        column.setID(source.getID());
        column.setColumnID(i);
        column.setPoints((i==0) ? source.getXPoints() : source.getYPoints());
        column.setXColumnVisible(false);
        columns.add(column);
      }
      return columns;
    }
  
    /**
     * Creates a data column from a double[] of data.
     *
     * @param data the data points
     * @param columnName the name of the column
     * @param columnID the column ID to assign the column
     * @param source the Data source providing the ID and colors
     * @return the data column dataset
     */
    private static DataColumn createDataColumn(double[] data, String columnName, int columnID, Data source) {
      if(data==null) {
        return null;
      }
      DataColumn column = new DataColumn();
      column.setXYColumnNames("row", columnName); //$NON-NLS-1$
      column.setConnected(true);
      Color[] lineColors = source.getLineColors();
      if((lineColors!=null)&&(lineColors[columnID]!=null)) {
        column.setLineColor(lineColors[columnID]);
      } else {
        column.setLineColor(DisplayColors.getLineColor(columnID));
      }
      column.setMarkerShape(Dataset.SQUARE);
      Color[] fillColors = source.getFillColors();
      if((lineColors!=null)&&(lineColors[columnID]!=null)&&(fillColors!=null)&&(fillColors[columnID]!=null)) {
        column.setMarkerColor(fillColors[columnID], lineColors[columnID]);
      } else {
        column.setMarkerColor(DisplayColors.getMarkerColor(columnID), DisplayColors.getLineColor(columnID));
      }
      column.setID(source.getID());
      column.setColumnID(columnID);
      column.setPoints(data);
      return column;
    }
  
    /**
     * Creates a dataset from double[] of data.
     *
     * @param xPoints the x points
     * @param yPoints the y points
     * @param xName the name of the x column
     * @param yName the name of the y column
     * @param columnID the column ID
     * @param source the Data source providing the ID and colors
     * @return the dataset
     */
    private static Dataset createDataset(double[] xPoints, double[] yPoints, String xName, String yName, int columnID, Data source) {
      if(yPoints==null) {
        return null;
      }
      Dataset dataset = new Dataset();
      dataset.setXYColumnNames(xName, yName);
      dataset.setConnected(true);
      Color[] lineColors = source.getLineColors();
      if((lineColors!=null)&&(lineColors[columnID]!=null)) {
        dataset.setLineColor(lineColors[columnID]);
      } else {
        dataset.setLineColor(DisplayColors.getLineColor(columnID));
      }
      dataset.setMarkerShape(Dataset.SQUARE);
      Color[] fillColors = source.getFillColors();
      if((lineColors!=null)&&(lineColors[columnID]!=null)&&(fillColors!=null)&&(fillColors[columnID]!=null)) {
        dataset.setMarkerColor(fillColors[columnID], lineColors[columnID]);
      } else {
        dataset.setMarkerColor(DisplayColors.getMarkerColor(columnID), DisplayColors.getLineColor(columnID));
      }
      dataset.setID(source.getID());
      dataset.append(xPoints, yPoints);
      return dataset;
    }
  
    /**
     * Copies a dataset. If includeDataAndID is false, only the name and
     * display properties are copied.
     *
     * @param source the source dataset
     * @param target the target dataset (may be null)
     * @param includeDataAndID true to copy data and ID
     * @return the copy
     */
    public static Dataset copyDataset(Dataset source, Dataset target, boolean includeDataAndID) {
      if(target==null) {
        target = new Dataset();
      }
      if(includeDataAndID) {
        target.clear();
        double[] x = source.getXPoints();
        double[] y = source.getYPoints();
        target.append(x, y);
        target.setID(source.getID());
      }
      target.setName(source.getName());
      target.setXYColumnNames(source.getXColumnName(), source.getYColumnName());
      target.setMarkerShape(source.getMarkerShape());
      target.setMarkerSize(source.getMarkerSize());
      Color fill = source.getFillColor();
      Color edge = source.getEdgeColor();
      target.setMarkerColor(fill, edge);
      target.setLineColor(source.getLineColor());
      target.setConnected(source.isConnected());
      target.setXColumnVisible(source.isXColumnVisible());
      target.setYColumnVisible(source.isYColumnVisible());
      return target;
    }
  
    /**
     * Inserts a specified value into an array.
     *
     * @param input the value to insert
     * @param array the array into which the value is inserted
     * @param trend positive if array is ascending, negative if descending, 0 if neither
     * @return an array containing the inserted value
     */
    protected static double[] insert(double input, double[] array, int trend) {
      int n = array.length;
      double[] newArray = new double[n+1];
      if(trend==0) {       // append at end
        System.arraycopy(array, 0, newArray, 0, n);
        newArray[n] = input;
      } else if(trend>0) { // append before first array value larger than input
        for(int i = 0; i<n; i++) {
          if(input<array[i]) {
            System.arraycopy(array, 0, newArray, 0, i);
            System.arraycopy(array, i, newArray, i+1, n-i);
            newArray[i] = input;
            return newArray;
          }
        }
        // if newArray not yet returned, then append at end
        System.arraycopy(array, 0, newArray, 0, n);
        newArray[n] = input;
      } else {             // append before first array value smaller than input
        for(int i = 0; i<n; i++) {
          if(input>array[i]) {
            System.arraycopy(array, 0, newArray, 0, i);
            System.arraycopy(array, i, newArray, i+1, n-i);
            newArray[i] = input;
            return newArray;
          }
        }
        // if newArray not yet returned, then append at end
        System.arraycopy(array, 0, newArray, 0, n);
        newArray[n] = input;
      }
      return newArray;
    }
  
    /**
     * Adds a tab. The tab should be named before calling this method.
     *
     * @param tab a DataToolTab
     */
    public void addTab(final DataToolTab tab) {
      // remove single empty tab, if any
      if(getTabCount()==1) {
        DataToolTab prev = getTab(0);
        if(prev.originatorID==0) {
          prev.tabChanged(false);
          removeTab(0, false);
        }
      }
      tab.dataTool = this;
      // assign a unique name (also traps for null name)
      tab.setName(getUniqueTabName(tab.getName()));
      OSPLog.finer("adding tab "+tab.getName()); //$NON-NLS-1$
      tabbedPane.addTab("", tab);                //$NON-NLS-1$
      tab.setFontLevel(FontSizer.getLevel());
      tabbedPane.setSelectedComponent(tab);
      //    validate();
      refreshTabTitles();
      refreshMenubar();
    }
  
    /**
     * Offers to save changes to the tab at the specified index.
     *
     * @param i the tab index
     * @return true unless canceled by the user
     */
    protected boolean saveChangesAt(int i) {
      if(OSPRuntime.appletMode) {
        return true;
      }
      DataToolTab tab = getTab(i);
      if(!tab.tabChanged) {
        return true;
      }
      String name = tab.getName();
      if(ToolsRes.getString("DataToolTab.DefaultName").equals(name) //$NON-NLS-1$
        &&(tab.originatorID==0)) {
        return true;
      }
      int selected = JOptionPane.showConfirmDialog(this, ToolsRes.getString("DataTool.Dialog.SaveChanges.Message1")+ //$NON-NLS-1$
        " \""+name+"\" "+                                             //$NON-NLS-1$ //$NON-NLS-2$
          ToolsRes.getString("DataTool.Dialog.SaveChanges.Message2"), //$NON-NLS-1$
            ToolsRes.getString("DataTool.Dialog.SaveChanges.Title"),  //$NON-NLS-1$
              JOptionPane.YES_NO_CANCEL_OPTION);
      if(selected==JOptionPane.CANCEL_OPTION) {
        return false;
      }
      if(selected==JOptionPane.YES_OPTION) {
        // save root and all owned nodes
        if(save(tab, tab.fileName)==null) {
          return false;
        }
      }
      return true;
    }
  
    /**
     * Gets the currently selected DataToolTab, if any.
     *
     * @return the selected tab
     */
    public DataToolTab getSelectedTab() {
      return(DataToolTab) tabbedPane.getSelectedComponent();
    }
  
    /**
     * Selects a DataToolTab.
     *
     * @param tab the tab to select
     */
    public void setSelectedTab(DataToolTab tab) {
      tabbedPane.setSelectedComponent(tab);
    }
  
    /**
     * Clears data by removing all tabs.
     */
    public void clearData() {
      removeAllTabs();
    }
  
    /**
     * Sets the font level.
     *
     * @param level the level
     */
    public void setFontLevel(int level) {
    	if (getJMenuBar()==null) return;
      super.setFontLevel(level);
  		FontSizer.setFonts(emptyMenubar, level);
  		FontSizer.setFonts(fileMenu, level);
   		FontSizer.setFonts(editMenu, level);
      double factor = FontSizer.getFactor(level);
      buttonHeight = (int) (factor*defaultButtonHeight);
      if(tabbedPane!=null) {
        for(int i = 0; i<getTabCount(); i++) {
          getTab(i).setFontLevel(level);
        }
      }
      if(dataBuilder!=null) {
        dataBuilder.setFontLevel(level);
      }
      if (fontSizeGroup!=null) {
  	    Enumeration<AbstractButton> e = fontSizeGroup.getElements();
  	    for (; e.hasMoreElements();) {
  	      AbstractButton button = e.nextElement();
  	      int i = Integer.parseInt(button.getActionCommand());
  	      if(i==FontSizer.getLevel()) {
  	        button.setSelected(true);
  	      }
  	    }
      }
  
  		FontSizer.setFonts(OSPLog.getOSPLog(), level);
    }
  
    @Override
    public void setVisible(boolean vis) {
    	// set preferred size the first time shown
    	if (contentPane.getPreferredSize().equals(dim)) {
  	  	double f = 1+(0.2*FontSizer.getLevel());
  	    Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
  	    int w = Math.min(screen.width-40, (int)(dim.width*f));
  