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