/**
* Copyright (C) 2006, Laboratorio di Valutazione delle Prestazioni - Politecnico di Milano
* 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package jmt.gui.exact.panels;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.DefaultCellEditor;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.KeyStroke;
import javax.swing.ScrollPaneConstants;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import jmt.framework.data.ArrayUtils;
import jmt.framework.gui.help.HoverHelp;
import jmt.framework.gui.table.editors.ButtonCellEditor;
import jmt.framework.gui.wizard.WizardPanel;
import jmt.gui.common.resources.JMTImageLoader;
import jmt.gui.exact.ExactConstants;
import jmt.gui.exact.ExactModel;
import jmt.gui.exact.ExactWizard;
import jmt.gui.exact.table.ComboBoxCell;
import jmt.gui.exact.table.DisabledCellRenderer;
import jmt.gui.exact.table.ExactTable;
import jmt.gui.exact.table.ExactTableModel;
import jmt.gui.exact.table.ListOp;
/**
* @author alyf (Andrea Conti)
* Date: 11-set-2003
* Time: 23.48.19
* Modifyed by Bertoli Marco
*/
/**
* 1st panel: classes number, names, types and data
*/
public final class ClassesPanel extends WizardPanel implements ExactConstants, ForceUpdatablePanel {
/**
*
*/
private static final long serialVersionUID = 1L;
private HoverHelp help;
private static final String helpText = "<html>In this panel you can define the number of stations in the system and their properties.<br><br>"
+ " To edit values, single-click on the desired cell"
+ " and start typing.<br> To select classes click or drag on the row headers.<br> <b>For a list of the available operations right-click"
+ " on the table</b>.<br>" + " Pressing DELETE removes all selected classes from the system.</html>";
private ExactWizard ew;
private boolean isLd;
private int classes;
private String[] classNames;
private int[] classTypes;
private double[] classData;
private int nameCounter = 1;
/** Edited by Georgios Poullaides **/
private int algIndex = 0;
/** End **/
private List<ListOp> classOps;
private boolean hasDeletes;
private boolean deleting = false;
private JSpinner classSpinner = new JSpinner(new SpinnerNumberModel(1, 1, MAX_CLASSES, 1));
private ClassTable classTable;
private ChangeListener spinnerListener = new ChangeListener() {
public void stateChanged(ChangeEvent ce) {
if (!deleting) {
updateSizes();
}
}
};
private AbstractAction deleteClass = new AbstractAction("Delete selected classes") {
/**
*
*/
private static final long serialVersionUID = 1L;
{
putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0, false));
putValue(Action.SHORT_DESCRIPTION, "Deletes selected classes from the system");
}
public void actionPerformed(ActionEvent e) {
deleteSelectedClasses();
}
};
private AbstractAction deleteOneClass = new AbstractAction("") {
/**
*
*/
private static final long serialVersionUID = 1L;
{
putValue(Action.SHORT_DESCRIPTION, "Delete This Class");
putValue(Action.SMALL_ICON, JMTImageLoader.loadImage("Close"));
}
public void actionPerformed(ActionEvent e) {
}
};
private AbstractAction addClass = new AbstractAction("New Class") {
/**
*
*/
private static final long serialVersionUID = 1L;
{
putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.ALT_MASK));
putValue(Action.SHORT_DESCRIPTION, "Adds a new Class to Model");
}
public void actionPerformed(ActionEvent e) {
addClass();
}
};
public ClassesPanel(ExactWizard ew) {
this.ew = ew;
help = ew.getHelp();
classOps = new ArrayList<ListOp>();
sync();
initComponents();
makeNames();
}
private void sync() {
hasDeletes = false;
classOps.clear();
/* sync status with data object */
/* arrays are copied to ensure data object consistency is preserved */
ExactModel data = ew.getData();
synchronized (data) {
classes = data.getClasses();
isLd = data.isLd();
classNames = ArrayUtils.copy(data.getClassNames());
classTypes = ArrayUtils.copy(data.getClassTypes());
classData = ArrayUtils.copy(data.getClassData());
//TODO
//NEW
//@author Stefano Omini
if (isLd) {
//if load dependent is no more supported after class changes
//remove all load dependent stations
if (!(data.isClosed() && !data.isMultiClass())) {
JOptionPane
.showMessageDialog(
this,
"<html><center>jMVA allows Load Dependent stations only for single class closed model. <br> Load Dependent stations will be replaced with Load Independent stations.</center></html>",
"Warning", JOptionPane.WARNING_MESSAGE);
data.removeLD();
isLd = false;
}
}
//end NEW
}
classSpinner.setValue(new Integer(classes));
}
/**
* make up names for null entries
*/
private void makeNames() {
for (int i = 0; i < classNames.length; i++) {
if (classNames[i] == null) {
classNames[i] = "Class" + (++nameCounter);
}
}
}
/**
* resize internal data structures according to new values. intended to be called from a listener.
*/
private void updateSizes() {
setNumberOfClasses(((Integer) classSpinner.getValue()).intValue());
}
private void addClass() {
setNumberOfClasses(classes + 1);
}
private void setNumberOfClasses(int number) {
classTable.stopEditing();
classes = number;
classNames = ArrayUtils.resize(classNames, classes, null);
makeNames();
classTypes = ArrayUtils.resize(classTypes, classes, CLASS_CLOSED);
classData = ArrayUtils.resize(classData, classes, 0.0);
classTable.updateStructure();
if (!deleting) {
classOps.add(ListOp.createResizeOp(classes));
}
classSpinner.setValue(new Integer(classes));
classTable.updateDeleteCommand();
updateAlgoPanel();
}
/**
* Set up the panel contents and layout
*/
private void initComponents() {
classSpinner.addChangeListener(spinnerListener);
help.addHelp(classSpinner, "Enter the number of classes for this system");
classTable = new ClassTable();
/* and now some Box black magic */
//DEK (Federico Granata) 26-09-2003
Box classSpinnerBox = Box.createHorizontalBox();
//OLD
//JLabel spinnerLabel = new JLabel("<html><font size=\"4\">Set the Number of classes (1-" + MAX_CLASSES + "):</font></html>");
//NEW
//@author Stefano
JLabel spinnerLabel = new JLabel(DESCRIPTION_CLASSES);
classSpinnerBox.add(spinnerLabel);
//END
//BEGIN Federico Dall'Orso 9/3/2005
//OLD
/*
classSpinnerBox.add(Box.createGlue());
*/
//NEW
classSpinnerBox.add(Box.createHorizontalStrut(10));
Box numberBox = Box.createVerticalBox();
JPanel spinnerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JLabel numberLabel = new JLabel("Number:");
classSpinner.setMaximumSize(new Dimension(600, 18));
spinnerPanel.add(numberLabel);
spinnerPanel.add(classSpinner);
numberBox.add(spinnerPanel);
numberBox.add(new JButton(addClass));
numberBox.setMaximumSize(new Dimension(150, 50));
classSpinnerBox.add(numberBox);
//END Federico Dall'Orso 9/3/2005
Box classBox = Box.createVerticalBox();
classBox.add(Box.createVerticalStrut(20));
classBox.add(classSpinnerBox);
classBox.add(Box.createVerticalStrut(10));
JScrollPane classTablePane = new JScrollPane(classTable);
classTablePane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
classTablePane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
classBox.add(classTablePane);
classBox.add(Box.createVerticalStrut(20));
Box totalBox = Box.createHorizontalBox();
totalBox.add(Box.createHorizontalStrut(20));
totalBox.add(classBox);
totalBox.add(Box.createHorizontalStrut(20));
setLayout(new BorderLayout());
add(totalBox, BorderLayout.CENTER);
}
/**
* @return the total number of customers in the system
*/
private int sumPop() {
int pop = 0;
for (int i = 0; i < classes; i++) {
if (classTypes[i] == CLASS_CLOSED) {
pop += classData[i];
}
}
return pop;
}
/**
* Checks if a class was not initialized
* @return false if a class was not inizialized, true otherwise
*/
private boolean checkData() {
for (int i = 0; i < classes; i++) {
if (classData[i] == 0.0) {
return false;
}
}
return true;
}
@Override
public String getName() {
return "Classes";
}
@Override
public void lostFocus() {
commit();
//release();
}
@Override
public void gotFocus() {
sync();
classTable.update();
}
@Override
public boolean canFinish() {
return checkPop() && !areThereDuplicates();
}
@Override
public boolean canGoBack() {
checkPop();
if (areThereDuplicates()) {
return false;
}
return true; //so that the user can correct errors
}
@Override
public boolean canGoForward() {
checkPop();
if (areThereDuplicates()) {
return false;
}
if (!checkData()) {
JOptionPane
.showMessageDialog(
this,
"<html><center>Please provide correct, not null, values for<br>'No. of Customers' and 'Arrival Rate' for each class.</center></html>",
"Warning", JOptionPane.WARNING_MESSAGE);
return false;
}
return true; // so that the user can correct errors
}
//checks wether population is greater than 0
private boolean checkPop() {
classTable.stopEditing();
if (isLd && sumPop() < 1) {
JOptionPane
.showMessageDialog(
this,
"<html><center>A system with load dependent stations cannot have zero customers.<br>Increase the number of customers or remove all load dependent stations.</center></html>",
"Warning", JOptionPane.WARNING_MESSAGE);
return false;
}
return true;
}
//checks for presence of classes with same name
private boolean areThereDuplicates() {
boolean thereAreDupl = false;
for (int i = 0; i < classNames.length; i++) {
for (int j = i + 1; j < classNames.length; j++) {
thereAreDupl = thereAreDupl || classNames[i].equalsIgnoreCase(classNames[j]);
}
}
if (thereAreDupl) {
JOptionPane.showMessageDialog(this,
"<html><center>Two or more classes in this system are identified by the same name.<br>Please modify names.</center></html>",
"Warning", JOptionPane.WARNING_MESSAGE);
return true;
} else {
return false;
}
}
private void commit() {
/* stop any editing in progress */
if (classSpinner.getEditor().getComponent(0).hasFocus()) {
//disgusting. there must be a better way...
try {
classSpinner.commitEdit();
updateSizes();
} catch (java.text.ParseException e) {
}
}
classTable.stopEditing();
ExactModel data = ew.getData();
synchronized (data) {
boolean whatIfChanged = false;
if (hasDeletes) {
//if at some point rows have been deleted
//play back all ops in the same order on the data object
playbackClassOps(data);
} else {
whatIfChanged |= data.resize(data.getStations(), classes); //otherwise a simple resize is ok
}
data.setClassNames(classNames);
whatIfChanged |= data.setClassTypes(classTypes);
whatIfChanged |= data.setClassData(classData);
if (whatIfChanged) {
data.recalculateWhatifValues();
}
sync();
}
}
private void deleteSelectedClasses() {
int[] selectedRows = classTable.getSelectedRows();
//System.out.println("deleteSelectedRows(): "+ArrayUtils.toString(selectedRows));
int nrows = selectedRows.length;
int left = classTable.getRowCount() - nrows;
if (left < 1) {
classTable.removeRowSelectionInterval(selectedRows[nrows - 1], selectedRows[nrows - 1]);
deleteSelectedClasses();
return;
}
deleteClasses(selectedRows);
}
private void deleteClasses(int[] idx) {
deleting = true;
Arrays.sort(idx);
for (int i = idx.length - 1; i >= 0; i--) {
deleteClass(idx[i]);
}
updateSizes();
deleting = false;
updateAlgoPanel();
}
private void deleteClass(int i) {
classes--;
classSpinner.setValue(new Integer(classes));
classNames = ArrayUtils.delete(classNames, i);
classTypes = ArrayUtils.delete(classTypes, i);
classData = ArrayUtils.delete(classData, i);
classOps.add(ListOp.createDeleteOp(i));
hasDeletes = true;
}
private void playbackClassOps(ExactModel data) {
for (int i = 0; i < classOps.size(); i++) {
ListOp lo = classOps.get(i);
if (lo.isDeleteOp()) {
data.deleteClass(lo.getData());
}
if (lo.isResizeOp()) {
data.resize(data.getStations(), lo.getData());
}
}
}
@Override
public void help() {
JOptionPane.showMessageDialog(this, helpText, "Help", JOptionPane.INFORMATION_MESSAGE);
}
//NEW Federico Dall'Orso
//Methods added to implement forcing of data refresh
public void retrieveData() {
sync();
}
public void commitData() {
commit();
}
/**
* Updates the algorithm panel based on the current classes.
*/
private void updateAlgoPanel() {
boolean isClosed = true;
boolean isOpen = true;
for (int type : classTypes) {
if (type == ExactModel.CLASS_CLOSED) {
isOpen = false;
} else if (type == ExactModel.CLASS_OPEN) {
isClosed = false;
}
}
// Updates the algorithm panel
ew.updateAlgoPanel(isClosed, isOpen, null, null);
}
//END
/* ------------------------------------------------------------------
The ClassTable is a fairly complex object that would be probably better of as an outer class.
However, it is very specialized and it needs access to the data structures of the ClassesPanel,
so having it as an inner class is *much* more practical
------------------------------------------------------------------
*/
/**
* Her Majesty, the class table herself
*/
private class ClassTable extends ExactTable {
/**
*
*/
private static final long serialVersionUID = 1L;
TableCellRenderer disabledCellRenderer;
TableCellEditor classTypeCellEditor;
//BEGIN Federico Dall'Orso 8/3/2005
ComboBoxCell classTypeComboBoxCell;
ButtonCellEditor deleteButtonCellRenderer;
JButton deleteButton;
//END Federico Dall'Orso 8/3/2005
ClassTable() {
super(new ClassTableModel());
disabledCellRenderer = new DisabledCellRenderer();
//BEGIN Federico Dall'Orso 8/3/2005
//NEW
classTypeComboBoxCell = new ComboBoxCell(CLASS_TYPENAMES);
deleteButton = new JButton(deleteOneClass);
deleteButtonCellRenderer = new ButtonCellEditor(deleteButton);
enableDeletes();
rowHeader.setRowHeight(18);
setRowHeight(18);
//END Federico Dall'Orso 8/3/2005
JComboBox classTypeBox = new JComboBox(CLASS_TYPENAMES);
classTypeCellEditor = new DefaultCellEditor(classTypeBox);
classTypeBox.setEditable(false);
setColumnSelectionAllowed(false);
setRowSelectionAllowed(true);
// not beautiful, but effective. See ClassTableModel.getColumnClass()
setDefaultRenderer(DisabledCellRenderer.class, disabledCellRenderer);
setDefaultEditor(String.class, classTypeCellEditor);
setDisplaysScrollLabels(true);
installKeyboardAction(getInputMap(), getActionMap(), deleteClass);
mouseHandler = new ExactTable.MouseHandler(makeMouseMenu());
mouseHandler.install();
help.addHelp(this,
"Click or drag to select classes; to edit data single-click and start typing. Right-click for a list of available operations");
help.addHelp(moreRowsLabel, "There are more classes: scroll down to see them");
help.addHelp(selectAllButton, "Click to select all classes");
tableHeader.setToolTipText(null);
rowHeader.setToolTipText(null);
help.addHelp(rowHeader, "Click, SHIFT-click or drag to select classes");
}
//BEGIN Federico Dall'Orso 14/3/2005
/*enables deleting operations with last column's button*/
private void enableDeletes() {
deleteOneClass.setEnabled(classes > 1);
/*It seems the only way to implement row deletion...*/
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if ((columnAtPoint(e.getPoint()) == getColumnCount() - 1) && getRowCount() > 1) {
setRowSelectionInterval(rowAtPoint(e.getPoint()), rowAtPoint(e.getPoint()));
deleteSelectedClasses();
}
}
});
getColumnModel().getColumn(getColumnCount() - 1).setMinWidth(20);
getColumnModel().getColumn(getColumnCount() - 1).setMaxWidth(20);
}
//END Federico Dall'Orso 14/3/2005
//new Federico Dall'Orso 8/3/2005
@Override
public TableCellRenderer getCellRenderer(int row, int column) {
//if this is type column, i must render it as a combo box instead of a jtextfield
if (column == 1) {
return classTypeComboBoxCell;
} else if (column == 4) {
return deleteButtonCellRenderer;
} else {
return disabledCellRenderer;
}
}
//end Federico Dall'Orso 8/3/2005
@Override
protected void installKeyboard() {
}
@Override
protected void installMouse() {
}
@Override
protected JPopupMenu makeMouseMenu() {
JPopupMenu menu = new JPopupMenu();
menu.add(deleteClass);
return menu;
}
/**
* Make sure the table reflects changes on editing end
* Overridden to truncate decimals in data if current class is closed
*/
@Override
public void editingStopped(ChangeEvent ce) {
if (classTypes[editingRow] == CLASS_CLOSED) {
classData[editingRow] = (int) classData[editingRow];
}
updateRow(editingRow);
super.editingStopped(ce);
}
//BEGIN Federico Dall'Orso 14/3/2005
//NEW
//Updates appearence of last column's buttons
void updateDeleteCommand() {
deleteOneClass.setEnabled(classes > 1);
getColumnModel().getColumn(getColumnCount() - 1).setMinWidth(20);
getColumnModel().getColumn(getColumnCount() - 1).setMaxWidth(20);
}
//END Federico Dall'Orso 14/3/2005
@Override
protected void updateActions() {
boolean isEnabled = classes > 1 && getSelectedRowCount() > 0;
deleteClass.setEnabled(isEnabled);
deleteOneClass.setEnabled(classes > 1);
}
}
/**
* the model backing the class table
*/
private class ClassTableModel extends ExactTableModel {
/**
*
*/
private static final long serialVersionUID = 1L;
private Object[] prototypes = { "10000", new String(new char[12]), "closed+cbox", new Integer(1000), new String(new char[12]), "" };
@Override
public Object getPrototype(int columnIndex) {
return prototypes[columnIndex + 1];
}
public int getRowCount() {
return classes;
}
@Override
public Class getColumnClass(int col) {
switch (col) {
case 1:
return String.class;
case 2:
case 3:
return DisabledCellRenderer.class;
default:
return Object.class;
}
}
public int getColumnCount() {
return 5;
}
@Override
public String getColumnName(int index) {
switch (index) {
case 0:
return "Name";
case 1:
return "Type";
case 2:
return "No. of Customers";
case 3:
return "Arrival Rate (\u03BB)";
default:
return null;
}
}
@Override
protected Object getValueAtImpl(int rowIndex, int columnIndex) {
switch (columnIndex) {
case 0://name
return classNames[rowIndex];
case 1://type
return CLASS_TYPENAMES[classTypes[rowIndex]];
case 2://customers
if (classTypes[rowIndex] == CLASS_CLOSED) {
return new Integer((int) classData[rowIndex]);
} else {
return null;
}
case 3://arrival rate
if (classTypes[rowIndex] == CLASS_OPEN) {
return new Double(classData[rowIndex]);
} else {
return null;
}
default:
return null;
}
}
@Override
protected Object getRowName(int rowIndex) {
return new Integer(rowIndex + 1);
}
@Override
public void setValueAt(Object value, int rowIndex, int columnIndex) {
switch (columnIndex) {
case 0: //name
classNames[rowIndex] = (String) value;
break;
case 1: //type
for (int i = 0; i < CLASS_TYPENAMES.length; i++) {
if (value == CLASS_TYPENAMES[i]) { //literal strings are canonicalized, hence == is ok
classTypes[rowIndex] = i;
break;
}
}
// Updates the algorithm panel
updateAlgoPanel();
break;
case 2: {//customers
try {
int newval = (int) Double.parseDouble((String) value);
if (newval >= 0) {
classData[rowIndex] = newval;
}
} catch (NumberFormatException e) {
}
break;
}
case 3: { //arrival rate
try {
double newval = Double.parseDouble((String) value);
if (newval >= 0.0) {
classData[rowIndex] = newval;
}
} catch (NumberFormatException e) {
}
break;
}
default:
}
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
switch (columnIndex) {
case 0:
case 1:
return true;
case 2:
return (classTypes[rowIndex] == CLASS_CLOSED);
case 3:
return (classTypes[rowIndex] == CLASS_OPEN);
default:
return false;
}
}
}
}