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