2
* This program is free software; you can redistribute it and/or modify
3
* it under the terms of the GNU General Public License as published by
4
* the Free Software Foundation; either version 2 of the License, or
5
* (at your option) any later version.
7
* This program is distributed in the hope that it will be useful,
8
* but WITHOUT ANY WARRANTY; without even the implied warranty of
9
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
* GNU General Public License for more details.
12
* You should have received a copy of the GNU General Public License
13
* along with this program; if not, write to the Free Software
14
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19
* Copyright (C) 2005 University of Waikato, Hamilton, New Zealand
23
package weka.gui.arffviewer;
25
import weka.core.Attribute;
26
import weka.core.Instance;
27
import weka.core.Instances;
28
import weka.core.Undoable;
29
import weka.core.converters.AbstractFileLoader;
30
import weka.core.converters.ConverterUtils;
31
import weka.filters.Filter;
32
import weka.filters.unsupervised.attribute.Reorder;
33
import weka.gui.ComponentHelper;
35
import java.io.BufferedInputStream;
36
import java.io.BufferedOutputStream;
38
import java.io.FileInputStream;
39
import java.io.FileOutputStream;
40
import java.io.ObjectInputStream;
41
import java.io.ObjectOutputStream;
42
import java.util.Arrays;
43
import java.util.HashSet;
44
import java.util.Iterator;
45
import java.util.Vector;
47
import javax.swing.JOptionPane;
48
import javax.swing.event.TableModelEvent;
49
import javax.swing.event.TableModelListener;
50
import javax.swing.table.TableModel;
53
* The model for the Arff-Viewer.
56
* @author FracPete (fracpete at waikato dot ac dot nz)
57
* @version $Revision: 1.8 $
59
public class ArffTableModel
60
implements TableModel, Undoable {
63
private HashSet m_Listeners;
65
private Instances m_Data;
66
/** whether notfication is enabled */
67
private boolean m_NotificationEnabled;
68
/** whether undo is active */
69
private boolean m_UndoEnabled;
70
/** whether to ignore changes, i.e. not adding to undo history */
71
private boolean m_IgnoreChanges;
72
/** the undo list (contains temp. filenames) */
73
private Vector m_UndoList;
74
/** whether the table is read-only */
75
private boolean m_ReadOnly;
78
* performs some initialization
80
private ArffTableModel() {
83
m_Listeners = new HashSet();
85
m_NotificationEnabled = true;
86
m_UndoList = new Vector();
87
m_IgnoreChanges = false;
93
* initializes the object and loads the given file
95
* @param filename the file to load
97
public ArffTableModel(String filename) {
100
if ( (filename != null) && (!filename.equals("")) )
105
* initializes the model with the given data
107
* @param data the data to use
109
public ArffTableModel(Instances data) {
116
* returns whether the notification of changes is enabled
118
* @return true if notification of changes is enabled
120
public boolean isNotificationEnabled() {
121
return m_NotificationEnabled;
125
* sets whether the notification of changes is enabled
127
* @param enabled enables/disables the notification
129
public void setNotificationEnabled(boolean enabled) {
130
m_NotificationEnabled = enabled;
134
* returns whether undo support is enabled
136
* @return true if undo support is enabled
138
public boolean isUndoEnabled() {
139
return m_UndoEnabled;
143
* sets whether undo support is enabled
145
* @param enabled whether to enable/disable undo support
147
public void setUndoEnabled(boolean enabled) {
148
m_UndoEnabled = enabled;
152
* returns whether the model is read-only
154
* @return true if model is read-only
156
public boolean isReadOnly() {
161
* sets whether the model is read-only
163
* @param value if true the model is set to read-only
165
public void setReadOnly(boolean value) {
170
* loads the specified ARFF file
172
* @param filename the file to load
174
private void loadFile(String filename) {
175
AbstractFileLoader loader;
177
loader = ConverterUtils.getLoaderForFile(filename);
179
if (loader != null) {
181
loader.setFile(new File(filename));
182
m_Data = loader.getDataSet();
184
catch (Exception e) {
185
ComponentHelper.showMessageBox(
187
"Error loading file...",
189
JOptionPane.OK_CANCEL_OPTION,
190
JOptionPane.ERROR_MESSAGE );
191
System.out.println(e);
200
* @param data the data to use
202
public void setInstances(Instances data) {
209
* @return the current data
211
public Instances getInstances() {
216
* returns the attribute at the given index, can be NULL if not an attribute
219
* @param columnIndex the index of the column
220
* @return the attribute at the position
222
public Attribute getAttributeAt(int columnIndex) {
223
if ( (columnIndex > 0) && (columnIndex < getColumnCount()) )
224
return m_Data.attribute(columnIndex - 1);
230
* returns the TYPE of the attribute at the given position
232
* @param columnIndex the index of the column
233
* @return the attribute type
235
public int getType(int columnIndex) {
236
return getType(0, columnIndex);
240
* returns the TYPE of the attribute at the given position
242
* @param rowIndex the index of the row
243
* @param columnIndex the index of the column
244
* @return the attribute type
246
public int getType(int rowIndex, int columnIndex) {
249
result = Attribute.STRING;
251
if ( (rowIndex >= 0) && (rowIndex < getRowCount())
252
&& (columnIndex > 0) && (columnIndex < getColumnCount()) )
253
result = m_Data.instance(rowIndex).attribute(columnIndex - 1).type();
259
* deletes the attribute at the given col index. notifies the listeners.
261
* @param columnIndex the index of the attribute to delete
263
public void deleteAttributeAt(int columnIndex) {
264
deleteAttributeAt(columnIndex, true);
268
* deletes the attribute at the given col index
270
* @param columnIndex the index of the attribute to delete
271
* @param notify whether to notify the listeners
273
public void deleteAttributeAt(int columnIndex, boolean notify) {
274
if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) {
275
if (!m_IgnoreChanges)
277
m_Data.deleteAttributeAt(columnIndex - 1);
279
notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
284
* deletes the attributes at the given indices
286
* @param columnIndices the column indices
288
public void deleteAttributes(int[] columnIndices) {
291
Arrays.sort(columnIndices);
295
m_IgnoreChanges = true;
296
for (i = columnIndices.length - 1; i >= 0; i--)
297
deleteAttributeAt(columnIndices[i], false);
298
m_IgnoreChanges = false;
300
notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
304
* renames the attribute at the given col index
306
* @param columnIndex the index of the column
307
* @param newName the new name of the attribute
309
public void renameAttributeAt(int columnIndex, String newName) {
310
if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) {
312
m_Data.renameAttribute(columnIndex - 1, newName);
313
notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
318
* sets the attribute at the given col index as the new class attribute, i.e.
319
* it moves it to the end of the attributes
321
* @param columnIndex the index of the column
323
public void attributeAsClassAt(int columnIndex) {
328
if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) {
332
// build order string (1-based!)
334
for (i = 1; i < m_Data.numAttributes() + 1; i++) {
336
if (i == columnIndex)
339
if (!order.equals(""))
341
order += Integer.toString(i);
343
if (!order.equals(""))
345
order += Integer.toString(columnIndex);
348
reorder = new Reorder();
349
reorder.setAttributeIndices(order);
350
reorder.setInputFormat(m_Data);
351
m_Data = Filter.useFilter(m_Data, reorder);
354
m_Data.setClassIndex(m_Data.numAttributes() - 1);
356
catch (Exception e) {
361
notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
366
* deletes the instance at the given index
368
* @param rowIndex the index of the row
370
public void deleteInstanceAt(int rowIndex) {
371
deleteInstanceAt(rowIndex, true);
375
* deletes the instance at the given index
377
* @param rowIndex the index of the row
378
* @param notify whether to notify the listeners
380
public void deleteInstanceAt(int rowIndex, boolean notify) {
381
if ( (rowIndex >= 0) && (rowIndex < getRowCount()) ) {
382
if (!m_IgnoreChanges)
384
m_Data.delete(rowIndex);
388
this, rowIndex, rowIndex,
389
TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE));
394
* deletes the instances at the given positions
396
* @param rowIndices the indices to delete
398
public void deleteInstances(int[] rowIndices) {
401
Arrays.sort(rowIndices);
405
m_IgnoreChanges = true;
406
for (i = rowIndices.length - 1; i >= 0; i--)
407
deleteInstanceAt(rowIndices[i], false);
408
m_IgnoreChanges = false;
412
this, rowIndices[0], rowIndices[rowIndices.length - 1],
413
TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE));
417
* sorts the instances via the given attribute
419
* @param columnIndex the index of the column
421
public void sortInstances(int columnIndex) {
422
if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) {
424
m_Data.sort(columnIndex - 1);
425
notifyListener(new TableModelEvent(this));
430
* returns the column of the given attribute name, -1 if not found
432
* @param name the name of the attribute
433
* @return the column index or -1 if not found
435
public int getAttributeColumn(String name) {
441
for (i = 0; i < m_Data.numAttributes(); i++) {
442
if (m_Data.attribute(i).name().equals(name)) {
452
* returns the most specific superclass for all the cell values in the
453
* column (always String)
455
* @param columnIndex the column index
456
* @return the class of the column
458
public Class getColumnClass(int columnIndex) {
463
if ( (columnIndex >= 0) && (columnIndex < getColumnCount()) ) {
464
if (columnIndex == 0)
465
result = Integer.class;
466
else if (getType(columnIndex) == Attribute.NUMERIC)
467
result = Double.class;
469
result = String.class; // otherwise no input of "?"!!!
476
* returns the number of columns in the model
478
* @return the number of columns
480
public int getColumnCount() {
485
result += m_Data.numAttributes();
491
* checks whether the column represents the class or not
493
* @param columnIndex the index of the column
494
* @return true if the column is the class attribute
496
private boolean isClassIndex(int columnIndex) {
500
index = m_Data.classIndex();
501
result = ((index == - 1) && (m_Data.numAttributes() == columnIndex))
502
|| (index == columnIndex - 1);
508
* returns the name of the column at columnIndex
510
* @param columnIndex the index of the column
511
* @return the name of the column
513
public String getColumnName(int columnIndex) {
518
if ( (columnIndex >= 0) && (columnIndex < getColumnCount()) ) {
519
if (columnIndex == 0) {
520
result = "<html><center>No.<br><font size=\"-2\"> </font></center></html>";
523
if (m_Data != null) {
524
if ( (columnIndex - 1 < m_Data.numAttributes()) ) {
525
result = "<html><center>";
527
if (isClassIndex(columnIndex))
529
+ m_Data.attribute(columnIndex - 1).name()
532
result += m_Data.attribute(columnIndex - 1).name();
535
switch (getType(columnIndex)) {
537
result += "<br><font size=\"-2\">Date</font>";
539
case Attribute.NOMINAL:
540
result += "<br><font size=\"-2\">Nominal</font>";
542
case Attribute.STRING:
543
result += "<br><font size=\"-2\">String</font>";
545
case Attribute.NUMERIC:
546
result += "<br><font size=\"-2\">Numeric</font>";
548
case Attribute.RELATIONAL:
549
result += "<br><font size=\"-2\">Relational</font>";
552
result += "<br><font size=\"-2\">???</font>";
555
result += "</center></html>";
565
* returns the number of rows in the model
567
* @return the number of rows
569
public int getRowCount() {
573
return m_Data.numInstances();
577
* checks whether the value at the given position is missing
579
* @param rowIndex the row index
580
* @param columnIndex the column index
581
* @return true if the value at the position is missing
583
public boolean isMissingAt(int rowIndex, int columnIndex) {
588
if ( (rowIndex >= 0) && (rowIndex < getRowCount())
589
&& (columnIndex > 0) && (columnIndex < getColumnCount()) )
590
result = (m_Data.instance(rowIndex).isMissing(columnIndex - 1));
596
* returns the double value of the underlying Instances object at the
597
* given position, -1 if out of bounds
599
* @param rowIndex the row index
600
* @param columnIndex the column index
601
* @return the underlying value in the Instances object
603
public double getInstancesValueAt(int rowIndex, int columnIndex) {
608
if ( (rowIndex >= 0) && (rowIndex < getRowCount())
609
&& (columnIndex > 0) && (columnIndex < getColumnCount()) )
610
result = m_Data.instance(rowIndex).value(columnIndex - 1);
616
* returns the value for the cell at columnindex and rowIndex
618
* @param rowIndex the row index
619
* @param columnIndex the column index
620
* @return the value at the position
622
public Object getValueAt(int rowIndex, int columnIndex) {
628
if ( (rowIndex >= 0) && (rowIndex < getRowCount())
629
&& (columnIndex >= 0) && (columnIndex < getColumnCount()) ) {
630
if (columnIndex == 0) {
631
result = new Integer(rowIndex + 1);
634
if (isMissingAt(rowIndex, columnIndex)) {
638
switch (getType(columnIndex)) {
640
case Attribute.NOMINAL:
641
case Attribute.STRING:
642
case Attribute.RELATIONAL:
643
result = m_Data.instance(rowIndex).stringValue(columnIndex - 1);
645
case Attribute.NUMERIC:
646
result = new Double(m_Data.instance(rowIndex).value(columnIndex - 1));
649
result = "-can't display-";
655
if (getType(columnIndex) != Attribute.NUMERIC) {
656
if (result != null) {
657
// does it contain "\n" or "\r"? -> replace with red html tag
658
tmp = result.toString();
659
if ( (tmp.indexOf("\n") > -1) || (tmp.indexOf("\r") > -1) ) {
660
tmp = tmp.replaceAll("\\r\\n", "<font color=\"red\"><b>\\\\r\\\\n</b></font>");
661
tmp = tmp.replaceAll("\\r", "<font color=\"red\"><b>\\\\r</b></font>");
662
tmp = tmp.replaceAll("\\n", "<font color=\"red\"><b>\\\\n</b></font>");
663
result = "<html>" + tmp + "</html>";
672
* returns true if the cell at rowindex and columnindexis editable
674
* @param rowIndex the index of the row
675
* @param columnIndex the index of the column
676
* @return true if the cell is editable
678
public boolean isCellEditable(int rowIndex, int columnIndex) {
679
return (columnIndex > 0) && !isReadOnly();
683
* sets the value in the cell at columnIndex and rowIndex to aValue.
684
* but only the value and the value can be changed
686
* @param aValue the new value
687
* @param rowIndex the row index
688
* @param columnIndex the column index
690
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
691
setValueAt(aValue, rowIndex, columnIndex, true);
695
* sets the value in the cell at columnIndex and rowIndex to aValue.
696
* but only the value and the value can be changed
698
* @param aValue the new value
699
* @param rowIndex the row index
700
* @param columnIndex the column index
701
* @param notify whether to notify the listeners
703
public void setValueAt(Object aValue, int rowIndex, int columnIndex, boolean notify) {
711
if (!m_IgnoreChanges)
714
oldValue = getValueAt(rowIndex, columnIndex);
715
type = getType(rowIndex, columnIndex);
716
index = columnIndex - 1;
717
inst = m_Data.instance(rowIndex);
718
att = inst.attribute(index);
721
if (aValue == null) {
722
inst.setValue(index, Instance.missingValue());
725
tmp = aValue.toString();
731
inst.setValue(index, att.parseDate(tmp));
733
catch (Exception e) {
738
case Attribute.NOMINAL:
739
if (att.indexOfValue(tmp) > -1)
740
inst.setValue(index, att.indexOfValue(tmp));
743
case Attribute.STRING:
744
inst.setValue(index, tmp);
747
case Attribute.NUMERIC:
749
Double.parseDouble(tmp);
750
inst.setValue(index, Double.parseDouble(tmp));
752
catch (Exception e) {
757
case Attribute.RELATIONAL:
759
inst.setValue(index, inst.attribute(index).addRelation((Instances) aValue));
761
catch (Exception e) {
767
throw new IllegalArgumentException("Unsupported Attribute type: " + type + "!");
771
// notify only if the value has changed!
772
if (notify && (!("" + oldValue).equals("" + aValue)) )
773
notifyListener(new TableModelEvent(this, rowIndex, columnIndex));
777
* adds a listener to the list that is notified each time a change to data
780
* @param l the listener to add
782
public void addTableModelListener(TableModelListener l) {
787
* removes a listener from the list that is notified each time a change to
788
* the data model occurs
790
* @param l the listener to remove
792
public void removeTableModelListener(TableModelListener l) {
793
m_Listeners.remove(l);
797
* notfies all listener of the change of the model
799
* @param e the event to send to the listeners
801
public void notifyListener(TableModelEvent e) {
803
TableModelListener l;
805
// is notification enabled?
806
if (!isNotificationEnabled())
809
iter = m_Listeners.iterator();
810
while (iter.hasNext()) {
811
l = (TableModelListener) iter.next();
817
* removes the undo history
819
public void clearUndo() {
820
m_UndoList = new Vector();
824
* returns whether an undo is possible, i.e. whether there are any undo points
827
* @return returns TRUE if there is an undo possible
829
public boolean canUndo() {
830
return !m_UndoList.isEmpty();
834
* undoes the last action
839
ObjectInputStream ooi;
843
tempFile = (File) m_UndoList.get(m_UndoList.size() - 1);
845
// read serialized data
846
ooi = new ObjectInputStream(new BufferedInputStream(new FileInputStream(tempFile)));
847
inst = (Instances) ooi.readObject();
852
notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
853
notifyListener(new TableModelEvent(this));
855
catch (Exception e) {
861
m_UndoList.remove(m_UndoList.size() - 1);
866
* adds an undo point to the undo history, if the undo support is enabled
867
* @see #isUndoEnabled()
868
* @see #setUndoEnabled(boolean)
870
public void addUndoPoint() {
872
ObjectOutputStream oos;
874
// undo support currently on?
875
if (!isUndoEnabled())
878
if (getInstances() != null) {
881
tempFile = File.createTempFile("arffviewer", null);
882
tempFile.deleteOnExit();
884
// serialize instances
885
oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(tempFile)));
886
oos.writeObject(getInstances());
891
m_UndoList.add(tempFile);
893
catch (Exception e) {