/*
 * DataPointRecorderNew.java
 *
 * Created on Apr 18, 2014 5:57am 
 */
package org.das2.components;

import org.das2.dataset.DataSetUpdateEvent;
import org.das2.dataset.VectorDataSet;
import org.das2.dataset.DataSetUpdateListener;
import org.das2.datum.DatumRange;
import org.das2.datum.Units;
import org.das2.datum.Datum;
import org.das2.datum.DatumUtil;
import org.das2.datum.TimeUtil;
import org.das2.util.monitor.NullProgressMonitor;
import org.das2.util.monitor.ProgressMonitor;
import org.das2.components.propertyeditor.PropertyEditor;
import org.das2.datum.format.DatumFormatter;
import org.das2.system.DasLogger;
import java.awt.BorderLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
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.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableColumn;
import org.das2.DasApplication;
import org.das2.datum.DatumRangeUtil;
import org.das2.datum.EnumerationUnits;
import org.das2.datum.InconvertibleUnitsException;
import org.das2.datum.UnitsUtil;
import org.das2.event.DataPointSelectionEvent;
import org.das2.qds.AbstractDataSet;
import org.das2.qds.ArrayDataSet;
import org.das2.qds.DDataSet;
import org.das2.qds.DataSetOps;
import org.das2.qds.DataSetUtil;
import org.das2.qds.QDataSet;
import org.das2.qds.SemanticOps;
import org.das2.qds.SparseDataSetBuilder;
import org.das2.qds.ops.Ops;
import org.das2.qds.util.DataSetBuilder;

/**
 * DataPointRecorderNew is a GUI for storing data points selected by the user.  
 * This is the old recorder but:
 * 1. uses QDataSet to handle the data.  No more strange internal object.
 * 2. allows the columns to be declared explicitly by code, and data is merged in by name.
 * @deprecated use DataPointRecorder, which has the same functionality as this "new" code.  DataPointRecorderNew is left because of use in Jython scripts.
 * @author  jbf
 */
public class DataPointRecorderNew extends JPanel {

    /**
     * width of time column
     */
    private static final int TIME_WIDTH = 180;

    protected JTable table;
    protected JScrollPane scrollPane;
    protected JButton updateButton;
    protected final transient List<QDataSet> dataPoints;
    private int selectRow; // this row needs to be selected after the update.
    
    /**
     * units[index]==null if HashMap contains non-datum object.
     */
    protected transient Units[] unitsArray;
    
    protected transient  Units[] defaultUnitsArray;
    
    /**
     * array of names that are also the column headers. 
     */
    protected transient  String[] namesArray;
    
    protected transient  String[] defaultNamesArray;
    
    private double[] defaultsArray;
            
    /**
     * bundleDescriptor for the dataset.
     */
    private transient QDataSet bundleDescriptor;
    
    protected AbstractTableModel myTableModel;
    private File saveFile;
    private boolean modified;
    private final JLabel messageLabel;
    private boolean active = true; // false means don't fire updates
    private transient Preferences prefs = Preferences.userNodeForPackage(this.getClass());
    private static final Logger logger = DasLogger.getLogger(DasLogger.GUI_LOG);
    private final JButton clearSelectionButton;


    private final Object namesArrayLock;
    
    private class MyTableModel extends AbstractTableModel {
        @Override
        public int getColumnCount() {
            synchronized (namesArrayLock) {
                return namesArray==null ? 0 : namesArray.length;
            }
        }

        @Override
        public String getColumnName(int j) {
            synchronized (namesArrayLock) {
                String result = namesArray[j];
                if (unitsArray[j] != null) {
                    if ( unitsArray[j] instanceof EnumerationUnits ) {
                        result += "(ordinal)";
                    } else if ( UnitsUtil.isTimeLocation( unitsArray[j] ) ) {
                        result += "(UTC)";
                    } else if ( unitsArray[j]==Units.dimensionless ) {
                        // add nothing.
                    } else {
                        result += "(" + unitsArray[j] + ")";
                    }
                }
                return result;
            }
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            return Datum.class;
        }

        
        @Override
        public int getRowCount() {
                int nrow = dataPoints.size();
                return nrow;
            }

        
        @Override
        public Object getValueAt(int i, int j) {
            QDataSet x;
            synchronized (dataPoints) {
                 x= (QDataSet) dataPoints.get(i);
            }
            if (j < x.length()) {
                Datum d = unitsArray[j].createDatum(x.value(j));
                DatumFormatter format = d.getFormatter();
                return format.format(d, unitsArray[j]);
            } else {
                throw new IndexOutOfBoundsException("no such column");
            }
        }
    }

    /** 
     * delete all the points within the interval.  This was introduced to support the
     * case where we are going to reprocess an interval, as with the  
     * RBSP digitizer.
     * 
     * @param range range to delete, end time is exclusive.
     */
    public void deleteInterval( DatumRange range ) {
        if ( !sorted ) {
            throw new IllegalArgumentException("data must be sorted");
        } else {
            synchronized ( dataPoints ) {
                Comparator comp= new Comparator() {
                    @Override
                    public int compare(Object o1, Object o2) {
                        Datum d1;
                        if ( o1 instanceof QDataSet ) {
                            d1= DataSetUtil.asDatum(((QDataSet)o1).slice(0));
                        } else if ( o1 instanceof Datum ) {
                            d1= (Datum)o1;
                        } else {
                            throw new IllegalArgumentException("expected Datum or QDataSet");
                        }
                        Datum d2;
                        if ( o2 instanceof QDataSet ) {
                            d2= DataSetUtil.asDatum(((QDataSet)o2).slice(0));
                        } else if ( o2 instanceof Datum ) {
                            d2= (Datum)o2;
                        } else {
                            throw new IllegalArgumentException("expected Datum or QDataSet");
                        }
                        return d1.compareTo(d2);
                    }
                };
                int index1= Collections.binarySearch( dataPoints, range.min(), comp );
                if ( index1<0 ) index1= ~index1;
                int index2= Collections.binarySearch( dataPoints, range.max(), comp );
                if ( index2<0 ) index2= ~index2;
                if ( index1==index2 ) return;
                int[] arr= new int[ index2-index1 ];
                for ( int i=0; i<arr.length ; i++ ) arr[i]= index1+i;
                deleteRows( arr );
            }
        }
    }
    
    /**
     * delete the specified row.
     * @param row the row, where zero is the first element.
     */
    public void deleteRow(int row) {
        synchronized (dataPoints) {
            dataPoints.remove(row);
            modified = true;
            updateClients();
            updateStatus();
        }
        if ( active ) {
            fireDataSetUpdateListenerDataSetUpdated(new DataSetUpdateEvent(this,getDataSet()));
        }
        myTableModel.fireTableDataChanged();
    }
    
