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