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