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