/*
* Copyright 2004-2010 Information & Software Engineering Group (188/1)
* Institute of Software Technology and Interactive Systems
* Vienna University of Technology, Austria
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.ifs.tuwien.ac.at/dm/somtoolbox/license.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.tuwien.ifs.feature.evaluation;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import javax.swing.AbstractButton;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.border.TitledBorder;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import org.apache.commons.lang.ArrayUtils;
import org.jdesktop.swingx.VerticalLayout;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.statistics.HistogramDataset;
import com.martiansoftware.jsap.Parameter;
import at.tuwien.ifs.commons.gui.controls.swing.table.ButtonCellEditor;
import at.tuwien.ifs.commons.gui.controls.swing.table.ButtonCellRenderer;
import at.tuwien.ifs.commons.gui.util.MaximisedJFrame;
import at.tuwien.ifs.commons.models.ClassComboBoxModel;
import at.tuwien.ifs.commons.util.io.ExtensionFileFilterSwing;
import at.tuwien.ifs.somtoolbox.SOMToolboxException;
import at.tuwien.ifs.somtoolbox.apps.SOMToolboxApp;
import at.tuwien.ifs.somtoolbox.apps.viewer.controls.player.AudioPlayer;
import at.tuwien.ifs.somtoolbox.apps.viewer.controls.player.PlayerControl;
import at.tuwien.ifs.somtoolbox.data.AbstractSOMLibSparseInputData;
import at.tuwien.ifs.somtoolbox.data.InputData;
import at.tuwien.ifs.somtoolbox.data.InputDataFactory;
import at.tuwien.ifs.somtoolbox.data.SOMLibClassInformation;
import at.tuwien.ifs.somtoolbox.data.metadata.MP3VectorMetaData;
import at.tuwien.ifs.somtoolbox.layers.metrics.AbstractMetric;
import at.tuwien.ifs.somtoolbox.layers.metrics.DistanceMetric;
import at.tuwien.ifs.somtoolbox.layers.metrics.L2Metric;
import at.tuwien.ifs.somtoolbox.layers.metrics.Metrics;
import at.tuwien.ifs.somtoolbox.util.FileUtils;
import at.tuwien.ifs.somtoolbox.util.GridBagConstraintsIFS;
import at.tuwien.ifs.somtoolbox.util.NumberUtils;
import at.tuwien.ifs.somtoolbox.util.StringUtils;
import at.tuwien.ifs.somtoolbox.util.UiUtils;
import at.tuwien.ifs.somtoolbox.util.comparables.InputDistance;
/**
* @author Rudolf Mayer
* @version $Id: $
*/
public class SimilarityRetrievalGUI extends MaximisedJFrame implements SOMToolboxApp {
private static final long serialVersionUID = 1L;
public static final Parameter[] OPTIONS = new Parameter[] {};
public static String DESCRIPTION = "GUI for similarity retrieval";
public static String LONG_DESCRIPTION = "Provides a graphical interface for similarity retrieval. Allows to load multiple vector files";
public static final Type APPLICATION_TYPE = Type.Utils;
private static final String[] resultColumnNames = { "Rank", "Vector Label", "Distance" };
private static final String[] databaseDetailsColumnNames = { "", "Filename", "Class" };
private ArrayList<InputData> inputData = new ArrayList<InputData>();
private SOMLibClassInformation classInfo = null;
private ButtonGroup bgInputData = new ButtonGroup();
private ButtonGroup bgDistanceDisplay = new ButtonGroup();
private JButton btnStart;
private JButton btnSaveResults;
private JButton buttonLoadClassInfo;
private JLabel labelNoInputData = new JLabel("No input data loaded!");
private SpinnerNumberModel modelNumberNeighbours = new SpinnerNumberModel(0, 0, 0, 1);
private JSpinner spinnerNumberNeighbours = new JSpinner(modelNumberNeighbours);
private JPanel panelLoadedFeatureFiles = new JPanel(new VerticalLayout());
private JPanel panelRetrieval;
private ChartPanel chartPanel = new ChartPanel(null);
private JComboBox comboQueryVector = new JComboBox();
private JComboBox boxMetric = new JComboBox(new ClassComboBoxModel<DistanceMetric>(
Metrics.getAvailableMetricClasses()));
private JTable resultsTable = new JTable(new DefaultTableModel(new Object[][] {}, resultColumnNames));
private JTable databaseDetailsTable;
private JFileChooser fileChooser = new JFileChooser("/data/music/ISMIRgenre/");
private JTextField txtFieldMusicPath = new JTextField(50);
private AudioPlayer player = new AudioPlayer();
public SimilarityRetrievalGUI() {
super("Similarity Retrieval GUI");
setLayout(new GridBagLayout());
GridBagConstraintsIFS gcMain = new GridBagConstraintsIFS(GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// the panel with the feature files, allows to load new ones
JPanel panelFeatureFiles = UiUtils.makeBorderedPanel(new VerticalLayout(), "Feature files");
getContentPane().add(panelFeatureFiles, gcMain);
panelLoadedFeatureFiles.add(labelNoInputData);
panelFeatureFiles.add(panelLoadedFeatureFiles);
JButton btnLoad = initButtonLoad();
panelFeatureFiles.add(btnLoad);
txtFieldMusicPath.setToolTipText("Path to the music");
// TODO: remove
txtFieldMusicPath.setText("/data/music/ISMIRgenre/mp3_44khz_128kbit_stereo_30sec");
JButton btnBrowseMusicPath = UiUtils.createBrowseButton(txtFieldMusicPath, this, true);
JPanel panelMusicPath = new JPanel();
panelMusicPath.add(new JLabel("Music path"));
panelMusicPath.add(txtFieldMusicPath);
panelMusicPath.add(btnBrowseMusicPath);
panelFeatureFiles.add(panelMusicPath);
initButtonStart();
initButtonSaveResults();
JRadioButton rbDistanceAbsolute = UiUtils.makeRadioButton("absolute", bgDistanceDisplay, true);
bgDistanceDisplay.add(rbDistanceAbsolute);
JRadioButton rbDistanceRelative = UiUtils.makeRadioButton("relative", bgDistanceDisplay);
bgDistanceDisplay.add(rbDistanceRelative);
initPanelRetrieval();
((JSpinner.DefaultEditor) spinnerNumberNeighbours.getEditor()).getTextField().setColumns(6);
panelRetrieval.setBorder(new TitledBorder("Options"));
GridBagConstraintsIFS gc = new GridBagConstraintsIFS().setInsets(5, 2);
panelRetrieval.add(new JLabel("# to retrieve"), gc);
panelRetrieval.add(spinnerNumberNeighbours, gc.nextCol());
panelRetrieval.add(new JLabel("Query vector"), gc.nextRow());
panelRetrieval.add(comboQueryVector, gc.nextCol());
panelRetrieval.add(new JLabel("Distances"), gc.nextRow());
panelRetrieval.add(UiUtils.makeAndFillPanel(rbDistanceAbsolute, rbDistanceRelative), gc.nextCol());
boxMetric.setSelectedItem(L2Metric.class.getSimpleName());
panelRetrieval.add(new JLabel("Distance metric"), gc.nextRow());
panelRetrieval.add(boxMetric, gc.nextCol());
gc.nextRow().setGridWidth(2).setAnchor(GridBagConstraints.CENTER);
panelRetrieval.add(UiUtils.makeAndFillPanel(btnStart, btnSaveResults), gc);
panelRetrieval.setEnabled(false);
getContentPane().add(panelRetrieval, gcMain.nextRow());
resizeResultTableColumns();
JScrollPane scrollPaneResults = new JScrollPane(resultsTable);
scrollPaneResults.setBorder(new TitledBorder("Results"));
getContentPane().add(scrollPaneResults, gcMain.nextRow());
databaseDetailsTable = new JTable(new DefaultTableModel(new Object[][] {}, databaseDetailsColumnNames));
databaseDetailsTable.setAutoCreateRowSorter(true);
databaseDetailsTable.setDefaultEditor(JButton.class, new ButtonCellEditor());
resizeDatabaseDetailsTableColumns();
JScrollPane scrollPaneDatabaseDetails = new JScrollPane(databaseDetailsTable);
// panel in the upper-right corner, holding the database table & buttons to load class assignment
JPanel databaseDetailsPanel = UiUtils.makeBorderedPanel(new GridBagLayout(), "Database Details");
GridBagConstraintsIFS gcDatabaseDetails = new GridBagConstraintsIFS(GridBagConstraints.CENTER,
GridBagConstraints.BOTH);
gcDatabaseDetails.setWeights(1, 1);
databaseDetailsPanel.add(scrollPaneDatabaseDetails, gcDatabaseDetails);
initButtonLoadClassInfo();
databaseDetailsPanel.add(buttonLoadClassInfo, gc.nextRow());
JPanel histogramPanel = UiUtils.makeBorderedPanel("Histogram of Distances");
histogramPanel.add(chartPanel);
JPanel detailsPanel = new JPanel(new VerticalLayout());
gcMain.setGridHeight(3);
gcMain.setWeights(1, 1);
getContentPane().add(detailsPanel, gcMain.moveTo(1, 0));
detailsPanel.add(databaseDetailsPanel);
detailsPanel.add(histogramPanel);
}
private void initPanelRetrieval() {
panelRetrieval = new JPanel(new GridBagLayout()) {
private static final long serialVersionUID = 1L;
@Override
public void setEnabled(boolean enabled) {
// also enable all subcomponents
super.setEnabled(enabled);
for (Component c : panelRetrieval.getComponents()) {
c.setEnabled(enabled);
}
}
};
}
private void initButtonSaveResults() {
btnSaveResults = new JButton("Save as ...");
btnSaveResults.setEnabled(false); // can't save right away, need a first search
btnSaveResults.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
if (fileChooser.getSelectedFile() != null) { // reusing the dialog
fileChooser.setSelectedFile(null);
}
int returnVal = fileChooser.showDialog(SimilarityRetrievalGUI.this, "Save results to file ...");
if (returnVal == JFileChooser.APPROVE_OPTION) {
try {
PrintWriter w = FileUtils.openFileForWriting("Results file",
fileChooser.getSelectedFile().getAbsolutePath());
TableModel model = resultsTable.getModel();
// column headers
for (int col = 0; col < model.getColumnCount(); col++) {
w.print(model.getColumnName(col) + "\t");
}
w.println();
// write each data row
for (int row = 0; row < model.getRowCount(); row++) {
for (int col = 0; col < model.getColumnCount(); col++) {
w.print(model.getValueAt(row, col) + "\t");
}
w.println();
}
w.flush();
w.close();
JOptionPane.showMessageDialog(SimilarityRetrievalGUI.this, "Successfully wrote to file "
+ fileChooser.getSelectedFile().getAbsolutePath(), "Results stored",
JOptionPane.INFORMATION_MESSAGE);
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
});
}
private void initButtonStart() {
btnStart = new JButton("Start");
btnStart.setEnabled(false);
btnStart.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Enumeration<AbstractButton> elements = bgInputData.getElements();
while (elements.hasMoreElements()) {
InputDataRadioButton rb = (InputDataRadioButton) elements.nextElement();
if (rb.isSelected()) {
AbstractSOMLibSparseInputData inputData = rb.inputData;
try {
@SuppressWarnings("unchecked")
Class<? extends DistanceMetric> selectedClass = ((ClassComboBoxModel<DistanceMetric>) boxMetric.getModel()).getSelectedClass();
int inputDatumIndex = inputData.getInputDatumIndex((String) comboQueryVector.getSelectedItem());
DistanceMetric metric = AbstractMetric.instantiateNice(selectedClass.getName());
ArrayList<InputDistance> distances = inputData.getDistances(inputDatumIndex, metric);
Collections.sort(distances);
// prepare the data for the table
double maxDistance = distances.get(distances.size() - 1).getDistance();
int neighbours = modelNumberNeighbours.getNumber().intValue();
String actionCommand = bgDistanceDisplay.getSelection().getActionCommand();
boolean isAbsolute = !actionCommand.equals("relative");
Object[][] data = new Object[neighbours][3];
if (isAbsolute) {
for (int i = 0; i < data.length; i++) {
data[i] = new Object[] { i + 1, distances.get(i).getInput().getLabel(),
NumberUtils.setScale(4, distances.get(i).getDistance()) };
}
} else {
for (int i = 0; i < data.length; i++) {
data[i] = new Object[] { i + 1, distances.get(i).getInput().getLabel(),
StringUtils.formatAsPercent(distances.get(i).getDistance(), maxDistance, 3) };
}
}
resultsTable.setModel(new DefaultTableModel(data, resultColumnNames));
resizeResultTableColumns();
// prepare the data for the chart
double[] values = InputDistance.getDistanceValuesOnly(distances);
if (!isAbsolute) { // convert values to percent
for (int i = 0; i < values.length; i++) {
values[i] = values[i] * 100.0 / maxDistance;
}
}
HistogramDataset ds = new HistogramDataset();
ds.addSeries("Distance", values, 100, 0, isAbsolute ? maxDistance : 100);
JFreeChart chart = ChartFactory.createHistogram(null, isAbsolute ? "absolute distance"
: "distance in percent", "# of objects", ds, PlotOrientation.VERTICAL, false, true,
true);
chartPanel.setChart(chart);
} catch (SOMToolboxException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
// enable the saveAs button
btnSaveResults.setEnabled(true);
}
});
}
public JButton initButtonLoad() {
JButton buttonLoad = new JButton("Load");
buttonLoad.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
fileChooser.setFileFilter(new ExtensionFileFilterSwing(new String[] { "arff", "vec", "rp", "rh", "ssd",
"tfxidf" }));
if (fileChooser.getSelectedFile() != null) { // reusing the dialog
fileChooser.setSelectedFile(null);
}
// TODO: remove
fileChooser.setSelectedFile(new File("/data/music/ISMIRgenre/vec/mp3_vec_conv_from_wav/ISMIRgenre.rp"));
int returnVal = fileChooser.showDialog(SimilarityRetrievalGUI.this, "Open input data");
if (returnVal == JFileChooser.APPROVE_OPTION) {
// FIXME: remove dependency on AbstractSOMLibSparseInputData
AbstractSOMLibSparseInputData data = (AbstractSOMLibSparseInputData) InputDataFactory.open(fileChooser.getSelectedFile().getAbsolutePath());
String[] newLabels = data.getLabels();
Arrays.sort(newLabels);
if (inputData.size() > 0) {
// check if the input data files match; if not, don't add this new one
String[] existingLabels = inputData.get(0).getLabels();
Arrays.sort(existingLabels);
if (!ArrayUtils.isEquals(existingLabels, newLabels)) {
JOptionPane.showMessageDialog(SimilarityRetrievalGUI.this,
"New data loaded doesn't have the same labels as the existing ones. Aborting",
"Error", JOptionPane.ERROR_MESSAGE);
System.out.println(Arrays.toString(existingLabels));
System.out.println(Arrays.toString(newLabels));
return;
}
}
inputData.add(data);
final InputDataRadioButton rb = new InputDataRadioButton(data);
bgInputData.add(rb);
rb.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String actionCommand = rb.getActionCommand();
System.out.println(actionCommand);
}
});
if (inputData.size() == 1) { // first time loaded
rb.setSelected(true);
// enable the retrieval panel
panelRetrieval.setEnabled(true);
panelLoadedFeatureFiles.remove(labelNoInputData);
// populate the combo box for the retrieval
comboQueryVector.setModel(new DefaultComboBoxModel(data.getLabels()));
modelNumberNeighbours.setMaximum(data.numVectors() - 1);
modelNumberNeighbours.setValue(Math.min(5, data.numVectors()));
spinnerNumberNeighbours.setToolTipText("Maximum value:" + (data.numVectors() - 1));
btnStart.setEnabled(true);
// fill the library tab
Object[][] libraryData = new Object[data.numVectors()][];
ImageIcon icon = UiUtils.getIcon(PlayerControl.ICON_PREFIX, "play" + PlayerControl.ICON_SUFFIX);
for (int i = 0; i < libraryData.length; i++) {
final JButton button = new JButton(icon);
final int index = i;
button.setActionCommand(String.valueOf(i));
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String label = inputData.get(0).getLabel(index);
try {
player.stop();
player.play(new MP3VectorMetaData(new File(txtFieldMusicPath.getText()
+ File.separator + label), label));
} catch (FileNotFoundException e1) {
JOptionPane.showMessageDialog(SimilarityRetrievalGUI.this,
"Error playing audio file: " + e1.getMessage(), "Playback Error",
JOptionPane.ERROR_MESSAGE);
e1.printStackTrace();
}
}
});
libraryData[i] = new Object[] { button, data.getLabel(i), "unknown" };
}
databaseDetailsTable.setModel(new DefaultTableModel(libraryData, databaseDetailsColumnNames));
databaseDetailsTable.getColumn(databaseDetailsColumnNames[0]).setCellEditor(
new ButtonCellEditor());
databaseDetailsTable.getColumn("").setCellRenderer(new ButtonCellRenderer());
resizeDatabaseDetailsTableColumns();
buttonLoadClassInfo.setEnabled(true);
}
panelLoadedFeatureFiles.add(rb);
pack();
}
}
});
return buttonLoad;
}
private JButton initButtonLoadClassInfo() {
buttonLoadClassInfo = new JButton("Load class info file");
buttonLoadClassInfo.setEnabled(false);
buttonLoadClassInfo.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
fileChooser.setFileFilter(new ExtensionFileFilterSwing(new String[] { "cls", "txt" }));
if (fileChooser.getSelectedFile() != null) { // reusing the dialog
fileChooser.setSelectedFile(null);
}
// TODO: remove
fileChooser.setSelectedFile(new File("/data/music/ISMIRgenre/filelist_ISMIRgenre_mp3_wclasses.txt"));
int returnVal = fileChooser.showDialog(SimilarityRetrievalGUI.this, "Open class information file");
if (returnVal == JFileChooser.APPROVE_OPTION) {
try {
SOMLibClassInformation clsInfo = new SOMLibClassInformation(
fileChooser.getSelectedFile().getAbsolutePath());
// Sanity check if all the inputs are in the class info file
String[] labels = inputData.get(0).getLabels();
ArrayList<String> missingLabels = new ArrayList<String>();
for (String label : labels) {
if (!clsInfo.hasClassAssignmentForName(label)) {
missingLabels.add(label);
}
}
int answer = JOptionPane.YES_OPTION;
if (missingLabels.size() > 0) {
System.out.println(missingLabels);
String missing = StringUtils.toString(missingLabels.toArray(), 5);
answer = JOptionPane.showConfirmDialog(SimilarityRetrievalGUI.this,
"Class information file '" + fileChooser.getSelectedFile().getAbsolutePath()
+ "' is missing the class assignment for " + missingLabels.size()
+ " vectors\n " + missing + "\nContinue loading?",
"Missing class assignment", JOptionPane.YES_NO_OPTION);
}
if (missingLabels.size() == 0 || answer == JOptionPane.YES_OPTION) {
classInfo = clsInfo;
TableModel model = databaseDetailsTable.getModel();
for (int i = 0; i < labels.length; i++) {
model.setValueAt(clsInfo.getClassName(labels[i]), i, 2);
}
resizeDatabaseDetailsTableColumns();
}
} catch (SOMToolboxException e1) {
JOptionPane.showMessageDialog(SimilarityRetrievalGUI.this,
"Error loading class information file: " + e1.getMessage() + ". Aborting", "Error",
JOptionPane.ERROR_MESSAGE);
e1.printStackTrace();
}
}
}
});
return buttonLoadClassInfo;
}
private void resizeResultTableColumns() {
UiUtils.packColumns(resultsTable, 2);
}
private void resizeDatabaseDetailsTableColumns() {
UiUtils.packColumns(databaseDetailsTable, 2);
}
public static void main(String[] args) {
SimilarityRetrievalGUI gui = new SimilarityRetrievalGUI();
gui.pack();
gui.setVisible(true);
}
private class InputDataRadioButton extends JRadioButton {
private static final long serialVersionUID = 1L;
private AbstractSOMLibSparseInputData inputData;
InputDataRadioButton(AbstractSOMLibSparseInputData data) {
super(data.getDataSource());
this.inputData = data;
}
}
}