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