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