JAL-3253 miscellaneous tidying up before merge to JS-develop
[jalview.git] / src / jalview / gui / PCAPanel.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.scoremodels.ScoreModels;
24 import jalview.api.AlignViewportI;
25 import jalview.api.analysis.ScoreModelI;
26 import jalview.api.analysis.SimilarityParamsI;
27 import jalview.bin.Cache;
28 import jalview.datamodel.Alignment;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.AlignmentView;
31 import jalview.datamodel.HiddenColumns;
32 import jalview.datamodel.SequenceI;
33 import jalview.gui.ImageExporter.ImageWriterI;
34 import jalview.gui.JalviewColourChooser.ColourChooserListener;
35 import jalview.jbgui.GPCAPanel;
36 import jalview.math.RotatableMatrix.Axis;
37 import jalview.util.ImageMaker;
38 import jalview.util.MessageManager;
39 import jalview.viewmodel.AlignmentViewport;
40 import jalview.viewmodel.PCAModel;
41
42 import java.awt.BorderLayout;
43 import java.awt.Color;
44 import java.awt.Dimension;
45 import java.awt.Graphics;
46 import java.awt.event.ActionEvent;
47 import java.awt.event.ActionListener;
48 import java.awt.print.PageFormat;
49 import java.awt.print.Printable;
50 import java.awt.print.PrinterException;
51 import java.awt.print.PrinterJob;
52
53 import javax.swing.ButtonGroup;
54 import javax.swing.JMenuItem;
55 import javax.swing.JRadioButtonMenuItem;
56 import javax.swing.event.InternalFrameAdapter;
57 import javax.swing.event.InternalFrameEvent;
58
59 /**
60  * The panel holding the Principal Component Analysis 3-D visualisation
61  */
62 public class PCAPanel extends GPCAPanel
63         implements Runnable, IProgressIndicator
64 {
65   private static final int MIN_WIDTH = 470;
66
67   private static final int MIN_HEIGHT = 250;
68
69   RotatableCanvas rc;
70
71   AlignmentPanel ap;
72
73   AlignmentViewport av;
74
75   private PCAModel pcaModel;
76
77   private int top = 0;
78
79   private IProgressIndicator progressBar;
80
81   private boolean working;
82
83   /**
84    * Constructor given sequence data, a similarity (or distance) score model
85    * name, and score calculation parameters
86    * 
87    * @param alignPanel
88    * @param modelName
89    * @param params
90    */
91   public PCAPanel(AlignmentPanel alignPanel, String modelName,
92           SimilarityParamsI params)
93   {
94     super();
95     this.av = alignPanel.av;
96     this.ap = alignPanel;
97     boolean nucleotide = av.getAlignment().isNucleotide();
98
99     progressBar = new ProgressBar(statusPanel, statusBar);
100
101     addInternalFrameListener(new InternalFrameAdapter()
102     {
103       @Override
104       public void internalFrameClosed(InternalFrameEvent e)
105       {
106         close_actionPerformed();
107       }
108     });
109
110     boolean selected = av.getSelectionGroup() != null
111             && av.getSelectionGroup().getSize() > 0;
112     AlignmentView seqstrings = av.getAlignmentView(selected);
113     SequenceI[] seqs;
114     if (!selected)
115     {
116       seqs = av.getAlignment().getSequencesArray();
117     }
118     else
119     {
120       seqs = av.getSelectionGroup().getSequencesInOrder(av.getAlignment());
121     }
122
123     ScoreModelI scoreModel = ScoreModels.getInstance()
124             .getScoreModel(modelName, ap);
125     setPcaModel(new PCAModel(seqstrings, seqs, nucleotide, scoreModel,
126             params));
127     PaintRefresher.Register(this, av.getSequenceSetId());
128
129     setRotatableCanvas(new RotatableCanvas(alignPanel));
130     this.getContentPane().add(getRotatableCanvas(), BorderLayout.CENTER);
131
132     addKeyListener(getRotatableCanvas());
133     validate();
134   }
135
136   /**
137    * Ensure references to potentially very large objects (the PCA matrices) are
138    * nulled when the frame is closed
139    */
140   protected void close_actionPerformed()
141   {
142     setPcaModel(null);
143   }
144
145   @Override
146   protected void bgcolour_actionPerformed()
147   {
148     String ttl = MessageManager.getString("label.select_background_colour");
149     ColourChooserListener listener = new ColourChooserListener()
150     {
151       @Override
152       public void colourSelected(Color c)
153       {
154         rc.setBgColour(c);
155         rc.repaint();
156       }
157     };
158     JalviewColourChooser.showColourChooser(this, ttl, rc.getBgColour(),
159             listener);
160   }
161
162   /**
163    * Calculates the PCA and displays the results
164    */
165   @Override
166   public void run()
167   {
168     working = true;
169     long progId = System.currentTimeMillis();
170     IProgressIndicator progress = this;
171     String message = MessageManager.getString("label.pca_recalculating");
172     if (getParent() == null)
173     {
174       progress = ap.alignFrame;
175       message = MessageManager.getString("label.pca_calculating");
176     }
177     progress.setProgressBar(message, progId);
178     try
179     {
180       getPcaModel().calculate();
181
182       xCombobox.setSelectedIndex(0);
183       yCombobox.setSelectedIndex(1);
184       zCombobox.setSelectedIndex(2);
185
186       getPcaModel().updateRc(getRotatableCanvas());
187       // rc.invalidate();
188       setTop(getPcaModel().getTop());
189
190     } catch (OutOfMemoryError er)
191     {
192       new OOMWarning("calculating PCA", er);
193       working = false;
194       return;
195     } finally
196     {
197       progress.setProgressBar("", progId);
198     }
199
200     repaint();
201     if (getParent() == null)
202     {
203       Desktop.addInternalFrame(this,
204               MessageManager.formatMessage("label.calc_title", "PCA",
205                       getPcaModel().getScoreModelName()),
206               475, 450);
207       this.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
208     }
209     working = false;
210   }
211
212   /**
213    * Updates the PCA display after a change of component to use for x, y or z
214    * axis
215    */
216   @Override
217   protected void doDimensionChange()
218   {
219     if (getTop() == 0)
220     {
221       return;
222     }
223
224     int dim1 = getTop() - xCombobox.getSelectedIndex();
225     int dim2 = getTop() - yCombobox.getSelectedIndex();
226     int dim3 = getTop() - zCombobox.getSelectedIndex();
227     getPcaModel().updateRcView(dim1, dim2, dim3);
228     getRotatableCanvas().resetView();
229   }
230
231   /**
232    * Sets the selected checkbox item index for PCA dimension (1, 2, 3...) for
233    * the given axis (X/Y/Z)
234    * 
235    * @param index
236    * @param axis
237    */
238   public void setSelectedDimensionIndex(int index, Axis axis)
239   {
240     switch (axis)
241     {
242     case X:
243       xCombobox.setSelectedIndex(index);
244       break;
245     case Y:
246       yCombobox.setSelectedIndex(index);
247       break;
248     case Z:
249       zCombobox.setSelectedIndex(index);
250       break;
251     default:
252     }
253   }
254
255   @Override
256   protected void outputValues_actionPerformed()
257   {
258     CutAndPasteTransfer cap = new CutAndPasteTransfer();
259     try
260     {
261       cap.setText(getPcaModel().getDetails());
262       Desktop.addInternalFrame(cap,
263               MessageManager.getString("label.pca_details"), 500, 500);
264     } catch (OutOfMemoryError oom)
265     {
266       new OOMWarning("opening PCA details", oom);
267       cap.dispose();
268     }
269   }
270
271   @Override
272   protected void showLabels_actionPerformed()
273   {
274     getRotatableCanvas().showLabels(showLabels.getState());
275   }
276
277   @Override
278   protected void print_actionPerformed()
279   {
280     PCAPrinter printer = new PCAPrinter();
281     printer.start();
282   }
283
284   /**
285    * If available, shows the data which formed the inputs for the PCA as a new
286    * alignment
287    */
288   @Override
289   public void originalSeqData_actionPerformed()
290   {
291     // JAL-2647 disabled after load from project (until save to project done)
292     if (getPcaModel().getInputData() == null)
293     {
294       Cache.log.info(
295               "Unexpected call to originalSeqData_actionPerformed - should have hidden this menu action.");
296       return;
297     }
298     // decide if av alignment is sufficiently different to original data to
299     // warrant a new window to be created
300     // create new alignment window with hidden regions (unhiding hidden regions
301     // yields unaligned seqs)
302     // or create a selection box around columns in alignment view
303     // test Alignment(SeqCigar[])
304     char gc = '-';
305     try
306     {
307       // we try to get the associated view's gap character
308       // but this may fail if the view was closed...
309       gc = av.getGapCharacter();
310     } catch (Exception ex)
311     {
312     }
313
314     Object[] alAndColsel = getPcaModel().getInputData()
315             .getAlignmentAndHiddenColumns(gc);
316
317     if (alAndColsel != null && alAndColsel[0] != null)
318     {
319       // AlignmentOrder origorder = new AlignmentOrder(alAndColsel[0]);
320
321       AlignmentI al = new Alignment((SequenceI[]) alAndColsel[0]);
322       AlignmentI dataset = (av != null && av.getAlignment() != null)
323               ? av.getAlignment().getDataset()
324               : null;
325       if (dataset != null)
326       {
327         al.setDataset(dataset);
328       }
329
330       if (true)
331       {
332         // make a new frame!
333         AlignFrame af = new AlignFrame(al, (HiddenColumns) alAndColsel[1],
334                 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
335
336         // >>>This is a fix for the moment, until a better solution is
337         // found!!<<<
338         // af.getFeatureRenderer().transferSettings(alignFrame.getFeatureRenderer());
339
340         // af.addSortByOrderMenuItem(ServiceName + " Ordering",
341         // msaorder);
342
343         Desktop.addInternalFrame(af, MessageManager.formatMessage(
344                 "label.original_data_for_params", new String[]
345                 { this.title }), AlignFrame.DEFAULT_WIDTH,
346                 AlignFrame.DEFAULT_HEIGHT);
347       }
348     }
349     /*
350      * CutAndPasteTransfer cap = new CutAndPasteTransfer(); for (int i = 0; i <
351      * seqs.length; i++) { cap.appendText(new jalview.util.Format("%-" + 15 +
352      * "s").form( seqs[i].getName())); cap.appendText(" " + seqstrings[i] +
353      * "\n"); }
354      * 
355      * Desktop.addInternalFrame(cap, "Original Data", 400, 400);
356      */
357   }
358
359   class PCAPrinter extends Thread implements Printable
360   {
361     @Override
362     public void run()
363     {
364       PrinterJob printJob = PrinterJob.getPrinterJob();
365       PageFormat defaultPage = printJob.defaultPage();
366       PageFormat pf = printJob.pageDialog(defaultPage);
367
368       if (defaultPage == pf)
369       {
370         /*
371          * user cancelled
372          */
373         return;
374       }
375
376       printJob.setPrintable(this, pf);
377
378       if (printJob.printDialog())
379       {
380         try
381         {
382           printJob.print();
383         } catch (Exception PrintException)
384         {
385           PrintException.printStackTrace();
386         }
387       }
388     }
389
390     @Override
391     public int print(Graphics pg, PageFormat pf, int pi)
392             throws PrinterException
393     {
394       pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
395
396       getRotatableCanvas().drawBackground(pg);
397       getRotatableCanvas().drawScene(pg);
398       if (getRotatableCanvas().drawAxes)
399       {
400         getRotatableCanvas().drawAxes(pg);
401       }
402
403       if (pi == 0)
404       {
405         return Printable.PAGE_EXISTS;
406       }
407       else
408       {
409         return Printable.NO_SUCH_PAGE;
410       }
411     }
412   }
413
414   @Override
415   public void makePCAImage(ImageMaker.TYPE type)
416   {
417     int width = getRotatableCanvas().getWidth();
418     int height = getRotatableCanvas().getHeight();
419     ImageWriterI writer = new ImageWriterI()
420     {
421       @Override
422       public void exportImage(Graphics g) throws Exception
423       {
424         RotatableCanvas canvas = getRotatableCanvas();
425         canvas.drawBackground(g);
426         canvas.drawScene(g);
427         if (canvas.drawAxes)
428         {
429           canvas.drawAxes(g);
430         }
431       }
432     };
433     String pca = MessageManager.getString("label.pca");
434     ImageExporter exporter = new ImageExporter(writer, null, type, pca);
435     exporter.doExport(null, this, width, height, pca);
436   }
437
438   @Override
439   protected void viewMenu_menuSelected()
440   {
441     buildAssociatedViewMenu();
442   }
443
444   /**
445    * Builds the menu showing the choice of possible views (for the associated
446    * sequence data) to which the PCA may be linked
447    */
448   void buildAssociatedViewMenu()
449   {
450     AlignmentPanel[] aps = PaintRefresher
451             .getAssociatedPanels(av.getSequenceSetId());
452     if (aps.length == 1 && getRotatableCanvas().av == aps[0].av)
453     {
454       associateViewsMenu.setVisible(false);
455       return;
456     }
457
458     associateViewsMenu.setVisible(true);
459
460     if ((viewMenu
461             .getItem(viewMenu.getItemCount() - 2) instanceof JMenuItem))
462     {
463       viewMenu.insertSeparator(viewMenu.getItemCount() - 1);
464     }
465
466     associateViewsMenu.removeAll();
467
468     JRadioButtonMenuItem item;
469     ButtonGroup buttonGroup = new ButtonGroup();
470     int iSize = aps.length;
471
472     for (int i = 0; i < iSize; i++)
473     {
474       final AlignmentPanel panel = aps[i];
475       item = new JRadioButtonMenuItem(panel.av.getViewName(),
476               panel.av == getRotatableCanvas().av);
477       buttonGroup.add(item);
478       item.addActionListener(new ActionListener()
479       {
480         @Override
481         public void actionPerformed(ActionEvent evt)
482         {
483           selectAssociatedView(panel);
484         }
485       });
486
487       associateViewsMenu.add(item);
488     }
489
490     final JRadioButtonMenuItem itemf = new JRadioButtonMenuItem(
491             "All Views");
492
493     buttonGroup.add(itemf);
494
495     itemf.setSelected(getRotatableCanvas().isApplyToAllViews());
496     itemf.addActionListener(new ActionListener()
497     {
498       @Override
499       public void actionPerformed(ActionEvent evt)
500       {
501         getRotatableCanvas().setApplyToAllViews(itemf.isSelected());
502       }
503     });
504     associateViewsMenu.add(itemf);
505
506   }
507
508   @Override
509   protected void outputPoints_actionPerformed()
510   {
511     CutAndPasteTransfer cap = new CutAndPasteTransfer();
512     try
513     {
514       cap.setText(getPcaModel().getPointsasCsv(false,
515               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
516               zCombobox.getSelectedIndex()));
517       Desktop.addInternalFrame(cap, MessageManager
518               .formatMessage("label.points_for_params", new String[]
519               { this.getTitle() }), 500, 500);
520     } catch (OutOfMemoryError oom)
521     {
522       new OOMWarning("exporting PCA points", oom);
523       cap.dispose();
524     }
525   }
526
527   @Override
528   protected void outputProjPoints_actionPerformed()
529   {
530     CutAndPasteTransfer cap = new CutAndPasteTransfer();
531     try
532     {
533       cap.setText(getPcaModel().getPointsasCsv(true,
534               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
535               zCombobox.getSelectedIndex()));
536       Desktop.addInternalFrame(cap, MessageManager.formatMessage(
537               "label.transformed_points_for_params", new String[]
538               { this.getTitle() }), 500, 500);
539     } catch (OutOfMemoryError oom)
540     {
541       new OOMWarning("exporting transformed PCA points", oom);
542       cap.dispose();
543     }
544   }
545
546   @Override
547   public void setProgressBar(String message, long id)
548   {
549     progressBar.setProgressBar(message, id);
550   }
551
552   @Override
553   public void registerHandler(final long id,
554           final IProgressIndicatorHandler handler)
555   {
556     progressBar.registerHandler(id, handler);
557   }
558
559   /**
560    * 
561    * @return true if any progress bars are still active
562    */
563   @Override
564   public boolean operationInProgress()
565   {
566     return progressBar.operationInProgress();
567   }
568
569   @Override
570   protected void resetButton_actionPerformed()
571   {
572     int t = getTop();
573     setTop(0); // ugly - prevents dimensionChanged events from being processed
574     xCombobox.setSelectedIndex(0);
575     yCombobox.setSelectedIndex(1);
576     setTop(t);
577     zCombobox.setSelectedIndex(2);
578   }
579
580   /**
581    * Answers true if PCA calculation is in progress, else false
582    * 
583    * @return
584    */
585   public boolean isWorking()
586   {
587     return working;
588   }
589
590   /**
591    * Answers the selected checkbox item index for PCA dimension for the X, Y or
592    * Z axis of the display
593    * 
594    * @param axis
595    * @return
596    */
597   public int getSelectedDimensionIndex(Axis axis)
598   {
599     switch (axis)
600     {
601     case X:
602       return xCombobox.getSelectedIndex();
603     case Y:
604       return yCombobox.getSelectedIndex();
605     default:
606       return zCombobox.getSelectedIndex();
607     }
608   }
609
610   public void setShowLabels(boolean show)
611   {
612     showLabels.setSelected(show);
613   }
614
615   /**
616    * Sets the input data used to calculate the PCA. This is provided for
617    * 'restore from project', which does not currently support this (AL-2647), so
618    * sets the value to null, and hides the menu option for "Input Data...". J
619    * 
620    * @param data
621    */
622   public void setInputData(AlignmentView data)
623   {
624     getPcaModel().setInputData(data);
625     originalSeqData.setVisible(data != null);
626   }
627
628   public AlignViewportI getAlignViewport()
629   {
630     return av;
631   }
632
633   public PCAModel getPcaModel()
634   {
635     return pcaModel;
636   }
637
638   public void setPcaModel(PCAModel pcaModel)
639   {
640     this.pcaModel = pcaModel;
641   }
642
643   public RotatableCanvas getRotatableCanvas()
644   {
645     return rc;
646   }
647
648   public void setRotatableCanvas(RotatableCanvas rc)
649   {
650     this.rc = rc;
651   }
652
653   public int getTop()
654   {
655     return top;
656   }
657
658   public void setTop(int top)
659   {
660     this.top = top;
661   }
662
663   /**
664    * set the associated view for this PCA.
665    * 
666    * @param panel
667    */
668   public void selectAssociatedView(AlignmentPanel panel)
669   {
670     getRotatableCanvas().setApplyToAllViews(false);
671
672     ap = panel;
673     av = panel.av;
674
675     getRotatableCanvas().av = panel.av;
676     getRotatableCanvas().ap = panel;
677     PaintRefresher.Register(PCAPanel.this, panel.av.getSequenceSetId());
678   }
679 }