    /**
     * delete the specified rows.
     * @param selectedRows the rows, where zero is the first element..
     */
    public void deleteRows(int[] selectedRows) {
        synchronized ( dataPoints ) {
            for ( int i = selectedRows.length-1; i>=0; i-- ) {
               dataPoints.remove(selectedRows[i]);
            }
            modified = true;
        }
        updateClients();
        updateStatus();
        if ( active ) {
            fireDataSetUpdateListenerDataSetUpdated(new DataSetUpdateEvent(this,getDataSet()));
        }
        myTableModel.fireTableDataChanged();
    }
    

    /**
     * returns a data set of the table data.
     * @return  a data set of the table data.
     */
    public QDataSet getDataSet() {
        DataSetBuilder b;
        synchronized ( dataPoints ) {
            if (dataPoints.isEmpty()) {
                return null;
            } else {
                b= new DataSetBuilder(2,dataPoints.size(),bundleDescriptor.length());
                b.putProperty( QDataSet.BUNDLE_1, bundleDescriptor );
                for (int irow = 0; irow < dataPoints.size(); irow++) {
                    QDataSet dp = dataPoints.get(irow);
                    b.putValues( -1, dp, dp.length() );
                    b.nextRecord();
                }
            }
        }
        return b.getDataSet();
    }
    
    /**
     * returns a data set of the selected table data.  Warning: this used to
     * return a bundle dataset with Y,plane1,plane2,etc that had DEPEND_0 for X.
     * This now returns a bundle ds[n,m] where m is the number of columns and
     * n is the number of records.
     * @return a data set of the selected table data.
     * @see #select(org.das2.datum.DatumRange, org.das2.datum.DatumRange) which selects part of the dataset.
     */
    public QDataSet getSelectedDataSet() {
        int[] selectedRows;
        List<QDataSet> ldataPoints;
        QDataSet lbundleDescriptor;
        synchronized (this) {
            selectedRows= getSelectedRowsInModel();
            ldataPoints= new ArrayList( dataPoints );
            lbundleDescriptor= bundleDescriptor;
        }
        DataSetBuilder b;
        if (selectedRows.length == 0) {
            return null;
        } else {
            b= new DataSetBuilder(2,selectedRows.length,lbundleDescriptor.length());
            b.putProperty( QDataSet.BUNDLE_1, lbundleDescriptor );
            for (int i = 0; i < selectedRows.length; i++) {
                int irow = selectedRows[i];
                if ( irow<ldataPoints.size() ) {
                    QDataSet dp = (QDataSet) ldataPoints.get(irow);
                    b.putValues( -1, dp, dp.length() );
                    b.nextRecord();
                }
            }
            return b.getDataSet();
        }
    }

    /**
     * Selects all the points where the first column is within xrange and
     * the second column is within yrange.
     * @param xrange the range constraint (non-null).
     * @param yrange the range constraint (non-null).
     * return the selected index, or -1 if no elements are found.
     */
    public void select(DatumRange xrange, DatumRange yrange) {
        Datum mid= DatumRangeUtil.rescale( xrange,0.5,0.5 ).min();
        synchronized (dataPoints) {
            List<Integer> selectMe = new ArrayList();
            int iclosest= -1;
            Datum closestDist=null;
            for (int i = 0; i < dataPoints.size(); i++) {
                QDataSet p = (QDataSet) dataPoints.get(i);
                if ( xrange.contains( DataSetUtil.asDatum(p.slice(0)) ) && yrange.contains(DataSetUtil.asDatum(p.slice(1))) ) {
                    selectMe.add( i );
                }
                if ( closestDist==null || DataSetUtil.asDatum((QDataSet)p.slice(0)).subtract(mid).abs().lt( closestDist ) ) {
                    iclosest= i;
                    closestDist= DataSetUtil.asDatum((QDataSet)p.slice(0)).subtract(mid).abs();
                }
            }
            if ( iclosest!=-1 && selectMe.isEmpty() ) {
                selectMe= Collections.singletonList(iclosest);
            }
            table.getSelectionModel().clearSelection();
            for (int i = 0; i < selectMe.size(); i++) {
                int iselect = selectMe.get(i);
                table.getSelectionModel().addSelectionInterval(iselect, iselect);
            }

            if ( selectMe.size()>0 ) {
                int iselect= selectMe.get(0);
                table.scrollRectToVisible(new Rectangle(table.getCellRect( iselect, 0, true)) );
            }
        }
    }

    /**
     * This should be called off the event thread.
     * @param file
     * @throws IOException 
     */
    public void saveToFile(File file) throws IOException {
        List<QDataSet> dataPoints1;
        String[] lnamesArray;
        Units[] lunitsArray;
        synchronized (this) {
            lnamesArray= Arrays.copyOf(namesArray,namesArray.length);
            lunitsArray= Arrays.copyOf(unitsArray,unitsArray.length);
            dataPoints1= new ArrayList( dataPoints );
        }
        FileOutputStream out = new FileOutputStream(file);
        try (BufferedWriter r = new BufferedWriter(new OutputStreamWriter(out))) {
            StringBuilder header = new StringBuilder();
            //header.append("## "); // don't use comment characters so that labels and units are used in Autoplot's ascii parser.
            for (int j = 0; j < lnamesArray.length; j++) {
                Units units= lunitsArray[j];
                String sunits;
                if ( UnitsUtil.isTimeLocation(units) ) {
                    sunits= "(UTC)";
                } else if ( UnitsUtil.isOrdinalMeasurement(units) ) {
                    sunits= "(ordinal)";
                } else if ( units==Units.dimensionless ) {
                    sunits= "";
                } else {
                    sunits= "("+units+")";
                }
                header.append( lnamesArray[j] ).append(sunits);
                if ( j<lnamesArray.length-1 ) header.append("\t");
            }
            r.write(header.toString());
            r.newLine();
            for (int i = 0; i < dataPoints1.size(); i++) {
                QDataSet x = (QDataSet) dataPoints1.get(i);
                StringBuilder s = new StringBuilder();
                for (int j = 0; j < x.length(); j++) {
                    Datum d= DataSetUtil.asDatum( x.slice(j) );
                    DatumFormatter formatter = d.getFormatter();
                    s.append( formatter.format( d, lunitsArray[j]).trim() );
                    if ( j<x.length()-1 ) s.append("\t");
                }
                r.write(s.toString());
                r.newLine();
                prefs.put("components.DataPointRecorder.lastFileSave", file.toString());
                prefs.put("components.DataPointRecorder.lastFileLoad", file.toString());
            }
        }
        modified = false;
        updateStatus();

    }

