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