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