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