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