30604cbfa8711fcb0bf2fe47f8aa2614d5020a2a
[jalview.git] / src / jalview / gui / TreePanel.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.AlignmentSorter;
24 import jalview.analysis.AverageDistanceTree;
25 import jalview.analysis.NJTree;
26 import jalview.analysis.TreeBuilder;
27 import jalview.analysis.TreeModel;
28 import jalview.analysis.scoremodels.ScoreModels;
29 import jalview.api.analysis.ScoreModelI;
30 import jalview.api.analysis.SimilarityParamsI;
31 import jalview.commands.CommandI;
32 import jalview.commands.OrderCommand;
33 import jalview.datamodel.Alignment;
34 import jalview.datamodel.AlignmentI;
35 import jalview.datamodel.AlignmentView;
36 import jalview.datamodel.BinaryNode;
37 import jalview.datamodel.DBRefEntry;
38 import jalview.datamodel.HiddenColumns;
39 import jalview.datamodel.NodeTransformI;
40 import jalview.datamodel.SequenceFeature;
41 import jalview.datamodel.SequenceI;
42 import jalview.datamodel.SequenceNode;
43 import jalview.gui.ImageExporter.ImageWriterI;
44 import jalview.io.JalviewFileChooser;
45 import jalview.io.JalviewFileView;
46 import jalview.io.NewickFile;
47 import jalview.jbgui.GTreePanel;
48 import jalview.util.ImageMaker.TYPE;
49 import jalview.util.MessageManager;
50 import jalview.viewmodel.AlignmentViewport;
51
52 import java.awt.Font;
53 import java.awt.Graphics;
54 import java.awt.event.ActionEvent;
55 import java.awt.event.ActionListener;
56 import java.beans.PropertyChangeEvent;
57 import java.beans.PropertyChangeListener;
58 import java.io.File;
59 import java.io.FileOutputStream;
60 import java.util.ArrayList;
61 import java.util.List;
62
63 import javax.swing.ButtonGroup;
64 import javax.swing.JMenuItem;
65 import javax.swing.JRadioButtonMenuItem;
66 import javax.swing.event.InternalFrameAdapter;
67 import javax.swing.event.InternalFrameEvent;
68
69 import org.jibble.epsgraphics.EpsGraphics2D;
70
71 /**
72  * DOCUMENT ME!
73  * 
74  * @author $author$
75  * @version $Revision$
76  */
77 public class TreePanel extends GTreePanel
78 {
79   String treeType;
80
81   String scoreModelName; // if tree computed
82
83   String treeTitle; // if tree loaded
84
85   SimilarityParamsI similarityParams;
86
87   private TreeCanvas treeCanvas;
88
89   TreeModel tree;
90
91   private AlignViewport av;
92
93   /**
94    * Creates a new TreePanel object.
95    * 
96    * @param ap
97    * @param type
98    * @param modelName
99    * @param options
100    */
101   public TreePanel(AlignmentPanel ap, String type, String modelName,
102           SimilarityParamsI options)
103   {
104     super();
105     this.similarityParams = options;
106     initTreePanel(ap, type, modelName, null, null);
107
108     // We know this tree has distances. JBPNote TODO: prolly should add this as
109     // a userdefined default
110     // showDistances(true);
111   }
112
113   public TreePanel(AlignmentPanel alignPanel, NewickFile newtree,
114           String theTitle, AlignmentView inputData)
115   {
116     super();
117     this.treeTitle = theTitle;
118     initTreePanel(alignPanel, null, null, newtree, inputData);
119   }
120
121   public AlignmentI getAlignment()
122   {
123     return getTreeCanvas().getViewport().getAlignment();
124   }
125
126   public AlignmentViewport getViewPort()
127   {
128     // @Mungo - Why don't we return our own viewport ???
129     return getTreeCanvas().getViewport();
130   }
131
132   void initTreePanel(AlignmentPanel ap, String type, String modelName,
133           NewickFile newTree, AlignmentView inputData)
134   {
135
136     av = ap.av;
137     this.treeType = type;
138     this.scoreModelName = modelName;
139
140     treeCanvas = new TreeCanvas(this, ap, scrollPane);
141     scrollPane.setViewportView(treeCanvas);
142
143     PaintRefresher.Register(this, ap.av.getSequenceSetId());
144
145     buildAssociatedViewMenu();
146
147     final PropertyChangeListener listener = addAlignmentListener();
148
149     /*
150      * remove listener when window is closed, so that this
151      * panel can be garbage collected
152      */
153     addInternalFrameListener(new InternalFrameAdapter()
154     {
155       @Override
156       public void internalFrameClosed(InternalFrameEvent evt)
157       {
158         if (av != null)
159         {
160           av.removePropertyChangeListener(listener);
161         }
162       }
163     });
164
165     TreeLoader tl = new TreeLoader(newTree, inputData);
166     tl.start();
167
168   }
169
170   /**
171    * @return
172    */
173   protected PropertyChangeListener addAlignmentListener()
174   {
175     final PropertyChangeListener listener = new PropertyChangeListener()
176     {
177       @Override
178       public void propertyChange(PropertyChangeEvent evt)
179       {
180         if (evt.getPropertyName().equals("alignment"))
181         {
182           if (tree == null)
183           {
184             System.out.println("tree is null");
185             // TODO: deal with case when a change event is received whilst a
186             // tree is still being calculated - should save reference for
187             // processing message later.
188             return;
189           }
190           if (evt.getNewValue() == null)
191           {
192             System.out.println(
193                     "new alignment sequences vector value is null");
194           }
195
196           tree.updatePlaceHolders((List<SequenceI>) evt.getNewValue());
197           treeCanvas.nameHash.clear(); // reset the mapping between canvas
198           // rectangles and leafnodes
199           repaint();
200         }
201       }
202     };
203     av.addPropertyChangeListener(listener);
204     return listener;
205   }
206
207   @Override
208   public void viewMenu_menuSelected()
209   {
210     buildAssociatedViewMenu();
211   }
212
213   void buildAssociatedViewMenu()
214   {
215     AlignmentPanel[] aps = PaintRefresher
216             .getAssociatedPanels(av.getSequenceSetId());
217     if (aps.length == 1 && getTreeCanvas().getAssociatedPanel() == aps[0])
218     {
219       associateLeavesMenu.setVisible(false);
220       return;
221     }
222
223     associateLeavesMenu.setVisible(true);
224
225     if ((viewMenu
226             .getItem(viewMenu.getItemCount() - 2) instanceof JMenuItem))
227     {
228       viewMenu.insertSeparator(viewMenu.getItemCount() - 1);
229     }
230
231     associateLeavesMenu.removeAll();
232
233     JRadioButtonMenuItem item;
234     ButtonGroup buttonGroup = new ButtonGroup();
235     int i, iSize = aps.length;
236     final TreePanel thisTreePanel = this;
237     for (i = 0; i < iSize; i++)
238     {
239       final AlignmentPanel ap = aps[i];
240       item = new JRadioButtonMenuItem(ap.av.getViewName(),
241               ap == treeCanvas.getAssociatedPanel());
242       buttonGroup.add(item);
243       item.addActionListener(new ActionListener()
244       {
245         @Override
246         public void actionPerformed(ActionEvent evt)
247         {
248           treeCanvas.applyToAllViews = false;
249           treeCanvas.setAssociatedPanel(ap);
250           treeCanvas.setViewport(ap.av);
251           PaintRefresher.Register(thisTreePanel, ap.av.getSequenceSetId());
252         }
253       });
254
255       associateLeavesMenu.add(item);
256     }
257
258     final JRadioButtonMenuItem itemf = new JRadioButtonMenuItem(
259             MessageManager.getString("label.all_views"));
260     buttonGroup.add(itemf);
261     itemf.setSelected(treeCanvas.applyToAllViews);
262     itemf.addActionListener(new ActionListener()
263     {
264       @Override
265       public void actionPerformed(ActionEvent evt)
266       {
267         treeCanvas.applyToAllViews = itemf.isSelected();
268       }
269     });
270     associateLeavesMenu.add(itemf);
271
272   }
273
274   class TreeLoader extends Thread
275   {
276     private NewickFile newtree;
277
278     private AlignmentView odata = null;
279
280     public TreeLoader(NewickFile newickFile, AlignmentView inputData)
281     {
282       this.newtree = newickFile;
283       this.odata = inputData;
284
285       if (newickFile != null)
286       {
287         // Must be outside run(), as Jalview2XML tries to
288         // update distance/bootstrap visibility at the same time
289         showBootstrap(newickFile.HasBootstrap());
290         showDistances(newickFile.HasDistances());
291       }
292     }
293
294     @Override
295     public void run()
296     {
297
298       if (newtree != null)
299       {
300         tree = new TreeModel(av.getAlignment().getSequencesArray(), odata,
301                 newtree);
302         if (tree.getOriginalData() == null)
303         {
304           originalSeqData.setVisible(false);
305         }
306       }
307       else
308       {
309         ScoreModelI sm = ScoreModels.getInstance()
310                 .getScoreModel(scoreModelName,
311                         treeCanvas.getAssociatedPanel());
312         TreeBuilder njtree = treeType.equals(TreeBuilder.NEIGHBOUR_JOINING)
313                 ? new NJTree(av, sm, similarityParams)
314                 : new AverageDistanceTree(av, sm, similarityParams);
315         tree = new TreeModel(njtree);
316         showDistances(true);
317       }
318
319       tree.reCount(tree.getTopNode());
320       tree.findHeight(tree.getTopNode());
321       treeCanvas.setTree(tree);
322       treeCanvas.repaint();
323       av.setCurrentTree(tree);
324       if (av.getSortByTree())
325       {
326         sortByTree_actionPerformed();
327       }
328     }
329   }
330
331   public void showDistances(boolean b)
332   {
333     treeCanvas.setShowDistances(b);
334     distanceMenu.setSelected(b);
335   }
336
337   public void showBootstrap(boolean b)
338   {
339     treeCanvas.setShowBootstrap(b);
340     bootstrapMenu.setSelected(b);
341   }
342
343   public void showPlaceholders(boolean b)
344   {
345     placeholdersMenu.setState(b);
346     treeCanvas.setMarkPlaceholders(b);
347   }
348
349   /**
350    * DOCUMENT ME!
351    * 
352    * @return DOCUMENT ME!
353    */
354   public TreeModel getTree()
355   {
356     return tree;
357   }
358
359   /**
360    * DOCUMENT ME!
361    * 
362    * @param e
363    *          DOCUMENT ME!
364    */
365   @Override
366   public void textbox_actionPerformed(ActionEvent e)
367   {
368     CutAndPasteTransfer cap = new CutAndPasteTransfer();
369
370     String newTitle = getPanelTitle();
371
372     NewickFile fout = new NewickFile(tree.getTopNode());
373     try
374     {
375       cap.setText(fout.print(tree.hasBootstrap(), tree.hasDistances(),
376               tree.hasRootDistance()));
377       Desktop.addInternalFrame(cap, newTitle, 500, 100);
378     } catch (OutOfMemoryError oom)
379     {
380       new OOMWarning("generating newick tree file", oom);
381       cap.dispose();
382     }
383
384   }
385
386   /**
387    * DOCUMENT ME!
388    * 
389    * @param e
390    *          DOCUMENT ME!
391    */
392   @Override
393   public void saveAsNewick_actionPerformed(ActionEvent e)
394   {
395     // TODO: JAL-3048 save newick file for Jalview-JS
396     JalviewFileChooser chooser = new JalviewFileChooser(
397             jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
398     chooser.setFileView(new JalviewFileView());
399     chooser.setDialogTitle(
400             MessageManager.getString("label.save_tree_as_newick"));
401     chooser.setToolTipText(MessageManager.getString("action.save"));
402
403     int value = chooser.showSaveDialog(null);
404
405     if (value == JalviewFileChooser.APPROVE_OPTION)
406     {
407       String choice = chooser.getSelectedFile().getPath();
408       jalview.bin.Cache.setProperty("LAST_DIRECTORY",
409               chooser.getSelectedFile().getParent());
410
411       try
412       {
413         jalview.io.NewickFile fout = new jalview.io.NewickFile(
414                 tree.getTopNode());
415         String output = fout.print(tree.hasBootstrap(), tree.hasDistances(),
416                 tree.hasRootDistance());
417         java.io.PrintWriter out = new java.io.PrintWriter(
418                 new java.io.FileWriter(choice));
419         out.println(output);
420         out.close();
421       } catch (Exception ex)
422       {
423         ex.printStackTrace();
424       }
425     }
426   }
427
428   /**
429    * DOCUMENT ME!
430    * 
431    * @param e
432    *          DOCUMENT ME!
433    */
434   @Override
435   public void printMenu_actionPerformed(ActionEvent e)
436   {
437     // Putting in a thread avoids Swing painting problems
438     treeCanvas.startPrinting();
439   }
440
441   @Override
442   public void originalSeqData_actionPerformed(ActionEvent e)
443   {
444     AlignmentView originalData = tree.getOriginalData();
445     if (originalData == null)
446     {
447       jalview.bin.Cache.log.info(
448               "Unexpected call to originalSeqData_actionPerformed - should have hidden this menu action.");
449       return;
450     }
451     // decide if av alignment is sufficiently different to original data to
452     // warrant a new window to be created
453     // create new alignmnt window with hidden regions (unhiding hidden regions
454     // yields unaligned seqs)
455     // or create a selection box around columns in alignment view
456     // test Alignment(SeqCigar[])
457     char gc = '-';
458     try
459     {
460       // we try to get the associated view's gap character
461       // but this may fail if the view was closed...
462       gc = av.getGapCharacter();
463
464     } catch (Exception ex)
465     {
466     }
467
468     Object[] alAndColsel = originalData.getAlignmentAndHiddenColumns(gc);
469
470     if (alAndColsel != null && alAndColsel[0] != null)
471     {
472       // AlignmentOrder origorder = new AlignmentOrder(alAndColsel[0]);
473
474       AlignmentI al = new Alignment((SequenceI[]) alAndColsel[0]);
475       AlignmentI dataset = (av != null && av.getAlignment() != null)
476               ? av.getAlignment().getDataset()
477               : null;
478       if (dataset != null)
479       {
480         al.setDataset(dataset);
481       }
482
483       if (true)
484       {
485         // make a new frame!
486         AlignFrame af = new AlignFrame(al, (HiddenColumns) alAndColsel[1],
487                 AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
488
489         // >>>This is a fix for the moment, until a better solution is
490         // found!!<<<
491         // af.getFeatureRenderer().transferSettings(alignFrame.getFeatureRenderer());
492
493         // af.addSortByOrderMenuItem(ServiceName + " Ordering",
494         // msaorder);
495
496         Desktop.addInternalFrame(af, MessageManager.formatMessage(
497                 "label.original_data_for_params", new Object[]
498                 { this.title }), AlignFrame.DEFAULT_WIDTH,
499                 AlignFrame.DEFAULT_HEIGHT);
500       }
501     }
502   }
503
504   /**
505    * DOCUMENT ME!
506    * 
507    * @param e
508    *          DOCUMENT ME!
509    */
510   @Override
511   public void fitToWindow_actionPerformed(ActionEvent e)
512   {
513     treeCanvas.fitToWindow = fitToWindow.isSelected();
514     repaint();
515   }
516
517   /**
518    * sort the associated alignment view by the current tree.
519    * 
520    * @param e
521    */
522   @Override
523   public void sortByTree_actionPerformed()
524   {
525
526     if (treeCanvas.applyToAllViews)
527     {
528       final ArrayList<CommandI> commands = new ArrayList<>();
529       for (AlignmentPanel ap : PaintRefresher
530               .getAssociatedPanels(av.getSequenceSetId()))
531       {
532         commands.add(sortAlignmentIn(ap.av.getAlignPanel()));
533       }
534       av.getAlignPanel().alignFrame.addHistoryItem(new CommandI()
535       {
536
537         @Override
538         public void undoCommand(AlignmentI[] views)
539         {
540           for (CommandI tsort : commands)
541           {
542             tsort.undoCommand(views);
543           }
544         }
545
546         @Override
547         public int getSize()
548         {
549           return commands.size();
550         }
551
552         @Override
553         public String getDescription()
554         {
555           return "Tree Sort (many views)";
556         }
557
558         @Override
559         public void doCommand(AlignmentI[] views)
560         {
561
562           for (CommandI tsort : commands)
563           {
564             tsort.doCommand(views);
565           }
566         }
567       });
568       for (AlignmentPanel ap : PaintRefresher
569               .getAssociatedPanels(av.getSequenceSetId()))
570       {
571         // ensure all the alignFrames refresh their GI after adding an undo item
572         ap.alignFrame.updateEditMenuBar();
573       }
574     }
575     else
576     {
577       treeCanvas.getAssociatedPanel().alignFrame
578               .addHistoryItem(
579                       sortAlignmentIn(treeCanvas.getAssociatedPanel()));
580     }
581
582   }
583
584   public CommandI sortAlignmentIn(AlignmentPanel ap)
585   {
586     // TODO: move to alignment view controller
587     AlignmentViewport viewport = ap.av;
588     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
589     AlignmentSorter.sortByTree(viewport.getAlignment(), tree);
590     CommandI undo;
591     undo = new OrderCommand("Tree Sort", oldOrder, viewport.getAlignment());
592
593     ap.paintAlignment(true, false);
594     return undo;
595   }
596
597   /**
598    * DOCUMENT ME!
599    * 
600    * @param e
601    *          DOCUMENT ME!
602    */
603   @Override
604   public void font_actionPerformed(ActionEvent e)
605   {
606     if (treeCanvas == null)
607     {
608       return;
609     }
610
611     new FontChooser(this);
612   }
613
614   public Font getTreeFont()
615   {
616     return treeCanvas.font;
617   }
618
619   public void setTreeFont(Font f)
620   {
621     if (treeCanvas != null)
622     {
623       treeCanvas.setFont(f);
624     }
625   }
626
627   /**
628    * DOCUMENT ME!
629    * 
630    * @param e
631    *          DOCUMENT ME!
632    */
633   @Override
634   public void distanceMenu_actionPerformed(ActionEvent e)
635   {
636     treeCanvas.setShowDistances(distanceMenu.isSelected());
637   }
638
639   /**
640    * DOCUMENT ME!
641    * 
642    * @param e
643    *          DOCUMENT ME!
644    */
645   @Override
646   public void bootstrapMenu_actionPerformed(ActionEvent e)
647   {
648     treeCanvas.setShowBootstrap(bootstrapMenu.isSelected());
649   }
650
651   /**
652    * DOCUMENT ME!
653    * 
654    * @param e
655    *          DOCUMENT ME!
656    */
657   @Override
658   public void placeholdersMenu_actionPerformed(ActionEvent e)
659   {
660     treeCanvas.setMarkPlaceholders(placeholdersMenu.isSelected());
661   }
662
663   /**
664    * Outputs the Tree in image format (currently EPS or PNG). The user is
665    * prompted for the file to save to, and for EPS (unless a preference is
666    * already set) for the choice of Text or Lineart for character rendering.
667    */
668   @Override
669   public void writeTreeImage(TYPE imageFormat)
670   {
671     int width = treeCanvas.getWidth();
672     int height = treeCanvas.getHeight();
673     ImageWriterI writer = new ImageWriterI()
674     {
675       @Override
676       public void exportImage(Graphics g) throws Exception
677       {
678         treeCanvas.draw(g, width, height);
679       }
680     };
681     String tree = MessageManager.getString("label.tree");
682     ImageExporter exporter = new ImageExporter(writer, null, imageFormat,
683             tree);
684     exporter.doExport(null, this, width, height, tree.toLowerCase());
685   }
686
687   /**
688    * change node labels to the annotation referred to by labelClass TODO:
689    * promote to a datamodel modification that can be undone TODO: make argument
690    * one case of a generic transformation function ie { undoStep = apply(Tree,
691    * TransformFunction)};
692    * 
693    * @param labelClass
694    */
695   public void changeNames(final String labelClass)
696   {
697     tree.applyToNodes(new NodeTransformI()
698     {
699
700       @Override
701       public void transform(BinaryNode node)
702       {
703         if (node instanceof SequenceNode
704                 && !((SequenceNode) node).isPlaceholder()
705                 && !((SequenceNode) node).isDummy())
706         {
707           String newname = null;
708           SequenceI sq = (SequenceI) ((SequenceNode) node).element();
709           if (sq != null)
710           {
711             // search dbrefs, features and annotation
712             DBRefEntry[] refs = jalview.util.DBRefUtils
713                     .selectRefs(sq.getDBRefs(), new String[]
714                     { labelClass.toUpperCase() });
715             if (refs != null)
716             {
717               for (int i = 0; i < refs.length; i++)
718               {
719                 if (newname == null)
720                 {
721                   newname = new String(refs[i].getAccessionId());
722                 }
723                 else
724                 {
725                   newname = newname + "; " + refs[i].getAccessionId();
726                 }
727               }
728             }
729             if (newname == null)
730             {
731               List<SequenceFeature> features = sq.getFeatures()
732                       .getPositionalFeatures(labelClass);
733               for (SequenceFeature feature : features)
734               {
735                 if (newname == null)
736                 {
737                   newname = feature.getDescription();
738                 }
739                 else
740                 {
741                   newname = newname + "; " + feature.getDescription();
742                 }
743               }
744             }
745           }
746           if (newname != null)
747           {
748             // String oldname = ((SequenceNode) node).getName();
749             // TODO : save oldname in the undo object for this modification.
750             ((SequenceNode) node).setName(newname);
751           }
752         }
753       }
754     });
755   }
756
757   /**
758    * Formats a localised title for the tree panel, like
759    * <p>
760    * Neighbour Joining Using BLOSUM62
761    * <p>
762    * For a tree loaded from file, just uses the file name
763    * 
764    * @return
765    */
766   public String getPanelTitle()
767   {
768     if (treeTitle != null)
769     {
770       return treeTitle;
771     }
772
773     /*
774      * i18n description of Neighbour Joining or Average Distance method
775      */
776     String treecalcnm = MessageManager
777             .getString("label.tree_calc_" + treeType.toLowerCase());
778
779     /*
780      * short score model name (long description can be too long)
781      */
782     String smn = scoreModelName;
783
784     /*
785      * put them together as <method> Using <model>
786      */
787     final String ttl = MessageManager.formatMessage("label.treecalc_title",
788             treecalcnm, smn);
789     return ttl;
790   }
791
792   /**
793    * Builds an EPS image and writes it to the specified file.
794    * 
795    * @param outFile
796    * @param textOption
797    *          true for Text character rendering, false for Lineart
798    */
799   protected void writeEpsFile(File outFile, boolean textOption)
800   {
801     try
802     {
803       int width = treeCanvas.getWidth();
804       int height = treeCanvas.getHeight();
805
806       FileOutputStream out = new FileOutputStream(
807               outFile);
808       EpsGraphics2D pg = new EpsGraphics2D("Tree", out, 0, 0, width,
809               height);
810       pg.setAccurateTextMode(!textOption);
811       treeCanvas.draw(pg, width, height);
812
813       pg.flush();
814       pg.close();
815     } catch (Exception ex)
816     {
817       System.err.println("Error writing tree as EPS");
818       ex.printStackTrace();
819     }
820   }
821   
822   public AlignViewport getViewport()
823   {
824     return av;
825   }
826
827   public void setViewport(AlignViewport av)
828   {
829     this.av = av;
830   }
831
832   public TreeCanvas getTreeCanvas()
833   {
834     return treeCanvas;
835   }
836 }