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