fe6336efeb418f6abcd4f8353a9199d1a3e30766
[jalview.git] / src / jalview / gui / CalculationChooser.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.analysis.TreeBuilder;
24 import jalview.analysis.scoremodels.ScoreModels;
25 import jalview.analysis.scoremodels.SimilarityParams;
26 import jalview.api.analysis.ScoreModelI;
27 import jalview.api.analysis.SimilarityParamsI;
28 import jalview.util.MessageManager;
29
30 import java.awt.BorderLayout;
31 import java.awt.Color;
32 import java.awt.Component;
33 import java.awt.Dimension;
34 import java.awt.FlowLayout;
35 import java.awt.Font;
36 import java.awt.GridLayout;
37 import java.awt.Insets;
38 import java.awt.event.ActionEvent;
39 import java.awt.event.ActionListener;
40 import java.awt.event.FocusEvent;
41 import java.awt.event.FocusListener;
42 import java.awt.event.MouseAdapter;
43 import java.awt.event.MouseEvent;
44 import java.beans.PropertyVetoException;
45 import java.util.ArrayList;
46 import java.util.List;
47
48 import javax.swing.BorderFactory;
49 import javax.swing.ButtonGroup;
50 import javax.swing.JButton;
51 import javax.swing.JCheckBox;
52 import javax.swing.JComboBox;
53 import javax.swing.JInternalFrame;
54 import javax.swing.JLabel;
55 import javax.swing.JLayeredPane;
56 import javax.swing.JPanel;
57 import javax.swing.JRadioButton;
58 import javax.swing.event.InternalFrameAdapter;
59 import javax.swing.event.InternalFrameEvent;
60
61 /**
62  * A dialog where a user can choose and action Tree or PCA calculation options
63  */
64 public class CalculationChooser extends JPanel
65 {
66   /*
67    * flag for whether gap matches residue in the PID calculation for a Tree
68    * - true gives Jalview 2.10.1 behaviour
69    * - set to false (using Groovy) for a more correct tree
70    * (JAL-374)
71    */
72   private static boolean treeMatchGaps = true;
73
74   private static final Font VERDANA_11PT = new Font("Verdana", 0, 11);
75
76   AlignFrame af;
77
78   JRadioButton pca;
79
80   JRadioButton neighbourJoining;
81
82   JRadioButton averageDistance;
83
84   JComboBox<String> modelNames;
85
86   JButton ok;
87
88   private JInternalFrame frame;
89
90   private JCheckBox includeGaps;
91
92   private JCheckBox matchGaps;
93
94   private JCheckBox includeGappedColumns;
95
96   private JCheckBox shorterSequence;
97
98   /**
99    * Constructor
100    * 
101    * @param af
102    */
103   public CalculationChooser(AlignFrame alignFrame)
104   {
105     this.af = alignFrame;
106     init();
107     af.alignPanel.setCalculationDialog(this);
108   }
109
110   /**
111    * Lays out the panel and adds it to the desktop
112    */
113   void init()
114   {
115     setLayout(new BorderLayout());
116     frame = new JInternalFrame();
117     frame.setContentPane(this);
118     this.setBackground(Color.white);
119     frame.addFocusListener(new FocusListener()
120     {
121
122       @Override
123       public void focusLost(FocusEvent e)
124       {
125       }
126
127       @Override
128       public void focusGained(FocusEvent e)
129       {
130         validateCalcTypes();
131       }
132     });
133     /*
134      * Layout consists of 3 or 4 panels:
135      * - first with choice of PCA or tree method NJ or AV
136      * - second with choice of score model
137      * - third with score model parameter options [suppressed]
138      * - fourth with OK and Cancel
139      */
140     pca = new JRadioButton(
141             MessageManager.getString("label.principal_component_analysis"));
142     pca.setOpaque(false);
143     neighbourJoining = new JRadioButton(
144             MessageManager.getString("label.tree_calc_nj"));
145     averageDistance = new JRadioButton(
146             MessageManager.getString("label.tree_calc_av"));
147     neighbourJoining.setOpaque(false);
148
149     JPanel calcChoicePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
150     calcChoicePanel.setOpaque(false);
151
152     // first create the Tree calculation's border panel
153     JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
154     treePanel.setOpaque(false);
155
156     treePanel.setBorder(BorderFactory.createTitledBorder(MessageManager
157             .getString("label.tree")));
158
159     // then copy the inset dimensions for the border-less PCA panel
160     JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT));
161     Insets b = treePanel.getBorder().getBorderInsets(treePanel);
162     pcaBorderless.setBorder(BorderFactory.createEmptyBorder(2, b.left, 2,
163             b.right));
164     pcaBorderless.setOpaque(false);
165
166     pcaBorderless.add(pca, FlowLayout.LEFT);
167     calcChoicePanel.add(pcaBorderless, FlowLayout.LEFT);
168
169
170     treePanel.add(neighbourJoining);
171     treePanel.add(averageDistance);
172
173     calcChoicePanel.add(treePanel);
174
175     ButtonGroup calcTypes = new ButtonGroup();
176     calcTypes.add(pca);
177     calcTypes.add(neighbourJoining);
178     calcTypes.add(averageDistance);
179     
180     ActionListener calcChanged = new ActionListener()
181     {
182       @Override
183       public void actionPerformed(ActionEvent e)
184       {
185         validateCalcTypes();
186       }
187     };
188     pca.addActionListener(calcChanged);
189     neighbourJoining.addActionListener(calcChanged);
190     averageDistance.addActionListener(calcChanged);
191     /*
192      * score models drop-down - with added tooltips!
193      */
194     modelNames = buildModelOptionsList();
195
196     JPanel scoreModelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
197     scoreModelPanel.setOpaque(false);
198     scoreModelPanel.add(modelNames);
199
200     /*
201      * score model parameters
202      */
203     JPanel paramsPanel = new JPanel(new GridLayout(5, 1));
204     paramsPanel.setOpaque(false);
205     includeGaps = new JCheckBox("Include gaps");
206     matchGaps = new JCheckBox("Match gaps");
207     includeGappedColumns = new JCheckBox("Include gapped columns");
208     shorterSequence = new JCheckBox("Match on shorter sequence");
209     paramsPanel.add(new JLabel("Pairwise sequence scoring options"));
210     paramsPanel.add(includeGaps);
211     paramsPanel.add(matchGaps);
212     paramsPanel.add(includeGappedColumns);
213     paramsPanel.add(shorterSequence);
214
215     /*
216      * OK / Cancel buttons
217      */
218     ok = new JButton(MessageManager.getString("action.calculate"));
219     ok.setFont(VERDANA_11PT);
220     ok.addActionListener(new java.awt.event.ActionListener()
221     {
222       @Override
223       public void actionPerformed(ActionEvent e)
224       {
225         ok_actionPerformed();
226       }
227     });
228     JButton cancel = new JButton(MessageManager.getString("action.close"));
229     cancel.setFont(VERDANA_11PT);
230     cancel.addActionListener(new java.awt.event.ActionListener()
231     {
232       @Override
233       public void actionPerformed(ActionEvent e)
234       {
235         cancel_actionPerformed(e);
236       }
237     });
238     JPanel actionPanel = new JPanel();
239     actionPanel.setOpaque(false);
240     actionPanel.add(ok);
241     actionPanel.add(cancel);
242
243     boolean includeParams = false;
244     this.add(calcChoicePanel, BorderLayout.CENTER);
245     calcChoicePanel.add(scoreModelPanel);
246     if (includeParams)
247     {
248       scoreModelPanel.add(paramsPanel);
249     }
250     this.add(actionPanel, BorderLayout.SOUTH);
251
252     int width = 350;
253     int height = includeParams ? 420 : 240;
254
255     setMinimumSize(new Dimension(325, height - 10));
256     String title = MessageManager.getString("label.choose_calculation");
257     if (af.getViewport().viewName != null)
258     {
259       title = title + " (" + af.getViewport().viewName + ")";
260     }
261
262     Desktop.addInternalFrame(frame,
263             title, width,
264             height, false);
265     calcChoicePanel.doLayout();
266     revalidate();
267     /*
268      * null the AlignmentPanel's reference to the dialog when it is closed
269      */
270     frame.addInternalFrameListener(new InternalFrameAdapter()
271     {
272       @Override
273       public void internalFrameClosed(InternalFrameEvent evt)
274       {
275         af.alignPanel.setCalculationDialog(null);
276       };
277     });
278
279     frame.setLayer(JLayeredPane.PALETTE_LAYER);
280   }
281
282   /**
283    * enable calculations applicable for the current alignment or selection.
284    */
285   protected void validateCalcTypes()
286   {
287     int size = af.getViewport().getAlignment().getHeight();
288     if (af.getViewport().getSelectionGroup() != null)
289     {
290       size = af.getViewport().getSelectionGroup().getSize();
291     }
292     if (!(checkEnabled(pca, size, 4)
293             | checkEnabled(neighbourJoining, size, 3) | checkEnabled(
294               averageDistance, size, 3)))
295     {
296       ok.setToolTipText(null);
297       ok.setEnabled(true);
298     }
299     else
300     {
301       ok.setEnabled(false);
302     }
303   }
304
305   /**
306    * Check the input and disable a calculation's radio button if necessary. A
307    * tooltip is shown for disabled calculations.
308    * 
309    * @param calc
310    *          - radio button for the calculation being validated
311    * @param size
312    *          - size of input to calculation
313    * @param minsize
314    *          - minimum size for calculation
315    * @return true if size < minsize *and* calc.isSelected
316    */
317   private boolean checkEnabled(JRadioButton calc, int size, int minsize)
318   {
319     String ttip = MessageManager.formatMessage(
320             "label.you_need_more_than_n_sequences", minsize);
321
322     calc.setEnabled(size >= minsize);
323     if (!calc.isEnabled())
324     {
325       calc.setToolTipText(ttip);
326     }
327     else
328     {
329       calc.setToolTipText(null);
330     }
331     if (calc.isSelected())
332     {
333       modelNames.setEnabled(calc.isEnabled());
334       if (!calc.isEnabled())
335       {
336         ok.setEnabled(false);
337         ok.setToolTipText(ttip);
338         return true;
339       }
340     }
341     return false;
342   }
343   /**
344    * A rather elaborate helper method (blame Swing, not me) that builds a
345    * drop-down list of score models (by name) with descriptions as tooltips.
346    * There is also a tooltip shown for the currently selected item when hovering
347    * over it (without opening the list).
348    */
349   protected JComboBox<String> buildModelOptionsList()
350   {
351     final JComboBox<String> comboBox = new JComboBox<String>();
352     ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
353     comboBox.setRenderer(renderer);
354     final List<String> tips = new ArrayList<String>();
355
356     /*
357      * show tooltip on mouse over the combobox
358      * note the listener has to be on the components that make up
359      * the combobox, doesn't work if just on the combobox
360      */
361     MouseAdapter mouseListener = new MouseAdapter()
362     {
363       @Override
364       public void mouseEntered(MouseEvent e)
365       {
366         comboBox.setToolTipText(tips.get(comboBox.getSelectedIndex()));
367       }
368
369       @Override
370       public void mouseExited(MouseEvent e)
371       {
372         comboBox.setToolTipText(null);
373       }
374     };
375     for (Component c : comboBox.getComponents())
376     {
377       c.addMouseListener(mouseListener);
378     }
379
380     /*
381      * now we can actually add entries to the combobox,
382      * remembering their descriptions for tooltips
383      */
384     ScoreModels scoreModels = ScoreModels.getInstance();
385     for (ScoreModelI sm : scoreModels.getModels())
386     {
387       boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
388       if (sm.isDNA() && nucleotide || sm.isProtein() && !nucleotide)
389       {
390         comboBox.addItem(sm.getName());
391
392         /*
393          * tooltip is description if provided, else text lookup with
394          * fallback on the model name
395          */
396         String tooltip = sm.getDescription();
397         if (tooltip == null)
398         {
399           tooltip = MessageManager.getStringOrReturn("label.score_model_",
400                   sm.getName());
401         }
402         tips.add(tooltip);
403       }
404
405       /*
406        * set the list of tooltips on the combobox's renderer
407        */
408       renderer.setTooltips(tips);
409     }
410
411     return comboBox;
412   }
413
414   /**
415    * Open and calculate the selected tree or PCA on 'OK'
416    */
417   protected void ok_actionPerformed()
418   {
419     boolean doPCA = pca.isSelected();
420     String modelName = modelNames.getSelectedItem().toString();
421     SimilarityParamsI params = getSimilarityParameters(doPCA);
422
423     if (doPCA)
424     {
425       openPcaPanel(modelName, params);
426     }
427     else
428     {
429       openTreePanel(modelName, params);
430     }
431
432     // closeFrame();
433   }
434
435   /**
436    * Open a new Tree panel on the desktop
437    * 
438    * @param modelName
439    * @param params
440    */
441   protected void openTreePanel(String modelName, SimilarityParamsI params)
442   {
443     String treeType = neighbourJoining.isSelected() ? TreeBuilder.NEIGHBOUR_JOINING
444             : TreeBuilder.AVERAGE_DISTANCE;
445     af.newTreePanel(treeType, modelName, params);
446   }
447
448   /**
449    * Open a new PCA panel on the desktop
450    * 
451    * @param modelName
452    * @param params
453    */
454   protected void openPcaPanel(String modelName, SimilarityParamsI params)
455   {
456     AlignViewport viewport = af.getViewport();
457     if (((viewport.getSelectionGroup() != null)
458             && (viewport.getSelectionGroup().getSize() < 4) && (viewport
459             .getSelectionGroup().getSize() > 0))
460             || (viewport.getAlignment().getHeight() < 4))
461     {
462       JvOptionPane
463               .showInternalMessageDialog(
464                       this,
465                       MessageManager
466                               .getString("label.principal_component_analysis_must_take_least_four_input_sequences"),
467                       MessageManager
468                               .getString("label.sequence_selection_insufficient"),
469                       JvOptionPane.WARNING_MESSAGE);
470       return;
471     }
472     new PCAPanel(af.alignPanel, modelName, params);
473   }
474
475   /**
476    * 
477    */
478   protected void closeFrame()
479   {
480     try
481     {
482       frame.setClosed(true);
483     } catch (PropertyVetoException ex)
484     {
485     }
486   }
487
488   /**
489    * Returns a data bean holding parameters for similarity (or distance) model
490    * calculation
491    * 
492    * @param doPCA
493    * @return
494    */
495   protected SimilarityParamsI getSimilarityParameters(boolean doPCA)
496   {
497     // commented out: parameter choices read from gui widgets
498     // SimilarityParamsI params = new SimilarityParams(
499     // includeGappedColumns.isSelected(), matchGaps.isSelected(),
500     // includeGaps.isSelected(), shorterSequence.isSelected());
501
502     boolean includeGapGap = true;
503     boolean includeGapResidue = true;
504     boolean matchOnShortestLength = false;
505
506     /*
507      * 'matchGaps' flag is only used in the PID calculation
508      * - set to false for PCA so that PCA using PID reproduces SeqSpace PCA
509      * - set to true for Tree to reproduce Jalview 2.10.1 calculation
510      * - set to false for Tree for a more correct calculation (JAL-374)
511      */
512     boolean matchGap = doPCA ? false : treeMatchGaps;
513
514     return new SimilarityParams(includeGapGap, matchGap, includeGapResidue, matchOnShortestLength);
515   }
516
517   /**
518    * Closes dialog on cancel
519    * 
520    * @param e
521    */
522   protected void cancel_actionPerformed(ActionEvent e)
523   {
524     try
525     {
526       frame.setClosed(true);
527     } catch (Exception ex)
528     {
529     }
530   }
531 }