Merge branch 'feature/JAL-4159_pasimap' into spike/JAL-4159_pasimap_2113_series
[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     {
445       exporter.doExport(null, this, width, height, pca);
446     } catch (ImageOutputException ioex)
447     {
448       Console.error("Unexpected error whilst writing " + type.toString(),
449               ioex);
450     }
451   }
452
453   @Override
454   protected void viewMenu_menuSelected()
455   {
456     buildAssociatedViewMenu();
457   }
458
459   /**
460    * Builds the menu showing the choice of possible views (for the associated
461    * sequence data) to which the PCA may be linked
462    */
463   void buildAssociatedViewMenu()
464   {
465     AlignmentPanel[] aps = PaintRefresher
466             .getAssociatedPanels(av.getSequenceSetId());
467     if (aps.length == 1 && getRotatableCanvas().av == aps[0].av)
468     {
469       associateViewsMenu.setVisible(false);
470       return;
471     }
472
473     associateViewsMenu.setVisible(true);
474
475     if ((viewMenu
476             .getItem(viewMenu.getItemCount() - 2) instanceof JMenuItem))
477     {
478       viewMenu.insertSeparator(viewMenu.getItemCount() - 1);
479     }
480
481     associateViewsMenu.removeAll();
482
483     JRadioButtonMenuItem item;
484     ButtonGroup buttonGroup = new ButtonGroup();
485     int iSize = aps.length;
486
487     for (int i = 0; i < iSize; i++)
488     {
489       final AlignmentPanel panel = aps[i];
490       item = new JRadioButtonMenuItem(panel.av.getViewName(),
491               panel.av == getRotatableCanvas().av);
492       buttonGroup.add(item);
493       item.addActionListener(new ActionListener()
494       {
495         @Override
496         public void actionPerformed(ActionEvent evt)
497         {
498           selectAssociatedView(panel);
499         }
500       });
501
502       associateViewsMenu.add(item);
503     }
504
505     final JRadioButtonMenuItem itemf = new JRadioButtonMenuItem(
506             "All Views");
507
508     buttonGroup.add(itemf);
509
510     itemf.setSelected(getRotatableCanvas().isApplyToAllViews());
511     itemf.addActionListener(new ActionListener()
512     {
513       @Override
514       public void actionPerformed(ActionEvent evt)
515       {
516         getRotatableCanvas().setApplyToAllViews(itemf.isSelected());
517       }
518     });
519     associateViewsMenu.add(itemf);
520
521   }
522
523   /*
524    * (non-Javadoc)
525    * 
526    * @see
527    * jalview.jbgui.GPCAPanel#outputPoints_actionPerformed(java.awt.event.ActionEvent
528    * )
529    */
530   @Override
531   protected void outputPoints_actionPerformed()
532   {
533     CutAndPasteTransfer cap = new CutAndPasteTransfer();
534     try
535     {
536       cap.setText(getPcaModel().getPointsasCsv(false,
537               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
538               zCombobox.getSelectedIndex()));
539       Desktop.addInternalFrame(cap, MessageManager
540               .formatMessage("label.points_for_params", new String[]
541               { this.getTitle() }), 500, 500);
542     } catch (OutOfMemoryError oom)
543     {
544       new OOMWarning("exporting PCA points", oom);
545       cap.dispose();
546     }
547   }
548
549   /*
550    * (non-Javadoc)
551    * 
552    * @see
553    * jalview.jbgui.GPCAPanel#outputProjPoints_actionPerformed(java.awt.event
554    * .ActionEvent)
555    */
556   @Override
557   protected void outputProjPoints_actionPerformed()
558   {
559     CutAndPasteTransfer cap = new CutAndPasteTransfer();
560     try
561     {
562       cap.setText(getPcaModel().getPointsasCsv(true,
563               xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
564               zCombobox.getSelectedIndex()));
565       Desktop.addInternalFrame(cap, MessageManager.formatMessage(
566               "label.transformed_points_for_params", new String[]
567               { this.getTitle() }), 500, 500);
568     } catch (OutOfMemoryError oom)
569     {
570       new OOMWarning("exporting transformed PCA points", oom);
571       cap.dispose();
572     }
573   }
574
575   /*
576    * (non-Javadoc)
577    * 
578    * @see jalview.gui.IProgressIndicator#setProgressBar(java.lang.String, long)
579    */
580   @Override
581   public void setProgressBar(String message, long id)
582   {
583     progressBar.setProgressBar(message, id);
584     // if (progressBars == null)
585     // {
586     // progressBars = new Hashtable();
587     // progressBarHandlers = new Hashtable();
588     // }
589     //
590     // JPanel progressPanel;
591     // Long lId = Long.valueOf(id);
592     // GridLayout layout = (GridLayout) statusPanel.getLayout();
593     // if (progressBars.get(lId) != null)
594     // {
595     // progressPanel = (JPanel) progressBars.get(Long.valueOf(id));
596     // statusPanel.remove(progressPanel);
597     // progressBars.remove(lId);
598     // progressPanel = null;
599     // if (message != null)
600     // {
601     // statusBar.setText(message);
602     // }
603     // if (progressBarHandlers.contains(lId))
604     // {
605     // progressBarHandlers.remove(lId);
606     // }
607     // layout.setRows(layout.getRows() - 1);
608     // }
609     // else
610     // {
611     // progressPanel = new JPanel(new BorderLayout(10, 5));
612     //
613     // JProgressBar progressBar = new JProgressBar();
614     // progressBar.setIndeterminate(true);
615     //
616     // progressPanel.add(new JLabel(message), BorderLayout.WEST);
617     // progressPanel.add(progressBar, BorderLayout.CENTER);
618     //
619     // layout.setRows(layout.getRows() + 1);
620     // statusPanel.add(progressPanel);
621     //
622     // progressBars.put(lId, progressPanel);
623     // }
624     // // update GUI
625     // // setMenusForViewport();
626     // validate();
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   @Override
784   public JProgressBar getProgressBar(long id)
785   {
786     return progressBar.getProgressBar(id);
787   }
788 }