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