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