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