import jalview.analysis.TreeBuilder;
import jalview.analysis.scoremodels.ScoreModels;
import jalview.analysis.scoremodels.SimilarityParams;
+import jalview.api.AlignViewportI;
import jalview.api.analysis.ScoreModelI;
import jalview.api.analysis.SimilarityParamsI;
+import jalview.bin.Cache;
+import jalview.datamodel.SequenceGroup;
import jalview.util.MessageManager;
-
+import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
+import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridLayout;
+import java.awt.Insets;
import java.awt.event.ActionEvent;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyVetoException;
import java.util.ArrayList;
import java.util.List;
+import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
+import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.event.InternalFrameEvent;
/**
- * A dialog where a user can choose and action Tree or PCA calculation options
+ * A dialog where a user can choose and action Tree or PCA calculation options.
+ *
+ * Allows also for dialog-free static methods openPCAPanel(...) and
+ * openTreePanel(...) for scripted use.
+ *
*/
+@SuppressWarnings("serial")
public class CalculationChooser extends JPanel
{
/*
*/
private static boolean treeMatchGaps = true;
- private static final Font VERDANA_11PT = new Font("Verdana", 0, 11);
+ private static Font VERDANA_11PT;
+
+ private static final int MIN_TREE_SELECTION = 3;
+
+ private static final int MIN_PCA_SELECTION = 4;
AlignFrame af;
JRadioButton pca;
- JRadioButton tree;
-
JRadioButton neighbourJoining;
JRadioButton averageDistance;
JComboBox<String> modelNames;
- private JInternalFrame frame;
+ JButton calculate;
- private ButtonGroup treeTypes;
+ private JInternalFrame frame;
private JCheckBox includeGaps;
private JCheckBox shorterSequence;
+ private static ComboBoxTooltipRenderer renderer; // BH was not static
+
+ List<String> tips = new ArrayList<>();
+
+ /*
+ * the most recently opened PCA results panel
+ */
+ private PCAPanel pcaPanel;
+
+ /**
+ * Open a new Tree panel on the desktop statically. Params are standard (not
+ * set by Groovy). No dialog is opened.
+ *
+ * @param af
+ * @param treeType
+ * @param modelName
+ * @return null if successful; the string
+ * "label.you_need_at_least_n_sequences" if number of sequences
+ * selected is inappropriate
+ */
+ public static Object openTreePanel(AlignFrame af, String treeType,
+ String modelName)
+ {
+ return openTreePanel(af, treeType, modelName, null);
+ }
+
+ /**
+ * public static method for JalviewJS API to open a PCAPanel without
+ * necessarily using a dialog.
+ *
+ * @param af
+ * @param modelName
+ * @return the PCAPanel, or the string "label.you_need_at_least_n_sequences"
+ * if number of sequences selected is inappropriate
+ */
+ public static Object openPcaPanel(AlignFrame af, String modelName)
+ {
+ return openPcaPanel(af, modelName, null);
+ }
+
/**
* Constructor
*
*/
void init()
{
+ setLayout(new BorderLayout());
frame = new JInternalFrame();
frame.setContentPane(this);
this.setBackground(Color.white);
+ frame.addFocusListener(new FocusListener()
+ {
+
+ @Override
+ public void focusLost(FocusEvent e)
+ {
+ }
+ @Override
+ public void focusGained(FocusEvent e)
+ {
+ validateCalcTypes();
+ }
+ });
/*
- * Layout consists of 4 or 5 panels:
- * - first with choice of Tree or PCA
- * - second with choice of tree method NJ or AV
- * - third with choice of score model
- * - fourth with score model parameter options [suppressed]
- * - fifth with OK and Cancel
+ * Layout consists of 3 or 4 panels:
+ * - first with choice of PCA or tree method NJ or AV
+ * - second with choice of score model
+ * - third with score model parameter options [suppressed]
+ * - fourth with OK and Cancel
*/
- tree = new JRadioButton(MessageManager.getString("label.tree"));
- tree.setOpaque(false);
pca = new JRadioButton(
MessageManager.getString("label.principal_component_analysis"));
pca.setOpaque(false);
+
neighbourJoining = new JRadioButton(
MessageManager.getString("label.tree_calc_nj"));
+ neighbourJoining.setSelected(true);
+ neighbourJoining.setOpaque(false);
+
averageDistance = new JRadioButton(
MessageManager.getString("label.tree_calc_av"));
- ItemListener listener = new ItemListener()
+ averageDistance.setOpaque(false);
+
+ JPanel calcChoicePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ calcChoicePanel.setOpaque(false);
+
+ // first create the Tree calculation's border panel
+ JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ treePanel.setOpaque(false);
+
+ JvSwingUtils.createTitledBorder(treePanel,
+ MessageManager.getString("label.tree"), true);
+
+ // then copy the inset dimensions for the border-less PCA panel
+ JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ Insets b = treePanel.getBorder().getBorderInsets(treePanel);
+ pcaBorderless.setBorder(
+ BorderFactory.createEmptyBorder(2, b.left, 2, b.right));
+ pcaBorderless.setOpaque(false);
+
+ pcaBorderless.add(pca, FlowLayout.LEFT);
+ calcChoicePanel.add(pcaBorderless, FlowLayout.LEFT);
+
+ treePanel.add(neighbourJoining);
+ treePanel.add(averageDistance);
+
+ calcChoicePanel.add(treePanel);
+
+ ButtonGroup calcTypes = new ButtonGroup();
+ calcTypes.add(pca);
+ calcTypes.add(neighbourJoining);
+ calcTypes.add(averageDistance);
+
+ ActionListener calcChanged = new ActionListener()
{
@Override
- public void itemStateChanged(ItemEvent e)
+ public void actionPerformed(ActionEvent e)
{
- neighbourJoining.setEnabled(tree.isSelected());
- averageDistance.setEnabled(tree.isSelected());
+ validateCalcTypes();
}
};
- pca.addItemListener(listener);
- tree.addItemListener(listener);
- ButtonGroup calcTypes = new ButtonGroup();
- calcTypes.add(pca);
- calcTypes.add(tree);
- JPanel calcChoicePanel = new JPanel();
- calcChoicePanel.setOpaque(false);
- tree.setSelected(true);
- calcChoicePanel.add(tree);
- calcChoicePanel.add(pca);
-
- neighbourJoining.setOpaque(false);
- treeTypes = new ButtonGroup();
- treeTypes.add(neighbourJoining);
- treeTypes.add(averageDistance);
- neighbourJoining.setSelected(true);
- JPanel treeChoicePanel = new JPanel();
- treeChoicePanel.setOpaque(false);
- treeChoicePanel.add(neighbourJoining);
- treeChoicePanel.add(averageDistance);
+ pca.addActionListener(calcChanged);
+ neighbourJoining.addActionListener(calcChanged);
+ averageDistance.addActionListener(calcChanged);
/*
* score models drop-down - with added tooltips!
*/
modelNames = buildModelOptionsList();
- JPanel scoreModelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ JPanel scoreModelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
scoreModelPanel.setOpaque(false);
- scoreModelPanel.add(modelNames, FlowLayout.LEFT);
+ scoreModelPanel.add(modelNames);
/*
* score model parameters
paramsPanel.add(includeGappedColumns);
paramsPanel.add(shorterSequence);
+ if (VERDANA_11PT == null)
+ {
+ VERDANA_11PT = new Font("Verdana", 0, 11);
+ }
/*
* OK / Cancel buttons
*/
- JButton ok = new JButton(MessageManager.getString("action.ok"));
- ok.setFont(VERDANA_11PT);
- ok.addActionListener(new java.awt.event.ActionListener()
+ calculate = new JButton(MessageManager.getString("action.calculate"));
+ calculate.setFont(VERDANA_11PT);
+ calculate.addActionListener(new java.awt.event.ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
- ok_actionPerformed();
+ calculate_actionPerformed();
}
});
- JButton cancel = new JButton(MessageManager.getString("action.cancel"));
- cancel.setFont(VERDANA_11PT);
- cancel.addActionListener(new java.awt.event.ActionListener()
+ JButton close = new JButton(MessageManager.getString("action.close"));
+ close.setFont(VERDANA_11PT);
+ close.addActionListener(new java.awt.event.ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
- cancel_actionPerformed(e);
+ close_actionPerformed();
}
});
JPanel actionPanel = new JPanel();
actionPanel.setOpaque(false);
- actionPanel.add(ok);
- actionPanel.add(cancel);
+ actionPanel.add(calculate);
+ actionPanel.add(close);
boolean includeParams = false;
- this.add(calcChoicePanel);
- this.add(treeChoicePanel);
- this.add(scoreModelPanel);
+ this.add(calcChoicePanel, BorderLayout.CENTER);
+ calcChoicePanel.add(scoreModelPanel);
if (includeParams)
{
- this.add(paramsPanel);
+ scoreModelPanel.add(paramsPanel);
}
- this.add(actionPanel);
+ this.add(actionPanel, BorderLayout.SOUTH);
int width = 350;
- int height = includeParams ? 400 : 220;
+ int height = includeParams ? 420 : 240;
+
+ setMinimumSize(new Dimension(325, height - 10));
String title = MessageManager.getString("label.choose_calculation");
- if (af.getViewport().viewName != null)
+ if (af.getViewport().getViewName() != null)
{
- title = title + " (" + af.getViewport().viewName + ")";
+ title = title + " (" + af.getViewport().getViewName() + ")";
}
- Desktop.addInternalFrame(frame,
- title, width,
- height, false);
-
+ Desktop.addInternalFrame(frame, title, Desktop.FRAME_MAKE_VISIBLE, width, height, Desktop.FRAME_NOT_RESIZABLE, Desktop.FRAME_SET_MIN_SIZE_300);
+ calcChoicePanel.doLayout();
+ revalidate();
/*
* null the AlignmentPanel's reference to the dialog when it is closed
*/
};
});
+ validateCalcTypes();
frame.setLayer(JLayeredPane.PALETTE_LAYER);
}
/**
+ * enable calculations applicable for the current alignment or selection.
+ */
+ protected void validateCalcTypes()
+ {
+ int size = af.getViewport().getAlignment().getHeight();
+ if (af.getViewport().getSelectionGroup() != null)
+ {
+ size = af.getViewport().getSelectionGroup().getSize();
+ }
+
+ /*
+ * disable calc options for which there is insufficient input data
+ * return value of true means enabled and selected
+ */
+ boolean checkPca = checkEnabled(pca, size, MIN_PCA_SELECTION);
+ boolean checkNeighbourJoining = checkEnabled(neighbourJoining, size,
+ MIN_TREE_SELECTION);
+ boolean checkAverageDistance = checkEnabled(averageDistance, size,
+ MIN_TREE_SELECTION);
+
+ if (checkPca || checkNeighbourJoining || checkAverageDistance)
+ {
+ calculate.setToolTipText(null);
+ calculate.setEnabled(true);
+ }
+ else
+ {
+ calculate.setEnabled(false);
+ }
+ updateScoreModels(modelNames, tips);
+ }
+
+ /**
+ * Check the input and disable a calculation's radio button if necessary. A
+ * tooltip is shown for disabled calculations.
+ *
+ * @param calc
+ * - radio button for the calculation being validated
+ * @param size
+ * - size of input to calculation
+ * @param minsize
+ * - minimum size for calculation
+ * @return true if size >= minsize and calc.isSelected
+ */
+ private boolean checkEnabled(JRadioButton calc, int size, int minsize)
+ {
+ String ttip = MessageManager
+ .formatMessage("label.you_need_at_least_n_sequences", minsize);
+
+ calc.setEnabled(size >= minsize);
+ if (!calc.isEnabled())
+ {
+ calc.setToolTipText(ttip);
+ }
+ else
+ {
+ calc.setToolTipText(null);
+ }
+ if (calc.isSelected())
+ {
+ modelNames.setEnabled(calc.isEnabled());
+ if (calc.isEnabled())
+ {
+ return true;
+ }
+ else
+ {
+ calculate.setToolTipText(ttip);
+ }
+ }
+ return false;
+ }
+
+ /**
* A rather elaborate helper method (blame Swing, not me) that builds a
* drop-down list of score models (by name) with descriptions as tooltips.
* There is also a tooltip shown for the currently selected item when hovering
*/
protected JComboBox<String> buildModelOptionsList()
{
- final JComboBox<String> comboBox = new JComboBox<String>();
- ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
- comboBox.setRenderer(renderer);
- final List<String> tips = new ArrayList<String>();
+ JComboBox<String> scoreModelsCombo = new JComboBox<>();
+ if (renderer == null)
+ {
+ renderer = new ComboBoxTooltipRenderer();
+ }
+ scoreModelsCombo.setRenderer(renderer);
/*
* show tooltip on mouse over the combobox
* note the listener has to be on the components that make up
* the combobox, doesn't work if just on the combobox
*/
- MouseAdapter mouseListener = new MouseAdapter()
+ final MouseAdapter mouseListener = new MouseAdapter()
{
@Override
public void mouseEntered(MouseEvent e)
{
- comboBox.setToolTipText(tips.get(comboBox.getSelectedIndex()));
+ scoreModelsCombo.setToolTipText(
+ tips.get(scoreModelsCombo.getSelectedIndex()));
}
@Override
public void mouseExited(MouseEvent e)
{
- comboBox.setToolTipText(null);
+ scoreModelsCombo.setToolTipText(null);
}
};
- for (Component c : comboBox.getComponents())
+ for (Component c : scoreModelsCombo.getComponents())
{
c.addMouseListener(mouseListener);
}
+ updateScoreModels(scoreModelsCombo, tips);
+
+ /*
+ * set the list of tooltips on the combobox's renderer
+ */
+ renderer.setTooltips(tips);
+
+ return scoreModelsCombo;
+ }
+
+ private void updateScoreModels(JComboBox<String> comboBox,
+ List<String> toolTips)
+ {
+ Object curSel = comboBox.getSelectedItem();
+ toolTips.clear();
+ DefaultComboBoxModel<String> model = new DefaultComboBoxModel<>();
+
+ /*
+ * select the score models applicable to the alignment type
+ */
+ boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
+ List<ScoreModelI> models = getApplicableScoreModels(nucleotide,
+ pca.isSelected());
+
/*
* now we can actually add entries to the combobox,
* remembering their descriptions for tooltips
*/
- ScoreModels scoreModels = ScoreModels.getInstance();
- for (ScoreModelI sm : scoreModels.getModels())
+ boolean selectedIsPresent = false;
+ for (ScoreModelI sm : models)
{
- boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
- if (sm.isDNA() && nucleotide || sm.isProtein() && !nucleotide)
+ if (curSel != null && sm.getName().equals(curSel))
{
- comboBox.addItem(sm.getName());
-
- /*
- * tooltip is description if provided, else text lookup with
- * fallback on the model name
- */
- String tooltip = sm.getDescription();
- if (tooltip == null)
- {
- tooltip = MessageManager.getStringOrReturn("label.score_model_",
- sm.getName());
- }
- tips.add(tooltip);
+ selectedIsPresent = true;
+ curSel = sm.getName();
}
+ model.addElement(sm.getName());
/*
- * set the list of tooltips on the combobox's renderer
+ * tooltip is description if provided, else text lookup with
+ * fallback on the model name
*/
- renderer.setTooltips(tips);
+ String tooltip = sm.getDescription();
+ if (tooltip == null)
+ {
+ tooltip = MessageManager.getStringOrReturn("label.score_model_",
+ sm.getName());
+ }
+ toolTips.add(tooltip);
+ }
+
+ if (selectedIsPresent)
+ {
+ model.setSelectedItem(curSel);
}
+ // finally, update the model
+ comboBox.setModel(model);
+ }
+
+ /**
+ * Builds a list of score models which are applicable for the alignment and
+ * calculation type (peptide or generic models for protein, nucleotide or
+ * generic models for nucleotide).
+ * <p>
+ * As a special case, includes BLOSUM62 as an extra option for nucleotide PCA.
+ * This is for backwards compatibility with Jalview prior to 2.8 when BLOSUM62
+ * was the only score matrix supported. This is included if property
+ * BLOSUM62_PCA_FOR_NUCLEOTIDE is set to true in the Jalview properties file.
+ *
+ * @param nucleotide
+ * @param forPca
+ * @return
+ */
+ protected static List<ScoreModelI> getApplicableScoreModels(
+ boolean nucleotide, boolean forPca)
+ {
+ List<ScoreModelI> filtered = new ArrayList<>();
- return comboBox;
+ ScoreModels scoreModels = ScoreModels.getInstance();
+ for (ScoreModelI sm : scoreModels.getModels())
+ {
+ if (!nucleotide && sm.isProtein() || nucleotide && sm.isDNA())
+ {
+ filtered.add(sm);
+ }
+ }
+
+ /*
+ * special case: add BLOSUM62 as last option for nucleotide PCA,
+ * for backwards compatibility with Jalview < 2.8 (JAL-2962)
+ */
+ if (nucleotide && forPca
+ && Cache.getDefault(Preferences.BLOSUM62_PCA_FOR_NUCLEOTIDE,
+ false))
+ {
+ filtered.add(scoreModels.getBlosum62());
+ }
+
+ return filtered;
}
/**
- * Open and calculate the selected tree on 'OK'
+ * Open and calculate the selected tree or PCA on 'OK'
*/
- protected void ok_actionPerformed()
+ protected void calculate_actionPerformed()
{
boolean doPCA = pca.isSelected();
- ScoreModelI sm = ScoreModels.getInstance().forName(
- modelNames.getSelectedItem().toString());
+ String modelName = modelNames.getSelectedItem().toString();
SimilarityParamsI params = getSimilarityParameters(doPCA);
if (doPCA)
{
- openPcaPanel(sm, params);
+ openPcaPanel(modelName, params);
}
else
{
- openTreePanel(sm, params);
+ openTreePanel(modelName, params);
}
// closeFrame();
/**
* Open a new Tree panel on the desktop
*
- * @param sm
+ * @param modelName
* @param params
*/
- protected void openTreePanel(ScoreModelI sm, SimilarityParamsI params)
+ protected void openTreePanel(String modelName, SimilarityParamsI params)
{
- String treeType = neighbourJoining.isSelected() ? TreeBuilder.NEIGHBOUR_JOINING
- : TreeBuilder.AVERAGE_DISTANCE;
- af.newTreePanel(treeType, sm, params);
+ Object ret = openTreePanel(af,
+ neighbourJoining.isSelected() ? TreeBuilder.NEIGHBOUR_JOINING
+ : TreeBuilder.AVERAGE_DISTANCE,
+ modelName, params);
+ if (ret instanceof String)
+ {
+ JvOptionPane.showMessageDialog(this, // was opening on Desktop?
+ MessageManager.formatMessage(
+ (String) ret,
+ MIN_TREE_SELECTION),
+ MessageManager.getString("label.not_enough_sequences"),
+ JvOptionPane.WARNING_MESSAGE);
+
+ }
}
/**
* Open a new PCA panel on the desktop
*
- * @param sm
+ * @param modelName
+ * @param params
+ */
+ protected void openPcaPanel(String modelName, SimilarityParamsI params)
+ {
+ Object ret = openPcaPanel(af, modelName, params);
+ if (ret instanceof String)
+ {
+ JvOptionPane.showInternalMessageDialog(this,
+ MessageManager.formatMessage(
+ (String) ret,
+ MIN_PCA_SELECTION),
+ MessageManager
+ .getString("label.sequence_selection_insufficient"),
+ JvOptionPane.WARNING_MESSAGE);
+ }
+ else
+ {
+ // only used for test suite
+ pcaPanel = (PCAPanel) ret;
+ }
+
+ }
+
+ /**
+ * Open a new Tree panel on the desktop statically
+ *
+ * @param af
+ * @param treeType
+ * @param modelName
+ * @param params
+ * @return null, or the string "label.you_need_at_least_n_sequences" if number
+ * of sequences selected is inappropriate
+ */
+ public static Object openTreePanel(AlignFrame af, String treeType,
+ String modelName, SimilarityParamsI params)
+ {
+
+ /*
+ * gui validation shouldn't allow insufficient sequences here, but leave
+ * this check in in case this method gets exposed programmatically in future
+ */
+ AlignViewportI viewport = af.getViewport();
+ SequenceGroup sg = viewport.getSelectionGroup();
+ if (sg != null && sg.getSize() < MIN_TREE_SELECTION)
+ {
+ return "label.you_need_at_least_n_sequences";
+ }
+
+ if (params == null)
+ {
+ params = getSimilarityParameters(false);
+ }
+
+ af.newTreePanel(treeType, modelName, params);
+ return null;
+ }
+
+ /**
+ * public static method for JalviewJS API
+ *
+ * @param af
+ * @param modelName
* @param params
+ * @return the PCAPanel, or null if number of sequences selected is
+ * inappropriate
*/
- protected void openPcaPanel(ScoreModelI sm, SimilarityParamsI params)
+ public static Object openPcaPanel(AlignFrame af, String modelName,
+ SimilarityParamsI params)
{
- AlignViewport viewport = af.getViewport();
+ AlignViewportI viewport = af.getViewport();
+
+ /*
+ * gui validation shouldn't allow insufficient sequences here, but leave
+ * this check in in case this method gets exposed programmatically in future
+ *
+ *
+ */
if (((viewport.getSelectionGroup() != null)
- && (viewport.getSelectionGroup().getSize() < 4) && (viewport
- .getSelectionGroup().getSize() > 0))
- || (viewport.getAlignment().getHeight() < 4))
- {
- JvOptionPane
- .showInternalMessageDialog(
- this,
- MessageManager
- .getString("label.principal_component_analysis_must_take_least_four_input_sequences"),
- MessageManager
- .getString("label.sequence_selection_insufficient"),
- JvOptionPane.WARNING_MESSAGE);
- return;
+ && (viewport.getSelectionGroup().getSize() < MIN_PCA_SELECTION)
+ && (viewport.getSelectionGroup().getSize() > 0))
+ || (viewport.getAlignment().getHeight() < MIN_PCA_SELECTION))
+ {
+ return "label.you_need_at_least_n_sequences";
}
- new PCAPanel(af.alignPanel, sm, params);
+
+ if (params == null)
+ {
+ params = getSimilarityParameters(true);
+ }
+
+ /*
+ * construct the panel and kick off its calculation thread
+ */
+ PCAPanel pcap = new PCAPanel(af.alignPanel, modelName, params);
+ new Thread(pcap).start();
+ return pcap;
}
/**
}
}
+
/**
* Returns a data bean holding parameters for similarity (or distance) model
* calculation
* @param doPCA
* @return
*/
- protected SimilarityParamsI getSimilarityParameters(boolean doPCA)
+ public static SimilarityParamsI getSimilarityParameters(
+ boolean doPCA)
{
// commented out: parameter choices read from gui widgets
// SimilarityParamsI params = new SimilarityParams(
*/
boolean matchGap = doPCA ? false : treeMatchGaps;
- return new SimilarityParams(includeGapGap, matchGap, includeGapResidue, matchOnShortestLength);
+ return new SimilarityParams(includeGapGap, matchGap, includeGapResidue,
+ matchOnShortestLength);
+
}
/**
- * Closes dialog on cancel
- *
- * @param e
+ * Closes dialog on Close button press
*/
- protected void cancel_actionPerformed(ActionEvent e)
+ protected void close_actionPerformed()
{
try
{
{
}
}
+
+ public PCAPanel getPcaPanel()
+ {
+ return pcaPanel;
+ }
}