Merge branch 'develop' into feature/JAL-3364splitFrameImage
[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 static jalview.util.ImageMaker.TYPE.PNG;
24
25 import jalview.analysis.scoremodels.ScoreModels;
26 import jalview.api.AlignViewportI;
27 import jalview.api.analysis.ScoreModelI;
28 import jalview.api.analysis.SimilarityParamsI;
29 import jalview.bin.Cache;
30 import jalview.datamodel.Alignment;
31 import jalview.datamodel.AlignmentI;
32 import jalview.datamodel.AlignmentView;
33 import jalview.datamodel.HiddenColumns;
34 import jalview.datamodel.SequenceI;
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.JColorChooser;
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   private 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     this.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
137   }
138
139   /**
140    * Ensure references to potentially very large objects (the PCA matrices) are
141    * nulled when the frame is closed
142    */
143   protected void close_actionPerformed()
144   {
145     setPcaModel(null);
146   }
147
148   @Override
149   protected void bgcolour_actionPerformed()
150   {
151     Color col = JColorChooser.showDialog(this,
152             MessageManager.getString("label.select_background_colour"),
153             getRotatableCanvas().getBgColour());
154
155     if (col != null)
156     {
157       getRotatableCanvas().setBgColour(col);
158     }
159     getRotatableCanvas().repaint();
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     }
208     working = false;
209   }
210
211   /**
212    * Updates the PCA display after a change of component to use for x, y or z
213    * axis
214    */
215   @Override
216   protected void doDimensionChange()
217   {
218     if (getTop() == 0)
219     {
220       return;
221     }
222
223     int dim1 = getTop() - xCombobox.getSelectedIndex();
224     int dim2 = getTop() - yCombobox.getSelectedIndex();
225     int dim3 = getTop() - zCombobox.getSelectedIndex();
226     getPcaModel().updateRcView(dim1, dim2, dim3);
227     getRotatableCanvas().resetView();
228   }
229
230   /**
231    * Sets the selected checkbox item index for PCA dimension (1, 2, 3...) for
232    * the given axis (X/Y/Z)
233    * 
234    * @param index
235    * @param axis
236    */
237   public void setSelectedDimensionIndex(int index, Axis axis)
238   {
239     switch (axis)
240     {
241     case X:
242       xCombobox.setSelectedIndex(index);
243       break;
244     case Y:
245       yCombobox.setSelectedIndex(index);
246       break;
247     case Z:
248       zCombobox.setSelectedIndex(index);
249       break;
250     default:
251     }
252   }
253
254   @Override
255   protected void outputValues_actionPerformed()
256   {
257     CutAndPasteTransfer cap = new CutAndPasteTransfer();
258     try
259     {
260       cap.setText(getPcaModel().getDetails());
261       Desktop.addInternalFrame(cap,
262               MessageManager.getString("label.pca_details"), 500, 500);
263     } catch (OutOfMemoryError oom)
264     {
265       new OOMWarning("opening PCA details", oom);
266       cap.dispose();
267     }
268   }
269
270   @Override
271   protected void showLabels_actionPerformed()
272   {
273     getRotatableCanvas().showLabels(showLabels.getState());
274   }
275
276   @Override
277   protected void print_actionPerformed()
278   {
279     PCAPrinter printer = new PCAPrinter();
280     printer.start();
281   }
282
283   /**
284    * If available, shows the data which formed the inputs for the PCA as a new
285    * alignment
286    */
287   @Override
288   public void originalSeqData_actionPerformed()
289   {
290     // JAL-2647 disabled after load from project (until save to project done)
291     if (getPcaModel().getInputData() == null)
292     {
293       Cache.log.info(
294               "Unexpected call to originalSeqData_actionPerformed - should have hidden this menu action.");
295       return;
296     }
297     // decide if av alignment is sufficiently different to original data to
298     // warrant a new window to be created
299     // create new alignment window with hidden regions (unhiding hidden regions
300     // yields unaligned seqs)
301     // or create a selection box around columns in alignment view
302     // test Alignment(SeqCigar[])
303     char gc = '-';
304     try
305     {
306       // we try to get the associated view's gap character
307       // but this may fail if the view was closed...
308       gc = av.getGapCharacter();
309     } catch (Exception ex)
310     {
311     }
312
313     Object[] alAndColsel = getPcaModel().getInputData()
314             .getAlignmentAndHiddenColumns(gc);
315
316     if (alAndColsel != null && alAndColsel[0] != null)
317     {
318       // AlignmentOrder origorder = new AlignmentOrder(alAndColsel[0]);
319
320       AlignmentI al = new Alignment((SequenceI[]) alAndColsel[0]);
321       AlignmentI dataset = (av != null && av.getAlignment() != null)
322               ? av.getAlignment().getDataset()
323               : null;
324       if (dataset != null)
325       {
326         al.setDataset(dataset);
327       }
328
329       if (true)
330       {
331         // make a new frame!
332         AlignFrame af = new AlignFrame(al, (HiddenColumns) alAndColsel[1],
333                 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
334
335         // >>>This is a fix for the moment, until a better solution is
336         // found!!<<<
337         // af.getFeatureRenderer().transferSettings(alignFrame.getFeatureRenderer());
338
339         // af.addSortByOrderMenuItem(ServiceName + " Ordering",
340         // msaorder);
341
342         Desktop.addInternalFrame(af, MessageManager.formatMessage(
343                 "label.original_data_for_params", new String[]
344                 { this.title }), AlignFrame.DEFAULT_WIDTH,
345                 AlignFrame.DEFAULT_HEIGHT);
346       }
347     }
348     /*
349      * CutAndPasteTransfer cap = new CutAndPasteTransfer(); for (int i = 0; i <
350      * seqs.length; i++) { cap.appendText(new jalview.util.Format("%-" + 15 +
351      * "s").form( seqs[i].getName())); cap.appendText(" " + seqstrings[i] +
352      * "\n"); }
353      * 
354      * Desktop.addInternalFrame(cap, "Original Data", 400, 400);
355      */
356   }
357
358   class PCAPrinter extends Thread implements Printable
359   {
360     @Override
361     public void run()
362     {
363       PrinterJob printJob = PrinterJob.getPrinterJob();
364       PageFormat defaultPage = printJob.defaultPage();
365       PageFormat pf = printJob.pageDialog(defaultPage);
366
367       if (defaultPage == pf)
368       {
369         /*
370          * user cancelled
371          */
372         return;
373       }
374
375       printJob.setPrintable(this, pf);
376
377       if (printJob.printDialog())
378       {
379         try
380         {
381           printJob.print();
382         } catch (Exception PrintException)
383         {
384           PrintException.printStackTrace();
385         }
386       }
387     }
388
389     @Override
390     public int print(Graphics pg, PageFormat pf, int pi)
391             throws PrinterException
392     {
393       pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
394
395       getRotatableCanvas().drawBackground(pg);
396       getRotatableCanvas().drawScene(pg);
397       if (getRotatableCanvas().drawAxes)
398       {
399         getRotatableCanvas().drawAxes(pg);
400       }
401
402       if (pi == 0)
403       {
404         return Printable.PAGE_EXISTS;
405       }
406       else
407       {
408         return Printable.NO_SUCH_PAGE;
409       }
410     }
411   }
412
413   /**
414    * Handler for 'Save as EPS' option
415    */
416   @Override
417   protected void eps_actionPerformed()
418   {
419     makePCAImage(ImageMaker.TYPE.EPS);
420   }
421
422   /**
423    * Handler for 'Save as PNG' option
424    */
425   @Override
426   protected void png_actionPerformed()
427   {
428     makePCAImage(ImageMaker.TYPE.PNG);
429   }
430
431   void makePCAImage(ImageMaker.TYPE type)
432   {
433     int width = getRotatableCanvas().getWidth();
434     int height = getRotatableCanvas().getHeight();
435
436     String dialogTitle = MessageManager
437             .formatMessage("label.make_pca_image", type.getName());
438     String imageTitle = type == PNG ? null : this.getTitle();
439     ImageMaker im = new ImageMaker(this, type, dialogTitle, width, height,
440             null, imageTitle, null, 0, false);
441
442     if (im.getGraphics() != null)
443     {
444       getRotatableCanvas().drawBackground(im.getGraphics());
445       getRotatableCanvas().drawScene(im.getGraphics());
446       if (getRotatableCanvas().drawAxes)
447       {
448         getRotatableCanvas().drawAxes(im.getGraphics());
449       }
450       im.writeImage();
451     }
452   }
453
454   @Override
455   protected void viewMenu_menuSelected()
456   {
457     buildAssociatedViewMenu();
458   }
459
460   /**
461    * Builds the menu showing the choice of possible views (for the associated
462    * sequence data) to which the PCA may be linked
463    */
464   void buildAssociatedViewMenu()
465   {
466     AlignmentPanel[] aps = PaintRefresher
467             .getAssociatedPanels(av.getSequenceSetId());
468     if (aps.length == 1 && getRotatableCanvas().av == aps[0].av)
469     {
470       associateViewsMenu.setVisible(false);
471       return;
472     }
473
474     associateViewsMenu.setVisible(true);
475
476     if ((viewMenu
477             .getItem(viewMenu.getItemCount() - 2) instanceof JMenuItem))
478     {
479       viewMenu.insertSeparator(viewMenu.getItemCount() - 1);
480     }
481
482     associateViewsMenu.removeAll();
483
484     JRadioButtonMenuItem item;
485     ButtonGroup buttonGroup = new ButtonGroup();
486     int iSize = aps.length;
487
488     for (int i = 0; i < iSize; i++)
489     {
490       final AlignmentPanel panel = aps[i];
491       item = new JRadioButtonMenuItem(panel.av.getViewName(),
492               panel.av == getRotatableCanvas().av);
493       buttonGroup.add(item);
494       item.addActionListener(new ActionListener()
495       {
496         @Override
497         public void actionPerformed(ActionEvent evt)
498         {
499           selectAssociatedView(panel);
500         }
501       });
502
503       associateViewsMenu.add(item);
504     }
505
506     final JRadioButtonMenuItem itemf = new JRadioButtonMenuItem(
507             "All Views");
508
509     buttonGroup.add(itemf);
510
511     itemf.setSelected(getRotatableCanvas().isApplyToAllViews());
512     itemf.addActionListener(new ActionListener()
513     {
514       @Override
515       public void actionPerformed(ActionEvent evt)
516       {
517         getRotatableCanvas().setApplyToAllViews(itemf.isSelected());
518       }
519     });
520     associateViewsMenu.add(itemf);
521
522   }
523
524   /*
525    * (non-Javadoc)
526    * 
527    * @see
528    * jalview.jbgui.GPCAPanel#outputPoints_actionPerformed(java.awt.event.ActionEvent
529    * )
530    */
531   @Override
532   protected void outputPoints_actionPerformed()
533   {
534     CutAndPasteTransfer cap = new CutAndPasteTransfer();
535     try
536     {
537       cap.setText(getPcaModel().getPointsasCsv(false,
538               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
539               zCombobox.getSelectedIndex()));
540       Desktop.addInternalFrame(cap, MessageManager
541               .formatMessage("label.points_for_params", new String[]
542               { this.getTitle() }), 500, 500);
543     } catch (OutOfMemoryError oom)
544     {
545       new OOMWarning("exporting PCA points", oom);
546       cap.dispose();
547     }
548   }
549
550   /*
551    * (non-Javadoc)
552    * 
553    * @see
554    * jalview.jbgui.GPCAPanel#outputProjPoints_actionPerformed(java.awt.event
555    * .ActionEvent)
556    */
557   @Override
558   protected void outputProjPoints_actionPerformed()
559   {
560     CutAndPasteTransfer cap = new CutAndPasteTransfer();
561     try
562     {
563       cap.setText(getPcaModel().getPointsasCsv(true,
564               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
565               zCombobox.getSelectedIndex()));
566       Desktop.addInternalFrame(cap, MessageManager.formatMessage(
567               "label.transformed_points_for_params", new String[]
568               { this.getTitle() }), 500, 500);
569     } catch (OutOfMemoryError oom)
570     {
571       new OOMWarning("exporting transformed PCA points", oom);
572       cap.dispose();
573     }
574   }
575
576   /*
577    * (non-Javadoc)
578    * 
579    * @see jalview.gui.IProgressIndicator#setProgressBar(java.lang.String, long)
580    */
581   @Override
582   public void setProgressBar(String message, long id)
583   {
584     progressBar.setProgressBar(message, id);
585     // if (progressBars == null)
586     // {
587     // progressBars = new Hashtable();
588     // progressBarHandlers = new Hashtable();
589     // }
590     //
591     // JPanel progressPanel;
592     // Long lId = Long.valueOf(id);
593     // GridLayout layout = (GridLayout) statusPanel.getLayout();
594     // if (progressBars.get(lId) != null)
595     // {
596     // progressPanel = (JPanel) progressBars.get(Long.valueOf(id));
597     // statusPanel.remove(progressPanel);
598     // progressBars.remove(lId);
599     // progressPanel = null;
600     // if (message != null)
601     // {
602     // statusBar.setText(message);
603     // }
604     // if (progressBarHandlers.contains(lId))
605     // {
606     // progressBarHandlers.remove(lId);
607     // }
608     // layout.setRows(layout.getRows() - 1);
609     // }
610     // else
611     // {
612     // progressPanel = new JPanel(new BorderLayout(10, 5));
613     //
614     // JProgressBar progressBar = new JProgressBar();
615     // progressBar.setIndeterminate(true);
616     //
617     // progressPanel.add(new JLabel(message), BorderLayout.WEST);
618     // progressPanel.add(progressBar, BorderLayout.CENTER);
619     //
620     // layout.setRows(layout.getRows() + 1);
621     // statusPanel.add(progressPanel);
622     //
623     // progressBars.put(lId, progressPanel);
624     // }
625     // // update GUI
626     // // setMenusForViewport();
627     // validate();
628   }
629
630   @Override
631   public void registerHandler(final long id,
632           final IProgressIndicatorHandler handler)
633   {
634     progressBar.registerHandler(id, handler);
635     // if (progressBarHandlers == null || !progressBars.contains(Long.valueOf(id)))
636     // {
637     // throw new
638     // Error(MessageManager.getString("error.call_setprogressbar_before_registering_handler"));
639     // }
640     // progressBarHandlers.put(Long.valueOf(id), handler);
641     // final JPanel progressPanel = (JPanel) progressBars.get(Long.valueOf(id));
642     // if (handler.canCancel())
643     // {
644     // JButton cancel = new JButton(
645     // MessageManager.getString("action.cancel"));
646     // final IProgressIndicator us = this;
647     // cancel.addActionListener(new ActionListener()
648     // {
649     //
650     // @Override
651     // public void actionPerformed(ActionEvent e)
652     // {
653     // handler.cancelActivity(id);
654     // us.setProgressBar(MessageManager.formatMessage("label.cancelled_params",
655     // new String[]{((JLabel) progressPanel.getComponent(0)).getText()}), id);
656     // }
657     // });
658     // progressPanel.add(cancel, BorderLayout.EAST);
659     // }
660   }
661
662   /**
663    * 
664    * @return true if any progress bars are still active
665    */
666   @Override
667   public boolean operationInProgress()
668   {
669     return progressBar.operationInProgress();
670   }
671
672   @Override
673   protected void resetButton_actionPerformed()
674   {
675     int t = getTop();
676     setTop(0); // ugly - prevents dimensionChanged events from being processed
677     xCombobox.setSelectedIndex(0);
678     yCombobox.setSelectedIndex(1);
679     setTop(t);
680     zCombobox.setSelectedIndex(2);
681   }
682
683   /**
684    * Answers true if PCA calculation is in progress, else false
685    * 
686    * @return
687    */
688   public boolean isWorking()
689   {
690     return working;
691   }
692
693   /**
694    * Answers the selected checkbox item index for PCA dimension for the X, Y or
695    * Z axis of the display
696    * 
697    * @param axis
698    * @return
699    */
700   public int getSelectedDimensionIndex(Axis axis)
701   {
702     switch (axis)
703     {
704     case X:
705       return xCombobox.getSelectedIndex();
706     case Y:
707       return yCombobox.getSelectedIndex();
708     default:
709       return zCombobox.getSelectedIndex();
710     }
711   }
712
713   public void setShowLabels(boolean show)
714   {
715     showLabels.setSelected(show);
716   }
717
718   /**
719    * Sets the input data used to calculate the PCA. This is provided for
720    * 'restore from project', which does not currently support this (AL-2647), so
721    * sets the value to null, and hides the menu option for "Input Data...". J
722    * 
723    * @param data
724    */
725   public void setInputData(AlignmentView data)
726   {
727     getPcaModel().setInputData(data);
728     originalSeqData.setVisible(data != null);
729   }
730
731   public AlignViewportI getAlignViewport()
732   {
733     return av;
734   }
735
736   public PCAModel getPcaModel()
737   {
738     return pcaModel;
739   }
740
741   public void setPcaModel(PCAModel pcaModel)
742   {
743     this.pcaModel = pcaModel;
744   }
745
746   public RotatableCanvas getRotatableCanvas()
747   {
748     return rc;
749   }
750
751   public void setRotatableCanvas(RotatableCanvas rc)
752   {
753     this.rc = rc;
754   }
755
756   public int getTop()
757   {
758     return top;
759   }
760
761   public void setTop(int top)
762   {
763     this.top = top;
764   }
765
766   /**
767    * set the associated view for this PCA.
768    * 
769    * @param panel
770    */
771   public void selectAssociatedView(AlignmentPanel panel)
772   {
773     getRotatableCanvas().setApplyToAllViews(false);
774
775     ap = panel;
776     av = panel.av;
777
778     getRotatableCanvas().av = panel.av;
779     getRotatableCanvas().ap = panel;
780     PaintRefresher.Register(PCAPanel.this, panel.av.getSequenceSetId());
781   }
782 }