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