/* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* * ArffTableModel.java * Copyright (C) 2005 University of Waikato, Hamilton, New Zealand * */ package weka.gui.arffviewer; import weka.core.*; import weka.core.converters.AbstractFileLoader; import weka.core.converters.ConverterUtils; import weka.filters.Filter; import weka.filters.unsupervised.attribute.Reorder; import weka.gui.ComponentHelper; import javax.swing.*; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.TableModel; import java.io.*; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.Vector; /** * The model for the Arff-Viewer. * * * @author FracPete (fracpete at waikato dot ac dot nz) * @version $Revision: 5987 $ */ public class ArffTableModel implements TableModel, Undoable { /** the listeners */ private HashSet m_Listeners; /** the data */ private Instances m_Data; /** whether notfication is enabled */ private boolean m_NotificationEnabled; /** whether undo is active */ private boolean m_UndoEnabled; /** whether to ignore changes, i.e. not adding to undo history */ private boolean m_IgnoreChanges; /** the undo list (contains temp. filenames) */ private Vector m_UndoList; /** whether the table is read-only */ private boolean m_ReadOnly; /** * performs some initialization */ private ArffTableModel() { super(); m_Listeners = new HashSet(); m_Data = null; m_NotificationEnabled = true; m_UndoList = new Vector(); m_IgnoreChanges = false; m_UndoEnabled = true; m_ReadOnly = false; } /** * initializes the object and loads the given file * * @param filename the file to load */ public ArffTableModel(String filename) { this(); if ( (filename != null) && (!filename.equals("")) ) loadFile(filename); } /** * initializes the model with the given data * * @param data the data to use */ public ArffTableModel(Instances data) { this(); this.m_Data = data; } /** * returns whether the notification of changes is enabled * * @return true if notification of changes is enabled */ public boolean isNotificationEnabled() { return m_NotificationEnabled; } /** * sets whether the notification of changes is enabled * * @param enabled enables/disables the notification */ public void setNotificationEnabled(boolean enabled) { m_NotificationEnabled = enabled; } /** * returns whether undo support is enabled * * @return true if undo support is enabled */ public boolean isUndoEnabled() { return m_UndoEnabled; } /** * sets whether undo support is enabled * * @param enabled whether to enable/disable undo support */ public void setUndoEnabled(boolean enabled) { m_UndoEnabled = enabled; } /** * returns whether the model is read-only * * @return true if model is read-only */ public boolean isReadOnly() { return m_ReadOnly; } /** * sets whether the model is read-only * * @param value if true the model is set to read-only */ public void setReadOnly(boolean value) { m_ReadOnly = value; } /** * loads the specified ARFF file * * @param filename the file to load */ private void loadFile(String filename) { AbstractFileLoader loader; loader = ConverterUtils.getLoaderForFile(filename); if (loader != null) { try { loader.setFile(new File(filename)); m_Data = loader.getDataSet(); } catch (Exception e) { ComponentHelper.showMessageBox( null, "Error loading file...", e.toString(), JOptionPane.OK_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE ); System.out.println(e); m_Data = null; } } } /** * sets the data * * @param data the data to use */ public void setInstances(Instances data) { m_Data = data; } /** * returns the data * * @return the current data */ public Instances getInstances() { return m_Data; } /** * returns the attribute at the given index, can be NULL if not an attribute * column * * @param columnIndex the index of the column * @return the attribute at the position */ public Attribute getAttributeAt(int columnIndex) { if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) return m_Data.attribute(columnIndex - 1); else return null; } /** * returns the TYPE of the attribute at the given position * * @param columnIndex the index of the column * @return the attribute type */ public int getType(int columnIndex) { return getType(0, columnIndex); } /** * returns the TYPE of the attribute at the given position * * @param rowIndex the index of the row * @param columnIndex the index of the column * @return the attribute type */ public int getType(int rowIndex, int columnIndex) { int result; result = Attribute.STRING; if ( (rowIndex >= 0) && (rowIndex < getRowCount()) && (columnIndex > 0) && (columnIndex < getColumnCount()) ) result = m_Data.instance(rowIndex).attribute(columnIndex - 1).type(); return result; } /** * deletes the attribute at the given col index. notifies the listeners. * * @param columnIndex the index of the attribute to delete */ public void deleteAttributeAt(int columnIndex) { deleteAttributeAt(columnIndex, true); } /** * deletes the attribute at the given col index * * @param columnIndex the index of the attribute to delete * @param notify whether to notify the listeners */ public void deleteAttributeAt(int columnIndex, boolean notify) { if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) { if (!m_IgnoreChanges) addUndoPoint(); m_Data.deleteAttributeAt(columnIndex - 1); if (notify) notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW)); } } /** * deletes the attributes at the given indices * * @param columnIndices the column indices */ public void deleteAttributes(int[] columnIndices) { int i; Arrays.sort(columnIndices); addUndoPoint(); m_IgnoreChanges = true; for (i = columnIndices.length - 1; i >= 0; i--) deleteAttributeAt(columnIndices[i], false); m_IgnoreChanges = false; notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW)); } /** * renames the attribute at the given col index * * @param columnIndex the index of the column * @param newName the new name of the attribute */ public void renameAttributeAt(int columnIndex, String newName) { if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) { addUndoPoint(); m_Data.renameAttribute(columnIndex - 1, newName); notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW)); } } /** * sets the attribute at the given col index as the new class attribute, i.e. * it moves it to the end of the attributes * * @param columnIndex the index of the column */ public void attributeAsClassAt(int columnIndex) { Reorder reorder; String order; int i; if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) { addUndoPoint(); try { // build order string (1-based!) order = ""; for (i = 1; i < m_Data.numAttributes() + 1; i++) { // skip new class if (i == columnIndex) continue; if (!order.equals("")) order += ","; order += Integer.toString(i); } if (!order.equals("")) order += ","; order += Integer.toString(columnIndex); // process data reorder = new Reorder(); reorder.setAttributeIndices(order); reorder.setInputFormat(m_Data); m_Data = Filter.useFilter(m_Data, reorder); // set class index m_Data.setClassIndex(m_Data.numAttributes() - 1); } catch (Exception e) { e.printStackTrace(); undo(); } notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW)); } } /** * deletes the instance at the given index * * @param rowIndex the index of the row */ public void deleteInstanceAt(int rowIndex) { deleteInstanceAt(rowIndex, true); } /** * deletes the instance at the given index * * @param rowIndex the index of the row * @param notify whether to notify the listeners */ public void deleteInstanceAt(int rowIndex, boolean notify) { if ( (rowIndex >= 0) && (rowIndex < getRowCount()) ) { if (!m_IgnoreChanges) addUndoPoint(); m_Data.delete(rowIndex); if (notify) notifyListener( new TableModelEvent( this, rowIndex, rowIndex, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE)); } } /** * deletes the instances at the given positions * * @param rowIndices the indices to delete */ public void deleteInstances(int[] rowIndices) { int i; Arrays.sort(rowIndices); addUndoPoint(); m_IgnoreChanges = true; for (i = rowIndices.length - 1; i >= 0; i--) deleteInstanceAt(rowIndices[i], false); m_IgnoreChanges = false; notifyListener( new TableModelEvent( this, rowIndices[0], rowIndices[rowIndices.length - 1], TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE)); } /** * sorts the instances via the given attribute * * @param columnIndex the index of the column */ public void sortInstances(int columnIndex) { if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) { addUndoPoint(); m_Data.sort(columnIndex - 1); notifyListener(new TableModelEvent(this)); } } /** * returns the column of the given attribute name, -1 if not found * * @param name the name of the attribute * @return the column index or -1 if not found */ public int getAttributeColumn(String name) { int i; int result; result = -1; for (i = 0; i < m_Data.numAttributes(); i++) { if (m_Data.attribute(i).name().equals(name)) { result = i + 1; break; } } return result; } /** * returns the most specific superclass for all the cell values in the * column (always String) * * @param columnIndex the column index * @return the class of the column */ public Class getColumnClass(int columnIndex) { Class result; result = null; if ( (columnIndex >= 0) && (columnIndex < getColumnCount()) ) { if (columnIndex == 0) result = Integer.class; else if (getType(columnIndex) == Attribute.NUMERIC) result = Double.class; else result = String.class; // otherwise no input of "?"!!! } return result; } /** * returns the number of columns in the model * * @return the number of columns */ public int getColumnCount() { int result; result = 1; if (m_Data != null) result += m_Data.numAttributes(); return result; } /** * checks whether the column represents the class or not * * @param columnIndex the index of the column * @return true if the column is the class attribute */ private boolean isClassIndex(int columnIndex) { boolean result; int index; index = m_Data.classIndex(); result = ((index == - 1) && (m_Data.numAttributes() == columnIndex)) || (index == columnIndex - 1); return result; } /** * returns the name of the column at columnIndex * * @param columnIndex the index of the column * @return the name of the column */ public String getColumnName(int columnIndex) { String result; result = ""; if ( (columnIndex >= 0) && (columnIndex < getColumnCount()) ) { if (columnIndex == 0) { result = "<html><center>No.<br><font size=\"-2\"> </font></center></html>"; } else { if (m_Data != null) { if ( (columnIndex - 1 < m_Data.numAttributes()) ) { result = "<html><center>"; // name if (isClassIndex(columnIndex)) result += "<b>" + m_Data.attribute(columnIndex - 1).name() + "</b>"; else result += m_Data.attribute(columnIndex - 1).name(); // attribute type switch (getType(columnIndex)) { case Attribute.DATE: result += "<br><font size=\"-2\">Date</font>"; break; case Attribute.NOMINAL: result += "<br><font size=\"-2\">Nominal</font>"; break; case Attribute.STRING: result += "<br><font size=\"-2\">String</font>"; break; case Attribute.NUMERIC: result += "<br><font size=\"-2\">Numeric</font>"; break; case Attribute.RELATIONAL: result += "<br><font size=\"-2\">Relational</font>"; break; default: result += "<br><font size=\"-2\">???</font>"; } result += "</center></html>"; } } } } return result; } /** * returns the number of rows in the model * * @return the number of rows */ public int getRowCount() { if (m_Data == null) return 0; else return m_Data.numInstances(); } /** * checks whether the value at the given position is missing * * @param rowIndex the row index * @param columnIndex the column index * @return true if the value at the position is missing */ public boolean isMissingAt(int rowIndex, int columnIndex) { boolean result; result = false; if ( (rowIndex >= 0) && (rowIndex < getRowCount()) && (columnIndex > 0) && (columnIndex < getColumnCount()) ) result = (m_Data.instance(rowIndex).isMissing(columnIndex - 1)); return result; } /** * returns the double value of the underlying Instances object at the * given position, -1 if out of bounds * * @param rowIndex the row index * @param columnIndex the column index * @return the underlying value in the Instances object */ public double getInstancesValueAt(int rowIndex, int columnIndex) { double result; result = -1; if ( (rowIndex >= 0) && (rowIndex < getRowCount()) && (columnIndex > 0) && (columnIndex < getColumnCount()) ) result = m_Data.instance(rowIndex).value(columnIndex - 1); return result; } /** * returns the value for the cell at columnindex and rowIndex * * @param rowIndex the row index * @param columnIndex the column index * @return the value at the position */ public Object getValueAt(int rowIndex, int columnIndex) { Object result; String tmp; result = null; if ( (rowIndex >= 0) && (rowIndex < getRowCount()) && (columnIndex >= 0) && (columnIndex < getColumnCount()) ) { if (columnIndex == 0) { result = new Integer(rowIndex + 1); } else { if (isMissingAt(rowIndex, columnIndex)) { result = null; } else { switch (getType(columnIndex)) { case Attribute.DATE: case Attribute.NOMINAL: case Attribute.STRING: case Attribute.RELATIONAL: result = m_Data.instance(rowIndex).stringValue(columnIndex - 1); break; case Attribute.NUMERIC: result = new Double(m_Data.instance(rowIndex).value(columnIndex - 1)); break; default: result = "-can't display-"; } } } } if (getType(columnIndex) != Attribute.NUMERIC) { if (result != null) { // does it contain "\n" or "\r"? -> replace with red html tag tmp = result.toString(); if ( (tmp.indexOf("\n") > -1) || (tmp.indexOf("\r") > -1) ) { tmp = tmp.replaceAll("\\r\\n", "<font color=\"red\"><b>\\\\r\\\\n</b></font>"); tmp = tmp.replaceAll("\\r", "<font color=\"red\"><b>\\\\r</b></font>"); tmp = tmp.replaceAll("\\n", "<font color=\"red\"><b>\\\\n</b></font>"); result = "<html>" + tmp + "</html>"; } } } return result; } /** * returns true if the cell at rowindex and columnindexis editable * * @param rowIndex the index of the row * @param columnIndex the index of the column * @return true if the cell is editable */ public boolean isCellEditable(int rowIndex, int columnIndex) { return (columnIndex > 0) && !isReadOnly(); } /** * sets the value in the cell at columnIndex and rowIndex to aValue. * but only the value and the value can be changed * * @param aValue the new value * @param rowIndex the row index * @param columnIndex the column index */ public void setValueAt(Object aValue, int rowIndex, int columnIndex) { setValueAt(aValue, rowIndex, columnIndex, true); } /** * sets the value in the cell at columnIndex and rowIndex to aValue. * but only the value and the value can be changed * * @param aValue the new value * @param rowIndex the row index * @param columnIndex the column index * @param notify whether to notify the listeners */ public void setValueAt(Object aValue, int rowIndex, int columnIndex, boolean notify) { int type; int index; String tmp; Instance inst; Attribute att; Object oldValue; if (!m_IgnoreChanges) addUndoPoint(); oldValue = getValueAt(rowIndex, columnIndex); type = getType(rowIndex, columnIndex); index = columnIndex - 1; inst = m_Data.instance(rowIndex); att = inst.attribute(index); // missing? if (aValue == null) { inst.setValue(index, Utils.missingValue()); } else { tmp = aValue.toString(); switch (type) { case Attribute.DATE: try { att.parseDate(tmp); inst.setValue(index, att.parseDate(tmp)); } catch (Exception e) { // ignore } break; case Attribute.NOMINAL: if (att.indexOfValue(tmp) > -1) inst.setValue(index, att.indexOfValue(tmp)); break; case Attribute.STRING: inst.setValue(index, tmp); break; case Attribute.NUMERIC: try { Double.parseDouble(tmp); inst.setValue(index, Double.parseDouble(tmp)); } catch (Exception e) { // ignore } break; case Attribute.RELATIONAL: try { inst.setValue(index, inst.attribute(index).addRelation((Instances) aValue)); } catch (Exception e) { // ignore } break; default: throw new IllegalArgumentException("Unsupported Attribute type: " + type + "!"); } } // notify only if the value has changed! if (notify && (!("" + oldValue).equals("" + aValue)) ) notifyListener(new TableModelEvent(this, rowIndex, columnIndex)); } /** * adds a listener to the list that is notified each time a change to data * model occurs * * @param l the listener to add */ public void addTableModelListener(TableModelListener l) { m_Listeners.add(l); } /** * removes a listener from the list that is notified each time a change to * the data model occurs * * @param l the listener to remove */ public void removeTableModelListener(TableModelListener l) { m_Listeners.remove(l); } /** * notfies all listener of the change of the model * * @param e the event to send to the listeners */ public void notifyListener(TableModelEvent e) { Iterator iter; TableModelListener l; // is notification enabled? if (!isNotificationEnabled()) return; iter = m_Listeners.iterator(); while (iter.hasNext()) { l = (TableModelListener) iter.next(); l.tableChanged(e); } } /** * removes the undo history */ public void clearUndo() { m_UndoList = new Vector(); } /** * returns whether an undo is possible, i.e. whether there are any undo points * saved so far * * @return returns TRUE if there is an undo possible */ public boolean canUndo() { return !m_UndoList.isEmpty(); } /** * undoes the last action */ public void undo() { File tempFile; Instances inst; ObjectInputStream ooi; if (canUndo()) { // load file tempFile = (File) m_UndoList.get(m_UndoList.size() - 1); try { // read serialized data ooi = new ObjectInputStream(new BufferedInputStream(new FileInputStream(tempFile))); inst = (Instances) ooi.readObject(); ooi.close(); // set instances setInstances(inst); notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW)); notifyListener(new TableModelEvent(this)); } catch (Exception e) { e.printStackTrace(); } tempFile.delete(); // remove from undo m_UndoList.remove(m_UndoList.size() - 1); } } /** * adds an undo point to the undo history, if the undo support is enabled * @see #isUndoEnabled() * @see #setUndoEnabled(boolean) */ public void addUndoPoint() { File tempFile; ObjectOutputStream oos; // undo support currently on? if (!isUndoEnabled()) return; if (getInstances() != null) { try { // temp. filename tempFile = File.createTempFile("arffviewer", null); tempFile.deleteOnExit(); // serialize instances oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(tempFile))); oos.writeObject(getInstances()); oos.flush(); oos.close(); // add to undo list m_UndoList.add(tempFile); } catch (Exception e) { e.printStackTrace(); } } } }