JAL-2094 new classes ColorI, Colour added
[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 leaves = tree.findLeaves(tree.getTopNode(), new Vector());
128     boolean has_placeholders = false;
129     longestName = "";
130
131     for (int i = 0; i < leaves.size(); i++)
132     {
133       SequenceNode lf = (SequenceNode) 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 leaves = new Vector();
532         tree.findLeaves(highlightNode, leaves);
533
534         for (int i = 0; i < leaves.size(); i++)
535         {
536           SequenceI seq = (SequenceI) ((SequenceNode) leaves.elementAt(i))
537                   .element();
538           treeSelectionChanged(seq);
539         }
540       }
541
542       PaintRefresher.Refresh(this, av.getSequenceSetId());
543       repaint();
544       av.sendSelection();
545     }
546   }
547
548   @Override
549   public void mouseDragged(MouseEvent ect)
550   {
551   }
552
553   @Override
554   public void mouseMoved(MouseEvent evt)
555   {
556     av.setCurrentTree(tree);
557
558     Object ob = findElement(evt.getX(), evt.getY());
559
560     if (ob instanceof SequenceNode)
561     {
562       highlightNode = (SequenceNode) ob;
563       repaint();
564     }
565     else
566     {
567       if (highlightNode != null)
568       {
569         highlightNode = null;
570         repaint();
571       }
572     }
573   }
574
575   @Override
576   public void mousePressed(MouseEvent e)
577   {
578     av.setCurrentTree(tree);
579
580     int x = e.getX();
581     int y = e.getY();
582
583     Object ob = findElement(x, y);
584
585     if (ob instanceof SequenceI)
586     {
587       treeSelectionChanged((Sequence) ob);
588       PaintRefresher.Refresh(this, av.getSequenceSetId());
589       repaint();
590       av.sendSelection();
591       return;
592     }
593     else if (!(ob instanceof SequenceNode))
594     {
595       // Find threshold
596
597       if (tree.getMaxHeight() != 0)
598       {
599         threshold = (float) (x - offx)
600                 / (float) (getSize().width - labelLength - 2 * offx);
601
602         tree.getGroups().removeAllElements();
603         tree.groupNodes(tree.getTopNode(), threshold);
604         setColor(tree.getTopNode(), Color.black);
605
606         av.setSelectionGroup(null);
607         av.getAlignment().deleteAllGroups();
608         av.clearSequenceColours();
609         final AlignViewportI codingComplement = av.getCodingComplement();
610         if (codingComplement != null)
611         {
612           codingComplement.setSelectionGroup(null);
613           codingComplement.getAlignment().deleteAllGroups();
614           codingComplement.clearSequenceColours();
615         }
616
617         colourGroups();
618
619       }
620     }
621
622     PaintRefresher.Refresh(this, av.getSequenceSetId());
623     repaint();
624
625   }
626
627   void colourGroups()
628   {
629     for (int i = 0; i < tree.getGroups().size(); i++)
630     {
631
632       Color col = new Color((int) (Math.random() * 255),
633               (int) (Math.random() * 255), (int) (Math.random() * 255));
634       setColor((SequenceNode) tree.getGroups().elementAt(i), col.brighter());
635
636       Vector l = tree.findLeaves(
637               (SequenceNode) tree.getGroups().elementAt(i), new Vector());
638
639       Vector sequences = new Vector();
640       for (int j = 0; j < l.size(); j++)
641       {
642         SequenceI s1 = (SequenceI) ((SequenceNode) l.elementAt(j))
643                 .element();
644         if (!sequences.contains(s1))
645         {
646           sequences.addElement(s1);
647         }
648       }
649
650       ColourSchemeI cs = null;
651
652       SequenceGroup sg = new SequenceGroup(sequences, "", cs, true, true,
653               false, 0, av.getAlignment().getWidth() - 1);
654
655       if (av.getGlobalColourScheme() != null)
656       {
657         if (av.getGlobalColourScheme() instanceof UserColourScheme)
658         {
659           cs = new UserColourScheme(
660                   ((UserColourScheme) av.getGlobalColourScheme())
661                           .getColours());
662
663         }
664         else
665         {
666           cs = ColourSchemeProperty.getColour(sg, ColourSchemeProperty
667                   .getColourName(av.getGlobalColourScheme()));
668         }
669         // cs is null if shading is an annotationColourGradient
670         if (cs != null)
671         {
672           cs.setThreshold(av.getGlobalColourScheme().getThreshold(),
673                   av.isIgnoreGapsConsensus());
674         }
675       }
676       // TODO: cs used to be initialized with a sequence collection and
677       // recalcConservation called automatically
678       // instead we set it manually - recalc called after updateAnnotation
679       sg.cs = cs;
680
681       sg.setName("JTreeGroup:" + sg.hashCode());
682       sg.setIdColour(col);
683       if (av.getGlobalColourScheme() != null
684               && av.getGlobalColourScheme().conservationApplied())
685       {
686         Conservation c = new Conservation("Group",
687                 ResidueProperties.propHash, 3, sg.getSequences(null),
688                 sg.getStartRes(), sg.getEndRes());
689
690         c.calculate();
691         c.verdict(false, av.getConsPercGaps());
692         cs.setConservation(c);
693
694         sg.cs = cs;
695
696       }
697
698       av.getAlignment().addGroup(sg);
699
700       // TODO this is duplicated with gui TreeCanvas - refactor
701       av.getAlignment().addGroup(sg);
702       final AlignViewportI codingComplement = av.getCodingComplement();
703       if (codingComplement != null)
704       {
705         SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(sg, av,
706                 codingComplement);
707         if (mappedGroup.getSequences().size() > 0)
708         {
709           codingComplement.getAlignment().addGroup(mappedGroup);
710           for (SequenceI seq : mappedGroup.getSequences())
711           {
712             // TODO why does gui require col.brighter() here??
713             codingComplement.setSequenceColour(seq, new Colour(col));
714           }
715         }
716       }
717
718     }
719     ap.updateAnnotation();
720     if (av.getCodingComplement() != null)
721     {
722       ((AlignmentViewport) av.getCodingComplement()).firePropertyChange(
723               "alignment", null, ap.av.getAlignment().getSequences());
724     }
725   }
726
727   public void setShowDistances(boolean state)
728   {
729     this.showDistances = state;
730     repaint();
731   }
732
733   public void setShowBootstrap(boolean state)
734   {
735     this.showBootstrap = state;
736     repaint();
737   }
738
739   public void setMarkPlaceholders(boolean state)
740   {
741     this.markPlaceholders = state;
742     repaint();
743   }
744
745 }