Merge branch 'develop' into trialMerge
[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   private 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     this.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
136   }
137
138   /**
139    * Ensure references to potentially very large objects (the PCA matrices) are
140    * nulled when the frame is closed
141    */
142   protected void close_actionPerformed()
143   {
144     setPcaModel(null);
145   }
146
147   @Override
148   protected void bgcolour_actionPerformed()
149   {
150     String ttl = MessageManager.getString("label.select_background_colour");
151     ColourChooserListener listener = new ColourChooserListener()
152     {
153       @Override
154       public void colourSelected(Color c)
155       {
156         rc.setBgColour(c);
157         rc.repaint();
158       }
159     };
160     JalviewColourChooser.showColourChooser(this, ttl, rc.getBgColour(),
161             listener);
162   }
163
164   /**
165    * Calculates the PCA and displays the results
166    */
167   @Override
168   public void run()
169   {
170     working = true;
171     long progId = System.currentTimeMillis();
172     IProgressIndicator progress = this;
173     String message = MessageManager.getString("label.pca_recalculating");
174     if (getParent() == null)
175     {
176       progress = ap.alignFrame;
177       message = MessageManager.getString("label.pca_calculating");
178     }
179     progress.setProgressBar(message, progId);
180     try
181     {
182       getPcaModel().calculate();
183
184       xCombobox.setSelectedIndex(0);
185       yCombobox.setSelectedIndex(1);
186       zCombobox.setSelectedIndex(2);
187
188       getPcaModel().updateRc(getRotatableCanvas());
189       // rc.invalidate();
190       setTop(getPcaModel().getTop());
191
192     } catch (OutOfMemoryError er)
193     {
194       new OOMWarning("calculating PCA", er);
195       working = false;
196       return;
197     } finally
198     {
199       progress.setProgressBar("", progId);
200     }
201
202     repaint();
203     if (getParent() == null)
204     {
205       Desktop.addInternalFrame(this,
206               MessageManager.formatMessage("label.calc_title", "PCA",
207                       getPcaModel().getScoreModelName()),
208               475, 450);
209     }
210     working = false;
211   }
212
213   /**
214    * Updates the PCA display after a change of component to use for x, y or z
215    * axis
216    */
217   @Override
218   protected void doDimensionChange()
219   {
220     if (getTop() == 0)
221     {
222       return;
223     }
224
225     int dim1 = getTop() - xCombobox.getSelectedIndex();
226     int dim2 = getTop() - yCombobox.getSelectedIndex();
227     int dim3 = getTop() - zCombobox.getSelectedIndex();
228     getPcaModel().updateRcView(dim1, dim2, dim3);
229     getRotatableCanvas().resetView();
230   }
231
232   /**
233    * Sets the selected checkbox item index for PCA dimension (1, 2, 3...) for
234    * the given axis (X/Y/Z)
235    * 
236    * @param index
237    * @param axis
238    */
239   public void setSelectedDimensionIndex(int index, Axis axis)
240   {
241     switch (axis)
242     {
243     case X:
244       xCombobox.setSelectedIndex(index);
245       break;
246     case Y:
247       yCombobox.setSelectedIndex(index);
248       break;
249     case Z:
250       zCombobox.setSelectedIndex(index);
251       break;
252     default:
253     }
254   }
255
256   @Override
257   protected void outputValues_actionPerformed()
258   {
259     CutAndPasteTransfer cap = new CutAndPasteTransfer();
260     try
261     {
262       cap.setText(getPcaModel().getDetails());
263       Desktop.addInternalFrame(cap,
264               MessageManager.getString("label.pca_details"), 500, 500);
265     } catch (OutOfMemoryError oom)
266     {
267       new OOMWarning("opening PCA details", oom);
268       cap.dispose();
269     }
270   }
271
272   @Override
273   protected void showLabels_actionPerformed()
274   {
275     getRotatableCanvas().showLabels(showLabels.getState());
276   }
277
278   @Override
279   protected void print_actionPerformed()
280   {
281     PCAPrinter printer = new PCAPrinter();
282     printer.start();
283   }
284
285   /**
286    * If available, shows the data which formed the inputs for the PCA as a new
287    * alignment
288    */
289   @Override
290   public void originalSeqData_actionPerformed()
291   {
292     // JAL-2647 disabled after load from project (until save to project done)
293     if (getPcaModel().getInputData() == null)
294     {
295       Cache.log.info(
296               "Unexpected call to originalSeqData_actionPerformed - should have hidden this menu action.");
297       return;
298     }
299     // decide if av alignment is sufficiently different to original data to
300     // warrant a new window to be created
301     // create new alignment window with hidden regions (unhiding hidden regions
302     // yields unaligned seqs)
303     // or create a selection box around columns in alignment view
304     // test Alignment(SeqCigar[])
305     char gc = '-';
306     try
307     {
308       // we try to get the associated view's gap character
309       // but this may fail if the view was closed...
310       gc = av.getGapCharacter();
311     } catch (Exception ex)
312     {
313     }
314
315     Object[] alAndColsel = getPcaModel().getInputData()
316             .getAlignmentAndHiddenColumns(gc);
317
318     if (alAndColsel != null && alAndColsel[0] != null)
319     {
320       // AlignmentOrder origorder = new AlignmentOrder(alAndColsel[0]);
321
322       AlignmentI al = new Alignment((SequenceI[]) alAndColsel[0]);
323       AlignmentI dataset = (av != null && av.getAlignment() != null)
324               ? av.getAlignment().getDataset()
325               : null;
326       if (dataset != null)
327       {
328         al.setDataset(dataset);
329       }
330
331       if (true)
332       {
333         // make a new frame!
334         AlignFrame af = new AlignFrame(al, (HiddenColumns) alAndColsel[1],
335                 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
336
337         // >>>This is a fix for the moment, until a better solution is
338         // found!!<<<
339         // af.getFeatureRenderer().transferSettings(alignFrame.getFeatureRenderer());
340
341         // af.addSortByOrderMenuItem(ServiceName + " Ordering",
342         // msaorder);
343
344         Desktop.addInternalFrame(af, MessageManager.formatMessage(
345                 "label.original_data_for_params", new String[]
346                 { this.title }), AlignFrame.DEFAULT_WIDTH,
347                 AlignFrame.DEFAULT_HEIGHT);
348       }
349     }
350     /*
351      * CutAndPasteTransfer cap = new CutAndPasteTransfer(); for (int i = 0; i <
352      * seqs.length; i++) { cap.appendText(new jalview.util.Format("%-" + 15 +
353      * "s").form( seqs[i].getName())); cap.appendText(" " + seqstrings[i] +
354      * "\n"); }
355      * 
356      * Desktop.addInternalFrame(cap, "Original Data", 400, 400);
357      */
358   }
359
360   class PCAPrinter extends Thread implements Printable
361   {
362     @Override
363     public void run()
364     {
365       PrinterJob printJob = PrinterJob.getPrinterJob();
366       PageFormat defaultPage = printJob.defaultPage();
367       PageFormat pf = printJob.pageDialog(defaultPage);
368
369       if (defaultPage == pf)
370       {
371         /*
372          * user cancelled
373          */
374         return;
375       }
376
377       printJob.setPrintable(this, pf);
378
379       if (printJob.printDialog())
380       {
381         try
382         {
383           printJob.print();
384         } catch (Exception PrintException)
385         {
386           PrintException.printStackTrace();
387         }
388       }
389     }
390
391     @Override
392     public int print(Graphics pg, PageFormat pf, int pi)
393             throws PrinterException
394     {
395       pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
396
397       getRotatableCanvas().drawBackground(pg);
398       getRotatableCanvas().drawScene(pg);
399       if (getRotatableCanvas().drawAxes)
400       {
401         getRotatableCanvas().drawAxes(pg);
402       }
403
404       if (pi == 0)
405       {
406         return Printable.PAGE_EXISTS;
407       }
408       else
409       {
410         return Printable.NO_SUCH_PAGE;
411       }
412     }
413   }
414
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   /*
509    * (non-Javadoc)
510    * 
511    * @see
512    * jalview.jbgui.GPCAPanel#outputPoints_actionPerformed(java.awt.event.ActionEvent
513    * )
514    */
515   @Override
516   protected void outputPoints_actionPerformed()
517   {
518     CutAndPasteTransfer cap = new CutAndPasteTransfer();
519     try
520     {
521       cap.setText(getPcaModel().getPointsasCsv(false,
522               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
523               zCombobox.getSelectedIndex()));
524       Desktop.addInternalFrame(cap, MessageManager
525               .formatMessage("label.points_for_params", new String[]
526               { this.getTitle() }), 500, 500);
527     } catch (OutOfMemoryError oom)
528     {
529       new OOMWarning("exporting PCA points", oom);
530       cap.dispose();
531     }
532   }
533
534   /*
535    * (non-Javadoc)
536    * 
537    * @see
538    * jalview.jbgui.GPCAPanel#outputProjPoints_actionPerformed(java.awt.event
539    * .ActionEvent)
540    */
541   @Override
542   protected void outputProjPoints_actionPerformed()
543   {
544     CutAndPasteTransfer cap = new CutAndPasteTransfer();
545     try
546     {
547       cap.setText(getPcaModel().getPointsasCsv(true,
548               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
549               zCombobox.getSelectedIndex()));
550       Desktop.addInternalFrame(cap, MessageManager.formatMessage(
551               "label.transformed_points_for_params", new String[]
552               { this.getTitle() }), 500, 500);
553     } catch (OutOfMemoryError oom)
554     {
555       new OOMWarning("exporting transformed PCA points", oom);
556       cap.dispose();
557     }
558   }
559
560   /*
561    * (non-Javadoc)
562    * 
563    * @see jalview.gui.IProgressIndicator#setProgressBar(java.lang.String, long)
564    */
565   @Override
566   public void setProgressBar(String message, long id)
567   {
568     progressBar.setProgressBar(message, id);
569     // if (progressBars == null)
570     // {
571     // progressBars = new Hashtable();
572     // progressBarHandlers = new Hashtable();
573     // }
574     //
575     // JPanel progressPanel;
576     // Long lId = new Long(id);
577     // GridLayout layout = (GridLayout) statusPanel.getLayout();
578     // if (progressBars.get(lId) != null)
579     // {
580     // progressPanel = (JPanel) progressBars.get(new Long(id));
581     // statusPanel.remove(progressPanel);
582     // progressBars.remove(lId);
583     // progressPanel = null;
584     // if (message != null)
585     // {
586     // statusBar.setText(message);
587     // }
588     // if (progressBarHandlers.contains(lId))
589     // {
590     // progressBarHandlers.remove(lId);
591     // }
592     // layout.setRows(layout.getRows() - 1);
593     // }
594     // else
595     // {
596     // progressPanel = new JPanel(new BorderLayout(10, 5));
597     //
598     // JProgressBar progressBar = new JProgressBar();
599     // progressBar.setIndeterminate(true);
600     //
601     // progressPanel.add(new JLabel(message), BorderLayout.WEST);
602     // progressPanel.add(progressBar, BorderLayout.CENTER);
603     //
604     // layout.setRows(layout.getRows() + 1);
605     // statusPanel.add(progressPanel);
606     //
607     // progressBars.put(lId, progressPanel);
608     // }
609     // // update GUI
610     // // setMenusForViewport();
611     // validate();
612   }
613
614   @Override
615   public void registerHandler(final long id,
616           final IProgressIndicatorHandler handler)
617   {
618     progressBar.registerHandler(id, handler);
619     // if (progressBarHandlers == null || !progressBars.contains(new Long(id)))
620     // {
621     // throw new
622     // Error(MessageManager.getString("error.call_setprogressbar_before_registering_handler"));
623     // }
624     // progressBarHandlers.put(new Long(id), handler);
625     // final JPanel progressPanel = (JPanel) progressBars.get(new Long(id));
626     // if (handler.canCancel())
627     // {
628     // JButton cancel = new JButton(
629     // MessageManager.getString("action.cancel"));
630     // final IProgressIndicator us = this;
631     // cancel.addActionListener(new ActionListener()
632     // {
633     //
634     // @Override
635     // public void actionPerformed(ActionEvent e)
636     // {
637     // handler.cancelActivity(id);
638     // us.setProgressBar(MessageManager.formatMessage("label.cancelled_params",
639     // new String[]{((JLabel) progressPanel.getComponent(0)).getText()}), id);
640     // }
641     // });
642     // progressPanel.add(cancel, BorderLayout.EAST);
643     // }
644   }
645
646   /**
647    * 
648    * @return true if any progress bars are still active
649    */
650   @Override
651   public boolean operationInProgress()
652   {
653     return progressBar.operationInProgress();
654   }
655
656   @Override
657   protected void resetButton_actionPerformed()
658   {
659     int t = getTop();
660     setTop(0); // ugly - prevents dimensionChanged events from being processed
661     xCombobox.setSelectedIndex(0);
662     yCombobox.setSelectedIndex(1);
663     setTop(t);
664     zCombobox.setSelectedIndex(2);
665   }
666
667   /**
668    * Answers true if PCA calculation is in progress, else false
669    * 
670    * @return
671    */
672   public boolean isWorking()
673   {
674     return working;
675   }
676
677   /**
678    * Answers the selected checkbox item index for PCA dimension for the X, Y or
679    * Z axis of the display
680    * 
681    * @param axis
682    * @return
683    */
684   public int getSelectedDimensionIndex(Axis axis)
685   {
686     switch (axis)
687     {
688     case X:
689       return xCombobox.getSelectedIndex();
690     case Y:
691       return yCombobox.getSelectedIndex();
692     default:
693       return zCombobox.getSelectedIndex();
694     }
695   }
696
697   public void setShowLabels(boolean show)
698   {
699     showLabels.setSelected(show);
700   }
701
702   /**
703    * Sets the input data used to calculate the PCA. This is provided for
704    * 'restore from project', which does not currently support this (AL-2647), so
705    * sets the value to null, and hides the menu option for "Input Data...". J
706    * 
707    * @param data
708    */
709   public void setInputData(AlignmentView data)
710   {
711     getPcaModel().setInputData(data);
712     originalSeqData.setVisible(data != null);
713   }
714
715   public AlignViewportI getAlignViewport()
716   {
717     return av;
718   }
719
720   public PCAModel getPcaModel()
721   {
722     return pcaModel;
723   }
724
725   public void setPcaModel(PCAModel pcaModel)
726   {
727     this.pcaModel = pcaModel;
728   }
729
730   public RotatableCanvas getRotatableCanvas()
731   {
732     return rc;
733   }
734
735   public void setRotatableCanvas(RotatableCanvas rc)
736   {
737     this.rc = rc;
738   }
739
740   public int getTop()
741   {
742     return top;
743   }
744
745   public void setTop(int top)
746   {
747     this.top = top;
748   }
749
750   /**
751    * set the associated view for this PCA.
752    * 
753    * @param panel
754    */
755   public void selectAssociatedView(AlignmentPanel panel)
756   {
757     getRotatableCanvas().setApplyToAllViews(false);
758
759     ap = panel;
760     av = panel.av;
761
762     getRotatableCanvas().av = panel.av;
763     getRotatableCanvas().ap = panel;
764     PaintRefresher.Register(PCAPanel.this, panel.av.getSequenceSetId());
765   }
766 }