    private int lineCount( File file ) throws IOException {
         BufferedReader r=null;
         int lineCount = 0;
         try {
            FileInputStream in = new FileInputStream(file);
            r = new BufferedReader(new InputStreamReader(in));


            for (String line = r.readLine(); line != null; line = r.readLine()) {
                lineCount++;
            }
        } catch ( IOException ex ) {
            throw ex;

        } finally {
            if ( r!=null ) r.close();
        }
        return lineCount;
        
    }

    /**
     * load the dataset from the file.  
     * TODO: this should be redone.
     * 
     * @param file
     * @throws IOException 
     */
    public void loadFromFile(File file) throws IOException {

        ProgressMonitor mon= new NullProgressMonitor();

        BufferedReader r=null;

        boolean active0= active;
        
        try {
            active = false;

            int lineCount= lineCount( file );

            r = new BufferedReader( new FileReader( file ) );

            dataPoints.clear();
            String[] namesArray1;
            Units[] unitsArray1 = null;
            QDataSet bundleDescriptor1= null;
            SparseDataSetBuilder bdsb= new SparseDataSetBuilder(2);
            
            if (lineCount > 500) {
                mon = DasProgressPanel.createFramed("reading file");
            }

            // tabs detected in file.
            String delim= ",";
            
            mon.setTaskSize(lineCount);
            mon.started();
            int linenum = 0;
            
            final List<QDataSet> records= new ArrayList<>(1440);
            
            for (String line = r.readLine(); line != null; line = r.readLine()) {
                linenum++;
                if (mon.isCancelled()) {
                    break;
                }
                line= line.trim();
                if ( line.length()==0 ) {
                    continue;
                }
                mon.setTaskProgress(linenum);
                if (line.startsWith("## ") || line.length()>0 && Character.isJavaIdentifierStart( line.charAt(0) ) ) {
                    if ( unitsArray1!=null ) continue;
                    while ( line.startsWith("#") ) line = line.substring(1);
                    if ( !line.contains(delim) ) delim= "\t";
                    if ( !line.contains(delim) ) delim= "\\s+";
                    String[] s = line.split(delim);
                    for ( int i=0; i<s.length; i++ ) {
                        s[i]= s[i].trim();
                    }                    
                    Pattern p = Pattern.compile("(.+)\\((.*)\\)");
                    namesArray1 = new String[s.length];
                    unitsArray1 = new Units[s.length];
                    for (int i = 0; i < s.length; i++) {
                        Matcher m = p.matcher(s[i]);
                        if (m.matches()) {
                            //System.err.printf("%d %s\n", i, m.group(1) );
                            namesArray1[i] = m.group(1).trim();
                            String m2= m.group(2).trim();
                            try {
                                switch (m2) {
                                    case "UTC":
                                        unitsArray1[i] = Units.cdfTT2000;
                                        break;
                                    case "ordinal":
                                        unitsArray1[i] = EnumerationUnits.create("default");
                                        break;
                                    default:
                                        unitsArray1[i] = Units.lookupUnits(m.group(2).trim());
                                        break;
                                }
                            } catch (IndexOutOfBoundsException e) {
                                throw e;
                            }
                        } else {
                            namesArray1[i] = s[i].trim();
                            unitsArray1[i] = Units.dimensionless;
                        }
                        bdsb.putProperty( QDataSet.NAME, i, namesArray1[i] );
                        bdsb.putProperty( QDataSet.UNITS, i, unitsArray1[i] );
                    }
                    bdsb.setLength( s.length );
                    continue;
                }
                String[] s = line.split(delim);
                for ( int i=0; i<s.length; i++ ) {
                    String s1= s[i];
                    s1= s1.trim();
                    if ( s1.startsWith("\"") && s1.endsWith("\"") ) { // pop off quotes used to delimit enumeration e.g. "fuh"
                        s1= s1.substring(1,s1.length()-1);
                    }
                    s[i]= s1;
                }
                if (unitsArray1 == null) {
                    // support for legacy files
                    unitsArray1 = new Units[s.length];
                    for (int i = 0; i < s.length; i++) {
                        if (s[i].charAt(0) == '"') {
                            unitsArray1[i] = null;
                        } else if (TimeUtil.isValidTime(s[i])) {
                            unitsArray1[i] = Units.us2000;
                        } else {
                            if ( s[i].startsWith("0x") ) {
                                unitsArray1[i]= Units.dimensionless;
                            } else {
                                unitsArray1[i] = DatumUtil.parseValid(s[i]).getUnits();
                            }
                        }
                    }
                }

                try {
                    DDataSet rec= DDataSet.createRank1(s.length);
                    for (int i = 0; i < s.length; i++) {
                        if (unitsArray1[i] == null) {
                            Pattern p = Pattern.compile("\"(.*)\".*");
                            Matcher m = p.matcher(s[i]);
                            if (m.matches()) {
                                EnumerationUnits eu= EnumerationUnits.create("default");
                                unitsArray1[i]= eu;
                                rec.putValue(i,eu.createDatum( m.group(1) ).doubleValue(eu));
                            } else {
                                throw new ParseException("parse error, expected \"\"", 0);
                            }
                        } else {
                            try {
                                if ( unitsArray1[i] instanceof EnumerationUnits ) {
                                    EnumerationUnits eu= (EnumerationUnits)unitsArray1[i];
                                    rec.putValue( i,eu.createDatum(  s[i] ).doubleValue(eu)  );
                                } else {
                                    rec.putValue( i, unitsArray1[i].parse(s[i]).doubleValue(unitsArray1[i]) );
                                }
                            } catch (ParseException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }

                    if ( bundleDescriptor1==null ) {
                        bundleDescriptor1= bdsb.getDataSet();
                    }
                    rec.putProperty( QDataSet.BUNDLE_0, bundleDescriptor1 );

                    records.add(rec);
                    
                    //addDataPoint( rec );

                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }

            }

            r.close();

            Runnable run= new Runnable() {
                @Override
                public void run() {
                    for ( QDataSet rec: records ) {
                        addDataPoint( rec );
                    }
                    updateStatus();
                    updateClients();
                    fireDataSetUpdateListenerDataSetUpdated(new DataSetUpdateEvent(this));
                }
            };
            
            saveFile= file;  // go ahead and set this in case client is going to do something with this.

            prefs.put("components.DataPointRecorder.lastFileLoad", file.toString());
            
            if ( SwingUtilities.isEventDispatchThread() ) {
                run.run();
            } else {
                try {
                    SwingUtilities.invokeAndWait(run);
                } catch (InterruptedException | InvocationTargetException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }
            
        } finally {

            mon.finished();

            if ( r!=null ) r.close();

            //active = true;
            active= active0;
            modified = false;

            //table.getColumnModel();
            myTableModel.fireTableStructureChanged();
            table.repaint();
        }
        
        if (active) {
            DataSetUpdateEvent ev= new DataSetUpdateEvent(this,getDataSet());
            fireDataSetUpdateListenerDataSetUpdated( ev );
        }

    }

    /**
     * active=true means fire off events on any change.  false= wait for update button.
     * @param active
     */
    public void setActive( boolean active ) {
        this.active= active;
    }

    /**
     * return the index into the model for the selection
     * @return 
     */
    private int[] getSelectedRowsInModel() {
        int[] selectedRows = table.getSelectedRows();
        for ( int i=0; i<selectedRows.length; i++ ) {
            selectedRows[i]= table.convertRowIndexToModel(selectedRows[i]);
        }
        return selectedRows;
    }
    
    private class MyMouseAdapter extends MouseAdapter {

        JPopupMenu popup;
        JMenuItem menuItem;
        final JTable parent;

        MyMouseAdapter(final JTable parent) {
            this.parent = parent;
            popup = new JPopupMenu("Options");
            menuItem = new JMenuItem("Delete Row(s)");
            menuItem.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    int[] selectedRows = getSelectedRowsInModel();
                    deleteRows(selectedRows);
                    //for (int i = 0; i < selectedRows.length; i++) {
                    //    deleteRow(selectedRows[i]);
                    //    for (int j = i + 1; j < selectedRows.length; j++) {
                    //        selectedRows[j]--; // indeces change because of deletion
                    //    }
                    //}
                }
            });
            popup.add(menuItem);
        }

        @Override
        public void mousePressed(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON3) {
                int rowCount = parent.getSelectedRows().length;
                menuItem.setText("Delete " + rowCount + " Row" + (rowCount != 1 ? "s" : ""));
                popup.show(e.getComponent(), e.getX(), e.getY());
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        // hide popup
        }
    }

