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