JAL-1551 formatting
[jalview.git] / src / jalview / appletgui / TreeCanvas.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2)
3  * Copyright (C) 2014 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.appletgui;
22
23 import jalview.analysis.Conservation;
24 import jalview.analysis.NJTree;
25 import jalview.datamodel.Sequence;
26 import jalview.datamodel.SequenceGroup;
27 import jalview.datamodel.SequenceI;
28 import jalview.datamodel.SequenceNode;
29 import jalview.schemes.ColourSchemeI;
30 import jalview.schemes.ColourSchemeProperty;
31 import jalview.schemes.ResidueProperties;
32 import jalview.schemes.UserColourScheme;
33 import jalview.util.Format;
34
35 import java.awt.Color;
36 import java.awt.Dimension;
37 import java.awt.Font;
38 import java.awt.FontMetrics;
39 import java.awt.Graphics;
40 import java.awt.Panel;
41 import java.awt.Point;
42 import java.awt.Rectangle;
43 import java.awt.ScrollPane;
44 import java.awt.event.MouseEvent;
45 import java.awt.event.MouseListener;
46 import java.awt.event.MouseMotionListener;
47 import java.util.Enumeration;
48 import java.util.Hashtable;
49 import java.util.Vector;
50
51 public class TreeCanvas extends Panel implements MouseListener,
52         MouseMotionListener
53 {
54   NJTree tree;
55
56   ScrollPane scrollPane;
57
58   AlignViewport av;
59
60   public static final String PLACEHOLDER = " * ";
61
62   Font font;
63
64   boolean fitToWindow = true;
65
66   boolean showDistances = false;
67
68   boolean showBootstrap = false;
69
70   boolean markPlaceholders = false;
71
72   int offx = 20;
73
74   int offy;
75
76   float threshold;
77
78   String longestName;
79
80   int labelLength = -1;
81
82   Hashtable nameHash = new Hashtable();
83
84   Hashtable nodeHash = new Hashtable();
85
86   SequenceNode highlightNode;
87
88   AlignmentPanel ap;
89
90   public TreeCanvas(AlignmentPanel ap, ScrollPane scroller)
91   {
92     this.ap = ap;
93     this.av = ap.av;
94     font = av.getFont();
95     scrollPane = scroller;
96     addMouseListener(this);
97     addMouseMotionListener(this);
98     setLayout(null);
99
100     PaintRefresher.Register(this, av.getSequenceSetId());
101   }
102
103   public void treeSelectionChanged(SequenceI sequence)
104   {
105     SequenceGroup selected = av.getSelectionGroup();
106     if (selected == null)
107     {
108       selected = new SequenceGroup();
109       av.setSelectionGroup(selected);
110     }
111
112     selected.setEndRes(av.getAlignment().getWidth() - 1);
113     selected.addOrRemove(sequence, true);
114   }
115
116   public void setTree(NJTree tree)
117   {
118     this.tree = tree;
119     tree.findHeight(tree.getTopNode());
120
121     // Now have to calculate longest name based on the leaves
122     Vector leaves = tree.findLeaves(tree.getTopNode(), new Vector());
123     boolean has_placeholders = false;
124     longestName = "";
125
126     for (int i = 0; i < leaves.size(); i++)
127     {
128       SequenceNode lf = (SequenceNode) leaves.elementAt(i);
129
130       if (lf.isPlaceholder())
131       {
132         has_placeholders = true;
133       }
134
135       if (longestName.length() < ((Sequence) lf.element()).getName()
136               .length())
137       {
138         longestName = TreeCanvas.PLACEHOLDER
139                 + ((Sequence) lf.element()).getName();
140       }
141     }
142
143     setMarkPlaceholders(has_placeholders);
144   }
145
146   public void drawNode(Graphics g, SequenceNode node, float chunk,
147           float scale, int width, int offx, int offy)
148   {
149     if (node == null)
150     {
151       return;
152     }
153
154     if (node.left() == null && node.right() == null)
155     {
156       // Drawing leaf node
157
158       float height = node.height;
159       float dist = node.dist;
160
161       int xstart = (int) ((height - dist) * scale) + offx;
162       int xend = (int) (height * scale) + offx;
163
164       int ypos = (int) (node.ycount * chunk) + offy;
165
166       if (node.element() instanceof SequenceI)
167       {
168         SequenceI seq = (SequenceI) node.element();
169
170         if (av.getSequenceColour(seq) == Color.white)
171         {
172           g.setColor(Color.black);
173         }
174         else
175         {
176           g.setColor(av.getSequenceColour(seq).darker());
177         }
178
179       }
180       else
181       {
182         g.setColor(Color.black);
183       }
184
185       // Draw horizontal line
186       g.drawLine(xstart, ypos, xend, ypos);
187
188       String nodeLabel = "";
189       if (showDistances && node.dist > 0)
190       {
191         nodeLabel = new Format("%-.2f").form(node.dist);
192       }
193       if (showBootstrap)
194       {
195         int btstrap = node.getBootstrap();
196         if (btstrap > -1)
197         {
198           if (showDistances)
199           {
200             nodeLabel = nodeLabel + " : ";
201           }
202           nodeLabel = nodeLabel + String.valueOf(node.getBootstrap());
203         }
204       }
205       if (!nodeLabel.equals(""))
206       {
207         g.drawString(nodeLabel, xstart + 2, ypos - 2);
208       }
209
210       String name = (markPlaceholders && node.isPlaceholder()) ? (PLACEHOLDER + node
211               .getName()) : node.getName();
212       FontMetrics fm = g.getFontMetrics(font);
213       int charWidth = fm.stringWidth(name) + 3;
214       int charHeight = fm.getHeight();
215
216       Rectangle rect = new Rectangle(xend + 10, ypos - charHeight,
217               charWidth, charHeight);
218
219       nameHash.put(node.element(), rect);
220
221       // Colour selected leaves differently
222       SequenceGroup selected = av.getSelectionGroup();
223       if (selected != null
224               && selected.getSequences(null).contains(node.element()))
225       {
226         g.setColor(Color.gray);
227
228         g.fillRect(xend + 10, ypos - charHeight + 3, charWidth, charHeight);
229         g.setColor(Color.white);
230       }
231       g.drawString(name, xend + 10, ypos);
232       g.setColor(Color.black);
233     }
234     else
235     {
236       drawNode(g, (SequenceNode) node.left(), chunk, scale, width, offx,
237               offy);
238       drawNode(g, (SequenceNode) node.right(), chunk, scale, width, offx,
239               offy);
240
241       float height = node.height;
242       float dist = node.dist;
243
244       int xstart = (int) ((height - dist) * scale) + offx;
245       int xend = (int) (height * scale) + offx;
246       int ypos = (int) (node.ycount * chunk) + offy;
247
248       g.setColor(node.color.darker());
249
250       // Draw horizontal line
251       g.drawLine(xstart, ypos, xend, ypos);
252       if (node == highlightNode)
253       {
254         g.fillRect(xend - 3, ypos - 3, 6, 6);
255       }
256       else
257       {
258         g.fillRect(xend - 2, ypos - 2, 4, 4);
259       }
260
261       int ystart = (int) (((SequenceNode) node.left()).ycount * chunk)
262               + offy;
263       int yend = (int) (((SequenceNode) node.right()).ycount * chunk)
264               + offy;
265
266       Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
267       nodeHash.put(node, pos);
268
269       g.drawLine((int) (height * scale) + offx, ystart,
270               (int) (height * scale) + offx, yend);
271
272       String nodeLabel = "";
273
274       if (showDistances && (node.dist > 0))
275       {
276         nodeLabel = new Format("%-.2f").form(node.dist);
277       }
278
279       if (showBootstrap)
280       {
281         int btstrap = node.getBootstrap();
282         if (btstrap > -1)
283         {
284           if (showDistances)
285           {
286             nodeLabel = nodeLabel + " : ";
287           }
288           nodeLabel = nodeLabel + String.valueOf(node.getBootstrap());
289         }
290       }
291
292       if (!nodeLabel.equals(""))
293       {
294         g.drawString(nodeLabel, xstart + 2, ypos - 2);
295       }
296
297     }
298   }
299
300   public Object findElement(int x, int y)
301   {
302     Enumeration keys = nameHash.keys();
303
304     while (keys.hasMoreElements())
305     {
306       Object ob = keys.nextElement();
307       Rectangle rect = (Rectangle) nameHash.get(ob);
308
309       if (x >= rect.x && x <= (rect.x + rect.width) && y >= rect.y
310               && y <= (rect.y + rect.height))
311       {
312         return ob;
313       }
314     }
315     keys = nodeHash.keys();
316
317     while (keys.hasMoreElements())
318     {
319       Object ob = keys.nextElement();
320       Rectangle rect = (Rectangle) nodeHash.get(ob);
321
322       if (x >= rect.x && x <= (rect.x + rect.width) && y >= rect.y
323               && y <= (rect.y + rect.height))
324       {
325         return ob;
326       }
327     }
328     return null;
329
330   }
331
332   public void pickNodes(Rectangle pickBox)
333   {
334     int width = getSize().width;
335     int height = getSize().height;
336
337     SequenceNode top = tree.getTopNode();
338
339     float wscale = (float) (width * .8 - offx * 2) / tree.getMaxHeight();
340     if (top.count == 0)
341     {
342       top.count = ((SequenceNode) top.left()).count
343               + ((SequenceNode) top.right()).count;
344     }
345     float chunk = (float) (height - offy) / top.count;
346
347     pickNode(pickBox, top, chunk, wscale, width, offx, offy);
348   }
349
350   public void pickNode(Rectangle pickBox, SequenceNode node, float chunk,
351           float scale, int width, int offx, int offy)
352   {
353     if (node == null)
354     {
355       return;
356     }
357
358     if (node.left() == null && node.right() == null)
359     {
360       float height = node.height;
361       // float dist = node.dist;
362
363       // int xstart = (int) ( (height - dist) * scale) + offx;
364       int xend = (int) (height * scale) + offx;
365
366       int ypos = (int) (node.ycount * chunk) + offy;
367
368       if (pickBox.contains(new Point(xend, ypos)))
369       {
370         if (node.element() instanceof SequenceI)
371         {
372           SequenceI seq = (SequenceI) node.element();
373           SequenceGroup sg = av.getSelectionGroup();
374           if (sg != null)
375           {
376             sg.addOrRemove(seq, true);
377           }
378         }
379       }
380     }
381     else
382     {
383       pickNode(pickBox, (SequenceNode) node.left(), chunk, scale, width,
384               offx, offy);
385       pickNode(pickBox, (SequenceNode) node.right(), chunk, scale, width,
386               offx, offy);
387     }
388   }
389
390   public void setColor(SequenceNode node, Color c)
391   {
392     if (node == null)
393     {
394       return;
395     }
396
397     if (node.left() == null && node.right() == null)
398     {
399       node.color = c;
400
401       if (node.element() instanceof SequenceI)
402       {
403         av.setSequenceColour((SequenceI) node.element(), c);
404       }
405     }
406     else
407     {
408       node.color = c;
409       setColor((SequenceNode) node.left(), c);
410       setColor((SequenceNode) node.right(), c);
411     }
412   }
413
414   @Override
415   public void update(Graphics g)
416   {
417     paint(g);
418   }
419
420   @Override
421   public void paint(Graphics g)
422   {
423     if (tree == null)
424     {
425       return;
426     }
427
428     if (nameHash.size() == 0)
429     {
430       repaint();
431     }
432
433     int width = scrollPane.getSize().width;
434     int height = scrollPane.getSize().height;
435     if (!fitToWindow)
436     {
437       height = g.getFontMetrics(font).getHeight() * nameHash.size();
438     }
439
440     if (getSize().width > width)
441     {
442       setSize(new Dimension(width, height));
443       scrollPane.validate();
444       return;
445     }
446
447     setSize(new Dimension(width, height));
448
449     g.setFont(font);
450     draw(g, width, height);
451     validate();
452   }
453
454   public void draw(Graphics g, int width, int height)
455   {
456     offy = font.getSize() + 10;
457
458     g.setColor(Color.white);
459     g.fillRect(0, 0, width, height);
460
461     labelLength = g.getFontMetrics(font).stringWidth(longestName) + 20; // 20
462     // allows
463     // for
464     // scrollbar
465
466     float wscale = (width - labelLength - offx * 2) / tree.getMaxHeight();
467
468     SequenceNode top = tree.getTopNode();
469
470     if (top.count == 0)
471     {
472       top.count = ((SequenceNode) top.left()).count
473               + ((SequenceNode) top.right()).count;
474     }
475     float chunk = (float) (height - offy) / top.count;
476
477     drawNode(g, tree.getTopNode(), chunk, wscale, width, offx, offy);
478
479     if (threshold != 0)
480     {
481       if (av.getCurrentTree() == tree)
482       {
483         g.setColor(Color.red);
484       }
485       else
486       {
487         g.setColor(Color.gray);
488       }
489
490       int x = (int) (threshold * (getSize().width - labelLength - 2 * offx) + offx);
491
492       g.drawLine(x, 0, x, getSize().height);
493     }
494
495   }
496
497   @Override
498   public void mouseReleased(MouseEvent e)
499   {
500   }
501
502   @Override
503   public void mouseEntered(MouseEvent e)
504   {
505   }
506
507   @Override
508   public void mouseExited(MouseEvent e)
509   {
510   }
511
512   @Override
513   public void mouseClicked(MouseEvent evt)
514   {
515     if (highlightNode != null)
516     {
517       if (evt.getClickCount() > 1)
518       {
519         tree.swapNodes(highlightNode);
520         tree.reCount(tree.getTopNode());
521         tree.findHeight(tree.getTopNode());
522       }
523       else
524       {
525         Vector leaves = new Vector();
526         tree.findLeaves(highlightNode, leaves);
527
528         for (int i = 0; i < leaves.size(); i++)
529         {
530           SequenceI seq = (SequenceI) ((SequenceNode) leaves.elementAt(i))
531                   .element();
532           treeSelectionChanged(seq);
533         }
534       }
535
536       PaintRefresher.Refresh(this, av.getSequenceSetId());
537       repaint();
538       av.sendSelection();
539     }
540   }
541
542   @Override
543   public void mouseDragged(MouseEvent ect)
544   {
545   }
546
547   @Override
548   public void mouseMoved(MouseEvent evt)
549   {
550     av.setCurrentTree(tree);
551
552     Object ob = findElement(evt.getX(), evt.getY());
553
554     if (ob instanceof SequenceNode)
555     {
556       highlightNode = (SequenceNode) ob;
557       repaint();
558     }
559     else
560     {
561       if (highlightNode != null)
562       {
563         highlightNode = null;
564         repaint();
565       }
566     }
567   }
568
569   @Override
570   public void mousePressed(MouseEvent e)
571   {
572     av.setCurrentTree(tree);
573
574     int x = e.getX();
575     int y = e.getY();
576
577     Object ob = findElement(x, y);
578
579     if (ob instanceof SequenceI)
580     {
581       treeSelectionChanged((Sequence) ob);
582       PaintRefresher.Refresh(this, av.getSequenceSetId());
583       repaint();
584       av.sendSelection();
585       return;
586     }
587     else if (!(ob instanceof SequenceNode))
588     {
589       // Find threshold
590
591       if (tree.getMaxHeight() != 0)
592       {
593         threshold = (float) (x - offx)
594                 / (float) (getSize().width - labelLength - 2 * offx);
595
596         tree.getGroups().removeAllElements();
597         tree.groupNodes(tree.getTopNode(), threshold);
598         setColor(tree.getTopNode(), Color.black);
599
600         av.setSelectionGroup(null);
601         av.getAlignment().deleteAllGroups();
602         av.clearSequenceColours();
603
604         colourGroups();
605
606       }
607     }
608
609     PaintRefresher.Refresh(this, av.getSequenceSetId());
610     repaint();
611
612   }
613
614   void colourGroups()
615   {
616     for (int i = 0; i < tree.getGroups().size(); i++)
617     {
618
619       Color col = new Color((int) (Math.random() * 255),
620               (int) (Math.random() * 255), (int) (Math.random() * 255));
621       setColor((SequenceNode) tree.getGroups().elementAt(i), col.brighter());
622
623       Vector l = tree.findLeaves(
624               (SequenceNode) tree.getGroups().elementAt(i), new Vector());
625
626       Vector sequences = new Vector();
627       for (int j = 0; j < l.size(); j++)
628       {
629         SequenceI s1 = (SequenceI) ((SequenceNode) l.elementAt(j))
630                 .element();
631         if (!sequences.contains(s1))
632         {
633           sequences.addElement(s1);
634         }
635       }
636
637       ColourSchemeI cs = null;
638
639       SequenceGroup sg = new SequenceGroup(sequences, "", cs, true, true,
640               false, 0, av.getAlignment().getWidth() - 1);
641
642       if (av.getGlobalColourScheme() != null)
643       {
644         if (av.getGlobalColourScheme() instanceof UserColourScheme)
645         {
646           cs = new UserColourScheme(
647                   ((UserColourScheme) av.getGlobalColourScheme())
648                           .getColours());
649
650         }
651         else
652         {
653           cs = ColourSchemeProperty.getColour(sg, ColourSchemeProperty
654                   .getColourName(av.getGlobalColourScheme()));
655         }
656         // cs is null if shading is an annotationColourGradient
657         if (cs != null)
658         {
659           cs.setThreshold(av.getGlobalColourScheme().getThreshold(),
660                   av.isIgnoreGapsConsensus());
661         }
662       }
663       // TODO: cs used to be initialized with a sequence collection and
664       // recalcConservation called automatically
665       // instead we set it manually - recalc called after updateAnnotation
666       sg.cs = cs;
667
668       sg.setName("JTreeGroup:" + sg.hashCode());
669       sg.setIdColour(col);
670       if (av.getGlobalColourScheme() != null
671               && av.getGlobalColourScheme().conservationApplied())
672       {
673         Conservation c = new Conservation("Group",
674                 ResidueProperties.propHash, 3, sg.getSequences(null),
675                 sg.getStartRes(), sg.getEndRes());
676
677         c.calculate();
678         c.verdict(false, av.getConsPercGaps());
679         cs.setConservation(c);
680
681         sg.cs = cs;
682
683       }
684
685       av.getAlignment().addGroup(sg);
686
687     }
688     ap.updateAnnotation();
689
690   }
691
692   public void setShowDistances(boolean state)
693   {
694     this.showDistances = state;
695     repaint();
696   }
697
698   public void setShowBootstrap(boolean state)
699   {
700     this.showBootstrap = state;
701     repaint();
702   }
703
704   public void setMarkPlaceholders(boolean state)
705   {
706     this.markPlaceholders = state;
707     repaint();
708   }
709
710 }