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