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