    private Action getSaveAsAction() {
        return new AbstractAction("Save As...") {
            @Override
            public void actionPerformed(ActionEvent e) {
                saveAs();
            }
        };
    }

    private Action getSaveAction() {
        return new AbstractAction("Save") {
            @Override
            public void actionPerformed(ActionEvent e) {
                save();
            }
        };
    }

    private Action getClearSelectionAction() {
        return new AbstractAction("Clear Selection") {
            @Override
            public void actionPerformed(ActionEvent e) {
                table.getSelectionModel().clearSelection();
                fireSelectedDataSetUpdateListenerDataSetUpdated(new DataSetUpdateEvent(this));  
            }
        };
    }

    
    /**
     * return true if the file was saved, false if cancel
     * @return true if the file was saved, false if cancel
     */
    public boolean saveAs() {
        final JFileChooser jj = new JFileChooser();
        final Map<String,Integer> statusHolder= new HashMap<>();
        Runnable run= new Runnable() {
            @Override
            public void run() {
                jj.setFileFilter( new FileFilter() {
                    @Override
                    public boolean accept(File pathname) {
                        if ( pathname.isDirectory() ) return true;
                        String fn= pathname.getName();
                        return fn.endsWith(".dat") || fn.endsWith(".txt");
                    }
                    @Override
                    public String getDescription() {
                        return "Flat Ascii Tables";
                    }
                });
                String lastFileString = prefs.get("components.DataPointRecorder.lastFileSave", "");
                if (lastFileString.length()>0) {
                    File lastFile= new File(lastFileString);
                    jj.setSelectedFile(lastFile);
                }

                statusHolder.put( "status", jj.showSaveDialog(DataPointRecorderNew.this) );
                
            }
        };
        
        if ( SwingUtilities.isEventDispatchThread() ) {
            run.run();
        } else {
            try {
                SwingUtilities.invokeAndWait(run);
            } catch (InterruptedException | InvocationTargetException ex) {
                logger.log(Level.SEVERE, null, ex);
            }
        }
        
        int status= statusHolder.get("status") ;
       
        if (status == JFileChooser.APPROVE_OPTION) {
            try {
                File pathname= jj.getSelectedFile();
                if ( !( pathname.toString().endsWith(".dat") || pathname.toString().endsWith(".txt") ) ) {
                    pathname= new File( pathname.getAbsolutePath() + ".dat" );
                }
                DataPointRecorderNew.this.saveFile = pathname;
                saveToFile(saveFile);
            //messageLabel.setText("saved data to "+saveFile);
            } catch (IOException e1) {
                DasApplication.getDefaultApplication().getExceptionHandler().handle(e1);
                return false;
            }
        } else if ( status == JFileChooser.CANCEL_OPTION ) {
            return false;
        }
        return true;
    }

    public boolean save() {
        if (saveFile == null) {
            return saveAs();
        } else {
            try {
                saveToFile(saveFile);
                return true;
            } catch (IOException ex) {
                DasApplication.getDefaultApplication().getExceptionHandler().handle(ex);
                return false;
            }
        }
    }

    /**
     * shows the current name for the file.
     * @return the current name for the file.
     */
    public File getCurrentFile() {
        return this.saveFile;
    }


    /**
     * return true if the file was saved or "don't save" was pressed by the user.
     * @return true if the file was saved or "don't save" was pressed by the user.
     */
    public boolean saveBeforeExit( ) {
        if ( this.modified ) {
            int i= JOptionPane.showConfirmDialog( this, "Save changes before exiting?");
            switch (i) {
                case JOptionPane.OK_OPTION:
                    return save();
                case JOptionPane.CANCEL_OPTION:
                    return false;
                default:
                    return true;
            }
        } else {
            return true;
        }
    }

