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