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