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