    private Action getLoadAction() {
        return new AbstractAction("Open...") {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (checkModified(e)) {
                    JFileChooser jj = new JFileChooser();
                    String lastFileString = prefs.get("components.DataPointRecorder.lastFileLoad", "");
                    if ( lastFileString.length()>0 ) {
                        File lastFile;
                        lastFile = new File(lastFileString);
                        jj.setSelectedFile(lastFile);
                    }

                    int status = jj.showOpenDialog(DataPointRecorderNew.this);
                    if (status == JFileChooser.APPROVE_OPTION) {
                        final File loadFile = jj.getSelectedFile();
                        prefs.put("components.DataPointRecorder.lastFileLoad", loadFile.toString());
                        Runnable run = new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    loadFromFile(loadFile);
                                    updateStatus();
                                } catch (IOException e) {
                                    DasApplication.getDefaultApplication().getExceptionHandler().handle(e);
                                }

                            }
                        };
                        new Thread(run).start();
                    }

                }
            }
        };
    }

    /**
     * returns true if the operation should continue, false
     * if not, meaning the user pressed cancel.
     */
    private boolean checkModified(ActionEvent e) {
        if (modified) {
            int n = JOptionPane.showConfirmDialog(
                    DataPointRecorderNew.this,
                    "Current work has not been saved.\n  Save first?",
                    "Save work first",
                    JOptionPane.YES_NO_CANCEL_OPTION);
            if (n == JOptionPane.YES_OPTION) {
                getSaveAction().actionPerformed(e);
            }

            return (n != JOptionPane.CANCEL_OPTION);
        } else {
            return true;
        }

    }

    private Action getNewAction() {
        return new AbstractAction("New") {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (checkModified(e)) {
                    dataPoints.clear();
                    saveFile =  null;
                    updateStatus();
                    updateClients();
                    table.repaint();
                }

            }
        };
    }

    private Action getPropertiesAction() {
        return new AbstractAction("Properties") {
            @Override
            public void actionPerformed(ActionEvent e) {
                new PropertyEditor(DataPointRecorderNew.this).showDialog(DataPointRecorderNew.this);
            }
        };
    }

    private Action getUpdateAction() {
        return new AbstractAction("Update") {
            @Override
            public void actionPerformed(ActionEvent e) {
                update();
            }
        };
    }

    /**
     * Notify listeners that the dataset has updated.  Pressing the "Update" 
     * button calls this.
     */
    public void update() {
        fireDataSetUpdateListenerDataSetUpdated(new DataSetUpdateEvent(this));
        fireSelectedDataSetUpdateListenerDataSetUpdated(new DataSetUpdateEvent(this));
    }
    
    /** Creates a new instance of DataPointRecorder */
    public DataPointRecorderNew() {
        super();
        this.namesArrayLock = new Object();
        dataPoints = new ArrayList();
        myTableModel = new MyTableModel();
        this.setLayout(new BorderLayout());

        JMenuBar menuBar = new JMenuBar();
        JMenu fileMenu = new JMenu("File");
        fileMenu.add(new JMenuItem(getNewAction()));
        fileMenu.add(new JMenuItem(getLoadAction()));
        fileMenu.add(new JMenuItem(getSaveAction()));
        fileMenu.add(new JMenuItem(getSaveAsAction()));
        menuBar.add(fileMenu);

        JMenu editMenu = new JMenu("Edit");
        editMenu.add(new JMenuItem(getPropertiesAction()));
        editMenu.add( new JMenuItem( new AbstractAction("Clear Table Sorting") {
            @Override
            public void actionPerformed(ActionEvent e) {
                table.setAutoCreateRowSorter(false);
                table.setAutoCreateRowSorter(true);
            }
        } ) );
        JMenuItem mi;
        mi= new JMenuItem( new AbstractAction("Delete Selected Items") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int[] selectedRows = getSelectedRowsInModel();
                deleteRows(selectedRows);         
            }
        } );        
        editMenu.add( mi );        
        menuBar.add(editMenu);

        this.add(menuBar, BorderLayout.NORTH);
                
        table = new JTable(myTableModel);
        table.setAutoCreateRowSorter(true); // Java 1.6

        table.getTableHeader().setReorderingAllowed(true);
        table.setColumnModel( new DefaultTableColumnModel() {

            @Override
            public int getColumnCount() {
                synchronized ( namesArrayLock ) {
                    return super.getColumnCount(); //To change body of generated methods, choose Tools | Templates.
                }
            }

            @Override
            public TableColumn getColumn(int columnIndex) {
                synchronized ( namesArrayLock ) {
                    return super.getColumn(columnIndex); //To change body of generated methods, choose Tools | Templates.
                }
            }
            
        });
        table.setRowSelectionAllowed(true);
        table.addMouseListener(new DataPointRecorderNew.MyMouseAdapter(table));
        table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                fireSelectedDataSetUpdateListenerDataSetUpdated(new DataSetUpdateEvent(DataPointRecorderNew.this));
                int selected = table.getSelectedRow(); // we could do a better job here
                if (selected > -1) {
                    QDataSet dp = dataPoints.get(selected);
                    //System.err.println(dp);
                    Datum x= DataSetUtil.asDatum( dp.slice(0) );
                    Datum y= DataSetUtil.asDatum( dp.slice(1) );
                    DataPointSelectionEvent e2 = new DataPointSelectionEvent(DataPointRecorderNew.this, x, y );
                    e2.setDataSet(dp);
                    fireDataPointSelectionListenerDataPointSelected(e2);
                }

            }
        });

        scrollPane = new JScrollPane(table);
        this.add(scrollPane, BorderLayout.CENTER);

        JPanel controlStatusPanel = new JPanel();
        controlStatusPanel.setLayout(new BoxLayout(controlStatusPanel, BoxLayout.Y_AXIS));

        final JPanel controlPanel = new JPanel();
        controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.X_AXIS));

        updateButton = new JButton(getUpdateAction());
        updateButton.setVisible(false);
        updateButton.setEnabled(false);

        controlPanel.add(updateButton);

        clearSelectionButton = new JButton( getClearSelectionAction() );
        controlPanel.add( clearSelectionButton );
        
        messageLabel = new JLabel("ready");
        messageLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);

        controlStatusPanel.add(messageLabel);

        controlPanel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
        controlStatusPanel.add(controlPanel);

        this.add(controlStatusPanel, BorderLayout.SOUTH);
    }

    public static DataPointRecorderNew createFramed() {
        DataPointRecorderNew result;
        JFrame frame = new JFrame("Data Point Recorder");
        result = new DataPointRecorderNew();
        frame.getContentPane().add(result);
        frame.pack();
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
        return result;
    }

    /**
     * update fires off the TableDataChanged, and sets the current selected
     * row if necessary.
     */
    private void updateClients() {
        if (active) {
            myTableModel.fireTableDataChanged();
            if (selectRow != -1 && table.getRowCount()>selectRow ) {
                table.setRowSelectionInterval(selectRow, selectRow);
                table.scrollRectToVisible(table.getCellRect(selectRow, 0, true));
                selectRow = -1;
            }
            table.repaint();
        }
    }

    /**
     * update the status label "(modified)"
     */
    private void updateStatus() {
        String statusString = (saveFile == null ? "" : (String.valueOf(saveFile) + " ")) +
                (modified ? "(modified)" : "");
        String t= messageLabel.getText();
        if ( !statusString.equals(t) ) {
            messageLabel.setText(statusString);
        }
    }

    private transient Comparator comparator= new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            if ( o1 instanceof QDataSet && o2 instanceof QDataSet ) {
                QDataSet qds1= (QDataSet)o1;
                QDataSet qds2= (QDataSet)o2;
                return DataSetUtil.asDatum(qds1.slice(0)).gt( DataSetUtil.asDatum( qds2.slice(0) ) ) ? 1 : -1;
            } else {
                throw new IllegalArgumentException("expected qdatasets");
            }
        }
    };
    
    /**
     * explicitly declare the number of columns.  Call this and then 
     * setColumn to define each column.
     * @param count the number of columns.
     */
    public void setColumnCount( int count ) {
        namesArray= new String[count];
        unitsArray= new Units[count];
        defaultsArray= new double[count];
        for ( int i=0; i<count; i++ ) {
            namesArray[i]= "field"+i;
            unitsArray[i]= Units.dimensionless;
        }
    }
    
    /**
     * identify the name and unit for each column.
     * @param i the column number 
     * @param name a Java identifier for the column, e.g. "StartTime"
     * @param units units for the column, or null for dimensionless.
     * @param deft default value to use when data is not provided.
     */
    public void setColumn( int i, String name, Units units, Datum deft ) {
        if ( units==null ) units= Units.dimensionless;
        if ( namesArray==null ) {
            throw new IllegalArgumentException("call setColumnCount first.");
        }
        if ( i>=namesArray.length ) {
            throw new IndexOutOfBoundsException("column index is out of bounds (and 0 is the first column)");
        }
        namesArray[i]= name;
        unitsArray[i]= units;
        defaultsArray[i]= deft.doubleValue(units);
    }

    /**
     * identify the name and unit for each column.
     * @param i the column number
     * @param name a Java identifier for the column, e.g. "StartTime"
     * @param units  units units for the column, or null for dimensionless.
     * @param deft default value to use when data is not provided, which must be parseable by units.
     * @throws java.text.ParseException
     */
    public void setColumn( int i, String name, Units units, String deft ) throws ParseException {
        if ( units==null ) units= Units.dimensionless;
        if ( units instanceof EnumerationUnits ) {
            setColumn( i, name, units, ((EnumerationUnits)units).createDatum(deft) );            
        } else {
            setColumn( i, name, units, units.parse(deft) );
        }
    }
                
    /**
     * identify the name and unit for each column.
     * @param i the column number
     * @param name a Java identifier for the column, e.g. "StartTime"
     * @param units  units units for the column, or null for dimensionless.
     * @param deft default value to use when data is not provided.
     */
    public void setColumn( int i, String name, Units units, double deft ) {
        if ( units==null ) units= Units.dimensionless;
        setColumn( i, name, units, units.createDatum(deft) );
    }
                    
    /**
     * insert the point into the data points.  If the dataset is sorted, then we
     * replace any point that is within X_LIMIT of the point.
     * @param newPoint 
     */
    private void insertInternal( QDataSet newPoint ) {
        int newSelect;
        if ( newPoint.rank()==2 && newPoint.length()==1 ) {
            newPoint= newPoint.slice(0);
        }
        
        // make sure all the units are correct by converting them as they come in.
        ArrayDataSet mnp;
        if ( defaultsArray!=null ) {
            mnp= DDataSet.wrap( Arrays.copyOf(defaultsArray,defaultsArray.length) );
        } else {
            mnp= DDataSet.createRank1(namesArray.length);
        }
        
        QDataSet bds= (QDataSet) newPoint.property(QDataSet.BUNDLE_0);
        
        for ( int i=0; i<newPoint.length(); i++ ) {
            Datum d= DataSetUtil.asDatum( newPoint.slice(i) );
            
            int idx= -1;
            if ( i< bds.length() && i<namesArray.length && bds.property(QDataSet.NAME,i).equals(namesArray[i]) ) {
                idx= i;
            } else {
                for ( int j=0; j<namesArray.length; j++ ) {
                    if ( bds.property(QDataSet.NAME,i).equals(namesArray[j]) ) {
                        idx= j;
                    }
                }
            }
            if ( idx==-1 ) {
                logger.log(Level.FINEST, "unable to find column for {0}", bds.property(QDataSet.NAME,i));
                continue;
            }
            if ( unitsArray[idx].isConvertibleTo(d.getUnits() ) ) {
                mnp.putValue( idx,d.doubleValue( unitsArray[idx] ) );
            } else {
                if ( UnitsUtil.isOrdinalMeasurement(unitsArray[idx]) ) {
                    mnp.putValue( idx, ((EnumerationUnits)unitsArray[idx]).createDatum(d.toString()).doubleValue(unitsArray[idx]));
                } else {
                    throw new InconvertibleUnitsException(d.getUnits(),unitsArray[idx]);
                }
            }
            mnp.putProperty( QDataSet.BUNDLE_0, bundleDescriptor );
        }
        
        newPoint= mnp;
        
        synchronized ( dataPoints ) {
            String[] keys;
            if (sorted) {
                int index = Collections.binarySearch( dataPoints, newPoint, comparator );
                if (index < 0) {
                    QDataSet qds1= null;
                    if ( ~index<dataPoints.size() ) {
                        qds1= (QDataSet)dataPoints.get(~index);
                        keys= DataSetUtil.bundleNames(newPoint);
                        for ( String key : keys ) {
                            if ( DataSetOps.indexOfBundledDataSet( qds1, key )!=-1 ) {
                                logger.log(Level.FINE, "no place to put key: {0}", key);
                            }
                        }
                    }
                    QDataSet dp1= null;
                    if  ( (~index+1)<dataPoints.size() ) { // check for very close point.
                        dp1= (QDataSet)dataPoints.get(~index+1);
                    }
                    
                    Datum epsilon= Units.microseconds.createDatum(10000);
                    if ( SemanticOps.getUnits(newPoint.slice(0)).getOffsetUnits().isConvertibleTo(Units.milliseconds) ) {
                        if ( qds1!=null && Ops.lt( Ops.abs( Ops.subtract( qds1.slice(0), newPoint.slice(0) ) ), epsilon ).value()==1 ) {
                            dataPoints.set( ~index, newPoint );
                        } else if ( dp1!=null && Ops.lt( Ops.abs( Ops.subtract( dp1.slice(0), newPoint.slice(0) ) ), epsilon ).value()==1 ) {
                            dataPoints.set( ~index, newPoint );
                        } else {
                            dataPoints.add(~index, newPoint);
                        }
                    } else {
                        dataPoints.add(~index, newPoint);
                    }
                    newSelect = ~index;
                } else {
                    dataPoints.set(index, newPoint);
                    newSelect = index;
                }

            } else {
                dataPoints.add(newPoint);
                newSelect = dataPoints.size() - 1;
            }

            selectRow = newSelect;
        }
        modified = true;
        updateStatus();
        updateClients();
        table.repaint();
        myTableModel.fireTableDataChanged();
    }
    
    
    /**
     * add just the x and y values.
     * @param x the x position
     * @param y the y position
     */
    public void addDataPoint( Datum x, Datum y ) {
        addDataPoint( x, y, null );
    }
    
    /**
     * add the x and y values with unnamed metadata.
     * @param x the x position
     * @param y the y position
     * @param meta any metadata (String, Double, etc ) to be recorded along with the data point.
     */    
    public void addDataPoint( Datum x, Datum y, Object meta ) {
        addDataPoint( x, y, Collections.singletonMap("meta",meta) );
    }
    
    /**
     * add the data point, along with metadata such as the key press.
     * @param x the x position
     * @param y the y position
     * @param planes null or additional planes.  Note LinkedHashMap will keep the order of the tabs.
     */
    public void addDataPoint( Datum x, Datum y, Map<String,Object> planes ) {

        if ( planes==null ) planes= Collections.emptyMap();
        
        DDataSet rec= DDataSet.createRank1( 2 + planes.size() );        
        SparseDataSetBuilder bdsb= new SparseDataSetBuilder(2);
        
        int ii= 0;
        
        bdsb.putProperty( QDataSet.NAME, ii, "x" );
        bdsb.putProperty( QDataSet.UNITS, ii, x.getUnits() );
        rec.putValue( ii, x.doubleValue( x.getUnits() ));
        ii++;
        
        bdsb.putProperty( QDataSet.NAME, ii, "y" );
        bdsb.putProperty( QDataSet.UNITS, ii, y.getUnits() );
        rec.putValue( ii, y.doubleValue( y.getUnits() ));
        ii++;
        
        for ( Entry<String,Object> e : planes.entrySet() ) {
            bdsb.putProperty( QDataSet.NAME, ii, e.getKey() );
            Object o= e.getValue();
            Units theu;
            if ( o instanceof String ) {
                Units eu= EnumerationUnits.create("default"); 
                theu= eu;
                try {
                    rec.putValue( ii, theu.parse((String)o).doubleValue(theu) );
                } catch (ParseException ex) {
                    rec.putValue( ii, -1 ); // fill
                }
            } else if ( o instanceof Datum ) {
                theu= ((Datum)o).getUnits();
                rec.putValue( ii, ((Datum)o).doubleValue(theu) );
            } else if ( o instanceof Number ) {           
                theu= Units.dimensionless;
                rec.putValue( ii, ((Number)o).doubleValue() );
            } else if ( o instanceof QDataSet ) {
                theu= SemanticOps.getUnits((QDataSet)o);
                rec.putValue( ii, ((QDataSet)o).value() );
            } else {
                throw new IllegalArgumentException("value must be String, Datum, DataSet or Number");
            }
            bdsb.putProperty( QDataSet.UNITS, ii, theu );
            ii++;
        }
        
        bdsb.setLength(ii);
        
        QDataSet bds= bdsb.getDataSet();
        rec.putProperty(QDataSet.BUNDLE_0,bds);
        
        addDataPoint( rec );
    }
    
    /**
     * add the record to the collection of records.  This should be a
     * rank 1 bundle or 1-record rank 2 bundle.
     *<blockquote><pre>{@code
     *dpr=DataPointRecorder()
     *dpr.addDataPoint( createEvent( '2014-04-23/P1D', 0xFF0000, 'alert' ) )
     *}</pre></blockquote>
     * 
     * @param rec rank 1 qdataset, or 1-record rank 2 dataset (ds[1,n])
     */   
    public void addDataPoint( QDataSet rec ) {
        
        if ( rec.rank()==2 && rec.length()==1 ) {
            rec= rec.slice(0); // Jython createEvent produces rank 2 dataset.
        }
        
        synchronized (dataPoints) {
            if (dataPoints.isEmpty()) {
                QDataSet bds= (QDataSet) rec.property( QDataSet.BUNDLE_0 );
                
                if ( bds==null ) {
                    SparseDataSetBuilder bdsb= new SparseDataSetBuilder(2);
                    Units u= SemanticOps.getUnits(rec);
                    for ( int i=0;i<rec.length();i++ ) {
                        bdsb.putProperty( QDataSet.NAME,i,"ch_"+i );
                        bdsb.putProperty( QDataSet.UNITS,i,u );
                    }
                    bdsb.setLength(rec.length());
                    bds= bdsb.getDataSet();
                }
                
                if ( namesArray==null ) {
                    logger.fine("first record defines columns");
                    Units[] lunitsArray    = new Units[ rec.length() ];
                    String[] lnamesArray   = new String[ rec.length() ];
                
                    for ( int index=0; index<bds.length(); index++ ) {
                        lnamesArray[index] = (String) bds.property(QDataSet.NAME,index);
                        Units u= (Units) bds.property(QDataSet.UNITS,index);
                        lunitsArray[index] = u!=null ? u : Units.dimensionless;
                    }
                
                    unitsArray= lunitsArray;
                    namesArray= lnamesArray;
                }
                
                bundleDescriptor= new AbstractDataSet() {
                    @Override
                    public int rank() {
                        return 2;
                    }
                    @Override
                    public Object property(String name, int i) {
                        switch (name) {
                            case QDataSet.NAME:
                                return namesArray[i];
                            case QDataSet.UNITS:
                                return unitsArray[i];
                            default:
                                return null;
                        }
                    }
                    @Override
                    public int length() {
                        return namesArray.length;
                    }
                    @Override
                    public int length(int i) {
                        return 0;
                    }
                };

                myTableModel.fireTableStructureChanged();
                for ( int i=0; i<1; i++ ) { //i<unitsArray.length
                    if ( UnitsUtil.isTimeLocation( unitsArray[i] ) ) {
                        table.getTableHeader().getColumnModel().getColumn(i).setMinWidth( TIME_WIDTH );   
                    }
                }

            }

            insertInternal( rec );
        }
        if (active) {
            DataSetUpdateEvent ev= new DataSetUpdateEvent(this,getDataSet());
            fireDataSetUpdateListenerDataSetUpdated( ev );
        }
 
    }

    public void appendDataSet(VectorDataSet ds) {

        Map planesMap = new LinkedHashMap();

        if (ds.getProperty("comment") != null) {
            planesMap.put("comment", ds.getProperty("comment"));
        }

        if (ds.getProperty("xTagWidth") != null) {
            DataPointRecorderNew.this.xTagWidth = (Datum) ds.getProperty("xTagWidth");
        } else {
            DataPointRecorderNew.this.xTagWidth = Datum.create(0);
        }

        String[] planes = ds.getPlaneIds();

        for (int i = 0; i < ds.getXLength(); i++) {
            for (String plane : planes) {
                if (!plane.equals("")) {
                    planesMap.put(plane, ((VectorDataSet) ds.getPlanarView(plane)).getDatum(i));
                }
            }
            addDataPoint(ds.getXTagDatum(i), ds.getDatum(i), planesMap);
        }

        updateClients();
        
    }

    /**
     * this adds all the points in the DataSet to the list.  This will also check the dataset for the special
     * property "comment" and add it as a comment.
     * @return the listener to receive data set updates
     * @see org.das2.dataset.DataSetUpdateEvent
     */
    public DataSetUpdateListener getAppendDataSetUpListener() {
        return new DataSetUpdateListener() {
            @Override
            public void dataSetUpdated(DataSetUpdateEvent e) {
                VectorDataSet ds = (VectorDataSet) e.getDataSet();
                if (ds == null) {
                    throw new RuntimeException("not supported, I need the DataSet in the update event");
                } else {
                    appendDataSet((VectorDataSet) e.getDataSet());
                }

            }
        };
    }


    /**
     * hide the update button if no one is listening.
     * @return true if it is now visible
     */
    private boolean checkUpdateEnable() {
        int listenerList1Count;
        //int selectedListenerListCount;
        listenerList1Count= listenerList1.getListenerCount();

        if ( listenerList1Count>0 ) { // || selectedListenerListCount>0 ) {
            updateButton.setEnabled(true);
            updateButton.setVisible(true);
            updateButton.setToolTipText(null);
            return true;
        } else {
            updateButton.setEnabled(false);
            updateButton.setToolTipText("no listeners. See File->Save to save table.");
            updateButton.setVisible(false);
            return false;
        }
    }
    private javax.swing.event.EventListenerList listenerList1 = new javax.swing.event.EventListenerList();

    public void addDataSetUpdateListener(org.das2.dataset.DataSetUpdateListener listener) {
        listenerList1.add(org.das2.dataset.DataSetUpdateListener.class, listener);
        checkUpdateEnable();
    }

    public void removeDataSetUpdateListener(org.das2.dataset.DataSetUpdateListener listener) {
        listenerList1.remove(org.das2.dataset.DataSetUpdateListener.class, listener);
        checkUpdateEnable();
    }

    private void fireDataSetUpdateListenerDataSetUpdated(org.das2.dataset.DataSetUpdateEvent event) {
        Object[] listeners= listenerList1.getListenerList();
        for (int i = listeners.length - 2; i >=0; i-= 2) {
            if (listeners[i] == org.das2.dataset.DataSetUpdateListener.class) {
                ((org.das2.dataset.DataSetUpdateListener) listeners[i + 1]).dataSetUpdated(event);
            }
        }
    }
    
    
    /**
     * the selection are the highlighted points in the table.  Listeners can grab this data and do something with the
     * dataset.
     */
    private javax.swing.event.EventListenerList selectedListenerList = new javax.swing.event.EventListenerList();

    public void addSelectedDataSetUpdateListener(org.das2.dataset.DataSetUpdateListener listener) {
        selectedListenerList.add(org.das2.dataset.DataSetUpdateListener.class, listener);
        checkUpdateEnable();
    }

    public void removeSelectedDataSetUpdateListener(org.das2.dataset.DataSetUpdateListener listener) {
        selectedListenerList.remove(org.das2.dataset.DataSetUpdateListener.class, listener);
        checkUpdateEnable();
    }

    
    private void fireSelectedDataSetUpdateListenerDataSetUpdated(org.das2.dataset.DataSetUpdateEvent event) {
        Object[] listeners= selectedListenerList.getListenerList();
        for ( int i = listeners.length - 2; i >=0; i-=2 ) {
            if (listeners[i] == org.das2.dataset.DataSetUpdateListener.class) {
                ((org.das2.dataset.DataSetUpdateListener) listeners[i + 1]).dataSetUpdated(event);
            }
        }

    }
    
    /**
     * Holds value of property sorted.
     */
    private boolean sorted = true;

    /**
     * Getter for property sorted.
     * @return Value of property sorted.
     */
    public boolean isSorted() {
        return this.sorted;
    }

    /**
     * Setter for property sorted.
     * @param sorted New value of property sorted.
     */
    public void setSorted(boolean sorted) {
        this.sorted = sorted;
    }

    /**
     * Registers DataPointSelectionListener to receive events.
     * @param listener The listener to register.
     */
    public void addDataPointSelectionListener(org.das2.event.DataPointSelectionListener listener) {
        listenerList1.add(org.das2.event.DataPointSelectionListener.class, listener);
    }

    /**
     * Removes DataPointSelectionListener from the list of listeners.
     * @param listener The listener to remove.
     */
    public void removeDataPointSelectionListener(org.das2.event.DataPointSelectionListener listener) {
        listenerList1.remove(org.das2.event.DataPointSelectionListener.class, listener);
    }

    /**
     * Notifies all registered listeners about the event.
     *
     * @param event The event to be fired
     */
    private void fireDataPointSelectionListenerDataPointSelected(org.das2.event.DataPointSelectionEvent event) {
        Object[] listeners= listenerList1.getListenerList();

        logger.fine("firing data point selection event");
        for (int i = listeners.length - 2; i >= 0; i -= 2 ) {
            if (listeners[i] == org.das2.event.DataPointSelectionListener.class) {
                ((org.das2.event.DataPointSelectionListener) listeners[i + 1]).dataPointSelected(event);
            }
        }

    }
    
    /**
     * Holds value of property xTagWidth.
     */
    private Datum xTagWidth = Datum.create(0);

    /**
     * Getter for property xTagWidth.  When xTagWidth is zero,
     * this implies there is no binning.
     * @return Value of property xTagWidth.
     */
    public Datum getXTagWidth() {
        return this.xTagWidth;
    }

    /**
     * bins for the data, when xTagWidth is non-zero.
     * @param xTagWidth New value of property xTagWidth.
     */
    public void setXTagWidth(Datum xTagWidth) {
        this.xTagWidth = xTagWidth;
    }
    /**
     * Holds value of property snapToGrid.
     */
    private boolean snapToGrid = false;

    /**
     * Getter for property snapToGrid.
     * @return Value of property snapToGrid.
     */
    public boolean isSnapToGrid() {
        return this.snapToGrid;
    }

    /**
     * Setter for property snapToGrid.  true indicates the xtag will be reset
     * so that the tags are equally spaced, each xTagWidth apart.
     * @param snapToGrid New value of property snapToGrid.
     */
    public void setSnapToGrid(boolean snapToGrid) {
        this.snapToGrid = snapToGrid;
    }

    /**
     * return true when the data point recorder has been modified.
     * @return true when the data point recorder has been modified.
     */
    public boolean isModified() {
        return modified;
    }
    
}