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