JAL-3878 Introduce IProgressIndicator#addProgressBar
[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 addProgressBar(long id, String message)
613   {
614     progressBar.addProgressBar(id, message);
615   }
616
617   @Override
618   public void removeProgressBar(long id)
619   {
620     progressBar.removeProgressBar(id);
621   }
622
623   @Override
624   public void registerHandler(final long id,
625           final IProgressIndicatorHandler handler)
626   {
627     progressBar.registerHandler(id, handler);
628     // if (progressBarHandlers == null || !progressBars.contains(Long.valueOf(id)))
629     // {
630     // throw new
631     // Error(MessageManager.getString("error.call_setprogressbar_before_registering_handler"));
632     // }
633     // progressBarHandlers.put(Long.valueOf(id), handler);
634     // final JPanel progressPanel = (JPanel) progressBars.get(Long.valueOf(id));
635     // if (handler.canCancel())
636     // {
637     // JButton cancel = new JButton(
638     // MessageManager.getString("action.cancel"));
639     // final IProgressIndicator us = this;
640     // cancel.addActionListener(new ActionListener()
641     // {
642     //
643     // @Override
644     // public void actionPerformed(ActionEvent e)
645     // {
646     // handler.cancelActivity(id);
647     // us.setProgressBar(MessageManager.formatMessage("label.cancelled_params",
648     // new String[]{((JLabel) progressPanel.getComponent(0)).getText()}), id);
649     // }
650     // });
651     // progressPanel.add(cancel, BorderLayout.EAST);
652     // }
653   }
654
655   /**
656    * 
657    * @return true if any progress bars are still active
658    */
659   @Override
660   public boolean operationInProgress()
661   {
662     return progressBar.operationInProgress();
663   }
664
665   @Override
666   protected void resetButton_actionPerformed()
667   {
668     int t = getTop();
669     setTop(0); // ugly - prevents dimensionChanged events from being processed
670     xCombobox.setSelectedIndex(0);
671     yCombobox.setSelectedIndex(1);
672     setTop(t);
673     zCombobox.setSelectedIndex(2);
674   }
675
676   /**
677    * Answers true if PCA calculation is in progress, else false
678    * 
679    * @return
680    */
681   public boolean isWorking()
682   {
683     return working;
684   }
685
686   /**
687    * Answers the selected checkbox item index for PCA dimension for the X, Y or
688    * Z axis of the display
689    * 
690    * @param axis
691    * @return
692    */
693   public int getSelectedDimensionIndex(Axis axis)
694   {
695     switch (axis)
696     {
697     case X:
698       return xCombobox.getSelectedIndex();
699     case Y:
700       return yCombobox.getSelectedIndex();
701     default:
702       return zCombobox.getSelectedIndex();
703     }
704   }
705
706   public void setShowLabels(boolean show)
707   {
708     showLabels.setSelected(show);
709   }
710
711   /**
712    * Sets the input data used to calculate the PCA. This is provided for
713    * 'restore from project', which does not currently support this (AL-2647), so
714    * sets the value to null, and hides the menu option for "Input Data...". J
715    * 
716    * @param data
717    */
718   public void setInputData(AlignmentView data)
719   {
720     getPcaModel().setInputData(data);
721     originalSeqData.setVisible(data != null);
722   }
723
724   public AlignViewportI getAlignViewport()
725   {
726     return av;
727   }
728
729   public PCAModel getPcaModel()
730   {
731     return pcaModel;
732   }
733
734   public void setPcaModel(PCAModel pcaModel)
735   {
736     this.pcaModel = pcaModel;
737   }
738
739   public RotatableCanvas getRotatableCanvas()
740   {
741     return rc;
742   }
743
744   public void setRotatableCanvas(RotatableCanvas rc)
745   {
746     this.rc = rc;
747   }
748
749   public int getTop()
750   {
751     return top;
752   }
753
754   public void setTop(int top)
755   {
756     this.top = top;
757   }
758
759   /**
760    * set the associated view for this PCA.
761    * 
762    * @param panel
763    */
764   public void selectAssociatedView(AlignmentPanel panel)
765   {
766     getRotatableCanvas().setApplyToAllViews(false);
767
768     ap = panel;
769     av = panel.av;
770
771     getRotatableCanvas().av = panel.av;
772     getRotatableCanvas().ap = panel;
773     PaintRefresher.Register(PCAPanel.this, panel.av.getSequenceSetId());
774   }
775
776   public static void addToDesktop(PCAPanel panel, String modelName)
777   {
778     Dimension dim = Platform.getDimIfEmbedded(panel, 475, 450);
779     Desktop.addInternalFrame(panel, MessageManager.formatMessage(
780             "label.calc_title", "PCA", modelName), dim.width,
781             dim.height);
782   }
783 }