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