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