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