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