fixed incorrect panel title
[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.TreeCalculator;
25 import jalview.analysis.TreeModel;
26 import jalview.analysis.scoremodels.ScoreModels;
27 import jalview.analysis.scoremodels.SimilarityParams;
28 import jalview.api.analysis.ScoreModelI;
29 import jalview.api.analysis.SimilarityParamsI;
30 import jalview.datamodel.SequenceGroup;
31 import jalview.ext.archaeopteryx.ArchaeopteryxTreeBuilder;
32 import jalview.util.MessageManager;
33
34 import java.awt.BorderLayout;
35 import java.awt.Color;
36 import java.awt.Component;
37 import java.awt.Dimension;
38 import java.awt.FlowLayout;
39 import java.awt.Font;
40 import java.awt.GridLayout;
41 import java.awt.Insets;
42 import java.awt.event.ActionEvent;
43 import java.awt.event.ActionListener;
44 import java.awt.event.FocusEvent;
45 import java.awt.event.FocusListener;
46 import java.awt.event.MouseAdapter;
47 import java.awt.event.MouseEvent;
48 import java.beans.PropertyVetoException;
49 import java.util.ArrayList;
50 import java.util.List;
51
52 import javax.swing.BorderFactory;
53 import javax.swing.ButtonGroup;
54 import javax.swing.DefaultComboBoxModel;
55 import javax.swing.JButton;
56 import javax.swing.JCheckBox;
57 import javax.swing.JComboBox;
58 import javax.swing.JInternalFrame;
59 import javax.swing.JLabel;
60 import javax.swing.JLayeredPane;
61 import javax.swing.JPanel;
62 import javax.swing.JRadioButton;
63 import javax.swing.event.InternalFrameAdapter;
64 import javax.swing.event.InternalFrameEvent;
65
66 /**
67  * A dialog where a user can choose and action Tree or PCA calculation options
68  */
69 public class CalculationChooser extends JPanel
70 {
71   /*
72    * flag for whether gap matches residue in the PID calculation for a Tree
73    * - true gives Jalview 2.10.1 behaviour
74    * - set to false (using Groovy) for a more correct tree
75    * (JAL-374)
76    */
77   private static boolean treeMatchGaps = true;
78
79   private static final Font VERDANA_11PT = new Font("Verdana", 0, 11);
80
81   private static final int MIN_TREE_SELECTION = 3;
82
83   private static final int MIN_PCA_SELECTION = 4;
84
85   AlignFrame af;
86
87   JRadioButton pca;
88
89   JRadioButton neighbourJoining;
90
91   JRadioButton averageDistance;
92
93   JComboBox<String> modelNames;
94
95   JButton calculate;
96
97   private JInternalFrame frame;
98
99   private JCheckBox includeGaps;
100
101   private JCheckBox matchGaps;
102
103   private JCheckBox includeGappedColumns;
104
105   private JCheckBox shorterSequence;
106
107   final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
108
109   List<String> tips = new ArrayList<>();
110
111   /**
112    * Constructor
113    * 
114    * @param af
115    */
116   public CalculationChooser(AlignFrame alignFrame)
117   {
118     this.af = alignFrame;
119     init();
120     af.alignPanel.setCalculationDialog(this);
121   }
122
123   /**
124    * Lays out the panel and adds it to the desktop
125    */
126   void init()
127   {
128     setLayout(new BorderLayout());
129     frame = new JInternalFrame();
130     frame.setContentPane(this);
131     this.setBackground(Color.white);
132     frame.addFocusListener(new FocusListener()
133     {
134
135       @Override
136       public void focusLost(FocusEvent e)
137       {
138       }
139
140       @Override
141       public void focusGained(FocusEvent e)
142       {
143         validateCalcTypes();
144       }
145     });
146     /*
147      * Layout consists of 3 or 4 panels:
148      * - first with choice of PCA or tree method NJ or AV
149      * - second with choice of score model
150      * - third with score model parameter options [suppressed]
151      * - fourth with OK and Cancel
152      */
153     pca = new JRadioButton(
154             MessageManager.getString("label.principal_component_analysis"));
155     pca.setOpaque(false);
156     neighbourJoining = new JRadioButton(
157             MessageManager.getString("label.tree_calc_nj"));
158     neighbourJoining.setSelected(true);
159     averageDistance = new JRadioButton(
160             MessageManager.getString("label.tree_calc_av"));
161     neighbourJoining.setOpaque(false);
162
163     JPanel calcChoicePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
164     calcChoicePanel.setOpaque(false);
165
166     // first create the Tree calculation's border panel
167     JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
168     treePanel.setOpaque(false);
169
170     treePanel.setBorder(BorderFactory
171             .createTitledBorder(MessageManager.getString("label.tree")));
172
173     // then copy the inset dimensions for the border-less PCA panel
174     JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT));
175     Insets b = treePanel.getBorder().getBorderInsets(treePanel);
176     pcaBorderless.setBorder(
177             BorderFactory.createEmptyBorder(2, b.left, 2, b.right));
178     pcaBorderless.setOpaque(false);
179
180     pcaBorderless.add(pca, FlowLayout.LEFT);
181     calcChoicePanel.add(pcaBorderless, FlowLayout.LEFT);
182
183     treePanel.add(neighbourJoining);
184     treePanel.add(averageDistance);
185
186     calcChoicePanel.add(treePanel);
187
188     ButtonGroup calcTypes = new ButtonGroup();
189     calcTypes.add(pca);
190     calcTypes.add(neighbourJoining);
191     calcTypes.add(averageDistance);
192
193     ActionListener calcChanged = new ActionListener()
194     {
195       @Override
196       public void actionPerformed(ActionEvent e)
197       {
198         validateCalcTypes();
199       }
200     };
201     pca.addActionListener(calcChanged);
202     neighbourJoining.addActionListener(calcChanged);
203     averageDistance.addActionListener(calcChanged);
204
205     /*
206      * score models drop-down - with added tooltips!
207      */
208     modelNames = buildModelOptionsList();
209
210     JPanel scoreModelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
211     scoreModelPanel.setOpaque(false);
212     scoreModelPanel.add(modelNames);
213
214     /*
215      * score model parameters
216      */
217     JPanel paramsPanel = new JPanel(new GridLayout(5, 1));
218     paramsPanel.setOpaque(false);
219     includeGaps = new JCheckBox("Include gaps");
220     matchGaps = new JCheckBox("Match gaps");
221     includeGappedColumns = new JCheckBox("Include gapped columns");
222     shorterSequence = new JCheckBox("Match on shorter sequence");
223     paramsPanel.add(new JLabel("Pairwise sequence scoring options"));
224     paramsPanel.add(includeGaps);
225     paramsPanel.add(matchGaps);
226     paramsPanel.add(includeGappedColumns);
227     paramsPanel.add(shorterSequence);
228
229     /*
230      * OK / Cancel buttons
231      */
232     calculate = new JButton(MessageManager.getString("action.calculate"));
233     calculate.setFont(VERDANA_11PT);
234     calculate.addActionListener(new java.awt.event.ActionListener()
235     {
236       @Override
237       public void actionPerformed(ActionEvent e)
238       {
239         calculate_actionPerformed();
240       }
241     });
242     JButton close = new JButton(MessageManager.getString("action.close"));
243     close.setFont(VERDANA_11PT);
244     close.addActionListener(new java.awt.event.ActionListener()
245     {
246       @Override
247       public void actionPerformed(ActionEvent e)
248       {
249         close_actionPerformed();
250       }
251     });
252     JPanel actionPanel = new JPanel();
253     actionPanel.setOpaque(false);
254     actionPanel.add(calculate);
255     actionPanel.add(close);
256
257     boolean includeParams = false;
258     this.add(calcChoicePanel, BorderLayout.CENTER);
259     calcChoicePanel.add(scoreModelPanel);
260     if (includeParams)
261     {
262       scoreModelPanel.add(paramsPanel);
263     }
264     this.add(actionPanel, BorderLayout.SOUTH);
265
266     int width = 350;
267     int height = includeParams ? 420 : 240;
268
269     setMinimumSize(new Dimension(325, height - 10));
270     String title = MessageManager.getString("label.choose_calculation");
271     if (af.getViewport().viewName != null)
272     {
273       title = title + " (" + af.getViewport().viewName + ")";
274     }
275
276     Desktop.addInternalFrame(frame, title, width, height, false);
277     calcChoicePanel.doLayout();
278     revalidate();
279     /*
280      * null the AlignmentPanel's reference to the dialog when it is closed
281      */
282     frame.addInternalFrameListener(new InternalFrameAdapter()
283     {
284       @Override
285       public void internalFrameClosed(InternalFrameEvent evt)
286       {
287         af.alignPanel.setCalculationDialog(null);
288       };
289     });
290
291     frame.setLayer(JLayeredPane.PALETTE_LAYER);
292   }
293
294   /**
295    * enable calculations applicable for the current alignment or selection.
296    */
297   protected void validateCalcTypes()
298   {
299     int size = af.getViewport().getAlignment().getHeight();
300     if (af.getViewport().getSelectionGroup() != null)
301     {
302       size = af.getViewport().getSelectionGroup().getSize();
303     }
304
305     /*
306      * disable calc options for which there is insufficient input data
307      * return value of true means enabled and selected
308      */
309     boolean checkPca = checkEnabled(pca, size, MIN_PCA_SELECTION);
310     boolean checkNeighbourJoining = checkEnabled(neighbourJoining, size,
311             MIN_TREE_SELECTION);
312     boolean checkAverageDistance = checkEnabled(averageDistance, size,
313             MIN_TREE_SELECTION);
314
315     if (checkPca || checkNeighbourJoining || checkAverageDistance)
316     {
317       calculate.setToolTipText(null);
318       calculate.setEnabled(true);
319     }
320     else
321     {
322       calculate.setEnabled(false);
323     }
324     updateScoreModels(modelNames, tips);
325   }
326
327   /**
328    * Check the input and disable a calculation's radio button if necessary. A
329    * tooltip is shown for disabled calculations.
330    * 
331    * @param calc
332    *          - radio button for the calculation being validated
333    * @param size
334    *          - size of input to calculation
335    * @param minsize
336    *          - minimum size for calculation
337    * @return true if size >= minsize and calc.isSelected
338    */
339   private boolean checkEnabled(JRadioButton calc, int size, int minsize)
340   {
341     String ttip = MessageManager
342             .formatMessage("label.you_need_at_least_n_sequences", minsize);
343
344     calc.setEnabled(size >= minsize);
345     if (!calc.isEnabled())
346     {
347       calc.setToolTipText(ttip);
348     }
349     else
350     {
351       calc.setToolTipText(null);
352     }
353     if (calc.isSelected())
354     {
355       modelNames.setEnabled(calc.isEnabled());
356       if (calc.isEnabled())
357       {
358         return true;
359       }
360       else
361       {
362         calculate.setToolTipText(ttip);
363       }
364     }
365     return false;
366   }
367
368   /**
369    * A rather elaborate helper method (blame Swing, not me) that builds a
370    * drop-down list of score models (by name) with descriptions as tooltips.
371    * There is also a tooltip shown for the currently selected item when hovering
372    * over it (without opening the list).
373    */
374   protected JComboBox<String> buildModelOptionsList()
375   {
376     final JComboBox<String> scoreModelsCombo = new JComboBox<>();
377     scoreModelsCombo.setRenderer(renderer);
378
379     /*
380      * show tooltip on mouse over the combobox
381      * note the listener has to be on the components that make up
382      * the combobox, doesn't work if just on the combobox
383      */
384     final MouseAdapter mouseListener = new MouseAdapter()
385     {
386       @Override
387       public void mouseEntered(MouseEvent e)
388       {
389         scoreModelsCombo.setToolTipText(
390                 tips.get(scoreModelsCombo.getSelectedIndex()));
391       }
392
393       @Override
394       public void mouseExited(MouseEvent e)
395       {
396         scoreModelsCombo.setToolTipText(null);
397       }
398     };
399     for (Component c : scoreModelsCombo.getComponents())
400     {
401       c.addMouseListener(mouseListener);
402     }
403
404     updateScoreModels(scoreModelsCombo, tips);
405
406     /*
407      * set the list of tooltips on the combobox's renderer
408      */
409     renderer.setTooltips(tips);
410
411     return scoreModelsCombo;
412   }
413
414   private void updateScoreModels(JComboBox<String> comboBox,
415           List<String> toolTips)
416   {
417     Object curSel = comboBox.getSelectedItem();
418     toolTips.clear();
419     DefaultComboBoxModel<String> model = new DefaultComboBoxModel<>();
420
421     /*
422      * now we can actually add entries to the combobox,
423      * remembering their descriptions for tooltips
424      */
425     ScoreModels scoreModels = ScoreModels.getInstance();
426     boolean selectedIsPresent = false;
427     for (ScoreModelI sm : scoreModels.getModels())
428     {
429       boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
430       if (sm.isDNA() && nucleotide || sm.isProtein() && !nucleotide)
431       {
432         if (curSel != null && sm.getName().equals(curSel))
433         {
434           selectedIsPresent = true;
435           curSel = sm.getName();
436         }
437         model.addElement(sm.getName());
438
439         /*
440          * tooltip is description if provided, else text lookup with
441          * fallback on the model name
442          */
443         String tooltip = sm.getDescription();
444         if (tooltip == null)
445         {
446           tooltip = MessageManager.getStringOrReturn("label.score_model_",
447                   sm.getName());
448         }
449         toolTips.add(tooltip);
450       }
451     }
452     if (selectedIsPresent)
453     {
454       model.setSelectedItem(curSel);
455     }
456     // finally, update the model
457     comboBox.setModel(model);
458   }
459
460   /**
461    * Open and calculate the selected tree or PCA on 'OK'
462    */
463   protected void calculate_actionPerformed()
464   {
465     boolean doPCA = pca.isSelected();
466     String substitutionMatrix = modelNames.getSelectedItem().toString();
467     SimilarityParamsI params = getSimilarityParameters(doPCA);
468
469     if (doPCA)
470     {
471       openPcaPanel(substitutionMatrix, params);
472     }
473     else
474     {
475
476       String treeAlgo = determineTreeAlgo();
477       TreeCalculator treeCalculator = new TreeCalculator(treeAlgo,
478               substitutionMatrix, params);
479       TreeBuilder calculatedTree = treeCalculator
480               .makeTree(af.getViewport());
481       TreeModel tree = new TreeModel(calculatedTree);
482       openTreePanel(tree, treeAlgo, substitutionMatrix);
483
484       ArchaeopteryxTreeBuilder aptxTree = new ArchaeopteryxTreeBuilder();
485
486
487     }
488
489     // closeFrame();
490   }
491
492   protected String determineTreeAlgo() // to be modified & expanded
493   {
494     String treeAlgorithm = neighbourJoining.isSelected()
495             ? TreeBuilder.NEIGHBOUR_JOINING
496             : TreeBuilder.AVERAGE_DISTANCE;
497
498     return treeAlgorithm;
499
500   }
501
502   protected void checkEnoughSequences(AlignViewport viewport)
503   {
504     SequenceGroup sg = viewport.getSelectionGroup();
505     if (sg != null && sg.getSize() < MIN_TREE_SELECTION)
506     {
507       JvOptionPane.showMessageDialog(Desktop.desktop,
508               MessageManager.formatMessage(
509                       "label.you_need_at_least_n_sequences",
510                       MIN_TREE_SELECTION),
511               MessageManager.getString("label.not_enough_sequences"),
512               JvOptionPane.WARNING_MESSAGE);
513       return;
514     }
515   }
516
517   /**
518    * Open a new Tree panel on the desktop
519    * 
520    * @param tree
521    * @param params
522    * @param treeAlgo
523    */
524   protected void openTreePanel(TreeModel tree, String treeAlgo,
525           String substitutionMatrix)
526   {
527     /*
528      * gui validation shouldn't allow insufficient sequences here, but leave
529      * this check in in case this method gets exposed programmatically in future
530      */
531     checkEnoughSequences(af.getViewport());
532
533     af.newTreePanel(tree, treeAlgo, substitutionMatrix);
534   }
535
536   /**
537    * Open a new PCA panel on the desktop
538    * 
539    * @param modelName
540    * @param params
541    */
542   protected void openPcaPanel(String modelName, SimilarityParamsI params)
543   {
544     AlignViewport viewport = af.getViewport();
545
546     /*
547      * gui validation shouldn't allow insufficient sequences here, but leave
548      * this check in in case this method gets exposed programmatically in future
549      */
550     if (((viewport.getSelectionGroup() != null)
551             && (viewport.getSelectionGroup().getSize() < MIN_PCA_SELECTION)
552             && (viewport.getSelectionGroup().getSize() > 0))
553             || (viewport.getAlignment().getHeight() < MIN_PCA_SELECTION))
554     {
555       JvOptionPane.showInternalMessageDialog(this,
556               MessageManager.formatMessage(
557                       "label.you_need_at_least_n_sequences",
558                       MIN_PCA_SELECTION),
559               MessageManager
560                       .getString("label.sequence_selection_insufficient"),
561               JvOptionPane.WARNING_MESSAGE);
562       return;
563     }
564     new PCAPanel(af.alignPanel, modelName, params);
565   }
566
567   /**
568    * 
569    */
570   protected void closeFrame()
571   {
572     try
573     {
574       frame.setClosed(true);
575     } catch (PropertyVetoException ex)
576     {
577     }
578   }
579
580   /**
581    * Returns a data bean holding parameters for similarity (or distance) model
582    * calculation
583    * 
584    * @param doPCA
585    * @return
586    */
587   protected SimilarityParamsI getSimilarityParameters(boolean doPCA)
588   {
589     // commented out: parameter choices read from gui widgets
590     // SimilarityParamsI params = new SimilarityParams(
591     // includeGappedColumns.isSelected(), matchGaps.isSelected(),
592     // includeGaps.isSelected(), shorterSequence.isSelected());
593
594     boolean includeGapGap = true;
595     boolean includeGapResidue = true;
596     boolean matchOnShortestLength = false;
597
598     /*
599      * 'matchGaps' flag is only used in the PID calculation
600      * - set to false for PCA so that PCA using PID reproduces SeqSpace PCA
601      * - set to true for Tree to reproduce Jalview 2.10.1 calculation
602      * - set to false for Tree for a more correct calculation (JAL-374)
603      */
604     boolean matchGap = doPCA ? false : treeMatchGaps;
605
606
607     return new SimilarityParams(includeGapGap, matchGap, includeGapResidue,
608             matchOnShortestLength);
609   }
610
611   /**
612    * Closes dialog on Close button press
613    */
614   protected void close_actionPerformed()
615   {
616     try
617     {
618       frame.setClosed(true);
619     } catch (Exception ex)
620     {
621     }
622   }
623 }