Merge branch 'develop' into features/JAL-2094_colourInterface
[jalview.git] / src / jalview / gui / 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.gui;
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.structure.SelectionSource;
36 import jalview.util.ColorUtils;
37 import jalview.util.Format;
38 import jalview.util.MappingUtils;
39 import jalview.util.MessageManager;
40
41 import java.awt.Color;
42 import java.awt.Dimension;
43 import java.awt.Font;
44 import java.awt.FontMetrics;
45 import java.awt.Graphics;
46 import java.awt.Graphics2D;
47 import java.awt.Point;
48 import java.awt.Rectangle;
49 import java.awt.RenderingHints;
50 import java.awt.event.MouseEvent;
51 import java.awt.event.MouseListener;
52 import java.awt.event.MouseMotionListener;
53 import java.awt.print.PageFormat;
54 import java.awt.print.Printable;
55 import java.awt.print.PrinterException;
56 import java.awt.print.PrinterJob;
57 import java.util.Enumeration;
58 import java.util.Hashtable;
59 import java.util.Vector;
60
61 import javax.swing.JColorChooser;
62 import javax.swing.JPanel;
63 import javax.swing.JScrollPane;
64 import javax.swing.SwingUtilities;
65 import javax.swing.ToolTipManager;
66
67 /**
68  * DOCUMENT ME!
69  * 
70  * @author $author$
71  * @version $Revision$
72  */
73 public class TreeCanvas extends JPanel implements MouseListener, Runnable,
74         Printable, MouseMotionListener, SelectionSource
75 {
76   /** DOCUMENT ME!! */
77   public static final String PLACEHOLDER = " * ";
78
79   NJTree tree;
80
81   JScrollPane scrollPane;
82
83   TreePanel tp;
84
85   AlignViewport av;
86
87   AlignmentPanel ap;
88
89   Font font;
90
91   FontMetrics fm;
92
93   boolean fitToWindow = true;
94
95   boolean showDistances = false;
96
97   boolean showBootstrap = false;
98
99   boolean markPlaceholders = false;
100
101   int offx = 20;
102
103   int offy;
104
105   float threshold;
106
107   String longestName;
108
109   int labelLength = -1;
110
111   Hashtable nameHash = new Hashtable();
112
113   Hashtable nodeHash = new Hashtable();
114
115   SequenceNode highlightNode;
116
117   boolean applyToAllViews = false;
118
119   /**
120    * Creates a new TreeCanvas object.
121    * 
122    * @param av
123    *          DOCUMENT ME!
124    * @param tree
125    *          DOCUMENT ME!
126    * @param scroller
127    *          DOCUMENT ME!
128    * @param label
129    *          DOCUMENT ME!
130    */
131   public TreeCanvas(TreePanel tp, AlignmentPanel ap, JScrollPane scroller)
132   {
133     this.tp = tp;
134     this.av = ap.av;
135     this.ap = ap;
136     font = av.getFont();
137     scrollPane = scroller;
138     addMouseListener(this);
139     addMouseMotionListener(this);
140     ToolTipManager.sharedInstance().registerComponent(this);
141   }
142
143   /**
144    * DOCUMENT ME!
145    * 
146    * @param sequence
147    *          DOCUMENT ME!
148    */
149   public void treeSelectionChanged(SequenceI sequence)
150   {
151     AlignmentPanel[] aps = getAssociatedPanels();
152
153     for (int a = 0; a < aps.length; a++)
154     {
155       SequenceGroup selected = aps[a].av.getSelectionGroup();
156
157       if (selected == null)
158       {
159         selected = new SequenceGroup();
160         aps[a].av.setSelectionGroup(selected);
161       }
162
163       selected.setEndRes(aps[a].av.getAlignment().getWidth() - 1);
164       selected.addOrRemove(sequence, true);
165     }
166   }
167
168   /**
169    * DOCUMENT ME!
170    * 
171    * @param tree
172    *          DOCUMENT ME!
173    */
174   public void setTree(NJTree tree)
175   {
176     this.tree = tree;
177     tree.findHeight(tree.getTopNode());
178
179     // Now have to calculate longest name based on the leaves
180     Vector<SequenceNode> leaves = tree.findLeaves(tree.getTopNode());
181     boolean has_placeholders = false;
182     longestName = "";
183
184     for (int i = 0; i < leaves.size(); i++)
185     {
186       SequenceNode lf = leaves.elementAt(i);
187
188       if (lf.isPlaceholder())
189       {
190         has_placeholders = true;
191       }
192
193       if (longestName.length() < ((Sequence) lf.element()).getName()
194               .length())
195       {
196         longestName = TreeCanvas.PLACEHOLDER
197                 + ((Sequence) lf.element()).getName();
198       }
199     }
200
201     setMarkPlaceholders(has_placeholders);
202   }
203
204   /**
205    * DOCUMENT ME!
206    * 
207    * @param g
208    *          DOCUMENT ME!
209    * @param node
210    *          DOCUMENT ME!
211    * @param chunk
212    *          DOCUMENT ME!
213    * @param scale
214    *          DOCUMENT ME!
215    * @param width
216    *          DOCUMENT ME!
217    * @param offx
218    *          DOCUMENT ME!
219    * @param offy
220    *          DOCUMENT ME!
221    */
222   public void drawNode(Graphics g, SequenceNode node, float chunk,
223           float scale, int width, int offx, int offy)
224   {
225     if (node == null)
226     {
227       return;
228     }
229
230     if ((node.left() == null) && (node.right() == null))
231     {
232       // Drawing leaf node
233       float height = node.height;
234       float dist = node.dist;
235
236       int xstart = (int) ((height - dist) * scale) + offx;
237       int xend = (int) (height * scale) + offx;
238
239       int ypos = (int) (node.ycount * chunk) + offy;
240
241       if (node.element() instanceof SequenceI)
242       {
243         SequenceI seq = (SequenceI) node.element();
244
245         if (av.getSequenceColour(seq) == Color.white)
246         {
247           g.setColor(Color.black);
248         }
249         else
250         {
251           g.setColor(ColorUtils.getColor(av.getSequenceColour(seq))
252                   .darker());
253         }
254       }
255       else
256       {
257         g.setColor(Color.black);
258       }
259
260       // Draw horizontal line
261       g.drawLine(xstart, ypos, xend, ypos);
262
263       String nodeLabel = "";
264
265       if (showDistances && (node.dist > 0))
266       {
267         nodeLabel = new Format("%-.2f").form(node.dist);
268       }
269
270       if (showBootstrap && node.bootstrap > -1)
271       {
272         if (showDistances)
273         {
274           nodeLabel = nodeLabel + " : ";
275         }
276
277         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
278       }
279
280       if (!nodeLabel.equals(""))
281       {
282         g.drawString(nodeLabel, xstart + 2, ypos - 2);
283       }
284
285       String name = (markPlaceholders && node.isPlaceholder()) ? (PLACEHOLDER + node
286               .getName()) : node.getName();
287
288       int charWidth = fm.stringWidth(name) + 3;
289       int charHeight = font.getSize();
290
291       Rectangle rect = new Rectangle(xend + 10, ypos - charHeight / 2,
292               charWidth, charHeight);
293
294       nameHash.put(node.element(), rect);
295
296       // Colour selected leaves differently
297       SequenceGroup selected = av.getSelectionGroup();
298
299       if ((selected != null)
300               && selected.getSequences(null).contains(node.element()))
301       {
302         g.setColor(Color.gray);
303
304         g.fillRect(xend + 10, ypos - charHeight / 2, charWidth, charHeight);
305         g.setColor(Color.white);
306       }
307
308       g.drawString(name, xend + 10, ypos + fm.getDescent());
309       g.setColor(Color.black);
310     }
311     else
312     {
313       drawNode(g, (SequenceNode) node.left(), chunk, scale, width, offx,
314               offy);
315       drawNode(g, (SequenceNode) node.right(), chunk, scale, width, offx,
316               offy);
317
318       float height = node.height;
319       float dist = node.dist;
320
321       int xstart = (int) ((height - dist) * scale) + offx;
322       int xend = (int) (height * scale) + offx;
323       int ypos = (int) (node.ycount * chunk) + offy;
324
325       g.setColor(node.color.darker());
326
327       // Draw horizontal line
328       g.drawLine(xstart, ypos, xend, ypos);
329       if (node == highlightNode)
330       {
331         g.fillRect(xend - 3, ypos - 3, 6, 6);
332       }
333       else
334       {
335         g.fillRect(xend - 2, ypos - 2, 4, 4);
336       }
337
338       int ystart = (int) (((SequenceNode) node.left()).ycount * chunk)
339               + offy;
340       int yend = (int) (((SequenceNode) node.right()).ycount * chunk)
341               + offy;
342
343       Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
344       nodeHash.put(node, pos);
345
346       g.drawLine((int) (height * scale) + offx, ystart,
347               (int) (height * scale) + offx, yend);
348
349       String nodeLabel = "";
350
351       if (showDistances && (node.dist > 0))
352       {
353         nodeLabel = new Format("%-.2f").form(node.dist);
354       }
355
356       if (showBootstrap && node.bootstrap > -1)
357       {
358         if (showDistances)
359         {
360           nodeLabel = nodeLabel + " : ";
361         }
362
363         nodeLabel = nodeLabel + String.valueOf(node.bootstrap);
364       }
365
366       if (!nodeLabel.equals(""))
367       {
368         g.drawString(nodeLabel, xstart + 2, ypos - 2);
369       }
370     }
371   }
372
373   /**
374    * DOCUMENT ME!
375    * 
376    * @param x
377    *          DOCUMENT ME!
378    * @param y
379    *          DOCUMENT ME!
380    * 
381    * @return DOCUMENT ME!
382    */
383   public Object findElement(int x, int y)
384   {
385     Enumeration keys = nameHash.keys();
386
387     while (keys.hasMoreElements())
388     {
389       Object ob = keys.nextElement();
390       Rectangle rect = (Rectangle) nameHash.get(ob);
391
392       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
393               && (y <= (rect.y + rect.height)))
394       {
395         return ob;
396       }
397     }
398
399     keys = nodeHash.keys();
400
401     while (keys.hasMoreElements())
402     {
403       Object ob = keys.nextElement();
404       Rectangle rect = (Rectangle) nodeHash.get(ob);
405
406       if ((x >= rect.x) && (x <= (rect.x + rect.width)) && (y >= rect.y)
407               && (y <= (rect.y + rect.height)))
408       {
409         return ob;
410       }
411     }
412
413     return null;
414   }
415
416   /**
417    * DOCUMENT ME!
418    * 
419    * @param pickBox
420    *          DOCUMENT ME!
421    */
422   public void pickNodes(Rectangle pickBox)
423   {
424     int width = getWidth();
425     int height = getHeight();
426
427     SequenceNode top = tree.getTopNode();
428
429     float wscale = (float) ((width * .8) - (offx * 2))
430             / tree.getMaxHeight();
431
432     if (top.count == 0)
433     {
434       top.count = ((SequenceNode) top.left()).count
435               + ((SequenceNode) top.right()).count;
436     }
437
438     float chunk = (float) (height - (offy)) / top.count;
439
440     pickNode(pickBox, top, chunk, wscale, width, offx, offy);
441   }
442
443   /**
444    * DOCUMENT ME!
445    * 
446    * @param pickBox
447    *          DOCUMENT ME!
448    * @param node
449    *          DOCUMENT ME!
450    * @param chunk
451    *          DOCUMENT ME!
452    * @param scale
453    *          DOCUMENT ME!
454    * @param width
455    *          DOCUMENT ME!
456    * @param offx
457    *          DOCUMENT ME!
458    * @param offy
459    *          DOCUMENT ME!
460    */
461   public void pickNode(Rectangle pickBox, SequenceNode node, float chunk,
462           float scale, int width, int offx, int offy)
463   {
464     if (node == null)
465     {
466       return;
467     }
468
469     if ((node.left() == null) && (node.right() == null))
470     {
471       float height = node.height;
472       float dist = node.dist;
473
474       int xstart = (int) ((height - dist) * scale) + offx;
475       int xend = (int) (height * scale) + offx;
476
477       int ypos = (int) (node.ycount * chunk) + offy;
478
479       if (pickBox.contains(new Point(xend, ypos)))
480       {
481         if (node.element() instanceof SequenceI)
482         {
483           SequenceI seq = (SequenceI) node.element();
484           SequenceGroup sg = av.getSelectionGroup();
485
486           if (sg != null)
487           {
488             sg.addOrRemove(seq, true);
489           }
490         }
491       }
492     }
493     else
494     {
495       pickNode(pickBox, (SequenceNode) node.left(), chunk, scale, width,
496               offx, offy);
497       pickNode(pickBox, (SequenceNode) node.right(), chunk, scale, width,
498               offx, offy);
499     }
500   }
501
502   /**
503    * DOCUMENT ME!
504    * 
505    * @param node
506    *          DOCUMENT ME!
507    * @param c
508    *          DOCUMENT ME!
509    */
510   public void setColor(SequenceNode node, Color c)
511   {
512     if (node == null)
513     {
514       return;
515     }
516
517     if ((node.left() == null) && (node.right() == null)) // TODO: internal node
518     {
519       node.color = c;
520
521       if (node.element() instanceof SequenceI)
522       {
523         AlignmentPanel[] aps = getAssociatedPanels();
524         if (aps != null)
525         {
526           for (int a = 0; a < aps.length; a++)
527           {
528             final SequenceI seq = (SequenceI) node.element();
529             aps[a].av.setSequenceColour(seq, new Colour(c));
530           }
531         }
532       }
533     }
534     else
535     {
536       node.color = c;
537       setColor((SequenceNode) node.left(), c);
538       setColor((SequenceNode) node.right(), c);
539     }
540   }
541
542   /**
543    * DOCUMENT ME!
544    */
545   void startPrinting()
546   {
547     Thread thread = new Thread(this);
548     thread.start();
549   }
550
551   // put printing in a thread to avoid painting problems
552   @Override
553   public void run()
554   {
555     PrinterJob printJob = PrinterJob.getPrinterJob();
556     PageFormat pf = printJob.pageDialog(printJob.defaultPage());
557
558     printJob.setPrintable(this, pf);
559
560     if (printJob.printDialog())
561     {
562       try
563       {
564         printJob.print();
565       } catch (Exception PrintException)
566       {
567         PrintException.printStackTrace();
568       }
569     }
570   }
571
572   /**
573    * DOCUMENT ME!
574    * 
575    * @param pg
576    *          DOCUMENT ME!
577    * @param pf
578    *          DOCUMENT ME!
579    * @param pi
580    *          DOCUMENT ME!
581    * 
582    * @return DOCUMENT ME!
583    * 
584    * @throws PrinterException
585    *           DOCUMENT ME!
586    */
587   @Override
588   public int print(Graphics pg, PageFormat pf, int pi)
589           throws PrinterException
590   {
591     pg.setFont(font);
592     pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
593
594     int pwidth = (int) pf.getImageableWidth();
595     int pheight = (int) pf.getImageableHeight();
596
597     int noPages = getHeight() / pheight;
598
599     if (pi > noPages)
600     {
601       return Printable.NO_SUCH_PAGE;
602     }
603
604     if (pwidth > getWidth())
605     {
606       pwidth = getWidth();
607     }
608
609     if (fitToWindow)
610     {
611       if (pheight > getHeight())
612       {
613         pheight = getHeight();
614       }
615
616       noPages = 0;
617     }
618     else
619     {
620       FontMetrics fm = pg.getFontMetrics(font);
621       int height = fm.getHeight() * nameHash.size();
622       pg.translate(0, -pi * pheight);
623       pg.setClip(0, pi * pheight, pwidth, (pi * pheight) + pheight);
624
625       // translate number of pages,
626       // height is screen size as this is the
627       // non overlapping text size
628       pheight = height;
629     }
630
631     draw(pg, pwidth, pheight);
632
633     return Printable.PAGE_EXISTS;
634   }
635
636   /**
637    * DOCUMENT ME!
638    * 
639    * @param g
640    *          DOCUMENT ME!
641    */
642   @Override
643   public void paintComponent(Graphics g)
644   {
645     super.paintComponent(g);
646     g.setFont(font);
647
648     if (tree == null)
649     {
650       g.drawString(MessageManager.getString("label.calculating_tree")
651               + "....", 20, getHeight() / 2);
652     }
653     else
654     {
655       fm = g.getFontMetrics(font);
656
657       if (nameHash.size() == 0)
658       {
659         repaint();
660       }
661
662       if (fitToWindow
663               || (!fitToWindow && (scrollPane.getHeight() > ((fm
664                       .getHeight() * nameHash.size()) + offy))))
665       {
666         draw(g, scrollPane.getWidth(), scrollPane.getHeight());
667         setPreferredSize(null);
668       }
669       else
670       {
671         setPreferredSize(new Dimension(scrollPane.getWidth(),
672                 fm.getHeight() * nameHash.size()));
673         draw(g, scrollPane.getWidth(), fm.getHeight() * nameHash.size());
674       }
675
676       scrollPane.revalidate();
677     }
678   }
679
680   /**
681    * DOCUMENT ME!
682    * 
683    * @param fontSize
684    *          DOCUMENT ME!
685    */
686   @Override
687   public void setFont(Font font)
688   {
689     this.font = font;
690     repaint();
691   }
692
693   /**
694    * DOCUMENT ME!
695    * 
696    * @param g1
697    *          DOCUMENT ME!
698    * @param width
699    *          DOCUMENT ME!
700    * @param height
701    *          DOCUMENT ME!
702    */
703   public void draw(Graphics g1, int width, int height)
704   {
705     Graphics2D g2 = (Graphics2D) g1;
706     g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
707             RenderingHints.VALUE_ANTIALIAS_ON);
708     g2.setColor(Color.white);
709     g2.fillRect(0, 0, width, height);
710     g2.setFont(font);
711
712     if (longestName == null || tree == null)
713     {
714       g2.drawString("Calculating tree.", 20, 20);
715     }
716     offy = font.getSize() + 10;
717
718     fm = g2.getFontMetrics(font);
719
720     labelLength = fm.stringWidth(longestName) + 20; // 20 allows for scrollbar
721
722     float wscale = (width - labelLength - (offx * 2)) / tree.getMaxHeight();
723
724     SequenceNode top = tree.getTopNode();
725
726     if (top.count == 0)
727     {
728       top.count = ((SequenceNode) top.left()).count
729               + ((SequenceNode) top.right()).count;
730     }
731
732     float chunk = (float) (height - (offy)) / top.count;
733
734     drawNode(g2, tree.getTopNode(), chunk, wscale, width, offx, offy);
735
736     if (threshold != 0)
737     {
738       if (av.getCurrentTree() == tree)
739       {
740         g2.setColor(Color.red);
741       }
742       else
743       {
744         g2.setColor(Color.gray);
745       }
746
747       int x = (int) ((threshold * (getWidth() - labelLength - (2 * offx))) + offx);
748
749       g2.drawLine(x, 0, x, getHeight());
750     }
751   }
752
753   /**
754    * Empty method to satisfy the MouseListener interface
755    * 
756    * @param e
757    */
758   @Override
759   public void mouseReleased(MouseEvent e)
760   {
761     /*
762      * isPopupTrigger is set on mouseReleased on Windows
763      */
764     if (e.isPopupTrigger())
765     {
766       chooseSubtreeColour();
767       e.consume(); // prevent mouseClicked happening
768     }
769   }
770
771   /**
772    * Empty method to satisfy the MouseListener interface
773    * 
774    * @param e
775    */
776   @Override
777   public void mouseEntered(MouseEvent e)
778   {
779   }
780
781   /**
782    * Empty method to satisfy the MouseListener interface
783    * 
784    * @param e
785    */
786   @Override
787   public void mouseExited(MouseEvent e)
788   {
789   }
790
791   /**
792    * Handles a mouse click on a tree node (clicks elsewhere are handled in
793    * mousePressed). Click selects the sub-tree, double-click swaps leaf nodes
794    * order, right-click opens a dialogue to choose colour for the sub-tree.
795    * 
796    * @param e
797    */
798   @Override
799   public void mouseClicked(MouseEvent evt)
800   {
801     if (highlightNode == null)
802     {
803       return;
804     }
805
806     if (evt.getClickCount() > 1)
807     {
808       tree.swapNodes(highlightNode);
809       tree.reCount(tree.getTopNode());
810       tree.findHeight(tree.getTopNode());
811     }
812     else
813     {
814       Vector<SequenceNode> leaves = tree.findLeaves(highlightNode);
815
816       for (int i = 0; i < leaves.size(); i++)
817       {
818         SequenceI seq = (SequenceI) leaves.elementAt(i)
819                 .element();
820         treeSelectionChanged(seq);
821       }
822       av.sendSelection();
823     }
824
825     PaintRefresher.Refresh(tp, av.getSequenceSetId());
826     repaint();
827   }
828
829   /**
830    * Offer the user the option to choose a colour for the highlighted node and
831    * its children; this colour is also applied to the corresponding sequence ids
832    * in the alignment
833    */
834   void chooseSubtreeColour()
835   {
836     Color col = JColorChooser.showDialog(this,
837             MessageManager.getString("label.select_subtree_colour"),
838             highlightNode.color);
839     if (col != null)
840     {
841       setColor(highlightNode, col);
842       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
843       repaint();
844     }
845   }
846
847   @Override
848   public void mouseMoved(MouseEvent evt)
849   {
850     av.setCurrentTree(tree);
851
852     Object ob = findElement(evt.getX(), evt.getY());
853
854     if (ob instanceof SequenceNode)
855     {
856       highlightNode = (SequenceNode) ob;
857       this.setToolTipText("<html>"
858               + MessageManager.getString("label.highlightnode"));
859       repaint();
860
861     }
862     else
863     {
864       if (highlightNode != null)
865       {
866         highlightNode = null;
867         setToolTipText(null);
868         repaint();
869       }
870     }
871   }
872
873   @Override
874   public void mouseDragged(MouseEvent ect)
875   {
876   }
877
878   /**
879    * Handles a mouse press on a sequence name or the tree background canvas
880    * (click on a node is handled in mouseClicked). The action is to create
881    * groups by partitioning the tree at the mouse position. Colours for the
882    * groups (and sequence names) are generated randomly.
883    * 
884    * @param e
885    */
886   @Override
887   public void mousePressed(MouseEvent e)
888   {
889     av.setCurrentTree(tree);
890
891     /*
892      * isPopupTrigger is set for mousePressed (Mac)
893      * or mouseReleased (Windows)
894      */
895     if (e.isPopupTrigger())
896     {
897       if (highlightNode != null)
898       {
899         chooseSubtreeColour();
900       }
901       return;
902     }
903
904     /*
905      * defer right-click handling on Windows to
906      * mouseClicked; note isRightMouseButton
907      * also matches Cmd-click on Mac which should do
908      * nothing here
909      */
910     if (SwingUtilities.isRightMouseButton(e))
911     {
912       return;
913     }
914
915     int x = e.getX();
916     int y = e.getY();
917
918     Object ob = findElement(x, y);
919
920     if (ob instanceof SequenceI)
921     {
922       treeSelectionChanged((Sequence) ob);
923       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
924       repaint();
925       av.sendSelection();
926       return;
927     }
928     else if (!(ob instanceof SequenceNode))
929     {
930       // Find threshold
931       if (tree.getMaxHeight() != 0)
932       {
933         threshold = (float) (x - offx)
934                 / (float) (getWidth() - labelLength - (2 * offx));
935
936         tree.getGroups().removeAllElements();
937         tree.groupNodes(tree.getTopNode(), threshold);
938         setColor(tree.getTopNode(), Color.black);
939
940         AlignmentPanel[] aps = getAssociatedPanels();
941
942         // TODO push calls below into a single AlignViewportI method?
943         // see also AlignViewController.deleteGroups
944         for (int a = 0; a < aps.length; a++)
945         {
946           aps[a].av.setSelectionGroup(null);
947           aps[a].av.getAlignment().deleteAllGroups();
948           aps[a].av.clearSequenceColours();
949           if (aps[a].av.getCodingComplement() != null)
950           {
951             aps[a].av.getCodingComplement().setSelectionGroup(null);
952             aps[a].av.getCodingComplement().getAlignment()
953                     .deleteAllGroups();
954             aps[a].av.getCodingComplement().clearSequenceColours();
955           }
956         }
957         colourGroups();
958       }
959
960       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
961       repaint();
962     }
963
964   }
965
966   void colourGroups()
967   {
968     AlignmentPanel[] aps = getAssociatedPanels();
969     for (int i = 0; i < tree.getGroups().size(); i++)
970     {
971       Color col = new Color((int) (Math.random() * 255),
972               (int) (Math.random() * 255), (int) (Math.random() * 255));
973       setColor(tree.getGroups().elementAt(i), col.brighter());
974
975       Vector<SequenceNode> l = tree.findLeaves(tree
976               .getGroups().elementAt(i));
977
978       Vector<SequenceI> sequences = new Vector<SequenceI>();
979
980       for (int j = 0; j < l.size(); j++)
981       {
982         SequenceI s1 = (SequenceI) l.elementAt(j)
983                 .element();
984
985         if (!sequences.contains(s1))
986         {
987           sequences.addElement(s1);
988         }
989       }
990
991       ColourSchemeI cs = null;
992       SequenceGroup sg = new SequenceGroup(sequences, null, cs, true, true,
993               false, 0, av.getAlignment().getWidth() - 1);
994
995       if (av.getGlobalColourScheme() != null)
996       {
997         if (av.getGlobalColourScheme() instanceof UserColourScheme)
998         {
999           cs = new UserColourScheme(
1000                   ((UserColourScheme) av.getGlobalColourScheme())
1001                           .getColours());
1002
1003         }
1004         else
1005         {
1006           cs = ColourSchemeProperty.getColour(sg, ColourSchemeProperty
1007                   .getColourName(av.getGlobalColourScheme()));
1008         }
1009         // cs is null if shading is an annotationColourGradient
1010         if (cs != null)
1011         {
1012           cs.setThreshold(av.getGlobalColourScheme().getThreshold(),
1013                   av.isIgnoreGapsConsensus());
1014         }
1015       }
1016       sg.cs = cs;
1017       // sg.recalcConservation();
1018       sg.setName("JTreeGroup:" + sg.hashCode());
1019       sg.setIdColour(col);
1020
1021       for (int a = 0; a < aps.length; a++)
1022       {
1023         if (aps[a].av.getGlobalColourScheme() != null
1024                 && aps[a].av.getGlobalColourScheme().conservationApplied())
1025         {
1026           Conservation c = new Conservation("Group",
1027                   ResidueProperties.propHash, 3, sg.getSequences(null),
1028                   sg.getStartRes(), sg.getEndRes());
1029
1030           c.calculate();
1031           c.verdict(false, aps[a].av.getConsPercGaps());
1032           sg.cs.setConservation(c);
1033         }
1034
1035         aps[a].av.getAlignment().addGroup(new SequenceGroup(sg));
1036         // TODO can we push all of the below into AlignViewportI?
1037         final AlignViewportI codingComplement = aps[a].av
1038                 .getCodingComplement();
1039         if (codingComplement != null)
1040         {
1041           SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(sg, av,
1042                   codingComplement);
1043           if (mappedGroup.getSequences().size() > 0)
1044           {
1045             codingComplement.getAlignment().addGroup(mappedGroup);
1046             for (SequenceI seq : mappedGroup.getSequences())
1047             {
1048               codingComplement.setSequenceColour(seq,
1049                       new Colour(col.brighter()));
1050             }
1051           }
1052         }
1053       }
1054     }
1055
1056     // notify the panel(s) to redo any group specific stuff.
1057     for (int a = 0; a < aps.length; a++)
1058     {
1059       aps[a].updateAnnotation();
1060       // TODO: JAL-868 - need to ensure view colour change message is broadcast
1061       // to any Jmols listening in
1062       final AlignViewportI codingComplement = aps[a].av
1063               .getCodingComplement();
1064       if (codingComplement != null)
1065       {
1066         ((AlignViewport) codingComplement).getAlignPanel()
1067                 .updateAnnotation();
1068       }
1069     }
1070   }
1071
1072   /**
1073    * DOCUMENT ME!
1074    * 
1075    * @param state
1076    *          DOCUMENT ME!
1077    */
1078   public void setShowDistances(boolean state)
1079   {
1080     this.showDistances = state;
1081     repaint();
1082   }
1083
1084   /**
1085    * DOCUMENT ME!
1086    * 
1087    * @param state
1088    *          DOCUMENT ME!
1089    */
1090   public void setShowBootstrap(boolean state)
1091   {
1092     this.showBootstrap = state;
1093     repaint();
1094   }
1095
1096   /**
1097    * DOCUMENT ME!
1098    * 
1099    * @param state
1100    *          DOCUMENT ME!
1101    */
1102   public void setMarkPlaceholders(boolean state)
1103   {
1104     this.markPlaceholders = state;
1105     repaint();
1106   }
1107
1108   AlignmentPanel[] getAssociatedPanels()
1109   {
1110     if (applyToAllViews)
1111     {
1112       return PaintRefresher.getAssociatedPanels(av.getSequenceSetId());
1113     }
1114     else
1115     {
1116       return new AlignmentPanel[] { ap };
1117     }
1118   }
1119 }