/* Copyright 2009-2015 David Hadka
*
* This file is part of the MOEA Framework.
*
* The MOEA Framework is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* The MOEA Framework 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 Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the MOEA Framework. If not, see <http://www.gnu.org/licenses/>.
*/
package org.moeaframework.analysis.diagnostics;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JProgressBar;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import org.moeaframework.analysis.collector.Accumulator;
import org.moeaframework.core.Settings;
import org.moeaframework.util.Localization;
/**
* The main window of the diagnostic tool.
*/
public class DiagnosticTool extends JFrame implements ListSelectionListener,
ControllerListener {
private static final long serialVersionUID = -8770087330810075627L;
/**
* The localization instance for produce locale-specific strings.
*/
private static Localization localization = Localization.getLocalization(
DiagnosticTool.class);
/**
* The controller which stores the underlying data model and notifies this
* diagnostic tool of any changes.
*/
private Controller controller;
/**
* The list of all available metrics.
*/
private JList metricList;
/**
* The underlying data model storing all available results.
*/
private SortedListModel<ResultKey> resultListModel;
/**
* The underlying data model storing all available metrics.
*/
private SortedListModel<String> metricListModel;
/**
* The container of all plots.
*/
private JPanel chartContainer;
/**
* The table for displaying all available results.
*/
private JTable resultTable;
/**
* The table model that allows {@code resultListModel} to be displayed in a
* table.
*/
private AbstractTableModel resultTableModel;
/**
* The button for selecting all results.
*/
private JButton selectAll;
/**
* The button for displaying a statistical comparison of selected results.
*/
private JButton showStatistics;
/**
* The control for setting the algorithm used by evaluation jobs.
*/
private JComboBox algorithm;
/**
* The control for setting the problem used by evaluation jobs.
*/
private JComboBox problem;
/**
* The control for setting the number of seeds used by evaluation jobs.
*/
private JSpinner numberOfSeeds;
/**
* The control for setting the number of evaluations used by evaluation
* jobs.
*/
private JSpinner numberOfEvaluations;
/**
* The button for starting a new evaluation job.
*/
private JButton run;
/**
* The button for canceling the current evaluation job.
*/
private JButton cancel;
/**
* The button for clearing all results contained in this diagnostic tool.
*/
private JButton clear;
/**
* The progress bar displaying the individual run progress.
*/
private JProgressBar runProgress;
/**
* The progress bar displaying the overall progress.
*/
private JProgressBar overallProgress;
/**
* The factory for the actions supported in this diagnostic tool window.
*/
private ActionFactory actionFactory;
/**
* Maintains a mapping from series key to paints displayed in the plot.
*/
private PaintHelper paintHelper;
/**
* Constructs a new diagnostic tool window.
*/
public DiagnosticTool() {
super(localization.getString("title.diagnosticTool"));
setSize(800, 600);
setMinimumSize(new Dimension(800, 600));
setExtendedState(JFrame.MAXIMIZED_BOTH);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
initialize();
layoutMenu();
layoutComponents();
}
/**
* Initializes this window. This method is invoked by the constructor, and
* should not be invoked again.
*/
protected void initialize() {
controller = new Controller(this);
controller.addControllerListener(this);
actionFactory = new ActionFactory(this, controller);
resultListModel = new SortedListModel<ResultKey>();
metricListModel = new SortedListModel<String>();
metricList = new JList(metricListModel);
paintHelper = new PaintHelper();
chartContainer = new JPanel();
metricList.addListSelectionListener(this);
//initialize the table containing all available results
resultTableModel = new AbstractTableModel() {
private static final long serialVersionUID = -4148463449906184742L;
@Override
public String getColumnName(int column) {
switch (column) {
case 0:
return localization.getString("text.algorithm");
case 1:
return localization.getString("text.problem");
case 2:
return localization.getString("text.numberOfSeeds");
default:
throw new IllegalStateException();
}
}
@Override
public int getColumnCount() {
return 3;
}
@Override
public int getRowCount() {
return resultListModel.getSize();
}
@Override
public Object getValueAt(int row, int column) {
ResultKey key = resultListModel.getElementAt(row);
switch (column) {
case 0:
return key.getAlgorithm();
case 1:
return key.getProblem();
case 2:
return controller.get(key).size();
default:
throw new IllegalStateException();
}
}
};
resultTable = new JTable(resultTableModel);
resultTable.getSelectionModel().addListSelectionListener(this);
resultTable.addMouseListener(new MouseAdapter() {
public void mouseClicked(final MouseEvent e) {
if (SwingUtilities.isRightMouseButton(e)) {
int index = resultTable.rowAtPoint(e.getPoint());
boolean containsSet = false;
if (index == -1) {
return;
}
ResultKey key = resultListModel.getElementAt(index);
//verify that at least one accumulator contains data
for (Accumulator accumulator : controller.get(key)) {
if (accumulator.keySet().contains(
"Approximation Set")) {
containsSet = true;
}
}
if (!containsSet) {
return;
}
JPopupMenu popupMenu = new JPopupMenu();
popupMenu.add(new JMenuItem(
actionFactory.getShowApproximationSetAction(
resultListModel.getElementAt(index))));
popupMenu.show(resultTable, e.getX(), e.getY());
}
}
});
selectAll = new JButton(actionFactory.getSelectAllAction(resultTable));
showStatistics = new JButton(actionFactory.getShowStatisticsAction());
//initialize the sorted list of algorithms
Set<String> algorithmNames = new HashSet<String>();
for (String algorithm : Settings.getDiagnosticToolAlgorithms()) {
algorithmNames.add(algorithm);
}
for (String algorithm : Settings.getPISAAlgorithms()) {
algorithmNames.add(algorithm);
}
List<String> sortedAlgorithmNames = new ArrayList<String>(
algorithmNames);
Collections.sort(sortedAlgorithmNames);
algorithm = new JComboBox(sortedAlgorithmNames.toArray());
//initialize the sorted list of problems
Set<String> problemNames = new HashSet<String>();
for (String problem : Settings.getDiagnosticToolProblems()) {
problemNames.add(problem);
}
for (String problem : Settings.getProblems()) {
problemNames.add(problem);
}
List<String> sortedProblemNames = new ArrayList<String>(problemNames);
Collections.sort(sortedProblemNames);
problem = new JComboBox(sortedProblemNames.toArray());
//initialize miscellaneous components
numberOfSeeds = new JSpinner(new SpinnerNumberModel(10, 1,
Integer.MAX_VALUE, 10));
numberOfEvaluations = new JSpinner(new SpinnerNumberModel(10000, 500,
Integer.MAX_VALUE, 1000));
run = new JButton(actionFactory.getRunAction());
cancel = new JButton(actionFactory.getCancelAction());
clear = new JButton(actionFactory.getClearAction());
runProgress = new JProgressBar();
overallProgress = new JProgressBar();
algorithm.setEditable(true);
problem.setEditable(true);
}
/**
* Lays out the menu on this window. This method is invoked by the
* constructor, and should not be invoked again.
*/
protected void layoutMenu() {
JMenu file = new JMenu(localization.getString("menu.file"));
file.add(new JMenuItem(actionFactory.getSaveAction()));
file.add(new JMenuItem(actionFactory.getLoadAction()));
file.addSeparator();
file.add(new JMenuItem(actionFactory.getExitAction()));
JMenu view = new JMenu(localization.getString("menu.view"));
JMenuItem individualTraces = new JRadioButtonMenuItem(
actionFactory.getShowIndividualTracesAction());
JMenuItem quantiles = new JRadioButtonMenuItem(
actionFactory.getShowQuantilesAction());
ButtonGroup traceGroup = new ButtonGroup();
traceGroup.add(individualTraces);
traceGroup.add(quantiles);
view.add(individualTraces);
view.add(quantiles);
view.addSeparator();
view.add(new JCheckBoxMenuItem(
actionFactory.getShowLastTraceAction()));
JMenu metrics = new JMenu(localization.getString("menu.collect"));
metrics.add(new JMenuItem(
actionFactory.getEnableAllIndicatorsAction()));
metrics.add(new JMenuItem(
actionFactory.getDisableAllIndicatorsAction()));
metrics.addSeparator();
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeHypervolumeAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeGenerationalDistanceAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeInvertedGenerationalDistanceAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeSpacingAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeAdditiveEpsilonIndicatorAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeContributionAction()));
metrics.addSeparator();
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeEpsilonProgressAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeAdaptiveMultimethodVariationAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeAdaptiveTimeContinuationAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeElapsedTimeAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludePopulationSizeAction()));
metrics.add(new JCheckBoxMenuItem(
actionFactory.getIncludeApproximationSetAction()));
JMenu help = new JMenu(localization.getString("menu.help"));
help.add(new JMenuItem(actionFactory.getAboutDialogAction()));
JMenu usage = new JMenu(actionFactory.getMemoryUsageAction());
JMenuBar menuBar = new JMenuBar();
menuBar.add(file);
menuBar.add(view);
menuBar.add(metrics);
menuBar.add(help);
menuBar.add(Box.createHorizontalGlue());
menuBar.add(usage);
setJMenuBar(menuBar);
}
/**
* Lays out the components on this window. This method is invoked by the
* constructor, and should not be invoked again.
*/
protected void layoutComponents() {
GridBagConstraints label = new GridBagConstraints();
label.gridx = 0;
label.gridy = GridBagConstraints.RELATIVE;
label.anchor = GridBagConstraints.EAST;
label.insets = new Insets(0, 5, 5, 25);
GridBagConstraints field = new GridBagConstraints();
field.gridx = 1;
field.gridy = GridBagConstraints.RELATIVE;
field.fill = GridBagConstraints.HORIZONTAL;
field.weightx = 1.0;
field.insets = new Insets(0, 0, 5, 5);
GridBagConstraints button = new GridBagConstraints();
button.gridx = 0;
button.gridwidth = 2;
button.fill = GridBagConstraints.HORIZONTAL;
button.insets = new Insets(0, 0, 5, 0);
JPanel analysisPane = new JPanel(new FlowLayout(FlowLayout.CENTER));
analysisPane.add(selectAll);
analysisPane.add(showStatistics);
JPanel resultPane = new JPanel(new BorderLayout());
resultPane.setBorder(BorderFactory.createTitledBorder(
localization.getString("text.displayedResults")));
resultPane.add(new JScrollPane(resultTable), BorderLayout.CENTER);
resultPane.add(analysisPane, BorderLayout.SOUTH);
resultPane.setMinimumSize(new Dimension(100, 100));
JPanel metricPane = new JPanel(new BorderLayout());
metricPane.setBorder(BorderFactory.createTitledBorder(
localization.getString("text.displayedMetrics")));
metricPane.add(new JScrollPane(metricList), BorderLayout.CENTER);
metricPane.setMinimumSize(new Dimension(100, 100));
JPanel selectionPane = new JPanel(new GridLayout(2, 1));
selectionPane.add(resultPane);
selectionPane.add(metricPane);
JPanel buttonPane = new JPanel(new FlowLayout(FlowLayout.CENTER));
buttonPane.add(run);
buttonPane.add(cancel);
buttonPane.add(clear);
JPanel controlPane = new JPanel(new GridBagLayout());
controlPane.setBorder(BorderFactory.createTitledBorder(
localization.getString("text.controls")));
controlPane.add(new JLabel(
localization.getString("text.algorithm") + ":"), label);
controlPane.add(algorithm, field);
controlPane.add(new JLabel(
localization.getString("text.problem") + ":"), label);
controlPane.add(problem, field);
controlPane.add(new JLabel(
localization.getString("text.numberOfSeeds") + ":"), label);
controlPane.add(numberOfSeeds, field);
controlPane.add(new JLabel(
localization.getString("text.numberOfEvaluations") + ":"),
label);
controlPane.add(numberOfEvaluations, field);
controlPane.add(buttonPane, button);
controlPane.add(new JPanel(), button);
controlPane.add(new JLabel(
localization.getString("text.runProgress") + ":"), label);
controlPane.add(runProgress, field);
controlPane.add(new JLabel(
localization.getString("text.overallProgress") + ":"), label);
controlPane.add(overallProgress, field);
JPanel controls = new JPanel();
controls.setLayout(new BoxLayout(controls, BoxLayout.Y_AXIS));
controls.add(controlPane);
controls.add(selectionPane);
controls.setMinimumSize(controlPane.getPreferredSize());
controls.setPreferredSize(controlPane.getPreferredSize());
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
controls, chartContainer);
splitPane.setDividerLocation(-1);
getContentPane().setLayout(new BorderLayout());
getContentPane().add(splitPane, BorderLayout.CENTER);
}
/**
* Updates the models underlying the GUI components as a result of model
* changes. This method must only be invoked on the event dispatch thread.
*/
protected void updateModel() {
//determine selection mode
List<ResultKey> selectedResults = getSelectedResults();
List<String> selectedMetrics = getSelectedMetrics();
boolean selectAllResults = false;
boolean selectFirstMetric = false;
if (selectedResults.size() == resultListModel.getSize()) {
selectAllResults = true;
}
if ((selectedMetrics.size() == 0) && (metricListModel.getSize() == 0)) {
selectFirstMetric = true;
}
//update metric list and result table contents
resultListModel.addAll(controller.getKeys());
for (ResultKey key : controller.getKeys()) {
for (Accumulator accumulator : controller.get(key)) {
metricListModel.addAll(accumulator.keySet());
}
}
//update metric list selection
metricList.getSelectionModel().removeListSelectionListener(this);
metricList.clearSelection();
if (selectFirstMetric) {
metricList.setSelectedIndex(0);
} else {
for (String metric : selectedMetrics) {
int index = metricListModel.getIndexOf(metric);
metricList.getSelectionModel().addSelectionInterval(index,
index);
}
}
metricList.getSelectionModel().addListSelectionListener(this);
//update result table selection
resultTable.getSelectionModel().removeListSelectionListener(this);
resultTableModel.fireTableDataChanged();
if (selectAllResults && (selectedResults.size() <
resultListModel.getSize())) {
resultTable.getSelectionModel().addSelectionInterval(0,
resultListModel.getSize()-1);
} else {
for (ResultKey key : selectedResults) {
int index = resultListModel.getIndexOf(key);
resultTable.getSelectionModel().addSelectionInterval(index,
index);
}
}
resultTable.getSelectionModel().addListSelectionListener(this);
}
/**
* Returns the controller used by this diagnostic tool instance. This
* controller provides access to the underlying data model displayed in
* this window.
*
* @return the controller used by this diagnostic tool instance
*/
public Controller getController() {
return controller;
}
/**
* Returns the paint helper used by this diagnostic tool instance. This
* paint helper contains the mapping from series to paints displayed in this
* window.
*
* @return the paint helper used by this diagnostic tool instance
*/
public PaintHelper getPaintHelper() {
return paintHelper;
}
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting()) {
return;
}
controller.fireViewChangedEvent();
}
/**
* Invoked when the underlying data model is cleared, resulting in the GUI
* removing and resetting all components. This method must only be invoked
* on the event dispatch thread.
*/
protected void clear() {
resultListModel.clear();
resultTable.getSelectionModel().clearSelection();
resultTableModel.fireTableDataChanged();
metricListModel.clear();
metricList.getSelectionModel().clearSelection();
paintHelper.clear();
chartContainer.removeAll();
chartContainer.revalidate();
chartContainer.repaint();
}
/**
* Updates the chart layout when the user changes which metrics to plot.
* This method must only be invoked on the event dispatch thread.
*/
protected void updateChartLayout() {
chartContainer.removeAll();
List<String> selectedMetrics = getSelectedMetrics();
if (selectedMetrics.size() > 0) {
if (selectedMetrics.size() <= 1) {
chartContainer.setLayout(new GridLayout(1, 1));
} else if (selectedMetrics.size() <= 2) {
chartContainer.setLayout(new GridLayout(2, 1));
} else if (selectedMetrics.size() <= 4) {
chartContainer.setLayout(new GridLayout(2, 2));
} else if (selectedMetrics.size() <= 6) {
chartContainer.setLayout(new GridLayout(3, 2));
} else {
chartContainer.setLayout(new GridLayout(
(int)Math.ceil(selectedMetrics.size()/3.0), 3));
}
GridLayout layout = (GridLayout)chartContainer.getLayout();
int spaces = layout.getRows()*layout.getColumns();
for (int i=0; i<Math.max(spaces, selectedMetrics.size()); i++) {
if (i < selectedMetrics.size()) {
chartContainer.add(createChart(selectedMetrics.get(i)));
} else {
chartContainer.add(new EmptyPlot(this));
}
}
}
chartContainer.revalidate();
}
/**
* Returns a list of the selected metrics.
*
* @return a list of the selected metrics
*/
protected List<String> getSelectedMetrics() {
List<String> selectedMetrics = new ArrayList<String>();
for (int index : metricList.getSelectedIndices()) {
selectedMetrics.add(metricListModel.getElementAt(index));
}
return selectedMetrics;
}
/**
* Returns a list of the selected results.
*
* @return a list of the selected results
*/
protected List<ResultKey> getSelectedResults() {
List<ResultKey> selectedResults = new ArrayList<ResultKey>();
for (int index : resultTable.getSelectedRows()) {
selectedResults.add(resultListModel.getElementAt(index));
}
return selectedResults;
}
/**
* Returns the algorithm selected in the run control pane.
*
* @return the algorithm selected for the next evaluation job
*/
protected String getAlgorithm() {
return (String)algorithm.getSelectedItem();
}
/**
* Returns the problem selected in the run control pane.
*
* @return the problem selected in the run control pane
*/
protected String getProblem() {
return (String)problem.getSelectedItem();
}
/**
* Returns the number of evaluations set in the run control pane.
*
* @return the number of evaluations set in the run control pane
*/
protected int getNumberOfEvaluations() {
return (Integer)numberOfEvaluations.getValue();
}
/**
* Returns the number of seeds set in the run control pane.
*
* @return the number of seeds set in the run control pane
*/
protected int getNumberOfSeeds() {
return (Integer)numberOfSeeds.getValue();
}
/**
* Creates and returns the GUI component for plotting the specified metric.
*
* @param metric the metric to plot
* @return the GUI component for plotting the specified metric
*/
protected ResultPlot createChart(String metric) {
if (metric.equals("Approximation Set")) {
return new ApproximationSetPlot(this, metric);
} else {
return new LinePlot(this, metric);
}
}
@Override
public void controllerStateChanged(ControllerEvent event) {
if (event.getType().equals(ControllerEvent.Type.MODEL_CHANGED)) {
if (controller.getKeys().isEmpty()) {
clear();
} else {
updateModel();
}
} else if (event.getType().equals(
ControllerEvent.Type.PROGRESS_CHANGED)) {
runProgress.setValue(controller.getRunProgress());
overallProgress.setValue(controller.getOverallProgress());
} else if (event.getType().equals(ControllerEvent.Type.VIEW_CHANGED)) {
updateChartLayout();
}
}
@Override
public void dispose() {
controller.cancel();
super.dispose();
}
}