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