Patch fixes JAL-4165 - added to release notes JAL-4351
[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     {
444       exporter.doExport(null, this, width, height, pca);
445     } catch (ImageOutputException ioex)
446     {
447       Console.error("Unexpected error whilst writing " + type.toString(),
448               ioex);
449     }
450   }
451
452   @Override
453   protected void viewMenu_menuSelected()
454   {
455     buildAssociatedViewMenu();
456   }
457
458   /**
459    * Builds the menu showing the choice of possible views (for the associated
460    * sequence data) to which the PCA may be linked
461    */
462   void buildAssociatedViewMenu()
463   {
464     AlignmentPanel[] aps = PaintRefresher
465             .getAssociatedPanels(av.getSequenceSetId());
466     if (aps.length == 1 && getRotatableCanvas().av == aps[0].av)
467     {
468       associateViewsMenu.setVisible(false);
469       return;
470     }
471
472     associateViewsMenu.setVisible(true);
473
474     if ((viewMenu
475             .getItem(viewMenu.getItemCount() - 2) instanceof JMenuItem))
476     {
477       viewMenu.insertSeparator(viewMenu.getItemCount() - 1);
478     }
479
480     associateViewsMenu.removeAll();
481
482     JRadioButtonMenuItem item;
483     ButtonGroup buttonGroup = new ButtonGroup();
484     int iSize = aps.length;
485
486     for (int i = 0; i < iSize; i++)
487     {
488       final AlignmentPanel panel = aps[i];
489       item = new JRadioButtonMenuItem(panel.av.getViewName(),
490               panel.av == getRotatableCanvas().av);
491       buttonGroup.add(item);
492       item.addActionListener(new ActionListener()
493       {
494         @Override
495         public void actionPerformed(ActionEvent evt)
496         {
497           selectAssociatedView(panel);
498         }
499       });
500
501       associateViewsMenu.add(item);
502     }
503
504     final JRadioButtonMenuItem itemf = new JRadioButtonMenuItem(
505             "All Views");
506
507     buttonGroup.add(itemf);
508
509     itemf.setSelected(getRotatableCanvas().isApplyToAllViews());
510     itemf.addActionListener(new ActionListener()
511     {
512       @Override
513       public void actionPerformed(ActionEvent evt)
514       {
515         getRotatableCanvas().setApplyToAllViews(itemf.isSelected());
516       }
517     });
518     associateViewsMenu.add(itemf);
519
520   }
521
522   /*
523    * (non-Javadoc)
524    * 
525    * @see
526    * jalview.jbgui.GPCAPanel#outputPoints_actionPerformed(java.awt.event.ActionEvent
527    * )
528    */
529   @Override
530   protected void outputPoints_actionPerformed()
531   {
532     CutAndPasteTransfer cap = new CutAndPasteTransfer();
533     try
534     {
535       cap.setText(getPcaModel().getPointsasCsv(false,
536               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
537               zCombobox.getSelectedIndex()));
538       Desktop.addInternalFrame(cap, MessageManager
539               .formatMessage("label.points_for_params", new String[]
540               { this.getTitle() }), 500, 500);
541     } catch (OutOfMemoryError oom)
542     {
543       new OOMWarning("exporting PCA points", oom);
544       cap.dispose();
545     }
546   }
547
548   /*
549    * (non-Javadoc)
550    * 
551    * @see
552    * jalview.jbgui.GPCAPanel#outputProjPoints_actionPerformed(java.awt.event
553    * .ActionEvent)
554    */
555   @Override
556   protected void outputProjPoints_actionPerformed()
557   {
558     CutAndPasteTransfer cap = new CutAndPasteTransfer();
559     try
560     {
561       cap.setText(getPcaModel().getPointsasCsv(true,
562               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
563               zCombobox.getSelectedIndex()));
564       Desktop.addInternalFrame(cap, MessageManager.formatMessage(
565               "label.transformed_points_for_params", new String[]
566               { this.getTitle() }), 500, 500);
567     } catch (OutOfMemoryError oom)
568     {
569       new OOMWarning("exporting transformed PCA points", oom);
570       cap.dispose();
571     }
572   }
573
574   /*
575    * (non-Javadoc)
576    * 
577    * @see jalview.gui.IProgressIndicator#setProgressBar(java.lang.String, long)
578    */
579   @Override
580   public void setProgressBar(String message, long id)
581   {
582     progressBar.setProgressBar(message, id);
583     // if (progressBars == null)
584     // {
585     // progressBars = new Hashtable();
586     // progressBarHandlers = new Hashtable();
587     // }
588     //
589     // JPanel progressPanel;
590     // Long lId = Long.valueOf(id);
591     // GridLayout layout = (GridLayout) statusPanel.getLayout();
592     // if (progressBars.get(lId) != null)
593     // {
594     // progressPanel = (JPanel) progressBars.get(Long.valueOf(id));
595     // statusPanel.remove(progressPanel);
596     // progressBars.remove(lId);
597     // progressPanel = null;
598     // if (message != null)
599     // {
600     // statusBar.setText(message);
601     // }
602     // if (progressBarHandlers.contains(lId))
603     // {
604     // progressBarHandlers.remove(lId);
605     // }
606     // layout.setRows(layout.getRows() - 1);
607     // }
608     // else
609     // {
610     // progressPanel = new JPanel(new BorderLayout(10, 5));
611     //
612     // JProgressBar progressBar = new JProgressBar();
613     // progressBar.setIndeterminate(true);
614     //
615     // progressPanel.add(new JLabel(message), BorderLayout.WEST);
616     // progressPanel.add(progressBar, BorderLayout.CENTER);
617     //
618     // layout.setRows(layout.getRows() + 1);
619     // statusPanel.add(progressPanel);
620     //
621     // progressBars.put(lId, progressPanel);
622     // }
623     // // update GUI
624     // // setMenusForViewport();
625     // validate();
626   }
627
628   @Override
629   public void registerHandler(final long id,
630           final IProgressIndicatorHandler handler)
631   {
632     progressBar.registerHandler(id, handler);
633     // if (progressBarHandlers == null ||
634     // !progressBars.contains(Long.valueOf(id)))
635     // {
636     // throw new
637     // Error(MessageManager.getString("error.call_setprogressbar_before_registering_handler"));
638     // }
639     // progressBarHandlers.put(Long.valueOf(id), handler);
640     // final JPanel progressPanel = (JPanel) progressBars.get(Long.valueOf(id));
641     // if (handler.canCancel())
642     // {
643     // JButton cancel = new JButton(
644     // MessageManager.getString("action.cancel"));
645     // final IProgressIndicator us = this;
646     // cancel.addActionListener(new ActionListener()
647     // {
648     //
649     // @Override
650     // public void actionPerformed(ActionEvent e)
651     // {
652     // handler.cancelActivity(id);
653     // us.setProgressBar(MessageManager.formatMessage("label.cancelled_params",
654     // new String[]{((JLabel) progressPanel.getComponent(0)).getText()}), id);
655     // }
656     // });
657     // progressPanel.add(cancel, BorderLayout.EAST);
658     // }
659   }
660
661   /**
662    * 
663    * @return true if any progress bars are still active
664    */
665   @Override
666   public boolean operationInProgress()
667   {
668     return progressBar.operationInProgress();
669   }
670
671   @Override
672   protected void resetButton_actionPerformed()
673   {
674     int t = getTop();
675     setTop(0); // ugly - prevents dimensionChanged events from being processed
676     xCombobox.setSelectedIndex(0);
677     yCombobox.setSelectedIndex(1);
678     setTop(t);
679     zCombobox.setSelectedIndex(2);
680   }
681
682   /**
683    * Answers true if PCA calculation is in progress, else false
684    * 
685    * @return
686    */
687   public boolean isWorking()
688   {
689     return working;
690   }
691
692   /**
693    * Answers the selected checkbox item index for PCA dimension for the X, Y or
694    * Z axis of the display
695    * 
696    * @param axis
697    * @return
698    */
699   public int getSelectedDimensionIndex(Axis axis)
700   {
701     switch (axis)
702     {
703     case X:
704       return xCombobox.getSelectedIndex();
705     case Y:
706       return yCombobox.getSelectedIndex();
707     default:
708       return zCombobox.getSelectedIndex();
709     }
710   }
711
712   public void setShowLabels(boolean show)
713   {
714     showLabels.setSelected(show);
715   }
716
717   /**
718    * Sets the input data used to calculate the PCA. This is provided for
719    * 'restore from project', which does not currently support this (AL-2647), so
720    * sets the value to null, and hides the menu option for "Input Data...". J
721    * 
722    * @param data
723    */
724   public void setInputData(AlignmentView data)
725   {
726     getPcaModel().setInputData(data);
727     originalSeqData.setVisible(data != null);
728   }
729
730   public AlignViewportI getAlignViewport()
731   {
732     return av;
733   }
734
735   public PCAModel getPcaModel()
736   {
737     return pcaModel;
738   }
739
740   public void setPcaModel(PCAModel pcaModel)
741   {
742     this.pcaModel = pcaModel;
743   }
744
745   public RotatableCanvas getRotatableCanvas()
746   {
747     return rc;
748   }
749
750   public void setRotatableCanvas(RotatableCanvas rc)
751   {
752     this.rc = rc;
753   }
754
755   public int getTop()
756   {
757     return top;
758   }
759
760   public void setTop(int top)
761   {
762     this.top = top;
763   }
764
765   /**
766    * set the associated view for this PCA.
767    * 
768    * @param panel
769    */
770   public void selectAssociatedView(AlignmentPanel panel)
771   {
772     getRotatableCanvas().setApplyToAllViews(false);
773
774     ap = panel;
775     av = panel.av;
776
777     getRotatableCanvas().av = panel.av;
778     getRotatableCanvas().ap = panel;
779     PaintRefresher.Register(PCAPanel.this, panel.av.getSequenceSetId());
780   }
781 }